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" });
}
}
}
}