docs: 平台标记单一源规则与迁移指南通俗版;补交 Word 报告流程生成脚本

- dev-conventions:平台标记收敛为注册分组表单一源,能力文件不得散写 process.platform
- rpa-core-migration-guide:重写为通俗版(现状/搬迁步骤/三端计划)
- gen-word-flow.py:Word 报告加工流程生成脚本(此前漏交)
This commit is contained in:
yu 2026-11-05 17:02:10 +08:00
parent de6165e76e
commit 230e3c79ec
3 changed files with 245 additions and 150 deletions

View File

@ -47,8 +47,12 @@
3. **错误契约**:失败抛 `RuntimeError(code, message, { recommendation })`;
code 用稳定英文枚举(必须登记在 `packages/rpa-core/src/engine/error-codes.ts`
的 `RUNTIME_ERROR_CODES`,任务 4.3 起机器可读),message 中文,recommendation 给可执行建议。禁止裸 throw string。
4. **平台标记**:Windows 专用能力(user32/UIA/COM)在 catalog 注册时带
`platforms: ["win32"]`;能用纯 Node 实现的能力不调 PowerShell。
4. **平台标记(单一源,三端优先 win)**:平台归属只有一处权威——注册分组表
(迁入前固化为 `register-all.ts` 的 `PLATFORM_GROUPS`,现阶段按分组在 catalog 构建时统一标 `win32`)。
能力文件**不得**自带平台标记、**不得**散写 `process.platform` 判断(例外白名单单独维护);
`meta.platforms` 与 catalog 的 `platformTag` 都从分组表派生,一致性由测试守卫。
现阶段所有能力只标 `win32`,`darwin`/`linux` 仅在类型枚举里占位,**不填任何数据**;
将来支持新平台 = 新增分组 + 表里加一行,不做别的。
5. **可测试**:每个能力有同名 `*.test.ts`(vitest),与实现同目录。
6. **结构化 meta(任务 4.1 起,新能力强制)**:`register(ids, handler, meta)` 第三参
必填 `CapabilityMeta`(summary / params / returns / timeoutMs / errorCodes /

View File

@ -1,179 +1,184 @@
# rpa-core 迁入 Agent-Employee 指南
# rpa-core 迁入 Agent-Employee 指南(通俗版)
> 本文是 `@rpa/core` 包从 demo(本仓库)迁入 `D:\code\Agent-Employee` 的完整操作指南。
> 抽离改造已于 2026-11-05 完成(阶段 0→5,分支 refactor/rpa-core 已合入 main,
> commit `4139400..a8084dc`),本指南对应任务清单中的「阶段 6 — 迁入 Agent-Employee」。
> 配套文档:`docs/rpa-core-refactor-tasks.md`(任务与风险清单)、`docs/dev-conventions.md`(开发规范)、`packages/rpa-core/README.md`(包边界与契约)。
## 这是一份什么文档?
我们花了两周把 RPA 的"能力层"(跑流程用的几百个能力 + 流程引擎)从现在这个 demo 项目里抽出来,
做成了一个独立软件包,叫 `@rpa/core`,放在本仓库的 `packages/rpa-core/` 目录。
**为什么要抽出来?** 因为这个包以后要搬到另一个项目 Agent-Employee 里用。
抽成独立包的好处:搬过去时不用改一行代码,两边也不用维护两份一样的代码。
**这份文档讲什么?** 讲三件事:
1. 这个包现在长什么样(现状)
2. 到时候怎么搬过去(操作步骤)
3. 以后要支持 Mac 和 Linux 怎么办(三端计划)
> 配套文档:`docs/rpa-core-refactor-tasks.md`(当时的改造任务清单)、`docs/dev-conventions.md`(日常开发规范)、`packages/rpa-core/README.md`(包的技术说明书)。
---
## 1. 迁入对象的当前形态
## 一、现状:包已经是什么样了
| 项 | 现状 |
改造已全部完成并合入 main 分支(6 个阶段提交,`4139400..a8084dc`)。现在:
| 你可能想知道的 | 答案 |
|---|---|
| 包位置 | `packages/rpa-core/`(npm workspace 成员,`@rpa/core`,private) |
| 依赖约束 | 零 Electron、零宿主引用(`npm run check:deps` 护栏强制) |
| 导出入口 | `.`(engine/driver/browser/types)+ `./capabilities`(副作用注册全量能力) |
| 运行时依赖 | exceljs / pdf-lib / nodemailer / imapflow / mailparser / mssql / mysql2 / pg / basic-ftp / adm-zip / pngjs / sm-crypto / ws / @gutenye/ocr-node |
| 宿主耦合点 | 全部经 `HostBridge` / `SubflowSource` / `BrowserLike` 三个接口注入,无反向依赖 |
| 验证基线 | 包内注册 719 个能力 id;demo 全量快照 909 id(含宿主浏览器能力);`npm run rpa-cli` 零 Electron 跑通 5 个代表能力 |
| 测试 | 包内独立 vitest(串行模式,防桌面自动化竞态);部分冒烟测试需真实 Windows 桌面 |
| 包在哪? | `packages/rpa-core/`,包名 `@rpa/core` |
| 里面有什么? | 流程引擎(跑流程的循环/变量/错误处理)+ 719 个能力(文件/Excel/HTTP/数据库/邮件/桌面操作等) |
| 它依赖 Electron 吗? | **完全不依赖**。有脚本 `npm run check:deps` 天天检查,谁往里加 Electron 代码就报错 |
| 它怎么用 Electron 的功能? | 见下面"宿主桥"的解释 |
| 怎么证明它能独立跑? | `npm run rpa-cli`:不启动 Electron,直接跑通 5 个代表能力(桌面点击/Excel/HTTP/文件/屏幕) |
## 2. 迁入前置项(在 demo 侧已延后,迁入时必须补做)
**几个名词解释(后文会反复出现):**
- [ ] **5.2 Bun 冒烟**(决策点 10 延期项,迁入第一件事)
在 Agent-Employee 环境用 `bun test` 跑包内**纯逻辑测试**,逐项验证:
- exceljs / pdf-lib / adm-zip / mailparser 等依赖在 Bun 下读写正常;
- `driver/windows/powershell.ts` 的 `execFile("powershell.exe")` + `-EncodedCommand` 在 Bun 子进程下编码行为一致(GB18030 回退、CLIXML 解析);
- Bun 的 `fetch` 与 Node 行为差异对 Http 能力无影响。
- `@gutenye/ocr-node` / onnxruntime 原生依赖**不在本次范围**,单独立项验证(见 §6 留守清单)。
- [ ] **catalog platformTag 数据重生成**
`scripts/build-catalog.mjs` 的 `platformTag: "win32"` 填充逻辑已就绪(desktop/app/office/vision 组),
因外部源数据(Agent-Employee 仓库)此前不在本机未生效。迁入后跑一次 `npm run catalog` 并核对。
- [ ] **注册入口切换评估**
双入口现状:旧 `import "@rpa/core/capabilities"`(副作用注册)+ 新 `await registerAll(engine, groups?)`(显式注册,可按域子集)。
Agent-Employee 建议直接用 `registerAll`;确认稳定后可在包内废弃副作用入口(单独决策,不阻塞迁移)。
- **宿主**:使用这个包的应用程序。现在宿主是本 demo(Electron 应用),以后宿主是 Agent-Employee。
- **宿主桥(HostBridge)**:包偶尔需要"只有应用程序才有"的功能,比如弹一个窗口、读取 AI 配置。
包不直接去拿,而是定了一个接口说"谁来用我,谁把这几个功能递给我"。
宿主启动时调用 `setHostBridge(...)` 把功能递进来。目前一共只有 4 个功能要递:
弹窗、两组屏幕坐标换算、读 AI 配置。另有 `setSubflowSource(...)` 递子流程仓库(2 个方法)。
- **驱动(Driver)**:真正干"动鼠标、敲键盘、找窗口"这些活的模块。现在的实现叫 `WindowsDriver`(用 PowerShell 实现)。
能力层不直接碰 PowerShell,而是喊"driver,帮我把鼠标移到 x,y"——所以以后要做 Mac 版,只需要再写一个 `MacDriver`,能力层一行不用改。
## 3. 迁入步骤(阶段 6.1 → 6.4)
---
### 6.1 ADR 评审材料
## 二、到时候怎么搬:三步走
按 Agent-Employee 流程提交架构决策记录,三份材料可直接取自:
### 第 -1 步:先知道对面已经有什么(2026-11-05 实地调研)
1. **包边界**:`packages/rpa-core/README.md`「包边界」表(engine / driver / capabilities / browser 四模块职责)。
2. **HostBridge 契约**:README「HostBridge 契约」章节 + 包内 `src/engine/host-bridge.ts`(接口定义即契约,共 4 个方法 + SubflowSource 2 个方法,见 §4.1)。
3. **driver 生命周期**:`RpaEngine.driver` 默认 `new WindowsDriver()`,`createEngine({ driver })` 可注入自定义实现;驱动只承载原始注入原语,拟真/换算/组合策略在能力层。element 域实现由 `capabilities/desktop-uia.ts` 经 `setElementDriverImplementation` 反向注册(避免 driver→capabilities 依赖)。
Agent-Employee(`D:\code\Agent-Employee`)是 Electron + SolidJS 客户端 + Bun 执行面 + Java 控制面的结构,
前端 `code/frontend`、后端 `code/backend` 是**两个独立的 bun workspace**(bun@1.4.3,workspaces 含 `apps/*`、`packages/*`、`services/*`)。
### 6.2 包迁入 `packages/`
关键事实:**对面已经为我们要搬的东西铺好了契约,但还没有实现**。`code/backend/packages/` 下有一批契约包(每个只有 1-9 个声明文件):
1. **保留 git 历史**:用 `git subtree` 或先在 demo 仓库完成搬迁 commit 再整体 fetch,禁止复制目录后重开历史(总原则 1:单一源,杜绝代码漂移 R3)。
2. **bun workspace 接入**:
- Agent-Employee 根 `package.json` 的 `workspaces` 加 `packages/*`;
- 包 `package.json` 的 `exports` 指向 `./src/index.ts` 源码直出(现状),若 Agent-Employee 构建链需要产物,再补 `build` 脚本与 `exports` 产物路径——此调整属于宿主侧决策,包源码不动;
- 根 `tsconfig` paths 增加 `"@rpa/core": ["packages/rpa-core/src/index.ts"]` 与 `"@rpa/core/capabilities"`(参考 demo 根 `tsconfig.json` 现有写法);electron.vite/构建器的对应 alias 同步。
3. **demo 侧解除引用**:迁入后 demo 改为只读引用该仓库(git dependency / npm pin),或明确解除引用并确认 demo 归档范围。两边同时活跃开发即违反单一源原则。
4. **快照基线随包走**:`resources/capability-snapshot.json` 的生成逻辑在 demo 侧 `scripts/capability-snapshot.test.ts`;迁入后在 Agent-Employee 重建等价快照测试,作为能力集回归基准。
| 对面现有契约包 | 它声明了什么 | 与 @rpa/core 的关系 |
|---|---|---|
| `workflow-engine` | workflow / step / approval / failure 状态模型(9 个文件,依赖 plugin-contract、capability-gateway、event-bus) | rpa-core 的 runtime 是它的**执行引擎候选**,迁入时需做状态模型对齐(审批/失败/证据如何挂到我们 RunResult/NodeState 上) |
| `capability-gateway` | 所有带副作用的能力执行前必须过:授权 → 租约签发 → 租约校验 → 执行约束;契约含 `CapabilityRunner` / `authorize()` / `execute()` | **权限治理(6.4)的现成落点**。rpa-core 能力执行要包进这个网关,而不是自建 allowlist |
| `computer-control` | 跨平台观察、定位、动作抽象与桌面会话策略 | rpa-core 五域 driver 是它的**实现候选**;三端设计(第三节)应对齐这份契约 |
| `windows-plugin` | Windows 目标、桌面会话策略、输入类高影响能力、执行器接口 | WindowsDriver 的接入契约 |
| `browser-plugin` | 浏览器动作、域名白名单、高影响能力审批边界、证据机制 | demo 侧浏览器能力的对面契约 |
| `runtime-governance` / `plugin-contract` | 运行时治理 / 插件契约 | 治理对接的参考 |
| `services/windows-runner` | Windows 交互 Runner(**空壳,0 个源文件**) | 对面 README 已明确"Windows 交互 Runner 留在宿主,不放入 Linux 容器"——与 rpa-core 桌面能力只能跑在 Windows 交互会话的约束天然一致 |
### 6.3 宿主适配层(product-shell 侧工作)
**结论:迁入的本质不是"新写一套宿主适配",而是"拿 rpa-core 的实现去填对面的契约"。**
这比原计划省了适配层设计,但多了一步契约对齐(接口签名互相对照,差异列清单评审)。
包要求的全部宿主注入点如下,迁入 = 逐项提供实现:
### 第 0 步:搬家之前,在 demo 里先补三件作业
**① HostBridge(必做,4 个方法)**——demo 参考实现在 `src/main/index.ts` L28-50:
这些事当时刻意延后了,搬家前必须补上(都在本仓库做,改起来便宜):
```ts
import { setHostBridge, type HostDialogWindow } from "@rpa/core";
1. **Bun 冒烟测试**。Agent-Employee 用 Bun 跑代码,我们一直用 Node。
搬过去第一件事:用 `bun test` 跑包里的纯逻辑测试,确认 exceljs、pdf-lib 这些库在 Bun 下行为一致,
尤其是调 PowerShell 的部分(编码处理容易有差异)。
2. **重新生成能力目录**。跑一次 `npm run catalog`,让每个能力带上 `platformTag: "win32"` 标记
(标记逻辑早就写好了,当时因为数据源不在这台机器上没生效)。
3. **决定用哪种注册方式**。包支持两种注册能力的方式:
旧方式 `import "@rpa/core/capabilities"`(一导入全注册),新方式 `await registerAll(engine)`(想注册哪几组就注册哪几组)。
建议 Agent-Employee 直接用新方式,旧的以后可以废掉。
setHostBridge({
// human-dialog 能力创建人机交互表单窗口;BrowserWindow 适配为 4 方法最小面
createFormWindow({ title }) {
const win = new BrowserWindow({ width: 480, useContentSize: true, alwaysOnTop: true,
webPreferences: { contextIsolation: true, sandbox: true } });
const dialogWindow: HostDialogWindow = {
isDestroyed: () => win.isDestroyed(),
destroy: () => win.destroy(),
loadURL: (url) => win.loadURL(url),
on: (event, listener) => win.on(event, listener),
executeJavaScript: (script) => win.webContents.executeJavaScript(script, true)
};
return dialogWindow;
},
// window-extended 鼠标/拖拽坐标换算;未注入时包内回退恒等(测试模式)
dipToScreenPoint: (p) => screen.dipToScreenPoint(p),
screenToDipPoint: (p) => screen.screenToDipPoint(p),
// ai-service 配置读取;未注入返回空配置,requireAiConfig 抛 ai_not_configured
loadAiConfig: () => ({ baseUrl, apiKey, model, visionModel }) // 来自宿主 settings
});
```
### 第 1 步:搬包(半天)
未注入时的降级行为(包内已定义,宿主测试环境可依赖):DPI 换算恒等、AI 空配置、human-dialog 抛 `host_unavailable`。
1. **评审**:按 Agent-Employee 团队的流程提交一份架构决策记录(ADR),内容就是本文档第一节 + 对面契约对照表 + 包的 README,不用另写。
2. **搬代码**:把 `packages/rpa-core/` 整个目录用 git 方式搬进 **`code/backend/packages/rpa-core`**
(执行面归 backend workspace;对面 bun@1.4.3 的 workspaces 已含 `packages/*`,放进去即生效)。
用 `git subtree` 之类保留提交历史,**千万不要复制文件夹另起炉灶**——两份代码会越走越远。
3. **包名决策**:对面 backend 包统一用 `@agent-worker/*` 前缀(如 `@agent-worker/plugin-contract`)。
建议迁入时改名 `@agent-worker/rpa-core`(一次 commit 完成 rename,包内代码不用动)。
4. **处理 demo 侧**:搬走之后 demo 要么改成只读引用,要么归档。**两边不能同时活跃改代码。**
**② SubflowSource(必做,2 个方法)**:`setSubflowSource({ listFlows, getFlow })`,子流程仓库在宿主侧(demo 对应 `flows.ts`)。`getFlow` 返回包内 `FlowDefinition`(注意:是包内领域类型,非宿主画布模型,需要做映射)。
### 第 2 步:契约对齐 + 宿主适配(核心工作量)
**③ BrowserLike(内置浏览器路径)**:包内 `BrowserLike` 接口(`src/engine/types.ts`)是浏览器能力的抽象面。Agent-Employee 若沿用 Electron product-shell,可直接移植 demo 的 `src/main/browser/session.ts` + `capabilities/browser*.ts`;若不是 Electron,需按 BrowserLike 契约重新实现(这是迁入工作量的最大变数,先评审 Agent-Employee 的浏览器方案再估工作量)。
分两类工作:
**④ ExternalTransport / EmbeddedTransport**:外部浏览器 CDP 走包内 `ExternalTransport`(零改动);内置浏览器 CDP 的 `EmbeddedTransport`(`webContents.debugger` 适配)留在宿主,随 ③ 一起移植。
**A. 契约对齐(对面已铺好,我们填实现)**——逐个对照,差异列清单评审后改:
**⑤ AI 编排能力**:`src/main/capabilities/ai-caps.ts` 消费包内 `chatCompletion`/`requireAiConfig`,配置经 `loadAiConfig` 注入。宿主提供等价 settings 即可,README 标注「可随后续阶段评估迁入包」。
### 6.4 权限与治理对接(R10)
- 危险能力清单(候选,迁入时与 Agent-Employee 权限模型逐一对齐):`System_ExecuteCommand`、`App_Kill`、键鼠注入家族(Mouse_*/Keyboard_*)、`Clipboard_*`、文件删除/移动家族。
- 对接方式:在宿主的 RunContext 组装层做能力级 allowlist 拦截(能力 handler 内部无权限概念,这是刻意的——权限属宿主治理职责);`registerAll(engine, groups?)` 的按域注册可作为粗粒度开关。
- 能力目录(catalog.json)与 Agent-Employee 插件体系对齐:`platformTag` 字段已就绪,能力 meta(`getCapabilityMeta(id)`,含参数 schema 与 errorCodes)可供其面板/Agent 消费。
## 4. 不迁入部分(宿主职责,迁入时明确归属)
| demo 侧模块 | 迁入时的处理 |
| 对面契约 | 对齐内容 |
|---|---|
| `src/renderer/` | 不迁(Agent-Employee 用 SolidJS) |
| `src/main/overlay.ts` / `desktop-pick.ts` / `vision-lab.ts` / `fixture/` | Electron 窗口强耦合,按 Agent-Employee 等价功能重写或放弃 |
| `src/main/local-vision/` | onnxruntime-node 原生依赖,Bun 兼容性单独立项,**不阻塞迁移** |
| `src/main/flows.ts` / `run/` / `settings.ts` | 应用级编排与持久化,宿主职责(包通过 SubflowSource/HostBridge 取用) |
| `src/main/browser/session.ts` + `transport.ts` + browser* 能力 | 随宿主浏览器方案决策(§6.3 ③④) |
| `capability-gateway` | 把 rpa-core 的能力执行包进 `authorize() / execute()` 流程(`CapabilityRunner` 接口);危险能力(System_ExecuteCommand、键鼠注入等)走它的租约与审批,**不再自建 allowlist** |
| `workflow-engine` | 状态模型对齐:我们 `RunResult/NodeState`(running/succeeded/failed/...)对照它的 workflow/step/approval/failure;人工审批节点(human-dialog)要能挂进它的 approval 流 |
| `computer-control` / `windows-plugin` | 五域 driver 的观察/定位/动作抽象与它们契约互相对照;WindowsDriver 作为执行器接入 |
| `browser-plugin` | 浏览器能力对齐域名白名单与审批边界 |
## 5. 迁入验收标准
**B. 宿主注入(照旧要做,但工作量变小)**:
1. `bun test`(包内纯逻辑测试)全绿 —— 补做 5.2 的结论。
2. `bun run` 等价于 `npm run rpa-cli` 的 CLI host 跑通 5 个代表能力(桌面 / Excel / HTTP / 文件 / 屏幕),证明零宿主可运行。
3. 能力快照零 diff(719 包内 id 对齐;宿主侧重建快照后 909 对齐)。
4. `check:deps` 通过(保持零 Electron 零宿主引用)。
5. HostBridge 注入后,human-dialog / window-extended DPI / ai-service 三条注入路径有真实交互验证(弹窗、高分屏拖拽、AI 调用各至少 1 例)。
6. demo 侧(若保留):test + typecheck + catalog 快照三项全绿不回归。
| 要递什么 | 干什么用 | 工作量 |
|---|---|---|
| `setHostBridge` 4 个方法 | 弹窗、DPI 坐标换算、读 AI 配置 | 小。对面也是 Electron(`code/frontend/apps/desktop`),参考实现照抄本仓库 `src/main/index.ts` 第 28-50 行 |
| `setSubflowSource` 2 个方法 | 子流程的存与取 | 小。对面有自己的流程存储,包一层即可 |
| **内置浏览器**(`BrowserLike` 接口) | "浏览器打开网址/点按钮/填表单"这组能力需要宿主提供浏览器 | **不确定,是最大变数**。对面的浏览器方案还没定型(browser-plugin 只是契约);需与对面团队评审后再估工作量 |
| AI 能力配置 | `ai-caps.ts` 需要 baseUrl/apiKey 等 | 小。对面有模型网关(model-gateway),接它的配置面 |
## 6. 关键决策记录(迁入时不再重新讨论)
**不搬的东西**(留在 demo / 由 Agent-Employee 重新做):
界面渲染层、悬浮框/取色器、本地视觉(onnxruntime 那套)、流程存储、内置浏览器编排。
完整清单见包 README 的「Electron 相关能力留守清单」。
### 第 3 步:验收(搬家算不算成功,看这 7 条)
1. `bun test` 包内纯逻辑测试全绿;
2. `npm run rpa-cli` 的等价物在 Bun 下跑通 5 个代表能力;
3. 能力注册数对得上(包内 719 个);
4. `check:deps` 通过(没人在搬家过程中偷偷加回 Electron);
5. 真实操作一遍:弹一次人机对话框、高分屏上拖一次窗口、调一次 AI——验证宿主桥三条路径通了;
6. 契约对齐有书面结论:capability-gateway(能力执行进网关)、workflow-engine(状态模型)、computer-control(driver 抽象)三份对照清单各评审通过;
7. demo 侧(如果还保留)测试全绿。
---
## 三、以后要支持 Mac 和 Linux 怎么办?
好消息:当初设计时已经把口子留好了——能力层只对"驱动接口"说话,不关心底下是 Windows 还是 Mac。
所以三端化不用推翻任何东西,分两批做:
### 搬家之前(demo 侧,5 件小事)
| # | 事项 | 说明 |
|---|---|---|
| 1 | **平台标记统一机制**(三端优先 win) | 平台归属收敛为一处权威数据:`register-all.ts` 分组表 `PLATFORM_GROUPS`。三个平台类型都能填,但**数据只填 win32**,darwin/linux 仅类型占位;`meta.platforms`(运行时)与 catalog `platformTag`(构建期)都从这张表派生,`build-catalog.mjs` 删掉自带的 `WIN32_GROUPS` 硬编码;加一致性守卫测试,两处不一致 CI 直接红 |
| 2 | 驱动改成按系统自动选 | 现在写死 `new WindowsDriver()`;改成看 `process.platform` 自动选,Mac/Linux 上没实现就明确报"该平台暂不支持"(加个 `platform_unsupported` 错误码) |
| 3 | 检查能力分组 | Excel COM、Word COM 这些 Windows 专属能力,确保归在能整组过滤的分组里——将来 Mac 版就不注册这组 |
| 4 | 测试加平台守卫 | 现在只有 4 个测试文件写了"非 Windows 就跳过",键盘/鼠标/窗口冒烟测试没写。补齐后,Mac/Linux 的 CI 才能跑(跑纯逻辑部分,跳过真桌面操作) |
| 5 | 开发规范加一条 | 禁止在能力文件里随手写 `process.platform` 判断(现在 utility-extended 里已有 2 处,收拢到固定位置),防止平台差异到处扩散 |
### 搬家之后(Agent-Employee 侧,按平台写驱动)
0. **先对齐对面的 `computer-control` 契约**:对面已声明"跨平台观察、定位、动作抽象"的契约(还是空壳)。
写 MacDriver / LinuxDriver 之前,先让五域 driver 接口与这份契约对表,避免两边各长各的。
1. **写 MacDriver / LinuxDriver**(接口现成,按五个域补实现):
- Linux:鼠标键盘用 xdotool(X11);Wayland 支持受限,是三端里最难的坑;界面元素识别走 AT-SPI;
- Mac:鼠标键盘用 cliclick 或 CGEvent;窗口操作走 AppleScript;界面元素走 AX 辅助功能 API。
2. **先评估 macOS 权限问题**:Mac 上 RPA 软件必须引导用户开"辅助功能/屏幕录制"授权(系统 TCC 机制),
这是 Mac 端最大的部署摩擦,做之前先摸清。
3. **Office 替代方案**:Excel/Word 的 COM 自动化只有 Windows 有。Mac 可走 AppleScript + Office,
Linux 可走 LibreOffice;或者第一版直接标注"仅 Windows"。
4. **浏览器路径**:外部浏览器启动路径现在写死了 Windows 的(`C:\Program Files\...`),要补 Mac/Linux 的路径表。
5. **剪贴板等小件**:PowerShell 换成 `pbpaste/pbcopy`(Mac)、`xclip`(Linux)。
6. **CI 三个平台各跑一遍**;产品界面按能力标注的平台支持情况置灰不支持的项。
### 一条原则
**平台标记必须统一,这是硬约束(三端优先 win):**
- 平台归属只有**一处权威数据**:包内注册分组表 `PLATFORM_GROUPS`(33 个能力组 × 平台数组)。
- 运行时的 `meta.platforms` 和构建期的 catalog `platformTag` 都**从这张表自动派生**,任何代码不得自行声明第二份平台数据。
- **数据里今天只有 `win32`**;`darwin`/`linux` 只存在于类型枚举里占位,不填任何数据。
- 将来支持新平台 = 新增能力分组 + 表里加一行,仅此而已;守卫测试保证两处派生永远一致,谁绕过就红。
平台差异**只允许**出现在两个地方:`driver/<平台>/` 目录下的驱动实现,以及极少数白名单能力文件。
引擎、注册表、能力接口、宿主桥,永远保持平台无关。
---
## 附:当时的 10 个关键决策(搬家时不要再讨论一遍)
| # | 决策 |
|---|---|
| 1 | 物理搬迁 `git mv`,非引用保留 |
| 2 | 保留全局注册表 + 副作用注册;`createEngine()` 工厂收拢进程级状态 |
| 3 | 集中突击式迁移,不长期双轨 |
| 4 | HostBridge 最小面:createFormWindow / dipToScreenPoint / screenToDipPoint / loadAiConfig |
| 5 | 浏览器 CDP:外部路径入包,Embedded 留宿主 |
| 6 | 五域 AutomationDriver 接口 + WindowsDriver 机械包一层(行为不变) |
| 7 | catalog 只填 platformTag 数据,渲染层置灰延后 |
| 8 | CapabilityMeta 可选 + 新能力强制 |
| 9 | 注册双入口过渡(副作用 + registerAll) |
| 10 | 5.2 Bun 冒烟延期到迁入时执行(本文 §2 第一项) |
风险 R1-R10 → 任务映射见 `docs/rpa-core-refactor-tasks.md` 末尾表格;迁移后的新规范增量见 `docs/dev-conventions.md`(结构化 meta 强制、错误码登记、powershell 助手唯一出口)。
---
## 7. 三端支持(Windows / macOS / Linux)
目标:项目最终要在三个桌面平台运行。架构钩子已预埋,工作分「迁入前小改」(demo 侧,避免跨仓库返工)与「迁入后实现」(Agent-Employee 侧,按域补 driver)两批。
### 7.1 已就位的三端基础(无需改动)
- **五域 AutomationDriver 接口**:能力层只面向 `driver.mouse/keyboard/window/screen/element`,Windows 实现整体可替换;
- **`createEngine({ driver })` 注入** + **`registerAll(engine, groups?)` 按域注册**(可只注册当前平台支持的能力组);
- **meta.platforms 字段**(`src/engine/registry.ts` 已定义,字面量待扩展);
- **HostBridge 的 DPI 接口**走 Electron,mac Retina / Linux 由 Electron 屏蔽,包内无感知。
### 7.2 迁入前改(demo 侧,成本低的准备工作)
| # | 改什么 | 为什么现在做 |
|---|---|---|
| 1 | `platforms` 类型扩成 `"win32" \| "darwin" \| "linux"`,能力 meta 填 platforms 数据(catalog 从「只标 win32 组」改为逐能力标注) | 纯类型+数据;拖到 Agent-Employee 侧要跨仓库改 |
| 2 | 引擎驱动工厂化:`engine.ts` 硬编码 `new WindowsDriver()`,改为按 `process.platform` 选择,非 win 平台明确抛 `platform_unsupported`;RUNTIME_ERROR_CODES 同步加该错误码 | 改动 10 行内 |
| 3 | 注册组与平台边界对齐检查:excel-com / word-extended(COM)/ desktop-uia / desktop 等 Windows 专属能力确保归入可按平台过滤的组 | registerAll 的 groups 就是三端注册开关,组错位到时要重构 |
| 4 | 测试 skipIf 守卫补齐:目前仅 4 个测试文件有 `process.platform !== "win32"` 守卫,键盘/鼠标/窗口冒烟都没有 | 补齐后纯逻辑测试能在 mac/linux CI 跑 |
| 5 | dev-conventions 加约束:能力文件禁止散落 `process.platform` 分支(utility-extended.ts 现有 2 处,收口到明确清单) | 防止平台判断扩散,三端化时找不到边界 |
### 7.3 迁入后实现(Agent-Employee 侧,独立项目)
1. **LinuxDriver / MacDriver 实现**(接口已就位,按域补齐):
- Linux:鼠标/键盘 xdotool(X11)、ydotool+uinput(Wayland,全局注入受限,是三端中最大技术坑)、element 走 AT-SPI、screen 走 xrandr;
- macOS:鼠标/键盘 cliclick 或 CGEvent、window 走 AppleScript / System Events、element 走 AX Accessibility API;
- **macOS TCC 权限**(辅助功能 / 屏幕录制 / 自动化授权)是 mac 端最大部署摩擦,迁入前先做可行性评估。
2. **Excel/Word COM 替代**:mac 走 AppleScript + Office 自动化,linux 走 LibreOffice UNO;或第一版直接标 `platforms: ["win32"]` 不支持。
3. **external.ts 浏览器路径表平台化**:当前硬编码 Windows 可执行路径 + 注册表查询 + powershell 进程枚举,需补 mac(`/Applications/Google Chrome.app/...` 等)与 linux(`which google-chrome`)路径表和对应进程发现分支。
4. **剪贴板/桌面助手替换**:powershell → `pbpaste`/`pbcopy`(mac)、`xclip`/`wl-copy`(linux);人机对话框走 HostBridge 由宿主 GUI 方案实现,包内不感知。
5. **onnxruntime/OCR 三端二进制**:原生依赖单独立项验证,不阻塞主线。
6. **CI 三端矩阵**;catalog 渲染层按 platforms 置灰(决策点 7 的延后项此时落地)。
### 7.4 三端化原则
- 平台差异只存在于 **driver 实现**与**极少数能力文件的明确清单**内;能力 handler 层、引擎、注册表、HostBridge 契约保持平台无关。
- 能力不支持当前平台时的统一行为:注册期按 platforms 过滤(面板不出现),运行期兜底抛 `platform_unavailable` / `platform_unsupported`。
- 新增平台专属脚本助手(如 osascript / xdotool 包装)与 `powershell.ts` 同级同规:放 `driver/<platform>/`,经 check:deps 与 review 把关,禁止能力文件自行 spawn。
| 1 | 代码物理搬移(git mv),不是留着引用 |
| 2 | 保留全局注册表;进程级状态收进 `createEngine()` 工厂 |
| 3 | 集中突击改造,不搞长期双轨 |
| 4 | 宿主桥只留 4 个方法(弹窗 / DPI×2 / AI 配置),能不加就不加 |
| 5 | 浏览器:外部浏览器(CDP)进包,内置浏览器留宿主 |
| 6 | 驱动接口五域划分(鼠标/键盘/窗口/屏幕/元素),Windows 实现原样包装、行为不变 |
| 7 | 能力目录只填平台标记数据,界面置灰以后再说 |
| 8 | 能力元数据(meta)老能力可选、新能力强制 |
| 9 | 注册双入口过渡(副作用导入 + registerAll 显式注册) |
| 10 | Bun 冒烟测试延期到搬家时做(本文第二节第 0 步) |

86
scripts/gen-word-flow.py Normal file
View File

@ -0,0 +1,86 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""生成「Word 报告加工」测试流程:打开比价报告 -> 读全文 -> 查找替换 -> 追加加工说明 -> 插入截图 -> 复制副本 -> 弹窗"""
import json
import os
DESK = "C:/Users/Administrator/Desktop"
SRC = f"{DESK}/淘宝手机比价报告.docx"
SHOT = f"{DESK}/RPA整理测试/整理完成截图.png"
nodes = []
edges = []
def node(nid, comp, label, values, x=300, y=0, output=None, options=None):
n = {
"id": nid,
"componentId": comp,
"label": label,
"values": values,
"position": {"x": x, "y": y},
}
if output:
n["options"] = dict(options or {}, outputVar=output)
elif options:
n["options"] = options
nodes.append(n)
return nid
def link(a, b):
edges.append({"id": f"edge_{a}_{b}", "source": a, "target": b})
seq = []
node("node_start", "Trigger.Manual", "触发·立即执行", {"entryName": "立即执行"}, 240, 70, options={"timeoutMs": 30000})
seq.append("node_start")
y = 160
nid = node("node_open", "Word_Open", "打开比价报告", {"filePath": SRC}, 300, y, output="wordDoc")
seq.append(nid); y += 160
nid = node("node_read", "Word_ReadAll", "读取报告全文", {"objWord": "${wordDoc}"}, 300, y, output="docText")
seq.append(nid); y += 160
nid = node("node_replace", "Word_Replace", "替换标注校验状态",
{"objWord": "${wordDoc}", "oldText": "RPA 自动采集", "newText": "RPA 自动采集(已加工校验)"}, 300, y)
seq.append(nid); y += 160
nid = node("node_enter", "Word_Enter", "文末空行", {"objWord": "${wordDoc}"}, 300, y)
seq.append(nid); y += 160
nid = node("node_write", "Word_Write", "追加加工说明",
{"objWord": "${wordDoc}",
"text": "—— RPA 加工记录:本报告全文 ${len(docText)} 字,已由晨间自动化流程于今日读取校验,并在文末插入整理完成截图。"},
300, y)
seq.append(nid); y += 160
nid = node("node_pic", "Word_InsertPicture", "文末插入整理截图",
{"objWord": "${wordDoc}", "imageaddr": SHOT}, 300, y)
seq.append(nid); y += 160
nid = node("node_save", "Word_Save", "保存修改", {"objWord": "${wordDoc}"}, 300, y)
seq.append(nid); y += 160
nid = node("node_copy", "File_Copy_V1", "复制生成加工版副本",
{"file_type": "file", "file_path": SRC, "folder_path": DESK, "target_path": DESK,
"rename": "1", "name": "淘宝手机比价报告_加工版.docx", "action": "覆盖"}, 300, y)
seq.append(nid); y += 160
nid = node("node_close", "Word_Close", "关闭文档", {"objWord": "${wordDoc}", "isKill": "0"}, 300, y)
seq.append(nid); y += 160
nid = node("node_notice", "Dialog_Notice_V2", "完成通知",
{"text": "Word 报告加工完成:已替换标注、追加说明、插入截图,副本存桌面《淘宝手机比价报告_加工版.docx》",
"title": "加工完成", "time_out": "10"}, 300, y)
seq.append(nid)
for i in range(len(seq) - 1):
link(seq[i], seq[i + 1])
flow = {"id": "flow_word_report_process", "name": "Word 报告加工", "nodes": nodes, "edges": edges, "variables": []}
out = os.path.join(os.environ["APPDATA"], "rpa-studio", "flows", "flow_word_report_process.json")
with open(out, "w", encoding="utf-8") as f:
json.dump(flow, f, ensure_ascii=False, indent=2)
print("written:", out, f"nodes={len(nodes)} edges={len(edges)}")