From e23788d7176bf6b54bd499bc946d3ad5128e18bf Mon Sep 17 00:00:00 2001 From: jkooe <62494645+jkooe@users.noreply.github.com> Date: Sat, 10 Oct 2026 14:27:12 +0800 Subject: [PATCH] =?UTF-8?q?refactor(versioning):=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E5=8F=B7=20=E2=80=94=E2=80=94=20semver=20?= =?UTF-8?q?=E4=B8=BA=E5=94=AF=E4=B8=80=E7=89=88=E6=9C=AC=E5=8F=B7=EF=BC=8C?= =?UTF-8?q?=E5=BC=80=E5=8F=91=E6=89=B9=E6=AC=A1=E4=BB=A3=E5=8F=B7=E5=B9=B6?= =?UTF-8?q?=E5=85=A5=E5=85=B6=E5=BD=92=E5=B1=9E=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 问题(皇上指出) 项目里两套编号并行,无法判断哪个是"当前版本": - **发布版本**(semver):v1.1.0 ~ v1.6.0、v2.0.0 - **开发批次代号**:v1 ~ v26(README 路线表 / 代码注释 / 文档) 时间线证据:v1.1.0 首发(09-29)时批次已推进到 v12.4 —— **二者从来不是一对一**(26 批次只发过 10 版)。 ## 规则(新增 docs/VERSIONING.md §0 版本号唯一性规范) - **唯一版本号 = semver `vX.Y.Z`**,两个来源必须永远一致:git tag 与 package.json(构建期注入 agent) - **批次代号不是版本号**,不再单独出现;需要区分同版内多批改动时用**功能名** - 附「批次 → 归属发布版本」映射表(按 tag 时间线与功能内容推定,并注明以 CHANGELOG 为准) ## 统一动作 1. **批次 v23 ~ v26 → v2.0.0**(22 文件 / 53 处)—— 它们确实属于 v2.0.0 2. **臣的批次号 v1.7 / v1.8 → v2.0.0**(27 文件)—— 未单独发版 3. **简写补全三段式**:v1.5 → v1.5.0、v1.6 → v1.6.0 4. **README 路线表**:`**v1**` → `**批次 1**`;表头加「批次 ≠ 版本号」说明并链到 §0; 补齐批次 16~29 与 **v2.0.0 发布行**;删掉过时的「待办」行(提到的分支早已删除) 5. `docs/VERSIONING.md` 里**故意保留** 3 处批次号 —— 那是规范说明本身 ## 保留项(真实发布版本,不动) v1.1.0 / v1.2.1 / v1.3.0 / v1.4.0~v1.4.3 / v1.5.0 / v1.6.0 / v2.0.0(10 个 tag) ## 验证 - 版本号一致性:package.json = tag = 8 个 workspace = tauri.conf.json = **2.0.0** ✓ - v1.7/v1.8/v23~v26 全仓残留 **3 处**(均在 VERSIONING.md 规范说明中,属预期) - 7 包构建 · 单测 273 · e2e 30 · ps1-encoding 24 全绿 --- CHANGELOG.md | 10 +-- README.md | 62 ++++++++++--------- apps/agent/src/audit.ts | 4 +- apps/agent/src/capabilities/await.ts | 10 +-- apps/agent/src/capabilities/index.ts | 6 +- apps/agent/src/capabilities/input-script.ts | 8 +-- apps/agent/src/capabilities/input.ts | 10 +-- apps/agent/src/capabilities/log.ts | 2 +- apps/agent/src/capabilities/monitor.ts | 2 +- apps/agent/src/capabilities/system.ts | 4 +- apps/agent/src/capabilities/task.ts | 2 +- .../src/capabilities/win-ps/prelude-uia.ts | 2 +- apps/agent/src/capabilities/window.ts | 8 +-- apps/agent/src/config.ts | 2 +- apps/agent/src/events.ts | 2 +- apps/agent/src/server.ts | 20 +++--- apps/cli/src/cmd/gui.ts | 8 +-- apps/cli/src/cmd/ops.ts | 4 +- apps/cli/src/cmd/system.ts | 2 +- apps/cli/src/help.ts | 6 +- apps/cli/src/types.ts | 12 ++-- apps/mcp/src/index.ts | 2 +- apps/mcp/src/tools/defs.ts | 10 +-- docs/DEVELOPMENT.md | 22 +++---- docs/REMOTE-LIMITS.md | 2 +- docs/RETROSPECTIVE.md | 8 +-- docs/ROADMAP.md | 18 +++--- docs/SECURITY.md | 14 ++--- docs/VERSIONING.md | 44 +++++++++++++ .../src/capabilities/manifest/graphics.ts | 16 ++--- .../src/capabilities/manifest/system.ts | 16 ++--- packages/protocol/src/hotkeys.ts | 2 +- scripts/install.cmd | 2 +- scripts/install.ps1 | 8 +-- tests/unit/audit-anchor.test.mjs | 2 +- tests/unit/capabilities.test.mjs | 4 +- tests/unit/gui-await.test.mjs | 6 +- tests/unit/install-zerotrust.test.mjs | 2 +- tests/unit/log-query.test.mjs | 2 +- tests/unit/monitor.test.mjs | 2 +- tests/unit/security-hardening.test.mjs | 4 +- 41 files changed, 211 insertions(+), 161 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e9b8b6..03d6da8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,10 +14,10 @@ > **兼容性**:完全向后兼容 —— 未改任何既有能力的参数名或语义,未新增必填参数, > 返回结构只增不减(遵循 `docs/VERSIONING.md` 的四条铁律)。 > -> **已知限制**:本版发布时被控端整机离线,`v1.5` 的预设别名/长按/后台投递/热键序列 -> 与 `v26` 的自重启任务清理策略**尚未经真机验证**(后者已在本地单测锁定判定条件)。 +> **已知限制**:本版发布时被控端整机离线,`v1.5.0` 的预设别名/长按/后台投递/热键序列 +> 与 `v2.0.0` 的自重启任务清理策略**尚未经真机验证**(后者已在本地单测锁定判定条件)。 -### 新增:语义可断言(v1.6 / v1.7) +### 新增:语义可断言(v1.6.0 / v2.0.0) - **`gui.await`** —— 等条件成立再返回。四类条件(`window` / `control` / `process` / `file`) × 两种状态(`present` / `absent`),复用既有能力,零新增平台探测代码。 @@ -31,7 +31,7 @@ 例:`[{window:"安装完成"},{control:"错误"}]` = 「装成功或报错,先出现的算」。 - CLI:`nodeagent await`;MCP:`na_await`。 -### 新增:状态可回看(v1.8) +### 新增:状态可回看(v2.0.0) - **`log.query`** —— 日志**在被控端侧过滤**后只回匹配行(`pattern` / `level` / `since` / `offset` / `limit` / `tail`)。流式逐行读,几万行日志也不整份拉回。 @@ -40,7 +40,7 @@ `metric`(CPU·内存)。**摘要直接给结论**:断了几次、最长断多久、值域。 - CLI:`nodeagent monitor` / `log`;MCP:`na_monitor` / `na_log`。 -### 新增:安全加固(v23 / v24 / v25) +### 新增:安全加固(v2.0.0) - **连接层三防护**: - 握手失败封禁 —— 连续失败达阈值 → 指数退避(封顶 24h),成功即清零,支持白名单豁免 diff --git a/README.md b/README.md index b7fd7da..8c74482 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ nodeagent 把一台 Windows 机器的能力**标准化成一组可授权、可 | **状态采样** | `log.query` · `monitor.start` · `monitor.report` · `monitor.stop` · `monitor.list` · `monitor.delete` | 日志**在被控端侧过滤**(不上传整份)/ 定时采样落盘 + **摘要回看**(断几次、最长断多久、CPU/内存值域) | | **审计锚定** | `system.audit.head` · `system.audit.anchor` | 链头可读 + 锚点**追加**落盘,让「有 root 者整链重写」可被发现 | -## 快捷键(hotkey)控制 —— v1.5 +## 快捷键(hotkey)控制 —— v1.5.0 `input.key.press` 支持 **4 种输入形式**(一次只用一种): @@ -84,7 +84,7 @@ OEM 符号键(`oem_plus`/`oem_comma`/`oem_period`… → 支持 `win+d`、`ctr > MCP 侧同名工具为 `na_key`(`action: "press"`),支持同一套参数。 -## 等待条件 gui.await —— v1.6 +## 等待条件 gui.await —— v1.6.0 GUI 操作从「按一下→睡几秒→截图碰运气」变成**可断言**流程: @@ -105,7 +105,7 @@ nodeagent --node win invoke gui.await --args '{"condition":"file","path":"C:\\lo **典型闭环**:`await control(text:"立即安装")` → `mouse.click(x, y)` → `await control(text:"完成", timeout_ms:60000)` → `click` → `await absent(text:"安装中")`。 -### 属性谓词与组合(v1.7) +### 属性谓词与组合(v2.0.0) `screen.find` 现在返回 UIA 属性,`gui.await` 可据此等**状态**而不只是「出现」: @@ -130,7 +130,7 @@ nodeagent --node win await --any_of '[{"condition":"window","title":"安装完 --- -## 状态采样:日志过滤与定时采样 —— v1.8 +## 状态采样:日志过滤与定时采样 —— v2.0.0 治**间歇性**问题(代理不通、杀软拦截、端口时开时闭)—— `system.status` 只能看当下快照, 而 `event.watch` 只解决「有事件时通知」,没有「**持续记录并回看**」。 @@ -151,7 +151,7 @@ nodeagent --node win monitor stop probe --- -## 审计链外部锚定 —— v25 +## 审计链外部锚定 —— v2.0.0 链内哈希只能发现「改一条」。**有 root 的攻击者可以整链重写** —— 从零重建一份自洽的假日志, `audit.verify` 照样通过。外部锚定是唯一能戳破它的手段:把链头记到**链外**。 @@ -611,30 +611,36 @@ nodeagent/ ## 路线图 -| 阶段 | 目标 | 状态 | +> 下表「**批次**」是**开发阶段的内部代号**(一批相关改动的集合),**不是版本号** —— +> 项目的唯一版本号是 semver(见 `git tag` / `package.json`)。批次与发布版本的对应关系 +> 及书写规范见 [`docs/VERSIONING.md`](docs/VERSIONING.md) §0。 + +| 阶段(开发批次) | 目标 | 状态 | |---|---|---| -| **v1** | 命令级接管 + 装软件 + 查状态 + 基本鉴权 + CLI + MCP | ✅ | -| **v2** | 图形接管(`screen.*` 截屏 + `input.*` 键鼠)+ 输入控制开关 | ✅ | -| **v3** | 零信任:Ed25519 身份认证 + 能力级 ACL | ✅ | -| **v3+** | 审计日志(留痕 + 查询)+ 调用限速 | ✅ | -| **v4** | 无感体验:局域网自动发现 + 断线自动重连 | ✅ | -| **v5** | 多设备管理 + 文件传输(分块 / 大文件 / 原子写) | ✅ | -| **v6** | Hub 中转:跨网段 / 公网接入(被控端主动外连,穿 NAT) | ✅ | -| **v7** | **自持**:受控重启(配置变更后让自身生效,不再丢连接) | ✅ | -| **v8** | **GUI 语义**:`window.list` / `window.focus` / `screen.find`(UIA 按名取坐标) | ✅ | -| **v9** | `screen.find` **OCR 兜底**(自绘 UI 也能定位)+ MCP 工具同步 | ✅ | -| **v10** | **长任务异步化**(`system.task.*`)+ 剪贴板 + CLI 常驻连接池 daemon | ✅ | -| **v11** | **安全加固**:私钥入系统密钥库(Keychain/DPAPI)+ 审计链防篡改 + ACL 细化(IP/时段/按能力限速) | ✅ | -| **v11+** | **操作面补完**:鼠标拖拽 / 录屏 / 剪贴板图片 / 多设备并发 / 一键升级 | ✅ | -| **v12** | **事件订阅**(file/process/net 主动推送)+ **GUI 宏**(步骤序列回放)+ **Hub 并发多控制端** + 设备分组 | ✅ | -| **v12.1** | **Hub 侧按控制端授权**(多令牌 + 按设备白/黑名单 + 注册准入) | ✅ | -| **v12.2** | GUI **等待/重试语义**(`wait_ms`)—— 界面加载/动画不再假失败 | ✅ | -| **v12.3** | **macOS 端 GUI 对齐**:AppleScript 窗口枚举/聚焦 + Vision OCR 元素定位(含 Retina 坐标换算) | ✅ | -| **v12.4** | **事件直达 MCP 宿主**(实时推送 + 拉取双通路) | ✅ | -| **v13** | **成功指标聚合**(PRD 2.2 四项)+ 依真实数据修正口径(时延分层) | ✅ | -| **v14** | **PowerShell 常驻助手** —— GUI 能力提速 4~25 倍(预加载 C#/UIA,免每次现编译) | ✅ | -| **v15** | `screen.find` **图像模板匹配**(纯图标/无文字控件盲区)+ 一堆真机根因修复 | ✅ | -| **待办** | CI 单元测试步骤已就绪(分支 `ci/workflow-unit-tests`,**需 `gh auth refresh -s workflow` 授权后推送**);GUI 图像锚点匹配(自绘 UI 更稳) | 🚧 | +| **批次 1** | 命令级接管 + 装软件 + 查状态 + 基本鉴权 + CLI + MCP | ✅ | +| **批次 2** | 图形接管(`screen.*` 截屏 + `input.*` 键鼠)+ 输入控制开关 | ✅ | +| **批次 3** | 零信任:Ed25519 身份认证 + 能力级 ACL | ✅ | +| **批次 3+** | 审计日志(留痕 + 查询)+ 调用限速 | ✅ | +| **批次 4** | 无感体验:局域网自动发现 + 断线自动重连 | ✅ | +| **批次 5** | 多设备管理 + 文件传输(分块 / 大文件 / 原子写) | ✅ | +| **批次 6** | Hub 中转:跨网段 / 公网接入(被控端主动外连,穿 NAT) | ✅ | +| **批次 7** | **自持**:受控重启(配置变更后让自身生效,不再丢连接) | ✅ | +| **批次 8** | **GUI 语义**:`window.list` / `window.focus` / `screen.find`(UIA 按名取坐标) | ✅ | +| **批次 9** | `screen.find` **OCR 兜底**(自绘 UI 也能定位)+ MCP 工具同步 | ✅ | +| **批次 10** | **长任务异步化**(`system.task.*`)+ 剪贴板 + CLI 常驻连接池 daemon | ✅ | +| **批次 11** | **安全加固**:私钥入系统密钥库(Keychain/DPAPI)+ 审计链防篡改 + ACL 细化(IP/时段/按能力限速) | ✅ | +| **批次 11+** | **操作面补完**:鼠标拖拽 / 录屏 / 剪贴板图片 / 多设备并发 / 一键升级 | ✅ | +| **批次 12** | **事件订阅**(file/process/net 主动推送)+ **GUI 宏**(步骤序列回放)+ **Hub 并发多控制端** + 设备分组 | ✅ | +| **批次 12.1** | **Hub 侧按控制端授权**(多令牌 + 按设备白/黑名单 + 注册准入) | ✅ | +| **批次 12.2** | GUI **等待/重试语义**(`wait_ms`)—— 界面加载/动画不再假失败 | ✅ | +| **批次 12.3** | **macOS 端 GUI 对齐**:AppleScript 窗口枚举/聚焦 + Vision OCR 元素定位(含 Retina 坐标换算) | ✅ | +| **批次 12.4** | **事件直达 MCP 宿主**(实时推送 + 拉取双通路) | ✅ | +| **批次 13** | **成功指标聚合**(PRD 2.2 四项)+ 依真实数据修正口径(时延分层) | ✅ | +| **批次 14** | **PowerShell 常驻助手** —— GUI 能力提速 4~25 倍(预加载 C#/UIA,免每次现编译) | ✅ | +| **批次 15** | `screen.find` **图像模板匹配**(纯图标/无文字控件盲区)+ 一堆真机根因修复 | ✅ | +| **批次 16 ~ 26** | 网络两阶段提交 · 音频控制 · 构建指纹 · 自更新 · 证书钉住 · 每客户端 PSK · 连接层三防护 · 零信任安装 · 审计锚定 | ✅ | +| **批次 27 ~ 29** | **语义可断言**(`gui.await` + UIA 属性 + `where` 谓词 + `any_of`)· **状态可回看**(`log.query` + `monitor.*`) | ✅ | +| **发布** | **v2.0.0**(2026-10-10):51 项能力 · 273 单测 · Windows/Rust CI 全绿 | ✅ 已发布 | ## 文档 diff --git a/apps/agent/src/audit.ts b/apps/agent/src/audit.ts index ccc8ab9..bd15401 100644 --- a/apps/agent/src/audit.ts +++ b/apps/agent/src/audit.ts @@ -18,7 +18,7 @@ export type AuditType = | 'net.deny' /** v19:agent 自更新(拉取式) */ | 'agent.update' - /** v23:连接空闲被主动断开 */ + /** v2.0.0:连接空闲被主动断开 */ | 'session.idle_close'; /** 一条审计记录(JSONL 单行)。 */ @@ -336,7 +336,7 @@ export function readAudit(query: AuditQuery = {}): { entries: AuditEntry[]; tota } -// ---------------- v25 审计链外部锚定 ---------------- +// ---------------- v2.0.0 审计链外部锚定 ---------------- // // ## 为什么需要 // 审计链的哈希使**链内**篡改可被发现(改一条就要重算其后全部)。但「有 root 的攻击者」 diff --git a/apps/agent/src/capabilities/await.ts b/apps/agent/src/capabilities/await.ts index 79b5b90..db07cf8 100644 --- a/apps/agent/src/capabilities/await.ts +++ b/apps/agent/src/capabilities/await.ts @@ -1,5 +1,5 @@ /** - * gui.await —— 等待条件成立(v1.6)。 + * gui.await —— 等待条件成立(v1.6.0)。 * * ## 为什么需要它(复盘三大短板之「语义」) * 原来的 GUI 操作是**盲试**:按一下、截个图、看看有没有变化,不行再试。 @@ -63,7 +63,7 @@ function buildInnerArgs(condition: Condition, args: Args): Args { for (const k of ['text', 'window', 'control_type', 'method', 'template', 'limit', 'timeout_ms']) { if (args[k] !== undefined) inner[k] = args[k]; } - // v1.7:属性谓词(仅 UIA 引擎生效)。有 where 时允许不传 text —— + // v2.0.0:属性谓词(仅 UIA 引擎生效)。有 where 时允许不传 text —— // 「等一个 enabled 的按钮」比「等一个叫某名字的按钮」更贴近真实意图。 const where = args['where']; if (where !== undefined) { @@ -117,7 +117,7 @@ function innerProbe(condition: Condition): (args: Args) => Promise { } /** - * v1.7 组合条件:`any_of: [ {...}, {...} ]`,任一命中原句即算命中。 + * v2.0.0 组合条件:`any_of: [ {...}, {...} ]`,任一命中原句即算命中。 * * 为什么只做 any_of 不做 and/or/not generalize:真实场景里"弹窗出现 **且** * 其中某按钮 enabled"用 any_of(弹窗) + where(按钮 enabled) 两步就能表达, @@ -152,7 +152,7 @@ function buildAnyOf(args: Args): Array<{ condition: Condition; inner: Args; labe } export async function guiAwait(args: Args): Promise { - // v1.7:有 any_of 时**不要求**顶层 condition(语义就是"这组条件任一命中"), + // v2.0.0:有 any_of 时**不要求**顶层 condition(语义就是"这组条件任一命中"), // 校验顺序也因此调整为 any_of 先行 —— 否则会先撞上"condition 缺失"的报错。 const condition = args['condition'] as Condition | undefined; if (!args['any_of'] && (!condition || !CONDITIONS.includes(condition))) { @@ -175,7 +175,7 @@ export async function guiAwait(args: Args): Promise { let lastHit = false; let lastError: string | null = null; - // v1.7:组合条件走单独路径(任一命中即算),失败语义与单条完全一致 + // v2.0.0:组合条件走单独路径(任一命中即算),失败语义与单条完全一致 const anyOf = args['any_of'] !== undefined ? buildAnyOf(args) : null; if (anyOf) { for (;;) { diff --git a/apps/agent/src/capabilities/index.ts b/apps/agent/src/capabilities/index.ts index a193632..20e2679 100644 --- a/apps/agent/src/capabilities/index.ts +++ b/apps/agent/src/capabilities/index.ts @@ -54,9 +54,9 @@ export function createCapabilityRegistry(cfg: AgentConfig): Record = { launch_mail: 0xb4, launch_media: 0xb5, launch_app1: 0xb6, launch_app2: 0xb7, browser_favorites: 0xab, browser_home: 0xac, - // ---- 左右侧修饰键(v1.5)---- + // ---- 左右侧修饰键(v1.5.0)---- // 游戏/部分应用区分左右(如游戏中把 LAlt 设为跑步、RAlt 设为瞄准),故不能只给一个 alt。 lctrl: 0xa2, rctrl: 0xa3, lshift: 0xa0, rshift: 0xa1, lalt: 0xa4, ralt: 0xa5, lwin: 0x5b, rwin: 0x5c, super: 0x5b, cmd: 0x5b, left_cmd: 0x5b, right_cmd: 0x5c, - // ---- 小键盘(v1.5)---- + // ---- 小键盘(v1.5.0)---- // numpadN 与数字 N 分开:Excel 等程序靠 NumLock 状态区分二者行为,合成一个会坏掉快捷键。 numpad0: 0x60, numpad1: 0x61, numpad2: 0x62, numpad3: 0x63, numpad4: 0x64, numpad5: 0x65, numpad6: 0x66, numpad7: 0x67, numpad8: 0x68, numpad9: 0x69, @@ -59,7 +59,7 @@ export const VK_MAP: Record = { numlock: 0x90, scrolllock: 0x91, numpad_enter: 0x0d, - // ---- OEM 符号键(v1.5,美式布局 VK 码)---- + // ---- OEM 符号键(v1.5.0,美式布局 VK 码)---- // 必备理由:大量系统/应用快捷键就是"修饰键 + 符号键"(win+d 显示桌面、win+, 速览、 // ctrl++ 放大、ctrl+- 缩小、win+. 表情面板),此前只能靠字母数字,覆盖不到。 oem_1: 0xba, // : ; @@ -75,7 +75,7 @@ export const VK_MAP: Record = { oem_comma: 0xbc, // , < oem_period: 0xbe,// . > - // ---- 系统/IME 杂项(v1.5)---- + // ---- 系统/IME 杂项(v1.5.0)---- sleep: 0x5f, help: 0x2f, select: 0x29, execute: 0x2b, clear: 0x0c, separator: 0x6c, oem_clear: 0xfe, kana: 0x15, hangul: 0x15, kanji: 0x19, hanja: 0x19, diff --git a/apps/agent/src/capabilities/input.ts b/apps/agent/src/capabilities/input.ts index b4d17c3..e469780 100644 --- a/apps/agent/src/capabilities/input.ts +++ b/apps/agent/src/capabilities/input.ts @@ -183,7 +183,7 @@ public static class NAInput { } /// - /// v1.5:向目标进程的**全部顶层窗口** PostMessage 键盘消息(后端定向快捷键投放)。 + /// v1.5.0:向目标进程的**全部顶层窗口** PostMessage 键盘消息(后端定向快捷键投放)。 /// /// 场景:目标窗口不在前台(被遮挡 / 最小化 / 聊天窗口在后台),又要给它发快捷键。 /// WM_KEYDOWN=0x0100,WM_KEYUP=0x0101;修饰键按顺序投递,调用方负责释放顺序。 @@ -364,7 +364,7 @@ export async function keyPress(args: Args): Promise { assertAllowed('input.key.press'); requireWindows('input.key.press'); - // v1.5:入参统一经 protocol 的 expandChords 展开 —— 字符串热键 / 预设名 / 老数组形式 + // v1.5.0:入参统一经 protocol 的 expandChords 展开 —— 字符串热键 / 预设名 / 老数组形式 // 全部收敛成「归一化和弦序列」。混用多种形式会直接报错,避免"以谁为准"的歧义。 let expanded; try { @@ -384,9 +384,9 @@ export async function keyPress(args: Args): Promise { const presets = expanded.presets; const gap = Math.max(0, Math.min(2000, (args['interval_ms'] as number) ?? 40)); - // v1.5:长按(按下到释放的保持时长),用于"长按音量/持续按住方向键"等场景 + // v1.5.0:长按(按下到释放的保持时长),用于"长按音量/持续按住方向键"等场景 const holdMs = Math.max(0, Math.min(5000, Math.round((args['hold_ms'] as number | undefined) ?? 0))); - // v1.5:投放路由。foreground = SendInput 注入当前焦点(默认,旧行为); + // v1.5.0:投放路由。foreground = SendInput 注入当前焦点(默认,旧行为); // post = 向目标进程全部顶层窗口 PostMessage(窗口在后台时可用,但兼容性差) const route = (args['route'] as string | undefined) ?? 'foreground'; if (route !== 'foreground' && route !== 'post') { @@ -423,7 +423,7 @@ export async function keyPress(args: Args): Promise { }; } - // v1.5:后端定向投递(route=post)—— 目标窗口不在前台时使用 + // v1.5.0:后端定向投递(route=post)—— 目标窗口不在前台时使用 if (route === 'post') { let sent = 0; const out = await runPs(buildPostBody(chords, { gapMs: gap, holdMs, targetPid: pid, route })); diff --git a/apps/agent/src/capabilities/log.ts b/apps/agent/src/capabilities/log.ts index 4e43e30..693b250 100644 --- a/apps/agent/src/capabilities/log.ts +++ b/apps/agent/src/capabilities/log.ts @@ -1,5 +1,5 @@ /** - * log.query(v1.8,方向二「状态」第一块)。 + * log.query(v2.0.0,方向二「状态」第一块)。 * * ## 为什么必须有 * 治间歇性问题(代理不通、杀软拦截、端口时开时闭)要看**历史**,而被控端日志常在 diff --git a/apps/agent/src/capabilities/monitor.ts b/apps/agent/src/capabilities/monitor.ts index 758a60c..9c1db90 100644 --- a/apps/agent/src/capabilities/monitor.ts +++ b/apps/agent/src/capabilities/monitor.ts @@ -1,5 +1,5 @@ /** - * monitor.*(v1.8,方向二「状态」第二块)—— 定时采样落盘与回看。 + * monitor.*(v2.0.0,方向二「状态」第二块)—— 定时采样落盘与回看。 * * ## 为什么需要(复盘 §四) * 代理不通、杀软拦截、端口时开时闭都是**间歇性**的,而 `system.status` 只能看当下快照。 diff --git a/apps/agent/src/capabilities/system.ts b/apps/agent/src/capabilities/system.ts index 566d105..112171e 100644 --- a/apps/agent/src/capabilities/system.ts +++ b/apps/agent/src/capabilities/system.ts @@ -616,7 +616,7 @@ export async function auditVerify(_args: Args): Promise { } /** - * v25:返回审计链的链头(只读)。 + * v2.0.0:返回审计链的链头(只读)。 * * 用途:控制端定期拉取并存到**链外**(Mac 本地 / 另一台机器)—— 这是"外部锚定"的一半。 * compare=true 时附带与最近一条锚点的比对结论(另一半)。 @@ -628,7 +628,7 @@ export async function auditHead(args: Args): Promise { } /** - * v25:把当前链头**追加**写到锚点文件(默认 <数据目录>/audit-anchors.jsonl)。 + * v2.0.0:把当前链头**追加**写到锚点文件(默认 <数据目录>/audit-anchors.jsonl)。 * * 为什么是追加而不是覆盖:历史锚点一旦写成就不可被后续覆盖,否则攻击者重写链后 * 再"刷新"锚点即可抹掉痕迹。追加式让每次锚定都留痕。 diff --git a/apps/agent/src/capabilities/task.ts b/apps/agent/src/capabilities/task.ts index 084d8fb..8e1e735 100644 --- a/apps/agent/src/capabilities/task.ts +++ b/apps/agent/src/capabilities/task.ts @@ -132,7 +132,7 @@ function getTask(id: string): TaskRecord { // ---------------- system.task.list ---------------- -/** v23:正在跑的任务数(连接层"有空闲断开但不动长任务"的判据)。 */ +/** v2.0.0:正在跑的任务数(连接层"有空闲断开但不动长任务"的判据)。 */ export function runningTaskCount(): number { let n = 0; for (const t of tasks.values()) if (t.state === 'running') n += 1; diff --git a/apps/agent/src/capabilities/win-ps/prelude-uia.ts b/apps/agent/src/capabilities/win-ps/prelude-uia.ts index 2c3b6bc..8ed605a 100644 --- a/apps/agent/src/capabilities/win-ps/prelude-uia.ts +++ b/apps/agent/src/capabilities/win-ps/prelude-uia.ts @@ -32,7 +32,7 @@ function Get-WinByTitle([string]$re) { # v14:按子串在给定 scope 内遍历查找(FindAll + 子串过滤)。 # UIA 的 PropertyCondition 只能精确匹配,故必须遍历后过滤;遍历规模设上限防超大 UI 树卡死。 -# v23:读取元素的 UIA 属性(v1.7 语义属性化)。 +# v2.0.0:读取元素的 UIA 属性(v2.0.0 语义属性化)。 # 为什么逐个 try:ValuePattern / SelectionItemPattern / TogglePattern 并非所有控件 # 都支持(不支持时 GetSupportedPattern 返回 false 或抛异常),而且**部分应用 # (Electron / 游戏 UI)根本给不出值** —— 拿不到就留空,由上层判"未知"而非"false"。 diff --git a/apps/agent/src/capabilities/window.ts b/apps/agent/src/capabilities/window.ts index 5a376e0..393a3ec 100644 --- a/apps/agent/src/capabilities/window.ts +++ b/apps/agent/src/capabilities/window.ts @@ -300,7 +300,7 @@ export async function screenFind(args: Args): Promise { } const text = (args['text'] as string | undefined) ?? ''; const wantImage = (args['method'] as string | undefined) === 'image'; - // v1.8 修正:`where` 可单独使用(不传 text)——「找一个 enabled 的按钮」比 + // v2.0.0 修正:`where` 可单独使用(不传 text)——「找一个 enabled 的按钮」比 // 「找一个叫某名字的按钮」更贴近真实意图。此前只在 gui.await 侧放开了, // screen.find 自身的入口校验没同步,导致直连调用(以及 await 的 where-only) // 被这里的"text 不能为空"拦下(真机 2026-10-09 验证时暴露)。 @@ -317,7 +317,7 @@ export async function screenFind(args: Args): Promise { } const windowTitle = args['window'] as string | undefined; const controlType = args['control_type'] as string | undefined; - // v1.7:属性谓词。仅 UIA 引擎生效;OCR / 图像模板忽略(无属性可读), + // v2.0.0:属性谓词。仅 UIA 引擎生效;OCR / 图像模板忽略(无属性可读), // 此时在返回里带 note 说明,避免调用方误以为"按属性过滤过了"。 const whereArg = args['where'] as Record | undefined; const limit = (args['limit'] as number | undefined) ?? 20; @@ -342,7 +342,7 @@ $text = ${JSON.stringify(text)} $limit = ${Number(limit)} $ct = ${JSON.stringify(controlType ?? '')} $winTitle = ${JSON.stringify(windowTitle ?? '')} -# v1.7 属性过滤:where 只含 enabled/selected/value/toggle 四类键(白名单式)。 +# v2.0.0 属性过滤:where 只含 enabled/selected/value/toggle 四类键(白名单式)。 # # ⚠️⚠️ 这里踩过**两次**同一个坑,真机 2026-10-09 连修两轮才通: # ✗ 第一版:$where = {"enabled":true} → PS 不认 JSON 字面量,ParseError @@ -626,7 +626,7 @@ $out | ConvertTo-Json -Compress await new Promise((r) => setTimeout(r, intervalMs)); last = await attempt(); } - // v1.8:where 只在 UIA 引擎下有属性可读 —— 若最终走的是 OCR/image, + // v2.0.0:where 只在 UIA 引擎下有属性可读 —— 若最终走的是 OCR/image, // 必须**明确告知**,否则调用方会以为"按属性过滤过了"而误判结果。 const whereIgnored = hasWhere && last.engine !== 'uia'; return { diff --git a/apps/agent/src/config.ts b/apps/agent/src/config.ts index 409e404..363d4fb 100644 --- a/apps/agent/src/config.ts +++ b/apps/agent/src/config.ts @@ -43,7 +43,7 @@ export interface AgentConfig { allow_from?: string[]; /** - * v23 连接层防护(三项,默认值见 server.ts)。 + * v2.0.0 连接层防护(三项,默认值见 server.ts)。 * 单项缺省即启用默认策略;显式设 0 可关闭对应项。 */ security?: { diff --git a/apps/agent/src/events.ts b/apps/agent/src/events.ts index f6fed01..d4f4fe9 100644 --- a/apps/agent/src/events.ts +++ b/apps/agent/src/events.ts @@ -261,7 +261,7 @@ export async function eventPoll(args: Args): Promise { } /** 连接断开时清理该连接创建的所有订阅(避免 watcher 泄漏)。 */ -/** v23:当前事件订阅数(空闲断开的豁免判据之一)。 */ +/** v2.0.0:当前事件订阅数(空闲断开的豁免判据之一)。 */ export function watchCount(): number { return watches.size; } diff --git a/apps/agent/src/server.ts b/apps/agent/src/server.ts index be1de46..7f48032 100644 --- a/apps/agent/src/server.ts +++ b/apps/agent/src/server.ts @@ -48,7 +48,7 @@ interface ConnState { remote: string; /** v12:连接标识,用于清理该连接创建的事件订阅 */ connId: string; - /** v23:末次活跃时间(任何入站消息),空闲断开的依据 */ + /** v2.0.0:末次活跃时间(任何入站消息),空闲断开的依据 */ lastActiveAt: number; } @@ -64,9 +64,9 @@ type LogLevel = 'debug' | 'info' | 'warn'; export interface AgentCore { /** 在给定连接上提供被控端服务(连接已建立)。 */ attach(ws: WebSocket, remote: string): void; - /** v23:该来源封禁剩余毫秒(0 = 不挡)。连接入口在业务握手前调用。 */ + /** v2.0.0:该来源封禁剩余毫秒(0 = 不挡)。连接入口在业务握手前调用。 */ banRemainingMs(remote: string): number; - /** v23:连接表(并发上限 / 空闲清扫判据;key 为 WebSocket)。 */ + /** v2.0.0:连接表(并发上限 / 空闲清扫判据;key 为 WebSocket)。 */ states: Map; } @@ -100,7 +100,7 @@ export function createAgentCore(cfg: AgentConfig): AgentCore { } }; - // v23:用 Map 而非 WeakMap —— 连接层需要 size(并发上限)与遍历(空闲清扫)。 + // v2.0.0:用 Map 而非 WeakMap —— 连接层需要 size(并发上限)与遍历(空闲清扫)。 // 代价是必须手动清理:close 时 delete,否则断开的连接会一直留在表里。 const states = new Map(); @@ -191,7 +191,7 @@ export function createAgentCore(cfg: AgentConfig): AgentCore { state.nonce = null; // 一次性,用后即废 state.nonceExpiresAt = 0; - recordAuthSuccess(state.remote); // v23:握手成功即清零失败计数(避免误伤手滑的控制端) + recordAuthSuccess(state.remote); // v2.0.0:握手成功即清零失败计数(避免误伤手滑的控制端) state.authenticated = true; state.clientId = clientId; state.authorized = authorized; @@ -414,7 +414,7 @@ export function createAgentCore(cfg: AgentConfig): AgentCore { } } - // ---------- v23 连接层防护:握手失败封禁 ---------- + // ---------- v2.0.0 连接层防护:握手失败封禁 ---------- // 状态放在 core:handleAuth 在此函数内,且 server 侧连接入口也要查它。 interface BanState { fails: number; bannedUntil: number } const authBan = new Map(); @@ -471,7 +471,7 @@ export function createAgentCore(cfg: AgentConfig): AgentCore { lastActiveAt: Date.now(), }); ws.on('message', (raw: RawData) => { - // v23:任何入站消息都算活跃(空闲断开以"完全静默"为准,不是"没有调用能力") + // v2.0.0:任何入站消息都算活跃(空闲断开以"完全静默"为准,不是"没有调用能力") const st = states.get(ws); if (st) st.lastActiveAt = Date.now(); void handleMessage(ws, raw.toString()); @@ -481,16 +481,16 @@ export function createAgentCore(cfg: AgentConfig): AgentCore { // v12:连接断开即释放其事件订阅,避免 watcher 泄漏 const n = disposeWatchesByOwner(connId); if (n > 0) log('debug', `已清理 ${n} 个事件订阅(连接 ${connId})`); - states.delete(ws); // v23:Map 需手动清理(WeakMap 时代靠 GC) + states.delete(ws); // v2.0.0:Map 需手动清理(WeakMap 时代靠 GC) }); ws.on('error', (err: Error) => log('warn', `连接错误: ${err.message}`)); } return { attach: attachConnection, - /** v23:连接入口用 —— 该来源是否在封禁期(返回剩余毫秒,0=不挡)。 */ + /** v2.0.0:连接入口用 —— 该来源是否在封禁期(返回剩余毫秒,0=不挡)。 */ banRemainingMs, - /** v23:连接表(空闲清扫与并发上限判据)。 */ + /** v2.0.0:连接表(空闲清扫与并发上限判据)。 */ states, }; } diff --git a/apps/cli/src/cmd/gui.ts b/apps/cli/src/cmd/gui.ts index 0a26117..0f1c8f4 100644 --- a/apps/cli/src/cmd/gui.ts +++ b/apps/cli/src/cmd/gui.ts @@ -172,7 +172,7 @@ export async function cmdKey(action: string | undefined, positionals: string[], return; } case 'press': { - // v1.5:三种用法(一次一种) + // v1.5.0:三种用法(一次一种) // nodeagent key press "ctrl+shift+esc" 字符串热键 // nodeagent key press copy 预设名 // nodeagent key press ctrl c 老形式(空格分隔的各键) @@ -193,7 +193,7 @@ export async function cmdKey(action: string | undefined, positionals: string[], } // 老形式说明:位置参数以空格分隔 → 兼容"ctrl c"(无 + 号) if (opts.interval) args['interval_ms'] = Number(opts.interval); - // v1.5 新增可选参数 + // v1.5.0 新增可选参数 if (opts.hold) args['hold_ms'] = Number(opts.hold); if (opts.route) args['route'] = opts.route; // 媒体键可定向到指定进程(播放器常建 30+ 辅助窗口,只投主窗口往往无效) @@ -232,7 +232,7 @@ export async function cmdKey(action: string | undefined, positionals: string[], } } -// ---------- v1.6 等待条件(gui.await) ---------- +// ---------- v1.6.0 等待条件(gui.await) ---------- /** * 等待一个条件成立再返回(只读低危)。 @@ -251,7 +251,7 @@ export async function cmdAwait(opts: Options): Promise { fail('用法: nodeagent await --condition --timeout [--text/--title/--process/--path] [--state absent]'); } const args: Record = { condition }; - // v1.7:where / any_of 是 JSON 字符串(属性谓词与组合条件不便用命令行开关表达) + // v2.0.0:where / any_of 是 JSON 字符串(属性谓词与组合条件不便用命令行开关表达) for (const k of ['where', 'any_of']) { const raw = (opts as unknown as Record)[k]; if (raw !== undefined) { diff --git a/apps/cli/src/cmd/ops.ts b/apps/cli/src/cmd/ops.ts index 4821eb9..6b68d32 100644 --- a/apps/cli/src/cmd/ops.ts +++ b/apps/cli/src/cmd/ops.ts @@ -641,7 +641,7 @@ export async function cmdClip(opts: Options): Promise { ); } -// ---------- v1.8 状态采样(monitor / log) ---------- +// ---------- v2.0.0 状态采样(monitor / log) ---------- /** * 状态采样与回看。 @@ -732,7 +732,7 @@ export async function cmdMonitor(action: string | undefined, positional: string[ } } -/** 在被控端侧过滤日志(v1.8):日志常几万行,过滤必须发生在被控端。 */ +/** 在被控端侧过滤日志(v2.0.0):日志常几万行,过滤必须发生在被控端。 */ export async function cmdLog(opts: Options): Promise { const logPath = opts.path; if (!logPath) fail('用法: nodeagent log --path <文件> [--pattern 正则] [--level ERROR] [--limit n] [--tail]'); diff --git a/apps/cli/src/cmd/system.ts b/apps/cli/src/cmd/system.ts index 49dd7d5..9ca4c2e 100644 --- a/apps/cli/src/cmd/system.ts +++ b/apps/cli/src/cmd/system.ts @@ -183,7 +183,7 @@ export async function cmdRestart(opts: Options): Promise { ); } -// ---------- v25 审计链外部锚定 ---------- +// ---------- v2.0.0 审计链外部锚定 ---------- /** * 打印审计链头(可选与最近锚点比对)。 diff --git a/apps/cli/src/help.ts b/apps/cli/src/help.ts index c455cc1..28e51ba 100644 --- a/apps/cli/src/help.ts +++ b/apps/cli/src/help.ts @@ -49,12 +49,12 @@ export const HELP = `nodeagent —— 跨机 AI 接管框架(控制端 CLI) nodeagent events [--kind file|process|net] [--path <路径>] [--pattern G] [--seconds 15] 订阅事件并实时接收推送(无 --kind 则列出订阅)(v12) nodeagent monitor start --source [--target X] [--interval ms] [--id 名] - 启动定时采样并落盘 (v1.8):port 验连通性/process 验存活/ + 启动定时采样并落盘 (v2.0.0):port 验连通性/process 验存活/ command 取退出码/metric 取 CPU·内存 - nodeagent monitor report 回看样本 + 摘要(断几次、最长断多久、值域)(v1.8) + nodeagent monitor report 回看样本 + 摘要(断几次、最长断多久、值域)(v2.0.0) nodeagent monitor stop|list|delete nodeagent log --path <文件> [--pattern 正则] [--level ERROR] [--limit n] [--tail] - 在被控端侧过滤日志,只回匹配行(日志几万行也不拖网)(v1.8) + 在被控端侧过滤日志,只回匹配行(日志几万行也不拖网)(v2.0.0) nodeagent record [--duration 5000] [--fps 2] [--scale 0.5] [--region x,y,w,h] 录屏为帧序列(有 ffmpeg 则封装 mp4)(v11) nodeagent update --url <地址> --sha256 <哈希> [--dry-run] diff --git a/apps/cli/src/types.ts b/apps/cli/src/types.ts index 2bcf8e9..2dbc090 100644 --- a/apps/cli/src/types.ts +++ b/apps/cli/src/types.ts @@ -20,11 +20,11 @@ export interface Options { /** 通用选项:文本 / 标题 */ text?: string; title?: string; - /** v25 审计锚定 */ + /** v2.0.0 审计锚定 */ compare?: boolean; anchorPath?: string; note?: string; - /** v1.8 状态采样 */ + /** v2.0.0 状态采样 */ source?: string; target?: string; pattern?: string; @@ -32,17 +32,17 @@ export interface Options { since?: string; offset?: string; tail?: boolean; - /** v1.7 gui.await 扩展 */ + /** v2.0.0 gui.await 扩展 */ where?: string; any_of?: string; - /** v1.6 gui.await */ + /** v1.6.0 gui.await */ condition?: string; state?: string; path?: string; process?: string; - /** v1.5 快捷键扩展:长按毫秒 */ + /** v1.5.0 快捷键扩展:长按毫秒 */ hold?: string; - /** v1.5 快捷键扩展:投递路由 foreground|post */ + /** v1.5.0 快捷键扩展:投递路由 foreground|post */ route?: string; pid?: string; /** v3:认证模式 psk | ed25519 */ diff --git a/apps/mcp/src/index.ts b/apps/mcp/src/index.ts index 8329afb..2272fc5 100644 --- a/apps/mcp/src/index.ts +++ b/apps/mcp/src/index.ts @@ -262,7 +262,7 @@ function resolveToolCall(toolName: string, input: Record): Reso return { capability: CapabilityNames.KeyType, args: { text: input['text'] } }; } if (action === 'press') { - // v1.5:四种形式一次只用一种(hotkeys > hotkey > presets > preset > keys), + // v1.5.0:四种形式一次只用一种(hotkeys > hotkey > presets > preset > keys), // 其余各端会做同样的校验与展开,这里只做薄校验后原样透传。 const has = (k: string): boolean => input[k] !== undefined && !(Array.isArray(input[k]) && (input[k] as unknown[]).length === 0); diff --git a/apps/mcp/src/tools/defs.ts b/apps/mcp/src/tools/defs.ts index f69897c..488bb43 100644 --- a/apps/mcp/src/tools/defs.ts +++ b/apps/mcp/src/tools/defs.ts @@ -257,7 +257,7 @@ export const TOOLS = [ { name: 'na_await', description: - '等待一个条件成立再返回(只读低危,v1.6)。\n' + + '等待一个条件成立再返回(只读低危,v1.6.0)。\n' + '四类条件:window=等窗口出现/消失;control=等界面元素出现/消失(复用 screen.find);' + 'process=等进程出现/消失;file=等文件出现/消失。\n' + 'state=present(默认,等出现)或 absent(等消失)。\n' + @@ -275,8 +275,8 @@ export const TOOLS = [ title: { type: 'string', description: 'window 条件:窗口标题(正则)' }, process: { type: 'string', description: 'process 条件:进程名(正则)' }, path: { type: 'string', description: 'file 条件:文件路径' }, - where: { type: 'object', description: 'v1.7 属性谓词(仅 UIA):{ enabled?, selected?, value?, toggle? }' }, - any_of: { type: 'array', maxItems: 8, description: 'v1.7 组合条件:每项一个条件对象,任一命中即算' }, + where: { type: 'object', description: 'v2.0.0 属性谓词(仅 UIA):{ enabled?, selected?, value?, toggle? }' }, + any_of: { type: 'array', maxItems: 8, description: 'v2.0.0 组合条件:每项一个条件对象,任一命中即算' }, }, required: ['condition'], additionalProperties: false, @@ -285,7 +285,7 @@ export const TOOLS = [ { name: 'na_monitor', description: - '定时采样并落盘,事后回看(v1.8,治间歇性问题)。\n' + + '定时采样并落盘,事后回看(v2.0.0,治间歇性问题)。\n' + 'action=start:起一个采样。source=port(target="host:port",测连通性与延迟)/' + 'process(target=进程名,测存活)/command(target=命令,取退出码)/metric(CPU·内存,无需 target)。\n' + 'action=report:读回样本序列与**摘要**(port/process 给「断了几次 + 最长连续中断」;' + @@ -309,7 +309,7 @@ export const TOOLS = [ { name: 'na_log', description: - '在被控端侧过滤日志,只回匹配行(v1.8,只读)。日志常几万行,整份拉回既慢又占带宽 —— ' + + '在被控端侧过滤日志,只回匹配行(v2.0.0,只读)。日志常几万行,整份拉回既慢又占带宽 —— ' + '本工具把过滤下推到被控端(流式逐行读)。支持 pattern(正则)/level(ERROR|WARN|INFO|DEBUG)/\n' + 'since(Unix ms)/offset/limit/tail(取末尾 N 条)。', inputSchema: { diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 0e96089..3e7cf5f 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -309,9 +309,9 @@ | 19 | `fs.write` | 🔴 | 写文件(分块 / 原子写) | v5 | | 20 | `window.list` | 🟢 | 枚举可见顶层窗口(标题/进程/精确矩形/是否前台) | v8 | | 21 | `window.focus` | 🟡 | 置前聚焦(Windows + macOS;越不过全屏独占,属预期) | v8 | -| 22 | `screen.find` | 🟢 | 元素定位三引擎(UIA → OCR → 图像模板)+ **UIA 属性**与 `where` 谓词 | v8 / v1.7 | +| 22 | `screen.find` | 🟢 | 元素定位三引擎(UIA → OCR → 图像模板)+ **UIA 属性**与 `where` 谓词 | v8 / v2.0.0 | | 23 | `screen.record` | 🟢 | 录屏为帧序列(有 ffmpeg 则封 mp4) | v11 | -| 24 | `gui.await` | 🟢 | 等条件成立再返回(四类条件 × 出现/消失)+ `any_of` 组合 | v1.6 | +| 24 | `gui.await` | 🟢 | 等条件成立再返回(四类条件 × 出现/消失)+ `any_of` 组合 | v1.6.0 | | 25 | `input.mouse.drag` | 🔴🔒 | 拖拽(拖文件 / 框选) | v11 | | 26 | `system.task.list` | 🟢 | 列出后台异步任务 | v10 | | 27 | `system.task.get` | 🟢 | 查询任务状态与输出(支持增量续读) | v10 | @@ -319,8 +319,8 @@ | 29 | `clip.get` | 🟡 | 读剪贴板(文本或图片 PNG) | v10 | | 30 | `clip.set` | 🟡 | 写剪贴板(文本或图片) | v10 | | 31 | `system.audit.verify` | 🟢 | 审计链完整性校验(哈希链,防篡改) | v11 | -| 32 | `system.audit.head` | 🟢 | 审计链头(chain head)—— 供**链外**留存 | v25 | -| 33 | `system.audit.anchor` | 🟡 | 链头**追加**写锚点文件,防「有 root 者整链重写」 | v25 | +| 32 | `system.audit.head` | 🟢 | 审计链头(chain head)—— 供**链外**留存 | v2.0.0 | +| 33 | `system.audit.anchor` | 🟡 | 链头**追加**写锚点文件,防「有 root 者整链重写」 | v2.0.0 | | 34 | `system.metrics` | 🟢 | 成功指标聚合(数据源为审计日志) | v13 | | 35 | `event.watch` | 🟢 | 订阅事件(file / process / net),**主动推送** | v12 | | 36 | `event.unwatch` | 🟢 | 取消订阅(连接断开也会自动清理) | v12 | @@ -331,14 +331,14 @@ | 41 | `system.net.status` | 🟢 | 网络现状 / 待确认变更 / 历史备份 | v16 | | 42 | `system.audio.get` | 🟢 | 读音量与静音状态(Core Audio COM) | v17 | | 43 | `system.audio.set` | 🟡 | 设音量 / 静音 | v17 | -| 44 | `system.agent.restart` | 🔴 | 受控重启自身(借计划任务;**非管理员时自动降级**) | v7 / v23 | +| 44 | `system.agent.restart` | 🔴 | 受控重启自身(借计划任务;**非管理员时自动降级**) | v7 / v2.0.0 | | 45 | `system.agent.update` | 🔴 | 拉取式自更新(校验 sha256,只需被控端能上网) | v19 | -| 46 | `log.query` | 🟢 | 日志**在被控端侧过滤**后只回匹配行(pattern/level/since/tail) | v1.8 | -| 47 | `monitor.start` | 🟢 | 启动定时采样(port/process/command/metric)并落盘 | v1.8 | -| 48 | `monitor.report` | 🟢 | 回看样本序列 + **摘要**(断几次 / 最长断多久 / 值域) | v1.8 | -| 49 | `monitor.stop` | 🟢 | 停止采样并返回最终摘要 | v1.8 | -| 50 | `monitor.list` | 🟢 | 列出运行中的监控与历史样本文件 | v1.8 | -| 51 | `monitor.delete` | 🟢 | 删除某监控的历史样本文件 | v1.8 | +| 46 | `log.query` | 🟢 | 日志**在被控端侧过滤**后只回匹配行(pattern/level/since/tail) | v2.0.0 | +| 47 | `monitor.start` | 🟢 | 启动定时采样(port/process/command/metric)并落盘 | v2.0.0 | +| 48 | `monitor.report` | 🟢 | 回看样本序列 + **摘要**(断几次 / 最长断多久 / 值域) | v2.0.0 | +| 49 | `monitor.stop` | 🟢 | 停止采样并返回最终摘要 | v2.0.0 | +| 50 | `monitor.list` | 🟢 | 列出运行中的监控与历史样本文件 | v2.0.0 | +| 51 | `monitor.delete` | 🟢 | 删除某监控的历史样本文件 | v2.0.0 | > 图例:🟢 只读低危 · 🟡 需谨慎 · 🔴 高风险 · 🔒 受 `allow_input` 开关管控。 > 本表为**总览**;权威定义(参数 schema / 返回值)以 `packages/protocol/src/capabilities/manifest/` 为准,能力数由单测守卫。 diff --git a/docs/REMOTE-LIMITS.md b/docs/REMOTE-LIMITS.md index eb0bf18..e9e6ff0 100644 --- a/docs/REMOTE-LIMITS.md +++ b/docs/REMOTE-LIMITS.md @@ -83,7 +83,7 @@ | 无法 7×24 盯 | 臣只在被调用时"看一眼",问题在两次调用之间发生就看不到 | | 应用层内部状态不可见 | 如 WorkBuddy 的"当前正在等哪个请求",臣只能从日志反推 | -**缓解(✅ 已于 v1.8 实现)**:`monitor.*` 定时采样并落盘,事后回看摘要 —— 正是为治 +**缓解(✅ 已于 v2.0.0 实现)**:`monitor.*` 定时采样并落盘,事后回看摘要 —— 正是为治 这类间歇性问题而做(原先只写"具备实现基础",现已成为能力): ```bash diff --git a/docs/RETROSPECTIVE.md b/docs/RETROSPECTIVE.md index bc97a8e..94bc43a 100644 --- a/docs/RETROSPECTIVE.md +++ b/docs/RETROSPECTIVE.md @@ -255,10 +255,10 @@ Start-Process powershell -ArgumentList '... Start-Sleep 3; Stop-Process ...; Sta | 复盘时的短板 | 解决于 | 现在长什么样 | |---|---|---| -| **自持**:无法受控重启、更新要人上手 | v7 / v19 / v23 | `system.agent.restart`(借计划任务,**非管理员身份自动降级**)+ `system.agent.update`(拉取式,校验 sha256) | -| **语义**:只能「找到」,读不懂状态、操作后无法断言 | v1.6 / v1.7 | `gui.await` 等待条件(四类条件 × 出现/消失 + `any_of` 组合);`screen.find` 返回 **UIA 属性**(`enabled/value/selected/toggle`)并支持 `where` 谓词 —— 从「盲试」到「可断言」 | -| **状态**:只有当下快照,间歇性问题无法回看 | v1.8 | `log.query` **在被控端侧过滤**日志;`monitor.*` 定时采样落盘 + 摘要回看(断了几次 / 最长断多久 / CPU·内存值域) | -| **安全残余**(SECURITY §5) | v23 / v24 / v25 | 握手失败封禁(指数退避)· 连接上限与空闲断开 · 安装期密钥走 stdin · **零信任优先安装**(有公钥即 ed25519)· **审计链外部锚定**(防整链重写) | +| **自持**:无法受控重启、更新要人上手 | v7 / v19 / v2.0.0 | `system.agent.restart`(借计划任务,**非管理员身份自动降级**)+ `system.agent.update`(拉取式,校验 sha256) | +| **语义**:只能「找到」,读不懂状态、操作后无法断言 | v1.6.0 / v2.0.0 | `gui.await` 等待条件(四类条件 × 出现/消失 + `any_of` 组合);`screen.find` 返回 **UIA 属性**(`enabled/value/selected/toggle`)并支持 `where` 谓词 —— 从「盲试」到「可断言」 | +| **状态**:只有当下快照,间歇性问题无法回看 | v2.0.0 | `log.query` **在被控端侧过滤**日志;`monitor.*` 定时采样落盘 + 摘要回看(断了几次 / 最长断多久 / CPU·内存值域) | +| **安全残余**(SECURITY §5) | v2.0.0 | 握手失败封禁(指数退避)· 连接上限与空闲断开 · 安装期密钥走 stdin · **零信任优先安装**(有公钥即 ed25519)· **审计链外部锚定**(防整链重写) | **当前规模**:51 项能力 · 269 单测 + 30 端到端 · Windows CI 13 步全绿。 diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 07a434e..35ffb90 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -4,7 +4,7 @@ > 盘点方式:真机验证 + 本地跑单测 + `gh run list`(Windows 被控端曾于 10-10 00:15 离线) > 基线:**`v2.0.0` 已发布**(2026-10-10,commit `457ead1`)|单测 **273 全绿** · e2e **30 全绿** · > 能力 **51 项** · Windows CI / Rust CI 全绿 -> 📌 v2.0.0 发布时被控端整机离线 → v1.5 的四项与 v26 清理策略的真机验证**仍待补**(见 §九) +> 📌 v2.0.0 发布时被控端整机离线 → v1.5.0 的四项与 v2.0.0 清理策略的真机验证**仍待补**(见 §九) > 适用范围:本文档随仓库公开,**不含任何真机地址与账户信息**(见 §6 脱敏约定) --- @@ -49,11 +49,11 @@ ``` 0d62cb1 (tag v1.5.0) chore(release): v1.5.0 -1c836e0 feat(v1.5): 快捷键控制范围扩展(字符串热键/预设别名/长按/后台定向投递) +1c836e0 feat(v1.5.0): 快捷键控制范围扩展(字符串热键/预设别名/长按/后台定向投递) 0ee8231 feat(v1.2): 媒体键 WM_APPCOMMAND 通道 + 定向投递 42ec556 (tag v1.4.3) chore(release): v1.4.3 7ad820c (本地 HEAD) ci: 补全 Windows 真机验证 ← 未推送 -[工作区未提交] feat(v1.6): gui.await 等待条件 ← 在建 +[工作区未提交] feat(v1.6.0): gui.await 等待条件 ← 在建 ``` ### 2.2 代码与测试 @@ -67,7 +67,7 @@ | e2e | 2 个套件(`run.mjs` 通用 29 例 / `windows.mjs` Windows 专属 13 例) | | **`gui.await` 覆盖** | **0** —— 单测、e2e、CLI、MCP 均无 `gui.await`/`GuiAwait` 命中 | -> ⚠️ 单测数从 162 增至 195(新增 33 例,来自 v1.5 快捷键的按键表覆盖断言),但**红了一例**:`tests/unit/capabilities.test.mjs:66` 的 `assert.equal(CAPABILITY_MANIFEST.length, 42)`,实际 43。**修法**:改为 43,或更稳妥地改成「断言每个 `CapabilityNames` 键都在 manifest 中」以免每加一能力都要改数。 +> ⚠️ 单测数从 162 增至 195(新增 33 例,来自 v1.5.0 快捷键的按键表覆盖断言),但**红了一例**:`tests/unit/capabilities.test.mjs:66` 的 `assert.equal(CAPABILITY_MANIFEST.length, 42)`,实际 43。**修法**:改为 43,或更稳妥地改成「断言每个 `CapabilityNames` 键都在 manifest 中」以免每加一能力都要改数。 **最大的三个文件**(后续拆分候选): @@ -86,7 +86,7 @@ | **屏幕与录制**(4) | `screen.info` `screen.capture` `screen.record` `screen.find` | | **输入**(6) | `input.mouse.move` `input.mouse.click` `input.mouse.scroll` `input.mouse.drag` `input.key.type` `input.key.press` | | **窗口**(2) | `window.list` `window.focus` | -| **GUI 等待**(1) | **`gui.await`** 🆕 v1.6 在建未提交 —— 等条件成立再返回 | +| **GUI 等待**(1) | **`gui.await`** 🆕 v1.6.0 在建未提交 —— 等条件成立再返回 | | **文件**(4) | `fs.list` `fs.stat` `fs.read` `fs.write`(分块 RPC + 原子写) | | **剪贴板**(2) | `clip.get` `clip.set` | | **异步任务**(3) | `system.task.list` `system.task.get` `system.task.kill` | @@ -96,7 +96,7 @@ | **音频**(2) | `system.audio.get` `system.audio.set`(Core Audio COM) | | **网络变更**(3) | `system.net.apply` `system.net.confirm` `system.net.status` | -**`gui.await` 设计要点**(工作区在建,v1.6): +**`gui.await` 设计要点**(工作区在建,v1.6.0): | 项 | 内容 | |---|---| @@ -190,7 +190,7 @@ await gui.await(condition:"control", text:"完成", timeout_ms:30000) 3. **install.cmd / control.cmd 双击链路冒烟** —— 这两个脚本是用户唯一上手入口,编码或语法坏了只有真机 cmd.exe 测得出。 - **收尾**:推送后可清理 `ci/workflow-unit-tests` 分支(贡献已并入,留着只会误导)。 -### 4.2 v1.5.0 真机验证尚未做(v1.5 提交信息自陈) +### 4.2 v1.5.0 真机验证尚未做(v1.5.0 提交信息自陈) `1c836e0` 的提交信息里写明: @@ -391,7 +391,7 @@ node scripts/verify.mjs --host <地址> --key <密钥> --report <报告.md> | 自重启任务 Running 僵尸清理 | ✅ 代码 / ⏳ 真机 | 清理条件加「LastRunTime 早于 1 小时」余量(防误杀当前宿主) | | **`SECURITY.md` §5「还没做的」清单** | ✅ **全部完成** | 见 §4.5~§4.8 | | **发版 v2.0.0** | ✅ 2026-10-10 | 37 个提交一次发出;资产 agent.mjs(sha256 `7164ea44…`) / win-x64.zip / latest.json / SHA256SUMS | -| v1.5 四项 + v26 清理策略**真机验证** | ⏳ 待被控端上线 | 发布时机器离线;已在 CHANGELOG 如实标注 | +| v1.5.0 四项 + v2.0.0 清理策略**真机验证** | ⏳ 待被控端上线 | 发布时机器离线;已在 CHANGELOG 如实标注 | ### 真机验证过程中抓到的真 bug(8 个,全部只有真机能发现) @@ -400,7 +400,7 @@ node scripts/verify.mjs --host <地址> --key <密钥> --report <报告.md> | 1 | `install.cmd` 双击首装即炸(`for /f` 调 PS,`^|` 漏进 PowerShell) | **用户首装必炸** | | 2 | `pack.mjs` 三连(spawn `.bin/esbuild` ENOENT / `zip` / `ls -la`+`du -sh`) | 打包脚本全平台假设 | | 3 | `where` 的 PS 注入语法错(连修**两轮**才通:JSON 字面量 ✗ → `\"` 转义 ✗ → PS 单引号 ✓) | 跨语言转义 | -| 4 | `screen.find` 的 where-only 被自己入口校验拦下 | v1.7 只改了一半 | +| 4 | `screen.find` 的 where-only 被自己入口校验拦下 | v2.0.0 只改了一半 | | 5 | 自重启在非管理员下失败 → `deploy` 永远"半成功" | v7 起的既有缺陷 | | 6 | `parseLeadingTs` 吃掉 ISO 的 `Z` 时区标记(偏 8 小时) | 单测抓到 | | 7 | manifest 承诺的 `note` 字段代码没实现 | 承诺未落地 | diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 7110079..0f598f3 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -116,7 +116,7 @@ nodeagent keygen --psk --id mac_02 | 未登记的 client_id 用共享 `key` | ✅ 通过(**旧部署行为不变**) | | 已登记的 client_id 用共享 `key` | ❌ 拒绝(**专属钥匙优先**,不被共享 key 绕过) | -## 4.7 零信任优先安装(v24) +## 4.7 零信任优先安装(v2.0.0) 安装时的认证模式选择,从「默认 psk + 可选 ed25519」改为**「能拿到控制端公钥就走 ed25519」**: @@ -141,7 +141,7 @@ nodeagent keygen --id mac_01 # 输出一段 JSON(acl.clients 的一项 > 真机验证(2026-10-09):CASE A 有文件 → `ed25519` + `default_effect=deny` + clients 完整; > CASE B 无文件 → `psk` 且**不写 acl 字段**;CASE C 缺 pubkey → 告警并回退 psk。 -## 4.8 审计链外部锚定(v25) +## 4.8 审计链外部锚定(v2.0.0) **问题**:链内哈希只能发现"改一条"(其后全部需重算)。「有 root 的攻击者」可以 **整链重写** —— 从零重建一份自洽的假日志,`system.audit.verify` 照样通过。 @@ -180,11 +180,11 @@ nodeagent --node win audit anchor --note "每日例行" # 追加一个锚点 | 项 | 说明 | |---|---| | ~~发现报文最小化~~ | ✅ **v22 已做**(见 §4.5) | -| ~~ed25519 默认化~~ | ✅ **v24 已做**(零信任优先安装,见 §4.7);另有 v22 的 `keys{}` 每客户端一把 PSK 作为轻量替代 | -| ~~握手失败封禁~~ | ✅ **v23 已做**:连续失败达阈值 → 指数退避封禁(封顶 24h),成功即清零,支持白名单豁免 | -| ~~会话上限~~ | ✅ **v23 已做**:并发上限 8 + 30 分钟空闲断开(有任务/订阅在跑时保守不踢) | -| ~~安装期密钥传递~~ | ✅ **v23 已做**:`install.cmd` 改用管道喂 stdin(`-KeyFromStdin`),密钥不再出现在命令行 | -| ~~审计链外部锚定~~ | ✅ **v25 已做**(见 §4.8):链头可读 + 锚点追加落盘 + 锚点比对 | +| ~~ed25519 默认化~~ | ✅ **v2.0.0 已做**(零信任优先安装,见 §4.7);另有 v22 的 `keys{}` 每客户端一把 PSK 作为轻量替代 | +| ~~握手失败封禁~~ | ✅ **v2.0.0 已做**:连续失败达阈值 → 指数退避封禁(封顶 24h),成功即清零,支持白名单豁免 | +| ~~会话上限~~ | ✅ **v2.0.0 已做**:并发上限 8 + 30 分钟空闲断开(有任务/订阅在跑时保守不踢) | +| ~~安装期密钥传递~~ | ✅ **v2.0.0 已做**:`install.cmd` 改用管道喂 stdin(`-KeyFromStdin`),密钥不再出现在命令行 | +| ~~审计链外部锚定~~ | ✅ **v2.0.0 已做**(见 §4.8):链头可读 + 锚点追加落盘 + 锚点比对 | ## 6. 应急处置 diff --git a/docs/VERSIONING.md b/docs/VERSIONING.md index 67b2d5e..bd3305c 100644 --- a/docs/VERSIONING.md +++ b/docs/VERSIONING.md @@ -3,6 +3,50 @@ > 为什么需要这份文档:agent 与 CLI/MCP 是**分开更新的**(CLI 随控制端装,agent 推/拉到被控端), > 新旧混用是常态而非例外。没有明文契约,升级顺序、什么算破坏性改动、出问题怎么定位全靠猜。 +## 0. 版本号唯一性规范(2026-10-10 确立) + +> **背景**:项目早期同时存在两套编号 —— 发布版本(`v1.1.0`/`v2.0.0`)与开发批次代号(`v1`~`v26`)。 +> 二者**从来不是一对一**(例:`v1.1.0` 首发时批次已推进到 `v12.4`),并列出现时无法判断哪个是"当前版本"。 +> 自 v2.0.0 起统一为下列规则。 + +### 唯一版本号 + +**`vX.Y.Z`(semver)是项目唯一的版本标识**,两个来源必须永远一致: + +| 来源 | 位置 | 谁在读 | +|---|---|---| +| git tag | `v2.0.0` | 人 / GitHub Release | +| `package.json` 的 `version` | 全部 workspace + `tauri.conf.json` | 构建期经 esbuild `--define` 注入 agent | + +```bash +nodeagent info # 被控端自报的 version 即来自这里 +git tag -l # 发布历史,唯一权威 +``` + +### 开发批次代号(已并入发布版本) + +`v1`~`v26` 是**开发阶段的内部代号**(一批相关改动的集合),**不是版本号,也不再单独出现**: + +| 批次 | 内容 | **归属发布版本** | +|---|---|---| +| v1 ~ v12.4 | 命令级接管 → 事件订阅 / GUI 宏 / Hub 并发(首发前的全部积累) | **v1.1.0** | +| v12.2 ~ v15 | 等待语义 / macOS GUI 对齐 / 常驻助手 / 图像模板 | v1.2.1 ~ v1.4.x | +| v16 ~ v18 | 网络两阶段提交 / 音频 / 构建指纹 | v1.4.x | +| v19 ~ v22 | 拉取式自更新 / 证书钉住 / 每客户端 PSK / 发现最小化 | v1.5.0 ~ v1.6.0 | +| v23 ~ v26、语义属性化、状态采样 | 连接层三防护 / 零信任安装 / 审计锚定 / UIA 属性 / monitor | **v2.0.0** | + +> 上表按 tag 时间线与功能内容推定,**近似**而非精确(批次与发布本无严格边界)。 +> 判断"某个改动属于哪个版本"时,**以 CHANGELOG 的版本段落为准**,不以批次号为准。 + +### 书写规则(新代码 / 新文档) + +| 场景 | 写法 | 例 | +|---|---|---| +| 引用发布版本 | `vX.Y.Z` | `v2.0.0 新增 gui.await` | +| 需要区分同一版本内的多批改动 | 用**功能名**,不用批次号 | `审计锚定(v2.0.0)` | +| 历史注释中的批次号 | 保留但**不得**再新增;见上表归属 | `v16:网络两阶段提交` | + + ## 1. 版本号 - 单一来源:**根 `package.json` 的 `version`**,构建时经 esbuild `--define` 注入 agent。 diff --git a/packages/protocol/src/capabilities/manifest/graphics.ts b/packages/protocol/src/capabilities/manifest/graphics.ts index cff11f5..1320949 100644 --- a/packages/protocol/src/capabilities/manifest/graphics.ts +++ b/packages/protocol/src/capabilities/manifest/graphics.ts @@ -188,7 +188,7 @@ export const GRAPHICS_CAPABILITIES: CapabilityDescriptor[] = [ name: CapabilityNames.KeyPress, version: '1.5', description: - '按下**快捷键**(v1.5 大幅扩展范围)。支持的输入形式(**一次只用一种**):' + + '按下**快捷键**(v1.5.0 大幅扩展范围)。支持的输入形式(**一次只用一种**):' + '① 字符串热键 hotkey: "ctrl+shift+esc"(配 repeat 可连按);② 热键序列 hotkeys: ["ctrl+c","ctrl+v"];' + '③ 预设别名 preset: "copy" / presets: ["copy","paste"](约 50 个语义名,免记键位);' + '④ 老数组形式 keys: ["ctrl","c"](配 repeat);⑤ 序列 sequence: [["up"],["enter"]]。' + @@ -243,14 +243,14 @@ export const GRAPHICS_CAPABILITIES: CapabilityDescriptor[] = [ minimum: 0, maximum: 5000, default: 0, - description: 'v1.5 长按:按下后保持该毫秒数再释放(如长按音量键)', + description: 'v1.5.0 长按:按下后保持该毫秒数再释放(如长按音量键)', }, route: { type: 'string', enum: ['foreground', 'post'], default: 'foreground', description: - 'v1.5 投放路由:foreground = SendInput 注入当前焦点(默认,需目标可见);' + + 'v1.5.0 投放路由:foreground = SendInput 注入当前焦点(默认,需目标可见);' + 'post = 向 target_pid 进程全部顶层窗口 PostMessage(目标在后台时可用)', }, target_pid: { @@ -392,7 +392,7 @@ export const GRAPHICS_CAPABILITIES: CapabilityDescriptor[] = [ where: { type: 'object', description: - 'v1.7 属性谓词(**仅 UIA 引擎生效**):{ enabled?, selected?, value?, toggle? }。' + + 'v2.0.0 属性谓词(**仅 UIA 引擎生效**):{ enabled?, selected?, value?, toggle? }。' + '例:{"enabled":true} 只回可用控件;{"value":"*已完成*"} 按值模糊匹配。' + '拿不到的属性(Electron/游戏 UI 常见)不参与命中,由调用方按 note 判断;' + 'OCR / image 引擎无属性可读,此时忽略本参数并带回 note。', @@ -436,7 +436,7 @@ export const GRAPHICS_CAPABILITIES: CapabilityDescriptor[] = [ type: 'object', properties: { engine: { type: 'string', description: '实际使用的引擎:uia | ocr' }, - note: { type: 'string', description: 'v1.7:where 在非 UIA 引擎下被忽略等降级说明' }, + note: { type: 'string', description: 'v2.0.0:where 在非 UIA 引擎下被忽略等降级说明' }, waited_ms: { type: 'integer', description: '实际等待时长(便于诊断是否命中等待窗口)' }, matches: { type: 'array', @@ -501,7 +501,7 @@ export const GRAPHICS_CAPABILITIES: CapabilityDescriptor[] = [ name: CapabilityNames.GuiAwait, version: '1.0', description: - '等待一个条件成立再返回(v1.6,**只读低危**)。四类条件:' + + '等待一个条件成立再返回(v1.6.0,**只读低危**)。四类条件:' + 'window=等窗口出现/消失;control=等界面元素出现/消失(复用 screen.find);' + 'process=等进程出现/消失;file=等文件出现/消失。' + '用途:把 GUI 操作从「按一下→睡几秒→截图碰运气」变成**可断言**的流程,例如:' + @@ -532,7 +532,7 @@ export const GRAPHICS_CAPABILITIES: CapabilityDescriptor[] = [ text: { type: 'string', description: 'control 条件:元素文本(有 where 时可省)' }, where: { type: 'object', - description: 'v1.7 属性谓词(仅 UIA):{ enabled?, selected?, value?, toggle? },如等一个可用的按钮', + description: 'v2.0.0 属性谓词(仅 UIA):{ enabled?, selected?, value?, toggle? },如等一个可用的按钮', }, window: { type: 'string', description: 'control 条件:限定窗口标题(正则)' }, control_type: { type: 'string', description: 'control 条件:限定控件类型(仅 UIA)' }, @@ -545,7 +545,7 @@ export const GRAPHICS_CAPABILITIES: CapabilityDescriptor[] = [ type: 'array', maxItems: 8, description: - 'v1.7 组合条件:数组,每项为一个条件对象(同顶层参数名),**任一命中**即算命中。' + + 'v2.0.0 组合条件:数组,每项为一个条件对象(同顶层参数名),**任一命中**即算命中。' + '例:[{"condition":"window","title":"安装完成"},{"condition":"control","text":"错误"}]' + '——「安装成功或报错,先出现的那个算」。与 where 合用可表达' + '「弹窗出现且其中某按钮 enabled」(用两条 await 亦可,任选)。', diff --git a/packages/protocol/src/capabilities/manifest/system.ts b/packages/protocol/src/capabilities/manifest/system.ts index 4f19858..28dd51a 100644 --- a/packages/protocol/src/capabilities/manifest/system.ts +++ b/packages/protocol/src/capabilities/manifest/system.ts @@ -393,7 +393,7 @@ export const SYSTEM_CAPABILITIES: CapabilityDescriptor[] = [ name: CapabilityNames.LogQuery, version: '1.0', description: - '在被控端侧**过滤**日志后只回匹配行(v1.8,只读低危)。' + + '在被控端侧**过滤**日志后只回匹配行(v2.0.0,只读低危)。' + '治间歇性问题的关键:日志常几万行,整份拉回既慢又占带宽,过滤必须发生在 Windows 侧。' + '支持 pattern(正则,忽略大小写)/ level(ERROR|WARN|INFO|DEBUG|TRACE)/ ' + 'since(Unix ms,按行首时间戳)/ offset / limit / tail(取末尾 N 条)。' + @@ -428,7 +428,7 @@ export const SYSTEM_CAPABILITIES: CapabilityDescriptor[] = [ name: CapabilityNames.MonitorStart, version: '1.0', description: - '启动一个定时采样并落盘(v1.8)。四类源:' + + '启动一个定时采样并落盘(v2.0.0)。四类源:' + 'port(target="host:port",连通性+延迟)/ process(target=进程名,存活+PID)/ ' + 'command(target=命令,退出码+首行输出)/ metric(CPU/内存,无需 target)。' + '样本写 <数据目录>/monitors/.jsonl,进程退出即停(不落定时任务)。' + @@ -457,7 +457,7 @@ export const SYSTEM_CAPABILITIES: CapabilityDescriptor[] = [ name: CapabilityNames.MonitorReport, version: '1.0', description: - '读回某个监控的样本序列与摘要(v1.8,只读)。摘要直接给结论:' + + '读回某个监控的样本序列与摘要(v2.0.0,只读)。摘要直接给结论:' + 'port/process 的「断线次数 + 最长连续中断样本数」、command 的「成功/失败数与出现过的退出码」、' + 'metric 的「CPU/内存 min/max/avg」。运行中或已停止的都能查。', risk: 'low', @@ -486,7 +486,7 @@ export const SYSTEM_CAPABILITIES: CapabilityDescriptor[] = [ { name: CapabilityNames.MonitorStop, version: '1.0', - description: '停止一个运行中的监控,并返回其最终摘要(v1.8)。', + description: '停止一个运行中的监控,并返回其最终摘要(v2.0.0)。', risk: 'low', params_schema: { type: 'object', @@ -502,7 +502,7 @@ export const SYSTEM_CAPABILITIES: CapabilityDescriptor[] = [ { name: CapabilityNames.MonitorList, version: '1.0', - description: '列出运行中的监控与历史样本文件(v1.8,只读)。', + description: '列出运行中的监控与历史样本文件(v2.0.0,只读)。', risk: 'low', params_schema: { type: 'object', properties: {}, additionalProperties: false }, returns_schema: { @@ -517,7 +517,7 @@ export const SYSTEM_CAPABILITIES: CapabilityDescriptor[] = [ { name: CapabilityNames.MonitorDelete, version: '1.0', - description: '删除某个监控的历史样本文件(不影响运行中的实例;运行中需先 stop)(v1.8)。', + description: '删除某个监控的历史样本文件(不影响运行中的实例;运行中需先 stop)(v2.0.0)。', risk: 'low', params_schema: { type: 'object', @@ -531,7 +531,7 @@ export const SYSTEM_CAPABILITIES: CapabilityDescriptor[] = [ name: CapabilityNames.AuditHead, version: '1.0', description: - '返回审计链的链头(v25,只读):{ entries, head_hash, head_ts, rotated_segments, file, file_bytes }。' + + '返回审计链的链头(v2.0.0,只读):{ entries, head_hash, head_ts, rotated_segments, file, file_bytes }。' + '**外部锚定的一半** —— 控制端定期拉取并把它存到链外(Mac 本地 / 另一台机器 / 网盘)。' + 'compare=true 时附带与最近一条锚点的比对结论(另一半),能识别「有 root 的攻击者整链重写」——' + '链内哈希只能发现改一条,整链重建照样自洽,唯有链外的历史记录能戳破。' + @@ -564,7 +564,7 @@ export const SYSTEM_CAPABILITIES: CapabilityDescriptor[] = [ name: CapabilityNames.AuditAnchor, version: '1.0', description: - '把当前审计链头**追加**写入锚点文件(v25)。追加而非覆盖:历史锚点一旦写成就不可被后续' + + '把当前审计链头**追加**写入锚点文件(v2.0.0)。追加而非覆盖:历史锚点一旦写成就不可被后续' + '刷新掉,否则攻击者重写链后再锚定一次即可抹除痕迹。' + '默认写 <数据目录>/audit-anchors.jsonl;指定 path 时仍受 fs_roots 白名单约束。' + '建议定期(如每天)调用,或用 automation 定时执行。' + diff --git a/packages/protocol/src/hotkeys.ts b/packages/protocol/src/hotkeys.ts index 41ec646..1cd1f9e 100644 --- a/packages/protocol/src/hotkeys.ts +++ b/packages/protocol/src/hotkeys.ts @@ -1,5 +1,5 @@ /** - * 快捷键(hotkey)解析与预设(v1.5)。 + * 快捷键(hotkey)解析与预设(v1.5.0)。 * * ## 为什么放在 protocol 包 * 解析逻辑必须**两端一致 + 可单测**:控制端 CLI 先把字符串展开成和弦列表用于本地校验, diff --git a/scripts/install.cmd b/scripts/install.cmd index 3ac1c1a..a1ecb13 100644 --- a/scripts/install.cmd +++ b/scripts/install.cmd @@ -84,7 +84,7 @@ echo. echo [*] Installing nodeagent ^(node: %COMPUTERNAME%, port %PORT%, input control ON^)... echo. -REM v23: the key now travels via PIPE (stdin), not the -Key command-line arg. +REM v2.0.0: the key now travels via PIPE (stdin), not the -Key command-line arg. REM Why: command lines are readable by other users on this machine REM (Win32_Process.CommandLine / Task Manager show them in clear text). REM NOTE: there must be NO space before the pipe - `echo key |` would fold the diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 938805b..1e491b9 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -42,11 +42,11 @@ param( [switch]$Unattended, # 追加「开机即启动」触发器(不依赖用户登录) [switch]$AtStartup, - # v23:从**标准输入**读密钥(而非命令行 -Key)。 + # v2.0.0:从**标准输入**读密钥(而非命令行 -Key)。 # 为什么:命令行参数对同机其他用户可见(任务管理器/Get-CimInstance Win32_Process # 的 CommandLine 字段就是明文)。install.cmd 用 `echo | powershell ...` 喂进来。 [switch]$KeyFromStdin, - # v24 零信任优先:控制端公钥文件路径(内容是 `nodeagent keygen` 打印的那一条 JSON, + # v2.0.0 零信任优先:控制端公钥文件路径(内容是 `nodeagent keygen` 打印的那一条 JSON, # 即 acl.clients 的一项)。给了它 → auth_mode 直接走 ed25519(零信任), # 不再依赖可被自报的 client_id。留空则自动探测脚本同目录的 client-acl.json。 [string]$ClientAclFile = "" @@ -119,7 +119,7 @@ $cfgDir = if ($env:NODEAGENT_HOME) { $env:NODEAGENT_HOME } else { Join-Path $en $cfgPath = Join-Path $cfgDir "agent.json" New-Item -ItemType Directory -Force -Path $cfgDir | Out-Null -# v23:stdin 读取要在「复用既有配置」之前 —— 调用方显式给了 key 就以它为准。 +# v2.0.0:stdin 读取要在「复用既有配置」之前 —— 调用方显式给了 key 就以它为准。 # 同时规避了一个易错点:[Console]::In.ReadLine() 在没有管道时会阻塞, # 所以必须先判 [Console]::IsInputRedirected。 if ($KeyFromStdin -and -not $Key) { @@ -155,7 +155,7 @@ if (-not $Key) { Write-Ok "Generated a new pre-shared key" } -# --- v24: zero-trust-first ------------------------------------------------- +# --- v2.0.0: zero-trust-first ------------------------------------------------- # PSK 有一个固有弱点:client_id 是调用方**自报**的,所以"谁知道共享 key,谁就能 # 冒充任意未登记的 client_id"—— ACL 的身份维度在 psk 模式下形同虚设。 # ed25519 没有这个问题(身份由私钥签名决定)。故:**只要拿到控制端公钥就走 ed25519**。 diff --git a/tests/unit/audit-anchor.test.mjs b/tests/unit/audit-anchor.test.mjs index 204cce6..8a332c7 100644 --- a/tests/unit/audit-anchor.test.mjs +++ b/tests/unit/audit-anchor.test.mjs @@ -13,7 +13,7 @@ import { } from '../../apps/agent/dist/audit.js'; /** - * v25 审计链外部锚定的逻辑测试。 + * v2.0.0 审计链外部锚定的逻辑测试。 * * 守的是三件最容易写错的事: * 1. 只记 head_hash 会把**轮转**误判成篡改(必须连 entries + rotated_segments 一起记) diff --git a/tests/unit/capabilities.test.mjs b/tests/unit/capabilities.test.mjs index 9c8db4c..f664b29 100644 --- a/tests/unit/capabilities.test.mjs +++ b/tests/unit/capabilities.test.mjs @@ -255,9 +255,9 @@ test('capabilityDiff:真实清单与自身比对应无差异(防手滑改坏 assert.equal(d.unknownLocally.length, 0); }); -// ---------- v1.5 快捷键扩展:新参数必须通过 schema 校验 ---------- +// ---------- v1.5.0 快捷键扩展:新参数必须通过 schema 校验 ---------- -test('input.key.press:v1.5 新参数(hotkey/hotkeys/preset/hold_ms/route)通过校验', () => { +test('input.key.press:v1.5.0 新参数(hotkey/hotkeys/preset/hold_ms/route)通过校验', () => { assert.equal(check('input.key.press', { hotkey: 'ctrl+shift+esc' }).length, 0); assert.equal(check('input.key.press', { hotkeys: ['ctrl+c', 'ctrl+v'] }).length, 0); assert.equal(check('input.key.press', { preset: 'copy' }).length, 0); diff --git a/tests/unit/gui-await.test.mjs b/tests/unit/gui-await.test.mjs index 375e729..0570b47 100644 --- a/tests/unit/gui-await.test.mjs +++ b/tests/unit/gui-await.test.mjs @@ -9,7 +9,7 @@ function check(capName, args) { } /** - * gui.await 的**契约层**测试(v1.6)。 + * gui.await 的**契约层**测试(v1.6.0)。 * * 说明:条件命中/超时/轮询的**运行时行为**在 tests/e2e/await.mjs 里用真实 agent 进程 * 验证(macOS 本地可跑:file/process 条件)。本文件只守**不需要平台的**部分: @@ -71,7 +71,7 @@ test('MCP 已暴露 na_await 工具', async () => { assert.ok(/na_await/.test(src), 'MCP 应有 na_await 工具'); }); -// ---------- v1.7 属性谓词与组合条件 ---------- +// ---------- v2.0.0 属性谓词与组合条件 ---------- test('gui.await:where 与 any_of 通过 schema', () => { assert.equal(check('gui.await', { condition: 'control', where: { enabled: true } }).length, 0); @@ -107,7 +107,7 @@ test('any_of 求值语义:任一命中即算(等价于逻辑 OR)', () => { assert.ok(![{ hit: false }, { hit: false }].some((s) => s.hit), '全不命中 → 不命中'); }); -test('screen.find:where-only 必须放行(v1.8 修——真机验证暴露的缺口)', () => { +test('screen.find:where-only 必须放行(v2.0.0 修——真机验证暴露的缺口)', () => { // 之前入口校验只认 text,导致 where-only 被"text 不能为空"拦下; // 真机 2026-10-09 验证时抓到。此处锁住:where 非空即可,text 可省。 assert.equal(check('screen.find', { where: { enabled: true } }).length, 0, 'where-only 应通过'); diff --git a/tests/unit/install-zerotrust.test.mjs b/tests/unit/install-zerotrust.test.mjs index 6070b16..2bb7207 100644 --- a/tests/unit/install-zerotrust.test.mjs +++ b/tests/unit/install-zerotrust.test.mjs @@ -5,7 +5,7 @@ import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; /** - * 零信任优先安装(v24)的**源码断言**。 + * 零信任优先安装(v2.0.0)的**源码断言**。 * * 为什么用源码断言:install.ps1 是 5.1 脚本,Node 侧无法直接执行它; * 而这里要守的是「关键决策逻辑不许被误删」—— 一旦有人重构掉 acl 写入或 diff --git a/tests/unit/log-query.test.mjs b/tests/unit/log-query.test.mjs index 7b1c6d8..c40d9ca 100644 --- a/tests/unit/log-query.test.mjs +++ b/tests/unit/log-query.test.mjs @@ -6,7 +6,7 @@ import { tmpdir } from 'node:os'; import { logQuery, parseLeadingTs } from '../../apps/agent/dist/capabilities/log.js'; /** - * log.query 的纯逻辑测试(v1.8 方向二第一块)。 + * log.query 的纯逻辑测试(v2.0.0 方向二第一块)。 * 文件级行为(流式读取/白名单)用真实临时文件验证,无需被控端进程。 */ diff --git a/tests/unit/monitor.test.mjs b/tests/unit/monitor.test.mjs index 76da26a..a45af8a 100644 --- a/tests/unit/monitor.test.mjs +++ b/tests/unit/monitor.test.mjs @@ -3,7 +3,7 @@ import assert from 'node:assert/strict'; import { findCapability, validate, CapabilityNames } from '../../packages/protocol/dist/index.js'; /** - * monitor.* 的契约层测试(v1.8 方向二第二块)。 + * monitor.* 的契约层测试(v2.0.0 方向二第二块)。 * 运行时行为(真采样/落盘/摘要)在本地真机探针里验证:port 探测 + metric 采样 + * report 摘要 + stop 落盘。 */ diff --git a/tests/unit/security-hardening.test.mjs b/tests/unit/security-hardening.test.mjs index 691a1e7..8e07fc0 100644 --- a/tests/unit/security-hardening.test.mjs +++ b/tests/unit/security-hardening.test.mjs @@ -3,7 +3,7 @@ import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; /** - * v23 连接层防护的**规则**测试(server.ts 的纯逻辑部分)。 + * v2.0.0 连接层防护的**规则**测试(server.ts 的纯逻辑部分)。 * * 说明:封禁/上限/空闲的**端到端行为**需真实 agent 进程(已在本地实测三场景: * 正确密钥可连 → 连续错误被拒 → 封禁期连正确密钥也被拒)。本文件守**可脱离进程 @@ -74,7 +74,7 @@ test('install.ps1 的 stdin 读取:校验规则与防阻塞', () => { assert.equal(' a1b2c3d4e5f6a7b8\r\n'.trim(), 'a1b2c3d4e5f6a7b8', 'Trim 后应为纯 hex'); }); -// ---------- v26 自重启任务清理的保守判定(源码断言) ---------- +// ---------- v2.0.0 自重启任务清理的保守判定(源码断言) ---------- // // 用 includes 而非正则:这里断言的是源码字面片段,正则转义层数容易写错 // (臣本轮就在这上面栽过两次)。