# 桌面操控手 —— 手工验收指南(Acceptance) > **状态说明(务必先读)** > > 本文档是**手工验收步骤**,不是自动化测试,也不是「已通过」的证明。开发阶段**没有**对 Blender 或任何 > 真实 GUI 应用执行过端到端实操验证。请由人按照本文档逐步操作并记录实际结果。 > > 开发阶段**已核实**的周边事实(可作为环境健康的参考,不代替真实 GUI 验收): > > - 自动化测试全量通过:**167/167**(fake 输入后端 + 单元/集成测试)。 > - 一个临时 DSH profile(headless 模板)成功安装插件包并启动,**没有激活告警**。 > - 本机安全实机自检(`node scripts/live-smoke.mjs`,只读、不注入输入)成功:**2560×1600** 屏幕、 > 读取指针/前台窗口、写出 **2560×1600** PNG 截图、停止标志守卫拒绝动作、Pause 热键监听启停正常。 > - **真实 Blender/GUI 输入未经验证**,这正是本文档要补的验收。 > > - 英文工具名(如 `desktop_hand_screenshot`)与配置字段名保持原样,便于对照日志与代码。 > - 所有涉及真实输入/文件删除的步骤都可能影响你的机器:**请先在一次性测试账户或测试目录中执行**, > 并确认紧急停止热键可用(默认 **Pause**)。 > - 文中「预期结果」是依据当前代码推导的,若实际结果不同,请保留截图与审计日志并反馈。 > > 相关文档:[README(中文)](../README.zh.md) · [配置参考(CONFIG.md)](./CONFIG.md) --- ## 0. 通用前置条件 1. Windows 10/11,已安装 DSH;桌面操控手已安装并在 profile 中启用(见 README 的安装章节)。 2. DSH 正常运行,能看到 `/desktop-hand` 命令可用。 3. 建议先备份并确认配置文件位置(通常是 `%DSH_HOME%\desktop-hand.config.json`, 即 `C:\Users\<你>\.dsh\desktop-hand.config.json`)。`install.ps1` 首次安装时会把随包示例配置播种到这里。 4. 创建一个专用验收目录,下面的示例都使用它: ```powershell New-Item -ItemType Directory -Force D:\DH-Acceptance\work | Out-Null New-Item -ItemType Directory -Force D:\DH-Acceptance\readonly | Out-Null Set-Content -LiteralPath D:\DH-Acceptance\readonly\secret.txt -Value 'TOP SECRET - should never be read by the model' ``` 5. 打开一个会话,先确认基础状态: ``` /desktop-hand status ``` 预期(关键行): - `halted: no` - `input grant: none` - `files grant: none` - `provider: windows`(或其他可用后端;若为 `not started`,先调用一次只读工具触发懒启动) - `hotkey: Pause` - `delete mode: trash` - `trash dir:` `<配置的回收站路径>`(随包配置为 `D:\DSH_Trash`;默认是 `\desktop-hand\trash`) - `audit dir:` 默认 `\desktop-hand\audit` 6. 记住审计目录和本次会话 id(`/desktop-hand status` 第一行 `Desktop Hand status — session `;审计文件为 `\audit-.jsonl`,紧急停止等全局事件写入 `audit-global.jsonl`)。 7. (可选,推荐)先做一次**安全、只读**的实机自检,不注入任何输入: ```powershell cd D:\Windows\Desktop\Code\desktop-hand node scripts/live-smoke.mjs ``` 预期输出 JSON 中 `screen_info`、`pointer`、`foreground`、`screenshot`、`emergency_guard` 各步骤均为 `ok: true`,截图位于 `%TEMP%\dh-live-home\live-shot.png`。该脚本**不会移动鼠标、点击或输入**。 ### 关于审批(重要) 默认审批配置(`lib/config.js` / `desktop-hand.config.json`): | 类别 | 默认 | 影响 | |------|------|------| | `fileMutations` | `ask` | 写入/移动/删除/恢复:每次都要审批 | | `directDelete` | `ask` | 永久(direct)删除:逐次审批 | | `drag` | `ask` | `desktop_hand_drag`:逐次审批 | | `combo` | `ask` | 含 `+` 的组合键(如 `shift+a`):逐次审批 | | `text` | `grant` | 文本输入以 input 会话授权作为同意;前台标题命中敏感模式时仍会额外审批 | | `authorize` / `policyOverride` | `ask` | 模型侧授权请求 / 策略收紧 | **关键后果:** 如果 DSH 会话审批策略是 `never`(或审批通道不可用),**任何 `"ask"` 类别都会失败关闭**: 调用在进入工具主体之前就被拒绝,什么都不会发生。所以: - 只测 GUI 输入的场景(a、f 部分、g 部分)在 `never` 下也会受影响:Blender 场景的 `shift+a` 属于 `combo`,必须能弹出审批。 - 推荐做法:把 DSH 审批策略切到 `ask`(能看到提示并逐次确认);或者临时把相关类别改成 `"grant"` 并重启 DSH,依赖 `/desktop-hand authorize` 的时间/次数受限会话授权。 - 测文件删除/策略拒绝的场景(b、c、d、e)同理:要么审批可用,要么把 `fileMutations`(必要时 `directDelete`/`drag`/`combo`)设为 `"grant"`;测试结束后改回。 - 任何「预期结果」都假定调用已经通过审批、进入了工具主体逻辑;被审批拒绝的调用不会产生工具自己的 审计行(它在 `tools/pre-execute` 阶段就被拦下)。 --- ## 场景 A:Blender「新建立方体 + 细分曲面修改器级别 2」 > **本场景为手工场景,开发阶段尚未执行,不要把下面的预期当作已验证。** > 目标是验证「截图 → read_image 看图 → 一个动作 → 再截图」的真实 GUI 闭环,以及敏感热键选择(Pause)在 > Blender 中不冲突。 ### A.1 前置检查 1. 打开 Blender(建议 4.x),新建 General 场景。 2. 手动确认 Blender 窗口在前台(可以先用鼠标点一下窗口)。 3. 如果场景里已有默认立方体,本场景需要**新建立方体**:可手工在视图里按 `X` → 回车删除默认物体, 或接受「场景中已有一个立方体」并跳过 A.2 里新建方体的步骤(在验收记录中注明)。 4. **确认审批可用(本场景必需):** `shift+a` 属于 `approval.combo`,默认 `ask`。如果 DSH 会话审批 策略是 `never`,这一步会**失败关闭**。请先把 DSH 审批策略切到 `ask`(推荐,能看到提示),或临时在 `desktop-hand.config.json` 里把 `"approval": { "combo": "grant", "drag": "grant" }` 后**重启 DSH**; 验收结束后改回。否则 `desktop_hand_press_key { "keys": "shift+a" }` 会在工具执行前被拒绝。 5. 在 DSH 中检查前台窗口与状态: ``` /desktop-hand authorize input 10 200 ``` 然后让模型调用: - `desktop_hand_foreground_window` → 预期 `title` 包含 `Blender`,`sensitive: false`。 - `desktop_hand_get_pointer` → 记下指针坐标与显示器边界(坐标是虚拟桌面物理像素)。 - `desktop_hand_screenshot` → 返回 `path`(形如 `<会话工作目录>\.desktop-hand\screenshots\shot-.png`)。 - 用 `read_image`(DSH 自带工具)读取该 `path`,确认能看到 Blender 界面。 ### A.2 生成步骤(每一步之间都截图并 read_image 验证) | 步骤 | 操作(模型调用) | 说明 | |------|------------------|------| | A.2.1 | `desktop_hand_press_key { "keys": "shift+a" }` | 在 Blender 视图区打开「添加 / Add」菜单。必须确保鼠标在 3D 视图内、且是物体模式。这是 `combo`(`ask`):应在 DSH 中弹出审批,**由人确认**;若没有审批通道则该调用失败关闭(见 A.1 第 4 步)。 | | A.2.2 | `desktop_hand_screenshot` → `read_image` | 确认菜单已弹出,记录「网格 / Mesh」与「立方体 / Cube」的屏幕坐标。 | | A.2.3 | `desktop_hand_click { "x": …, "y": … }` | 点击「立方体 / Cube」。不同 Blender 版本/语言下菜单层级可能不同:可能需要先悬停或点击「网格 / Mesh」再点「立方体 / Cube」;**以截图为准**,每个子步骤只做一个点击。 | | A.2.4 | `desktop_hand_screenshot` → `read_image` | 确认视图里出现立方体、大纲视图出现 `Cube`。 | | A.2.5 | `desktop_hand_click` | 点击右侧「修改器属性 / Modifier Properties」(扳手图标)。坐标从截图读取。 | | A.2.6 | `desktop_hand_click` | 点击「添加修改器 / Add Modifier」。 | | A.2.7 | `desktop_hand_click` | 在弹出分类中选择「生成 / Generate」→「细分曲面 / Subdivision Surface」。若菜单不易定位,可改用 `desktop_hand_press_key { "keys": "f3" }`,再 `desktop_hand_type_text { "text": "Subdivision" }`,点击搜索结果中的「添加细分曲面修改器 / Add Subdivision Surface Modifier」。 | | A.2.8 | `desktop_hand_screenshot` → `read_image` | 确认修改器堆栈出现「细分曲面 / Subdivision Surface」。 | | A.2.9 | `desktop_hand_click` | 点击「级别视图 / Levels Viewport」数值框,`desktop_hand_press_key { "keys": "ctrl+a" }` 全选,然后 `desktop_hand_type_text { "text": "2" }`,`desktop_hand_press_key { "keys": "enter" }`。如界面为中文,字段名为「级别视图」;渲染级别「渲染 / Render Levels」可选设为 2。 | | A.2.10 | `desktop_hand_screenshot` → `read_image` | 确认立方体表面被细分(外观变圆润),修改器级别显示 2。 | > 目标结果:Blender 场景中有一个**新建立方体**,并带有一个 **Subdivision Surface(细分曲面)修改器**, > 「级别视图 / Levels Viewport」为 **2**。 ### A.3 预期工具调用与审计 预期调用序列(顺序可能因截图定位而略有出入): 1. `/desktop-hand authorize input 10 200`(用户命令) 2. `desktop_hand_foreground_window`(只读) 3. `desktop_hand_get_pointer`(只读) 4. `desktop_hand_screenshot` + `read_image` 5. `desktop_hand_press_key`(`shift+a`)—— 通过 `combo` 审批后才会执行 6. `desktop_hand_screenshot` + `read_image` 7. 若干 `desktop_hand_click`(每次一个点击,点击之间截图) 8. `desktop_hand_type_text`(`"2"`) 9. `desktop_hand_press_key`(`ctrl+a`,同样是 `combo` 审批)+ `desktop_hand_press_key`(`enter`) 10. 收尾 `desktop_hand_screenshot` + `read_image` > 审批预期:`shift+a`、`ctrl+a` 这类组合键默认 `ask`,每次都会出现审批提示(由人确认)。如果 A.1 > 第 4 步把 `combo` 设为 `grant`,则不再出现逐次提示,以 input 会话授权作为同意。`click`、 > `move_mouse`、单键、截图在默认配置下没有额外审批。 在 `\audit-.jsonl` 中应能看到(字段顺序无关,值为示例;组合键的 `press_key` 行只有在审批通过、动作真正执行后才会出现): ```json {"ts":"2026-10-31T08:00:00.000Z","at":"2026-10-31T08:00:00.000Z","sessionId":"","kind":"input","action":"press_key","keys":"shift+a","foregroundTitle":"Blender","sensitiveWindow":false,"tool":"desktop_hand_press_key","callId":"..."} {"ts":"...","sessionId":"...","kind":"input","action":"click","x":123,"y":456,"button":"left","count":1,"tool":"desktop_hand_click","callId":"..."} {"ts":"...","sessionId":"...","kind":"input","action":"type_text","foregroundTitle":"Blender","sensitiveWindow":false,"text":{"length":1,"preview":"2…","redacted":true},"tool":"desktop_hand_type_text","callId":"..."} {"ts":"...","sessionId":"...","kind":"observe","action":"screenshot","path":"...\\shot-....png","width":2560,"height":1440,"bytes":123456,"tool":"desktop_hand_screenshot","callId":"..."} ``` 每次成功输入的工具返回里应包含 `actionsUsed` / `actionsRemaining`(授权总次数 200,截图不扣次数)。 `/desktop-hand status` 的 `input grant` 行应显示已用动作数递增。 ### A.4 记录 - 实际是否成功:______(成功 / 失败 / 部分成功) - Blender 版本与界面语言:______ - 组合键审批是否正常出现(`shift+a`/`ctrl+a`):______(是 / 否,若否则检查 DSH 审批策略) - 失败时的截图与审计行:______ - 备注:本场景**未在开发阶段执行**;首次真机执行请重点观察热键 Pause 是否与 Blender 快捷键冲突 (预期不会;F9 才是冲突键,因此默认不用 F9)。 --- ## 场景 B:只读禁区读取被拒绝 ### B.1 准备 1. 创建测试文件(见前置条件): `D:\DH-Acceptance\readonly\secret.txt`。 2. 用**会话收紧**添加只读禁区(无需重启 DSH): ``` /desktop-hand tighten readonly D:\DH-Acceptance\readonly ``` 预期返回:`已为会话添加只读禁区:D:\DH-Acceptance\readonly ...`,并且 `desktop_hand_status` **工具输出**的 `session tightening` 行显示 `+1 readonly`(斜杠命令 `/desktop-hand status` 不显示该行,它显示授权/策略摘要)。 > 也可以用配置文件 `policy.readonly` 加一条后重启 DSH,但这会影响所有会话;验收推荐会话收紧。 ### B.2 调用与预期 让模型调用: - `desktop_hand_read_file { "file_path": "D:\\DH-Acceptance\\readonly\\secret.txt" }` 预期工具报错(双语、带明确原因),关键词: ``` 文件操作被策略拒绝:denied: cannot read "D:\DH-Acceptance\readonly\secret.txt": path matches read-only zone "D:\DH-Acceptance\readonly" ``` 并且**不会返回文件内容**。再调用 `desktop_hand_list_dir { "dir_path": "D:\\DH-Acceptance\\readonly" }` 同样应被拒绝(只读禁区同时禁止列出目录)。 ### B.3 审计证据 审计文件最后一行附近应出现: ```json {"ts":"...","sessionId":"...","kind":"fs-denied","action":"read_file","code":"readonly-zone","reason":"denied: cannot read ... read-only zone ...","rule":{"kind":"prefix","pattern":"D:\\DH-Acceptance\\readonly","display":"D:\\DH-Acceptance\\readonly","source":"session"},"tool":"desktop_hand_read_file","callId":"..."} ``` 对照组:读取一个不在禁区内的文件(例如 `D:\DH-Acceptance\work\hello.txt`,需先手工创建)应成功,证明 「只读不需要授权」且只有禁区在拦截。审计中会出现 `kind: "fs"`, `action: "read_file"`。 记录:实际结果 ______ --- ## 场景 C:回收站模式删除的证据链(隔离 → 列表 → 恢复) ### C.1 准备 1. 确认 `/desktop-hand status` 中 `delete mode: trash`,并记下 `trash dir:` 的实际路径 (随包配置为 `D:\DSH_Trash`;若使用默认值则是 `\desktop-hand\trash`)。 2. 创建待删文件: ```powershell Set-Content -LiteralPath D:\DH-Acceptance\work\delete-me.txt -Value 'desktop-hand acceptance trash test' ``` 3. 授予文件授权(按审批提示确认): ``` /desktop-hand authorize files 5 50 ``` ### C.2 删除 让模型调用: ```json desktop_hand_delete_path { "path": "D:\\DH-Acceptance\\work\\delete-me.txt", "reason": "acceptance-trash" } ``` 预期结果: - `mode: "trash"` - `trashId`:形如 `20261031T162738-1a2b3c4d`(UTC 时间戳 + 8 位十六进制) - `trashPath`:`\items\` - `size`、`mtime`、`type: "file"` - `restoreHint`:`desktop_hand_restore_path({ trash_id: "" })` - **原文件 `D:\DH-Acceptance\work\delete-me.txt` 已不存在**(被移动,而非复制)。 ### C.3 回收站证据(手工 + 工具) 手工检查(若你的 `trash dir` 不是 `D:\DSH_Trash`,把下面的路径替换成 C.1 记下的实际路径): ```powershell $id = '<上一步返回的 trashId>' Get-ChildItem -LiteralPath "D:\DSH_Trash\items\$id" # 应有 payload 与 meta.json Get-ChildItem -LiteralPath "D:\DSH_Trash\items\$id\payload" # 原文件内容 Get-Content -LiteralPath "D:\DSH_Trash\items\$id\meta.json" -Raw Get-Content -LiteralPath "D:\DSH_Trash\index.jsonl" -Tail 2 ``` `meta.json` 应包含这些字段(值不要求逐字一致): | 字段 | 预期 | |------|------| | `id` | 与 `trashId` 相同 | | `originalPath` | `D:\DH-Acceptance\work\delete-me.txt` | | `displayPath` | 同上(展示用路径) | | `payloadPath` | `\items\\payload` | | `size` / `type` / `mtime` | 原文件大小 / `file` / 修改时间 | | `deletedAt` | ISO 时间 | | `sessionId` / `callId` / `tool` | 会话 id、调用 id、`desktop_hand_delete_path` | | `reason` | `acceptance-trash` | | `policyRule` | 命中的规则;无特定规则时可为 `null` | | `mode` | `trash` | | `moveKind` | `rename` 或 `copy`(跨卷时退化) | | `restoredAt` / `restoredTo` | 初始为 `null` | `index.jsonl` 最后一行应为 `{"action":"quarantine","id":...}` 的 JSON。 工具侧:让模型调用 ```json desktop_hand_list_trash { "limit": 10 } ``` 预期在 `entries` 中找到该 `id`,包含 `originalPath`、`deletedAt`、`size`、`type`、`mode: "trash"`、`reason`。 ### C.4 恢复 ```json desktop_hand_restore_path { "trash_id": "" } ``` 预期: - `restoredTo` 为原始路径,`originalPath` 与删除时一致。 - 原文件重新出现,内容不变(`Get-Content` 应仍是 `desktop-hand acceptance trash test`)。 - `meta.json` 新增/更新 `restoredAt`、`restoredTo`、`restoreCount: 1`、`lastRestoreKind`。 - `index.jsonl` 追加 `{"action":"restore", ...}`。 - `desktop_hand_list_trash` 默认不再显示该条目(已恢复的条目默认隐藏)。 ### C.5 审计证据 审计中应出现: - `kind: "fs"`, `action: "delete_trash"`, `trashId`, `trashPath`, `type`, `size`, `reason` - `kind: "observe"`, `action: "list_trash"`, `entries: <数量>` - `kind: "fs"`, `action: "restore_path"`, `trashId`, `restoredTo` 记录:实际结果 ______ --- ## 场景 D:`forbid` 模式下删除被拒绝 ### D.1 方式一(推荐,无需重启):会话强制 forbid 1. 创建测试文件: ```powershell Set-Content -LiteralPath D:\DH-Acceptance\work\forbid-me.txt -Value 'should not be deleted' ``` 2. 授予文件授权:`/desktop-hand authorize files 5 50` 3. 收紧删除模式: ``` /desktop-hand tighten delete=forbid ``` 预期返回:`已将会话删除模式强制为 forbid(本会话所有删除都会被拒绝)`。 4. 调用: ```json desktop_hand_delete_path { "path": "D:\\DH-Acceptance\\work\\forbid-me.txt", "reason": "acceptance-forbid" } ``` 预期错误: ``` 文件操作被策略拒绝:denied: deleting "D:\DH-Acceptance\work\forbid-me.txt" is forbidden by policy (mode "forbid", source: session) ``` - 文件仍在原处。 - 回收站目录没有新增条目。 5. 审计预期:`kind: "fs-denied"`, `action: "delete_path"`, `code: "delete-forbidden"`,`rule.mode: "forbid"`、 `rule.kind: "session"`。 > 如果删除调用在到达策略引擎之前先被审批拦下(例如审批通道拒绝),那是另一种失败关闭路径;为了看到 > 本场景的策略拒绝,请先确认审批通道可用,或临时把 `approval.fileMutations` 设为 `"grant"` 并重启。 ### D.2 方式二(配置级 forbid) 1. 编辑 `%DSH_HOME%\desktop-hand.config.json`: ```json { "policy": { "delete": { "mode": "forbid" } } } ``` (只写这一项即可,其余字段会与默认值深合并;若文件中已有其他字段,请保留。) 2. 重启 DSH,确认 `/desktop-hand status` 显示 `delete mode: forbid`。 3. 重复 D.1 第 4 步,预期错误中 `source: global`。 4. 可选:在 `policy.delete.pathOverrides` 中为某个路径设置 `mode: "forbid"`,验证针对单路径的拒绝 (错误中 `rule` 为 `delete-override`,`source: override`)。 ### D.3 收紧不可逆 会话内执行 `tighten delete=forbid` 后,无法通过命令或工具放宽回 `trash`/`direct`(只有重启/新会话会 恢复配置模式)。预期行为,不是缺陷。 记录:实际结果 ______ --- ## 场景 E:`direct`(直接删除)模式下禁区仍然优先 目的:验证「只读禁区优先于删除模式」——即使删除模式是 `direct`,命中的只读禁区仍然拒绝删除, 不会发生永久删除。 ### E.1 准备(需要改配置并重启) 1. 创建文件:`D:\DH-Acceptance\readonly\do-not-delete.txt`。 2. 编辑 `%DSH_HOME%\desktop-hand.config.json`,确保包含: ```json { "policy": { "readonly": [ { "path": "D:\\DH-Acceptance\\readonly", "reason": "acceptance readonly" } ], "delete": { "mode": "direct", "directDeleteRequiresUnlock": true } } } ``` 3. 重启 DSH,`/desktop-hand status` 应显示 `delete mode: direct` 且 `readonly zones: 1`(或更多,取决于配置)。 4. 授予带直接删除解锁的文件授权: ``` /desktop-hand authorize files 5 50 direct-delete ``` ### E.2 调用与预期 ```json desktop_hand_delete_path { "path": "D:\\DH-Acceptance\\readonly\\do-not-delete.txt", "confirm_direct": true, "reason": "acceptance-zone-priority" } ``` 调用前请按审批提示确认(默认 `approval.directDelete: "ask"`,可能还会经过 `fileMutations` 审批; 若审批通道不可用则调用会失败关闭,这属于审批拒绝,不是本场景要验证的策略拒绝)。 预期结果: - 调用被拒绝,错误关键词 `read-only zone`(**而不是** direct/confirm 相关错误)。 - 文件仍然存在、内容不变。 - 回收站无新增;没有任何永久删除发生。 - 审计:`kind: "fs-denied"`, `action: "delete_path"`, `code: "readonly-zone"`。 对照(可选):删除一个**不在**只读禁区内、同样位于「可删除区域」的文件,在授权解锁并传 `confirm_direct: true` 后应成功(`mode: "direct"`,文件消失、回收站无条目、审计 `action: "delete_direct"`)。这一步会**永久删除**测试文件,请只用一次性文件。 ### E.3 复原 测试结束后把配置改回 `"mode": "trash"`、移除测试用只读禁区(或保留),重启 DSH。 记录:实际结果 ______ --- ## 场景 F:Pause 紧急停止与 `/desktop-hand resume` > 热键监听仅 **Windows** 支持。其他平台请用 `/desktop-hand stop` / `desktop_hand_emergency_stop` 代替 > 第 2 步,其余验收相同。 ### F.1 准备 1. 确认 DSH 日志出现过 `[desktop-hand] emergency-stop hotkey: Pause`(或 `/desktop-hand status` 显示 `hotkey: Pause`)。 2. 授予输入授权:`/desktop-hand authorize input 10 200`。 3. 调用一次 `desktop_hand_screenshot` 确认输入链路正常。 ### F.2 触发紧急停止 1. **人**按下键盘上的 **Pause** 键(部分键盘标为 `Pause/Break`)。 2. 预期(全部应发生): - DSH 日志出现 `EMERGENCY STOP (hotkey)`; - 停止标志文件被创建:`%DSH_HOME%\desktop-hand\emergency-stop.json`,字段形如(`source` 可能是 `hotkey` 或热键监听脚本的 `hotkey-monitor`,`detail` 是字符串,可能内含按键信息): ```json { "source": "hotkey", "detail": "{\"hotkey\":\"Pause\",\"at\":\"2026-10-31T08:00:00.000Z\"}", "at": "2026-10-31T08:00:00.000Z", "pid": 12345 } ``` - 模型侧收到注入通知:`【桌面操控手】操作已被用户中止(来源:hotkey)…`; - 输入后端进程被终止(下次使用会重新启动)。 3. `/desktop-hand status` 应显示 `halted: YES (...)`。 ### F.3 停止期间的工具行为 依次让模型调用,预期全部被拒绝,错误中含 `operations are emergency-stopped` / `操作已被用户中止`: - `desktop_hand_screenshot` - `desktop_hand_get_pointer` - `desktop_hand_click` - `desktop_hand_delete_path`(即使已有 files 授权) 预期例外(始终允许):`desktop_hand_status`、`desktop_hand_revoke`、`desktop_hand_emergency_stop`。 ### F.4 恢复 1. 由**人**执行: ``` /desktop-hand resume ``` 预期:`Desktop Hand emergency stop cleared (halted=false)...`;停止标志文件被删除;热键监听重新启动。 2. 再次 `desktop_hand_screenshot` 应成功(如果 input 授权仍未过期;过期则重新授权)。 3. `/desktop-hand status` 显示 `halted: no`。 ### F.5 模型侧紧急停止(补充) 在未停止状态下,让模型调用: ```json desktop_hand_emergency_stop { "reason": "acceptance model stop" } ``` 预期:`halted: true`、返回「Desktop Hand stopped…」;随后人工 `/desktop-hand resume` 恢复。 审计(`audit-global.jsonl` 与/或对应会话文件)应包含 `emergency-stop`、`emergency.halt`、 `emergency-reset` 相关记录。 记录:实际结果 ______ --- ## 场景 G:授权过期与动作预算 ### G.1 动作预算(input) 1. 授予一个很小的预算: ``` /desktop-hand authorize input 1 3 ``` `/desktop-hand status` 的 `input grant` 行应显示 `actions 0/3`。 2. 让模型执行 3 次安全动作(例如在空白桌面移动鼠标 `desktop_hand_move_mouse`,每次 1 个动作)。 每次返回的 `actionsUsed` 应依次为 1、2、3;第 3 次后 `actionsRemaining` 为 0。 3. 第 4 次再调用任意输入工具(包括 `desktop_hand_screenshot`,它本身不扣次数,但需要有效授权), 预期被拒绝,错误关键词: ``` input authorization is missing, expired or exhausted: ... action budget exhausted (used 3/3) ... ``` 4. `/desktop-hand status` 显示 `actions 3/3`。 ### G.2 时间过期(input) 1. 重新授权(替换旧授权、重置预算):`/desktop-hand authorize input 1 200`。 2. 等待超过 1 分钟(`session.inputDefaultMinutes` 的最小值是 1 分钟)。 3. 调用任意输入工具,预期错误关键词: ``` input authorization is missing, expired or exhausted: ... expired at ... ``` 4. `/desktop-hand status` 显示的过期时间应已过去。 ### G.3 文件动作预算(files,可选) 1. 准备 3 个一次性测试文件。 2. `/desktop-hand authorize files 1 2`。 3. 连续两次 `desktop_hand_delete_path`(trash 模式,需通过审批)应成功,`actionsUsed` 为 1、2。 4. 第三次删除预期被拒绝:`file mutation authorization is ... exhausted (used 2/2)`。 ### G.4 授权是会话级、内存态的补充验证(可选) - 重新授权会**替换**旧授权(新的到期时间与预算,`actionsUsed` 归零),不会叠加。 - 重启 DSH 后所有授权消失,`/desktop-hand status` 恢复 `none`。 - 撤销立即生效:`/desktop-hand revoke` 之后任何输入/文件改动工具都应被拒绝(只读不受影响)。 审计预期:`/desktop-hand authorize` 会写 `kind: "authorization"`, `action: "grant"`, `source: "slash-command"`;授权管理器还会写 `kind: "authorization.grant"`;每次成功输入动作后写 `kind: "authorization.consume"`(含 `consumed` / `remaining`)。过期或用尽发生在 `tools/pre-execute` 阶段,不会产生工具自己的审计行(见附录 A 的注意)。 记录:实际结果 ______ --- ## 附录 A:审计日志速查 - 位置:`\audit-.jsonl`;全局事件(紧急停止等)写 `audit-global.jsonl`。 - 每行一个 JSON 对象;公共字段:`ts`(写入时间)、`sessionId`、`kind`、`action`、`tool`、`callId`、 `at`(运行时钟,部分记录有)。 - 常见 `kind`: | kind | 产生位置 | 关键字段 | |------|----------|----------| | `authorization` | 斜杠命令 / `desktop_hand_authorize` / `revoke` / `policy_override` 的运行时记录 | `action`, `mode`, `grant`, `source` | | `authorization.grant` / `authorization.consume` / `authorization.revoke` / `authorization.tighten` | 授权管理器(`lib/security/authorization.js`)的审计事件 | `mode`, `grantId`, `minutes`, `maxActions`, `consumed`, `remaining` 等 | | `input` | 输入工具成功 | `action`, `keys`/`x`/`y`/`text`/`path`, `foregroundTitle`, `sensitiveWindow` | | `input-denied` | 输入工具执行期失败/被拒(provider、坐标、限制等) | `action`, `error` | | `observe` | 只读工具 | `action`, `path`/`title`/`entries`/`pointer` | | `fs` | 文件工具成功 | `action`, `path`/`source`/`destination`/`trashId`, `policy` | | `fs-denied` | 文件工具被策略拒绝(readonly/write-protected/delete mode 等) | `action`, `code`, `reason`, `rule` | | `emergency-stop` / `emergency.halt` / `emergency.reset` | 紧急停止/恢复 | `source`, `detail`, `hotkey` | > 注意:**授权过期/次数用尽、审批拒绝**发生在 `tools/pre-execute` 阶段,工具主体没有执行,因此不会产生 > 该工具自己的审计行(例如 `input-denied`);它们会体现在调用方看到的拒绝原因、`/desktop-hand status` > 的授权状态以及 DSH 自身的日志中。不要把「没有审计行」当作调用成功。 查看末尾几条(PowerShell): ```powershell $audit = Join-Path $env:DSH_HOME 'desktop-hand\audit' Get-ChildItem $audit Get-Content (Get-ChildItem $audit -Filter 'audit-*.jsonl' | Sort-Object LastWriteTime | Select-Object -Last 1).FullName -Tail 20 ``` ## 附录 B:验收后清理 ```powershell Remove-Item -LiteralPath D:\DH-Acceptance -Recurse -Force # 回收站中的测试条目可手工删除(若确认无用): # Remove-Item -LiteralPath D:\DSH_Trash\items\ -Recurse -Force # 若测试时改过配置,请把 policy.delete.mode / approval.fileMutations 改回原值并重启 DSH。 ``` ## 附录 C:结果汇总表 | 场景 | 结果(通过/失败/未执行) | 备注 | |------|--------------------------|------| | A. Blender 建方体 + 细分曲面级别 2 | **未执行(手工场景)** | 开发阶段无真实 GUI 验证 | | B. 只读禁区读取拒绝 | ______ | | | C. 回收站删除证据 + 恢复 | ______ | | | D. forbid 模式删除拒绝 | ______ | | | E. direct 模式下禁区优先 | ______ | | | F. Pause 紧急停止 + resume | ______ | | | G. 授权过期 / 动作预算 | ______ | |