dsh-any-agent 0.3.9 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.zh.md CHANGED
@@ -15,38 +15,12 @@
15
15
 
16
16
  </div>
17
17
 
18
- 🧪 0.3.9 本机 843 项检查全绿(九种客户端配置齐全时 879 项;其中 524 项属于客户端注册器套件,齐全时 560 项) · 🔌 MCP over stdio · 📦 零依赖 · 📜 MIT
18
+ 🧪 0.4.0 本机离线套件全绿(内存回收、团队模式、派发面工具均有回归) · 🔌 MCP over stdio · 📦 零依赖 · 📜 MIT
19
19
 
20
20
  一个 MCP 服务端,把 DeepSeek Harness 当作**长驻的编码 agent** 来驱动。开个会话、扔个目标,它自己写代码、跑脚本、派子代理;你随时能看、能插话、能回头验收。
21
21
 
22
22
  **Claude Code · Claude Desktop · Codex · Cursor · Windsurf · Zed · Gemini CLI · opencode · VS Code**,以及任何支持 MCP over stdio 的客户端。
23
23
 
24
- ## 快速开始
25
-
26
- 需要 **Node.js ≥ 22.15**,并且**已安装 DSH、配置好可用的模型路由与凭据**。详见[前置要求](#-前置要求)。
27
-
28
- ```bash
29
- npm install -g dsh-any-agent
30
- dsh-any-agent-setup
31
- dsh-any-agent-setup --yes
32
- ```
33
-
34
- 第二条命令只预览,加 `--yes` 才会写入配置。重新连接客户端的 MCP 服务,然后让你的 agent 在当前项目中开启 DSH 会话、派活、收结果。[先派活,回头验收](#-用法先派活回头再验收)展示了完整的工具调用顺序。
35
-
36
- > **第一次派活前:** 先读[使用注意事项](USAGE-NOTES.md),尤其是模型配置与外部 MCP 的权限边界。**外部 MCP 工具的副作用不受 DSH 沙箱约束。** 新机器部署可按[迁移指引](USAGE-MIGRATION.md)逐步操作。
37
-
38
- <details>
39
- <summary>展开完整目录</summary>
40
-
41
- - **快速开始** —— [安装并连接](#快速开始) · [更新日志](CHANGELOG.md)
42
- - **先看这里** —— [解决什么问题](#-解决什么问题) · [主要优势](#-主要优势) · [为什么不是包一层](#-为什么不是直接把-dsh-包一层) · [前置要求](#-前置要求) · [安装配置](#-安装配置)
43
- - **怎么用** —— [工具](#-工具) · [先派活回头验收](#-用法先派活回头再验收) · [跑完自动通知](#-用法跑完自动通知不轮询) · [多会话并发](#-用法同时控制多个会话)
44
- - **行为** —— [权限](#-权限) · [思考内容](#-思考内容默认隐藏) · [日志与隐私](#-日志与隐私) · [工作区与 GUI](#-会话在-dsh-gui-里的可见性)
45
- - **难点** —— [写锁](#-写锁一个会话同一时刻只有一个写者)
46
- - **参考** —— [环境变量](#-环境变量) · [测试](#-测试) · [已知限制](#-已知限制) · [为什么还叫旧 profile 名?](#-为什么-profile-还叫-dsh-mcp)
47
-
48
- </details>
49
-
50
24
  ## 🎯 解决什么问题
51
25
 
52
26
  你的编辑器里已经有一个编码 agent 了。你**没有**的是这样一个 agent:
@@ -75,319 +49,63 @@ DSH(DeepSeek Harness)本身**全都能做** —— 它有文件/shell 工具
75
49
  | 🧩 **能力可叠加** | DSH 自己还能挂别的 MCP(IDA Pro、浏览器、内部工具)——这是一座通往整个工具箱的桥。 |
76
50
  | 🛡️ **只读档真能只读** | `read-only` 沙箱 + 审批一律拒绝,适合分析不信任样本。 |
77
51
 
78
- ## ⚡ 为什么不是直接把 DSH 包一层
79
-
80
- DSH 自带两个可编程入口。`sdk` profile 看着更简单,但它的 JSON-RPC 只有 **3 个请求 + 4 个通知**,缺了三样让 agent 实际不可用的能力;`acp` profile(Agent Client Protocol)全都有。以下结论全部针对 DSH `0.2.1-rc.5` 实测得出。
81
-
82
- | 能力 | `sdk` profile | `acp` profile(**本项目**) |
83
- |---|---|---|
84
- | **恢复会话** | ❌ 无 resume 方法,复用 id 报 `already exists` | ✅ `session/resume`,**跨进程实测通过** |
85
- | **中断回合** | ❌ 只能杀进程 | ✅ `session/cancel`,与用户手动按停止同一条路径 |
86
- | **应答审批** | ❌ 无通道,操作静默 fail-closed | ✅ `session/request_permission` 往返 |
87
- | 列出 / 关闭会话 | ❌ 无 | ✅ `session/list`、`session/close` |
88
- | 灰字 vs 黑字 | 同一条消息里靠 `type` 字段分 | ✅ 两条独立通道 `agent_thought_chunk` / `agent_message_chunk` |
89
- | 每会话 MCP 服务器 | ❌ 进程级 | ✅ `session/new` 与 `session/resume` 都接受 `mcpServers` |
90
-
91
- **核心思想:进程只是缓存,不是身份。** 对话的身份是 DSH 的 `sessionId`,落盘在 `~/.dsh/sessions`。进程被回收、崩溃、甚至整个 MCP 服务重启,下一次调用都会自动 `session/resume` 把它接回来,历史完整保留。
92
-
93
- <details>
94
- <summary>客户端、服务端与 DSH 是怎样连接的</summary>
95
-
96
- ```
97
- 任意 MCP 客户端 ← Claude Code、Codex、Cursor、Zed、Cline、你自己写的…
98
- (Claude/Codex/Cursor/…) │
99
- │ MCP over stdio │ 换行分隔 JSON-RPC(ACP)
100
- ▼ ▼
101
- ┌─────────────┐ ┌──────────────────────────┐
102
- │dsh-any-agent│─────────▶│ dsh --profile dsh-mcp │ 每个会话一个长驻进程
103
- └─────────────┘ │ │
104
- └────────────┬─────────────┘
105
- │
106
- ┌────────────┴─────────────┐
107
- │ DSH 自带工具 │ 文件/shell/搜索/
108
- │ │ 技能/子代理/workflow
109
- ├──────────────────────────┤
110
- │ dsh-mcp-client │ → 你自己的其他 MCP 服务器
111
- └──────────────────────────┘
112
- ```
113
-
114
- **MCP 兼容性。** 服务端只使用 `initialize`、`ping`、`tools/list`、`tools/call` 四个标准方法。已用一个协商旧版协议、从不发送 `notifications/initialized` 的原始客户端验证。
115
-
116
- </details>
117
-
118
- ## 📦 前置要求
119
-
120
- - **Node.js ≥ 22.15** —— 服务用 `node:zlib` 的 `zstdDecompressSync` 读 DSH 会话头,这个 API 在 22.15 之前不存在
121
- - **已安装 DSH**(`@deepseek-ai/dsh`),且 `~/.dsh` 下已有可用的 provider 与凭据(GUI 能正常聊天即可)
122
-
123
- > 本服务**不自带** provider 配置,也**从不**接收或保存 API 密钥。
124
-
125
- ## 🚀 安装配置
126
-
127
- ### 1. 让 profile 自己就绪(通常无需手工操作)
128
-
129
- > 📌 **为什么还叫 `dsh-mcp`?** 项目已改名 **dsh-any-agent**,但 DSH profile 名、`DSH_MCP_*` 环境变量、`# dsh-mcp:managed` 标记行都**故意保留旧名**。它们是已落盘的稳定标识符:改了会让现存 profile、会话和你的客户端配置一起失联。这是刻意的向后兼容,不是漏改。
130
-
131
- 首次拉起 DSH 时,[`src/bootstrap.mjs`](src/bootstrap.mjs) 会自动:
132
-
133
- 1. 在 `$DSH_HOME/profiles/dsh-mcp/` 建一个基于出厂 `acp` 模板的 profile;
134
- 2. 从本机**已有**的 profile(`$DSH_HOME/cordis.patch.yml` → `desktop` → `web` → `headless` → `acp` 取第一个有配置的)复制模型路由;
135
- 3. 写 `acp`、`agent-default-model` 与三档 `permission` presets(**不写 `defaultPreset`**,否则会覆盖权限档),让 ACP 会话用与 GUI 相同的模型,并让 read-only 真正生效。
136
-
137
- 复制的内容只有 `apiKeyEnv` / `baseURL` / `api` / `models` / `reasoningEfforts`;`apiKey`(适配器从不读取的死字段)与 `headers`(可能夹凭据)一律剥掉。凭据本身由 DSH 的凭据服务逐次请求解析,路径与 GUI 完全一致。
138
-
139
- 想要的来源不是自动挑的那个,可设 `DSH_MCP_ROUTE_SOURCE=desktop`(或 `__home__`);彻底关掉自动同步则设 `DSH_MCP_BOOTSTRAP=0`。
140
-
141
- <details>
142
- <summary>手工维护 profile 与 ACP 配置细节(可选)</summary>
143
-
144
- ### 1b. 手工维护(可选)
145
-
146
- ```bash
147
- dsh --profile dsh-mcp --from-default-profile acp
148
- ```
149
-
150
- 然后编辑 `~/.dsh/profiles/dsh-mcp/cordis.patch.yml`,**并删掉文件开头的 `# dsh-mcp:managed` 行** —— 有那行时本服务每次启动都会按来源重新生成,手改会被覆盖。可以从 [`profile-example/cordis.patch.yml`](profile-example/cordis.patch.yml) 起步。
151
-
152
- > ⚠️ **这里有两个坑**(都是踩出来的):
153
- >
154
- > 1. **ACP 的模型不走 `agent-default-model`**,而是读 `dsh-acp` 插件自己的 `config.provider` / `config.model` —— 而 `acp` bundle 把它硬编码成了 `deepseek-official`。不覆盖 `acp` 这一行,会话会静默跑在错误的 provider 上,报 `no API key for provider route "deepseek-official"`。
155
- > 2. **ACP 的 `reasoning_effort` 默认是空串**(= "Provider default"),不是最大档。本服务在每次 `session/new` 以及每次 `session/resume` 之后都会显式设成 `max` —— 因为新进程不会记住上一次的选择。
52
+ ## 🧭 定位:本服务只负责"派活",能力复用 DSH
156
53
 
157
- </details>
158
-
159
- ### 2. 注册 MCP 服务
160
-
161
- 执行[快速开始](#快速开始)中的三条命令,再重新连接客户端。需要手工配置时,可展开下方的客户端示例。
162
-
163
- `setup` 会找出你机器上的 MCP 客户端 —— **Claude Code、Claude Desktop、Codex、Cursor、Windsurf、Zed、Gemini CLI、opencode、VS Code** —— 逐个写进去,没装的跳过。它的安全性是结构性保证的:
164
-
165
- - **默认 dry-run**,必须加 `--yes` 才写;预览模式下**不启动任何子进程**,只读取它将要触及的那些文件
166
- - 每个文件写前备份成 `<file>.bak-dsh-<时间戳>`,内容是**写入前的原始字节**
167
- - 写入前后各解析一次,坏配置绝不落盘;坏 JSON 与只读文件一律拒绝而不是硬写,目录不存在则安全创建
168
- - **文本级写入**:只替换自己那个条目的字节,缩进(2 空格 / 4 空格 / Tab)、键顺序、CRLF 行尾、UTF-8 BOM、有无末尾换行都原样保留
169
- - **幂等**,重复运行只改变有差异的部分;第二次 `--yes` 不写盘、不新增 `.bak`
170
-
171
- **实测影响范围(2026-11-02,本机 plan 模式):** 9 个客户端中检测到 5 个(Claude Code、Codex、Cursor、Zed、VS Code),其余 4 个跳过;`--json` 报告 `mode=plan, changed=5, skipped=4, failed=0`。所有已存在客户端配置的 SHA-256、大小与 mtime **全部未变**。真正执行 `--yes` 也是增量写入:只写 `dsh-any-agent` 这一个条目,其余字节原样保留,写前先落一份 `.bak-dsh-<时间戳>`;`--remove` 也只删这一个条目。
172
-
173
- ```bash
174
- dsh-any-agent-setup --yes --only cursor,zed # 只装这几个
175
- dsh-any-agent-setup --yes --permission read-only
176
- dsh-any-agent-setup --remove --yes # 全部移除,只摘自己那条
177
- ```
178
-
179
- <details>
180
- <summary>高级开关与各客户端的配置路径</summary>
54
+ **它不是能力聚合器,而是派发层。** DSH 已经有的能力,本服务默认不重做、不代理、不预加载——
55
+ 调用方给一个目标,DSH 自己决定用哪些工具、技能、子代理或团队。
181
56
 
182
- **新增的三个开关**
183
-
184
- | 开关 | 作用 |
57
+ | 归 DSH 管(本服务不碰) | 归本服务管(派发语义) |
185
58
  |---|---|
186
- | `--json` | **stdout 只有一个可解析的 JSON 对象,人类可读日志全部走 stderr**。结构:`{ ok, mode, clients: [{ id, label, path, status, summary, error? }], changed, skipped, failed }`,其中 `mode` 取 `plan` / `apply` / `remove`。这是**契约**,不是便利功能:脚本可以把 stdout 直接喂给 JSON 解析器 |
187
- | `--home <dir>` | 用指定的 home 目录代替真实 `HOME` / `USERPROFILE` / `APPDATA`,注册—注销可以整套演练而不碰真实配置 |
188
- | `--force` | 只做两件事:(a) 目录或文件不存在时创建它们(默认也会创建,这个开关让它在自动化里、以及检测不到客户端时也照做);(b) 目标文件里已有同名条目、但形状不是本程序期望的样子时,允许覆盖它。它**绝不删别人的条目**,也**绝不跳过校验** |
189
-
190
- **每个客户端到底会碰哪个文件**
191
-
192
- 写入前一律先落 `.bak-dsh-<时间戳>` 备份。表中路径均为通用示例,「自己那条」指名为 `dsh-any-agent` 的那一个条目 / 那一段。
59
+ | 文件读写、shell、搜索、编辑 | 会话身份与跨崩溃 resume |
60
+ | 技能、子代理、Agent Teams、任务板 | 异步派活、收据、完成哨兵 |
61
+ | 模型路由、沙箱、审批策略 | 增量读、插话、中断 |
62
+ | 记忆/上下文/压缩 | 审批桥、写锁治理、队列、收件箱 |
63
+ | worktree(agent 自己用 git 建) | 安装、状态、体检、审计 |
193
64
 
194
- | 客户端(`--only` 取值) | 配置文件 | 容器键 | 安装 / 更新 | 卸载(`--remove`) | 撤销 |
195
- |---|---|---|---|---|---|
196
- | Claude Code(`claude-code`) | `~/.claude.json` | `mcpServers` | 只增 / 改自己那条,且带 `type: "stdio"` | 删掉自己那条 | 把备份复制回去 |
197
- | Claude Desktop(`claude-desktop`) | Windows `%APPDATA%\Claude\claude_desktop_config.json`;macOS `~/Library/Application Support/Claude/claude_desktop_config.json`;Linux `~/.config/Claude/claude_desktop_config.json` | `mcpServers` | 只增 / 改自己那条 | 删掉自己那条 | 把备份复制回去 |
198
- | Codex(`codex`) | `~/.codex/config.toml` | `[mcp_servers.dsh-any-agent]` | 按行追加自己那两段 TOML | 只删这两段 | 把备份复制回去 |
199
- | Cursor(`cursor`) | `~/.cursor/mcp.json` | `mcpServers` | 只增 / 改自己那条 | 删掉自己那条 | 把备份复制回去 |
200
- | Windsurf(`windsurf`) | `~/.codeium/windsurf/mcp_config.json` | `mcpServers` | 只增 / 改自己那条 | 删掉自己那条 | 把备份复制回去 |
201
- | Zed(`zed`) | Windows `%APPDATA%\Zed\settings.json`;其他平台 `~/.config/zed/settings.json` | `context_servers` | 只增 / 改自己那条 | 删掉自己那条 | 把备份复制回去 |
202
- | Gemini CLI(`gemini`) | `~/.gemini/settings.json` | `mcpServers` | 只增 / 改自己那条 | 删掉自己那条 | 把备份复制回去 |
203
- | opencode(`opencode`) | `~/.config/opencode/opencode.json` | `mcp` | 只增 / 改自己那条,且按 opencode 的形状写:`{ type: "local", command: [exe, ...args], enabled: true }` —— 一个数组,不是分开的 `command`/`args` | 删掉自己那条 | 把备份复制回去 |
204
- | VS Code(`vscode`) | Windows `%APPDATA%\Code\User\mcp.json`;其他平台是 VS Code 用户目录下的同名 `mcp.json` | `servers` —— **不是** `mcpServers` | 只增 / 改自己那条 | 删掉自己那条 | 把备份复制回去 |
65
+ **按需派发**:`dsh_send` 只传目标,不传工具清单,也不要求调用方先了解 DSH 有哪些工具。
66
+ DSH 的 agent 按任务自己选能力——包括它自己新长出来的能力(比如实验性 Agent Teams),
67
+ 本服务不需要为每一个新能力加一个 MCP 工具。
205
68
 
206
- 具体路径以 `--json` 输出里每个客户端的 `path` 字段为准 —— 那是本次运行在你这个系统上真正要写的文件,比任何文档都准。
69
+ **团队模式(0.5.0 起按需开启)**:默认关闭。运行 `dsh-any-agent team on` 并重连后,
70
+ profile 切到 `dsh-team`(含实验性 Agent Teams),`dsh_send` 会提示"需要时先建团队",简单任务不组队。
71
+ 关闭:`dsh-any-agent team off`;单次关闭:`dsh_send(team=false)`。
72
+ 注意 team profile 会关闭旧的 `subagent` 工具,换成 9 个团队工具;成员共享一个工作区。
207
73
 
208
- 有三种形状值得单独点出来,写错了是**静默失效**。**VS Code** 的 MCP 配置放在 `servers` 下,不是 `mcpServers`;按常见写法写进去,编辑器会直接忽略。**opencode** 用的是**一个** `command` 数组(`{ type: "local", command: [exe, ...args], enabled: true }`),不是分开的 `command`/`args`。**Claude Code** 则是本次发布**完全不用 JSON 库**的原因:`~/.claude.json` 的 `projects` 映射里确实存在**只差大小写**的重复键(`.../Rust` 和 `.../rust` 并存),朴素的 `JSON.parse` 在那里会失败,而任何整份重写都会毁掉用户的项目列表。所以改用按字符区间扫描、**从不构造值对象**的扫描器;早先那版「调 `claude mcp add`」也出于同样理由被放弃 —— 那条命令会用它自己的格式重写整个文件,且不产生我们可以回滚的备份。
74
+ **只有"派发语义"本身需要的才做成工具**:建会话、派活、读进度、验收、插话、中断、审批、
75
+ 队列、收件箱、状态、释放、抢占、体检。文件读写 / shell / 编辑器 / 任务板都不做。
209
76
 
210
- </details>
211
-
212
- <details>
213
- <summary>注册器检查与注册、撤销的往返验证</summary>
214
-
215
- **setup 会动什么、不会动什么**
216
-
217
- 它只做新增,而且只动自己那一条。在真正装着别的 MCP 服务器的机器上验证过(Cursor 4 个、Codex 2 个段、Claude Code 5 个、Zed 33 个配置键):所有既有条目**逐字节不变**,键顺序不变,唯一的差异是每个客户端多了一个条目。
218
-
219
- ```
220
- Cursor 4 -> 5 个条目 既有条目逐字节相同
221
- Codex 2 -> 3 个段 node_repl 与 rea 内容不变
222
- Claude Code 5 -> 6 个服务 原有四个都还在列表里
223
- Zed 33 -> 33 个键 只有 context_servers 多了个子项
224
- ```
225
-
226
- 和 0.1.x 的区别:过去 JSON 配置是**整份重新序列化**的(值保留、排版丢失),所以「白空格不保证原样」。现在写入是文本级的 —— 除了自己那一条,文件其余部分逐字节相同:4 空格缩进、2 空格缩进、Tab、单行 JSON、没有末尾换行、CRLF 行尾、带 UTF-8 BOM,一律原样。
227
-
228
- Codex 的 `config.toml` 同理,是按行处理的:其它 `[表]` 及其注释、顶层键(`model`、`approval_policy`、`sandbox_mode` 等)、CRLF 行尾都原样保留 —— `--remove` 只删 `[mcp_servers.dsh-any-agent]` 那两段,其余一律不动。
229
-
230
- 这些断言的详细覆盖见 [`test/`](test/)(`test/config-safety.mjs` 共 407 项,逐条断言上面每一条)。
77
+ **什么时候需要外层再加东西**:只有当你需要"保证"而不是"行为"时——
78
+ 例如多个顶层会话并发改同一个仓库,需要外层 worktree 隔离;团队协作则优先让 DSH 自己开团队
79
+ (需要把实验性 team 包挂进 profile,成员共享一个工作区)。
231
80
 
232
- **在真实 home 上做过只读验证。** 用 `setup --json`(纯计划模式)跑一遍开发机上的真实配置:9 个客户端全部报出,而现存的每份配置都**原封未动** —— size、mtime、SHA-256 全同,没有新增任何 `.bak-dsh-*`,也没有新建任何文件:
233
-
234
- ```
235
- ~/.claude.json 182693 B 未变
236
- ~/.cursor/mcp.json 1396 B 未变
237
- ~/.codex/config.toml 6647 B 未变
238
- ~/.gemini/settings.json 465 B 未变
239
- ~/.config/opencode/opencode.json 441 B 未变
240
- %APPDATA%/Zed/settings.json 5647 B 未变
241
- %APPDATA%/Claude/claude_desktop_config.json 247 B 未变
242
- ```
243
-
244
- 同一条命令加 `--home <临时目录>` 时,报出的**每一个** path 都落进了那个目录(`APPDATA` 也跟着走)—— 注册 / 注销整套演练之所以安全,靠的就是这条。
245
-
246
- **在真实 home 上做过完整的注册 → 撤销往返(不只是预览)。** 备份全部 7 份配置后,实际执行 `--yes` 再执行 `--remove --yes`,然后逐字节核对:
247
-
248
- | 客户端 | 注册前 | 注册后 | 撤销后 | 其它 MCP 服务器 |
249
- |---|---|---|---|---|
250
- | ~/.claude.json | 182693 B | 182693 B(原本已是最新) | 逐字节回到基线 | 未动;67 个顶层键、60 个 projects 条目、两个只差大小写的重复键都还在 |
251
- | ~/.cursor/mcp.json | 1396 B | 1389 B | 1009 B | stitch / obsidian-mcp-server / codegraph / rea 逐字段相同 |
252
- | ~/.codex/config.toml | 6647 B | 6647 B(原本已是最新) | 6239 B | 其它 `[表]`、注释、顶层键全部保留 |
253
- | %APPDATA%/Zed/settings.json | 5647 B | 5647 B(原本已是最新) | 5240 B | 32 个顶层设置键全部保留 |
254
- | ~/.gemini/settings.json | 465 B | 845 B | 465 B | 逐字节还原 |
255
- | ~/.config/opencode/opencode.json | 441 B | 869 B | 441 B | `$schema` 与 pencil 条目保留 |
256
- | %APPDATA%/Claude/claude_desktop_config.json | 247 B | 627 B | 247 B | 逐字节还原 |
257
-
258
- 撤销的验收方式不是「看起来对」,而是**从备份原文里用同一套编辑引擎摘掉 `dsh-any-agent` 这一条,结果必须与磁盘上的当前文件逐字节相同** —— 7 个文件全部通过。这条证明撤销是注册的严格逆运算,不会顺手带走别的东西。
259
-
260
- 撤销后所有 JSON 仍可解析、`codex mcp list` 仍能正常读取配置,客户端不需要人工修复。
261
-
262
- </details>
263
-
264
- **`cwd` 可以省略:默认用客户端当前打开的项目**
265
-
266
- 客户端本来就知道用户在哪个目录干活(MCP roots)。现在 `dsh_start` 不传 `cwd` 时,会向客户端发一次 `roots/list`,把返回的 `file://` URI 转成路径直接用。于是「客户端里看到哪个项目,DSH 就在哪个项目干活」成了默认行为,也顺带堵掉了「手拼路径拼到家目录」这条最常见的翻车路径。客户端不声明 roots、或者超时没回,就退回一条能照着改的报错,而不是猜一个目录。
267
-
268
- **cwd 不要选家目录本身**
269
-
270
- DSH 自己的 ACL 沙箱要求临时目录在工作区**之外**。Windows 上 `%TEMP%` 是 `C:/Users/<你>/AppData/Local/Temp`,就在家目录里面,所以 `cwd` 取家目录时,第一条 shell 命令会失败并报 `Windows ACL: temp root must be outside the workspace`。现在 `dsh_start` 会直接拒绝这个组合并给出两条出路:换一个具体项目目录,或改用 `permission=read-only` / `danger-full-access`。
271
-
272
- **备份与回滚**
273
-
274
- 注册和删除**都会**在动文件之前先存一份:`<配置文件>.bak-dsh-<时间戳>`,内容就是操作前的原始字节。两个方向都存,因为要退回的状态不同——注册前那份用来「装错了退回原样」,删除前那份用来「删错了装回去」。
275
-
276
- 每次实际写入后,`--json` 的每个客户端条目里都带 `backup` 字段,直接告诉你备份在哪;人类可读的输出也会在每条下面打印 `backup: <路径>`。
277
-
278
- 备份散落在各客户端目录里,手动翻文件容易挑错,所以附带一个还原命令:
279
-
280
- | 要做的事 | 命令 |
281
- |---|---|
282
- | 看有哪些备份(新的在前) | `dsh-any-agent-restore --list` |
283
- | 退回每个文件最新的那份 | `dsh-any-agent-restore --latest` |
284
- | 退回某一批次 | `dsh-any-agent-restore --stamp 2026-10-30T01-35` |
285
- | 退回指定的一份 | `dsh-any-agent-restore --file <备份路径>` |
286
- | 先看会做什么 | 任意命令加 `--dry-run` |
287
-
288
- 还原本身也是破坏性操作,所以覆盖之前会把**当前**状态另存为 `.bak-dsh-pre-restore-<时间戳>`——退错了还能再退回来。
289
-
290
- **安装 / 更新 / 删除**
291
-
292
- | 要做的事 | 命令 |
293
- |---|---|
294
- | 预览会改什么(不写) | `dsh-any-agent-setup` |
295
- | 注册到所有检测到的客户端 | `dsh-any-agent-setup --yes` |
296
- | 只注册某几个 | `dsh-any-agent-setup --yes --only cursor,zed` |
297
- | 就地改权限档 | `dsh-any-agent-setup --yes --permission read-only` |
298
- | 升级包之后更新配置 | `dsh-any-agent-setup --yes`(幂等,只重写有差异的条目) |
299
- | 从所有客户端移除 | `dsh-any-agent-setup --remove --yes` |
300
- | 只从某几个移除 | `dsh-any-agent-setup --remove --yes --only codex` |
301
- | 撤销某次误写 | 把 `.bak-dsh-<时间戳>` 复制回原文件 |
302
-
303
- <details>
304
- <summary>移除操作的实测记录</summary>
305
-
306
- **`--remove` 到底动了什么 —— 实测**
307
-
308
- 注销只删掉每个客户端里的**那一个键**,其余一概不碰。在真机上测得的前后对比:
309
-
310
- ```
311
- Cursor { keep, dsh-any-agent } -> { keep }
312
- 手工写的那条原样保留
313
- Claude Code 6 个服务 -> 5 个服务
314
- 原有五个都还在列表里
315
- Zed 33 个顶层键 -> 33 个键
316
- 只有 context_servers 少了子项,现在是空对象
317
- Codex 我们的两个 TOML 段 -> 已移除
318
- model、approval_policy、sandbox_mode、node_repl、rea 全部完好
319
- ```
320
-
321
- 这些条目之外的东西它一概不读、不改、不删。你的凭据、其他 MCP 服务器、无关设置都不受影响。
322
-
323
- 注意:**`--remove` 不会卸载包本身** —— 那是 `npm uninstall -g dsh-any-agent`,两件事互相独立:你可以保留包但不注册任何客户端,也可以只注销而不卸载。
324
-
325
- 事后可自行核验:`claude mcp list`、`codex mcp list`,以及看 `~/.cursor/mcp.json` 和 Zed 设置里的 `context_servers` 键。
326
-
327
- </details>
328
-
329
- **记录的是绝对路径**
330
-
331
- `setup` 写进去的是它运行时所处安装目录的**绝对路径**。**如果你移动或删除了那个目录,所有客户端都会报 `CONNECTION_CLOSED`**,直到你在新位置重新跑一遍 `setup`,或者用 `--remove` 把失效条目清掉。
332
-
333
- 最容易踩到的情况:装到临时目录里试用,然后清理掉 —— 配置还指着那个已经不存在的路径。
334
-
335
- <details>
336
- <summary>手工配置客户端:Claude Code、Codex 与通用 MCP</summary>
337
-
338
- **从本地目录安装**
339
-
340
- 解压到任意位置,把 `command` 指向 `node`、`args` 指向 `<解压目录>/bin/dsh-any-agent.mjs`(见下)。
341
-
342
- ---
81
+ ## 快速开始
343
82
 
344
- **Claude Code**
83
+ 需要 **Node.js ≥ 22.15**,并且**已安装 DSH、配置好可用的模型路由与凭据**。详见[前置要求](#-前置要求)。
345
84
 
346
85
  ```bash
347
- # 全局安装
348
- claude mcp add dsh-any-agent -- dsh-any-agent
349
-
350
- # 本地目录
351
- claude mcp add dsh-any-agent -- node /绝对路径/server/bin/dsh-any-agent.mjs
86
+ npm install -g dsh-any-agent
87
+ dsh-any-agent-setup
88
+ dsh-any-agent-setup --yes
352
89
  ```
353
90
 
354
- **Codex**(`~/.codex/config.toml`)
91
+ 第二条命令只预览,加 `--yes` 才会写入配置。重新连接客户端的 MCP 服务,然后让你的 agent 在当前项目中开启 DSH 会话、派活、收结果。[先派活,回头验收](#-用法先派活回头再验收)展示了完整的工具调用顺序。
355
92
 
356
- ```toml
357
- [mcp_servers.dsh-any-agent]
358
- command = "dsh-any-agent" # 或 "node" + bin/dsh-any-agent.mjs 的路径
359
- args = []
360
- startup_timeout_sec = 20.0
361
- tool_timeout_sec = 120.0
362
- ```
93
+ > **第一次派活前:** 先读[使用注意事项](USAGE-NOTES.md),尤其是模型配置与外部 MCP 的权限边界。**外部 MCP 工具的副作用不受 DSH 沙箱约束。** 新机器部署可按[迁移指引](USAGE-MIGRATION.md)逐步操作。
363
94
 
364
- **通用 MCP 客户端** —— 任何支持 stdio 的 MCP 客户端:
365
-
366
- ```json
367
- {
368
- "mcpServers": {
369
- "dsh-any-agent": {
370
- "command": "dsh-any-agent",
371
- "args": []
372
- }
373
- }
374
- }
375
- ```
95
+ > **想先看效果?** `npm run demo` 是一条能跑通的 5 分钟路径:派活 → 中途插话两次 → 脚本自己复核产物(连 `node --test` 都替你跑)。不想花 token 就先 `npm run demo -- --replay`。见 [5 分钟案例](docs/zh/demo.md)。
376
96
 
377
- 本地目录安装时改用 `"command": "node"` 和 `"args": ["/绝对路径/server/bin/dsh-any-agent.mjs"]`。
97
+ <details>
98
+ <summary>展开完整目录</summary>
378
99
 
379
- > Cursor、Windsurf、Cline、Continue 等大部分客户端都吃同一套 `mcpServers` 结构——只要能拉起 stdio MCP 服务,就能用。Zed 是例外:它用 `context_servers`(见上方示例)。
100
+ - **快速开始** —— [安装并连接](#快速开始) · [更新日志](CHANGELOG.md)
101
+ - **先看这里** —— [解决什么问题](#-解决什么问题) · [主要优势](#-主要优势) · [为什么不是包一层](#-为什么不是直接把-dsh-包一层) · [前置要求](#-前置要求) · [安装配置](#-安装配置)
102
+ - **怎么用** —— [工具](#-工具) · [先派活回头验收](#-用法先派活回头再验收) · [跑完自动通知](#-用法跑完自动通知不轮询) · [多会话并发](#-用法同时控制多个会话)
103
+ - **行为** —— [权限](#-权限) · [思考内容](#-思考内容默认隐藏) · [日志与隐私](#-日志与隐私) · [工作区与 GUI](#-会话在-dsh-gui-里的可见性)
104
+ - **难点** —— [写锁](#-写锁一个会话同一时刻只有一个写者)
105
+ - **参考** —— [环境变量](#-环境变量) · [测试](#-测试) · [已知限制](#-已知限制) · [为什么还叫旧 profile 名?](#-为什么-profile-还叫-dsh-mcp)
380
106
 
381
107
  </details>
382
108
 
383
- ### 3. 自检
384
-
385
- ```bash
386
- dsh-any-agent --version # 打印版本,不启动任何东西
387
- dsh-any-agent --help # 参数、关键环境变量、文档链接
388
- node test/smoke.mjs # 不调用 LLM,验证整条管道
389
- ```
390
-
391
109
  ## 🧰 工具
392
110
 
393
111
  | 工具 | 作用 |
@@ -402,338 +120,35 @@ node test/smoke.mjs # 不调用 LLM,验证整条管道
402
120
  | `dsh_interrupt` | 打断当前回合(会话不会损坏) |
403
121
  | `dsh_release` | 交还会话(释放写锁,之后可在你自己的 DSH GUI 里打开)。仍可 resume;`forget=true` 会写墓碑,此后按 id 查询一律拒绝,直到删除 `$DSH_HOME/.dsh-any-agent-forgotten/<id>.json` |
404
122
  | `dsh_approval_decide` | 裁决待决审批(仅当 `on_approval=ask`) |
405
- | `dsh_status` | 轻量状态查询 |
406
-
407
- ## 📋 用法:先派活,回头再验收
408
-
409
- MCP 工具调用是**阻塞**的,所以 `dsh_send` **默认就不阻塞**(`wait=false`):派完立刻拿到收据,回合在后台跑,跑完由哨兵文件唤醒你。整个流程是这样:
410
-
411
- ```text
412
- 1. dsh_send(conversation_id, prompt, wait=false)
413
- → { run_id: "run-abc123", background: true } # 不等回合跑完就返回
123
+ | `dsh_status` | 轻量状态查询(单个会话) |
124
+ | `dsh_inbox` | **跨会话待决审批收件箱**:一次列出所有会话的审批、原因、输入与等待时长;裁决仍用 `dsh_approval_decide` |
125
+ | `dsh_queue` | 查看/修改会话队列:`list` / `remove` / `update` / `promote`(队列由 `dsh_send(queue=true)` 产生,仅内存) |
126
+ | `dsh_doctor` | 只读体检:DSH 版本、profile、状态/目录可写、孤儿锁、会话与 RSS、生效配置 |
414
127
 
415
- 2. …继续做你自己的事(实测:期间在另一个会话上跑完了一整个回合)…
128
+ ## 📚 文档地图
416
129
 
417
- 3. dsh_status(conversation_id)
418
- → state=running | elapsed=3.5s | current_tool=pwsh | out=37 chars
419
-
420
- 4. dsh_read(conversation_id, cursor) # 中途偷看
421
- → [我] … | [轮次] 第 1 轮开始 | [工具] pwsh [in_progress]
422
-
423
- 5. dsh_get(conversation_id, run_id) # 验收
424
- → status=done, elapsed=20.3s, 完整答复
425
- ```
426
-
427
- `run_id` 是验收凭据。回合记录**只在内存、只留最近 20 条**;并发跑很多 run 时旧记录会被挤掉,`dsh_get` 就会失败——这正是下面"完成哨兵"自带结果的原因。
428
-
429
- ## 🔔 用法:跑完自动通知(不轮询)
430
-
431
- `wait=false` 除了 `run_id`,还会返回 **`sentinel_file`**(哨兵文件路径)以及 **`sentinel_file_posix`**(Bash/MSYS 形式,**直接用它,别无手工转换**——转错了等待任务会一直挂到超时)。回合结束时——**无论成功、失败还是被取消**——本服务都会往这个路径**原子写出**一个含最终 `status` 与 `result` 的小文件。于是你不用轮询、也不用傻等:**用你自己宿主(Claude/Codex)的后台任务等这个文件出现即可,文件一到就会唤醒你。**
432
-
433
- ```bash
434
- # sent="$sentinel_file_posix" ← 直接用工具返回里那个字段
435
- sent='/c/Users/you/.dsh/.dsh-any-agent/runs/<conversation_id>/<run_id>.json'
436
- dl=$(( $(date +%s) + 2100 )) # 35min 兜底,防服务中途死掉时无限挂
437
- until [ -f "$sent" ]; do
438
- [ "$(date +%s)" -ge "$dl" ] && { echo "TIMEOUT"; exit 1; }
439
- sleep 2
440
- done
441
- echo "DONE" # 后台任务退出 → 宿主唤醒你 → 读文件收活
442
- ```
443
-
444
- 文件出现后:**直接读它**即得 `status` 和 `result`(推荐——不受内存窗口限制、还扛服务重启),或用 `dsh_get(conversation_id, run_id)` 验收。
445
-
446
- 设计要点(尤其**多会话并发**):
447
-
448
- - **不串台**:哨兵路径为 `<状态目录>/runs/<conversation_id>/<run_id>.json`,按会话分目录。`run_id` 的序号是每会话独立计数的,不同会话在同一毫秒可能生成同名 `run_id`——分目录后各写各的,绝不互相覆盖。每个 run 起一个独立后台任务即可,各自完成、各自唤醒。
449
- - **闩锁语义**:文件写一次并保留。即便 run 在你启动等待任务**之前**就已完成,`[ -f ]` 也立刻为真,不会漏。
450
- - **原子落地**:`tmp` + `rename`,文件一旦存在内容必然完整;每个 run 用独立 tmp,并发写互不干扰。
451
- - **无主动推送**:本服务在 MCP 协议上只应答请求、不发通知;"跑完通知"完全由这个哨兵文件 + 你的后台等待任务实现。
452
- - **★ 哨兵里没有思考内容**:载荷含正文、思考**统计**、用量与(已脱敏的)工具输入,但没有思考正文;`result.thinking` 会被剥掉,并始终给出布尔 `thinking_omitted`(剥离了为 `true`,本来没有为 `false`)——本服务自己的落盘物里不写思考。`dsh_get(run_id)` 在内存里仍能看到它(记录还在时)。设 `DSH_MCP_SENTINEL_INCLUDE_REASONING=1` 可以故意打破这条保证。注意:**DSH 自己的会话日志仍含思考**,见“日志与隐私”。
453
-
454
- > 清理:哨兵内含完整正文,所以服务**启动时会清掉超过 7 天的哨兵**(用 `DSH_MCP_SENTINEL_TTL_MS` 调;`0` 关闭)。等待任务消费后仍建议自行 `rm`。
455
-
456
- ## 🧵 用法:同时控制多个会话
457
-
458
- ```text
459
- 1. dsh_list(only_running=true)
460
- - … | id=448b239b… | 🔵进行中(已跑9s, 工具=pwsh, 本轮输出=40字) | cwd=…
461
- - … | id=33427096… | 🔵进行中(已跑8s, 本轮输出=34字) | cwd=…
462
- 2. dsh_read(id, cursor=0) # 读第一个,记下 cursor
463
- 3. dsh_read(id2, cursor=0) # 切到第二个
464
- 4. dsh_read(id, cursor=<上次>) # 切回来;上次之后被追加过的条目会再次出现(按 seq 替换全文)
465
- ```
466
-
467
- ## 🤫 思考内容:默认隐藏
468
-
469
- DSH 在模型层就把思考与正文分开,ACP 又把它们做成**两条独立流**。本服务默认隐藏思考内容,只返回**统计**(字数、耗时、段数)——调用方知道它想过,但不必为思考内容付上下文成本。
470
-
471
- - `reasoning=hide`(默认)—— 丢弃内容,返回统计
472
- - `reasoning=marker` / `summary` / `full` —— 逐步放出更多内容(注意上下文预算)
473
- - `dsh_read(include_reasoning=true)` —— 只返回**当前正在跑那一轮**的实时思考;不进缓冲,回合结束即丢弃
474
-
475
- ## 🔐 权限
476
-
477
- | 档位 | 审批默认 | 用途 |
478
- |---|---|---|
479
- | `danger-full-access`(服务端裸默认) | `auto-allow` | 零摩擦;`setup` 默认写入 `workspace-write` |
480
- | `workspace-write` | `auto-allow` | DSH 沙箱限制可写范围;越界请求默认放行,传 `on_approval=ask` 才挂起审批 |
481
- | `read-only` | `auto-deny` | 分析不信任样本(恶意软件、来路不明 dump) |
482
-
483
- `on_approval` 可选 `auto-allow` / `auto-deny` / `ask`(`ask` 会把审批挂起,交给 `dsh_approval_decide` 裁决)。
484
-
485
- **`read-only` 是强制边界,不只是默认值**:它固定使用 `auto-deny`;给只读会话传 `auto-allow` / `ask` 会在 `dsh_start` 被直接拒绝,审批层也会拒绝来自只读会话的任何权限请求。建会话/resume 后还会回读会话自己记录的权限档;只读档如果在 DSH 投影缓存刷盘窗口内仍读不到记录,会 **fail-closed 拒绝继续**,而不是只依赖审批层兜底。否则 profile 的 `defaultPreset` 加宽松审批策略可能放行写操作(对抗审计实测过)。
486
-
487
- ## 🪟 会话在 DSH GUI 里的可见性
488
-
489
- 两件事,别混淆:
490
-
491
- | | 取决于 | 现状 |
492
- |---|---|---|
493
- | **工作区是否生效** | 会话的 `cwd` | ✅ 永远正确。进程工作目录、文件落点、沙箱可写根都跟着它 |
494
- | **GUI 里是否分组** | `~/.dsh/storages/workspace.json` 的 `sessionIds` | ⚠️ 本服务会登记,但**正在运行的 DSH 服务把注册表缓存在内存里**,外部改文件要重启 DSH 才生效 |
495
-
496
- **"生效"有实测**([`test/workspace-effect.mjs`](test/workspace-effect.mjs)):桌面两个文件夹各开一个会话,派活时**不给任何路径**,结果两个会话各自把文件写进了自己的工作区、自报的当前目录正确、没有泄漏到 MCP 服务工作目录 / 家目录 / 临时目录 / 另一个工作区(24/24 全过)。
497
-
498
- 维护命令:
499
-
500
- ```bash
501
- node bin/dsh-any-agent-workspaces.mjs --list # 查看注册表
502
- node bin/dsh-any-agent-workspaces.mjs --backfill # 扫描磁盘上的会话,全部补登记
503
- node bin/dsh-any-agent-workspaces.mjs --prune-temp # 清掉临时目录条目
504
- node bin/dsh-any-agent-workspaces.mjs --prune-empty # 清掉"无会话且路径已不存在"的工作区
505
- node bin/dsh-any-agent-workspaces.mjs --purge-test-sessions # 删测试会话(规则写得很死)
506
- ```
507
-
508
- > ⚠️ 重启 DSH 之前**别在 GUI 里操作工作区**,否则服务会用内存状态把文件覆盖回去。
509
-
510
- ## 🕶️ 日志与隐私
511
-
512
- 默认**完全静默**,一个字节都不输出。
513
-
514
- 1. MCP 的 **stdout 是协议通道**,混进任何东西都会破坏报文;
515
- 2. 本服务被 Claude/Codex 拉起,它的 stderr 会进**对方的日志**;
516
- 3. DSH 子进程的 stderr 可能夹带模型输出(**含思考内容**),所以默认既不转发也不落盘。
517
-
518
- | 级别 | 行为 |
519
- |---|---|
520
- | `silent`(默认) | 什么都不输出 |
521
- | `info` | 只输出本服务自己的生命周期事件,**绝不含 DSH 的任何输出** |
522
- | `debug` | 更细的本服务事件;仍不打印 DSH 输出 |
523
- | `DSH_MCP_LOG_STDERR=1` | 额外转发 DSH 原始 stderr(**可能含思考内容**) |
524
-
525
- **本服务不把思考内容写进自己的落盘物**,只有一个**刻意留下**的例外:
526
-
527
- - 不写日志(默认静默);状态文件与 `<状态目录>/meta/` 下的会话元数据侧车都只有元数据(id、cwd、权限、计数、标题提示),冒烟测试里有一条断言专门守注册表。
528
- - **异步完成哨兵需要把正文写进文件**(否则调用方无法离线收活),载荷还包含用量与(已按审批规则脱敏的)工具输入,但 `result.thinking` 会被**剥掉**,并始终给出布尔 `thinking_omitted`。测试里专门验证了:即使请求 `reasoning=full`,哨兵里也不出现任何思考文本——而同一 run 在内存(`dsh_get`)里仍能看到。设 `DSH_MCP_SENTINEL_INCLUDE_REASONING=1` 可刻意打破这条。
529
- - 哨兵默认 7 天后由启动清理删除(`DSH_MCP_SENTINEL_TTL_MS`,`0` 表示永久保留)。
530
-
531
- > ⚠️ **DSH 自己会在 `~/.dsh/sessions` 写会话日志,而且其中包含完整 reasoning 文本**(即使 `reasoning=hide` 也照存)。那是 resume 的依据,不在本服务控制范围内;“思考不落盘”的绝对表述只适用于**本服务自己的**日志/哨兵/注册表。
532
-
533
- ## ⚙️ 环境变量
534
-
535
- | 变量 | 默认 | 说明 |
536
- |---|---|---|
537
- | `DSH_BIN` | 自动探测 | `@deepseek-ai/dsh/lib/bin.js` 路径 |
538
- | `DSH_MCP_PROFILE` | `dsh-mcp` | 驱动用的 DSH profile |
539
- | `DSH_HOME` | `~/.dsh` | DSH 主目录。**默认就用本机用户自己的**,凭据与会话历史因此与 GUI 共用。指向别处即隔离环境(那就得自己准备那个目录的凭据与 profile) |
540
- | `DSH_MCP_BOOTSTRAP` | 开 | `0` 关闭首启 profile 自动创建/同步(关掉后只驱动已存在的 profile,不写任何文件) |
541
- | `DSH_MCP_ROUTE_SOURCE` | 自动 | 复制模型路由的来源 profile 名;`__home__` 表示只认 `$DSH_HOME/cordis.patch.yml` |
542
- | `DSH_MCP_STATE` | `$DSH_HOME/.dsh-any-agent/conversations.json` | 会话注册表,跨重启存活。旧版 `<仓库>/.state` 会在首次启动时自动迁移一次 |
543
- | `DSH_MCP_RUNS_DIR` | `<状态文件同目录>/runs` | 异步完成哨兵的根目录(`<此目录>/<会话id>/<run_id>.json`) |
544
- | `DSH_MCP_LOCKS_DIR` | `<状态文件同目录>/locks` | 写锁"持有者登记"的目录。⚠️ **多个实例必须指向同一个值**,否则彼此看不见对方的持锁登记、会把对方误判成"无法识别的持有者(GUI)"而拒绝抢占 |
545
- | `DSH_MCP_APPROVAL` | `auto-allow`(非只读档) | 默认审批策略。设成 `ask` 才能让越界请求挂起给 `dsh_approval_decide`,而不是自动放行 |
546
- | `DSH_MCP_APPROVAL_TIMEOUT_MS` | `300000` | 待决 `ask` 审批的等待上限;超时按拒绝处理 |
547
- | `DSH_MCP_FORGOTTEN_DIR` | `$DSH_HOME/.dsh-any-agent-forgotten` | `dsh_release(forget=true)` 写的墓碑目录 |
548
- | `DSH_MCP_LOCK_STALE_MS` | `90000`(3× `REAP_INTERVAL_MS`,下限 30 秒) | 持锁登记多久没续心跳就算"持有者失联"(此时可被 `dsh_takeover` 自动抢占);改回收间隔会改变推导出的默认值 |
549
- | `DSH_MCP_IDLE_TTL_MS` | `0`(永不回收) | 会话空闲多久回收其 DSH 进程。★★ **`0` = 永不回收(默认)**:本服务一直握着写锁,**GUI 抢不走**(你在 GUI 里点开是**只读**,照样能看 ✓)。代价:每会话常驻一个 DSH 进程(**两次冷启动实测 476MB / 295MB**)。设成毫秒数(如 `300000`)可省内存,但要接受"锁可能被 GUI 抢走" |
550
- | `DSH_MCP_MAX_LIVE` | `8`(最坏约 2.5–4GB,见第 8 条) | ★ **最多同时保活几个会话进程**(`0` = 不限)。超标时回收**最久没用过**的(**在跑的回合绝不回收**)—— "**内存有界,锁尽量在手**"。内存紧用 `4`,机器大可以 `0`。用完想立刻放掉空闲会话:`dsh_release(all=true)`(busy 回合默认跳过,`force=true` 才中断) |
551
- | `DSH_MCP_LIST_PROBE_TTL_MS` | `10000` | `dsh_list` 磁盘探测结果的缓存时长(探测要 spawn 一个 DSH 进程,实测约 1 秒) |
552
- | `DSH_MCP_SENTINEL_TTL_MS` | `604800000`(7 天) | 启动时清理超过此时长的哨兵;`0` 关闭清理 |
553
- | `DSH_MCP_SENTINEL_INCLUDE_REASONING` | 未设置 | `1` 会把 `result.thinking` 一并写进哨兵(**打破"思考不落盘"的保证**);answer/error/title/工具输入仍会脱敏 |
554
- | `DSH_MCP_PERMISSION` | `danger-full-access` | `dsh_start` 未指定时的权限档 |
555
- | `DSH_MCP_REASONING_EFFORT` | `max` | 默认思考深度 |
556
- | `DSH_MCP_PROMPT_TIMEOUT_MS` | `0`(不设超时) | **仅 `wait=true` 时**的等待上限;到期只转后台,不取消回合 |
557
- | `DSH_MCP_REGISTER_WORKSPACE` | `project` | `project` / `all` / `0` |
558
- | `DSH_MCP_GIT_STATUS` | 开 | `0` 关闭每回合的 `git status` 摘要。分析不可信仓库时建议关掉(git 可能触发仓库配置的 fsmonitor/filter,且发生在 DSH 沙箱之外) |
559
- | `DSH_MCP_BACKUP_KEEP` | `20` | 每个客户端配置最多保留多少份 `.bak-dsh-*`(尽力轮转) |
560
- | `DSH_MCP_REQUEST_TIMEOUT_MS` | `60000` | ACP 通道单请求超时(initialize、session/new 等) |
561
- | `DSH_MCP_REAP_INTERVAL_MS` | `30000` | 空闲回收器多久检查一次。低于安全下限的值会回落到默认值 |
562
- | `DSH_MCP_LOG` | `silent` | `silent` / `info` / `debug` |
563
- | `DSH_MCP_LOG_STDERR` | 未设置 | `1` 才转发 DSH 原始 stderr |
564
-
565
- ## 🧪 测试
566
-
567
- ```bash
568
- npm test # 全部离线套件;没有 DSH 时 smoke 自动跳过会话段
569
- npm run test:all # 离线 + 需要模型的套件(需要 DSH/provider)
570
- node test/cleanup.mjs # 单独跑收尾清理(只清测试自己造的目录;--dry-run 可预演)
571
- ```
572
-
573
- 模型套件不在 GitHub 托管 CI 里(需要真实 DSH、provider 与凭据)。在本机跑,
574
- 或用 `model-suites` workflow 指定一台装好 DSH 的 self-hosted runner。
575
-
576
- 测试套件**会自己收尾**:`run.mjs` 结束时必定执行 `cleanup.mjs`,它只清理 cwd 在**测试自己造的目录**(`tmpdir()/dsh-any-agent-*` 或桌面 `dsh-any-agent-test-*`)里的会话与工作区登记。**用户在 `%TEMP%` 里自己的真实项目、以及你的状态目录都不会被碰**。要连桌面测试目录一起清,可显式跑 `node bin/dsh-any-agent-workspaces.mjs --purge-test-sessions`。
577
-
578
- | 套件 | 检查数 | 覆盖 |
579
- |---|---|---|
580
- | `smoke` | 29 | 握手、工具表、建会话、配置、注册表纯净性 |
581
- | `boundary` | 46 | 协议边界(重复 initialize、脏行、未知方法)、参数校验(含 read-only + 宽松审批必须拒绝、未知 provider/model、失败创建不留下写锁登记)、未知 id、游标边界、生命周期幂等、unicode |
582
- | `integration` | 30 | 跨进程 resume 与记忆、中断、两种插话 |
583
- | `async` | 16 | fire-and-forget + 事后验收 |
584
- | `sentinel` | 36 | 完成哨兵:原子性、闩锁语义、多会话并发不串台、被取消也落地、**思考不进文件** |
585
- | `prune` | 11 | **哨兵留存**:清超期与崩溃残留、留新、清空壳目录、`TTL=0` 关闭清理、且绝不碰同目录的会话注册表(不花 token,每次必跑) |
586
- | `guards` | 31 | **环境变量守卫**:奇怪的数值一律压到安全侧 —— `IDLE_TTL_MS` 的 `-1`/非法值都当"永不回收"(绝不主动让出写锁)、回收间隔过小压回默认(防空转紧循环)、**失联阈值下限 30 秒**(防别的实例"合法地"抢占并杀掉我们的回合)(不花 token,每次必跑) |
587
- | `sandbox-guard` | 21 | **测试沙箱自身**:「临时 HOME」如果落到真实用户目录、它的子目录、盘符根目录,或 OS 临时目录之外,一律报错拒绝,绝不被写入和删除。其余套件都通过 `test/sandbox-home.mjs` 拿目录、只销毁自己建的那个(不花 token,每次必跑) |
588
- | `approval-details` | 27 | **审批审查上下文**:待决审批的快照带命令、理由、cwd 与原始审批 ID —— 不只是一个裸 ID,调用方才能真正裁决;缺失的 `rawInput` 会按 ID 从先前的 `tool_call` 更新关联回来;凭据形状的字段与常见内联字面量在结构化上下文和展示标题里都会脱敏;关闭会话会取消待决审批。经真实 stdio MCP 端到端验证 |
589
- | `read-cursor` | 10 | **`dsh_read` 游标语义**:追加到已读条目里的文本必须再次返回(修订号游标,按 `seq` 替换全文);`limit` 钳到 1..500,`0`/负数不会空转或丢尾(不花 token) |
590
- | `line-decoder` | 4 | **UTF-8 行分帧**:MCP/ACP 的 chunk 边界不能把中文/emoji 截成 U+FFFD;覆盖逐字节、任意 chunk、CRLF 与 EOF 残留(不花 token) |
591
- | `sentinel-redaction` | 19 | **哨兵落盘卫生**:`result.thinking` 被剥掉、`thinking_omitted` 始终是布尔值、`answer`/`error`/`title`/`tools_used[].input` 都走与审批相同的凭据脱敏(不花 token) |
592
- | `forget-tombstone` | 10 | **`forget` 是终态**:`release(forget=true)` 写墓碑,按 id 查询明确失败并给出恢复路径,列表不再展示;删除墓碑后回到常规“未知会话”路径(不花 token) |
593
- | `bootstrap-profile` | 24 | **首启 profile 在零依赖发布形态下可用**:内置 YAML 子集解析器复制路由、剥掉 `apiKey`/`headers`/字面密钥、绝不写 `defaultPreset`、幂等;用户手写文件逐字节不动(不花 token) |
594
- | `workspace-safety` | 23 | **数据丢失边界**:`isTempPath` 按路径分段(`tmpdir()+'Suffix'`、`/tmpfoo` 都不算临时目录)、坏 `workspace.json` 绝不覆盖、会话根不可读时绝不清空 `sessionIds`、自动清理只删测试造的会话(不花 token) |
595
- | `permission-adopt` | 17 | **不无声提权**:接管没有权限记录的外部会话时 fail-closed 到 `read-only`;记录为 `read-only` 的会话不能被侧车提升;运行期回读记录与请求不一致会失败(不花 token) |
596
- | `state-safety` | 26 | **状态/哨兵/锁目录**:默认在 `$DSH_HOME/.dsh-any-agent`、不可写时启动明确失败、哨兵写失败返回 `null` + `sentinel_error`、非法数值落安全侧(不花 token) |
597
- | `setup` | 45 | **注册 CLI**:预览不写盘、`--remove` 只摘自己那条、没装客户端算跳过不算失败、Codex TOML 段往返、以及把写进配置里的命令真的拉起一次并要求应答 `initialize`(不花 token)。**不会调用 `claude mcp get`** —— 那个 CLI 会整体回写 `~/.claude.json` |
598
- | `removal-scope` | 24 | **注销只动自己那条**:给一份装着别的 MCP 服务器、顶层 Codex 设置和 Zed 兄弟设置的配置,注册再注销,断言其余每个值逐字节不变;删一个从没写过的东西 = 无写入、无报错(不花 token) |
599
- | `config-safety` | 407 | **0.3 这条线的核心性质**,逐客户端验证:预览后文件逐字节不变且不产生 `.bak`;注册再注销后其余条目逐字节还原、键顺序不变、顶层键不丢;4 空格 / 2 空格缩进、单行 JSON、无末尾换行、CRLF、UTF-8 BOM 都能在写入后保留;坏 JSON 与只读文件被拒绝且原文件无损;第二次 `--yes` 空跑不新增备份;每个 `.bak-dsh-<时间戳>` 只出现一次且内容等于写入前字节;PATH 上有 `claude` 也绝不调用(不花 token) |
600
- | `real-configs` | 84(7/9 配置时 66,本机 5/9 时 48) | **拿真家伙做往返**:把本机**真实**的客户端配置复制进临时目录(18 万字节、含几百个 `projects` 条目与大小写重复键的 `~/.claude.json`,238 行的 Codex TOML,Zed 的完整 `settings.json`),注册再撤销,要求每个文件**逐字节**还原;条目是否存在按**键**判断而不是裸子串;缺少 fixture 时明确打印 `skipped`,绝不假绿(不花 token) |
601
- | `commit-script` | 26 | 提交信息走 stdin,绝不落临时文件让 `git add -A` 卷进发布提交(不花 token) |
602
- | `roots` | 12 | 不传 `cwd` 时通过 MCP roots 取客户端当前项目;不支持 roots 的客户端得到可操作的报错(不花 token) |
603
- | `workspace-titles` | 15 | 家目录当工作区会被提前拦住;`title_hint` 与 DSH 自动标题分开返回(不花 token) |
604
- | `package-check` | 14 | `files` 白名单、零运行时依赖、Node 下限、bin shebang(不花 token) |
605
- | `timeout` | 38 | **超时语义**:`wait=false` 不受 `timeout_ms` 影响、`wait=true` 到期只转后台(不取消/不丢结果、`busy` 不说谎)、`timeout_ms<=0` 一直等、resume 失败会作废进程而不永久卡死、空 prompt 给清晰错误 |
606
- | `lock` | 79 | **写锁与抢占**:四类持有者判定、`writeMarker` 不覆盖活着的持有者、登记文件损坏/目录缺失等边界;**端到端**两实例抢锁 → 清晰报错 → 拒绝 → `force` 夺锁;**崩溃自动释放锁**、**卡死持有者无 force 自动抢占**;以及"一个回合 = 一个哨兵"的不变量(含内联回合与空闲插话) |
607
- | `cycle` | 36 | **异步派活的生命周期**:异步派活 → 收哨兵 → 空闲回收 → 再派活,连做 3 轮必须全程无锁错误;贴着回收窗口立刻重派(放大竞态);`dsh_release` 后立刻重派;并断言回收后**不留下无法归因的锁**(这条实测抓到过"刚拉起的进程被 reaper 当空闲收掉"的竞态) |
608
- | `concurrency` | 31 | 三会话并发 + 实时增量读 |
609
- | `capability` | 22 | 写代码、跑脚本、**自行派子代理**(用磁盘上的子会话头验证) |
610
- | `workspace-effect` | 24 | 不给路径时工作区是否真的生效 |
611
- | `acceptance` | 36 | 两个文件夹 × 两个会话做只读 IDA Pro 分析 |
612
- | `multi` | 19 | **多实例共存 + 非 ASCII 路径**:复现"两实例共写注册表 → 后写的把先建的会话抹掉",并验证**仅凭 id 就能从会话存储找回并真的派活**(指注册表丢失,不是显式 `forget` —— forget 是终态墓碑);**兜底取回不会悄悄提权**(read-only 会话取回后仍是 read-only);不留 `.tmp` 残渣;**中文 / emoji 工作区路径**能建会话、能干活、文件落在正确目录 |
613
- | `permission` | 11 | ★ **权限档是否真的生效**(安全属性):不看我们的返回值(那正是会说谎的地方),而是读**会话自己记录的事实**(投影缓存里的 `permissions.preset` / `sandboxMode`),三档逐一核对且三档记录值互不相同。**这条测试抓到过无声提权**:profile 里 `defaultPreset` 会在会话创建时盖掉 `DSH_PERMISSION_MODE` |
614
- | `list` | 13 | **`dsh_list` 的开销**(实测驱动):含磁盘探测的默认调用约 1 秒(要 spawn 一个 DSH 进程),**连续调用命中缓存降到个位数毫秒**;`only_running=true` 与 `include_closed=false` **跳过探测**且语义等价(不把磁盘上的未打开会话算进来) |
615
- | `reasoning-check` | 8 | **思考可见性**:`reasoning=hide` 不返回思考正文但统计非零;`reasoning=full` 返回正文且渲染出思考段 |
616
-
617
- **`node test/run.mjs` 在本机 5/9 客户端配置时跑 843 项(九种齐全时 879 项)** —— smoke 29 + 离线套件 814 项(九种齐全时 850 项)。0.3.9 上全部退出码 0;都不需要 provider(没有 DSH 时 smoke 自动跳过建会话段)。
618
-
619
- 其中 **524 项属于客户端注册器套件**(`setup` 45、`removal-scope` 24、`config-safety` 407、`real-configs` 48;九种齐全时 560 项)—— 0.3 的活其实都在这里。
620
-
621
- 需要模型的套件(`smoke` 29、`boundary` 46、`integration` 30、`async` 16、`sentinel` 36、`timeout` 38、`lock` 79、`cycle` 36、`concurrency` 31、`capability` 22、`workspace-effect` 24、`acceptance` 36、`multi` 19、`permission` 11、`list` 13、`reasoning-check` 8)合计 474 项,**最近一次完整实测在 0.3.9** —— 它们要可用 provider,发版前请自己重跑一遍:`node test/run.mjs --all`。`acceptance` 在没有 IDA MCP(`mcp__ida__*`)时会明确 **skip**。
622
-
623
- > 三个总数是刻意分开的:`node test/run.mjs`(843/879)、CI 离线 job(名义 814/850,不含 smoke;hosted runner 没有客户端 fixture 时 real-configs 的 48 项会跳过)、需要模型的集合(名义 474)。数字取各套件自己打印的汇总,不在这里手抄;不要把一个数字套到另一个 runner 上。
624
-
625
- ### 🧯 为什么会有 `sandbox-guard`
626
-
627
- 2026-10-29 的一次性验证脚本把沙箱路径存进了 **PowerShell** 的 `$home`。`$home` 是只读自动变量,赋值静默失败(脚本设了 `$ErrorActionPreference = 'Continue'`),`$env:USERPROFILE` 照样被改成了那个假路径,于是收尾那句 `Remove-Item $home -Recurse -Force` 落在**真实用户目录**上。那次运行在 120 秒超时被杀在半路,删掉了 `~/.codex`、`~/.cursor`、`~/.claude` 的一部分。
628
-
629
- 此前所有套件对**写入方**的断言都是真的,缺的是对**沙箱本身**的断言。`sandbox-guard` 就是这条断言:真实 HOME、它的子目录、大小写变体、盘符根目录、`os.tmpdir()` 之外的位置,全部拒绝。现在 `test/sandbox-home.mjs` 是唯一创建与销毁沙箱的地方,`setup`/`removal-scope` 自己不再调用 `mkdtempSync` 或 `rmSync` —— 一旦退回旧写法,套件立刻失败。
630
-
631
- ### 📏 本文档里的数字必须可复现
632
-
633
- 这里的每一个数字,要么是**测试打印出来的**,要么是**你用一条命令就能重跑出来的**。要改数字,就改成实测值 —— 不是估算,更不是一个看起来合理的值。
634
-
635
- 单列这一节,是因为这条规矩被违反过。早期版本的 README 声称 `dsh_send(wait=false)` "在 ~1ms 内返回"(实测 81ms)、打断收敛 "~18ms"(没有任何测试产出过这个数 —— `integration.mjs` 只断言 `waited_ms` 落在 0–25 秒之间;人工实测四次为 91/102/105/119ms)、每会话占用 "~120MB"(实测 295–476MB)—— 最后这个还在同一句话里自相矛盾,同时写着 "每个 120MB" 和 "3 个 = 428MB"。
636
-
637
- **三档,以及各自该怎么写:**
638
-
639
- | 档位 | 含义 | 写法 |
640
- |---|---|---|
641
- | **测试断言** | 有套件在断言它 | 直接给数字,套件就是权威 |
642
- | **实测** | 你跑过命令 | 给数字 **和测法**,让别人能复现 |
643
- | **估算** | 推导或上界 | 明确写出来("约"、"最坏"、"≈"),并给出算法 |
644
-
645
- **把测法写在同一处。** 内存那两个数字就是范例 —— 它写清了工具、前后对比、以及两次采样:
646
-
647
- ```text
648
- 实测 300-480MB / 会话
649
- (Get-Process node: 在单个活跃会话跑到 end_turn 前后分别求和 WorkingSet64;
650
- 两次冷启动分别得到 476MB 和 295MB)
651
- ```
652
-
653
- **你的测量和文档不符,就说出来。** 数字依赖机器、DSH 版本和负载。实测落在文档区间之外,值得开个 issue —— 区间是一个主张,不是装饰。
654
-
655
- ## 🔒 写锁:一个会话同一时刻只有一个写者
656
-
657
- DSH 的会话有**跨进程写锁**:**持有者活着就永不过期,也没有 API 能从活的持有者手里夺走** —— 所以**谁先拿到就决定一切**。本服务把它做成"**你看得见,我们写得动**":
658
-
659
- | 情形 | 结论 |
130
+ | 文档 | 内容 |
660
131
  |---|---|
661
- | **本服务持有锁**(默认) | ✅ 你在 GUI 里点开它是**只读**的:**内容照看**,但**抢不走锁**。默认 `DSH_MCP_IDLE_TTL_MS=0`(永不回收)保证锁不会空出来 |
662
- | **锁空着时你在 GUI 点开它** | ❌ **GUI 会永久持有它**(切走、等待、归档都不释放),本服务**再也接不回来**,只能重启 `dsh web` |
663
- | **推荐做法** | 看进度用 **`dsh_read`**(游标增量读)或等**哨兵文件**;非要用 GUI 就先 `dsh_release`(会中断在跑的回合,适合任务间隙) |
664
-
665
- **锁在谁手上**看 `dsh_status.lock_holder`,`dsh_takeover` 按四类分别处置:
666
-
667
- | `lock_holder` | 含义 | 处置 |
668
- |---|---|---|
669
- | `self` | 本进程持有 | 无需抢占 |
670
- | `stale-mcp` | 持有者**活着但已失联**(心跳 90 秒未续,典型是那个 MCP 卡死了) | ✅ `dsh_takeover` **自动杀掉并接管** |
671
- | `live-mcp` | 另一个**活着**的 dsh-any-agent 实例在用 | ⚠️ 默认拒绝,`force=true` 才夺 |
672
- | `none` | 无登记 → 多半是**你自己的 DSH GUI** | ❌ **绝不杀**(GUI 会话跑在 `dsh web` **单进程**里,杀它 = 整个界面连同其它 GUI 会话一起断),只报告 |
673
-
674
- > **两条实测补充**:① **崩溃不会留下孤儿锁** —— `SIGKILL` 掉 MCP,它拉起的子进程随 stdio 管道关闭一起退出,锁自动释放 ✓;所以 `stale-mcp` 真正对应的是"**活着但卡死**"。② **多实例**:注册表是"整份文档、最后写入者获胜"(后保存的会抹掉它没见过的会话),但**会话存储才是权威** —— 只要会话在磁盘上,**仅凭 id 就能取回并派活**;若你给每个实例配了不同的 `DSH_MCP_STATE`,**务必把 `DSH_MCP_LOCKS_DIR` 指到同一处**,否则彼此看不见对方的持锁登记。
675
- >
676
- > 完整实测过程(A/B 探测方法、GUI"只读打开"的证据、为什么连归档都不释放、损坏与卡死怎么区分)见 [使用注意事项 §3.5–3.12](USAGE-NOTES.md)。
677
-
678
- ## ⚠️ 已知限制
679
-
680
- 1. **逐字流式进不了模型上下文** —— MCP 的固有限制,不是 DSH 的。调用方拿到的是分步结果;人能通过 stderr 日志跟进。
681
- 2. **真·中途改向做不到** —— ACP 明确拒绝并发 prompt(`a prompt is already in flight for this session`)。`dsh_interject` 是实用等价物:取消 + 立刻开新回合,历史保留。
682
- 3. **图片提示不支持** —— ACP 自报 `promptCapabilities: {image: false}`。
683
- 4. **MCP 服务重启会连坐** —— 本服务被重启/杀掉时,它拉起的 DSH 子进程会随 stdio 管道关闭一起退出:**进行中的回合会中断、哨兵不会落地**(此时要靠核对工作区状态判断,别干等哨兵)。好消息是**写锁会自动释放**,重启后总能拿回来。
684
- 另注:本服务不会主动发 MCP 通知,"跑完通知"靠哨兵文件 + 调用方的后台等待任务实现。
685
- 5. **首次成功回合之前就死掉的会话可能尚未落盘** —— 此时 resume 会报清晰错误。发出第一条消息后就安全了。
686
- 6. **不能改名** —— DSH 的标题子系统没有对外改名接口(`SessionTitleService.rename` 需要进程内的活会话)。标题一律由首条消息自动生成。
687
- 7. **`session/list` 只返回 `{sessionId, cwd}`,且排除已打开的会话** —— 对话名由本服务从 DSH 投影缓存补全。
688
- 8. **默认"永不回收"要花内存(实测 300–480MB/会话)** —— 在 `MAX_LIVE` 窗口内,换来的是"GUI 抢不走写锁"。两次冷启动实测单个活跃会话新增常驻内存分别为 476MB 和 295MB(含 DSH 子进程与本服务),冷进程首次偏高。两种省内存办法:① **`DSH_MCP_MAX_LIVE`(默认 8**,超标就回收最久没用过的 → 内存有界 ✓ 锁仍尽量在手 ✓);② 把 `DSH_MCP_IDLE_TTL_MS` 设成毫秒数(代价是锁可能被 GUI 拿走)。一批活干完想立刻归零:**`dsh_release(all=true)`** ✓(默认只释放空闲会话,**正在跑的回合会跳过、继续跑**;要连它们一起停掉传 `force=true`。之后照样 resume)。
689
-
690
- ## ❓ 为什么 profile 还叫 `dsh-mcp`?
691
-
692
- 因为改名会打断一堆已经落盘的东西。
693
-
694
- 本服务首次启动时会在 `~/.dsh/profiles/dsh-mcp/` 建一个 DSH profile。这个目录名被另外三样持久化标识引用着:
695
-
696
- | 持久标识 | 位置 | 改名的后果 |
697
- |---|---|---|
698
- | profile 目录 | `~/.dsh/profiles/dsh-mcp/` | 已有会话解析不到 provider(`no adapter registered for provider "…"`) |
699
- | `DSH_MCP_*` 环境变量 | 你的客户端配置 | 所有部署都得同时改 |
700
- | `# dsh-mcp:managed` 标记行 | 生成的 `cordis.patch.yml` 首行 | bootstrap 不再认它是自己写的,从此不再同步 |
701
-
702
- 所以**产品名**是 `dsh-any-agent`,**持久标识**故意保留旧名。这是刻意的兼容决定,不是漏改。如果你是全新部署、想要别的 profile 名,直接设 `DSH_MCP_PROFILE` —— 服务会照你给的名字用。
703
-
704
- ## 📦 发布流程
705
-
706
- 发版清单。顺序有讲究 —— 其中两步是因为跳过它们发过一次坏版本才补上的。
707
-
708
- ```bash
709
- # 1. 升版本号。SERVER_VERSION 读的是 package.json,别处不用改
710
- # (以前这里有两份数字,漂移过)
711
- npm version patch --no-git-tag-version
712
- # 然后手工改 CHANGELOG.md:把 [Unreleased] 变成新版本一节
713
-
714
- # 2. 本地跑一遍离线套件(和 CI 相同,不花 token、不需要 DSH)
715
- node test/run.mjs
716
- node test/package-check.mjs
717
-
718
- # 3. 确认 tarball 本身:打得出来、装得上、起得来
719
- npm pack --dry-run
720
- # 再在临时目录里:npm install <tgz> && node_modules/.bin/dsh-any-agent --version
721
-
722
- # 4. 打 tag 并推送(CI 绿了再发)
723
- git add -A && git commit -m "chore: release x.y.z"
724
- git tag vx.y.z && git push origin main --tags
725
-
726
- # 5. 发布
727
- npm publish
728
- ```
729
-
730
- **发布后看到 404,不要当成失败。** npm 对新版本是异步入库(返回 `PUT 202` 而不是 `200`),几秒后去查 registry 仍然是 404,看起来像没发成功。先确认本地 npm 日志里有这一行:
731
-
732
- ```
733
- grep 'http fetch PUT 202' "$LOCALAPPDATA/npm-cache/_logs/<最新的>.log"
734
- ```
735
-
736
- 然后等一两分钟再查 `npm view dsh-any-agent dist-tags`。
132
+ | [安装与配置](docs/zh/install.md) | 前置要求、profile 引导、9 个客户端注册 |
133
+ | [5 分钟案例](docs/zh/demo.md) | 一条能跑通的端到端路径:派活、插话、自己复核 |
134
+ | [工具与用法](docs/zh/usage.md) | 派活/验收、哨兵、多会话、思考内容 |
135
+ | [团队模式](docs/zh/team.md) | 默认行为、关闭步骤(各客户端)、验证与回退 |
136
+ | [权限与隐私](docs/zh/security.md) | 权限档、审批、GUI 可见性、日志 |
137
+ | [写锁](docs/zh/locking.md) | 四类持有者、takeover、GUI 竞争 |
138
+ | [环境变量](docs/zh/reference.md) | 全部开关与默认值 |
139
+ | [测试](docs/zh/tests.md) | 离线/集成套件与覆盖 |
140
+ | [已知限制](docs/zh/limits.md) | 限制、profile 命名 |
141
+ | [兼容性](docs/zh/compatibility.md) | DSH 版本矩阵、团队包要求、升级检查 |
142
+ | [架构](docs/zh/architecture.md) | 为什么用 ACP、与 sdk/插件对比 |
143
+ | [发布流程](docs/zh/release.md) | 发版清单 |
144
+
145
+ ## 🧑‍🤝‍🧑 团队模式(按需开启)
146
+
147
+ 默认关闭。运行 `dsh-any-agent team on`,重连服务后 profile 切到 `dsh-team`(含实验性 Agent Teams);
148
+ `dsh_send` 会提示"需要并行/多角色时先建团队",简单任务不组队。
149
+ 关闭:`dsh-any-agent team off`;单次关闭:`dsh_send(team=false)`。
150
+
151
+ > 完整步骤(命令、验证、回退)见 [团队模式](docs/zh/team.md)。
737
152
 
738
153
  ## Star History
739
154