什么是方小代

产品定义和组成部分

方小代是一个「AI和RPA脚本工具链」,它包含「命令行/脚本」、「跨平台GUI(客户端)」、「脚本商店」。

RPA,即机器人流程自动化,是一种技术,它使用软件机器人(或“机器人”)来模拟人类在计算机上执行任务的方式。

方小代的目标是:

  1. 通过规范、SDK和AIGC(如方小代GPT)极大地降低开发者编写脚本的成本
  2. 通过GUI读取脚本的配置(位于package.json中),自动实现「参数设置」、「任务管理」、「结果输出」、「定时执行」、「消息通知」等通用功能,实现非技术向用户的轻松使用
  3. 通过「脚本商店」,提供常用脚本和预设,实现一键安装,解决包名和参数填写的门槛

命令行·脚本

Fxd的命令行或者叫脚本,被设计为完整而独立的应用,可以脱离GUI和脚本商店,直接通过npm安装运行。

方小代将通过GUI以Freeium的方式来商业化,因此命令行对个人和非商用场景是完全免费的。

由于命令行基于Node实现,因此能运行Node18+的环境,均可使用。为了更好的和其他语言、命令行工具交换数据,Fxd命令行支持 format 参数,可直接返回 json。你可以轻松地将它和其他工具比如 ffmpegmdbook甚至本地LLM进行混搭,从而极速实现需要的功能。

方小代命令行相关的数据,位于当前用户目录下的 .fxd 文件夹,你可以手动打包迁移

派生脚本

如果脚本某些方面不符合你需求,你可以直接 extends 它,然后改写少量内容快速实现自己的需求。

更多关于脚本开发的内容,可参见Fxd开发者手册

AI辅助开发

我们提供了一个方小代GPT,关于脚本开发,你可以向它提问,甚至让它帮助你编写内容。不过由于目前AI智力所限,生成的代码依然需要手工调试和修正。

使用GPTs需要ChatGPT Plus账号,你可以通过合租兔,以35RMB/月的价格合租

当然,你也可以通过其他支持文档问答的AI比如 Claude2 来辅助开发。

GUI

Fxd的GUI采用electron封装,支持Mac和Windows平台。GUI底层是通过命令行和Fxd脚本进行交互,同时通过API实现云服务和会员功能。

GUI 包含的主要功能为:

  1. 首页:脚本商店(云端)
  2. 动态:任务执行结果输出(云端)
  3. 任务:任务管理(导入、添加、编辑和删除)、定时和手动执行、包更新(云端)
  4. 数据表:类似excel的数据表格,可以批量编辑,表内数据可用于任务参数(本地)
  5. 设置:任务中可能用到的参数,可以用[FXD_DEFAULT::$KEY]的方式引用(本地)

数据表和设置都是本地的。好处是即使官方也无法获知这些数据;坏处是它们无法同步,更换机器后需要重新配置

脚本商店

可以通过首页的脚本商店实现对脚本的一键安装。针对不同的场景,商店还提供 预设参数,无需手工填入参数即可使用。

脚本商店在测试期只上架官方脚本;但GUI并未限制只能安装商店的脚本,只要发布到NPM的脚本,均可安装

预设链接

即使不上架商店,开发者也可以通过构造URL的方式来提供预置参数:

入口URL如下:http://localhost:55106/task?action=add Query参数如下:

  1. title: 任务名称
  2. package_name: npm包名
  3. method: 任务对应的方法,main是默认方法
  4. args_*: 任务参数,比如参数的name是url,则使用args_url

商店的路线图

未来商店将开放第三方脚本上架,暂定规则如下:

  1. Pro会员可以上架原创脚本,脚本需先发布到NPM
  2. 脚本审核通过后将上架商店
  3. 上架商店的脚本在安装时,将会自动扣除安装人点数(通过npm包名),点数将转给上架人
  4. 点数可用于购买Pro会员

用户数达到一定量后,商店会考虑开放点数兑换限量周边、京东购物卡等功能

为什么要使用方小代

  1. 开放性:方小代不同于其他的商业软件,它在设计上依托于npm,这注定了它只能保持开放
  2. 便利性:编写一个小脚本很简单,但为脚本开发GUI、让非技术人群用起来却很麻烦。现在只要遵循Fxd开发规范,Fxd的GUI即能为你所用
  3. 生态:随着Fxd用户的增多,将有更多遵循Fxd开发规范的脚本可以使用和派生

可重用性

Fxd 提供了 SDK 和 Cli 核心应用;同时实现了基于 Playwright 的浏览器自动化基础库,并在此基础上实现了网页监测账号保活。基于浏览器的自动化都可以直接派生。

Fxd 的SDK中,内置了AI(支持OpenAI、API2d和Azure)、WebSocket(可实时显示到GUI中)、KV和文件读写功能。整合AI更方便。

开放性

Fxd的包都将发布到 npm 上,可以通过命令行免费使用。

Pro会员是针对GUI和云端功能的,命令行个人和非商业场景均可免费使用

和GUI的无缝整合

通过在 package.json 中定义参数,可以直接在GUI中填入。

当然这些参数也可以在命令行的 help 中显示。

如何使用方小代

客户端使用教程

下载

链接:https://share.weiyun.com/a0DgGwCy 密码:fxdfxd

安装

安装 Node 和 NPM

由于方小代依赖 Npm 和 Node,因此您需要先在系统中安装 Node 和 NPM。它们可以通过 NodeJS.org 提供的安装软件一次性安装。

请点此下载安装

安装完成后,在Mac的Terminal(终端)应用中或者Windows的PowerShell应用中输入:

node -v

npm -v

应该可以看到对应的版本号:

$ node -v
v18.17.1
$ npm -v
9.6.7

请确保 node 的版本大于等于 18,否则可能出现报错。

安装 Playwright 依赖

因为Check酱应用使用了 Playwright 这个库,因此我们还需要安装 Playwright 的依赖。为了保证我们安装的版本和方小代内置的 Playwright 库一致,需要先通过 cd 命令进入方小代的web目录:

  1. 在 Mac 系统中,路径为: 存放方小代应用的目录/方小代.app/Contents/Resources/app.asar.unpacked/src/local-api
  2. 在 Win 系统中,路径为:存放方小代应用的目录/resources/app.asar.unpacked/src/local-api

再在终端中输入:

npx playwright install --with-deps chromium

这将安装我们用来自动化的浏览器。如果安装很慢,可以考虑使用 taobao 的镜像:

npx --registery=https://npm.taobao.com playwright install --with-deps chromium 

如果在 Windows 上提示错误: \Users\admin\AppData\Roaming\npm 不存在,可手工建立该目录后再次运行命令。或者尝试用管理员账号启动PowerShell再运行命令。

启动方小代

你可以通过双击方小代图标启动应用;也可以在终端中输入方小代应用的路径来启动。后一种方式会在终端输出日志,可以较为方便的定位问题。

  1. 在 Mac 系统中,启动路径为: 存放方小代应用的目录/Contents/MacOS/方小代
  2. 在 Win 系统中,启动路径为:存放方小代应用的目录/方小代.exe

登入

点击左下角的「登入」按钮,在浏览器打开的页面中用微信扫码。

在手机成功登入后,回到电脑点击确认二维码下方的按钮。

遇到「是否登入客户端」的提问时,选择「是」;遇到「浏览器正在打开方小代」的提问时,选择「是」。这时候客户端应该会自动登入。

对于一些低版本的操作系统,如果没有自动登入,请将扫码网页上的Token复制。

并填回客户端后点确认。

使用

目前我们提供了Check酱应用供大家测试,它的功能Check酱1.0(浏览器插件版)类似,但提供了运行自定义代码的功能,同时可以直接在命令行行使用。

安装应用

在首页点击 Check酱 应用一栏的「安装」按钮。

在填出的「任务属性」面板中,点击「继续」。你也可以在任务页面,点加号后手工填写应用包名。

「调用方法」选择「watch」,然后就可以填入参数了:

其中最主要的参数是要监测页面的「URL」和要监测元素的「Selector」。

你可以通过Chrome/Edge浏览器的DevTools工具复制元素的Selector;也可以通过Check酱浏览器插件的可视化选择器来选取。

点击「进阶设置」,可以打开更多的设置项,包括自定义代码、超时时间等。

为了方便Check酱1.0用户使用,我们提供了将1.0的数据链接直接导入为任务的功能。

首先在1.0的任务列表中复制任务的Data URI:

然后回到方小代,点击任务页面右上角的Check酱Logo图标。

将 Data URI 粘贴进去:

确认后,将进入预填好参数的任务添加页面:

保存即可。

由于方小代支持自定义JS和Playwright代码,因此导入未支持关于返回值比较等1.0进阶功能

使用应用

任务添加后将出现在任务列表,你可以通过按钮面板的按钮来讲操作:

注意:如果快捷键和定时任务添加后不生效,请重启应用

我们后续将上架更多应用供大家使用。

命令行使用教程

  1. 命令行使用教程

命令行使用指南

安装

环境要求

  1. node.js >= 18 (更低版本也许可以使用,但不在官方测试和支持范围)

安装 fxd-cli

  1. 首先使用终端进入你想要存放 fxd 代码的目录,没有可以创建一个,记为 FoldA
  2. npm install fxd-cli fxd-app-core 安装命令行和核心应用。
  3. sudo ln -s $PWD/node_modules/fxd-cli/index.js /usr/local/bin/fxd 创建快捷命令。

Windows 下建议使用 WSL 来运行

安装完成以后,即可以在命令行中使用fxd了。

$ fxd
Usage:
 fxd core [command] [options]

使用

安装了 fxd 后,就可以安装具体的应用了。

Check酱2

  1. 进入 FoldA
  2. npm install fxd-app-check-chan 安装Check酱2
  3. sudo npx playwright install-deps 安装无头浏览器框架 playwright 依赖
  4. fxd checkChan help 查看Check酱2帮助:
fxd checkChan help
Usage:
 fxd check-chan [command] [options]

command: main|check|watch:
 --url <string>	要打开的页面 URL (required)
 --headless <boolean>	是否使用无头模式 (default: true)
 --selectors <string>	要检测的元素的 CSS 选择器,多个用逗号分隔 (default: body) (required)
 --prejs <string>	页面加载完成后执行的自定义 JavaScript 代码
 --prejs_args <string>	自定义 JavaScript 代码的参数
 --preplay <string>	页面加载完成后执行的自定义 Playwright 代码
 --timeout <number>	 Playwright 操作超时时间,单位毫秒 (default: 60000)
 --list <boolean>	 选择器是否返回元素列表 (default: false)
 --user <string>	浏览器使用的用户目录 (default: default)
 --format <string>	返回的数据格式 (default: text)
 --wait_type <string>	等待元素出现的方式 (default: domcontentloaded)

command: watch:
 --sendkey <string>	填入后,检测到变化时会调用Server酱发消息
 --apprise_server_url <string>	apprise的Server URL;需安装 apprise 命令行以后才可使用
 --task_title <string>	显示在动态中,非命令行模式则无需填写
 --feed_publish <boolean>	是否发布为Feed,将显示在动态页面 (default: false)
 --feed_as_public <boolean>	Feed 是否公开 (default: true)

命令

应用可以包含多个命令,在help命令中会显示命令和对应的参数。在上边的例子中,check-chan 这个应用,包含了 maincheckwatch 三个命令, main 为入口命令,在不指定时命令是作为默认命令使用。

  1. command: main|check|watch 表示以下参数是 maincheckwatch 三个命令都支持的。
  2. command: watch: 表示以下参数是 watch 特有的。

check命令

check命令用于获取某个网页上某个选择器(selector,可以用 Check酱浏览器插件或者Chrome浏览器DevTools获得)的内容。

基本命令

使用实例,获取方糖气球首页第一篇文章的标题:

fxd checkChan --url="https://ftqq.com/" --selectors=".entry-title"

执行结果:

$ fxd checkChan --url="https://ftqq.com/" --selectors=".entry-title"
检测到的数据 [
  {
    "selector": ".entry-title",
    "html": "<a href=\"https://ftqq.com/notice/\">看这里看这里</a>",
    "text": "看这里看这里",
    "meta": {
      "html": [
        "<a href=\"https://ftqq.com/notice/\">看这里看这里</a>"
      ],
      "text": [
        "看这里看这里"
      ]
    }
  }
]

JSON输出

可以添加 --format=json 参数,保证输出是JSON:

fxd checkChan --url="https://ftqq.com/" --selectors=".entry-title" --format=json
{
    "merged_html": "\n\n<a href=\"https://ftqq.com/notice/\">看这里看这里</a>",
    "merged_text": "\n\n看这里看这里",
    "output": "\n\n看这里看这里",
    "data": [
        {
            "selector": ".entry-title",
            "html": "<a href=\"https://ftqq.com/notice/\">看这里看这里</a>",
            "text": "看这里看这里",
            "meta": {
                "html": [
                    "<a href=\"https://ftqq.com/notice/\">看这里看这里</a>"
                ],
                "text": [
                    "看这里看这里"
                ]
            }
        }
    ]
}

返回列表

默认情况下,只返回匹配选择器的第一个值,如果想要返回全部,可增加 --list=true 参数:

fxd checkChan --url="https://ftqq.com/" --selectors=".entry-title" --list=true
检测到的数据 [
  {
    "selector": ".entry-title",
    "html": "<a href=\"https://ftqq.com/notice/\">看这里看这里</a>\n<a href=\"https://ftqq.com/real-node-package/\">方糖迷因·Node Package</a>\n<a href=\"https://ftqq.com/telechan/\">Tele 酱:基于 Telegram 和 Vercel 的开源 Server 酱实现</a>\n<a href=\"https://ftqq.com/member-prism/\">开源项目:Member Prism</a>\n<a href=\"https://ftqq.com/docker2saas/\">开源项目:Docker2SaaS</a>\n<a href=\"https://ftqq.com/serverchan2/\">Server酱实战课</a>",
    "text": "看这里看这里\n方糖迷因·Node Package\nTele 酱:基于 Telegram 和 Vercel 的开源 Server 酱实现\n开源项目:Member Prism\n开源项目:Docker2SaaS\nServer酱实战课",
    "meta": {
      "html": [
        "<a href=\"https://ftqq.com/notice/\">看这里看这里</a>",
        "<a href=\"https://ftqq.com/real-node-package/\">方糖迷因·Node Package</a>",
        "<a href=\"https://ftqq.com/telechan/\">Tele 酱:基于 Telegram 和 Vercel 的开源 Server 酱实现</a>",
        "<a href=\"https://ftqq.com/member-prism/\">开源项目:Member Prism</a>",
        "<a href=\"https://ftqq.com/docker2saas/\">开源项目:Docker2SaaS</a>",
        "<a href=\"https://ftqq.com/serverchan2/\">Server酱实战课</a>"
      ],
      "text": [
        "看这里看这里",
        "方糖迷因·Node Package",
        "Tele 酱:基于 Telegram 和 Vercel 的开源 Server 酱实现",
        "开源项目:Member Prism",
        "开源项目:Docker2SaaS",
        "Server酱实战课"
      ]
    }
  }
]

可视化

添加 --headless=false 参数,可以将背后工作的浏览器显示出来,从而更直观地看到整个执行过程。

如果你是在远程服务器上执行,需要安装桌面环境和 VNC 才能看到界面,否则会提示 Looks like you launched a headed browser without having a XServer running.

base64参数传递

在通过命令行传递参数时,如果参数中包含众多换行和特殊字符,会非常容易出错。因此,所有的参数都可以用过base64的方式来传递。

假设参数为 a ,传递的值为 base64: 前缀 + base64(JSON.stringify(a)) 。

以下是 fxd 读取参数的源码,可以参考其逻辑

if( content.startsWith('base64:') )
{
    content = jsonDecode(Buffer.from(content.replace('base64:', ''), 'base64').toString());
}

watch 命令

check命令只负责获取数据,watch命令除了完整执行check的逻辑,还会比较每次获得的内容,并发送通知。

通过Server酱发送通知

 fxd checkChan watch --url="https://ftqq.com/" --selectors=".entry-title" --sendkey="SCT..."
检测到的数据 [
  {
    "selector": ".entry-title",
    "html": "<a href=\"https://ftqq.com/notice/\">看这里看这里</a>",
    "text": "看这里看这里",
    "meta": {
      "html": [
        "<a href=\"https://ftqq.com/notice/\">看这里看这里</a>"
      ],
      "text": [
        "看这里看这里"
      ]
    }
  }
]
不存在历史记录,将当前结果保存为历史记录

定时执行(比如把这个命令加入到 crontab)watch命令,这样在内容变动时,可以收到 Server酱推送:

fxd checkChan watch --url="https://ftqq.com/" --selectors=".entry-title" --sendkey="SCT..."
检测到的数据 [
  {
    "selector": ".entry-title",
    "html": "<a href=\"https://ftqq.com/notice/\">看这里看这里1</a>",
    "text": "看这里看这里1",
    "meta": {
      "html": [
        "<a href=\"https://ftqq.com/notice/\">看这里看这里1</a>"
      ],
      "text": [
        "看这里看这里1"
      ]
    }
  }
]
检测到变动  {
-  merged_html: "\n\n<a href=\"https://ftqq.com/notice/\">看这里看这里</a>"
+  merged_html: "\n\n<a href=\"https://ftqq.com/notice/\">看这里看这里1</a>"
-  merged_text: "\n\n看这里看这里"
+  merged_text: "\n\n看这里看这里1"
-  output: "\n\n看这里看这里"
+  output: "\n\n看这里看这里1"
   data: [
     {
-      html: "<a href=\"https://ftqq.com/notice/\">看这里看这里</a>"
+      html: "<a href=\"https://ftqq.com/notice/\">看这里看这里1</a>"
-      text: "看这里看这里"
+      text: "看这里看这里1"
       meta: {
         html: [
-          "<a href=\"https://ftqq.com/notice/\">看这里看这里</a>"
+          "<a href=\"https://ftqq.com/notice/\">看这里看这里1</a>"
         ]
         text: [
-          "看这里看这里"
+          "看这里看这里1"
         ]
       }
     }
   ]
 }

存在sendkey,发送结果到Server酱 {"code":0,"message":"","data":{"pushid":"1502...","readkey":"SCT...","error":"SUCCESS","errno":0}}

通过 Apprise 发送通知

除了使用 Server酱,你还可以通过 Apprise 将消息发送到上百个通道。

在传递参数前,你需要安装 Apprise 的命令行工具。

pip install apprise

如果运行环境没有 pip,你需要先安装它

安装完成后,注意按提示将 apprise 所在路径加入到 PATH 中:

export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/home/ubuntu/.local/bin"

然后通过 --apprise_server_url 参数传递 Server URL:

fxd checkChan watch --url="https://ftqq.com/" --selectors=".entry-title" --apprise_server_url="schan://SCT..."

上边的例子是用过 Apprise 调用Server酱通道;你可以随意更换为其他通道。可以参照它的文档来拼接 Server URL。

带状态页面和 KeepLive

原理

Check酱2应用的原理,是在背后启动一个浏览器,然后通过交互命令去读取浏览器中的页面信息。为了访问带状态的页面,我们需要先在对应的网站上进行登录。

这些操作我们可以通过 fxd-app-keep-live 应用来完成。

KeepLive

安装

  1. 进入 FoldA
  2. npm install fxd-app-keep-live 安装 KeepLive
  3. fxd keepLive help 查看应用帮助:
 fxd keepLive help
Usage:
 fxd keep-live [command] [options]

command: main|check:
 --check_url <string>	状态检测页面URL (required)
 --check_selector <string>	状态检测页面待检测元素的selector (default: body)
 --check_text <string>	状态检测页面待检测元素应该包含的文本
 --user <string>	浏览器使用的用户目录 (default: default)
 --timeout <number>	状态检测页面加载超时时间 (default: 5000)
 --headless <boolean>	是否使用无头模式 (default: true)
 --format <string>	返回的数据格式 (default: text)

command: auth:
 --auth_url <string>	登录页面URL (required)
 --user <string>	浏览器使用的用户目录 (default: default)

command: refresh:
 --refresh_url <string>	刷新页面URL (required)
 --user <string>	浏览器使用的用户目录 (default: default)
 --headless <boolean>	是否使用无头模式 (default: true)
 --timeout <number>	状态检测页面加载超时时间 (default: 5000)
 --format <string>	返回的数据格式 (default: text)

命令

KeepLive 包含以下命令:

  1. auth: 打开浏览器,等待用户登录后关闭
  2. refresh: 用浏览器刷新页面,以保持cookie/session活跃,可以把这个命令加入到crontab,定时执行
  3. check: 用浏览器监测某页面某元素的值,用来判断用户状态是否失效。可以把可以把这个命令加入到crontab,定时执行

auth 命令会打开浏览器,因此只能在配置了桌面的环境运行

打开浏览器:

fxd keepLive auth --auth_url="https://m.weibo.cn"

手动登录微博以后,关闭浏览器。这时候再运行 fxd checkChan --url="https://m.weibo.cn/..." ,获取到的页面就是已经登录状态的了。

  1. 注意登录后一定时间后会过期,需要重新按上述步骤登录
  2. 定时刷新页面,可以延迟过期时间,可以使用 refresh 命令+Crontab来实现
  3. 如果你希望保持多个不同用户的登录,可以将 --user 设置为不同的值。并在使用 fxd checkChan 时传入同样的参数。

Fxd应用指南

Check酱(fxd-app-check-chan)

命令行使用请见命令行使用指南,客户端使用教程稍后补充

状态保活(fxd-app-keep-live2)

命令行使用请见命令行使用指南,客户端使用教程稍后补充

Fxd应用开发指南

Fxd 是什么

Fxd 是一个「AI和RPA脚本工具链」。它具有以下特点:

  1. 采用继承方式实现重用。所有的应用都直接或间接地继承自核心应用FxdApp,这样一些基础能力无需编码即可重用。当特定的应用满足不了你的需求,你可以简单地继承并进行修改来实现更细分的需求。
  2. 采用 NPM 管理版本和依赖。所有的应用都是一个标准的 NPM Package ,采用语义化版本命名。既可以通过 Fxd-Cli 工具在命令行调用,也可以在其他程序中作为库使用。

授权

Fxd (不包含Fxd Dektop)采用 PolyForm-Noncommercial 协议,允许非商业使用

Fxd 的扩展应用不受上述协议限制,由应用作者自行决定授权。

举个例子:开发者A开发了 Fxd 的扩展应用 B,并将其以MIT协议发布。那么其他用户可以在 MIT 协议下分发不包含 Fxd-cli / Fxd-app-core 的 B。但如果将 Fxd-cli / Fxd-app-core 等应用打包到 B 中,作为一个整体分发,则必须遵守 PolyForm-Noncommercial 协议。

SDK、CLI、Core和官方APP

Fxd的架构遵守以下逻辑:

  1. 工具类和基础功能封装到 SDK 中,如Websocket通信、KV操作、AI调用函数
  2. Core APP提供命令行、云端通信等基础能力
  3. 所有APP从Core继承,亦支持命令行的能力
  4. Browser处理浏览器自动化相关通用功能
  5. CheckChan和KeepLive是官方应用,扩展浏览器自动化的能力

SDK 主要功能

Core 主要功能

Browser 主要功能

Fxd 基础应用列表

基础应用可以通过命令行调用,也可以在代码中 import 来使用

浏览器基础应用 Fxd-app-browser

方法:

  1. getUserDirFullPath(username) // 获得某个用户的浏览器数据存储目录
  2. async getBrowserAndMore( userDirFullPath, options ) // 创建 Playwright 绑定的 Chrome 浏览器对象。返回 { browser, page, context }

页面检测基础应用 Fxd-app-check-chan

方法:

  1. async check(args, opts, command) // 监测网页指定的元素(通过selectors参数指定,支持多个),并返回监测结果。

  2. async watch(args, opts, command) // 先调用check返回监测结果,并针对检测结果判断是否更新,发送通知和发布Feed。

命令行参数说明:

command: watch: --sendkey 检测到变化时发送消息的发送键 --feed_publish 是否发布为RSS Feed (default: false) --feed_as_public Feed 是否公开 (default: true)

command: main|check|watch: --url 要打开的页面 URL (required) --headless 是否使用无头模式 (default: true) --selectors 要检测的元素的 CSS 选择器,多个用逗号分隔 (default: body) --prejs 页面加载完成后执行的自定义 JavaScript 代码 --prejs_args 自定义 JavaScript 代码的参数 --preplay 页面加载完成后执行的自定义 Playwright 代码 --timeout Playwright 操作超时时间,单位毫秒 (default: 60000) --list 选择器是否返回元素列表 (default: false) --user 浏览器使用的用户目录 (default: default) --format 返回的数据格式 (default: text) --wait_type 等待元素出现的方式 (default: domcontentloaded)

实现代码:

import FxdBrowser from '/Users/easy/Code/gitcode/fxd/packages/fxd-app-browser/index.js';
import { FxdSdk, getDirname } from 'fxd-sdk';
import { readPackageSync } from 'read-pkg';
import { diffString, diff } from 'json-diff';

export default class FxdCheckChan extends FxdBrowser {
   constructor() {
       super();
       this.sdk = new FxdSdk(readPackageSync({ cwd: getDirname(import.meta.url) }));
   }

   async main(args, opts, command) {
       return await this.check(args, opts, command);
   }

   async check(args, opts, command) {
       this.setDeaultOpts(opts);
       this.setDeaultCommand(command);
       this.format = this.get('format');
       
       const ret = [];
       let ret_texts = '';
       let ret_htmls = '';
       // 是否使用 headless 模式
       const headless = this.get('headless');

       // 要检测的元素 CSS 选择器,多个选择器用逗号分隔
       const selectors = this.get('selectors') ? this.get('selectors').split(',') : ['body'];
       // process.exit(0);
       
       // URL
       const url = this.get('url');
       if (!url) this.echoError("url is required");

       const { browser, page, context } = await this.getBrowserAndMore(this.getUserDirFullPath(this.get('user')), { headless });

       page.setDefaultTimeout(this.get('timeout')); // 设置超时
       await page.goto(url);// 打开URL
       // console.log('goto', url);
       await page.waitForLoadState( this.get('wait_type')); // 确保页面加载完成
       // console.log('waitForLoadState', 'networkidle end');

       // 执行自定义 playwirght 代码
       const preplay = this.get('preplay');
       if (preplay) {
           // 定义一个异步函数,用于执行动态代码
           const asyncFn = new Function('page', 'context', `return (async () => {${preplay}})();`);

           // 执行异步函数
           await asyncFn(page, context);
       }

       // 执行自定义的 js 代码
       // @Todo 这个地方需要测试下,是否可以正常执行
       const prejs = this.get('prejs');
       const prejsArgs = this.get('prejs_args');
       if (prejs) {
           await page.evaluate(prejs, prejsArgs);
       }

       // 循环 selectors 数组,获取每个元素的 HTML、innerText
       for (const selector of selectors) {
           let elements = await page.locator(selector).all();
           
           if (!this.get('list')) elements = [elements[0]];
           let htmls = [], texts = [];
           // 循环每个元素,push HTML、innerText
           for (const element of elements) {
               htmls.push(await element.innerHTML());
               texts.push(await element.innerText());
           }

           const html = htmls.join('\n');
           const text = texts.join('\n');

           ret.push({ selector, html, text, meta: { html: htmls, text: texts } });

           if (html) ret_htmls += '\n\n' + html;
           if (text) ret_texts += '\n\n' + text;
       }

       // 关闭浏览器
       // await context.close();
       await browser.close();

       // 输出中间数据
       this.log('检测到的数据', JSON.stringify(ret, null, 2));

       return this.return({
           merged_html: ret_htmls,
           merged_text: ret_texts,
           data: [...ret],
       })
   }

   async watch(args, opts, command) {
       const ret = await this.check(args, opts, 'check');
       // 借用 check 命令的参数设置
       const url = this.get('url');

       // 切换为 watch 命令的设置
       this.setDeaultCommand(command);

       // 获取上一次的结果
       const urlSHA = this.sdk.sha1(url);
       const lastRet = await this.sdk.getValue(urlSHA);
       // 保存本次结果
       await this.sdk.setValue(urlSHA, ret);

       if (!lastRet) {
           this.log("不存在历史记录,将当前结果保存为历史记录");
       } else {
           // 比较两次结果
           if (diff(lastRet, ret)) {
               const differenceString = diffString(lastRet, ret);
               const differenceText = diffString(lastRet, ret, { color: false });
               this.log("检测到变动", differenceString);

               // 如果 opts.sendkey 存在,则发送到指定的 key
               const sendkey = this.get('sendkey');
               if (sendkey) {
                   const send_ret = await this.sdk.scSend(`${this.sdk.displayName} 有新的动态`, `\`\`\`diff
${differenceText}
\`\`\``,
                       sendkey);
                   this.log("存在sendkey,发送结果到Server酱", send_ret);
               }

               // 如果 opts.feed_publish 存在,则发布到 feed
               const feed_publish = this.get('feed_publish');
               if (feed_publish) {
                   const feed_ret = await this.feedPublish(ret.merged_text, ret , this.get('feed_as_public'), command);

                   this.log("发布结果到feed列表", feed_ret);
               }
               

               return this.return({
                   "message": "检测到变动",
                   "diff": differenceText,
                   "action": "changed",
                   "data": ret
               });
           } else {
               this.log("目标数据没有变化");
               return this.return({ "message": "目标数据没有变化", "action": "unchanged" });
           }
       }
   }
}

Cli入口和核心应用

Cli 入口

用户可以在终端输入以下命令启动核心应用(fxd-app-core)

fxd _login

或其他子应用(比如 fxd-app-check-chan...)

fxd checkChan --url="https://sct.ftqq.com" --selectors=".marktext table"

应用的载入逻辑可参考以下代码:

// 如果命令不存在,则默认为 core help
if( !command )
{
    command = 'core';
    params = ['help'];
}

// 如果命令以 _ 开头,则为内部命令,转向 core
if( command.startsWith('_') )
{
    // 内部命令
    params = [command.slice(1), ...params];
    command = 'core';
}

// 检查 fxd_app_${command} module 是否存在(ESM语法)
let module;
const formattedCommand = humps.decamelize(command, { separator: '-' });
const packageName = `fxd-app-${formattedCommand}`;
try {
    
    // 调试模式则加载本地文件
    const thePath = DEV ? path.resolve(__dirname, `../${packageName}/index.js`) : `${packageName}`;

    const app = await import(thePath);
    module = new app.default();
    // 如果存在 module.run 方法,则执行
    if (module && module.run) {
        await module.run(params, opts);
    }else
    {
        console.log("no module.run", module);
    }
}    
catch(e)
{
   //...
}

SDK

Fxd SDK Package

fxd-sdk npm package 包含了 FxdSdk 类 和一些辅助函数:

Fxd SDK 类

  1. 构造函数
  • 用途:SDK 的主类

  • 构造函数参数:

    • packageInfo:对象类型,包信息,可选
      constructor(packageInfo = null) {
          if (!packageInfo) packageInfo = readPackageSync();
          this.apiBase = API_URL;
          this.token = null;
          this.loadToken();
          this.name = packageInfo.name || this.constructor.name;
          this.displayName = packageInfo.displayName || this.name ;
          this.args = packageInfo.meta?.args || [];
          const dbFolder = path.resolve(process.env.HOME, '.fxd', 'db');
          const dbFile = path.resolve(dbFolder, `${this.name}.json`);
          const dbConfig = new Config(dbFile, true, true);
          this.db = new JsonDB(dbConfig);
      }
    
  1. setValue 方法
  • 用途:设置数据
  • 参数:
    • key:字符串,键名,必填
    • value:任意类型,值,必填
    • overwrite:布尔类型,是否覆盖,可选,默认为 true
  1. getValue 方法
  • 用途:获取数据
  • 参数:
    • key:字符串,键名,必填
  1. setToken 方法
  • 用途:设置 token
  • 参数:
    • token:字符串,token 值,必填
  1. saveToken 方法
  • 用途:保存 token 到文件
  • 参数:
    • token:字符串,token 值,必填
  1. loadToken 方法
  • 用途:从文件加载 token
  • 参数:无
  1. cleanToken 方法
  • 用途:清除保存的 token
  • 参数:无
  1. _request 方法
  • 用途:发起请求
  • 参数:
    • method:字符串,请求方法,必填
    • path:字符串,路径,必填
    • data:对象,数据,可选
  1. profile 方法
  • 用途:获取 profile
  • 参数:无
  1. sha1 方法
  • 用途:计算 sha1 值
  • 参数:
    • string:字符串,输入内容,必填
  1. scSend 方法
  • 用途:发送Server酱消息
  • 参数:
    • text:字符串,文本内容,必填
    • desp:字符串,消息内容,可选
    • key:字符串,Server酱的key,可选

辅助函数

  1. getDirname 函数
  • 用途:获取目录名
  • 参数:
    • filePath:字符串,文件路径,必填
  1. getPackageInfo 函数
  • 用途:获取 package.json 信息
  • 参数:
    • filePath:字符串,文件路径,必填
  1. myFetch 函数
  • 用途:扩展 fetch,添加 try catch 和超时
  • 参数:
    • url:字符串,请求地址,必填
    • options:对象,选项,必填
    • timeout:数字,超时时间,可选,默认 5 秒

SDK 的方法在所有的 Fxd APP 中都可以通过 this.sdk.method(args) 的方式来调用。

核心应用 fxd-app-core

fxd-app-core 使用 fxd-sdk 完成一些辅助功能,同时提供 run 方法作为入口。也包含了一些命令行和云端需要使用的内部命令,比如 list(显示本地保存的命令列表,以避免重复输入),login(登入到云端,以使用云端能力)。所有内部命令在命令行下使用时,均以 _ 开头,如 fxd _login

初始化

constructor() 
{
    this.sdk = new fxdsdk(getpackageinfo(import.meta.url)); 
    // 参数获取方法 this.get依赖于this.sdk.args,因此只要使用 this.get 的程序都需要在构造函数中加入上边这行
    // ...
}

fxd-app-core 的其他方法和函数

FxdApp 的方法

  1. run 方法
  • 用途:处理命令行参数,调用对应命令的处理函数
  • 参数:
    • params:数组,命令行参数
    • opts:对象,选项参数
    • raw:布尔值,是否原始模式
  • 返回值:无
  1. list 方法
  • 用途:列出保存的命令
  • 参数:无
  • 返回值:布尔值,表示是否成功
  1. exe 方法
  • 用途:执行保存的某条命令
  • 参数:
    • index:数字,命令索引值
  • 返回值:无
  1. exec 方法
  • 用途:执行保存的某条命令
  • 参数:
    • index:数字,命令索引值
  • 返回值:无
  1. login方法
  • 用途:登录
  • 参数:
    • args:数组,参数
    • opts:对象,选项参数
      • token:字符串,登录 token
  • 返回值:无
  1. logout方法
  • 用途:退出登录
  • 参数:无
  • 返回值:无
  1. profile方法
  • 用途:获取用户信息
  • 参数:无
  • 返回值:用户信息对象
  1. echoError方法
  • 用途:显示错误信息
  • 参数:
    • msg:字符串,错误信息
  • 返回值:无
  1. help方法
  • 用途:显示帮助信息
  • 参数:无
  • 返回值:无
  1. setDefaultOpts方法
  • 用途:设置默认选项参数
  • 参数:
    • opts:对象,要设置的默认选项参数
  • 返回值:无
  1. setDefaultCommand方法
  • 用途:设置默认命令
  • 参数:
    • command:字符串,要设置的默认命令
  • 返回值:无
  1. get方法
  • 用途:获取参数值
  • 参数:
    • key:字符串,参数名
    • opts:对象,选项参数
    • command:字符串,命令名
  • 返回值:参数值
  1. log方法
  • 用途:打印日志
  • 参数:任意个要打印的日志内容
  • 返回值:无
  1. return方法
  • 用途:统一输出最终结果
  • 参数:
    • ret:任意类型,要输出的结果
  • 返回值:传入的结果
  1. feedPublish方法
  • 用途:发布feed
  • 参数:
    • content:字符串,feed内容
    • meta:对象,feed元信息
    • is_public:布尔值,是否公开
    • command:字符串,命令名
  • 返回值:布尔值,表示是否成功
  1. feedRemove方法
  • 用途:删除已发布的feed
  • 参数:
    • id:字符串,feed的id
  • 返回值:布尔值,表示是否成功

辅助函数

  1. mergeProps函数
  • 用途:合并属性
  • 参数:
    • oldObject:对象,原有对象
    • props:字符串,属性定义
  • 返回值:合并后的新对象

mergeProps函数主要用于派生类选择性的继承父类写在 Package.json 中的参数定义等信息。

比如, fxd-app-check-chanPackage.json 中定义了参数:

{
  "name": "fxd-app-check-chan",
  //...
  },
  "meta":
  {
    "args":
    {
      "watch":
      {
        "sendkey": {
          "name": "sendkey",
          "description": "检测到变化时发送消息的发送键",
          "type": "string",
          "example": "abcdefg1234"  
        },
        "feed_publish":
        {
          "name": "feed_publish",
          "description": "是否发布为RSS Feed",
          "type": "boolean",
          "default": false,
          "example": "true"
        }
      },
      "main|check":
      {
        "url": {
          "name": "url",
          "description": "要打开的页面 URL",
          "type": "string",
          "required": true,
          "example": "https://www.example.com" 
        }
        // ... 
      }
    }
}

fxd-app-douyin-count 继承于它,但不想重复在 Package.json 中书写参数定义,那么可以在构造函数中如下处理:

constructor() {
    super();
    const oldArgsSettings = this.sdk.args; // 获得父类的参数定义
    this.sdk = new FxdSdk(getPackageInfo(import.meta.url));
    // 使用 mergeProps 用父类定义中选择性复制部分
    this.sdk.args = this.mergeProps(oldArgsSettings,[
        'watch',// watch 下的全部属性
        'main|check.-selectors,prejs' // 排除 main|check 属性下的 selectors,prejs 属性
    ]);
}

如何扩展现有应用

扩展现有应用

下边我们举一个具体例子来讲解如何扩展现有应用,来加入新功能或者简化使用。

fxd-app-check-chan 是一个监测 url 内容变化并发送通知的应用,它接受如下参数:

 --url <string> 要打开的页面 URL (required)
 --headless <boolean>   是否使用无头模式 (default: true)
 --selectors <string>   要检测的元素的 CSS 选择器,多个用逗号分隔 (default: body)
 --prejs <string>       页面加载完成后执行的自定义 JavaScript 代码
 --prejs_args <string>  自定义 JavaScript 代码的参数
 --preplay <string>     页面加载完成后执行的自定义 Playwright 代码
 --timeout <number>      Playwright 操作超时时间,单位毫秒 (default: 60000)
 --list <boolean>        选择器是否返回元素列表 (default: false)
 --user <string>        浏览器使用的用户目录 (default: default)
 --format <string>      返回的数据格式 (default: text)
 --sendkey <string>      Server酱sendkey,设置后有变动时会发微信通知
 --feed_publish <boolean>   有变动时是否发布Feed
 --feed_as_public <boolean>   发布Feed时,是否在RSS中可见
 --wait_type <string>   等待元素出现的方式 (default: domcontentloaded)

通过指定 selectors 参数,理论上我们可以监测所有网页。但 selectors 过于技术化,并不是所有用户都会使用。因此,我们针对只需要监测抖音账号是否有作品更新用户,提供一个定制版的应用 fxd-app-douyin-count

这个应用只接受一个参数:

--url <string> 抖音用户主页 URL (required)

selectors 我们会直接写死在应用中,这样用户不用了解这个非常技术化的概念。

fxd-app-douyin-count 的代码如下:

import FxdCheckChan from 'fxd-app-check-chan';
import { FxdSdk, getPackageInfo } from 'fxd-sdk';

export default class FxdDouyinCoutn extends FxdCheckChan {
    constructor() {
        super();
        const oldArgsSettings = this.sdk.args;
        this.sdk = new FxdSdk(getPackageInfo(import.meta.url));
        this.sdk.args = this.mergeProps(oldArgsSettings,[
            'watch',
            'main|check|watch.-selectors,prejs,prejs_args,preplay,list'
        ]);
    }

    async main(args, opts, command) {
        opts['selectors'] = `[data-e2e='user-tab-count']`;
        return await this.watch(args, opts, 'watch');    
    }
}

这样 fxd-app-douyin-count 应用就已经完成了。使用方式:

fxd douyinCount --url="https://www.douyin.com/user/MS4wLjABAAAAHwf1DAfgUg4cxizx9nLC1JozAR1P-jGOhagrX9pgLz8" --format="json"

额外说明

  1. Fxd系列应用一般不是常驻后台,而是由用户将其添加到Cron中执行,因此即使是定时监测网页的任务,也只需要处理一次的情况。可以使用SDK中的命令来存储数据以供下次使用。

如何在应用内调用现有应用

应用内调用其他应用

有时候我们需要开发的功能,另外一个Fxd App已经开发完成了。这种时候完全没有必要重复开发,直接把它用起来就好了。

假设我们正在开发一个定时检测RSS并发布将最新的一个文章发布到微博的应用。当我们开发完成RSS监测功能后,就需要发布微博。

而发布微博这个功能 fxd-app-weibo-publish 已经做过了。所以我们可以直接使用它。具体方式如下:

import FxdWeiboPublish from 'fxd-app-weibo-publish'; // 首先 import进来

async main(args, opts, command) {
    this.setDeaultOpts(opts);
    this.setDeaultCommand(command);
    this.format = this.get('format');
    // ...
    // 这里要发布微博了
    const weibo_publish = new FxdWeiboPublish();// 创建对象
    result = await weibo_publish.publish( // 调用 publish 方法
        null, // 第一个参数留空,第二个参数参入调用参数 
        { 
            content: 微博内容,
            headless: 'false',
            user: this.get('user'),
        }, 
        'publish' // 第三个参数固定为被调用的方法名,也就是 publish
    );
}

那么如何知道 fxd-app-weibo-publish 有哪些方法和参数可以用呢?输入

npm view fxd-app-weibo-publish meta

可以看到相关设置:

{
  args: {
    'main|publish': {
      content: {
        name: 'content',
        cn_name: '微博正文',
        type: 'string',
        required: true,
        description: '微博内容'
      },
      headless: {
        name: 'headless',
        cn_name: '后台模式',
        description: '是否使用后台模式',
        type: 'boolean',
        default: true,
        example: 'true'
      },
      user: {
        name: 'user',
        description: '浏览器使用的用户目录',
        type: 'string',
        default: 'default',
        example: 'admin'
      },
      format: {
        name: 'format',
        description: '返回的数据格式',
        type: 'string',
        default: 'text',
        example: 'json',
        enum: [ 'json', 'text' ]
      },
      timeout: {
        name: 'timeout',
        description: ' Playwright 操作超时时间,单位毫秒',
        type: 'number',
        default: 60000,
        example: 30000
      },
      wait_type: {
        name: 'wait_type',
        description: '等待元素出现的方式',
        type: 'string',
        default: 'domcontentloaded',
        example: 'networkidle',
        enum: [ 'domcontentloaded', 'load', 'networkidle' ]
      },
      images: {
        name: 'images',
        cn_name: '微博配图URL',
        description: '图片地址,用逗号分隔',
        type: 'string',
        default: '',
        example: 'https://www.baidu.com/1.jpg,https://www.baidu.com/2.jpg'
      },
      self_only: {
        name: 'self_only',
        cn_name: '仅自己可见',
        description: '是否仅对自己可见',
        type: 'boolean',
        default: false,
        example: 'true'
      }
    }
  }
}

FxdApp的元信息和参数定义

元信息和参数定义

通过在 npm 的 package.json 中添加 meta 信息,我们可以指定脚本的作者、以及每一个方法支持的参数。

"meta": {
    "for": "fxd",
    "author_uid": 1,
    "args": {
      "main|check|watch": {
        "url": {
          "name": "url",
          "description": "要打开的页面 URL",
          "type": "string",
          "required": true,
          "example": "https://www.example.com"
        },
      },
      "watch": {
        "sendkey": {
          "name": "sendkey",
          "description": "填入后,检测到变化时会调用Server酱发消息",
          "type": "string",
          "example": "abcdefg1234"
        }
      }
    }
  }

指定参数的作用

通过在 package.json 中定义参数,可以直接在GUI中填入。

当然这些参数也可以在命令行的 help 中显示。

支持的属性如下:

  1. for: 固定为"fxd",用于标识这是一个fxd app
  2. author_uid: 用于指定 app 作者,只有 app 作者才有权限上架 fxd 商店
  3. args: 参数信息。
  4. 第一级为方法,如 watchcheck;多个方法同时支持的参数可以放到一起,方法名用竖线隔开,如 main|check|watch
  5. 第二级为字段说明,字段说明包含以下属性: 1. name: 英文字段名 1. cn_name: 显示在客户端界面中的中文字段名 1. description: 对字段的说明,通常会作为 placeholder 显示 1. default: 默认值 1. required: 是否必填 1. example: 字段值示例 1. advanced: 是否为进阶参数,如是,则只在「进阶设置」按钮按下时显示 1. type: 字段类型,支持 stringbooleannumber 1. ui: 字段UI类型,UI默认会根据type自动适配,但可以强制指定。如 typestring 时默认为文本输入框,但可以指定 UI 为 textarea,这样会将其变为长文本编辑区域。支持 textinput,shortcutinput,textarea,numberinput,dateinput,select,multiselect,jsoninput,passwordinput,switch,cron
  6. enum: 枚举值,限制输入值为枚举数组的内容,如 ["json","text"]。设置枚举值通常会将UI转化为 select

从头开始开发Fxd应用

一般情况下,我们推荐直接继承现有应用,当然如果没有合适的应用,则可以按以下步骤编写新应用。

创建目录

fxd-app- 开头,用 - 分隔单词,比如 fxd-app-demo

package.json

运行 yarn init -y,初始化一个 package.json。补充 meta 字段,用于描述作者和参数信息

{
  "name": "fxd-app-demo",
  "version": "1.0.0",
  "main": "index.js",
  "license": "PolyForm-Noncommercial",
  "type": "module",
  "meta":
  {
    "for":"fxd",
    "author_uid":0,
    "args": 
    {
      "main":{
        "url": {
        "name": "url",
        "description": "要打开的页面 URL",
        "type": "string",
        "required": false,
        "default": "https://f.ftqq.com",
        "example": "https://www.example.com" 
        }
      }
    }
  }
}

index.js

创建 index.js,填入以下代码:

import FxdApp from 'fxd-app-core';
import { FxdSdk, getPackageInfo } from 'fxd-sdk';

export default class FxdDemo extends FxdApp {
    constructor() {
        super();
        this.sdk = new FxdSdk(getPackageInfo(import.meta.url));
        // ...
    }

    async main(args, opts, command) {
        this.setDeaultOpts(opts);
        this.setDeaultCommand(command);
        // 以上两行设置后,this.get可以不传入第二和第三个参数

        console.log(this.sdk.name, command, args, opts, this.get('url', opts));
    }
}

测试应用

通过终端进入安装方小代可执行文件所在的目录:

  1. 在 Mac 系统中,启动路径为: 存放方小代应用的目录/Contents/MacOS/方小代
  2. 在 Win 系统中,启动路径为:存放方小代应用的目录/方小代.exe

运行 npm install DemoAPP的绝对路径,将其添加到 node_modules 目录。

然后就可以运行这个应用了。

  1. 命令行:在当前目录输入 node node_moduels/fxd-cli/index.js Demo 即可。注意这里的应用名是包名去掉 fxd-app- 后,按小驼峰书写
  2. 客户端:按包名添加应用 fxd-app-demo

发布应用

当测试完成后,可以通过 npm publish命令发布到官方。当然,你需要注册并登入 npmjs.com 。具体的教程请自行查找。

删除本地测试应用

打开方小代可执行文件所在的目录的 package.json 删除其中关于 fxd-app-demo 的依赖并保存后,重新 npm install 即可。