deepseek-coordination 0.1.8 → 0.10.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-CN.md CHANGED
@@ -1,697 +1,349 @@
1
- # deepseek-coordination
2
-
3
- 面向 DeepSeek Harness 的跨平台**原子 OS 交互层**。
4
-
5
- 一个通过**字符串**与操作系统对话的 agent,只要遇到路径含空格、文件带 UTF-8 BOM、行尾是 CRLF、文档不是 UTF-8,或命令由拼接产生,就会拿到错误答案。本项目用一套**参数稳定的动词集**取代字符串拼接,并把编码、字节序标记与行尾处理变成显式且逐字节精确的行为。
6
-
7
- 同一套动词面,在 Windows 与 POSIX 上各自原生实现,既可被 agent 通过单个 harness 工具调用,也可被人直接在终端里使用。
8
-
9
- ```console
10
- $ dsh-os selfcheck
11
- {"ok":true,"verb":"selfcheck","result":{"script":"bin/dsh-os.ps1","capabilities":{...}},"meta":{...}}
12
-
13
- $ dsh-os file charset "C:\path with spaces\notes.txt"
14
- {"ok":true,"verb":"file.charset","result":{"charset":"utf-8-bom","bom":true,"eol":"crlf"},"meta":{...}}
15
- ```
16
-
17
- * **完全开源**,MIT 许可。无遥测、无账号、无网络请求。
18
- * **零运行时依赖。** Windows 侧是 `Add-Type` P/Invoke;POSIX 侧使用 coreutils 一类的系统工具。运行时不会去获取任何东西。
19
- * **每个平台一个自包含文件**,由确定性构建从模块拼装而成。
20
-
21
- > [!WARNING]
22
- > 本交互层可以**读取你的屏幕、截取屏幕截图、读写剪贴板、合成鼠标与键盘输入、启动与结束进程、删除文件、修改环境变量**。在授予它任何权限之前,请先读[危险权限](#危险权限)一章。
23
-
24
- ---
25
-
26
- ## 为什么需要它
27
-
28
- 失败模式永远是同一个形状:某个值被变成了字符串的一部分,然后在下游被重新解释。
29
-
30
- | 故障 | 坏在哪里 | 本项目的做法 |
31
- |---|---|---|
32
- | `C:\Program Files\a b.exe` | 空格把一个路径拆成两个参数 | 值始终是**独立的 argv 元素**,绝不拼进 shell 字符串 |
33
- | 文本中间冒出 UTF-8 BOM | 每个消费方都看见 `\uFEFF` | 读取时剥离 BOM,且**绝不**泄漏到结果里;写入时还原 |
34
- | CRLF 文件被改写成 LF | diff 噪声、脚本损坏 | 读取报告 `eol`;写入还原原有约定 |
35
- | GBK 文档被当作 UTF-8 读 | 静默乱码(或静默退化到 `latin1`) | 编码显式声明;先试严格 UTF-8,再考虑回退 |
36
- | 命令靠拼接生成 | 注入、引号 bug | `execute.command` 接收真正的 **argv 数组**,永不经 shell |
37
- | 字节偏移落在字符中间 | 文本被切碎 | 偏移会按编码的字符边界校验 |
38
-
39
- 每次调用都恰好返回**一个 JSON 信封**,调用方按 `error.code` 分支而不是解析散文——而且**失败**的调用同样会输出可解析的信封,因为裸退出码对 agent 毫无信息量。
40
-
41
- ---
42
-
43
- ## 仓库结构
44
-
45
- ```
46
- deepseek-coordination/
47
- ├─ src/host/command.txt 权威动词契约,一个动词一行
48
- ├─ src/host/spec/verbs.tsv 机器可读动词表(单一真源),86 个动词
49
- ├─ src/host/spec/charsets.tsv IANA 名 → .NET 码页 → iconv 名 → BOM 字节,15 个规范编码
50
- ├─ src/host/lib/powershell5.1/*.ps1 真源:Windows PowerShell 5.1 侧,build.ps1 登记的 16 个模块 + 独立脚本 git.ps1
51
- ├─ src/host/lib/powershell7.6/*.ps1 真源:PowerShell 7.6 侧,同样的 16 个模块 + 独立脚本 git.ps1
52
- ├─ src/host/lib/*.sh 真源:POSIX 侧,16 个 shell 模块 + 独立脚本 git.sh
53
- ├─ src/host/lib/powershell.md 5.1 → 7.6 的 API 变更参考(无任何代码/测试/package.json 引用,不随包发布)
54
- ├─ src/host/bin/dsh-os.ps1 【生成物】Windows PowerShell 5.1 实现(218,514 字节)
55
- ├─ src/host/bin/dsh-os7.ps1 【生成物】PowerShell 7.6 实现(255,565 字节)
56
- ├─ src/host/bin/dsh-os.sh 【生成物】POSIX 实现(142,744 字节)
57
- ├─ src/host/bin/dsh-os.bat 仅 ASCII 的 cmd.exe 外壳(手写,非生成物)
58
- ├─ src/host/docs/os-spec.md 完整规范:契约、错误码、黄金用例矩阵
59
- ├─ src/host/docs/permission-model.md 授权模型及其残余风险
60
- ├─ src/plugin/index.js 宿主插件:注册 `dsh_os` 工具
61
- ├─ src/plugin/invoke.js 共享调用层:生成物/版本解析、spawn、信封解析
62
- ├─ src/plugin/settings.js 设置服务 + 面板的两条 HTTP 路由(`/api/dsh-os/*`)
63
- ├─ src/plugin/takeover.js `tools/execute` 拦截:把内置 read/write/edit/pwsh 交给 dsh-os 答复
64
- ├─ client.js 浏览器半边:渲染设置面板的 `dsh.client` 生成物
65
- ├─ src/permission/index.js 同意写入委托:`permission.json` 的唯一写入方,注册 5 个 `os_permission_*` 工具
66
- ├─ tools/build.ps1 拼装三个生成脚本;`-Check` 在漂移时失败
67
- ├─ tools/parse-check.ps1 在**将要执行代码的那个运行时**下解析检查模块,并报告非 ASCII 与 BOM
68
- ├─ tools/json-field.mjs 按点分路径读取信封里的一个字段(shell 测试用,`sed` 看不进嵌套对象)
69
- ├─ tools/verify-bundle.mjs 校验 loader 的解析契约(14 项)
70
- ├─ tools/verify-plugin-load.mjs 走真实 `apply()` 路径(30 项)
71
- ├─ tools/verify-editions.mjs 证明两个 PowerShell 生成物都可达(14 项)
72
- ├─ tools/probe-pipeline.mjs 在打桩的动词层上端到端驱动拦截链路(23 项)
73
- ├─ tests/ 黄金(Windows/POSIX/桌面注入)、两表一致性、编码、插件、授权、接管各套测试
74
- │ ├─ conformance.ps1 `command.txt` 与 `spec/verbs.tsv` 必须描述同一个面(5 项)
75
- │ ├─ takeover.test.mjs 字节级格式保留、读取窗口切片、可拦截资格(41 项)
76
- │ ├─ smoke-desktop-sh.sh POSIX 桌面动词**实际注入了什么**(52 项)
77
- │ └─ fake-xdotool, fake-wmctrl 会记录调用的替身,使无 X server 的机器也能校验桌面动词
78
- └─ permission.json 授权数据(随仓库为空文件,这就是初始状态)
79
- ```
80
-
81
- 两套 PowerShell 运行时分目录存放,原因是差异并非表面性的:.NET Framework 4.8 没有 `ProcessStartInfo.ArgumentList`,且需要一条感知 ANSI 的编码路径;.NET 10 两者皆备。分开存放意味着任何一侧都不必背负另一侧的权宜写法。
82
-
83
- `src/host/lib/` 中每个模块成对提供:
84
-
85
- | 模块 | 职责 |
86
- |---|---|
87
- | `preamble` | 严格模式、编码钉死、仓库根定位 |
88
- | `json` | 错误模型、JSON 序列化、信封写出 |
89
- | `command` | 参数解析(位置与 `name="value"`)、断言、类型转换 |
90
- | `charset` | 编码表、BOM 策略、字节级文本模型 |
91
- | `native` | P/Invoke 声明与原子替换(仅 Windows;POSIX 侧为空) |
92
- | `paths` | 路径解析与受保护路径边界 |
93
- | `file` | `file.*`、`file verify.*`、`file search` |
94
- | `system` | `system.*` 查询 |
95
- | `execute` | `execute.command` / `.synchronous` / `.asynchronous` |
96
- | `task` | `task.*` |
97
- | `environment` | `environment.*` 与 `environment attribute.*` |
98
- | `service` | `service.*` |
99
- | `permission` | `permission.*`(只读)与 `selfcheck` |
100
- | `window` | `window.*`、`mouse.*`、`keyboard.*`、`clipboard.*`、`wait.*`、观察面 |
101
- | `dispatch` | 动词解析与分发表 |
102
- | `entry` | 运行、输出、退出 |
103
-
104
- 生成脚本是**单个自包含文件**,而不是运行时 dot-source。在 Windows PowerShell 5.1 上,一个不带 BOM 的被 source 文件会按 ANSI 码页解码——这恰恰就是本项目要消灭的那类编码故障。
105
-
106
- ---
107
-
108
- ## 开始使用
109
-
110
- 已发布到 npm,因此 DSH profile 可以把它作为 bundle 安装:
111
-
112
- ```console
113
- dsh plugin install deepseek-coordination
114
- ```
115
-
116
- 或从源码使用:
117
-
118
- ```console
119
- git clone https://github.com/eloyhere/deepseek-coordination.git
120
- cd deepseek-coordination
121
- ```
122
-
123
- 脚本无需构建或安装,直接运行:
124
-
125
- ```powershell
126
- # Windows
127
- src\host\bin\dsh-os.ps1 selfcheck
128
- src\host\bin\dsh-os.ps1 file charset "C:\path with spaces\file.txt"
129
- src\host\bin\dsh-os.ps1 file verify charset .\file.txt utf-8
130
- src\host\bin\dsh-os.ps1 environment attribute get PATH --scope process
131
- ```
132
-
133
- ```sh
134
- # POSIX
135
- src/host/bin/dsh-os.sh selfcheck
136
- src/host/bin/dsh-os.sh file charset "./a file.txt"
137
- ```
138
-
139
- `dsh-os.bat` 是给人在 `cmd.exe` 里手敲的。能传 argv 数组的调用方——harness、Node 的 `execFile`、CI——应当**直接**调用 `.ps1` 或 `.sh` 并彻底绕开 `cmd.exe`,因为批处理文件的参数处理本身就是引号 bug 的来源。
140
-
141
- ### 从 harness 使用
142
-
143
- `package.json` 声明了 `dsh.bundle.patch`,因此本包可以作为一个 bundle 安装进 DSH profile,并插入五行:
144
-
145
- | 行 | 入口 | 作用 |
146
- |---|---|---|
147
- | `dsh-os` | `.` | 注册 `dsh_os` 工具 |
148
- | `dsh-os-settings` | `./settings` | 面板编辑的设置,以及它的两条 HTTP 路由 |
149
- | `dsh-os-takeover` | `./takeover` | 在开启时把内置 `read`/`write`/`edit`/`pwsh` 交给 dsh-os 答复 |
150
- | `dsh-os-permission` | `./permission` | 同意写入委托,`permission.json` 的唯一写入方 |
151
- | `dsh-os-workflow` | `./workflow` | 意图优先的工作流工具及其系统提示词小节 |
152
-
153
- 各行必须使用**不同的模块说明符**:`name` 按包说明符解析,若两行都用裸包名就会把同一个模块加载两次,从而抛出 `tool "dsh_os" is already registered` 并中止整个 entry。子路径导出正是为此存在。
154
-
155
- | 工具 | 用途 |
156
- |---|---|
157
- | `dsh_os` | 执行动词面上的一个动词,并原样返回其 JSON 信封 |
158
- | `os_permission_*` | 读取、授予、撤销与审计持久授权决定 |
159
- | `workflow_plan` | 执行意图优先工作流的一个阶段(见[工作流](#工作流)) |
160
-
161
- 只注册一个工具而非每个动词一个,理由有三:动词表就是契约且存放在 `src/host/spec/verbs.tsv`,新增动词时无需重新生成 schema;agent 用一次 `verb: "selfcheck"` 调用就能发现整个动词面;信封本身已是稳定的结果形状,不会出现第二份会与脚本漂移的契约。
162
-
163
- ```jsonc
164
- // 调用形状
165
- { "verb": "file charset", "arguments": ["path=\"./a.txt\""], "allow": false }
166
- { "verb": "environment attribute get", "arguments": ["PATH"], "flags": ["--scope", "process"] }
167
- ```
168
-
169
- 实现以绝对路径加 argv 数组方式拉起,永不经 shell,因此含空格的值仍然是一个值。
170
-
171
- ### 接管内置操作
172
-
173
- `dsh_os` 是一个**必须被告知才会用**的工具,而实际上永远不会被告知:内置的 `read`/`write`/`edit`/`pwsh` 就在手边,能力几乎完全重叠,提示词里也没有任何东西偏向其中一方。于是本项目存在的那些保证从未被真正走到——插件装上了,却感知不到。
174
-
175
- **接管**消除了这个选择。在 **设置 → DSH OS** 里打开后,内置调用会由同一套机制答复,无论 agent 是否配合。逐工具开关可以只接管 `write`、或只接管 `edit`,其余保持不变。
176
-
177
- 它走的是 `tools/execute` 这条 around-dispatch waterfall:处理器返回 `{ value }` 并且不调用 `next()`,该值随后由**内置**工具自己的 `output.schema` 校验、由**内置**的 `output.render` 渲染。因此模型看到的文本、diff 卡片与展示元数据都没变,变的只是这个值的字节来源。
178
-
179
- **不接管的部分及原因:**
180
-
181
- * `glob` 与 `grep`。本动词面**完全没有按内容检索的动词**(`file search` 匹配的是**文件名**),所以 `grep` 的 `lineNumber`/`line` 物理上无法产出,glob 的忽略规则与 mtime 排序也没有对应物。接管它们会静默返回一个不同的结果集。
182
- * 后台命令与沙箱升级。后台调用的结果是一个 harness 作业 id,本动词面造不出来;`sandbox_permissions` 则是 harness 自己的审批缝。
183
- * 相对路径。内置工具经 `ctx.fs` 按会话工作区解析它们,本插件没有等价能力;去猜进程工作目录只会读到与调用方所指不同的文件。
184
-
185
- **有两项性质是被刻意放弃的,开启前应当知道:**
186
-
187
- 1. **被接管的调用不再受文件系统沙箱约束。** 内置写路径经 `ctx.fs`,沙箱策略与"写前必读"的观察守卫都在那里;被接管的写直接执行动词,只剩 dsh-os 自己的受保护路径检查。这是真实的防御削弱,不是细节。
188
- 2. **上报的文本与磁盘上的字节可以不同。** 默认的*沿用原文件*格式下,CRLF 文件保持 CRLF、BOM 保持 BOM,而 `value.after` 是归一化后的 LF 文本——也就是内置 `read` 会显示、diff 卡片会比较的那份文本。若希望接管在字节上什么都不改,请选择*同内置*。
189
-
190
- 每一次被接管的调用在任何失败下都会回退到内置实现,因此接管只能改变结果**如何产生**,绝不会改变调用方**是否拿到结果**。面板的「最近活动」会显示实际接管了什么、以及任何回退的原因。
191
-
192
- **选择 PowerShell 版本。** 默认仍是 5.1 产物(`bin/dsh-os.ps1`),**刻意不变**:已经工作的部署不应因为插件升级而在脚下改变行为。要选 7.6 产物(`bin/dsh-os7.ps1`):
193
-
194
- * `powershellEdition: '7.6'` —— 插件配置;
195
- * `DSH_OS_POWERSHELL_EDITION=7.6` —— 无法改 profile 的部署;
196
- * `'auto'` —— 仅当产物与可用的 `pwsh` **同时**存在时才用 7.6。
197
-
198
- 配置优先于环境变量;显式的 `scriptPath` 完全禁用该选择。`pwsh` 的查找顺序是显式 `DSH_OS_POWERSHELL` → `PATH` → 标准安装位置,因为 `pwsh` 并不总在 `PATH` 上(本仓库自己的环境里就不在,而 7.6.6 明明装在标准位置)。
199
-
200
- > [!IMPORTANT]
201
- > **仍未证明:harness 自身的 loader 会在活动 profile 里组合这个 bundle。** 插件的解析、装载与执行路径现由 `tools/verify-bundle.mjs`、`tools/verify-plugin-load.mjs` 与 `tools/verify-editions.mjs` 覆盖(详见[安装现状](#安装现状与已知缺陷)),但"装进用户的桌面活动 profile 并重启 harness"这一步**没有被跑过**。
202
-
203
- ### 工作流
204
-
205
- 本包其余各行让一次**操作系统调用**变正确;这一行让一个**需求**在被执行之前就被决定。它消除的失效模式是:一个从未成为规格的需求——模型直接开始改文件,等到评审时才发现方向偏了,而 token 已经花掉。
206
-
207
- `dsh-os-workflow` 注册一个工具 `workflow_plan`,以及一个系统提示词小节。
208
-
209
- ```
210
- intake -> plan -> choose -> run -> done
211
- ```
212
-
213
- | 阶段 | 动作 | 它确立了什么 |
214
- |---|---|---|
215
- | intake | `start` | **逐字**记录的提示词,外加 1–3 个关于真实意图的问题,**或**你所做的假设 |
216
- | plan | `plan` | 方案与流程、**至少两个**各带利弊的备选、评判标准,以及你的推荐 |
217
- | choose | `choose` | **用户的选择**,在同一次调用内经 `ctx.userQuestions` 取得 |
218
- | run | `run` | 执行、步骤,以及在同一次调用内关闭工作流的结论 |
219
- | — | `note` | 执行期间记下的一条备注或一个决定 |
220
- | — | `checkpoint` | 把记录折叠进一个文件,并回传摘要 |
221
- | — | `status`、`reset` | 读取状态;丢弃并重新开始 |
222
-
223
- > [!IMPORTANT]
224
- > **工具名是 `workflow_plan` 而不是 `workflow`,原因是真实存在的命名冲突。** 宿主组合里已经有一个内置的 `workflow` 工具——来自 `@deepseek-ai/dsh-tool-workflow` 的 JavaScript 多 agent 编排器。同一作用域内两个工具同名会相撞:后注册者中止,或者两者都解析成功、模型在同一名字下拿到两个互不相关的工具。由于本行的工具位于 host 层,而 `workflow` 是按 agent preset 挂载的,本仓库里没有任何测试能发现这一点;它是在读取**实时工具目录**时发现的。`_plan` 同时也说明了这个工具是什么:工作流的规划那一半。
225
-
226
- **门是硬的。** 入口尚未记录的阶段会被拒绝,且拒绝信息会点明必须先跑哪个阶段。`choose` 无法由模型自己回答:问题经与 `ask_user_question` 相同的缝交给用户,除此之外没有任何路径能进入 `run`。
227
-
228
- **四次调用、一次提问。** 不可避免的路径是 `start` → `plan` → `choose` → `run`。它原本是八次调用、两次提问,差别来自三处刻意的合并——每一处都去掉一个来回,而没有去掉任何一个决定:
229
-
230
- * **设计与备选是同一次调用**,因为它们本就是同一个决定。这就是 QOC(Questions、Options、Criteria)与 ADR(Context、Decision、Consequences)的形状:一条决策记录同时持有它被选出的那些备选、以及评判它们所用的标准。拆成两步会让第二步重新推导第一步,并让用户把同一份架构读两遍。
231
- * **意图清晰的请求零提问。** `start` 接受 `assumptions` 而非 `questions`,因此不含歧义的提示词不会被打断——而假设被记录在案,用户可以纠正,这比它所替代的那个问题更便宜。"最小且相关"的提问优于盘问,其证据是 Ask-to-Act 这一线工作([arXiv:2504.00907](https://arxiv.org/abs/2504.00907))。
232
- * **单步执行在开启的同一次调用内关闭。** 给 `run` 传 `outcome` 即结束工作流;只有多步工作才需要第二次调用。
233
-
234
- **压缩是另一半。** 被叙述进对话的计划会随着之后每一步被重新发送,这正是短任务变贵的原因。所以它从不进入对话:每个阶段写下一份真正的文档,只回传一行——阶段、摘要、文件路径、下一个调用。`tests/workflow.test.mjs` 会度量这个比例,并断言任何阶段的信封都不超过 2000 字符。这个形状有实测依据而非风格偏好:剪枝加摘要能在降低 token 与运行时的同时提升完成率([Less Context, Better Agents](https://export.arxiv.org/pdf/2608.16370)),而规划/执行分离能以 token 成本的一小部分达到与交错推理相当的准确率([Plan Once, Ground Locally](https://zenodo.org/records/22904595))。
235
-
236
- **开销落在哪里**,以复杂度表述,因为它本来就是复杂度:
237
-
238
- | 路径 | 开销 |
239
- |---|---|
240
- | 读取状态 | 每次工作流调用一次,按文件 mtime 缓存——而**绝不在模型步上**发生,因为提示词小节根本不读状态 |
241
- | 一次阶段跃迁 | 一次原子状态写入;第一版每个动作写三次 |
242
- | 执行期间的一条 note | 一次 `appendFileSync` 追加,O(1),由压实维持有界——而不是"读-推-重写"那种在整个工作流上 O(n²) 的做法 |
243
- | 提示词小节 | 一个**常量**:不读、不查、不格式化,在每个工作流的每个阶段都完全一致 |
244
-
245
- ### KV 缓存效应——提示词小节为何必须是常量
246
-
247
- 这一行注册进系统提示词的小节是一个**常量字符串**。这不是为了整洁,这就是设计本身,理由是:提示词是任何会变动的内容最不该出现的地方。
248
-
249
- **本包实际测到了什么。** `tools/measure-cache.mjs` 直连真实端点(`deepseek-flash`、`max_tokens: 1`、同一条谱系先热身一次、每个变量只发一次请求),读取 `cache_read_input_tokens`。测了两遍,结果一致:
250
-
251
- | 形状 | 缓存读 | 总输入 | 命中率 |
252
- |---|---:|---:|---:|
253
- | 不作改动(对照) | 5,632 | 5,780 | 97.4% |
254
- | 追加一条 user 消息 | 5,632 | 5,784 | 97.4% |
255
- | 追加一条 tool result | 5,632 | 5,853 | 96.2% |
256
- | **一次工作流:四次追加调用** | 5,632 → 5,888 | 每次约 5,900 | **均值 96.6%** |
257
-
258
- 也就是说,本行自身的形状是被实测为「热」的:四次调用、23,040 个缓存命中 token、每一次都在 96% 以上。
259
-
260
- **本包没能测到、因此不会声称的部分。** 实验同样尝试了反事实——重写前导系统提示词、改一个工具描述、对调两个工具——但这些读数**不可复现**:第一遍全部读作 0.0%,第二遍全部读作 97%。provider 文档给出了原因,且这一点本身值得知道:它会持久化**跨请求检测到的公共前缀**,因此到第二遍时,反复发送的相同前缀已被学习,改动之后依然能匹配。共享账号上的「每变量 A/B 隔离」无法对改动一侧下定论,所以 `tools/measure-cache.mjs` 只对追加一侧下断言(那一侧稳定),其余只作为观察打印、不作为结论。
261
-
262
- **反事实一侧改由独立证据支撑**,而这些证据很强:
263
-
264
- * provider 自己的规则是:一次缓存命中要求请求**完整匹配一个已持久化的 cache prefix unit**,而系统提示词排在工具目录与全部历史之前([上下文硬盘缓存](https://api-docs.deepseek.com/zh-cn/guides/kv_cache/));
265
- * 在同一端点上的十变量实验发现:末尾追加消息保住 **98.7%** 命中,而在**系统提示词末尾**加一行得到 **0.0%**([前缀稳定性一章](https://github.com/diguike/book-deepseek-harness/blob/main/book/11-prefix-stability.md));
266
- * harness 自己付过这笔账:沙箱策略原本写在系统提示词里,用户切一次权限,缓存命中从 **约 14,700 token 掉到 256**;修法是把会变的事实挪出提示词、放进消息序列([DSH Agent Note](https://github.com/deepseek-ai/deepseek-harness/blob/master/.agents/notes/implemented/feature/2026-09-02-in-history-system-prompt-replacement.md))。
267
-
268
- **这一行的第一版把当前阶段渲染进了那个小节**,于是四次调用的工作流会三次重写系统提示词。无论 provider 某一天是否恰好宽容这件事,本行都不该依赖它——所以修法是结构性的:
269
-
270
- * 小节是常量,因此本行的**缓存效应是 `prefix-stable`**:每个工作流零次提示词变更,由 `tests/workflow.test.mjs` 在每个阶段对注册文本逐字节断言,并在删掉 `state.json` 之后再断言一次;
271
- * 当前阶段与**下一个该发的调用**改由 `workflow_plan` 工具自身的结果携带——追加的历史,上表实测 96–97%;
272
- * 顺序错误的调用会被拒绝,且拒绝信息点明必须先记录哪个阶段,因此模型无需提示词告知任何东西也能自我纠正;
273
- * 发现入口放在工具**描述**里。它位于工具目录中:在前缀之内、只发送一次、整个会话不变。
274
-
275
- > [!IMPORTANT]
276
- > **读者应当为自己插件得出的结论。** 任何逐步变化的东西——时间、当前目录、分支、权限、阶段——都不能写进提示词小节。本 harness 为这类内容准备了专门的通道:用 `ctx.systemPrompt.context()` 注册,它会作为一条追加的 user 消息送达,并且只在内容变化时重新投影。小节只用于在整个会话内稳定的文本。
277
-
278
- **状态存放在哪。** `$DSH_HOME/dsh-os/workflows/<session>/`,与设置文档同级,理由相同:包目录会在升级时被整体替换,而工作区属于用户。`state.json` 是紧凑记录,其余文件都是细节。已结束的工作流会被归档而不是覆盖,因此下一个从干净状态开始,而上一个仍然可读。
279
-
280
- ```
281
- state.json 紧凑记录:阶段、意图、决定、步骤、产物路径
282
- journal.jsonl 仅追加:备注与步骤,有界,由 checkpoint 折入
283
- 01-intent.md 逐字提示词,随后是问答(或假设)
284
- 02-plan.md 方案与流程、备选及其利弊、标准、决定
285
- 03-execution.md 选中的选项、目标、步骤、涉及文件、约束
286
- 04-outcome.md 交付了什么,以及任何仍未关闭的步骤
287
- 05-checkpoint.md 折叠:至今为止的全部决定,在需要压缩时写出
288
- ```
289
-
290
- **可配置项。** `workflowRoot` 改变目录位置;`autoCompact`(默认 `true`)控制 checkpoint 是否报告引擎可能执行的压缩,`maxCheckpoints` 限制其数量。结构性的一半——细节落盘、只回传一行——**不可配置**,因为它正是这个功能能省下东西的原因。
291
-
292
- > [!WARNING]
293
- > **这一行会向用户提问,因此部署必须能回答。** 它注入 `userQuestions`;缺少该服务的宿主什么都不注册,而不是注册一个第二次调用就会失败的工具。有两次调用会阻塞等人:给了问题的 `start`,以及永远如此的 `choose`。
294
-
295
- > [!NOTE]
296
- > **这一行不会调用引擎的压缩服务。** `ctx.compaction` 确实存在,但从这里调用它意味着构造一个会话形状的上下文,而那个契约并未公开——那是把猜测包装成功能。本插件的做法是确定且可检查的:细节离开对话、进入文件,而 `checkpoint` 记录下压缩安全的边界。正因如此,这里的压缩是被 `tests/workflow.test.mjs` **度量**的,而不是仅仅被声称。
297
-
298
- ---
299
-
300
- ## JSON 契约
301
-
302
- 每次调用都向 stdout 写出**恰好一个** JSON 信封,UTF-8 **无 BOM**,以单个 LF 结尾。
303
-
304
- ```jsonc
305
- // 成功
306
- { "ok": true, "verb": "file.charset", "result": { "charset": "utf-8", "bom": false, "eol": "crlf" },
307
- "meta": { "durationMs": 3, "platform": "win32", "shell": "pwsh" } }
308
-
309
- // 失败 —— 仍然恰好一个信封,仍然在 stdout 上
310
- { "ok": false, "verb": "file.charset",
311
- "error": { "code": "E_NOT_A_FILE", "message": "…", "hint": "…" },
312
- "meta": { "durationMs": 2, "platform": "win32", "shell": "pwsh" } }
313
- ```
314
-
315
- * `error.hint` 仅在确有可操作建议时才出现。
316
- * `--pretty` 缩进输出;`--quiet` 只输出 `result`(或只输出 `error`)。
317
- * **stderr 只承载人类可读的诊断,绝不参与解析。** Windows 侧设置 `DSH_OS_TRACE=1` 可在 stderr 上得到失败追踪。
318
-
319
- ### `error.code` 才是权威
320
-
321
- 退出码只是粗分类,方便 shell 廉价地分支。**绝不要解析 `error.message`。**
322
-
323
- | 退出码 | 类别 | 代表错误码 |
324
- |---|---|---|
325
- | 0 | 成功 | — |
326
- | 1 | 用法 | `E_USAGE` |
327
- | 2 | 类型/取值 | `E_TYPE`、`E_CHARSET_UNKNOWN`、`E_BOUNDARY`、`E_RANGE`、`E_WINDOW_AMBIGUOUS` |
328
- | 3 | 授权 | `E_PERMISSION`、`E_ELEVATION_REQUIRED`、`E_PERMISSION_PROTECTED_PATH`、`E_PERMISSION_DELEGATE_ONLY`、`E_PERMISSION_FILE_UNTRUSTED`、`E_PERMISSION_DENIED_BY_RULE`、`E_PERMISSION_NO_ANSWERER` |
329
- | 4 | 平台不支持 | `E_NOT_SUPPORTED`、`E_NO_DISPLAY`、`E_ACCESSIBILITY_DENIED`、`E_ACCESSIBILITY_UNAVAILABLE` |
330
- | 5 | 目标/编码/IO | `E_NOT_FOUND`、`E_NOT_A_FILE`、`E_EXISTS`、`E_BINARY_CONTENT`、`E_CHARSET_MISMATCH`、`E_TRUNCATED`、`E_PARTIAL_WRITE`、`E_WINDOW_NOT_FOUND`、`E_IO` |
331
- | 6 | 超时 | `E_TIMEOUT` |
332
- | 7 | 内部/被中止 | `E_INTERNAL`、`E_ABORTED` |
333
-
334
- Windows 实现登记了 **36** 个错误码;POSIX 实现以 `case` 分组。两者对每个**已具名**错误码的归类一致,但对**未登记**错误码不一致:Windows 返回 7,POSIX 返回 5。
335
-
336
- ### 调用约定
337
-
338
- 值始终是独立 argv 元素,**绝不**拼进 shell 字符串。两种写法等价,但同一参数不得混用:
339
-
340
- ```sh
341
- # 位置形式
342
- dsh-os file create "./a.txt" "123" "utf-8-bom"
343
-
344
- # 具名形式 —— 顺序任意;实际顺序取自动词表的参数列表
345
- dsh-os file create content="123" charset="utf-8-bom" path="./a.txt"
346
- ```
347
-
348
- 以下情况以 `E_USAGE` 拒绝:未知标志、空的 `--`、取值标志缺值、具名参数重复、未知名字、同一参数既给位置又给名字、多余的位置参数。读操作永不被要求 `--allow`;写操作总是被要求。
349
-
350
- 动词深度为一到三段——`selfcheck`、`file charset`、`file verify charset`——解析器只在扩展形状确实已知时才吞掉下一个 token,因此一个恰好以单词开头的路径参数不会被当成动词的一部分吃掉。
351
-
352
- ### 编码、BOM 与行尾
353
-
354
- `src/host/spec/charsets.tsv` 定义了 15 个规范编码名,各含别名、.NET 码页、`iconv` 名与 BOM 字节:
355
-
356
- `utf-8`、`utf-8-bom`、`utf-16le`、`utf-16be`、`utf-32le`、`utf-32be`、`gbk`、`gb18030`、`big5`、`shift_jis`、`euc-jp`、`euc-kr`、`latin1`、`windows-1252`、`ascii`。
357
-
358
- 探测顺序是先嗅 BOM,再试**严格** UTF-8,然后是 GBK,最后 latin1。读取时剥离 BOM 并把 `\r\n` 归一为 `\n` 用于输出;**任何写回操作都还原文件原有的 BOM 存在性与行尾约定。** 偏移是半开区间 `[start, end)` 的字节偏移,必须落在字符边界上,否则以 `E_BOUNDARY` 失败,而不是把一个字符切碎。
359
-
360
- 二进制判定是载荷前 8 KiB 内出现 NUL 字节,报 `E_BINARY_CONTENT`,而不是给出乱码。
361
-
362
- ---
363
-
364
- ## 动词面
365
-
366
- `src/host/spec/verbs.tsv` 中**声明了 86 个动词**,它是单一真源。其中一部分只声明未实现;在依赖某个动词之前,请先看[实际可用范围](#实际可用范围)。
367
-
368
- | 组 | 动词 |
369
- |---|---|
370
- | `file` | `create` `charset` `encode` `length` `information` `list` `delete` `move` `copy` `mkdir` `append` `insert` `view` `search` |
371
- | `file verify` | `charset` `encoding` `completion` |
372
- | `execute` | `command` `synchronous` `asynchronous` `query` `kill` |
373
- | `system` | `brand` `cpu` `gpu` `version` `info` |
374
- | `task` | `list` `query` `show` `kill` `launch` |
375
- | `service` | `list` `query` `start` `stop` `restart` |
376
- | `environment` | `list` `get` `add` `set` `delete` |
377
- | `environment attribute` | `list` `get` `add` `set` `delete` |
378
- | `permission` | `query` `list` `audit` `grant` `revoke` `acl init` |
379
- | `window` | `find` `list` `active` `close` `top` `bottom` `move` `resize` `minimize` `maximize` `restore` |
380
- | `mouse` | `click` `context` `over` `wheel` `down` `up` `cursor` |
381
- | `keyboard` | `down` `up` `press` `combination` `type` `codes` |
382
- | `clipboard` | `read` `write` `paste` |
383
- | `wait` | `window` `ms` |
384
- | 观察面 | `screenshot` `text` `structure` |
385
- | 自检 | `selfcheck` |
386
-
387
- 几个值得先知道的设计点:
388
-
389
- * **`file verify.*` 是断言,不是抛错。** `file verify charset` 会在返回观测值的同时给出 `match: false`,让调用方能区分"文件不对"与"我要求错了"。`file verify completion` 在你没给任何预期时返回 `complete: null`——这是刻意不写成 `false`。
390
- * **`file insert` 的 `--charset` 是标志,不是位置参数。** 夹在 `content` 与 `offset` 之间时它会**静默顶掉 offset**。
391
- * **`file.delete` 在 Windows 上默认进回收站。** 在 POSIX 上需要 `trash-put`,否则报 `E_NOT_SUPPORTED`——它**不会**静默地真删。
392
- * **`environment` 有两层。** 变量持有一个字符串;**属性**(PATH、PATHEXT、LIB、INCLUDE)持有一个有序列表。写入会返回 `visibleToRunningProcesses`,因为 `user` 或 `machine` 级写入对已经在运行的进程不可见。
393
- * **`keyboard type` 以 Unicode 为先。** 非 ASCII 文本走剪贴板粘贴,因为在有输入法的情况下 `KEYEVENTF_UNICODE` 并不可靠。
394
-
395
- ---
396
-
397
- ## 危险权限
398
-
399
- 在授予任何权限之前,这一章是必读的。本交互层刻意做得很有能力,而它给出的保证比功能列表看起来要窄。
400
-
401
- ### 一个被完全授权的 agent 能做什么
402
-
403
- | 能力 | 涉及动词 | 授权要求 |
404
- |---|---|---|
405
- | **读取你的屏幕内容** | `screenshot`、`text`、`structure` | **只读,永不弹窗** |
406
- | **读写你的剪贴板** | `clipboard.read`、`clipboard.write` | `read` 永不弹窗;`write` 需 `--allow` |
407
- | **合成输入**——点击、拖拽、打字、组合键 | `mouse.*`、`keyboard.*` | `--allow` |
408
- | **操纵你的窗口**——移动、改尺寸、关闭、置前 | `window.*` | `--allow` |
409
- | **启动与结束进程** | `task.kill`、`execute.*` | `--allow` |
410
- | **执行任意命令行** | `execute.synchronous`、`execute.asynchronous` | `--allow` |
411
- | **删除、移动、覆盖文件** | `file.delete`、`file.move`、`file.copy` | `--allow` |
412
- | **持久修改你的环境变量** | `environment.set`、`.add`、`.delete`、`attribute.*` | `--allow` |
413
- | **控制系统服务**(已声明,未实现) | `service.start`、`.stop`、`.restart` | `--allow` + 提权 |
414
- | **读取进程与系统清单** | `task.*`、`system.*` | **只读,永不弹窗** |
415
-
416
- 说得直白一些:`mouse.*` 与 `keyboard.*` 意味着一个被授权的 agent 能以你的权限操作**任何**你能操作的程序。又因为它能读屏,它还能观察自己刚刚做了什么。桌面之内没有沙箱。
417
-
418
- ### 授权实际上是怎么运作的
419
-
420
- **B 层——结构强制(始终生效,跨平台)。**
421
-
422
- * 每个变更类动词都要求 `--allow`。缺失即 `E_PERMISSION`,且**什么都不会发生**。
423
- * **`permission.json` 的三个写入动词存在,但它们只走委托,不走脚本自身的授权。** 动词表里声明了 `permission grant`、`permission revoke`、`permission acl.init`(三个都是 `W` 类),而 `permission query`、`permission list`、`permission audit` 是只读的。`grant` 与 `revoke` 的 flags 只接受 `delegate`(外加 `allow`),也就是**只能由同意写入委托调用**;脚本本身不能凭 `--allow` 创建持久授权,缺委托时报 `E_PERMISSION_DELEGATE_ONLY`。
424
- * 一条硬性的**受保护路径边界**拒绝任何对"决定本层能做什么"的文件的修改:`permission.json`(及 `.bak`)、`src/host/command.txt`、**四个** `src/host/bin/dsh-os.*`(`.ps1`、`dsh-os7.ps1`、`.sh`、`.bat`)、`src/host/spec/verbs.tsv` 与 `src/host/spec/charsets.tsv`,以及任何可能在其旁边暂存内容的同名临时文件。违反者报 `E_PERMISSION_PROTECTED_PATH`,且无任何副作用。读取仍然允许,以便你审阅规则。POSIX 侧的受保护清单有 8 项——同样的名单但**不含 `dsh-os7.ps1`**。
425
- * 该检查作用于**解析并归一化之后**的路径,逐段比较——而不是正则匹配,因为 `..`、8.3 短名或结尾的句点都能绕过正则。`file.move` 与 `file.copy` **两端都检查**,因为移动受保护文件同样是一种破坏方式。
426
-
427
- **A 层——内核 ACL(Windows,需显式开启,需提权)。**
428
-
429
- `permission acl init` 会给 `permission.json` 的 DACL 追加一条针对 `Everyone`(`S-1-1-0`)的显式 **DENY**,覆盖 `FILE_GENERIC_WRITE | DELETE | WRITE_DAC | WRITE_OWNER`。该动作幂等;无法执行时报 `E_ELEVATION_REQUIRED`,而不是含糊地失败。
430
-
431
- `selfcheck` 报告 `aclEnforced` 的方式是**读取文件真实的安全描述符**并遍历 DACL。早期版本是从一个状态文件里读标志位——那会声称一种并未生效的强制;该实现已被删除。`aclReason` 会解释 `false` 的原因,使其可被处置而不只是被疑惑。
432
-
433
- ### 四条需要如实说明的限制
434
-
435
- 这些在 `src/host/docs/permission-model.md` 中有记录,在此重述是因为它们改变了上述保证的含义。
436
-
437
- 1. **持久规则被记录了,但没有在动词层参与执法。** `permission.json` 的规则由同意写入委托及其 `os_permission_query` 工具来匹配;脚本读取该文件**仅用于展示**(`permission list/query/audit`)。没有任何动词在行动前查规则:真正的门禁是 `--allow`、受保护路径检查,以及(若已开启的)A 层。一条 `deny-always` 规则目前**并不能**拦住一个带着 `--allow` 到来的动词。
438
- 2. **`E_PERMISSION_DENIED_BY_RULE` 已登记但从未被抛出。** 它在错误表里并映射到退出码 3,但没有任何代码路径产生它——与第 1 条一致。
439
- 3. **脚本内的规则读取器没有实现 deny 优先。** 脚本侧的 `permission query` 取的是**第一条**匹配规则,不区分其决定是允许还是拒绝。委托侧正确实现了 deny 优先并有测试覆盖;因此两个读取器可能给出不同答案,而脚本侧是更弱的那个。
440
- 4. **`execute` 是绕过受保护路径边界的一条通道。** 该边界守卫的是 `file.*` 与 `environment.*`。它无法约束子进程:`execute.synchronous` 在 Windows 上把命令行交给 `cmd.exe /d /s /c`,在 POSIX 上交给 `sh -c`,因此被启动的程序可以写任何它自身权限允许的东西,包括 `permission.json`。只有 A 层能阻止这一点。当 `aclEnforced: false` 时,B 层做不到。
441
-
442
- 同意写入委托还有一个**设计缺口**需要直说:委托注册了五个工具(`os_permission_query`、`os_permission_grant`、`os_permission_revoke`、`os_permission_list`、`os_permission_audit`),但**没有注册 `approval/request` answerer**。它自己的设计文档写明应当注册,并且 `permission grant` **必须**经过用户确认的弹窗。就代码现状而言,强制手段是一段**工具描述**在告诉模型"只在用户选择之后才调用 `os_permission_grant`";代码并不校验弹窗是否真的发生过。请把该委托当作可信基础设施,而不是对 agent 的检查。
443
-
444
- 关于 A 层还有两点:它要求 harness 与委托以**不同身份**运行(否则退化为 B 层);其 POSIX 等价物更弱——一次改权限模式,同一用户即可撤销。`permission acl init` 在自己的返回结果里也这么说。
445
-
446
- ### 审阅已被授予了什么
447
-
448
- ```sh
449
- dsh-os permission list # 规则、默认值,以及【实测】的 aclEnforced
450
- dsh-os permission query file.delete "R:\project\**"
451
- dsh-os permission audit --limit 50 # 追加式尾部;委托裁剪到 200 条
452
- ```
453
-
454
- 若 `permission.json` 的 `updatedBy` 不是 `consent-delegate`,它会被视为**不可信**:规则被忽略,一切退回默认值,并报告原因。也就是说,手工编辑该文件塞进去的恶意规则**不会**生效——它会被忽略,而且你会被告知。
455
-
456
- 要**创建**持久决定,必须使用委托的工具。通配目标(`target.value === "*"`)会被拒绝,除非显式传入 `acknowledgeBroad: true`——因为"对一切永久同意"必须是一个独立的决定,而不能是某次点击的副作用。
457
-
458
- ---
459
-
460
- ## 实际可用范围
461
-
462
- 统计方式:抽出各平台的分发表,与声明的动词表逐一比对。未实现的动词**从不静默失败**:它会返回一个指名道姓的 `E_NOT_SUPPORTED` 信封。
463
-
464
- | 平台 | 已声明 | 已实现 |
465
- |---|---|---|
466
- | Windows PowerShell 5.1(`src/host/bin/dsh-os.ps1`) | 86 | **82** |
467
- | PowerShell 7.6(`src/host/bin/dsh-os7.ps1`) | 86 | **82** |
468
- | POSIX(`src/host/bin/dsh-os.sh`) | 86 | **61** |
469
-
470
- 两套 PowerShell 运行时的已实现集合**完全相同**(逐条比对两张分发表得出,不是目测)。
471
-
472
- ### Windows(5.1 与 7.6 相同)
473
-
474
- **已实现(82):** 全部 `file.*`(含 `file.encode`);`file verify.*`;`file.search`;五个 `execute` 动词(`command`、`synchronous`、`asynchronous`、`query`、`kill`);`system.*`;全部 `task.*`(含 `task.launch`);`service.list/query`;整个 `environment` 面(含 `attribute.*`);整个 `permission` 面(含 `grant`、`revoke`、`acl.init`);整个 `window` 面;`screenshot`;`clipboard.read/write`;`wait.window/ms`;整个 `mouse` 与 `keyboard` 面;`text`;`structure`;`selfcheck`。
475
-
476
- **未实现(4):** `clipboard.paste`、`service.start`、`service.stop`、`service.restart`。
477
-
478
- ### POSIX
479
-
480
- **已实现(61):** 除 `file.encode` 外的整个 `file` 面;`file verify.*`;`file.search`;五个 `execute` 动词(含 Windows 侧同样具备的 `execute.command`);`system.*`;`task.list`/`query`/`show`/`kill`(**不含 `task.launch`**);整个 `environment` 变量层(含持久写入)与 `attribute.*` 五个动词;整个 `permission` 面;`wait.ms`;整个 `mouse` 面(7 个)与 `keyboard` 面(6 个);`selfcheck`。
481
-
482
- 这 61 个里,**13 个需要 X display**(`mouse.*` 7 个 + `keyboard.*` 6 个),其余 48 个不需要。
483
-
484
- **未实现(25):** `file.encode`;全部 `window.*`(11 个);`screenshot`、`text`、`structure`;`clipboard.read`、`clipboard.write`、`clipboard.paste`;`wait.window`;`task.launch`;全部 `service.*`(5 个:`list`、`query`、`start`、`stop`、`restart`)。这些会经分发表的默认分支返回**带类型的** `E_NOT_SUPPORTED`,而不是静默缺失。
485
-
486
- > [!NOTE]
487
- > POSIX 没有持久化变量的内核 API,因此 `environment add/set/delete --scope user` 是通过一个**托管块**去编辑 `${HOME}/.profile` 的,块由 `# >>> dsh-os managed environment >>>` 与 `# <<< dsh-os managed environment <<<` 标记界定。每次改动都会重新生成标记之间的全部内容,因此这些操作是幂等的,用户也可以直接删掉整个块来一次性撤销。若直接往文件里写裸赋值,第二次调用就会重复追加,而且无法撤销。`--scope machine` 不可用,会报 `E_NOT_SUPPORTED`。
488
- >
489
- > `environment attribute add/set/delete` 则是**仅进程作用域**,持久作用域一律以 `E_NOT_SUPPORTED` 拒绝。这是刻意的:POSIX 没有列表型变量的内核 API,要持久化就得重写 shell 启动文件,而那里一旦重建错 `PATH`,整台机器的 shell 都可能不可用。
490
-
491
- > [!IMPORTANT]
492
- > **POSIX 只有 `mouse.*`(7 个)与 `keyboard.*`(6 个)需要 X display。** 没有 `DISPLAY` 时它们返回**带类型的** `E_NO_DISPLAY`;装了显示但没有 `xdotool` 时返回 `E_NOT_SUPPORTED` 并点名缺失的工具。
493
- >
494
- > **`wait.ms` 不需要**——它只是一次 sleep,因此本机可以完整验收。`environment attribute.*` 五个动词同样不需要(它们动的是环境变量,不是桌面)。
495
- >
496
- > 本机(无 `DISPLAY`、无 xdotool)的黄金测试因此把它们断言成 `E_NO_DISPLAY`:**这断言的是"缺失路径",不是"它们能用"。** 注入路径由 `tests/smoke-desktop-sh.sh` 用替身工具覆盖,见[测试](#测试)。
497
-
498
- > [!NOTE]
499
- > POSIX 上仍**结构性缺失**的是 `window.*`(11 个)、`screenshot`、`text`、`structure`、`clipboard.*`、`wait.window` 与全部 `service.*`。`native.sh`(515 字节)与 `service.sh`(333 字节)是刻意留空的桩,其文件头解释了原因:POSIX 上的对应物是外部程序(`wmctrl`、`import`、`xclip`、`scrot`),而不是可链接的库。
500
- >
501
- > **`window.sh` 不再是桩**(26 KB):它现在实现了 `wait.ms`、全部 `mouse.*` 与 `keyboard.*`,通过 `xdotool` 驱动 X11,并在动手之前先做显示与工具的能力检查。其余桌面动词仍从分发表的默认分支返回 `E_NOT_SUPPORTED`。
502
-
503
- ### 两套实现并不对等
504
-
505
- 同名动词、同一调用约定,但在若干处**行为不同**。重要的那些:
506
-
507
- | 方面 | Windows | POSIX |
508
- |---|---|---|
509
- | `execute.command` | 未实现 | 已实现(真 argv,不经 shell) |
510
- | `file.encode` | 已实现 | 未实现 |
511
- | `file.delete` 默认 | 进回收站 | 无 `trash-put` 则失败 |
512
- | `file.search` 字面匹配 | 大小写不敏感 | 大小写敏感 |
513
- | `file.view` 字符边界 | 强制校验(`E_BOUNDARY`) | **完全不校验** |
514
- | `file.view --max-bytes` | 生效 | 被忽略(`truncated` 恒为 `false`) |
515
- | 超过 32 MiB 的文件 | `E_TRUNCATED` | 无上限 |
516
- | 创建/追加时的 `--eol crlf` | 精确字节 | 会多出一个结尾 CRLF |
517
- | 追加时遇到混合行尾文件 | 原有行尾不动 | 整个文件被重新归一 |
518
- | 无法表示的字符 | 静默写成 `?` | `E_ENCODING_INVALID` |
519
- | `--pretty` / `--quiet` | 生效 | 接受但被忽略 |
520
- | `--` | `E_USAGE` | 选项结束标记 |
521
- | 受保护路径比较 | 大小写不敏感、已归一化 | 大小写敏感、按原参数 |
522
- | 未登记错误码 | 退出码 7 | 退出码 5 |
523
- | 失败时的 `meta.durationMs` | 真实耗时 | `0` |
524
-
525
- `file.search` 的 `--type` 在两套实现上都是一个潜在缺陷:实现会读它,但它并未登记为已知标志,因此行为取决于解析器的兜底路径。
526
-
527
- ---
528
-
529
- ## 安装现状与已知缺陷
530
-
531
- 直说,以免有人措手不及。
532
-
533
- ### bundle 的装载路径已由三个校验器覆盖
534
-
535
- 此处曾记录:loader 端到端路径**未**验证。该缺口**已补齐**,方式是三个校验器,且它们找出了一个真实缺陷:
536
-
537
- | 校验器 | 证明什么 | 用例 |
538
- |---|---|---|
539
- | `tools/verify-bundle.mjs` | loader 的**解析**契约:manifest 头、patch 文件、每一行都能从一个 profile 形态的 `node_modules` 解析、每个被解析模块都导出 loader 会调用的 Cordis 形状(`apply`/`inject`/`name`)、两行解析到**不同**模块(共享说明符会导致同一个工具注册两次并让整个入口中止)、以及版本闸门 | 10 |
540
- | `tools/verify-plugin-load.mjs` | 真实的 `apply()` 路径:宿主插件以**根类型为 `object`** 的参数 schema 注册 `dsh_os`,授权插件注册其五个工具,并且**注册后的 handler 真的 spawn 了脚本**并返回真实信封(`selfcheck` → ok,`verbCount` 86);不带 `allow` 的变更类动词以带类型的 `E_PERMISSION` 被拒,而不是抛异常。另含工作流一节:工具与提示词小节确实注册,且 `start → plan → choose → run` 经真实 handler 跑通、阶段门依然成立 | 30 |
541
- | `tools/verify-editions.mjs` | 版本选择:默认仍是 PowerShell 5.1;`powershellEdition: '7.6'` 与 `DSH_OS_POWERSHELL_EDITION` 选中 7.6 产物;`'auto'` 仅在产物与可用的 `pwsh` **同时**存在时才用 7.6;配置优先于环境变量;显式 `scriptPath` 完全禁用该选择。`-ExecutionPolicy` 只传给 5.1 可执行文件 | 14 |
542
-
543
- bundle 解析校验把包以 junction 链接进一个一次性的 profile 沙箱,因此解析行为与真实 profile 一致,而**活动 profile 从不被写入**。
544
-
545
- 它找出的缺陷是:`bin/dsh-os7.ps1` 此前**从插件侧不可达**——它存在、能构建、能测试,但 `resolveInvocation` 只找 `dsh-os.ps1`,于是全部 7.6 工作从插件角度看都是死代码。现已可选,且**默认不变**(仍为 5.1),因为一个已经工作的部署不应仅仅因为插件升级就改变行为。
546
-
547
- **仍未证明、也不主张:** harness **自身的 loader** 真的会在活动 profile 里组合这些 bundle。那需要把本包装进用户的桌面活动 profile 并重启 harness,这一步没有做。
548
-
549
- ### Windows 黄金测试的顺序依赖已修复
550
-
551
- 此处曾记录:在干净检出上实测 `passed: 47 failed: 1`,失败用例是 `parser/one-two-three-token-verbs`,因为它断言 `file length` 作用于一个由**更晚**的用例才创建的夹具目录。
552
-
553
- 该缺陷**已修复**:`tests/golden.ps1` 的解析器用例现在自己创建夹具(`$fixtureDir = Join-Path $sandbox 'parser\verify'`,随后 `New-Item` 与 `Set-Content`),再对其断言。当前实测为 **50 通过,0 失败**。
554
-
555
- ### `tests/smoke-desktop-sh.sh` 查出的两个 POSIX 真实缺陷
556
-
557
- 这两个都在**写完之后、review 看不出来**——因为本机没有 `DISPLAY`,它们平时只表现为"反正都是 `E_NO_DISPLAY`"。用替身工具把注入路径打开后才暴露:
558
-
559
- 1. **校验发生在注入之后。** `mouse click 10 20 --count abc` **返回了错误,但指针已经被移走**——一次"失败"的调用已经改变了机器状态。Windows 侧是**先校验全部参数再注入**,POSIX 侧现已对齐。(`mouse over abc 20` 同理。)
560
-
561
- 2. **`$(...)` 里的 `fail` 不会终止动词。** 坐标校验器调用 `fail`,但调用方写的是 `x="$(window_int_arg ...)"`,于是 `exit` **只结束了子 shell**:动词带着空值继续往下走并**照常注入**——`mouse over abc 20` **移动了指针并返回 `ok`**。现在三个会拒绝输入的校验器改为**赋值变量**(`WINDOW_INT`、`WINDOW_BUTTON`、`WINDOW_KEY`),并在**调用者自己的 shell** 中读取。
562
-
563
- **这个陷阱本仓库的 `environment` 模块早已在注释里记录过**(`env_resolve_scope` 专门讲过为什么它赋值而不打印),而它仍然在这里被重复了一次。这两个缺陷现在由 3 个断言固化:非法入参必须**被拒绝且未注入任何东西**。
564
-
565
- ### 其他缺口
566
-
567
- * **`tests/conformance.ps1` 曾经并不存在**,尽管 `spec/verbs.tsv` 的文件头声称它存在、并会校验与 `command.txt` 的一致性。**该文件现已创建**(见下文「两张表的一致性」),文件头的声明因此成为事实。一个"指名了守卫、而守卫并不存在"的注释比没有注释更糟,因为它会被当作保证来读。
568
- * **同意写入委托从未注册审批 answerer**,如前所述。
569
- * **委托在其 `query` 结果里无条件报告 `aclEnforced: false`**,而不是像脚本那样去实测 ACL。因此在 A 层已开启时,它会**低报**防护强度。
570
- * **此处曾记载「`--shell`、`--env`、`--stdin`、`--background` 被接受但从不被读取」,以及「Windows 上 `--encoding` 构造了从未被应用的值」——两处记载均已过时,且曾经是真实缺陷,现已修复:**
571
- * **Windows 侧**:`--shell`、`--env`、`--stdin`、`--background` 都确实被读取(`execute.ps1` 中有对应分支)。`--encoding` 曾经**真的是惰性的**——它构造了 `$enc` 却从未使用,`StandardOutputEncoding` 在全套模块中一次都没出现,因此子进程输出始终按运行时默认解码。现已接上输出/错误流编码。**实测**:让子进程输出 GBK 字节 `D6 D0 CE C4`,默认解码得到 `����`(4 个替换字符),`--encoding gbk` 得到 `中文`(2 个字符)。
572
- * **POSIX 侧**:这些标志**确实从未被读取**。现已改为**显式拒绝**——`--shell`、`--env`、`--stdin`、`--background`、`--encoding` 一律返回 `E_NOT_SUPPORTED`(退出码 4),而不是静默忽略。这正是本项目自己的契约所要求的:**不可用的能力必须是显式错误,绝不能是空成功**;一个传了 `--stdin` 却没被喂入任何输入、却收到成功信封的调用方,是被误导了。
573
- * **若干已声明的错误码确实不可达。** 在两侧实现中全文检索后确认,**零次出现**的有 `E_ENCODING_INVALID`(`file.encode`)、`E_IS_A_DIR`(`file.delete`)与 `E_ABORTED`。与此相对,`E_CHARSET_MISMATCH`(3 处)、`E_BINARY_CONTENT`、`E_TRUNCATED` 都有真实抛出点——因此这张表并非普遍不可信,但上述三个应视为尚未实现的意图,而不是可依赖的返回码。
574
- * **此处曾记载"`E_TIMEOUT` 从未被任何一侧抛出"——该记载已过时且错误。** 它在 `execute.ps1`(`execute command` 超时)与 `window.ps1`(`wait window` 超时)中都会被抛出。实测:
575
- `execute command cmd.exe /c ping -n 10 127.0.0.1 --timeout 300 --allow` 返回 `{"ok":false,...,"code":"E_TIMEOUT",...,"hint":"captured so far: stdout=86 bytes, stderr=0 bytes"}`,退出码 **6**。POSIX 侧同样有 `E_TIMEOUT`(`json.sh` 映射到退出码 6)。
576
- * **`src/client/` 是未改动的 Vite 脚手架。** 它没有与宿主接线,`index.html` 引用 `./src/main.ts` 而实际文件在 `./src/client/main.ts`,且 `node_modules` 为空——因此 `npm run build` 与 `npm run dev` 目前没有意义。
577
- * **`git` 不是必需依赖,但也并非每个目标环境都有**,因此没有任何验收步骤依赖它。
578
-
579
- ### 尚未证实的部分
580
-
581
- * DSH loader 会组合这个 bundle,并让 `dsh_os` 出现在活动会话中。
582
- * 两个平台之间的动词完全对等——两个方向都没有。
583
- * 在 harness 与委托共用同一身份的机器上 A 层的强制效果——按构造那种情况会退化为 B 层。
584
- * 关于 macOS 或非 Linux 的 POSIX,除代码审阅之外的一切:POSIX 测试套件是为 GNU 用户态写的,此处未运行。
585
-
586
- ---
587
-
588
- ## 测试
589
-
590
- ```console
591
- npm run test:all # 按顺序跑下面全部
592
- ```
593
-
594
- `test:all` 依次串联 `test:conformance`、`test:encoding`、`test`(Windows 黄金)、`test:plugin`、`test:takeover`、`test:permission`、`test:workflow`、`test:bundle`(`tools/verify-bundle.mjs`)、`test:package`(`tools/verify-package.mjs`)、`test:load`(先 `tools/verify-plugin-load.mjs`,再 `tools/verify-editions.mjs`)与 `test:desktop`(`bash tests/smoke-desktop-sh.sh`)。
595
-
596
- | 套件 | 命令 | 用例数 | 实测结果 |
597
- |---|---|---|---|
598
- | 两表一致性 | `powershell -File tests/conformance.ps1` | 5 | **5 通过,0 失败** |
599
- | 编码契约 | `powershell -File tests/encoding.ps1` | 32 个构建模块 | **全部契约成立** |
600
- | Windows 黄金 | `powershell -File tests/golden.ps1` | 50 | **50 通过,0 失败** |
601
- | POSIX 黄金 | `bash tests/golden-sh.sh` | 116 | **116 通过,0 失败** |
602
- | POSIX 桌面注入 | `bash tests/smoke-desktop-sh.sh` | 52 | **52 通过,0 失败** |
603
- | 插件 | `node tests/plugin.test.mjs` | 13 | **13 通过,0 失败** |
604
- | 接管拦截 | `node tests/takeover.test.mjs` | 41 | **41 通过,0 失败** |
605
- | 授权委托 | `node tests/permission.test.mjs` | 18 | **18 通过,0 失败** |
606
- | 工作流 | `node tests/workflow.test.mjs` | 31 | **31 通过,0 失败** |
607
- | bundle 解析 | `node tools/verify-bundle.mjs` | 16 | **16 通过,0 失败** |
608
- | 打包完整性 | `node tools/verify-package.mjs` | 21 | **21 通过,0 失败** |
609
- | 插件装载 | `node tools/verify-plugin-load.mjs` | 30 | **30 通过,0 失败** |
610
- | 版本选择 | `node tools/verify-editions.mjs` | 14 | **14 通过,0 失败** |
611
- | 构建漂移 | `powershell -File tools/build.ps1 -Check` | 3 个生成物 | **无漂移** |
612
- | 前缀缓存(`npm run measure:cache`) | `node tools/measure-cache.mjs` | 2 项断言 | **PASS——四次工作流调用均值命中 96.6%** |
613
-
614
- `measure:cache` **不在** `test:all` 里:它直连真实 provider,因此需要 `DEEPSEEK_API_KEY`(从 `$DSH_HOME/.credentials.yaml` 读取)并会消耗 token。只有当某项被测量的断言不成立时它才以非零码退出。
615
-
616
- > [!NOTE]
617
- > **`test:desktop` 需要 `bash`,而本机没有。** 因此在没有 Git Bash 或 WSL 的 Windows 主机上,`npm run test:all` 会以 `'bash' is not recognized` 结束。这是运行环境缺口而非断言失败:同一次调用中位于它之前的所有套件都跑过并通过。POSIX 套件在这里依旧未被验证,原因与过去相同——它们需要一台 POSIX 主机。
618
-
619
- ### 两张表的一致性
620
-
621
- `spec/verbs.tsv` 的文件头一直声称「`tests/conformance.ps1` 会校验它与 `command.txt` 一致」,而那个文件**并不存在**——**现已补上**,声明因此成为事实。该守卫按「**最长的、能在词表中命中某个动词的词前缀**」把 `command.txt` 的每一行解析成动词(所以 `file verify charset path charset` 解析为 `file.verify.charset`,而不会误认成不存在的 `file.verify`),然后校验:两表动词集合互相包含、数量相等、R/W 类别一致、位置参数名按序一致。
622
-
623
- **写它的过程中查出 5 处真实不一致**,除一处外都在 `command.txt`(它是权威契约):
624
-
625
- | 动词 | 原声明 | 按实现应有的样子 |
626
- |---|---|---|
627
- | `file.append` | `path content charset` | charset 是**可选**的第三个位置参数,原写法让人以为必需 |
628
- | `file.insert` | `path content charset offset` | charset 走 `--charset`,**刻意不是**位置参数——若它是位置参数,省略它会让 `offset` 静默移位 |
629
- | `wait.ms` | 无位置参数 | 接受 `ms` |
630
- | `keyboard.combination` | 两行 `... down a b c d` / `... up a b c d` | 是**一个**动词,含 `phase` 位置参数;两行写法读起来像两个动词 |
631
- | `execute.command` | tsv 写作 `command\|arguments` | 整个位置参数列表就是 argv,不存在单独的 `command` |
632
-
633
- 其中四处改在 `command.txt`,`verbs.tsv` 的 `execute.command` 行改为 `arguments...`。**运行面没有任何改变**——变的只是两份文档的说法,而这正是这种一致性守卫存在的意义。
634
-
635
- `tests/encoding.ps1` 守卫的是让其余一切有意义的那些契约:`.ps1` 生成物带 BOM、`.sh` 生成物不带、行尾遵循 `.gitattributes`、`command.txt` 与 `.tsv` 表保持纯 ASCII + LF,以及 **stdout 永不携带 BOM**。
636
-
637
- **此前记载的「4 项编码违规」现已消失,但这不是"放宽了守卫"。** 规则本身从未松动,改的是它的**适用范围**:原先它要求 `src/host/lib/` 下**每个** `*.ps1` 都不带 BOM 且纯 ASCII,而其中两个 `git.ps1` 是**独立工具**——不会被拼进任何生成物,且必须带 BOM 才能在 Windows PowerShell 5.1 下正确解析其中文提示字面量。对"构建从不读取的文件"套用该规则属于**误报**。现在守卫只校验 `tools/build.ps1` 实际拼接的 **32 个模块**,并**打印出它跳过了哪些文件**,使范围的收窄是可见的而不是静默的。
638
-
639
- > [!NOTE]
640
- > **POSIX 桌面动词的「116 通过」与「52 通过」都不是「驱动真实窗口管理器」的证据。** 两套测试从两个方向覆盖同一批动词,而本机没有 X server:
641
- > * `tests/golden-sh.sh` 断言**缺失路径**——没有 `DISPLAY` 时,每个桌面动词都必须返回带类型的 `E_NO_DISPLAY`,而不是空成功。
642
- > * `tests/smoke-desktop-sh.sh` 断言**注入路径**——它把会记录调用的 `tests/fake-xdotool` 放进 `PATH` 并设置 `DISPLAY`,于是动词走真实代码路径,断言落在**交给工具的确切参数向量**上:`--button right` 是否变成按键 `3`、`Enter` 是否变成 keysym `Return`、表外的 `XF86AudioMute` 是否被原样送达、`--clearmodifiers` 与 `--repeat` 是否在正确位置、以及 xdotool 失败是否报成 `E_IO` 而不是假成功。
643
- >
644
- > **正是这个测试台查出了 POSIX 实现里的两个真实缺陷**(见上文「实际可用范围」与「已知缺陷」)。没有任何一套证明它们真的操作了桌面。
645
-
646
- > [!WARNING]
647
- > **两套黄金测试必须串行运行。** 两套实现都会把失败信封暂存到一个**固定路径**的临时文件——`.temporary/dsh-os.err`,既无 PID 也无锁——因此并发调用会互相覆盖对方待取的结果。实测并行运行 `golden.ps1` 与 `golden-sh.sh` 时报出了 2 个**假**失败:退出码与信封自身的 `error.code` 相互矛盾;改为串行后二者全部消失。请把它当作缺陷而不是随机抖动:该暂存路径在构造上就是竞态的。
648
-
649
- ---
650
-
651
- ## 参与贡献
652
-
653
- `src/host/lib/powershell5.1/*`、`src/host/lib/powershell7.6/*` 与 `src/host/lib/*.sh` 是真源。**绝不要手改 `src/host/bin/dsh-os.ps1`、`dsh-os7.ps1` 或 `.sh`**——它们是生成物。请改模块,然后:
654
-
655
- ```console
656
- powershell -File tools/build.ps1 # 重新生成三个脚本
657
- powershell -File tools/build.ps1 -Check # 已提交脚本一旦漂移即失败
658
- ```
659
-
660
- 三个生成物是分开的而不是"一份脚本加运行时分支",因为两个运行时差异无法用分支干净地抹平:.NET Framework 没有 `ProcessStartInfo.ArgumentList`,而 .NET 10 没有理由继续背负这套项目为绕开它而手写的 Windows 命令行转义。
661
-
662
- `git.ps1` / `git.sh` **不在** `tools/build.ps1` 的模块清单内,因此它们不被拼进任何生成物;它们是各自独立、自包含的交互式工具,详见上文"独立工具"。
663
-
664
- `.gitattributes` 是契约的一部分而不是偏好:`command.txt` 与 `.tsv` 表必须是 LF,两套实现才能解析逐字节一致的输入;`dsh-os.bat` 必须是 CRLF 且仅 ASCII,否则 `cmd.exe` 会解析错误;`dsh-os.ps1` 必须是 CRLF 且 UTF-8 **带** BOM,否则 PowerShell 5.1 会按 ANSI 码页读取其中的非 ASCII 字面量。
665
-
666
- 两条在这个仓库里踩出来的教训,值得一并继承:
667
-
668
- * **依赖路径推导的安全边界必须 fail-closed。** 一次重构曾让仓库根多算上一层,于是没有任何受保护路径匹配。其失败形态不是报错,而是**保护静默失效**——受保护的文件真的被删掉了。
669
- * **临时文件统一放在 `.temporary/` 下**,黄金测试夹具也在那里。
670
-
671
- 各模块文件头仍引用已不存在的 `tools/split-once.ps1`;当前的拼装器是 `tools/build.ps1`。
672
-
673
- ---
674
-
675
- ## 独立工具
676
-
677
- `src/host/lib/git.sh`、`src/host/lib/powershell5.1/git.ps1` 与 `src/host/lib/powershell7.6/git.ps1` 是交互式 git 运行器。它们**刻意不属于动词面**:不在 `tools/build.ps1` 的 `$modulesPs1`/`$modulesSh` 清单里,因此永远不会被拼进 `bin/dsh-os.ps1`、`bin/dsh-os7.ps1` 或 `bin/dsh-os.sh`,也不输出 JSON 信封。三者各自是单个自包含文件、彼此字节独立,靠人工评审保持一致而不是靠生成器。
678
-
679
- 它们按三阶段定位 git:`GIT_EXE`/`GIT_HOME`/`PATH` → 已知安装位置(含 Git for Windows 注册表键)→ 对每个挂载点做**有深度与时限约束**的全盘遍历;全都找不到时列出搜索过的位置并以 127 退出。参数以独立 argv 元素送达 git,绝不拼接 shell 字符串。`git add` 的输入为空(直接回车)即等价于 `git add .`。三者的编码各自服从其运行时的契约——这正是 `git.sh` 纯 ASCII(英文提示)而两个 `git.ps1` 不是的原因。
680
-
681
- ---
682
-
683
- ## 许可
684
-
685
- **MIT**,在 `package.json` 中声明(`"license": "MIT"`,作者 Eloy Kim),并以独立的 [`LICENSE`](LICENSE) 文件随仓库分发。完全开源:整个动词面、规范文档与测试套件都在仓库里。无遥测、无需账号、运行时无网络访问。
686
-
687
- ```
688
- MIT License
689
-
690
- Copyright (c) 2026 Eloy Kim
691
- ```
692
-
693
- 许可证文件为纯 ASCII、LF 行尾、无 BOM,与仓库其余的编码契约保持一致。需要更正一处旧描述:它**并未**列入 `package.json` 的 `files` 白名单(该白名单共 13 项,不含 `LICENSE`,也不含 `README.zh-CN.md` 与 `dsh-os7.ps1`),因此它留在检出目录里,但不会随发布的包一起分发。
694
-
695
- 动词契约与规范文档属于源码的一部分:行为变更应当同步反映到 `src/host/spec/verbs.tsv`、`src/host/command.txt` 与 `src/host/docs/os-spec.md`,而不只是改代码。
696
-
697
- 另需说明:规范文档(`src/host/docs/os-spec.md`、`src/host/docs/permission-model.md`、`src/host/command.txt`)以**中文**撰写,而本 README 与动词表为英文。
1
+ # deepseek-coordination
2
+
3
+ [English](README.md) · **简体中文** · [更新日志](CHANGELOG.zh-CN.md) · [English changelog](CHANGELOG.md)
4
+
5
+ 一个面向 DeepSeek Harness 的双端 Cordis 插件:宿主端持有协调 SQLite 数据库、7 个模型可见工具和一组 HTTP 路由;浏览器端在输入框挂载五个下拉框,并提供设置目录与服务级接管对话框。
6
+
7
+ **当前版本:v0.10.0(2026-10-18)。** 它在 v0.9.2 的 tooltip 修复之后,加入设置目录、持久文件层、Turn 流水线、阶段冻结和数据结构采用。包版本、更新日志和发布 tarball 来自同一棵树;下文按源码核对,不以设计文档代替实现。
8
+
9
+ | 状态 | 含义 |
10
+ |---|---|
11
+ | **已接线** | 由 `src/index.ts` 注册,或被已组合的 service、路由、工具实际调用。 |
12
+ | **仅库** | 模块已实现并有测试,但当前 Turn 生命周期不驱动它,也没有路由/工具暴露。 |
13
+ | **设计/计划** | 只写在文档、配置或 schema 中,尚无运行时消费者。 |
14
+
15
+ ## 这是什么
16
+
17
+ | 组成 | 位置 | 作用 |
18
+ |---|---|---|
19
+ | Bundle | `cordis.patch.yml`、`package.json` 的 `dsh.bundle` | 把一行 `deepseek-coordination` 插入 DSH profile 的 bundle 列表。 |
20
+ | 宿主端 | `lib/index.mjs`(ESM,Node) | `CoordinationService`、`coordination` SQLite 库、7 个工具、14 条路由路径和 agent 钩子。 |
21
+ | 浏览器端 | `lib/client.js`(CJS,浏览器,`__ModuleLoader__` 包装) | React 18 + Material UI 9:输入框下拉框、设置页、接管对话框。 |
22
+ | 数据库 | 默认 `%DSH_HOME%\coordination\coordination.db` | 插件自有 SQLite 文件;63 张表、7 个触发器、63 个索引,schema 版本 24。从不打开 DSH 的 `harness` 库。 |
23
+
24
+ 插件不替代 DSH 的 agent 循环。它记录协调状态、为每个任务锁定一个职能、提供输入框选择与文件元数据、在上下文压力越过阈值时请求 DSH 压缩,并把审批/提问转给人工对话框。
25
+
26
+ 五个输入框控件是 **权限、职能、交互、情景、验证**。命令面(`/goal`、`/plan`、`/feedback`、`/compact`、`/permission`)是消息前缀,不占第六个下拉位。
27
+
28
+ ## 安装与构建
29
+
30
+ | 要求 | 说明 |
31
+ |---|---|
32
+ | Node.js | `package.json` 声明 `>=18`,但宿主端使用 `node:sqlite`,必须使用带该模块的 Node 构建。本次验证使用 Node 24.21.0。 |
33
+ | npm | npm 11(随 Node 24 提供)按 `package-lock.json` 安装依赖,并驱动 scripts 与发布工作流。 |
34
+ | DSH | 装有 `dsh` CLI 的 DSH,且 profile 含 `@deepseek-ai/dsh-base` 与 `@deepseek-ai/dsh-web-app`。 |
35
+ | Peer 包 | 见下表;它们由 DSH 安装提供,不由本包携带。 |
36
+
37
+ ```powershell
38
+ cd deepseek-coordination
39
+ npm ci # 全新 checkout 也可用 `npm install`
40
+ npm run typecheck # tsc -p tsconfig.json --noEmit
41
+ npm test # tsdown 测试构建 + node --test
42
+ npm run build # 生成 lib/index.mjs、lib/index.d.mts、lib/client.js
43
+ npm pack # 先执行 prepack,生成可安装 tarball
44
+ ```
45
+
46
+ `npm run test:tsc` 检查测试树;`npm run pack:check` 校验 tarball 中的运行时文件并检查宿主入口语法。仓库提交了 `package-lock.json`,`npm ci` 会精确复现它。
47
+
48
+ | Peer 包 | 范围 | 端 |
49
+ |---|---|---|
50
+ | `@deepseek-ai/cordis` | `^4.0.1` | 宿主运行时 |
51
+ | `@deepseek-ai/dsh-host-webserver` | `^0.1.0-rc.7 \|\| ^0.2.0-rc.0` | 宿主路由 |
52
+ | `@deepseek-ai/dsh-client-runtime`、`-locale`、`-ui-slots`、`-ui-settings` | `^0.1.0-rc.7 \|\| ^0.2.0-rc.0` | 浏览器模块表 |
53
+ | `react` | `^18.2.0` | 浏览器模块表;不会被打包 |
54
+
55
+ ### 安装到 profile
56
+
57
+ 安装打包后的 tarball,不要装 GitHub 源码:
58
+
59
+ ```powershell
60
+ npm pack
61
+ dsh plugin --profile <name> add .\deepseek-coordination-0.10.0.tgz
62
+ dsh --profile <name> --dump-config # 应出现 deepseek-coordination 层
63
+ ```
64
+
65
+ **不支持从 `github:eloyhere/deepseek-coordination` 直接安装。** `lib/` 被 gitignore,而 bundle 只在 `prepack`(或显式 `npm run build`)时构建,npm 安装 git 依赖时不会执行这一步,安装后的包在激活时会因找不到 `lib/index.mjs` 报 `ERR_MODULE_NOT_FOUND`。打 tag 时触发的 GitHub Actions(`.github/workflows/release.yml`)会构建并把校验过的 tarball 附到 Release,请安装该产物。
66
+
67
+ **npm 上的 latest 仍是旧的 `0.1.9` dsh-os 线。** 目前不要按包名安装。本地 checkout 在 `npm run build` 之后可以用 `dsh plugin --profile <name> add .` 链接。
68
+
69
+ `dsh plugin add` 会写入依赖和 bundle 行。如果 profile 里仍记录着别的包 spec,请替换它,不要假定新代码已经加载;用 `dsh --profile <name> --dump-config` 和已安装的 `package.json` 版本核对。
70
+
71
+ | 产物 | 格式 | 本次构建大小 |
72
+ |---|---|---|
73
+ | `lib/index.mjs` | 宿主 ESM,`platform: node` | 940,726 B |
74
+ | `lib/index.d.mts` | 宿主类型声明 | 9,095 B |
75
+ | `lib/client.js` | 浏览器 CJS,包在 `__ModuleLoader__.load()` 中 | 1,170,887 B |
76
+
77
+ Material UI、emotion 与 `react-dom` 被打进 `lib/client.js`,React 本身来自宿主模块表。内联的 `react-dom` 是 18.3.1 线,与宿主的 React 18 匹配——npm 不会为 React 18 的 `react` 自动配一个 React 19 的 `react-dom`。客户端体积大是因为内联了 MUI 与 `react-dom`,不是因为多带了一份 React。
78
+
79
+ ## 架构
80
+
81
+ 宿主端(`src/index.ts`):
82
+
83
+ - 导出 `name`、`inject = ['webServer', 'tools']` 和 `Config`;`apply()` 解析配置、创建 service、恢复被中断的 Turn,并通过 `ctx.effect` 安装所有 effect。
84
+ - `CoordinationService`(`src/host/coordination.ts`)是 Cordis 的 `coordination` service,持有数据库、module host 和已组合模块:session、selection、turn、profession、plan、goal、compact、directive、permission、takeover/gate、provider、cache、command。
85
+ - SQLite 使用 `node:sqlite` 的 `DatabaseSync`,开启外键、5 秒 busy timeout,非 `:memory:` 时使用 WAL。触发器拒绝修改已锁定职能,`orchestration_trace` 只允许追加。
86
+ - `/deepseek-coordination` 前缀路由返回 JSON `{ ok, value | error }`;请求体上限 1 MiB;未知路径返回 404 路由信封。
87
+ - 7 个工具通过 `ctx.tools` 注册;三个输入框工具在 `POST /locale` 后重新注册,因为工具描述属于模型提示词。
88
+ - 钩子:`agent/pre-step` 打开协调 Turn 并运行压缩治理器;`agent/turn-stopping` 以 `done` 关闭;`agent/error` 以失败关闭;`approval/request` 与 `user-questions/request` 进入接管闸门。60 秒定时器清理过期 lock 行。
89
+
90
+ 浏览器端(`src/client/index.ts`):
91
+
92
+ - `inject = ['slots', 'locale']`;`package.json` 的 `dsh.client.inject` 另行列出四个模块表包。
93
+ - `settings.section`(order 50)挂载 `CoordinationSettings`;`conversation.input.left`(order 40)挂载输入框控件,接管对话框作为兄弟节点。
94
+ - 语言跟随 `ctx.locale.getLocale().active`,把 `zh*`/`en*` 归一为 `zh-CN`/`en-US`,并上报 `POST /locale`;宿主据此重建本地化工具描述。
95
+ - 客户端 bridge 使用同源 `fetch`:选择接口 3 秒超时,接管/语言接口 2 秒超时。宿主不可达时,五个选择回退到每会话的 `localStorage`;权限预设永不离线写入。
96
+ - 输入框组件只按 `sessionId` memo;slot 每次击键也会传 `input`,若按默认浅比较重渲染,emotion 会触碰 `<head>`,导致宿主的弹层关闭。
97
+
98
+ ```text
99
+ deepseek-coordination/
100
+ ├─ src/
101
+ │ ├─ index.ts # 宿主组合根:service、路由、工具、钩子
102
+ │ ├─ host/
103
+ │ │ ├─ coordination.ts # CoordinationService(Cordis service "coordination")
104
+ │ │ ├─ config.ts # Schemastery schema + resolveConfig()
105
+ │ │ ├─ database.ts # SQLite schema(24),63 表,触发器
106
+ │ │ ├─ dsh.ts # 窄 DSH port + Turn 生命周期
107
+ │ │ ├─ tools.ts # 7 个模型可见工具
108
+ │ │ ├─ chain.ts # 有界 Chain 投影引擎
109
+ │ │ ├─ node-registry.ts # NodeCard 注册表与 describe()
110
+ │ │ ├─ parse-service.ts # 工作区内的解析与读取服务
111
+ │ │ ├─ token.ts # TokenGovernor 与纯策略
112
+ │ │ ├─ takeover-gate.ts # 审批/提问瀑布闸门
113
+ │ │ ├─ selection.ts turn.ts profession.ts … # 选择、锁定、生命周期
114
+ │ │ ├─ bayesian.ts comprehension.ts organization.ts … # 库级引擎
115
+ │ │ └─ text/ # P0 解析器:JS/TS、CSS/HTML/MD、TeX、公式、依赖图
116
+ │ ├─ client/
117
+ │ │ ├─ index.ts # slots、语言 store、导航图标、接管 CSS
118
+ │ │ ├─ controls.ts # 五个输入框下拉框、tooltip、离线重放
119
+ │ │ ├─ settings.ts # 13 组、151 控件的设置目录页
120
+ │ │ ├─ takeover.ts # 审批/提问对话框
121
+ │ │ ├─ bridge.ts store.ts i18n.ts theme.ts selection-cache.ts
122
+ │ └─ shared/ # entity/enum/selection/i18n/error + 数据结构
123
+ ├─ test/ # 24 个 *.test.ts + helpers.ts
124
+ ├─ lib/ # 构建产物;gitignore,不手改
125
+ ├─ cordis.patch.yml # 插入 profile 的 bundle 行
126
+ ├─ scripts/check-pack.mjs # tarball 运行时文件校验
127
+ └─ .github/workflows/release.yml
128
+ ```
129
+
130
+ ## 输入框控件
131
+
132
+ | 控件 | 选项 | 行为 |
133
+ |---|---|---|
134
+ | 权限 | DSH 预设目录,或中性的“跟随 DSH” | 通过 `ctx.permissionPresets` 立即写入 DSH,不写入协调库。仅在本机“权限接管”开关打开时显示。 |
135
+ | 职能 | 决策 / 成果 / 资源 / 人员 / 任职 / 执行 | 活动任务内锁定;选择其他值会返回 `PROFESSION_LOCKED`,并打开重建/丢弃对话框。 |
136
+ | 交互 | 感情 / 思维 / 启发 / 收束 / 统筹 | 写入 `selection` 行,供下一 Turn 使用;活动 Turn 有未清除的目标时冻结。 |
137
+ | 情景 | 对话 / 写作 / 办公 / 研究 / 工程 / 迭代 | 写入下一 Turn;`writing` 位于“对话”和“办公”之间;情景策略由 Turn 流水线读取。 |
138
+ | 验证 | low / medium / high | 写入下一 Turn;活动 Turn 有 draft/scored/active 计划时冻结;三档都会检索。 |
139
+
140
+ - 每个控件和每个选项都有 tooltip。`tooltipVisibility` 单所有权 store 保证同一时刻只有一个输入框 tooltip 可见;位置在左/右,退出为 160 ms 淡出,淡出中的 popper 不接收指针事件。
141
+ - 下拉框第一次点击就打开菜单;pointerdown/mousedown 不改变状态,因此不会吞掉 Select 的点击。
142
+ - 锁定的职能触发器被禁用,其无障碍名称带锁定解释;选项 `MenuItem` 带标签和提示,键盘与读屏用户无需悬停即可获得说明。
143
+ - 阶段冻结每轮从主机重新推导:Turn → 职能,Plan → 验证,Goal → 交互。下拉框禁用但仍可聚焦,aria-label/tooltip 说明恢复时机;服务端对冻结字段的改值返回 `INVALID_STATE`(重复写原值允许)。
144
+ - tooltip 受宿主 `ui.showTooltip` 总开关约束;本机“显示提示”只能收窄,不能恢复宿主已关闭的 tooltip。
145
+ - 保存是乐观的,并做 120 ms 防抖。保存被拒绝时回退到最近一次宿主确认的状态;离线编辑在宿主恢复后重放。完全权限预设需要第二次显式确认。
146
+ - 宿主状态加载中时下拉框保持禁用并显示 loading;权限目录缺失就显示缺失,不编造预设。
147
+
148
+ 验证预算由 Turn 流水线实际读取:
149
+
150
+ | 档位 | 检索预算(次数 × 条数) | 证据范围 | 交互范围 |
151
+ |---|---|---|---|
152
+ | low | 1 × 3 | 仅当前 Turn | 当前选择的交互 |
153
+ | medium | 2 × 5 | 当前会话 | 当前选择的交互 |
154
+ | high | 4 × 8 | 本地全部内容 | 全部交互 |
155
+
156
+ 预算定义在 `src/host/comprehension.ts` 和共享的 `ValidationBudgetByLevel` 表。`command=none` 的 Turn 现在由 `CoordinationService` 驱动 Comprehension → Organization → Plan/score;检索仍受 `allowWeb` 和缺失 web 端口约束。
157
+
158
+ ### 情景与写作档
159
+
160
+ | 情景 | 思考 / effort | 计划 | 大纲 | 修订 | 评分 | Bash | 联网 | 最大轮次 |
161
+ |---|---|---|---|---|---|---|---|---|
162
+ | 对话 | 关闭 / medium | – | – | – | – | 否 | 是 | 1 |
163
+ | 写作 | 开启 / high | 是 | 是 | 是 | 是 | 否 | 是 | 3 |
164
+ | 办公 | 关闭 / medium | 是 | – | – | – | 否 | 是 | 1 |
165
+ | 研究 | 开启 / high | 是 | – | – | – | 否 | 是 | 3 |
166
+ | 工程 | 开启 / high | 是 | – | – | – | 是 | 是 | 3 |
167
+ | 迭代 | 开启 / high | – | – | – | 是 | 否 | 是 | 3 |
168
+
169
+ “写作”要求大纲和至少一轮修订,必须有质量评分,不允许 bash,允许联网,最多三轮。通过阈值为 `0.6`,`plan.ts` 使用该写作阈值。Turn 流水线读取 reasoning effort、require-* 标志、引用/证据、`allowWeb` 与 `sandboxOnly`;`thinking`、`allowBash`、`maxIteration` 和非写作阈值仍是没有 DSH 端口/消费者的延迟诊断项。
170
+
171
+ ## 职能锁定与 Turn 生命周期
172
+
173
+ - 一个任务只有一个职能。`turn.create` 写入 `profession NOT NULL`、`locked = 1`,并写一条职能记录和一条 `lock` 追踪事件。
174
+ - 只要存在未终结 Turn,`SelectionModule.set` 就以 `ProfessionLockedError` 拒绝改职能;HTTP 层返回 409 `PROFESSION_LOCKED`,选择不会被修改。
175
+ - `POST /profession/rebuild` 把活动任务以 `abandoned` 结束,创建带新序号、新职能的替代 Turn,复制 prompt/command,并写 `rebuild` + `lock` 追踪事件。它要求存在活动 Turn。
176
+ - `POST /profession/abandon` 结束活动任务但不创建替代;下一个任务可重新选择。两个重选开关都由 `resolveConfig` 强制为 true。
177
+ - SQLite 再兜一层:`trg_turn_profession_lock`、`trg_profession_record_lock`、`trg_subtask_profession_lock` 在锁定时拒绝改职能;`trg_turn_profession_unlock`、`trg_subtask_profession_unlock` 拒绝清除锁定标记。
178
+
179
+ | 钩子 | 作用 |
180
+ |---|---|
181
+ | `agent/pre-step` | 确保协调 session,打开(或复用)当前 DSH Turn 对应的协调 Turn;同时在 `next()` 前运行 Token 治理器。 |
182
+ | `agent/turn-stopping` | 以 `done` 关闭 Turn,下一 Turn 解锁职能。 |
183
+ | `agent/error` | 以 `failed` 关闭 Turn。 |
184
+ | 宿主启动 | `recoverInterrupted()` 把所有未终结 Turn 标为 failed,原因 `turn was interrupted by a host restart`。 |
185
+ | 每 60 秒 | 删除超过 `defaultLease` 的已结束 lock 行。 |
186
+
187
+ `done`、`failed`、`cancelled`、`abandoned` 是不可逆终态。Turn 钩子只记录 Turn 元数据和一条空 prompt 行,不把 DSH 对话镜像进协调库。
188
+
189
+ ## 设置与配置
190
+
191
+ 设置页由 `src/shared/selection.ts` 的 `SETTINGS_CONTROLS` 生成:**13 组、151 个控件、134 个可写宿主行,覆盖 155 个非秘密配置叶子**。18 个顶层配置键包含 `database` 与 `databasePath`,这两个键故意不投影、不可写。
192
+
193
+ | 组 | 默认值 / 范围 | 写入路径 |
194
+ |---|---|---|
195
+ | `session` | `maxTurnPerSession=1000`、`defaultLease=30000 ms`、`clarifyTimeout=30000 ms` | 宿主行,经 `/config` |
196
+ | `profession` | enabled/lock/rebuild/abandon 强制 `true`;default 镜像 `defaults.profession`;audit/errorHint `true` | 强制项只读;audit/errorHint 为宿主行(audit 无消费者) |
197
+ | `plan` | threshold `0.5`、maxRebuild `3`、8 个权重和为 1、8 个维度阈值 | 宿主行;保存后权重重新归一 |
198
+ | `orchestration` | maxReject `2`、maxSubTask `32`、groundTruth `true`、timeout `5000 ms`、shell ground truth `false`、modelRouting `true` | 宿主行;`groundTruthTimeout` 无消费者 |
199
+ | `cache` | enabled `true`、ttl `60000 ms`、fingerprintWindow `50`、effortWarning `true` | 宿主行;`fingerprintWindow` 无消费者 |
200
+ | `bayesian` | prior `0.5`、stopEntropy `0.05`、infoGain `0.01`、maxRoutes `12`、calibrationFloor `0.1` | 宿主行;`calibrationFloor` 无消费者 |
201
+ | `comprehension` | stageTimeout `30000 ms`、searchEnabled `true`、maxEvidence `8`、highHistoryWindow `50` | 宿主行 |
202
+ | `compaction` | auto `true`、soft `0.70`、hard `0.88`、reserve `2048`、recentTurns `6`、cooldown `30000 ms`、maxPerSession `20` | auto/soft/hard 为宿主行;结构字段只读 |
203
+ | `ui` | toolbar `true`(废弃别名)、showTooltip `true`、takeover `false`、refreshInterval `5000 ms`、compact `true`、locale `auto` | 宿主行 + 两个本机开关;toolbar/locale 只读 |
204
+ | `scenes` | 六个情景各 13 个字段;写作阈值 `0.6` | 宿主行;只有写作评分阈值被 `plan.ts` 读取 |
205
+ | `parser` | enabled `true`、1 MiB、200k tokens、depth `256`、2 s、cache `256`、graph 20k/100k | enabled + 四个上限为宿主行;depth 与 graph 上限仍是计划 |
206
+ | `chain` | depth `8`、links `50`、tokenBudget `1500` | 宿主行 |
207
+ | `tools` | maxResultBytes `0` | 计划/只读;未被消费 |
208
+ | `defaults` | 职能 `execution`、交互 `think`、情景 `dialogue`、验证 `medium`、命令 `none` | 宿主行 |
209
+
210
+ `GET /deepseek-coordination/config` 返回 `{ values, defaults }`;`defaults` 来自 `resolveConfig(undefined)`,页面不复制默认字面量。`POST /config` 接收扁平点路径补丁(如 `{ "plan.threshold": 0.7 }`),按目录和真实 Schemastery schema 校验,写入活动配置,重新执行不变量(权重归一、别名镜像、强制开关、压缩阈值夹取),在压缩变化时重建 `TokenGovernor`,最后尽力通过 `ctx.settings.update` 持久化。
211
+
212
+ 持久化是显式的:只有 DSH 接受设置写入时 `persisted` 才为 true;否则修改只在当前进程生效,页面会如实说明。未知键、`databasePath`、`database` 会以 `INVALID_ARGUMENT` 拒绝;已知但不可写的键进入 `ignored`,绝不写入。`POST /config/compaction` 保留为旧的 auto/soft/hard 兼容小路由。
213
+
214
+ 设置页整体使用 Material UI:每组一个默认展开的 `Accordion`,行放在 `List` 中;六个情景各一张 `Table`(页面没有情景下拉框);布尔用 `Checkbox`,枚举用 `RadioGroup`,比例型数字用 `Slider`,计数/字节/毫秒用 `TextField type="number"`;`Chip` 只表示 `主机 / 本机 / 强制 / 只读 / 计划中` 状态和连接状态。没有保存按钮,控件在 change/blur 时提交。强制行画禁用控件,只读行显示值,计划行明确说明当前运行时尚未读取。设置导航图标是宿主 shell 的限制所致:section 只接受 `{ id, order, label }`,插件只能用 CSS mask 给导航按钮补一个 Material `sync` 图标。
215
+
216
+ ## 服务级接管
217
+
218
+ - 打开 `ui.takeover` 后,`TakeoverGate` 监听 DSH 的 `approval/request` 与 `user-questions/request` 瀑布。它写一条持久 pending 行、发出 `takeover/pending`,等待客户端路由送来人工答案。
219
+ - 超时、请求 signal 中止、显式取消或插件卸载都会调用 `next()`;闸门绝不把“没人回答”变成 `allowed-once` 或编造的问题答案。`unavailable` 是宿主 fail-closed 的结果,不作为用户决定接受。
220
+ - 对话框只在以下条件同时成立时每 1 秒轮询 `/takeover/pending`:本机“权限接管”开关打开、session id 真实、页面可见。它渲染第一条 pending 请求;队列多于一条时显示 `1 / N` 徽标。
221
+ - 审批提供 仅本次允许 / 拒绝 / 取消;只提交 `allowed-once` 与 `rejected`,取消/Esc/点背景走 cancel 路由。提问按单选渲染 radio、多选渲染 checkbox,并允许每题自由文本;所有问题答完前提交按钮禁用。
222
+ - **两个开关都需要**:宿主 `ui.takeover`(默认 `false`)负责打开闸门,本机“权限接管”负责让对话框轮询。宿主开关关闭时闸门立即委托,没有窗口可显示。
223
+
224
+ ## 自动压缩与 Token 治理器
225
+
226
+ `TokenGovernor` 估算上下文(CJK 感知的 O(n) 估算器,带 256 项有界 memo),按 `contextWindow - reserve` 规划预算,并在 `agent/pre-step` 利用率越过阈值时请求 DSH 的 `compactIfNeeded`。默认:soft `0.70`、hard `0.88`、reserve `max(2048, 窗口的 8%)` 且不超过窗口 25%、recentTurns `6`、cooldown `30 s`、maxPerSession `20`。
227
+
228
+ soft 阈值可按 0.30–0.90 的比例编辑;hard 为 0.30–0.98,且必须严格高于 soft。旧界面里的 60/70/80/88% 仍是合法取值(`0.60`、`0.70`、`0.80`、`0.88`),只是目录页现在用 slider,而不是四项选择器。`compaction.auto=false` 只关闭自动压缩,不约束显式 compact 命令。
229
+
230
+ 失败封闭是刻意设计:钩子绝不阻塞 step;压缩 port 缺失、返回 `null`/`undefined`、超时、抛错、每会话额度用尽或 cooldown 未到,全部按 no-op 处理,agent 继续执行 `next()`。治理器在构造时快照策略,所以设置热更新会同时重建它。实际安全范围和替换由 DSH 负责,本插件只决定何时值得请求。
231
+
232
+ ## 文件工具、解析器与 Chain
233
+
234
+ 文件层以四个模型可见工具接线,加上三个输入框/职能工具,共 7 个:
235
+
236
+ | 工具 | 用途 |
237
+ |---|---|
238
+ | `coordination_parse` | 解析工作区文件或给定文本。`path`/`text` 二选一;返回引用、诊断、字符集、截断标记和节点卡片,绝不返回文件正文。 |
239
+ | `coordination_describe` | 按 handle 返回一个 `NodeCard`,可选 `summary`/`description`/`usage`/`purpose`;不返回正文。 |
240
+ | `coordination_chain` | 查询一条有界关系链:dependencies、dependents、satisfies、satisfied_by、baseline、prerequisites、unmet、path、cycle。深度 ≤ 8、链接 ≤ 50、约 1,500 tokens;不返回 AST 或图对象。 |
241
+ | `coordination_read` | 唯一返回文件内容的工具。读字节范围(end 不含)或行范围(1 基,end 含),单次上限 256 KiB / 2,000 行 / 16k tokens。 |
242
+
243
+ P0 解析器是词法级、确定性的:JS/TS/JSX(import/export/`require`/动态 import/符号/注释)、CSS/HTML/Markdown(`@import`/`url()`、script/link/img、链接/图片/标题/front matter/fenced div/公式)、TeX(定理类环境、证明、label/ref、数学、`\input`/`\include`)、通用公式(Markdown/TeX/表格/MathML/OMML/unknown;不求值)、词法依赖图(外部过滤、拓扑序、环、层级、孤立点)。畸形输入产生诊断和 `truncated`,不崩溃。
244
+
245
+ 每个解析节点都是**蓝图式元数据**:`path`、UTF-8 `byteStart`/`byteEnd`、1 基 `lineStart`/`lineEnd`、`sha256:<hex>` 指纹和 `charset`;没有 `text`、`body`、`content` 或 AST。`NodeCard` 与 `ChainLink` 同样如此。`coordination_read` 是唯一内容路径;它拒绝绝对路径、UNC、目录穿越和符号链接逃逸,拒绝 NUL/二进制/不可解码内容,并对返回内容做哈希。
246
+
247
+ | 上限 | 值 |
248
+ |---|---|
249
+ | 单次解析源文本 | 1 MiB;只解析前缀并标 `truncated` |
250
+ | 单次解析 token | 200,000 |
251
+ | AST 深度 | 256 |
252
+ | 解析墙钟时间 | 2,000 ms |
253
+ | 诊断保留 | 100 条 |
254
+ | 单次解析节点元数据 | 20,000 |
255
+ | 依赖图 | 20,000 节点 / 100,000 边,超限 fail-closed |
256
+ | 公式 | 单次扫描 5,000 个 / 512 条引用 |
257
+ | 解析缓存 | 256 文件,按 `path+mtime+size+parserId` 做 LRU |
258
+ | 读取范围 | 256 KiB / 2,000 行 / 16,000 估算 token |
259
+ | Chain 投影 | 深度 ≤ 8 / 链接 ≤ 50 / 约 1,500 tokens;50k 节点 / 200k 边 fail-closed |
260
+
261
+ `parser.enabled` 在注册文件工具时读取;parser 各上限与 `chain.depth`/`links`/`tokenBudget` 在惰性工具运行时创建 `ParseService`/`ChainEngine` 时捕获。因此 `/config` 的热改不会调整已创建的运行时,要到下一次注册(目前是语言切换或重启)才生效。工作区根默认回退到 `process.cwd()`,因为插件入口没有拿到 DSH 的 workspace root;cwd 不是工作区的宿主必须通过工具选项传入根目录。
262
+
263
+ **没有写入或刷新工具。** v24 的写入契约需要 `expectedFingerprint` 冲突检测、事务式重解析与范围重算、显式 charset/BOM 变更以及 watcher/rename 策略;这些都没有实现,贸然提供 writer 会覆盖并发编辑或返回过期范围。`coordination_write` 与 `coordination_refresh` 属于设计/计划。
264
+
265
+ ## 数据结构
266
+
267
+ `src/shared/` 的共享库是纯 ES2022:hash/LRU、节点、二叉树/B+ 树/红黑树、堆、栈/双端队列/队列、有序 map/set/skip list、AST/TokenStream、有向图、Trie、稀疏网格。当前运行时实际使用:
268
+
269
+ - `LruList`:token 估算 memo、解析缓存、节点注册表、Chain 投影缓存。
270
+ - `BinaryHeap`:`selectContext`,保持原全序(value 降序 → created 升序 → id 字节升序 → 输入下标)。
271
+ - `RedBlackTree` 前驱/后继:数值一致性(numeric agreement);精确 argmax 证明在采用文档中。
272
+ - `OrderedSet`:comprehension/emotion/permission 的首次出现去重;`Queue`:权限命令 tokenizer。
273
+ - `DirectedGraph`:依赖图,以及每个 Chain relation 一张图。
274
+
275
+ 第二轮采用有随机化等价测试,也有一个诚实 caveat:在 20k chunk 的 **wide** 合成用例里,堆并不比 V8 sort 快(17.7 ms 对 15.9 ms);它在预算提前填满时占优,且结果不劣于 sort。`SparseGrid` 与 `BPlusTree` 只在共享库里有回归测试,**没有接到运行时路径**;不要据此认为它们在用。
276
+
277
+ ## 工具与路由
278
+
279
+ | 工具 | 返回 |
280
+ |---|---|
281
+ | `coordination_selection` | 读取或修改某 session 的五个选择(含 `command` 与 DSH `preset`);返回带锁、预设和配置投影的 `SelectionState`。 |
282
+ | `coordination_profession` | 读取当前职能记录;仅在活动任务之前/不存在时设置职能。任务已锁时返回 `PROFESSION_LOCKED`,不改选择。 |
283
+ | `coordination_professionlock` | 读取活动 Turn 的锁定状态/职能,或指定序号 Turn 的状态。 |
284
+ | `coordination_parse` | 无正文的解析投影(引用、诊断、字符集、截断、卡片)。 |
285
+ | `coordination_describe` | 无正文的 `NodeCard` 和请求的元数据字段。 |
286
+ | `coordination_chain` | 只返回有界 `ChainProjection`。 |
287
+ | `coordination_read` | 唯一返回内容的工具;有上限的字节/行范围。 |
288
+
289
+ 所有路由都在 `/deepseek-coordination` 下,共 **14 条路径 / 16 个方法处理器**:
290
+
291
+ | 方法 | 路径 | 作用 |
292
+ |---|---|---|
293
+ | GET | `/ping` | 存活检查;返回插件名、`pong`、当前语言。 |
294
+ | POST | `/echo` | 回显 JSON body(诊断用)。 |
295
+ | GET, POST | `/selection` | 读 `SelectionState`;修改五个选择。 |
296
+ | GET | `/presets` | 投影 DSH 权限预设目录。 |
297
+ | POST | `/permission` | 立即应用一个 DSH 预设。 |
298
+ | GET, POST | `/config` | 读 `{ values, defaults }`;应用扁平配置补丁。 |
299
+ | POST | `/config/compaction` | `auto`/`soft`/`hard` 的兼容补丁。 |
300
+ | GET | `/takeover/pending` | 某 session 的待处理审批/提问窗口。 |
301
+ | POST | `/takeover/answer` | 提交合法人工答案;非法答案保持窗口。 |
302
+ | POST | `/takeover/cancel` | 取消窗口并交还宿主默认处理。 |
303
+ | POST | `/profession/rebuild` | 结束活动任务,用新职能创建替代 Turn。 |
304
+ | POST | `/profession/abandon` | 结束活动任务,不创建替代。 |
305
+ | POST | `/locale` | 上报客户端语言;重新注册本地化工具描述。 |
306
+ | GET | `/state` | 选择 + 协调指标(`session`、`turn`、`plan`、`subtask`、`trace`、`takeover`、`schema`)。 |
307
+
308
+ 没有 `coordination_write`、`coordination_refresh`、`outline`、`findSymbol`、`searchSummary`、`commentsOf` 工具。四个文件工具的说明目前是内联中英双语,因为共享 `ToolName` 表还没有它们的键;三个输入框/职能工具使用共享 copy 表。
309
+
310
+ ## 数据、隐私与安全
311
+
312
+ - 插件使用独立 SQLite 库,从不打开 DSH 的 `harness` 库。默认路径是 `%DSH_HOME%\coordination\coordination.db`;`database`/`databasePath` 只属于部署配置,既不投影也不允许 `/config` 写入。
313
+ - 解析节点、`NodeCard`、`ChainLink` 及其工具结果都不带文件正文、附件或 AST。只有 `coordination_read` 返回文本,而且只返回请求的范围。
314
+ - 文件路径必须是工作区相对路径;绝对路径、盘符路径、UNC、`..` 逃逸与符号链接逃逸在读取前拒绝。二进制/NUL/不可解码内容直接拒绝,不猜。
315
+ - Turn 钩子只写 Turn 元数据和一条空 prompt 行,不镜像 DSH 对话,也不把文件内容复制进协调库。
316
+ - 权限预设立即写给 DSH(完全权限需二次确认),从不进入离线 localStorage;离线重放只覆盖五个已存选择字段。
317
+ - 接管绝不自动批准。超时、中止、取消、卸载都以 `next()` 委托;只有通过 answer 路由送达的人工决定才会结算窗口。
318
+ - 失败封闭是明确的:非法接管答案保持窗口;`PROFESSION_LOCKED` 不改选择;解析不确定标 `unverifiable`;解析/图溢出标 `truncated`;压缩失败是 no-op;Chain 的 `unknown` 不会自动通过。
319
+ - 协调库保存任务/审计行(包括 `prompt` 与追踪行),不保存凭据或 provider secret。配置投影中没有 DSH 凭据、token 或 provider key。
320
+
321
+ ## 测试、质量与限制
322
+
323
+ | 检查 | 结果 |
324
+ |---|---|
325
+ | `npm test` | **473 tests,472 pass,1 skipped(仅 GC),0 fail**,Node 24.21.0 上约 3.3 s |
326
+ | `npm run typecheck` / `npm run test:tsc` | 通过 |
327
+ | `npm run build` | 宿主 + 客户端 bundle 通过;MUI 的 `"use client"` directive 警告不致命 |
328
+ | `npm run pack:check` | OK:`deepseek-coordination-0.10.0.tgz`(538,358 B),四个运行时文件齐全 |
329
+ | 发布 CI | `.github/workflows/release.yml` 运行 typecheck、test:tsc、测试、pack:check,并在 `v*` Release 附上 tarball |
330
+
331
+ 解析器与结构的数字是守卫,不是承诺:100k 字符解析测试保持在 3 秒内;数据结构报告记录 B+ 树 10k 插入 8.3/9.8 ms、skip list 10k 插入 6.4 ms、红黑树 10k 随机 set/delete 13.5 ms、稀疏网格 10k 单元 5.1 ms、`hashString` 100k 次 4.3 ms;复杂度审计记录 Turn 序号从 `COUNT(*)` 改为 `MAX(ordinal)`(2,000 条语句约 65–69 ms → 约 12–14 ms)以及路由排序(200 × 12 条路由的 2,400 次 posterior 查询降到 200 次)。
332
+
333
+ 已知限制,直接列出而不是隐藏:
334
+
335
+ 1. **持久图有边界。** `node`/`edge`/`snapshot` 已是 schema 24,跨运行时保留;但重水化只取最新 10k 节点 / 50k 边,删除的符号不会清理,超出窗口的旧 handle 返回 `NOT_FOUND`。
336
+ 2. **领域提取在解析器层,关系链投影仍不完整。** Book/Article/Part/Chapter/Section/Paragraph、Person/Venue、Condition、Sheet/Cell/Range、TeX 证明状态和 CSV/TSV 表格已产出并投影到节点卡;`partOf`/`condition` 边还不能通过 `coordination_chain` 查询。xlsx/docx/pptx、Sign 提升、Lean/Coq/Isabelle 状态和跨文件 Book 组合仍未实现。
337
+ 3. **核心 Turn 流水线已接线,更宽的引擎没有。** `command=none` 的 Turn 会运行 Comprehension → Organization → Plan/score,以及需要的 Predict 行;有端口的 scene 策略会生效。独立 Emotion/Bayesian/Cognition/Orchestration/Learning/Resource/Score、计划重建循环和 decompose/dispatch/verify/synthesize 仍是库级。`thinking`、`allowBash`、`maxIteration` 和非写作阈值记录为延迟诊断,不伪造。
338
+ 4. **配置上限已生效。** `parser.maxAstDepth`、两个依赖图上限和 `tools.maxResultBytes` 都有消费者;parser/chain 上限会 retune 已存在运行时,不再留下陈旧运行时。
339
+ 5. **工作区根已解析,不再硬编码。** 工具参数 → service 根 → host 根 → sandbox policy 根 → 最后才回退 `process.cwd()`。
340
+ 6. **`parser.enabled` 仍需重新注册。** 其他文件层上限会即时 retune;关闭 parser 会立即失败关闭,但从工具列表增删四个工具仍要等下一次注册(重启)。
341
+ 7. **没有浏览器 E2E。** UI 由类型、构建、settings-catalog 测试以及 takeover/bridge/集成测试覆盖;视觉验证仍是人工/截图。
342
+ 8. **Node 引擎为 `>=22.13.0`。** `node:sqlite` 从 22.13.0/23.4.0 起不再需要实验开关;CI 与本次验证使用 24.21.0。
343
+ 9. **profile 里可能仍记录着旧的包 spec。** 请核对实际安装的版本(`dsh --profile <name> --dump-config` 或 profile 的 `package.json`);安装 0.10.0 tarball 或链接已构建的 checkout,不要假定上一次安装已被升级。
344
+
345
+ ## 仓库历史与许可
346
+
347
+ `0.1.x` 是另一个项目 **dsh-os**,已不在分支顶端;`0.2.0` 变成 DSH 插件 bundle;`0.3`–`0.6` 的 UI 从 Angular 经 MDUI、纯 React 迁到 Material UI;`0.7`–`0.9` 加入五个下拉框、i18n、职能锁定、接管、压缩、P0 解析器、领域模型和共享数据结构;`0.10.0` 加入设置目录、四个文件工具、第二轮数据结构采用和可验证 tarball。旧状态仍在 git 历史和 tag 中。
348
+
349
+ MIT © 2026 Eloy Kim。见 [LICENSE](LICENSE)。源码:[github.com/eloyhere/deepseek-coordination](https://github.com/eloyhere/deepseek-coordination)。