Compare commits

...

7 Commits

9 changed files with 793 additions and 1 deletions

View File

@ -0,0 +1,71 @@
# 2026-11-06 工作日志
## ADR-R2 补记:A++ 检查点协议(拍板)
- 用户确认在方案 A 基础上采用 A++:节点事件流从"透明度日志"升级为检查点协议(nodeId/status/checkpointId/idempotencyKey/resumable),崩溃后 rpa.flow.execute 带 resumeToken 重入断点续跑,权威账本仍为单 step。不动 A、不启用 B。
- 业界对照论据(可与对面沟通用):UiPath Orchestrator≈A(Job 黑盒+日志旁路);Camunda/Zeebe External Task 的单引擎假设不适用于双引擎集成(B 被否的根因);AWS Step Functions callback≈补丁 2;Temporal 理论最干净但需重写,不采纳。
- 两侧改动清单落档 §7-A5:RPA 侧=事件负载字段+resumeToken 重入+幂等键贯通(复用 A3 idempotent 门控);对面侧=step 允许带 token 重入不改租约语义+event-bus 识别检查点+workflow-recovery 回传 token。
- 落地节奏:阶段 2 只定字段设计(近零成本),阶段 3 垂直切片最小验证,阶段 4/5 强制。
## 文档已完成项标定
- §0 核心结论第 2 条重写:P0 整改已完成(原表述"大部分未解决"已过时);仅剩 bun 冒烟+包依赖 3 项核实。
- §7 A1/A3/A9 方案行标 ★ 已实现(2026-11-06 阶段 1)。
- §8 阶段 1 标 ✅ 已完成(提交 9b76dc6/1da0082/1a20dd7/5e8fc96);阶段 2 加"A++ 字段设计定稿";阶段 3 加"检查点续跑最小验证"。
- §9 R8 缓解列补 A++ 统一崩溃恢复语义。
- 提交:d99be8f。
## bun 冒烟(2026-11-06 下午补)
- bun 已装于 C:\Users\Administrator\.bun\bin\bun.exe(版本 1.4.4,PATH 未刷新需全路径调用;对面锁 1.4.3 为 patch 级差异)。
- 纯逻辑冒烟通过:bun test packages/rpa-core/src/engine/ + excel-common/excel-store/pdf-high = 37/37 全绿,exceljs/pdf 行为与 Node 无差异。
- 注意:engine 测试必须从仓库根目录跑(catalog.json 按 cwd 解析,包内跑必挂 2 个),与 npm 侧既有 cwd 问题一致。
- PowerShell 编码差异项留待阶段 2 迁包后验证(R6 已标"部分缓解")。提交:b18d5b1。
## Word_Save/Word_SaveAs 真实现(2026-11-06)
- word.ts:44 占位(只回显路径)替换为真实现:Word_Save=存在性校验+打开即存;Word_SaveAs=打开原文档→SaveAs2(目标)→返回新路径句柄。
- 参数与 catalog 对齐:objWord(必填)+fileAddr(目录,空则沿用原目录,相对时基于句柄当前目录解析)+filename(空则沿用原名,无扩展名补 .docx)。
- 新增 COM 用例(word-extended.test.ts):另存新目录+改名/空名沿用/相对目录/Save 拒绝不存在句柄,4/4 全绿。注意:相对目录基于句柄当前目录(与 Excel 一致),首版测试预期写错已修正。
- 提交:4b5c73b。
## catalog 3 个数字脏值清洗(2026-11-06)
- 脏值:Basic_BusinessOrderCount / Dialog_GetConfigV5 / Dialog_GetConfigV7 的 platformTag=数字 1,源头是上游 shizai 数据(源数据分布 null=994 / 1=3)。
- 修复:build-catalog.mjs 白名单归一化(非 win32/darwin/linux 字符串一律重派生,A8 方案落地);三条均为包内真实现,重派生为 win32。
- 验证:重生成后 win32=690/null=326/总数 1016 不变;全字段 diff 仅 3 条仅 platformTag 变化(overrides 无损);platform-groups+register-all 测试 6/6。
- 注意:Git Bash 的 /tmp 与 node 路径映射不一致(node 把 /tmp 解析为 <盘>:\tmp),快照 diff 用 cygpath 取真实路径。
- 提交:2bcda3a。
## 两个接入前补丁完成(2026-11-06,a01667b)
- 对面源码在 D:\code\Agent-Employee(backend/packages/...),StepStatus 8 态(workflow-engine/src/index.ts:37),StepRun 带 idempotencyKey(来自租约);对面明确"重启后中断步骤走恢复流程不得续跑"——与 A++ resumeToken 重入设计吻合。
- 补丁 1:NodeState 补 waiting_user/unknown 两态;导出 toStepStatus/NODE_STATUS_TO_STEP_STATUS 全量映射(唯一非同名 idle↔pending)。
- 补丁 2:RunEvent 增 checkpointId/resumable/idempotencyKey;runtime 在 node.finished 成功时填充(checkpointId=node.id,幂等键=runId:nodeId 跨重入稳定,meta.idempotent===false 不作为锚点)。契约测试 checkpoint-events.test.ts 4 用例。
- 附带:sm-crypto.d.ts 类型声明补齐(typecheck 归零,此前 basic-encryption 一直报 TS7016)。
- engine 全量 31→33 用例全绿;账面:本仓库代码活清零,剩余全为对面配合项。
## 遗留待办
- ocr-smoke 清理:用户拍板"算了"(2026-11-06),维持现状——素材缺失时整 describe 静默跳过;D:/office-test2 硬编码保留。后续若迁移对面仓库需重提。
- bun@1.4.3 安装+冒烟(阶段 2 前置,上次因用户在用电脑未装)
- Word_SaveAs 真实现、晨间助手截图时机(待对面确认)、ocr-smoke 清理、catalog 3 个数字脏值清洗
- 跑全量测试前先询问用户(占桌面)
## 对面更新核查(13:30)
- windows-runner 自 10-22 未变;workflow-engine 代码未变——既有可行性结论全部有效
- 对面新增事实一致性门禁(document-facts.mjs + 当前事实.json):代码/事实/文档区块须同提交,CI 拦截
- 对面 README Windows 行明确下一步"click/type、视觉/OCR 或随本轮交付"——R3 路线吻合
- V2 补记门禁合规要求(407aa38)
## C# actuate 原型补丁包(65343da)
- 产出 contrib/windows-runner-actuate/:PORTING-MAP.md(逐原语对照+3 语义差异+错误码映射)、ActuateProtocol.cs、NativeInput.cs(五招兜底逐行移植)、ActuateService.cs(安全门同构+前后证据)、PATCH-NOTES.md(Program.cs 最小 diff+门禁合规清单)
- 关键设计:NativeWindows 只读边界不动(输入 P/Invoke 独立);mouse_event→SendInput;模糊→精确匹配由 TS 宿主 observe 弥合
- 未编译:本机无 dotnet SDK;编译验证待装 .NET 10 SDK 或对面侧首次合入时
## .NET SDK 安装 + 编译验证(13:40)
- winget 装 .NET SDK 10.0.403(对面 net10.0-windows 匹配)
- 编译方式:对面 native 拷贝到 .workbuddy/tmp/actuate-build + 三件套 + PATCH-NOTES §2 最小 diff,不碰对面仓库
- dotnet build 0 警告 0 错误(TreatWarningsAsErrors 开启)
- 编译修正 2 处:NativeInput.Point private→internal(CS0050)、ActuateService 补 using System.Diagnostics(CS0103)→ 提交 f0defc2
- 零桌面交互协议冒烟 5 例:policy_invalid/desktop_session_mismatch/observe 无回归/requestId 回显 全过;越权窗口/非法动作先报 window_not_found(窗口选择先于动作校验,与 observe 顺序一致,预期行为)
- 真实窗口用例(click/type/activate)留对面 test/fixture
## 远端核查 + port 9222 保护(13:50)
- 远端无强推、无被删文件回填:33b85c0/1b952e3 是"yu"的合并提交,另一侧只有 93915df(catalog 远程连接端口 61619→9222)
- 发现陷阱:9222 只改了生成产物,未进 overrides,重生成会打回 61619(已发生过一次,故 merge 标题有 re-apply)
- 修复:WebBrowser_Create/V2/V3 的 port 字段(defaultValue+description)写入 catalog-overrides.json,重生成验证 1016 条零差异
- 本地领先远端多笔,待推送

View File

@ -0,0 +1,81 @@
# PATCH-NOTES:合入对面 windows-runner 的集成指引
本目录是 **本地原型补丁包**,不改动对面仓库。合入时按以下步骤执行。
## 1. 文件落点
| 本目录文件 | 落到对面仓库 | 说明 |
|---|---|---|
| `native/ActuateProtocol.cs` | `code/backend/services/windows-runner/native/` | 新增类型,不动现有文件 |
| `native/ActuateService.cs` | 同上 | 新增类型,不动现有文件 |
| `native/NativeInput.cs` | 同上 | 新增类型;**NativeWindows.cs 保持只读边界原样不动**(对面注释明言该层无输入接口,编译边界得以保全) |
csproj 无需改动:`UseWindowsForms` 已启用(SendKeys/Clipboard/Screen 可用),FlaUI 引用与本补丁无关。
## 2. Program.cs 分发改造(最小 diff)
现有 Main 只认 `observe`。改造为按 Operation 分发:
```csharp
// 原:if (request.ProtocolVersion != 1 || request.Operation != "observe" || ...)
// 改为先解公共头再分发(保持 64KiB/UTF-8/UUID 校验全部不变):
if (request.ProtocolVersion != 1 || !Guid.TryParse(requestId, out _))
throw new RunnerException("request_invalid");
switch (request.Operation)
{
case "observe": { /* 原 observe 路径逐字保留 */ }
case "actuate":
{
var actuate = JsonSerializer.Deserialize<ActuateRequest>(line, JsonOptions)
?? throw new RunnerException("request_invalid");
var result = ActuateService.Actuate(actuate, evidenceRoot);
Write(new RunnerResponse(1, requestId, true, null, null, result));
return 0;
}
default: throw new RunnerException("request_invalid");
}
```
RunnerResponse 需加一个可选字段(observe 的线上形状不变,`WhenWritingNull` 已保证):
```csharp
internal sealed record RunnerResponse(int ProtocolVersion, string RequestId, bool Ok,
ComputerObservation? Data, string? ErrorCode, ActuateResult? ActuateData = null);
```
## 3. TS 宿主侧(native-observer.ts → actuate 适配器,属后续工作)
- 请求侧:actuate 前先用 observe 解析**精确**窗口标题与进程名(RPA 侧"标题包含匹配"的语义在宿主层弥合,见 PORTING-MAP 第二节);
- 响应侧:对 ActuateData.Evidence 逐项复核(realpath 归属证据根目录、SHA-256、PNG 魔数——复用 native-observer.ts 现成校验);
- 高影响审批:`rpa.windows` manifest 已是 `type=highImpact + idempotencyRequired`,capability-gateway 的精确 actionId 放行与租约语义直接适用,**无需改 gateway**;
- 超时:宿主杀进程超时须覆盖 mouse_move_smooth 最坏路径(≈4s)+ 两次截图(≈2s),建议 ≥15s。
## 4. 验证计划(用对面现成夹具)
- `test/fixture/`(AgentWorker.WindowsFixture):仿照 `native-observer.integration.test.ts` 增加 actuate 集成测试:
1. `mouse_move` → GetCursorPos 断言落点;
2. `mouse_click`(无桌面断言,仅断言 ok + 证据 SHA 与文件存在);
3. `keyboard_send`(chars 模式)→ UIA 读回文本;
4. `window_activate` → GetForegroundWindow 断言;
5. 反向用例:越权窗口 → `window_not_allowed`;Session 不匹配 → `desktop_session_mismatch`;非法 Kind → `action_invalid`。
- 兼容性矩阵(微信/Office/UWP)按对面既有测试纪律单独排期,不在本补丁范围。
## 5. 门禁合规清单(对面 2026-11-06 新规,硬性)
合入提交必须**同一提交内**包含:
1. `项目资料/开发方案/当前事实.json`:factRevision 递增,trackedInputs 如受影响同步更新;
2. 相关 README 的机器生成区块(`node code/tooling/document-facts.mjs --write` 重新生成);
3. `code/backend/README.md` Windows 行状态更新("click/type" 从"或随本轮交付"改为已实现口径);
4. 提交前本地跑:`node code/tooling/document-facts.mjs --check` && `node code/tooling/ci-gate.mjs --mode fast` && `git diff --check`。
## 6. 验证状态与已知限制
- **已编译通过 + 协议冒烟通过(2026-11-06,.NET SDK 10.0.403)**:
- `dotnet build`(对面 csproj 原样 + 本补丁三文件 + §2 最小 diff):**0 警告 0 错误**(TreatWarningsAsErrors 开启,直接达到对面合入标准);
- 编译修正两处:`NativeInput.Point` 由 private 改 internal(CS0050 可访问性)、`ActuateService.cs` 补 `using System.Diagnostics;`(CS0103);
- 零桌面交互协议冒烟 5 例:`policy_invalid` ✅ / `desktop_session_mismatch` ✅ / observe 老路径无回归 ✅ / requestId 逐字回显 ✅;另 2 例(越权窗口/非法动作)因**窗口选择先于动作校验**的既有顺序先报 `window_not_found`——顺序与对面 observe 一致,属预期行为,用例已按此修正理解;
- 需要真实窗口的用例(click/type/window_activate/action_invalid 路径)留待对面 `test/fixture` 集成测试(第 4 节),避免占用用户桌面。
- `Clipboard.SetDataObject` 依赖 STA:对面 Main 已是 `[STAThread]`,满足;
- mouse_event → SendInput 为现代等价替换(标志位映射见 PORTING-MAP 第四节);如对面评审倾向零差异,可回退为 mouse_event P/Invoke,一行替换;
- 平滑移动公式中的伪随机抖动与 JS `%` 保号语义一致(C# `%` 对 double 同为保号),无漂移。

View File

@ -0,0 +1,49 @@
# PowerShell → C# 移植对照表(RPA 桌面驱动 → windows-runner actuate)
来源:`packages/rpa-core/src/driver/windows/windows-driver.ts`(269 行,内嵌 PowerShell)
目标:对面 `code/backend/services/windows-runner/native/`(原型见本目录 `native/`)
## 一、原语级映射
| RPA 侧 PowerShell 原语 | 用途(驱动行号) | C# 等价物(原型落点) | 语义保真要点 |
|---|---|---|---|
| `SetCursorPos(x,y)` | moveTo / click / drag / moveSmooth(L142,150,158,173) | `NativeInput.SetCursorPos` | 物理像素;进程入口声明 Per-Monitor V2 DPI(对应 PS 每脚本的 `SetProcessDpiAwarenessContext(-4)`,一次声明进程级生效) |
| `mouse_event(flags,0,0,data,0)` down/up | click / drag(L150,158) | `SendInput`(MOUSEEVENTF_LEFTDOWN=0x0002 / LEFTUP=0x0004 / RIGHTDOWN=0x0008 / RIGHTUP=0x0010) | SendInput 是 mouse_event 的现代等价;标志位一一对应。双击=repeats 2,settleMs 逐次保留 |
| `mouse_event(0x0800,...,amount)` | scroll(L161) | `SendInput` + MOUSEEVENTF_WHEEL,mouseData=有符号 delta | 负值向下滚,`unchecked((uint)amount)` 保持位型一致 |
| `GetCursorPos` + JS `mousePath` 插值 | moveSmooth(L163-174) | `ActuateService.SmoothMovePath`(公式逐行移植) | 缓入缓出二次曲线 + 垂直抖动 `sin(i*12.9898)*43758.5453 % 1`;stepCount=clamp(distance/12, 6, 50);delay=clamp(1500/speed, 4, 80)ms。JS 与 C# 的 `%` 对负数同为保号语义,公式无漂移 |
| `Set-Clipboard` + `SendWait('^v')` | keyboard clipboard 模式(L183) | `Clipboard.SetDataObject(text, true)` + `SendKeys.SendWait("^v")` | 120ms 间隙保留;STA 已由对面 `[STAThread]` 保证 |
| `SendWait($c)` 逐字符循环 | keyboard manual 模式(L188) | 逐字符 `SendKeys.SendWait(Escape(ch))` + `Thread.Sleep(intervalMs)` | intervalMs 逐字符保留 |
| `SendWait(escaped)` 整段 | keyboard 默认模式(L192) | `SendKeys.SendWait(escaped)` | `sendKeysEscape` 字符集 `+^%~(){}[]` → `{+}` 等花括号转义,逐字符同规则移植(`ActuateService.EscapeSendKeys`) |
| `activateWindowScript` **抢前台五招兜底** | focus(L73-105,键盘与激活共用) | `NativeInput.FocusChain`(逐行移植) | 顺序与睡眠严格保留:①IsIconic→SW_RESTORE(9)+150ms ②SwitchToThisWindow+150ms ③ALT(0x12 down/up)+SetForegroundWindow+200ms ④AttachThreadInput 对线程+SetForegroundWindow+200ms ⑤SW_MINIMIZE(6)+150ms→SW_RESTORE(9)+300ms ⑥仍失败→稳定错误码 `focus_failed`(PS 版走 `RPA_ERROR::` stdout 通道) |
| `Get-Process | MainWindowTitle -like *title*` | 窗口查找(L60-68 等) | `Win32 EnumWindows` + 精确标题 + PID 回查进程名 + Session 校验(复用对面 observe 的 SelectWindow 模式) | **唯一语义变更**,见下节 1 |
| `GetWindowRect` + JSON 输出 | windowObject / foregroundPosition(L66,213) | `NativeWindows.GetWindowRect`(对面已有,直接复用) | RECT right/bottom → width/height 换算保持 |
| `ShowWindow(h, 3/6/9/cmd)` | activate/maximize / setState / 五招内部(L203,224) | `NativeInput.ShowWindow` | 命令值原样透传 |
| `SetWindowPos(h,0,x,y,w,h,4)` | setSize / setPos(L227,231) | `NativeInput.SetWindowPos` | flags=4(SWP_NOZORDER)原样保留;尺寸>0 校验保留(L230) |
| `Process.CloseMainWindow()` | close(L221) | `PostMessage(h, WM_CLOSE=0x0010)` | 等价语义(礼貌关闭,不杀进程) |
| `System.Windows.Forms.Screen.PrimaryScreen.Bounds` | screen.size(L246) | 不移植——observe op 的 CoordinateSpace 已携带窗口矩形+DPI | 避免重复采集 |
## 二、必须知道的三处语义差异(宿主适配层负责弥合)
1. **窗口匹配从模糊变精确**。PS 版 `Get-Process -like *title*` 是包含匹配;对面安全政策要求 WindowTitle 与 AllowedWindows **精确相等**(模糊匹配会把同名网页/提示框误当目标)。弥合方式:TS 宿主适配器先用 observe 解析出精确标题与进程名,再发起 actuate 请求——RPA 能力层(`window.findByTitle`)语义不变,只是解析下沉到宿主。
2. **进程模型从"每命令一进程"变"单请求单响应"**。PS 版每个原语起一个 powershell.exe;C# 版一次请求一次响应、宿主超时杀进程(对面现行架构)。RPA 流程里相邻原语本来也无状态依赖(除句柄传递,改由标题精确匹配承担),语义等价。注意:宿主超时必须覆盖 moveSmooth 最坏情况(约 4s)+ 两次证据截图。
3. **副作用证据从无到有**。PS 版动作无留痕;C# 版按对面证据体系在动作**前后各截一张**目标窗口 PNG(PrintWindow,SHA-256 落盘,默认 secret)——这是对面 capability-gateway 审计链要求的,属于增强而非等价项。
## 三、稳定错误码映射(PS 侧 → C# 侧)
| PS 侧失败 | C# actuate 稳定错误码 |
|---|---|
| `找不到窗口:<title>`(throw/RPA_ERROR) | `window_not_found`(无候选)/ `window_ambiguous`(多候选,对面已有语义)/ `window_not_allowed`(不在白名单) |
| `无法把窗口带到前台…`(五招全失败) | `focus_failed` |
| 输入注入 Win32 失败 | `input_failed` |
| 证据截图失败/空白 | `capture_unavailable`(沿用对面现有码) |
| 政策校验失败 | `policy_invalid` / `desktop_session_mismatch`(沿用对面现有码) |
## 四、开关映射:mouse_event → SendInput 标志位
| mouse_event flag | SendInput MOUSEEVENTF | 用途 |
|---|---|---|
| 0x0002 | LEFTDOWN | 左键按下 |
| 0x0004 | LEFTUP | 左键抬起 |
| 0x0008 | RIGHTDOWN | 右键按下 |
| 0x0010 | RIGHTUP | 右键抬起 |
| 0x0800 | WHEEL | 滚轮(data=delta,120 的倍数) |

View File

@ -0,0 +1,31 @@
# windows-runner-actuate 原型补丁包
**用途**:把 RPA 桌面驱动(`packages/rpa-core/src/driver/windows/windows-driver.ts`,PowerShell 实现)的鼠标/键盘/窗口执行能力移植为对面 Agent-Employee `windows-runner` 的 C# 原生实现——即 ADR-R3(C# 路线)的第一块实体。
**状态**:本地原型,未合入对面主干。对面 backend/README Windows 行已明确下一步为"click/type、视觉/OCR 或随本轮客户端交付",本补丁正是其路线图的第一步。
## 内容清单
| 文件 | 内容 |
|---|---|
| `PORTING-MAP.md` | PowerShell → C# 逐原语移植对照表:三处语义差异(模糊→精确匹配、进程模型、证据增强)、错误码映射、mouse_event→SendInput 标志位映射 |
| `native/ActuateProtocol.cs` | actuate 协议扩展类型(请求/动作/结果),复用对面 DesktopPolicy/EvidenceRef/ObservedWindow |
| `native/NativeInput.cs` | 输入类 P/Invoke + **抢前台五招兜底逐行移植**(RPA 实战参数,一行语义不改) |
| `native/ActuateService.cs` | 执行服务:安全门与 observe 同构(白名单/Session/精确匹配),10 种动作闭集分发,动作前后证据截图,平滑移动公式逐行保真 |
| `PATCH-NOTES.md` | 合入指引:Program.cs 最小 diff、TS 宿主适配要点、验证计划、**门禁合规硬性清单** |
## 设计红线(对面安全模型的尊重项)
1. `NativeWindows.cs` 只读编译边界不动——输入类 P/Invoke 独立成 `NativeInput.cs`;
2. 安全门一个不少:进程文件名白名单正则、窗口精确匹配、OS Session 三重核验后才允许输入;
3. 副作用证据:动作前后各一张窗口 PNG(PrintWindow + SHA-256,默认 secret),接入对面证据体系;
4. 稳定错误码进 stdout,系统路径/控件文字不回显;
5. 单请求单响应、超时杀进程,遵守对面进程模型。
## 交接方式
按 `PATCH-NOTES.md` 执行:3 个新文件 + Program.cs 最小 diff + TS 适配器后续工作。合入提交必须带事实文件与文档区块更新(对面 2026-11-06 门禁),清单见 PATCH-NOTES 第 5 节。
## 验证状态
**已编译通过 + 协议冒烟通过**(2026-11-06,.NET SDK 10.0.403):对面 csproj 原样 + 本补丁三文件 + Program.cs 最小 diff → `dotnet build` 0 警告 0 错误;五例零桌面交互协议冒烟全部符合预期(安全门错误码、双路分发、requestId 回显、observe 无回归)。真实窗口用例留待对面 `test/fixture` 集成测试。

View File

@ -0,0 +1,57 @@
using System.Text.Json.Serialization;
namespace AgentWorker.WindowsRunner;
// actuate 协议扩展提案:在不动 observe 契约的前提下增加执行类操作。
// 设计原则与 observe 完全一致:单请求单响应、稳定错误码、宿主固定证据根目录。
// 本文件只新增类型;Program.cs 的分发改造见 PATCH-NOTES.md(diff 指引)。
//
// 与 observe 的关键差异(由 RPA 桌面驱动语义决定):
// 1. Action 携带具体执行参数(坐标/文本/命令),全部字段服务端可审计;
// 2. 坐标一律物理屏幕像素,与 RPA 驱动现状及 CoordinateSpace 契约一致;
// 3. WindowTitle 必须是 AllowedWindows 的精确成员(模糊匹配在 TS 宿主 observe 阶段完成解析),
// RPA 侧"标题包含匹配"的旧语义不进入原生层。
// 操作闭集:mouse_move / mouse_click / mouse_drag / mouse_scroll / mouse_move_smooth /
// keyboard_send / window_activate / window_set_state / window_set_bounds / window_close。
// 与 observe 相同:模型不能直接指定 HWND;证据目录来自宿主启动参数。
internal sealed record ActuateRequest(
int ProtocolVersion,
string RequestId,
string Operation,
string WindowTitle,
DesktopPolicy Policy,
ActuateAction Action);
// 每个字段都可被逐项审计;不支持的 Kind 由原生层拒绝,不静默降级为观察。
internal sealed record ActuateAction(
string Kind,
int? X,
int? Y,
int? ToX,
int? ToY,
// 滚轮 delta(120 的倍数,负值向下)或窗口 ShowWindow 命令值。
int? Amount,
// keyboard_send 文本;上限由 64 KiB 请求预算约束。
string? Text,
// keyboard_send 三模式:"clipboard"(粘贴)/ "chars"(逐字符)/ "keys"(整段 SendKeys)。
string? Mode,
int? IntervalMs,
bool? Double,
// "left"(默认)或 "right"。
string? Button,
// mouse_move_smooth:false=匀速直线,true/缺省=缓入缓出+抖动。
bool? Ease,
// mouse_move_smooth 速度 1-100(默认 35,与 RPA 驱动一致)。
int? Speed,
// window_set_bounds 用(x/y 复用 X/Y,Amount 不用)。
int? Width,
int? Height,
int? HoldMs,
int? ReleaseDelayMs,
int? SettleMs);
// 动作结果:证据前后各一张,宿主 TS 适配器按 EvidenceRef 逐项复核。
// Data 与 ErrorCode 互斥,与 RunnerResponse 同规则。
internal sealed record ActuateResult(string ActionKind, string CapturedAt,
ObservedWindow ObservedWindow, IReadOnlyList<EvidenceRef> Evidence, int DurationMs);

View File

@ -0,0 +1,279 @@
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.Globalization;
using System.Security.Cryptography;
using System.Text.RegularExpressions;
using System.ComponentModel;
namespace AgentWorker.WindowsRunner;
// actuate 执行服务:把 RPA 桌面驱动(PowerShell 实现)的鼠标/键盘/窗口操作
// 移植进对面 windows-runner 协议,安全门与 observe 完全同构。
// 移植来源:packages/rpa-core/src/driver/windows/windows-driver.ts(对照表见 PORTING-MAP.md)。
//
// 安全边界(与 WindowObservationService 同级):
// - 进程白名单(文件名正则)+ 窗口精确匹配 + OS Session 三重核验后才允许输入;
// - 只操作本用户交互 Session 的一个顶层窗口,不扫描其它桌面;
// - 后续动作前仍重新枚举窗口,不信任上一次请求的 HWND;
// - 副作用证据:动作前后各一张目标窗口 PNG(PrintWindow + SHA-256,默认 secret);
// - 非法 Kind 拒绝执行,不静默降级为观察;
// - 稳定错误码进 stdout,系统路径与控件文字不回显。
internal static partial class ActuateService
{
[GeneratedRegex("^[A-Za-z0-9._-]+\\.exe$", RegexOptions.IgnoreCase)]
private static partial Regex ProcessNamePattern();
private const long MaxWindowPixels = 16_000_000;
private const long MaxPngBytes = 8 * 1024 * 1024;
private sealed record WindowCandidate(nint Handle, string ProcessName, string Title);
// DPI 感知一次性声明:与 RPA 驱动每脚本 SetProcessDpiAwarenessContext(-4) 等价,
// 保证此后 SetCursorPos/GetCursorPos 全程物理像素语义。
internal static ActuateResult Actuate(ActuateRequest request, string evidenceRoot)
{
NativeInput.EnsureDpiAwareness();
ValidatePolicy(request);
var selected = SelectWindow(request.WindowTitle, request.Policy);
if (!NativeWindows.GetWindowRect(selected.Handle, out var nativeRect))
throw new RunnerException("window_unavailable");
var bounds = new WindowRect(nativeRect.Left, nativeRect.Top,
nativeRect.Right - nativeRect.Left, nativeRect.Bottom - nativeRect.Top);
if (bounds.Width < 1 || bounds.Height < 1 ||
(long)bounds.Width * bounds.Height > MaxWindowPixels)
throw new RunnerException("window_size_unsupported");
var startedAt = DateTimeOffset.UtcNow;
var before = CaptureWindowEvidence(selected.Handle, bounds, evidenceRoot, startedAt);
Execute(request.Action, selected);
var finishedAt = DateTimeOffset.UtcNow;
var after = CaptureWindowEvidence(selected.Handle, bounds, evidenceRoot, finishedAt);
NativeWindows.GetWindowThreadProcessId(selected.Handle, out var pid);
var observed = new ObservedWindow(selected.ProcessName, selected.Title,
$"0x{selected.Handle.ToInt64():X}",
selected.Handle == NativeWindows.GetForegroundWindow());
return new ActuateResult(request.Action.Kind, finishedAt.ToString("O", CultureInfo.InvariantCulture),
observed, [before, after],
(int)(finishedAt - startedAt).TotalMilliseconds);
}
// 与 observe 的 ValidatePolicy 同构:两个白名单均非空、进程名仅接受文件名、
// WindowTitle 精确成员、OS Session 与本进程一致。actuate 不放宽任何一项。
private static void ValidatePolicy(ActuateRequest request)
{
var policy = request.Policy;
if (policy is null || policy.AllowedProcesses is null || policy.AllowedWindows is null ||
policy.AllowedProcesses.Length == 0 || policy.AllowedWindows.Length == 0 ||
policy.AllowedProcesses.Any(name => !ProcessNamePattern().IsMatch(name) || name.Contains("..")) ||
policy.AllowedWindows.Any(title => string.IsNullOrWhiteSpace(title) || title.Length > 256))
throw new RunnerException("policy_invalid");
if (string.IsNullOrWhiteSpace(request.WindowTitle) || request.WindowTitle.Length > 256 ||
!policy.AllowedWindows.Contains(request.WindowTitle, StringComparer.Ordinal))
throw new RunnerException("window_not_allowed");
var currentSession = Process.GetCurrentProcess().SessionId;
if (currentSession == 0 || policy.DesktopSessionId != currentSession.ToString(CultureInfo.InvariantCulture))
throw new RunnerException("desktop_session_mismatch");
if (request.Action is null || string.IsNullOrWhiteSpace(request.Action.Kind))
throw new RunnerException("action_invalid");
}
// 与 observe 的 SelectWindow 同构:枚举本 Session 顶层窗口,精确标题 + PID 回查进程名。
// 同名同进程窗口多于一个时报 ambiguous,不任选——副作用落错对象不可接受。
private static WindowCandidate SelectWindow(string title, DesktopPolicy policy)
{
var candidates = new List<WindowCandidate>();
var session = Process.GetCurrentProcess().SessionId;
_ = NativeWindows.EnumWindows((handle, _) =>
{
if (!NativeWindows.IsWindowVisible(handle)) return true;
if (NativeWindows.Title(handle) != title) return true;
NativeWindows.GetWindowThreadProcessId(handle, out var pid);
if (pid == 0) return true;
try
{
using var process = Process.GetProcessById(checked((int)pid));
if (process.SessionId != session) return true;
var name = process.ProcessName + ".exe";
if (!policy.AllowedProcesses.Contains(name, StringComparer.OrdinalIgnoreCase))
return true;
candidates.Add(new WindowCandidate(handle, name, title));
}
catch (ArgumentException) { /* 进程已退出;旧 HWND 不能成为目标。 */ }
catch (InvalidOperationException) { /* 进程在枚举期间退出。 */ }
catch (Win32Exception) { /* 受保护进程不能读取。 */ }
return true;
}, nint.Zero);
if (candidates.Count == 0) throw new RunnerException("window_not_found");
if (candidates.Count != 1) throw new RunnerException("window_ambiguous");
return candidates[0];
}
// 动作闭集分发;RPA 驱动 windows-driver.ts 各方法的逐段移植(见 PORTING-MAP.md)。
private static void Execute(ActuateAction action, WindowCandidate window)
{
switch (action.Kind)
{
case "mouse_move":
RequirePoint(action);
if (!NativeInput.SetCursorPos(action.X!.Value, action.Y!.Value))
throw new RunnerException("input_failed");
break;
case "mouse_click":
RequirePoint(action);
if (!NativeInput.SetCursorPos(action.X!.Value, action.Y!.Value))
throw new RunnerException("input_failed");
NativeInput.Click(action.Button == "right", action.Double == true ? 2 : 1,
Math.Max(0, action.SettleMs ?? 0));
break;
case "mouse_drag":
RequirePoint(action);
if (action.ToX is null || action.ToY is null) throw new RunnerException("action_invalid");
NativeInput.DragSequence(action.Button == "right", action.X!.Value, action.Y!.Value,
action.ToX.Value, action.ToY.Value,
Math.Max(0, action.HoldMs ?? 80), Math.Max(0, action.ReleaseDelayMs ?? 80));
break;
case "mouse_scroll":
if (action.Amount is null || action.Amount == 0) throw new RunnerException("action_invalid");
NativeInput.SendWheel(action.Amount.Value);
break;
case "mouse_move_smooth":
RequirePoint(action);
SmoothMove(action);
break;
case "keyboard_send":
if (string.IsNullOrEmpty(action.Text)) throw new RunnerException("action_invalid");
// 输入必须落在已核验窗口:先抢前台(五招兜底),对应 RPA 驱动
// activateTitle 选项 + 300ms 间隙;窗口已在前台时五招直接通过。
NativeInput.FocusChain(window.Handle);
Thread.Sleep(300);
SendKeyboard(action);
break;
case "window_activate":
NativeInput.FocusChain(window.Handle);
break;
case "window_set_state":
if (action.Amount is null) throw new RunnerException("action_invalid");
_ = NativeInput.ShowWindow(window.Handle, action.Amount.Value);
break;
case "window_set_bounds":
if (action.X is null || action.Y is null ||
action.Width is null || action.Height is null ||
action.Width < 1 || action.Height < 1)
throw new RunnerException("action_invalid");
NativeInput.MoveWindowBounds(window.Handle, action.X.Value, action.Y.Value,
action.Width.Value, action.Height.Value);
break;
case "window_close":
NativeInput.CloseWindow(window.Handle);
break;
default:
throw new RunnerException("action_invalid");
}
}
private static void RequirePoint(ActuateAction action)
{
if (action.X is null || action.Y is null) throw new RunnerException("action_invalid");
}
// keyboard 三模式(RPA 驱动 KeyboardDriver.send 的移植):
// clipboard:原文进剪贴板 + Ctrl+V(不做 SendKeys 转义,适合长文本);
// chars:逐字符 SendWait(每个字符按 SendKeys 规则转义,intervalMs 控速);
// keys:整段 SendWait 原样发送(保留 ^+% 等组合语义,供按键组合类能力使用)。
private static void SendKeyboard(ActuateAction action)
{
switch (action.Mode)
{
case "clipboard":
Clipboard.SetDataObject(action.Text!, true);
Thread.Sleep(120);
SendKeys.SendWait("^v");
break;
case "chars":
var interval = Math.Max(0, action.IntervalMs ?? 30);
foreach (var character in action.Text!)
{
SendKeys.SendWait(EscapeSendKeys(character));
if (interval > 0) Thread.Sleep(interval);
}
break;
default: // "keys"
SendKeys.SendWait(action.Text!);
break;
}
}
// RPA 驱动 sendKeysEscape 的逐字符移植:SendKeys 特殊字符用花括号表示字面量。
private static string EscapeSendKeys(char character) =>
"+^%~(){}[]".Contains(character) ? $"{{{character}}}" : character.ToString();
// mouse.moveSmooth 的移植:从当前光标位置出发,按 RPA 驱动 mousePath 公式插值。
// 公式(缓入缓出 + 垂直抖动)逐行保真;JS 与 C# 的 % 对负数同为保号语义。
private static void SmoothMove(ActuateAction action)
{
var from = NativeInput.CursorPosition();
var toX = action.X!.Value;
var toY = action.Y!.Value;
var dx = toX - from.X;
var dy = toY - from.Y;
var distance = Math.Sqrt((double)dx * dx + (double)dy * dy);
if (distance < 2) return;
var stepCount = Math.Max(6, Math.Min(50, (int)Math.Round(distance / 12)));
var ease = action.Ease != false;
var speed = Math.Clamp(action.Speed ?? 35, 1, 100);
var delay = Math.Clamp((int)Math.Round(1500.0 / speed), 4, 80);
for (var i = 1; i <= stepCount; i++)
{
var t = (double)i / stepCount;
var easeFactor = ease ? (t < 0.5 ? 2 * t * t : 1 - Math.Pow(-2 * t + 2, 2) / 2) : t;
var jitter = ease ? Math.Sin(t * Math.PI) * (Math.Sin(i * 12.9898) * 43758.5453 % 1) * 3 : 0;
var x = (int)Math.Round(from.X + dx * easeFactor - (dy / distance) * jitter);
var y = (int)Math.Round(from.Y + dy * easeFactor + (dx / distance) * jitter);
if (!NativeInput.SetCursorPos(x, y)) throw new RunnerException("input_failed");
Thread.Sleep(delay);
}
if (!NativeInput.SetCursorPos(toX, toY)) throw new RunnerException("input_failed");
}
// 证据采集:与 observe 的 CaptureWindow 同构(PrintWindow → PNG → SHA-256)。
// actuate 始终留证据(副作用审计需要);PrintWindow 语义与像素有效性检查逐行保真。
private static EvidenceRef CaptureWindowEvidence(nint handle, WindowRect bounds,
string evidenceRoot, DateTimeOffset capturedAt)
{
var path = Path.Combine(evidenceRoot, $"actuate-{Guid.NewGuid():N}.png");
var stamp = capturedAt.ToString("O", CultureInfo.InvariantCulture);
try
{
using var bitmap = new Bitmap(bounds.Width, bounds.Height);
using var graphics = Graphics.FromImage(bitmap);
var deviceContext = graphics.GetHdc();
bool captured;
try { captured = NativeWindows.PrintWindow(handle, deviceContext, 2); }
finally { graphics.ReleaseHdc(deviceContext); }
if (!captured || IsBlank(bitmap)) throw new RunnerException("capture_unavailable");
bitmap.Save(path, ImageFormat.Png);
var size = new FileInfo(path).Length;
if (size < 1 || size > MaxPngBytes) throw new RunnerException("capture_size_unsupported");
using var stream = File.OpenRead(path);
var hash = Convert.ToHexString(SHA256.HashData(stream)).ToLowerInvariant();
return new EvidenceRef($"ev_{hash[..24]}", "window_snapshot",
new Uri(path).AbsoluteUri, hash, "secret", stamp);
}
catch
{
if (File.Exists(path)) File.Delete(path);
throw;
}
}
private static bool IsBlank(Bitmap bitmap)
{
var first = bitmap.GetPixel(0, 0).ToArgb();
for (var row = 0; row < 10; row++)
for (var column = 0; column < 10; column++)
if (bitmap.GetPixel(column * (bitmap.Width - 1) / 9,
row * (bitmap.Height - 1) / 9).ToArgb() != first) return false;
return true;
}
}

View File

@ -0,0 +1,212 @@
using System.Runtime.InteropServices;
namespace AgentWorker.WindowsRunner;
// 输入类 P/Invoke 独立成类:NativeWindows 保持对面"只读编译边界"原样不动。
// 本层没有进程启动接口;所有输入只作用于经策略核验后的前台窗口。
// 坐标一律物理屏幕像素——进程入口(ActuateService.EnsureDpiAwareness)声明
// Per-Monitor V2 DPI 感知,与 RPA 侧 PowerShell 驱动每脚本声明的
// SetProcessDpiAwarenessContext(-4) 语义一致,保证 SetCursorPos 全程物理像素。
// 抢前台五招兜底从 RPA 驱动 activateWindowScript 逐行移植:
// Windows 前台锁定策略会拒绝普通 SetForegroundWindow(应用点击"运行"后自身是前台),
// 五招顺序与睡眠时间是踩坑调出来的实战参数,一行语义都不允许改动。
internal static class NativeInput
{
private const uint InputMouse = 0;
private const uint MouseEventfMove = 0x0001;
private const uint MouseEventfLeftDown = 0x0002;
private const uint MouseEventfLeftUp = 0x0004;
private const uint MouseEventfRightDown = 0x0008;
private const uint MouseEventfRightUp = 0x0010;
private const uint MouseEventfWheel = 0x0800;
internal const int SwRestore = 9;
internal const int SwMinimize = 6;
internal const int SwMaximize = 3;
private const uint WmClose = 0x0010;
private const byte VkMenu = 0x12; // ALT
private const uint KeyeventfKeyup = 0x0002;
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool SetCursorPos(int x, int y);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetCursorPos(out Point point);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool SetForegroundWindow(nint handle);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool ShowWindow(nint handle, int command);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool IsIconic(nint handle);
[DllImport("user32.dll")]
internal static extern void keybd_event(byte virtualKey, byte scanCode, uint flags, nint extraInfo);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool AttachThreadInput(uint currentThread, uint targetThread, bool attach);
[DllImport("user32.dll")]
private static extern void SwitchToThisWindow(nint handle, bool altTab);
[DllImport("kernel32.dll")]
private static extern uint GetCurrentThreadId();
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetProcessDpiAwarenessContext(nint value);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetWindowPos(nint handle, nint after, int x, int y,
int width, int height, uint flags);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool PostMessage(nint handle, uint message, nint wParam, nint lParam);
[DllImport("user32.dll", SetLastError = true)]
private static extern uint SendInput(uint count, Input[] inputs, int size);
[StructLayout(LayoutKind.Sequential)]
internal struct Point { public int X; public int Y; }
[StructLayout(LayoutKind.Sequential)]
private struct Input
{
public uint Type;
public InputUnion Union;
}
[StructLayout(LayoutKind.Explicit)]
private struct InputUnion
{
[FieldOffset(0)] public MouseInput Mouse;
}
[StructLayout(LayoutKind.Sequential)]
private struct MouseInput
{
public int Dx;
public int Dy;
public uint MouseData;
public uint Flags;
public uint Time;
public nint ExtraInfo;
}
// DPI 感知声明是进程级一次性设置;失败(如已被宿主先行声明)不影响后续调用。
internal static void EnsureDpiAwareness()
{
try { _ = SetProcessDpiAwarenessContext(new nint(-4)); }
catch { /* 旧系统无此 API 时按物理像素继续;对面 CoordinateSpace 以 GetDpiForWindow 实测为准。 */ }
}
// SendInput 批量提交鼠标标志组合;单次失败按 input_failed 处理由上层转稳定错误码。
internal static void SendMouse(uint flags, uint data)
{
var input = new Input
{
Type = InputMouse,
Union = new InputUnion
{
Mouse = new MouseInput { Dx = 0, Dy = 0, MouseData = data, Flags = flags }
}
};
if (SendInput(1, [input], Marshal.SizeOf<Input>()) != 1)
throw new RunnerException("input_failed");
}
internal static void SendWheel(int amount) =>
SendMouse(MouseEventfWheel, unchecked((uint)amount));
internal static void Click(bool right, int repeats, int settleMs)
{
var down = right ? MouseEventfRightDown : MouseEventfLeftDown;
var up = right ? MouseEventfRightUp : MouseEventfLeftUp;
for (var i = 0; i < repeats; i++)
{
SendMouse(down, 0);
SendMouse(up, 0);
if (settleMs > 0) Thread.Sleep(settleMs);
}
}
internal static Point CursorPosition()
{
if (!GetCursorPos(out var point)) throw new RunnerException("input_failed");
return point;
}
// 窗口拖拽时序:按下→hold→移动→release→抬起,与 RPA 驱动逐段一致。
internal static void DragSequence(bool right, int fromX, int fromY, int toX, int toY,
int holdMs, int releaseDelayMs)
{
var down = right ? MouseEventfRightDown : MouseEventfLeftDown;
var up = right ? MouseEventfRightUp : MouseEventfLeftUp;
if (!SetCursorPos(fromX, fromY)) throw new RunnerException("input_failed");
SendMouse(down, 0);
if (holdMs > 0) Thread.Sleep(holdMs);
if (!SetCursorPos(toX, toY)) throw new RunnerException("input_failed");
if (releaseDelayMs > 0) Thread.Sleep(releaseDelayMs);
SendMouse(up, 0);
}
// 抢前台五招兜底(RPA 驱动 activateWindowScript 的逐行移植):
// ①最小化则恢复 +150ms ②SwitchToThisWindow +150ms ③ALT 借位 + SetForegroundWindow +200ms
// ④AttachThreadInput 挂到前台线程 + SetForegroundWindow +200ms ⑤最小化再恢复 +450ms
// 五招全失败抛 focus_failed——对应 PS 版 RPA_ERROR '无法把窗口带到前台'。
internal static void FocusChain(nint handle)
{
if (IsIconic(handle))
{
_ = ShowWindow(handle, SwRestore);
Thread.Sleep(150);
}
if (NativeWindows.GetForegroundWindow() != handle)
{
SwitchToThisWindow(handle, true);
Thread.Sleep(150);
}
if (NativeWindows.GetForegroundWindow() != handle)
{
keybd_event(VkMenu, 0, 0, nint.Zero);
_ = SetForegroundWindow(handle);
keybd_event(VkMenu, 0, KeyeventfKeyup, nint.Zero);
Thread.Sleep(200);
}
if (NativeWindows.GetForegroundWindow() != handle)
{
// 返回值即前台窗口的线程号;PID 出参不需要。
var foregroundThread = NativeWindows.GetWindowThreadProcessId(
NativeWindows.GetForegroundWindow(), out _);
var currentThread = GetCurrentThreadId();
var attached = AttachThreadInput(currentThread, foregroundThread, true);
try { _ = SetForegroundWindow(handle); }
finally { if (attached) _ = AttachThreadInput(currentThread, foregroundThread, false); }
Thread.Sleep(200);
}
if (NativeWindows.GetForegroundWindow() != handle)
{
_ = ShowWindow(handle, SwMinimize);
Thread.Sleep(150);
_ = ShowWindow(handle, SwRestore);
Thread.Sleep(300);
}
if (NativeWindows.GetForegroundWindow() != handle)
throw new RunnerException("focus_failed");
}
internal static void MoveWindowBounds(nint handle, int x, int y, int width, int height) =>
_ = SetWindowPos(handle, nint.Zero, x, y, width, height, 4); // flags=4 SWP_NOZORDER,与 RPA 驱动一致
internal static void CloseWindow(nint handle) =>
_ = PostMessage(handle, WmClose, nint.Zero, nint.Zero);
}

View File

@ -339,7 +339,7 @@
| ADR | 问题 | 决定 |
|---|---|---|
| R1 | Registry 作用域:backend 是否多租户并存 | **全局一份能力清单**(菜单所有租户共用),权限隔离完全由 capability-gateway 按租户管;长期将清单挂到 engine 实例(已从对面源码确认 WorkflowIdentity 带 tenantId,但清单本身无需按租户复制) |
| R3 | 桌面执行安全模型:RPA PowerShell 驱动 vs 对面"受信 C# 原生进程"红线 | **C# 扩展 actuate**:把 RPA 五域执行逻辑移植进 windows-runner 原生进程;PowerShell 通道仅限本 demo,不进宿主 |
| R3 | 桌面执行安全模型:RPA PowerShell 驱动 vs 对面"受信 C# 原生进程"红线 | **C# 扩展 actuate**:把 RPA 五域执行逻辑移植进 windows-runner 原生进程;PowerShell 通道仅限本 demo,不进宿主。**原型已产出并编译验证(2026-11-06)**:`contrib/windows-runner-actuate/` 补丁包(协议扩展 + NativeInput + ActuateService + 逐原语移植对照表 + 合入指引含门禁合规清单);对面 csproj 原样合编 0 警告 0 错误(.NET 10.0.403),协议冒烟 5 例通过(安全门错误码/双路分发/observe 无回归);对面 README Windows 行"click/type 或随本轮交付"与该路线吻合 |
| R4 | 浏览器双路径 | **按场景分工**:RPA 场景走咱们的 CDP 接管路线(复用用户登录态/真实页面——RPA 核心价值);对面 PlaywrightBrowserRunner 留给 AI 智能体场景(无头自管浏览器)。约束:①咱们 CDP 路线必须接对面 BrowserContextPolicy 白名单与证据体系,不自建;②迁移前修完 transport 超时与定位器不统一两缺陷;③需对面在插件清单给咱们的 runner 备案(rpa.browser) |
| R5 | 包消费方式 | **源码并入 + 锁版本**:git subtree 进对面仓库统一编译,Bun 锁 1.4.3(与对面全家 workspace:* 源码消费现状一致) |
| R6 | 部署拓扑:rpa-core 各域跑在哪 | **按能力域拆分部署**:纯 Node 域→backend plugin-worker;桌面/视觉/弹窗域→用户电脑 Electron 宿主;同一包按 PLATFORM_GROUPS 分组加载,不维护两份代码 |

View File

@ -15114,6 +15114,10 @@
"value": "edge"
}
]
},
"port": {
"defaultValue": "9222",
"description": "输入远程连接计算机的端口号,若未单独设置端口号默认为 9222。"
}
}
},
@ -15410,6 +15414,10 @@
"value": "edge"
}
]
},
"port": {
"defaultValue": "9222",
"description": "输入远程连接计算机的端口号,若未单独设置端口号默认为 9222。"
}
}
},
@ -15666,6 +15674,10 @@
"value": "edge"
}
]
},
"port": {
"defaultValue": "9222",
"description": "输入远程连接计算机的端口号,若未单独设置端口号默认为 9222。"
}
}
},