什么是方小代
产品定义和组成部分
方小代是一个「AI和RPA脚本工具链」,它包含「命令行/脚本」、「跨平台GUI(客户端)」、「脚本商店」。
RPA,即机器人流程自动化,是一种技术,它使用软件机器人(或“机器人”)来模拟人类在计算机上执行任务的方式。
方小代的目标是:
- 通过规范、SDK和AIGC(如方小代GPT)极大地降低开发者编写脚本的成本
- 通过GUI读取脚本的配置(位于package.json中),自动实现「参数设置」、「任务管理」、「结果输出」、「定时执行」、「消息通知」等通用功能,实现非技术向用户的轻松使用
- 通过「脚本商店」,提供常用脚本和预设,实现一键安装,解决包名和参数填写的门槛
命令行·脚本
Fxd的命令行或者叫脚本,被设计为完整而独立的应用,可以脱离GUI和脚本商店,直接通过npm安装运行。
方小代将通过GUI以Freeium的方式来商业化,因此命令行对个人和非商用场景是完全免费的。
由于命令行基于Node实现,因此能运行Node18+的环境,均可使用。为了更好的和其他语言、命令行工具交换数据,Fxd命令行支持 format
参数,可直接返回 json
。你可以轻松地将它和其他工具比如 ffmpeg
、mdbook
甚至本地LLM进行混搭,从而极速实现需要的功能。
方小代命令行相关的数据,位于当前用户目录下的
.fxd
文件夹,你可以手动打包迁移
派生脚本
如果脚本某些方面不符合你需求,你可以直接 extends 它,然后改写少量内容快速实现自己的需求。
更多关于脚本开发的内容,可参见Fxd开发者手册
AI辅助开发
我们提供了一个方小代GPT,关于脚本开发,你可以向它提问,甚至让它帮助你编写内容。不过由于目前AI智力所限,生成的代码依然需要手工调试和修正。
使用GPTs需要ChatGPT Plus账号,你可以通过合租兔,以35RMB/月的价格合租
当然,你也可以通过其他支持文档问答的AI比如 Claude2 来辅助开发。
GUI
Fxd的GUI采用electron封装,支持Mac和Windows平台。GUI底层是通过命令行和Fxd脚本进行交互,同时通过API实现云服务和会员功能。
GUI 包含的主要功能为:
- 首页:脚本商店(云端)
- 动态:任务执行结果输出(云端)
- 任务:任务管理(导入、添加、编辑和删除)、定时和手动执行、包更新(云端)
- 数据表:类似excel的数据表格,可以批量编辑,表内数据可用于任务参数(本地)
- 设置:任务中可能用到的参数,可以用
[FXD_DEFAULT::$KEY]
的方式引用(本地)
数据表和设置都是本地的。好处是即使官方也无法获知这些数据;坏处是它们无法同步,更换机器后需要重新配置
脚本商店
可以通过首页的脚本商店实现对脚本的一键安装。针对不同的场景,商店还提供 预设参数
,无需手工填入参数即可使用。
脚本商店在测试期只上架官方脚本;但GUI并未限制只能安装商店的脚本,只要发布到NPM的脚本,均可安装
预设链接
即使不上架商店,开发者也可以通过构造URL的方式来提供预置参数:
入口URL如下:http://localhost:55106/task?action=add
Query参数如下:
- title: 任务名称
- package_name: npm包名
- method: 任务对应的方法,main是默认方法
- args_*: 任务参数,比如参数的name是url,则使用args_url
商店的路线图
未来商店将开放第三方脚本上架,暂定规则如下:
- Pro会员可以上架原创脚本,脚本需先发布到NPM
- 脚本审核通过后将上架商店
- 上架商店的脚本在安装时,将会自动扣除安装人点数(通过npm包名),点数将转给上架人
- 点数可用于购买Pro会员
用户数达到一定量后,商店会考虑开放点数兑换限量周边、京东购物卡等功能
为什么要使用方小代
- 开放性:方小代不同于其他的商业软件,它在设计上依托于npm,这注定了它只能保持开放
- 便利性:编写一个小脚本很简单,但为脚本开发GUI、让非技术人群用起来却很麻烦。现在只要遵循Fxd开发规范,Fxd的GUI即能为你所用
- 生态:随着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目录:
- 在 Mac 系统中,路径为:
存放方小代应用的目录/方小代.app/Contents/Resources/app.asar.unpacked/src/local-api
- 在 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再运行命令。
启动方小代
你可以通过双击方小代图标启动应用;也可以在终端中输入方小代应用的路径来启动。后一种方式会在终端输出日志,可以较为方便的定位问题。
- 在 Mac 系统中,启动路径为:
存放方小代应用的目录/Contents/MacOS/方小代
- 在 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进阶功能
使用应用
任务添加后将出现在任务列表,你可以通过按钮面板的按钮来讲操作:
注意:如果快捷键和定时任务添加后不生效,请重启应用
我们后续将上架更多应用供大家使用。
命令行使用教程
命令行使用指南
安装
环境要求
- node.js >= 18 (更低版本也许可以使用,但不在官方测试和支持范围)
安装 fxd-cli
- 首先使用终端进入你想要存放 fxd 代码的目录,没有可以创建一个,记为
FoldA
。 npm install fxd-cli fxd-app-core
安装命令行和核心应用。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
- 进入
FoldA
npm install fxd-app-check-chan
安装Check酱2sudo npx playwright install-deps
安装无头浏览器框架 playwright 依赖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
这个应用,包含了 main
、check
和 watch
三个命令, main
为入口命令,在不指定时命令是作为默认命令使用。
command: main|check|watch
表示以下参数是main
、check
和watch
三个命令都支持的。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
安装
- 进入
FoldA
npm install fxd-app-keep-live
安装 KeepLivefxd 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
包含以下命令:
auth
: 打开浏览器,等待用户登录后关闭refresh
: 用浏览器刷新页面,以保持cookie/session活跃,可以把这个命令加入到crontab,定时执行check
: 用浏览器监测某页面某元素的值,用来判断用户状态是否失效。可以把可以把这个命令加入到crontab,定时执行
auth
命令会打开浏览器,因此只能在配置了桌面的环境运行
打开浏览器:
fxd keepLive auth --auth_url="https://m.weibo.cn"
手动登录微博以后,关闭浏览器。这时候再运行 fxd checkChan --url="https://m.weibo.cn/..."
,获取到的页面就是已经登录状态的了。
- 注意登录后一定时间后会过期,需要重新按上述步骤登录
- 定时刷新页面,可以延迟过期时间,可以使用
refresh
命令+Crontab来实现 - 如果你希望保持多个不同用户的登录,可以将
--user
设置为不同的值。并在使用fxd checkChan
时传入同样的参数。
Fxd应用指南
Check酱(fxd-app-check-chan)
命令行使用请见命令行使用指南,客户端使用教程稍后补充
状态保活(fxd-app-keep-live2)
命令行使用请见命令行使用指南,客户端使用教程稍后补充
Fxd应用开发指南
Fxd 是什么
Fxd 是一个「AI和RPA脚本工具链」。它具有以下特点:
- 采用继承方式实现重用。所有的应用都直接或间接地继承自核心应用FxdApp,这样一些基础能力无需编码即可重用。当特定的应用满足不了你的需求,你可以简单地继承并进行修改来实现更细分的需求。
- 采用 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的架构遵守以下逻辑:
- 工具类和基础功能封装到 SDK 中,如Websocket通信、KV操作、AI调用函数
- Core APP提供命令行、云端通信等基础能力
- 所有APP从Core继承,亦支持命令行的能力
- Browser处理浏览器自动化相关通用功能
- CheckChan和KeepLive是官方应用,扩展浏览器自动化的能力
SDK 主要功能
Core 主要功能
Browser 主要功能
Fxd 基础应用列表
基础应用可以通过命令行调用,也可以在代码中 import 来使用
浏览器基础应用 Fxd-app-browser
方法:
- getUserDirFullPath(username) // 获得某个用户的浏览器数据存储目录
- async getBrowserAndMore( userDirFullPath, options ) // 创建 Playwright 绑定的 Chrome 浏览器对象。返回 { browser, page, context }
页面检测基础应用 Fxd-app-check-chan
方法:
-
async check(args, opts, command) // 监测网页指定的元素(通过selectors参数指定,支持多个),并返回监测结果。
-
async watch(args, opts, command) // 先调用check返回监测结果,并针对检测结果判断是否更新,发送通知和发布Feed。
命令行参数说明:
command: watch:
--sendkey
command: main|check|watch:
--url
实现代码:
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 类
- 构造函数
-
用途: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); }
- setValue 方法
- 用途:设置数据
- 参数:
- key:字符串,键名,必填
- value:任意类型,值,必填
- overwrite:布尔类型,是否覆盖,可选,默认为 true
- getValue 方法
- 用途:获取数据
- 参数:
- key:字符串,键名,必填
- setToken 方法
- 用途:设置 token
- 参数:
- token:字符串,token 值,必填
- saveToken 方法
- 用途:保存 token 到文件
- 参数:
- token:字符串,token 值,必填
- loadToken 方法
- 用途:从文件加载 token
- 参数:无
- cleanToken 方法
- 用途:清除保存的 token
- 参数:无
- _request 方法
- 用途:发起请求
- 参数:
- method:字符串,请求方法,必填
- path:字符串,路径,必填
- data:对象,数据,可选
- profile 方法
- 用途:获取 profile
- 参数:无
- sha1 方法
- 用途:计算 sha1 值
- 参数:
- string:字符串,输入内容,必填
- scSend 方法
- 用途:发送Server酱消息
- 参数:
- text:字符串,文本内容,必填
- desp:字符串,消息内容,可选
- key:字符串,Server酱的key,可选
辅助函数
- getDirname 函数
- 用途:获取目录名
- 参数:
- filePath:字符串,文件路径,必填
- getPackageInfo 函数
- 用途:获取 package.json 信息
- 参数:
- filePath:字符串,文件路径,必填
- 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 的方法
- run 方法
- 用途:处理命令行参数,调用对应命令的处理函数
- 参数:
- params:数组,命令行参数
- opts:对象,选项参数
- raw:布尔值,是否原始模式
- 返回值:无
- list 方法
- 用途:列出保存的命令
- 参数:无
- 返回值:布尔值,表示是否成功
- exe 方法
- 用途:执行保存的某条命令
- 参数:
- index:数字,命令索引值
- 返回值:无
- exec 方法
- 用途:执行保存的某条命令
- 参数:
- index:数字,命令索引值
- 返回值:无
- login方法
- 用途:登录
- 参数:
- args:数组,参数
- opts:对象,选项参数
- token:字符串,登录 token
- 返回值:无
- logout方法
- 用途:退出登录
- 参数:无
- 返回值:无
- profile方法
- 用途:获取用户信息
- 参数:无
- 返回值:用户信息对象
- echoError方法
- 用途:显示错误信息
- 参数:
- msg:字符串,错误信息
- 返回值:无
- help方法
- 用途:显示帮助信息
- 参数:无
- 返回值:无
- setDefaultOpts方法
- 用途:设置默认选项参数
- 参数:
- opts:对象,要设置的默认选项参数
- 返回值:无
- setDefaultCommand方法
- 用途:设置默认命令
- 参数:
- command:字符串,要设置的默认命令
- 返回值:无
- get方法
- 用途:获取参数值
- 参数:
- key:字符串,参数名
- opts:对象,选项参数
- command:字符串,命令名
- 返回值:参数值
- log方法
- 用途:打印日志
- 参数:任意个要打印的日志内容
- 返回值:无
- return方法
- 用途:统一输出最终结果
- 参数:
- ret:任意类型,要输出的结果
- 返回值:传入的结果
- feedPublish方法
- 用途:发布feed
- 参数:
- content:字符串,feed内容
- meta:对象,feed元信息
- is_public:布尔值,是否公开
- command:字符串,命令名
- 返回值:布尔值,表示是否成功
- feedRemove方法
- 用途:删除已发布的feed
- 参数:
- id:字符串,feed的id
- 返回值:布尔值,表示是否成功
辅助函数
- mergeProps函数
- 用途:合并属性
- 参数:
- oldObject:对象,原有对象
- props:字符串,属性定义
- 返回值:合并后的新对象
mergeProps函数主要用于派生类选择性的继承父类写在 Package.json
中的参数定义等信息。
比如, fxd-app-check-chan
在 Package.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"
额外说明
- 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 中显示。
支持的属性如下:
- for: 固定为"fxd",用于标识这是一个fxd app
- author_uid: 用于指定 app 作者,只有 app 作者才有权限上架 fxd 商店
- args: 参数信息。
- 第一级为方法,如
watch
、check
;多个方法同时支持的参数可以放到一起,方法名用竖线隔开,如main|check|watch
- 第二级为字段说明,字段说明包含以下属性:
1. name: 英文字段名
1. cn_name: 显示在客户端界面中的中文字段名
1. description: 对字段的说明,通常会作为 placeholder 显示
1. default: 默认值
1. required: 是否必填
1. example: 字段值示例
1. advanced: 是否为进阶参数,如是,则只在「进阶设置」按钮按下时显示
1. type: 字段类型,支持
string
、boolean
、number
1. ui: 字段UI类型,UI默认会根据type
自动适配,但可以强制指定。如type
为string
时默认为文本输入框,但可以指定 UI 为textarea
,这样会将其变为长文本编辑区域。支持textinput
,shortcutinput
,textarea
,numberinput
,dateinput
,select
,multiselect
,jsoninput
,passwordinput
,switch
,cron
- 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));
}
}
测试应用
通过终端进入安装方小代可执行文件所在的目录:
- 在 Mac 系统中,启动路径为:
存放方小代应用的目录/Contents/MacOS/方小代
- 在 Win 系统中,启动路径为:
存放方小代应用的目录/方小代.exe
运行 npm install DemoAPP的绝对路径
,将其添加到 node_modules
目录。
然后就可以运行这个应用了。
- 命令行:在当前目录输入 node node_moduels/fxd-cli/index.js Demo 即可。注意这里的应用名是包名去掉
fxd-app-
后,按小驼峰书写 - 客户端:按包名添加应用
fxd-app-demo
发布应用
当测试完成后,可以通过 npm publish
命令发布到官方。当然,你需要注册并登入 npmjs.com 。具体的教程请自行查找。
删除本地测试应用
打开方小代可执行文件所在的目录的 package.json
删除其中关于 fxd-app-demo
的依赖并保存后,重新 npm install
即可。