refactor(versioning): 统一版本号 —— semver 为唯一版本号,开发批次代号并入其归属版本
## 问题(皇上指出) 项目里两套编号并行,无法判断哪个是"当前版本": - **发布版本**(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 全绿
This commit is contained in:
1 parent
5b69d6cec9
commit
e23788d717
41 files changed
+211
-161
No files matched your search
+5
-5
@@ -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),成功即清零,支持白名单豁免
|
||||
|
||||
@@ -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 全绿 | ✅ 已发布 |
|
||||
|
||||
## 文档
|
||||
|
||||
|
||||
@@ -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 的攻击者」
|
||||
|
||||
@@ -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<unknown> {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<unknown> {
|
||||
// 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<unknown> {
|
||||
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 (;;) {
|
||||
|
||||
@@ -54,9 +54,9 @@ export function createCapabilityRegistry(cfg: AgentConfig): Record<string, Capab
|
||||
[CapabilityNames.MouseDrag]: mouseDrag,
|
||||
[CapabilityNames.KeyType]: keyType,
|
||||
[CapabilityNames.KeyPress]: keyPress,
|
||||
// v1.6 等待条件(把盲试变为可断言)
|
||||
// v1.6.0 等待条件(把盲试变为可断言)
|
||||
[CapabilityNames.GuiAwait]: guiAwait,
|
||||
// v1.8 状态采样(方向二)
|
||||
// v2.0.0 状态采样(方向二)
|
||||
[CapabilityNames.LogQuery]: logQuery,
|
||||
[CapabilityNames.MonitorStart]: monitorStart,
|
||||
[CapabilityNames.MonitorReport]: monitorReport,
|
||||
@@ -67,7 +67,7 @@ export function createCapabilityRegistry(cfg: AgentConfig): Record<string, Capab
|
||||
[CapabilityNames.AuditList]: auditList,
|
||||
// v11 审计防篡改
|
||||
[CapabilityNames.AuditVerify]: auditVerify,
|
||||
// v25 审计链外部锚定(防整链重写)
|
||||
// v2.0.0 审计链外部锚定(防整链重写)
|
||||
[CapabilityNames.AuditHead]: auditHead,
|
||||
[CapabilityNames.AuditAnchor]: auditAnchor,
|
||||
// v13 成功指标
|
||||
|
||||
@@ -45,13 +45,13 @@ export const VK_MAP: Record<string, number> = {
|
||||
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<string, number> = {
|
||||
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<string, number> = {
|
||||
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,
|
||||
|
||||
@@ -183,7 +183,7 @@ public static class NAInput {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// v1.5:向目标进程的**全部顶层窗口** PostMessage 键盘消息(后端定向快捷键投放)。
|
||||
/// v1.5.0:向目标进程的**全部顶层窗口** PostMessage 键盘消息(后端定向快捷键投放)。
|
||||
///
|
||||
/// 场景:目标窗口不在前台(被遮挡 / 最小化 / 聊天窗口在后台),又要给它发快捷键。
|
||||
/// WM_KEYDOWN=0x0100,WM_KEYUP=0x0101;修饰键按顺序投递,调用方负责释放顺序。
|
||||
@@ -364,7 +364,7 @@ export async function keyPress(args: Args): Promise<unknown> {
|
||||
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<unknown> {
|
||||
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<unknown> {
|
||||
};
|
||||
}
|
||||
|
||||
// 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 }));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* log.query(v1.8,方向二「状态」第一块)。
|
||||
* log.query(v2.0.0,方向二「状态」第一块)。
|
||||
*
|
||||
* ## 为什么必须有
|
||||
* 治间歇性问题(代理不通、杀软拦截、端口时开时闭)要看**历史**,而被控端日志常在
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* monitor.*(v1.8,方向二「状态」第二块)—— 定时采样落盘与回看。
|
||||
* monitor.*(v2.0.0,方向二「状态」第二块)—— 定时采样落盘与回看。
|
||||
*
|
||||
* ## 为什么需要(复盘 §四)
|
||||
* 代理不通、杀软拦截、端口时开时闭都是**间歇性**的,而 `system.status` 只能看当下快照。
|
||||
|
||||
@@ -616,7 +616,7 @@ export async function auditVerify(_args: Args): Promise<unknown> {
|
||||
}
|
||||
|
||||
/**
|
||||
* v25:返回审计链的链头(只读)。
|
||||
* v2.0.0:返回审计链的链头(只读)。
|
||||
*
|
||||
* 用途:控制端定期拉取并存到**链外**(Mac 本地 / 另一台机器)—— 这是"外部锚定"的一半。
|
||||
* compare=true 时附带与最近一条锚点的比对结论(另一半)。
|
||||
@@ -628,7 +628,7 @@ export async function auditHead(args: Args): Promise<unknown> {
|
||||
}
|
||||
|
||||
/**
|
||||
* v25:把当前链头**追加**写到锚点文件(默认 <数据目录>/audit-anchors.jsonl)。
|
||||
* v2.0.0:把当前链头**追加**写到锚点文件(默认 <数据目录>/audit-anchors.jsonl)。
|
||||
*
|
||||
* 为什么是追加而不是覆盖:历史锚点一旦写成就不可被后续覆盖,否则攻击者重写链后
|
||||
* 再"刷新"锚点即可抹掉痕迹。追加式让每次锚定都留痕。
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"。
|
||||
|
||||
@@ -300,7 +300,7 @@ export async function screenFind(args: Args): Promise<unknown> {
|
||||
}
|
||||
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<unknown> {
|
||||
}
|
||||
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<string, unknown> | 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 {
|
||||
|
||||
@@ -43,7 +43,7 @@ export interface AgentConfig {
|
||||
allow_from?: string[];
|
||||
|
||||
/**
|
||||
* v23 连接层防护(三项,默认值见 server.ts)。
|
||||
* v2.0.0 连接层防护(三项,默认值见 server.ts)。
|
||||
* 单项缺省即启用默认策略;显式设 0 可关闭对应项。
|
||||
*/
|
||||
security?: {
|
||||
|
||||
@@ -261,7 +261,7 @@ export async function eventPoll(args: Args): Promise<unknown> {
|
||||
}
|
||||
|
||||
/** 连接断开时清理该连接创建的所有订阅(避免 watcher 泄漏)。 */
|
||||
/** v23:当前事件订阅数(空闲断开的豁免判据之一)。 */
|
||||
/** v2.0.0:当前事件订阅数(空闲断开的豁免判据之一)。 */
|
||||
export function watchCount(): number {
|
||||
return watches.size;
|
||||
}
|
||||
|
||||
+10
-10
@@ -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<WebSocket, ConnState>;
|
||||
}
|
||||
|
||||
@@ -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<WebSocket, ConnState>();
|
||||
|
||||
@@ -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<string, BanState>();
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<void> {
|
||||
fail('用法: nodeagent await --condition <window|control|process|file> --timeout <ms> [--text/--title/--process/--path] [--state absent]');
|
||||
}
|
||||
const args: Record<string, unknown> = { 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<string, string | undefined>)[k];
|
||||
if (raw !== undefined) {
|
||||
|
||||
@@ -641,7 +641,7 @@ export async function cmdClip(opts: Options): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- 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<void> {
|
||||
const logPath = opts.path;
|
||||
if (!logPath) fail('用法: nodeagent log --path <文件> [--pattern 正则] [--level ERROR] [--limit n] [--tail]');
|
||||
|
||||
@@ -183,7 +183,7 @@ export async function cmdRestart(opts: Options): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- v25 审计链外部锚定 ----------
|
||||
// ---------- v2.0.0 审计链外部锚定 ----------
|
||||
|
||||
/**
|
||||
* 打印审计链头(可选与最近锚点比对)。
|
||||
|
||||
@@ -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 <port|process|command|metric> [--target X] [--interval ms] [--id 名]
|
||||
启动定时采样并落盘 (v1.8):port 验连通性/process 验存活/
|
||||
启动定时采样并落盘 (v2.0.0):port 验连通性/process 验存活/
|
||||
command 取退出码/metric 取 CPU·内存
|
||||
nodeagent monitor report <id> 回看样本 + 摘要(断几次、最长断多久、值域)(v1.8)
|
||||
nodeagent monitor report <id> 回看样本 + 摘要(断几次、最长断多久、值域)(v2.0.0)
|
||||
nodeagent monitor stop|list|delete <id>
|
||||
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]
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -262,7 +262,7 @@ function resolveToolCall(toolName: string, input: Record<string, unknown>): 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);
|
||||
|
||||
@@ -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: {
|
||||
|
||||
+11
-11
@@ -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/` 为准,能力数由单测守卫。
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
| 无法 7×24 盯 | 臣只在被调用时"看一眼",问题在两次调用之间发生就看不到 |
|
||||
| 应用层内部状态不可见 | 如 WorkBuddy 的"当前正在等哪个请求",臣只能从日志反推 |
|
||||
|
||||
**缓解(✅ 已于 v1.8 实现)**:`monitor.*` 定时采样并落盘,事后回看摘要 —— 正是为治
|
||||
**缓解(✅ 已于 v2.0.0 实现)**:`monitor.*` 定时采样并落盘,事后回看摘要 —— 正是为治
|
||||
这类间歇性问题而做(原先只写"具备实现基础",现已成为能力):
|
||||
|
||||
```bash
|
||||
|
||||
@@ -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 步全绿。
|
||||
|
||||
|
||||
+9
-9
@@ -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` 字段代码没实现 | 承诺未落地 |
|
||||
|
||||
+7
-7
@@ -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. 应急处置
|
||||
|
||||
|
||||
@@ -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。
|
||||
|
||||
@@ -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 亦可,任选)。',
|
||||
|
||||
@@ -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/<id>.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 定时执行。' +
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* 快捷键(hotkey)解析与预设(v1.5)。
|
||||
* 快捷键(hotkey)解析与预设(v1.5.0)。
|
||||
*
|
||||
* ## 为什么放在 protocol 包
|
||||
* 解析逻辑必须**两端一致 + 可单测**:控制端 CLI 先把字符串展开成和弦列表用于本地校验,
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+4
-4
@@ -42,11 +42,11 @@ param(
|
||||
[switch]$Unattended,
|
||||
# 追加「开机即启动」触发器(不依赖用户登录)
|
||||
[switch]$AtStartup,
|
||||
# v23:从**标准输入**读密钥(而非命令行 -Key)。
|
||||
# v2.0.0:从**标准输入**读密钥(而非命令行 -Key)。
|
||||
# 为什么:命令行参数对同机其他用户可见(任务管理器/Get-CimInstance Win32_Process
|
||||
# 的 CommandLine 字段就是明文)。install.cmd 用 `echo <key>| 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**。
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from '../../apps/agent/dist/audit.js';
|
||||
|
||||
/**
|
||||
* v25 审计链外部锚定的逻辑测试。
|
||||
* v2.0.0 审计链外部锚定的逻辑测试。
|
||||
*
|
||||
* 守的是三件最容易写错的事:
|
||||
* 1. 只记 head_hash 会把**轮转**误判成篡改(必须连 entries + rotated_segments 一起记)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 应通过');
|
||||
|
||||
@@ -5,7 +5,7 @@ import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
/**
|
||||
* 零信任优先安装(v24)的**源码断言**。
|
||||
* 零信任优先安装(v2.0.0)的**源码断言**。
|
||||
*
|
||||
* 为什么用源码断言:install.ps1 是 5.1 脚本,Node 侧无法直接执行它;
|
||||
* 而这里要守的是「关键决策逻辑不许被误删」—— 一旦有人重构掉 acl 写入或
|
||||
|
||||
@@ -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 方向二第一块)。
|
||||
* 文件级行为(流式读取/白名单)用真实临时文件验证,无需被控端进程。
|
||||
*/
|
||||
|
||||
|
||||
@@ -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 落盘。
|
||||
*/
|
||||
|
||||
@@ -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 而非正则:这里断言的是源码字面片段,正则转义层数容易写错
|
||||
// (臣本轮就在这上面栽过两次)。
|
||||
|
||||
Reference in new issue
Block a user