# 桌面操控手(Desktop Hand) [![GitHub release](https://img.shields.io/github/v/release/Zero20230619/desktop-hand)](https://github.com/Zero20230619/desktop-hand/releases) [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) **让 DeepSeek Harness 里的模型直接操作真实的桌面鼠标和键盘,并提供一套带禁区、回收站删除与会话级授权的文件工具。** > ## ⚠️ 高风险提醒 —— 请先读这一段 > > 桌面操控手会让模型**操作你的真实桌面**:移动真实鼠标、点击、拖拽、输入文本、按快捷键、截屏, > 以及在本机**写入、移动、删除和恢复真实文件**。 > > - 真实输入和文件改动**默认全部关闭**,必须由**人**批准一份**有期限、有动作次数上限**的会话级授权后 > 才能进行(默认 `approval.authorize: "ask"`;若你的 DSH 审批策略会自动批准,这层保障会被削弱—— > 详见 [docs/CONFIG.md](docs/CONFIG.md))。 > - 它**不是沙箱**:不是虚拟机,不是独立账户,也不是操作系统权限边界。桌面操控手以 DeepSeek Harness > 进程的权限运行,除了被策略保护的路径外,凡是该进程能碰的东西它都能碰。 > - 文件策略只约束**桌面操控手自己的工具**(除非你开启 `policy.enforceOnDshFsTools`)。`bash`、`pwsh` > 等 shell 工具不受约束,可以绕过它。 > - 删除默认走**回收站(隔离区)模式**,可恢复;但 `direct`(直接删除)一旦由用户解锁就是**永久销毁**。 > - **请先用 fake 输入后端或一次性测试账户,按 [docs/ACCEPTANCE.md](docs/ACCEPTANCE.md) 走完验收,并把手放在 > 紧急停止热键上。** 默认热键是 **Pause**(刻意不用 `F9`:Blender 的「调整上一步操作」占用了 F9, > 建模时会误触)。 > - 开发阶段**没有**对 Blender 等真实应用做过端到端 GUI 实操验证,自动化测试只使用 fake 输入后端和单元测试。 > 真实应用的验收属于手工步骤。 --- ## 目录 - [这是什么](#这是什么) - [架构](#架构) - [环境要求与平台支持](#环境要求与平台支持) - [安装](#安装) - [快速上手](#快速上手) - [工具列表](#工具列表) - [斜杠命令](#斜杠命令) - [配置](#配置) - [文件策略、删除模式与回收站](#文件策略删除模式与回收站) - [安全模型与威胁模型](#安全模型与威胁模型) - [常见问题排查](#常见问题排查) - [开发与测试](#开发与测试) - [许可证](#许可证) --- ## 这是什么 桌面操控手是一个 DeepSeek Harness(DSH)插件,为模型提供两类被严格隔开的能力: 1. **真实桌面输入**:观察屏幕(`get_pointer`、`foreground_window`、`screenshot`),并操作真实桌面 (`move_mouse`、`click`、`drag`、`scroll`、`press_key`、`type_text`)。模型按「看 → 做 → 再看」的 循环工作:截图 → 用 DSH 的 `read_image` 查看 → 只做一个动作 → 再截图确认。 2. **受策略保护的文件操作**:`read_file`、`list_dir`、`write_file`、`move_path`、`delete_path`、 `restore_path`、`list_trash`。读取不需要授权,但仍受禁区约束;写入、移动、删除需要单独的一份 **files(文件)授权**。删除默认进入可恢复的「回收站」隔离区,带元数据、只增日志和一次调用即可恢复的能力。 所有这些能力同时受以下机制约束:互相独立的 **input(输入)** 与 **files(文件)** 会话级授权、每次调用的 **审批**策略、**动作与坐标上限**、**文件禁区/系统关键区/敏感屏幕区域**、**全系统紧急停止**,以及 **JSONL 审计日志**。 **它不是**:远程桌面协议、沙箱、后台常驻服务,也不能替代 DSH 自带的 `read`/`write`/`edit` 工具 (日常工作区的文件编辑请继续使用那些工具)。 --- ## 架构 ``` DSH 宿主进程 │ │ profile patch 行(cordis.patch.yml / install.ps1 写入的托管块) ▼ index.js —— 插件入口 │ • Config schema(部署级覆盖:enabled/configPath/provider/trashDir/…) │ • 注册 21 个工具、/desktop-hand 命令、系统提示词指引 │ • tools/pre-execute:授权 + 审批 + 紧急停止拦截 │ • 启动紧急停止热键监听 ▼ lib/runtime.js —— 每次插件激活只创建一次 │ ├── lib/config.js 默认值 · 配置发现 · 深合并 · 校验 · 路径展开 │ │ │ ├── lib/tools/ │ │ input-tools.js 观察 + 鼠标/键盘/文本/截图(需要 input 授权) │ │ fs-tools.js 读/列目录/写/移动/删除/恢复/回收站(写操作需要 files 授权) │ │ control-tools.js 授权 · 撤销 · 状态 · 策略收紧 · 紧急停止 │ │ │ ├── lib/security/ │ │ authorization.js 会话授权、动作预算、敏感区/直接删除解锁、会话收紧 │ │ limits.js 坐标/时长/次数上限、敏感屏幕区域、窗口标题模式 │ │ emergency.js 暂停/恢复、AbortSignal、监听器 │ │ │ ├── lib/fs/ │ │ policy.js 禁区 + 受保护根目录 → 允许/拒绝;删除模式 │ │ trash.js /items//payload + meta.json + index.jsonl │ │ audit.js audit-.jsonl、轮转、输入文本脱敏 │ │ │ └── lib/input/ │ index.js 输入后端工厂(auto | windows | linux | macos | fake | disabled) │ windows.js 常驻 PowerShell JSON-lines 代理(scripts/input-agent.ps1) │ linux.js xdotool 逐条命令 │ macos.js cliclick + osascript + screencapture │ fake.js 确定性的内存假后端(测试 / 演练) │ hotkey.js 隐藏的 scripts/hotkey-monitor.ps1 轮询器(仅 Windows) │ └── 审计文件、回收站、截图(工作区 .desktop-hand/screenshots) ``` --- ## 环境要求与平台支持 | 平台 | 真实输入 | 截图 | 紧急停止热键 | 说明 | |------|---------|------|-------------|------| | **Windows 10/11** | ✅ 完整 | ✅ 完整 | ✅ 默认 **Pause** | 主目标平台。使用 Windows PowerShell 5.1(`powershell.exe`,回退 `pwsh.exe`)、常驻输入代理和隐藏的热键监听进程,无需额外安装组件。 | | **Linux** | ⚠️ 尽力而为 | ⚠️ 尽力而为 | ❌ 无 | 需要 PATH 中有 `xdotool`;截图依赖 ImageMagick `import`、`scrot` 或 `gnome-screenshot`。多显示器只上报为一块显示区域。紧急停止仍可用 `/desktop-hand stop` 或 `desktop_hand_emergency_stop`;输入后端每次操作前也会检查停止标志文件。 | | **macOS** | ⚠️ 尽力而为 | ⚠️ 尽力而为 | ❌ 无 | 需要 `cliclick`;使用 `osascript`、`pbcopy`+`Cmd+V` 输入文本(**会改动剪贴板**)和 `screencapture`。不支持滚轮和中键点击,拖拽仅左键;需要手动授予辅助功能/自动化权限。无热键监听。 | | **fake** | —(模拟) | ✅ 生成真实 PNG | ❌ 无 | 确定性的内存假后端,用于测试/演练(`input.provider: "fake"`),完全不碰真实桌面。 | | **disabled** | ❌ | ❌ | ❌ | `input.provider: "disabled"`,输入工具直接失败关闭。 | 运行要求:**Node.js ≥ 22**(插件为 ESM,见 `package.json` 的 `engines`)、带 profile 的 DSH; Windows 上需要 PowerShell 5.1 或 PowerShell 7。本仓库测试在 Windows + Node **v26.13.0** 上运行通过。 --- ## 安装 插件包目录是 `desktop-hand/`(本仓库/本文件夹),包含 `index.js`、`lib/`、`scripts/`、`locale/`、 `cordis.patch.yml`、`desktop-hand.config.json`、`install.ps1`、`uninstall.ps1` 以及本文档。 ### 推荐方式:`install.ps1` ```powershell # DSH 运行时已设置 DSH_PROFILE_DIR;若没有则显式传入。 pwsh -File .\install.ps1 -ProfileDir "$env:DSH_PROFILE_DIR" ``` 参数: | 参数 | 默认值 | 含义 | |------|--------|------| | `-ProfileDir` | `$env:DSH_PROFILE_DIR` | 目标 DSH profile(必须已存在 `node_modules/` 和 `cordis.patch.yml`)。 | | `-SourceDir` | 脚本所在目录 | 要安装的插件包目录。 | | `-DshHome` | `$env:DSH_HOME` 或 `$HOME\.dsh` | 播种 `desktop-hand.config.json` 的位置。 | | `-SkipConfig` | 关闭 | 不播种配置文件。 | | `-Force` | 关闭 | 覆盖已安装副本(例如应用可能正在运行)。 | 脚本会做这些事: 1. 校验源包(`package.json`、`index.js`、包名必须是 `desktop-hand`)。 2. 把 `cordis.patch.yml` 备份为 `cordis.patch.yml.bak-<时间戳>`。 3. 如果 profile patch 里在托管标记之外已有一行**手写的** `desktop-hand` 配置,脚本会拒绝执行,需先移除。 4. 把插件包复制到 `/node_modules/desktop-hand`(跳过 `node_modules`、`.git`、`.desktop-hand`、 `test`、`coverage`)。 5. 仅在文件不存在时播种 `/desktop-hand.config.json`(让你的策略在升级后保留)。 6. 在 profile 的 `cordis.patch.yml` 末尾写入幂等的托管块: ```yaml # >>> desktop-hand managed block (install.ps1) >>> - insert: - id: desktop-hand name: desktop-hand config: {} # <<< desktop-hand managed block <<< ``` ### 手工修改 profile patch 不想运行脚本时: 1. 把插件包目录复制到 `/node_modules/desktop-hand`。 2. 把上面的 `- insert:` 行(不加托管标记也可以)加到 profile 的 `cordis.patch.yml` **末尾**—— 它是 bundle 层之后应用的用户 patch 层。 3. 重启 DSH(或重新加载 profile)。 ### 开发用补丁叠加层(`--patch`) DSH 启动器支持通过 `--patch <文件>` 追加补丁层(启动器参数必须位于应用参数之前)。补丁文件是与 `cordis.patch.yml` 相同的 YAML 列表;各层按顺序叠加:**bundle 层 → profile patch → `--patch` 叠加层**。 开发时先让插件包安装进 profile(执行一次 `install.ps1`,之后每次改动用 `-Force` 覆盖), 再用叠加层按次启用/禁用或改配置,而不必反复编辑 profile patch: ```yaml # dev.patch.yml - insert: - id: desktop-hand name: desktop-hand config: enabled: true provider: windows ``` ```powershell # 示例:带叠加层启动另一个 DSH 实例。请使用你所装 DSH 版本的启动器命令/参数; # 启动器参数必须位于应用参数之前。 dsh --patch .\dev.patch.yml ``` 叠加层是启动器能力,不是包管理器,不会替你复制任何文件。叠加层里的行可以指向本地 `index.js` 路径(在部分环境可用于行本身),**但插件里裸写的 `@deepseek-ai/*` import 仍然需要 profile 里已安装 的副本才能解析**。所以请先安装(`install.ps1` 会复制插件包),再用 `--patch` 只覆盖行/配置。 ### 重启 / 热更新 `install.ps1` 结束时会提示:「重启 DeepSeek Harness(或等待 profile HMR 拾取新包)」。正式部署请 **重启 DSH**。配置文件只在插件激活时读取一次(没有文件监听),因此**改配置后也需要重启/重载**。 ### 卸载 ```powershell pwsh -File .\uninstall.ps1 -ProfileDir "$env:DSH_PROFILE_DIR" ``` 会移除托管块(先备份 patch)和已安装的包。**用户数据会保留**:`desktop-hand.config.json`、审计目录和 回收站目录都保留,以便之后审计或恢复历史删除。 ### 目录结构 ``` desktop-hand/ ├── index.js 插件入口:Config、工具注册、审批拦截、系统提示词 ├── lib/ 运行时、配置、工具、安全、文件策略、输入层 ├── scripts/ │ ├── input-agent.ps1 常驻 Windows JSON-lines 输入代理 │ ├── hotkey-monitor.ps1 隐藏的 GetAsyncKeyState 轮询器(紧急热键) │ └── live-smoke.mjs 安全、只读的后端实机自检(不注入输入) ├── locale/{en,zh}.json 插件市场元数据 ├── cordis.patch.yml 随包发布的 bundle 层 patch ├── desktop-hand.config.json 示例/用户可编辑配置(install.ps1 会播种) ├── install.ps1 / uninstall.ps1 ├── docs/{CONFIG.md,ACCEPTANCE.md} └── test/ node:test 单元测试(fake 后端) ``` --- ## 快速上手 1. **安装并重启 DSH**,在一个一次性工作区里打开会话。 2. **查看状态**(用户专用命令,任何时候都允许): ``` /desktop-hand status ``` 确认 `provider`、`config`、`hotkey: Pause`、`delete mode: trash`,以及 input/files 两份授权都是 `none`。 3. **授予有时限、有次数上限的输入授权**: ``` /desktop-hand authorize input 10 200 ``` (需要点击任务栏/屏幕角落时加 `sensitive`:`/desktop-hand authorize input 10 200 sensitive`。) 请只给**短**预算:授权会过期,也受动作次数限制。 4. **先看屏幕,再一步一步做**: - 模型先用 `desktop_hand_get_pointer` 和 `desktop_hand_foreground_window` 获取坐标、确认目标窗口; - `desktop_hand_screenshot` 把 PNG 存到 `<会话工作目录>/.desktop-hand/screenshots/`; - 模型必须再用 DSH 的 **`read_image`** 读取该路径才真正「看见」画面; - 每次工具调用只做一个 `click` / `press_key` / `type_text`,然后再次截图确认。 5. **文件改动需要另一份授权**: ``` /desktop-hand authorize files 5 50 ``` 默认 `approval.fileMutations: "ask"`,每次写入/移动/删除/恢复还需要实时审批。如果 DSH 关闭了审批 (策略为 `never`),这些调用会**失败关闭**;如何改成 `"grant"` 见 [docs/CONFIG.md](docs/CONFIG.md)。 拖拽和组合键(`approval.drag`/`approval.combo`)默认也走审批——Blender 的 `shift+a` 这一步需要有可用 的审批提示,或把这两个类别改成 `"grant"`。 6. **随时按 `Pause` 紧急停止**,或输入 `/desktop-hand stop`。只有人才能执行 `/desktop-hand resume`。 > 不想碰真实桌面时:把配置改为 `"input": { "provider": "fake" }` 并重启,就能用同一套流程在确定性的 > 假桌面上演练。 --- ## 工具列表 共 **21 个**模型可见工具(`input` 授权 = 会话级输入授权;`files` 授权 = 会话级文件操作授权;标为 **只读**的工具不需要授权,但仍受文件禁区约束;紧急停止期间除 `status`、`revoke`、`emergency_stop` 外全部被拦截)。 | # | 工具 | 作用 | 授权 / 审批 | 限制与说明 | |---|------|------|------------|-----------| | 1 | `desktop_hand_get_pointer` | 返回当前指针坐标、各显示器边界、虚拟桌面边界、前台窗口标题与句柄。 | **只读**,无需授权 | 点击前用来确认坐标。 | | 2 | `desktop_hand_foreground_window` | 返回前台窗口标题、原生句柄、进程号,并报告标题是否命中敏感窗口模式。 | **只读**,无需授权 | 输入文本/快捷键前用来确认目标应用。 | | 3 | `desktop_hand_move_mouse` | 把真实指针移动到虚拟桌面绝对坐标。 | **input** 授权 | 必填 `x`、`y`;`duration_ms` 默认 `0`(瞬移),上限 `limits.maxMoveDurationMs`(5000)。消耗 1 次输入动作。 | | 4 | `desktop_hand_click` | 在坐标处(或省略 `x`/`y` 时的当前指针处)点击真实鼠标键。 | **input** 授权 | `button` 取 left/right/middle(默认 left);`count` 默认 1,上限 `limits.maxClicksPerCall`(3);`interval_ms` 默认 80。命中敏感屏幕区域会被拒绝,除非授权解锁了敏感区。消耗 1 次动作。 | | 5 | `desktop_hand_drag` | 按下鼠标、按时长移动到终点、松开。 | **input** 授权;审批 `drag`(默认 `ask`) | 必填 `from_*`、`to_*`;`duration_ms` 默认 500,上限 `limits.maxDragDurationMs`(10000);起点受敏感区域检查。消耗 1 次动作。 | | 6 | `desktop_hand_scroll` | 在指针处或指定 `x`/`y` 处滚轮滚动。 | **input** 授权 | 必填 `direction`;`amount` 默认 3,上限 `limits.maxScrollAmount`(50)。消耗 1 次动作。 | | 7 | `desktop_hand_press_key` | 在真实键盘上按单键或组合键,如 `shift+a`、`ctrl+z`、`f12`、`win+r`。 | **input** 授权;含 `+` 的组合键受审批 `combo`(默认 `ask`) | 必填 `keys`;`hold_ms` 默认 40;组合键段数上限 `limits.maxComboKeys`(4)。消耗 1 次动作。 | | 8 | `desktop_hand_type_text` | 把 Unicode 文本作为真实键盘输入送进前台应用。 | **input** 授权;审批 `text`(默认 `grant`);命中敏感窗口标题时额外审批 | 必填 `text`(不能为空),上限 `limits.maxTextChars`(2000 个 UTF-16 码元);`interval_ms` 默认 `input.textIntervalMs`(8)。审计默认脱敏,除非 `audit.logTypedText` 为 true。消耗 1 次动作。 | | 9 | `desktop_hand_screenshot` | 截取全部显示器或指定区域,保存为会话工作区内的 PNG。 | **input** 授权(屏幕内容属于隐私) | 可选 `region_x/y/width/height`;PNG 不得超过 `screenshots.maxBytes`(8 MiB);**不消耗**动作预算。需再用 DSH 的 `read_image` 读取返回路径才能看到。 | | 10 | `desktop_hand_read_file` | 读取 UTF-8 文本文件,附带大小/修改时间。 | **只读**,无需授权 | `file_path`;`max_bytes` 默认/上限 1 MiB;`offset` 按字节;超过 64 MiB 与二进制文件会被拒绝;只读禁区会带明确原因拒绝。不消耗动作。 | | 11 | `desktop_hand_list_dir` | 列出目录的直接子项(类型/大小/修改时间,含隐藏项)。 | **只读**,无需授权 | `dir_path`;`max_entries` 默认/上限 500;只读禁区拒绝。 | | 12 | `desktop_hand_write_file` | 创建或修改 UTF-8 文本文件。 | **files** 授权;审批 `fileMutations`(默认 `ask`) | `file_path`、`content`;`mode` 为 overwrite(默认)/append;`create_dirs` 默认 false;受禁区、系统关键位置与插件数据保护约束。消耗 1 次文件动作。 | | 13 | `desktop_hand_move_path` | 移动或重命名文件/目录。 | **files** 授权;审批 `fileMutations`(默认 `ask`) | `source_path`、`dest_path`;`overwrite` 默认 false;**源与目标两端**都做禁区检查。消耗 1 次动作。 | | 14 | `desktop_hand_delete_path` | 按当前删除模式删除文件/目录。 | **files** 授权;直接删除依次受 `directDelete`、`fileMutations` 审批,回收站模式受 `fileMutations` 审批 | `path`;`recursive`;`confirm_direct`(永久删除必填);`reason`(写入审计与回收站元数据)。回收站模式**移动**到隔离区并返回 `trashId`/`trashPath`;`forbid` 直接拒绝;`direct` 需要解锁 + `confirm_direct=true`(目录还需 `recursive=true`)。消耗 1 次动作。 | | 15 | `desktop_hand_restore_path` | 按 id 恢复一条隔离区删除。 | **files** 授权;审批 `fileMutations`(默认 `ask`) | `trash_id`;`destination`(默认原路径);`overwrite` 默认 false;目标路径会重新做禁区检查。消耗 1 次动作。 | | 16 | `desktop_hand_list_trash` | 列出最近的隔离区条目(id、原路径、删除时间、大小、原因)。 | **只读**,无需授权 | `limit` 默认 50,上限 200;不读取 payload 内容。 | | 17 | `desktop_hand_authorize` | 由模型侧发起会话授权请求。 | 审批 `authorize`(默认 `ask`) | `mode` 取 input/files;`minutes`/`max_actions` 会被夹到会话上限;`allow_sensitive_zones`;`allow_direct_delete`;`note`。建议优先走人手动 `/desktop-hand authorize`。 | | 18 | `desktop_hand_revoke` | 立即撤销本会话的输入和/或文件授权。 | **无需授权**,始终允许 | `mode` 取 input/files/all(默认 all)。紧急停止期间也可用。 | | 19 | `desktop_hand_status` | 报告授权、紧急停止状态、输入后端、策略摘要、目录与审批模式。 | **只读**,始终允许 | 仅读取状态;自身标为高风险工具。 | | 20 | `desktop_hand_policy_override` | **仅收紧**本会话的文件策略。 | 审批 `policyOverride`(默认 `ask`) | `add_readonly[]`、`add_write_protected[]`、`force_delete_mode: "forbid"`、`reason`。无法删除禁区,也无法放宽删除模式。 | | 21 | `desktop_hand_emergency_stop` | 立即中止所有桌面操控手操作并杀掉输入后端。 | **无需授权**,始终允许 | `reason`(写审计)。除 `status`/`revoke`/`emergency_stop` 外全部被拦截,直到人执行 `/desktop-hand resume`。 | 说明: - **每次调用只做一个原子动作。** 系统提示词要求模型每次只做一个点击/组合键/文本/拖拽,并用截图验证后 再做下一步。授权次数在动作**成功之后**扣减(截图和只读工具不扣次数)。 - **两类授权互相独立。** input 授权绝不允许文件改动,反之亦然。 - **工具动作与策略拒绝都会审计。** 多数调用会追加一条审计记录,拒绝(`input-denied`、`fs-denied`) 会连同命中的规则/原因一起记录。当授权/审批/紧急停止判定不允许时,`tools/pre-execute` 会在工具主体 执行之前直接拒绝。 --- ## 斜杠命令 `/desktop-hand` 是**用户专用**斜杠命令,不会进入模型对话,因此它是权威的「人工同意」路径。 | 命令 | 作用 | |------|------| | `/desktop-hand` 或 `/desktop-hand status` | 显示停止状态、input/files 授权(到期时间、已用/上限动作数)、输入后端、配置路径、审计/回收站目录、删除模式、热键、策略摘要。 | | `/desktop-hand authorize input [分钟] [次数] [sensitive]` | 授予本会话真实鼠标/键盘/文本/截图权限。默认取 `session.inputDefaultMinutes`(10 分钟)与 `inputDefaultActions`(200 次),并夹到配置上限。加 `sensitive` 会在本次授权中解锁敏感屏幕区域。 | | `/desktop-hand authorize files [分钟] [次数] [direct-delete]` | 授予文件写入/移动/删除/恢复权限。默认 5 分钟 / 50 次。`direct-delete` 解锁永久删除(每次调用仍需 `confirm_direct=true`,按配置可能还要逐次审批)。 | | `/desktop-hand revoke [input\|files\|all]` | 立即撤销(默认 `all`)。 | | `/desktop-hand tighten readonly <模式>` | 为本次会话追加只读禁区(语法同配置:路径、通配符或 `/正则/`)。 | | `/desktop-hand tighten write <模式>` | 为本次会话追加写保护禁区。 | | `/desktop-hand tighten delete=forbid` | 本会话强制删除模式为 `forbid`。 | | `/desktop-hand resume`(别名 `reset`) | 解除紧急停止。**只有人**能执行。 | | `/desktop-hand stop`(别名 `halt`) | 立即紧急停止。 | | `/desktop-hand policy` | 打印当前生效的文件策略(配置禁区、删除覆盖规则)。 | | `/desktop-hand help` | 显示命令语法。 | 会话收紧是**只增不减**的:只能追加禁区、只能强制 `forbid`,不能删除禁区或放宽删除模式。它只存在于内存, 会随会话/重启消失。 --- ## 配置 完整的逐字段注释参考见 [docs/CONFIG.md](docs/CONFIG.md)。要点: - JSON 配置的发现顺序:插件 `Config.configPath`(必须存在)→ `/desktop-hand.config.json` (**推荐**,安装脚本会播种)→ `<插件目录>/desktop-hand.config.json` → `/desktop-hand/config.json` → 内置默认值(并打印警告)。`DSH_HOME` 默认是 `~/.dsh`。 - 配置文件在默认值之上深合并;数组整体替换。路径字段支持 `~`、`$VAR`、`${VAR}`、`${env:VAR}` 和 `%VAR%` 展开。非法配置会**让插件激活失败并报错**,绝不会悄悄降级为默认值。 - profile patch 行可以覆盖少量部署级参数(`enabled`、`configPath`、`provider`、`trashDir`、`auditDir`、 `screenshotsDir`、`hotkey`、`emergencyStop`);策略、上限和审批模式都在 JSON 文件里。 - 配置只在插件激活时读取一次;修改后请重启/重载。 必须知道的关键默认值: | 配置项 | 默认值 | 为什么重要 | |--------|--------|-----------| | `policy.delete.mode` | `trash` | 删除是可恢复的隔离区移动,而不是直接 unlink。随包配置把回收站放在 `D:\DSH_Trash`。 | | `approval.fileMutations` | `ask` | 每次写入/移动/删除/恢复都需要实时审批。 | | `approval.directDelete` | `ask` | 永久删除需要用户逐次明确确认。 | | `approval.drag` / `approval.combo` | `ask` | 拖拽和组合键(例如 Blender 的 `shift+a`)属于高风险动作,默认逐次审批。 | | `approval.text` | `grant` | 文本输入以 input 会话授权作为同意依据;**但**当前台窗口标题命中 `limits.sensitiveZones.foregroundTitlePatterns` 时仍会额外审批。 | | `emergency.hotkey` | `Pause` | 全系统停止热键。刻意避开 `F9`,因为 Blender 用它做「调整上一步操作」。 | | `session.*` | 10 分钟/200 次输入,5 分钟/50 次文件;上限 30 分钟/1000 次 | 授权同时受时间和动作预算约束。 | | `input.provider` | `auto` | Windows → windows,Linux → linux,macOS → macos;演练用 `fake`。 | > **`"ask"` 不等于一定成功。** 如果 DSH 会话审批策略是 `never`(或审批通道不可用),所有 `"ask"` 类别 > 都会**失败关闭**——调用在进入工具主体前就被拒绝,什么也不会发生。Blender 验收流程里的 `shift+a` > 属于 `combo`:要么把 DSH 审批策略切回 `ask`(推荐,能看到提示),要么把相关类别(`combo`、`drag`, > 文件操作则是 `fileMutations`/`directDelete`)改成 `"grant"`,依赖 `/desktop-hand authorize` > 的时间/次数受限会话授权。 --- ## 文件策略、删除模式与回收站 ### 禁区与优先级 桌面操控手的每个文件工具都会先规范化调用方给的路径(相对路径按会话工作目录解析;展开 `~`/环境变量; 剥离 Win32 命名空间前缀;解析符号链接/junction),然后**同时用字面路径和真实路径**匹配禁区, 防止用链接绕过。 优先级刻意按「安全优先」排列: 1. 命中**只读禁区** → 拒绝读、列目录、写、移动(源和目标)、删除。这一步**先于任何删除模式判断**, 所以 `direct` 钉死规则也无法绕过只读禁区。 2. 命中**写保护禁区** → 拒绝创建/覆盖/追加、移动和删除;读取与列目录仍允许。 3. **操作系统关键位置**(`protectSystemRoots`,默认 true)→ 拒绝写入和删除;允许读取。 4. **插件数据**(`protectPluginData`,默认 true):插件目录、审计目录和回收站目录不能被工具修改。 5. **受保护的删除根目录**(`delete.protectRoots`,默认 true):盘符根、用户主目录、回收站目录、 插件目录,以及会话工作目录根及其上级都不能被删除(也不能被移走)。只做等值判断——子项通常可以删。 6. **删除模式判定**:会话强制 `forbid` > 按路径的 `delete.pathOverrides` > 全局 `delete.mode`。 `direct` 模式下,除非 `directDeleteRequiresUnlock` 为 false,否则需要带 `allowDirectDelete` 的 files 授权;非空目录还需要 `recursive=true`。`trash` 模式需要配置 `trashDir`。 7. 另外,`desktop_hand_delete_path` 会拒绝删除**会话工作目录根**或其上级目录。 ### 删除模式 | 模式 | 行为 | 取证 / 恢复 | |------|------|------------| | `forbid` | 所有删除都被拒绝(`delete-forbidden`)。 | 审计 `fs-denied`。 | | `trash`(**默认**) | 文件/目录被**移动**到 `/items//payload`,并写 `meta.json` 与 `index.jsonl`;工具返回回收站 id 与恢复提示。 | `desktop_hand_list_trash`、`desktop_hand_restore_path`。跨卷移动退化为先复制再删除源。 | | `direct` | 永久 `rm`。需要已解锁的 files 授权(`allowDirectDelete`)、`confirm_direct=true`、非空目录还需要 `recursive=true`,并按默认需要 `directDelete` 逐次审批(还会参考 `approval.fileMutations`)。 | 审计 `fs`/`delete_direct`;**无法恢复**。 | ### 回收站布局与恢复 ``` D:\DSH_Trash\ # policy.delete.trashDir(随包配置) ├── index.jsonl # 只增日志:quarantine 与 restore 记录 └── items\ └── 20261031T162738-1a2b3c4d\ # id = UTC 时间戳 + 随机 hex ├── meta.json # id、originalPath、displayPath、payloadPath、size、type、 │ # mtime、deletedAt、sessionId、callId、tool、reason、 │ # policyRule、mode、moveKind、restoredAt、restoredTo └── payload # 被移动的文件或目录(符号链接仍按链接移动) ``` - `desktop_hand_delete_path` 返回 `mode: "trash"`、`trashId`、`trashPath` 和形如 `desktop_hand_restore_path({ trash_id: "..." })` 的恢复提示。 - `desktop_hand_restore_path` 把 payload 移回记录的原始路径(或显式 `destination`),恢复前会重新 对目标做禁区检查;除非 `overwrite=true`,否则不覆盖已存在路径。恢复后 `meta.json` 会更新 `restoredAt`、`restoredTo`、`restoreCount`、`lastRestoreKind`,index 追加 `restore` 记录。 - `meta.json` 在 payload 移动**之前**写入,因此即使崩溃也不会出现「有数据没元数据」的情况。 - 回收站 payload **不会自动清理**,保留与清理由你负责。 ### 会话收紧 `/desktop-hand tighten readonly|write <模式>` 和 `/desktop-hand tighten delete=forbid` (以及 `desktop_hand_policy_override`)只能让会话**更**严格。没有任何工具或命令能放宽配置禁区或 重新允许删除。 --- ## 安全模型与威胁模型 ### 控制措施 - **两类互相独立的会话级授权。** `input` 与 `files` 分开管理,只存在内存中,按分钟到期,并受动作次数 预算限制。撤销立即生效(`/desktop-hand revoke`、`desktop_hand_revoke`)。重启 DSH 会清空所有授权。 - **人工同意路径。** `/desktop-hand authorize …` 是用户专用斜杠命令;模型侧的 `desktop_hand_authorize` 还额外受 `approval.authorize`(默认 `ask`)约束,因此在默认审批模式下模型 无法静默给自己授权。若部署把审批改成自动批准,这层保障会被削弱——高风险/无人值守环境请保持 `approval.authorize` 为 `ask`/`deny`。 - **审批拦截(`tools/pre-execute`)。** 高风险调用在工具主体执行前返回 `ask`/`deny`:授权类工具、 策略收紧、文件改动、直接删除、拖拽、组合键和文本输入。默认:`fileMutations`、`directDelete`、 `drag`、`combo` 为 `ask`;`text` 为 `grant`(以 input 会话授权作为同意),但命中敏感窗口标题时仍会 额外审批。**若 DSH 审批策略是 `never`,所有 `ask` 类别都会失败关闭**——需要显式把类别改成 `grant`,或有意开启审批。 - **紧急停止。** 默认 **Pause** 热键由隐藏的 PowerShell 监听进程轮询,触发后写入 `/desktop-hand/emergency-stop.json`;运行时随即停止、通过 `AbortSignal` 中止进行中的输入、 杀掉输入后端并通知模型。停止期间,除 `status`、`revoke`、`emergency_stop` 外的全部桌面操控手工具 都被拒绝,只有人能执行 `/desktop-hand resume`。刻意不选 `F9`,因为 Blender 把它绑定到 「调整上一步操作」。 - **硬性限制。** 坐标校验(必须是整数、落在真实显示器内、不在显示器之间的虚拟空隙)、次数/时长/文本 长度上限、敏感屏幕矩形(任务栏条、屏幕角落;点击与拖拽起点默认拒绝,除非授权解锁敏感区)。 敏感前台窗口标题模式可在输入文本前强制走审批。 - **文件策略。** 如上文的禁区、删除模式、受保护根目录和「真实路径」检查;拒绝都会返回带代码与原因的 明确错误,绝不静默跳过。 - **审计。** 每会话一个只增 JSONL 文件,支持轮转,默认对输入文本脱敏。工具级别的拒绝 (`input-denied`、`fs-denied`)会连同命中的规则/原因记录,授权变更和紧急停止也会记录。由 `tools/pre-execute` 在工具主体执行前做出的授权/审批拒绝不会进入桌面操控手审计日志——请查看调用方 收到的拒绝原因、`/desktop-hand status` 和 DSH 日志。 ### 威胁模型与坦白的局限 桌面操控手假设**人类用户是主体**,而模型可能犯错、被提示注入操纵,或只是理解错了: - 一旦出问题,限制损失靠的是授权的时限/次数预算、紧急停止热键、回收站的可恢复性以及审计日志, **而不是**操作系统沙箱。 - **策略范围:** `policy.*` 只约束**桌面操控手自己的工具**。除非开启 `policy.enforceOnDshFsTools: true`(此时也会保护 DSH 自带的 `read`/`read_image`/`list_dir`/ `write`/`edit`/`write_file`),shell 命令(`bash`、`pwsh` 等)可以读写同样的文件——包括只读禁区里的 文件——并**完全绕过删除模式**。`forbid` 不是文件系统权限。 - **不是操作系统沙箱。** 插件以 DSH 进程的用户权限运行,无法防御同用户的恶意进程,也无法阻止模型 被诱导去申请一份过大的授权。 - **屏幕隐私:** 截图会拍下所有显示器上的可见内容(或指定区域);`type_text` 可能输入敏感信息。 审计默认脱敏(`audit.logTypedText: false`),但这些内容仍会作为真实输入发送。 - **破坏性路径依然存在:** 人若解锁 `direct-delete`、提供 `confirm_direct` 并批准,就能永久删除文件; `trashDir` 不会自动清理,但可以在插件之外被手工删掉——如果把它配到不受保护/不可信的卷上, 插件也保护不了它。 - **审批依赖:** 使用 `approval.*: "ask"` 的类别(默认包括 `fileMutations`、`directDelete`、`drag`、 `combo`)在没有实时审批通道的部署(例如 DSH 审批策略 `never`、无头运行)中会失败关闭。这是刻意的 失败关闭行为,不是 bug。 - **平台缺口:** 紧急停止热键监听**仅 Windows** 有;Linux/macOS 上紧急路径是 `/desktop-hand stop` 命令和停止标志文件检查。Linux/macOS 输入后端属于尽力而为,且 macOS 输入文本会改动剪贴板。 - **真实应用验证:** 自动化测试使用 fake 后端;开发阶段**没有**对 Blender、Office、浏览器等真实 GUI 做过端到端实操验证。请按 [docs/ACCEPTANCE.md](docs/ACCEPTANCE.md) 走验收,并在用于重要工作前 自行小心试点。 - **文件护栏不是备份:** 重要数据请始终保留独立备份。 --- ## 常见问题排查 | 现象 | 可能原因 / 处理 | |------|----------------| | 输入后端不可用 / `INPUT_MISSING_BINARY` | Windows:确认 PowerShell 可用(`powershell.exe` 或 `pwsh.exe`)。Linux:安装 `xdotool`(或设置 `input.linux.xdotoolPath`)。macOS:安装 `cliclick` 并授予辅助功能/自动化权限。演练可设 `input.provider: "fake"`。 | | 输入工具被拒,提示授权未开启/过期 | 执行 `/desktop-hand authorize input <分钟> <次数>`(需要时加 `sensitive`)。授权会过期且受次数限制——用 `/desktop-hand status` 查看。 | | 文件写入/删除被审批拒绝 | 默认 `approval.fileMutations: "ask"` 需要实时审批;DSH 审批策略为 `never` 时会失败关闭。若希望「会话授权即可」,把 `desktop-hand.config.json` 改成 `"grant"` 并重启。 | | 拖拽或组合键(如 `shift+a`)被审批拒绝 | `approval.drag` 与 `approval.combo` 默认 `ask`;DSH 审批策略为 `never` 时会失败关闭。把审批策略切到 `ask`(看到提示)或把类别改成 `"grant"`,依赖 input 会话授权。 | | 想在不注入输入的情况下做安全自检 | 在包目录执行 `node scripts/live-smoke.mjs`:它会启动 Windows 后端、读取屏幕/指针/前台窗口、把 PNG 存到 `%TEMP%\dh-live-home\live-shot.png`,并验证停止标志守卫;**绝不会移动鼠标、点击或输入**。 | | `file operation denied by policy: ... read-only zone ...` | 路径命中了 `policy.readonly`(或会话收紧的禁区)。这是预期行为;改配置移除该禁区或换路径。不要用 shell 命令绕过——那会让策略失去意义。 | | `delete-forbidden` | 删除模式是 `forbid`(全局、按路径覆盖或本会话 `tighten delete=forbid`)。改配置并重载;注意会话收紧在该会话内不会解除。 | | `direct-delete-locked` / 要求 `confirm_direct` | 授权 files 时加 `direct-delete`,调用时传 `confirm_direct=true`,非空目录还要 `recursive=true`;按配置可能仍需逐次审批。 | | 授权过期 / 动作次数用尽 | 重新执行 `/desktop-hand authorize …`;重新授权会替换旧授权并重置预算(不叠加)。`desktop_hand_screenshot` 不消耗次数。 | | 截图已保存但「看不到」 | 模型必须用 DSH 的 `read_image` 读取返回路径;截图位于 `<会话工作目录>/.desktop-hand/screenshots/`。 | | 紧急热键没反应 | 仅 Windows 实现。查看 DSH 日志中的 `emergency-stop hotkey` 警告(无效按键、缺少 PowerShell/脚本、平台不支持)。可改用 `/desktop-hand stop`。停止后只有人能 `/desktop-hand resume`。 | | 停止后所有工具都被拦截 | 这是设计:只允许 `status`、`revoke`、`emergency_stop`。执行 `/desktop-hand resume`。 | | 改配置后插件激活失败 | 配置校验很严格;看报错(非法模式/上限/正则/版本、文件不可读)。坏配置绝不会静默回退默认值。 | | 恢复被拒 | `desktop_hand_restore_path` 默认不覆盖;传 `overwrite=true`(会重新做禁区检查)或改用其他 `destination`。 | --- ## 开发与测试 ```powershell # 在 desktop-hand 包目录下 npm test # node --test test/ npm run test:watch # node --test --watch test/ node --test test/ ``` - Node.js **≥ 22**(ESM、`node:test`)。当前测试套件共 **167 个通过用例**,不需要 DSH 运行时: `defineTool` 由外部注入,输入后端使用假实现,配置/策略/授权/限制/紧急停止/审计/回收站模块均直接做 单元测试。 - 测试布局(`test/`):`config.test.js`(默认值、发现顺序、合并、校验)、`policy.test.js`(禁区、 优先级、删除模式、受保护根、符号链接、会话收紧)、`auth.test.js`(授权、过期、预算、撤销、敏感区/ 直接删除解锁、收紧)、`limits.test.js`(几何、敏感区、各类上限)、`emergency.test.js` (停止/重置/中止)、`audit.test.js`(JSONL、轮转、脱敏)、`trash.test.js`(隔离/恢复/EXDEV 回退)、 `input.test.js` 与 `keys.test.js`(输入后端契约/后端实现与按键解析)、`tools-fs.test.js` 与 `tools-input.test.js`(用假运行时测工具处理逻辑)、`integration.test.js`(运行时装配、端到端流程与 `/desktop-hand` 命令处理)。 - `scripts/live-smoke.mjs` 是**安全、只读**的 Windows 后端实机自检 (`node scripts/live-smoke.mjs`):读取屏幕/指针/前台窗口、把 PNG 存到 `%TEMP%\dh-live-home\live-shot.png`、验证停止标志守卫会拒绝动作;**绝不会移动鼠标、点击或输入**。 - 真实桌面的 GUI 自动化属于**手工**验收:[docs/ACCEPTANCE.md](docs/ACCEPTANCE.md)。开发演练可把 `input.provider` 设为 `"fake"`。 **开发阶段已核实的证据**(可作为依据,但不是普遍保证): - 自动化测试全量通过:**167/167**。 - 一个临时 DSH profile(headless 模板)成功把包装进 `node_modules` 并启动插件,**没有激活告警**。 - 本机安全实机自检成功:报告 **2560×1600** 屏幕、读取指针/前台窗口、写出 **2560×1600** PNG 截图、 停止标志守卫拒绝了一次动作、Pause 热键监听进程启停正常。 - **没有**对真实 Blender/GUI 输入做验证——见 [docs/ACCEPTANCE.md](docs/ACCEPTANCE.md)。 贡献建议:保持代码风格;新增工具/策略行为要补测试;新增配置字段时同步更新 [docs/CONFIG.md](docs/CONFIG.md)。 --- ## 许可证 MIT——见 `package.json` 的 `license` 字段。(本包目前没有单独的 `LICENSE` 文件,包元数据声明为 MIT。) --- 相关文档:[配置参考(docs/CONFIG.md)](docs/CONFIG.md) · [手工验收(docs/ACCEPTANCE.md)](docs/ACCEPTANCE.md) · [English README](README.md)