deepseek-coordination 0.1.1 → 0.1.5
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.md +271 -14
- package/README.zh-CN.md +160 -11
- package/client.js +338 -0
- package/cordis.patch.yml +40 -0
- package/package.json +22 -2
- package/src/host/bin/dsh-os.ps1 +10 -1
- package/src/host/bin/dsh-os7.ps1 +16 -1
- package/src/plugin/index.js +4 -213
- package/src/plugin/invoke.js +301 -0
- package/src/plugin/settings.js +340 -0
- package/src/plugin/takeover.js +432 -0
- package/src/plugin/workflow.js +1464 -0
- package/tools/measure-cache.mjs +223 -0
package/README.zh-CN.md
CHANGED
|
@@ -58,15 +58,21 @@ deepseek-coordination/
|
|
|
58
58
|
├─ src/host/docs/os-spec.md 完整规范:契约、错误码、黄金用例矩阵
|
|
59
59
|
├─ src/host/docs/permission-model.md 授权模型及其残余风险
|
|
60
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` 生成物
|
|
61
65
|
├─ src/permission/index.js 同意写入委托:`permission.json` 的唯一写入方,注册 5 个 `os_permission_*` 工具
|
|
62
66
|
├─ tools/build.ps1 拼装三个生成脚本;`-Check` 在漂移时失败
|
|
63
67
|
├─ tools/parse-check.ps1 在**将要执行代码的那个运行时**下解析检查模块,并报告非 ASCII 与 BOM
|
|
64
68
|
├─ tools/json-field.mjs 按点分路径读取信封里的一个字段(shell 测试用,`sed` 看不进嵌套对象)
|
|
65
|
-
├─ tools/verify-bundle.mjs 校验 loader 的解析契约(
|
|
66
|
-
├─ tools/verify-plugin-load.mjs 走真实 `apply()` 路径(
|
|
69
|
+
├─ tools/verify-bundle.mjs 校验 loader 的解析契约(14 项)
|
|
70
|
+
├─ tools/verify-plugin-load.mjs 走真实 `apply()` 路径(30 项)
|
|
67
71
|
├─ tools/verify-editions.mjs 证明两个 PowerShell 生成物都可达(14 项)
|
|
68
|
-
├─
|
|
72
|
+
├─ tools/probe-pipeline.mjs 在打桩的动词层上端到端驱动拦截链路(23 项)
|
|
73
|
+
├─ tests/ 黄金(Windows/POSIX/桌面注入)、两表一致性、编码、插件、授权、接管各套测试
|
|
69
74
|
│ ├─ conformance.ps1 `command.txt` 与 `spec/verbs.tsv` 必须描述同一个面(5 项)
|
|
75
|
+
│ ├─ takeover.test.mjs 字节级格式保留、读取窗口切片、可拦截资格(41 项)
|
|
70
76
|
│ ├─ smoke-desktop-sh.sh POSIX 桌面动词**实际注入了什么**(52 项)
|
|
71
77
|
│ └─ fake-xdotool, fake-wmctrl 会记录调用的替身,使无 X server 的机器也能校验桌面动词
|
|
72
78
|
└─ permission.json 授权数据(随仓库为空文件,这就是初始状态)
|
|
@@ -101,7 +107,13 @@ deepseek-coordination/
|
|
|
101
107
|
|
|
102
108
|
## 开始使用
|
|
103
109
|
|
|
104
|
-
|
|
110
|
+
已发布到 npm,因此 DSH profile 可以把它作为 bundle 安装:
|
|
111
|
+
|
|
112
|
+
```console
|
|
113
|
+
dsh plugin install deepseek-coordination
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
或从源码使用:
|
|
105
117
|
|
|
106
118
|
```console
|
|
107
119
|
git clone https://github.com/eloyhere/deepseek-coordination.git
|
|
@@ -128,11 +140,23 @@ src/host/bin/dsh-os.sh file charset "./a file.txt"
|
|
|
128
140
|
|
|
129
141
|
### 从 harness 使用
|
|
130
142
|
|
|
131
|
-
`package.json` 声明了 `dsh.bundle.patch`,因此本包可以作为一个 bundle 安装进 DSH profile
|
|
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。子路径导出正是为此存在。
|
|
132
154
|
|
|
133
155
|
| 工具 | 用途 |
|
|
134
156
|
|---|---|
|
|
135
157
|
| `dsh_os` | 执行动词面上的一个动词,并原样返回其 JSON 信封 |
|
|
158
|
+
| `os_permission_*` | 读取、授予、撤销与审计持久授权决定 |
|
|
159
|
+
| `workflow_plan` | 执行意图优先工作流的一个阶段(见[工作流](#工作流)) |
|
|
136
160
|
|
|
137
161
|
只注册一个工具而非每个动词一个,理由有三:动词表就是契约且存放在 `src/host/spec/verbs.tsv`,新增动词时无需重新生成 schema;agent 用一次 `verb: "selfcheck"` 调用就能发现整个动词面;信封本身已是稳定的结果形状,不会出现第二份会与脚本漂移的契约。
|
|
138
162
|
|
|
@@ -144,6 +168,27 @@ src/host/bin/dsh-os.sh file charset "./a file.txt"
|
|
|
144
168
|
|
|
145
169
|
实现以绝对路径加 argv 数组方式拉起,永不经 shell,因此含空格的值仍然是一个值。
|
|
146
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
|
+
|
|
147
192
|
**选择 PowerShell 版本。** 默认仍是 5.1 产物(`bin/dsh-os.ps1`),**刻意不变**:已经工作的部署不应因为插件升级而在脚下改变行为。要选 7.6 产物(`bin/dsh-os7.ps1`):
|
|
148
193
|
|
|
149
194
|
* `powershellEdition: '7.6'` —— 插件配置;
|
|
@@ -155,6 +200,101 @@ src/host/bin/dsh-os.sh file charset "./a file.txt"
|
|
|
155
200
|
> [!IMPORTANT]
|
|
156
201
|
> **仍未证明:harness 自身的 loader 会在活动 profile 里组合这个 bundle。** 插件的解析、装载与执行路径现由 `tools/verify-bundle.mjs`、`tools/verify-plugin-load.mjs` 与 `tools/verify-editions.mjs` 覆盖(详见[安装现状](#安装现状与已知缺陷)),但"装进用户的桌面活动 profile 并重启 harness"这一步**没有被跑过**。
|
|
157
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
|
+
|
|
158
298
|
---
|
|
159
299
|
|
|
160
300
|
## JSON 契约
|
|
@@ -397,7 +537,7 @@ dsh-os permission audit --limit 50 # 追加式尾部;委托裁剪到 200
|
|
|
397
537
|
| 校验器 | 证明什么 | 用例 |
|
|
398
538
|
|---|---|---|
|
|
399
539
|
| `tools/verify-bundle.mjs` | loader 的**解析**契约:manifest 头、patch 文件、每一行都能从一个 profile 形态的 `node_modules` 解析、每个被解析模块都导出 loader 会调用的 Cordis 形状(`apply`/`inject`/`name`)、两行解析到**不同**模块(共享说明符会导致同一个工具注册两次并让整个入口中止)、以及版本闸门 | 10 |
|
|
400
|
-
| `tools/verify-plugin-load.mjs` | 真实的 `apply()` 路径:宿主插件以**根类型为 `object`** 的参数 schema 注册 `dsh_os`,授权插件注册其五个工具,并且**注册后的 handler 真的 spawn 了脚本**并返回真实信封(`selfcheck` → ok,`verbCount` 86);不带 `allow` 的变更类动词以带类型的 `E_PERMISSION`
|
|
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 |
|
|
401
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 |
|
|
402
542
|
|
|
403
543
|
bundle 解析校验把包以 junction 链接进一个一次性的 profile 沙箱,因此解析行为与真实 profile 一致,而**活动 profile 从不被写入**。
|
|
@@ -410,7 +550,7 @@ bundle 解析校验把包以 junction 链接进一个一次性的 profile 沙箱
|
|
|
410
550
|
|
|
411
551
|
此处曾记录:在干净检出上实测 `passed: 47 failed: 1`,失败用例是 `parser/one-two-three-token-verbs`,因为它断言 `file length` 作用于一个由**更晚**的用例才创建的夹具目录。
|
|
412
552
|
|
|
413
|
-
该缺陷**已修复**:`tests/golden.ps1` 的解析器用例现在自己创建夹具(`$fixtureDir = Join-Path $sandbox 'parser\verify'`,随后 `New-Item` 与 `Set-Content`),再对其断言。当前实测为 **
|
|
553
|
+
该缺陷**已修复**:`tests/golden.ps1` 的解析器用例现在自己创建夹具(`$fixtureDir = Join-Path $sandbox 'parser\verify'`,随后 `New-Item` 与 `Set-Content`),再对其断言。当前实测为 **50 通过,0 失败**。
|
|
414
554
|
|
|
415
555
|
### `tests/smoke-desktop-sh.sh` 查出的两个 POSIX 真实缺陷
|
|
416
556
|
|
|
@@ -451,21 +591,30 @@ bundle 解析校验把包以 junction 链接进一个一次性的 profile 沙箱
|
|
|
451
591
|
npm run test:all # 按顺序跑下面全部
|
|
452
592
|
```
|
|
453
593
|
|
|
454
|
-
`test:all` 依次串联 `test:conformance`、`test:encoding`、`test`(Windows 黄金)、`test:plugin`、`test:permission`、`test:bundle`(`tools/verify-bundle.mjs`)、`test:load`(先 `tools/verify-plugin-load.mjs`,再 `tools/verify-editions.mjs`)与 `test:desktop`(`bash tests/smoke-desktop-sh.sh`)。
|
|
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`)。
|
|
455
595
|
|
|
456
596
|
| 套件 | 命令 | 用例数 | 实测结果 |
|
|
457
597
|
|---|---|---|---|
|
|
458
598
|
| 两表一致性 | `powershell -File tests/conformance.ps1` | 5 | **5 通过,0 失败** |
|
|
459
599
|
| 编码契约 | `powershell -File tests/encoding.ps1` | 32 个构建模块 | **全部契约成立** |
|
|
460
|
-
| Windows 黄金 | `powershell -File tests/golden.ps1` |
|
|
600
|
+
| Windows 黄金 | `powershell -File tests/golden.ps1` | 50 | **50 通过,0 失败** |
|
|
461
601
|
| POSIX 黄金 | `bash tests/golden-sh.sh` | 116 | **116 通过,0 失败** |
|
|
462
602
|
| POSIX 桌面注入 | `bash tests/smoke-desktop-sh.sh` | 52 | **52 通过,0 失败** |
|
|
463
603
|
| 插件 | `node tests/plugin.test.mjs` | 13 | **13 通过,0 失败** |
|
|
604
|
+
| 接管拦截 | `node tests/takeover.test.mjs` | 41 | **41 通过,0 失败** |
|
|
464
605
|
| 授权委托 | `node tests/permission.test.mjs` | 18 | **18 通过,0 失败** |
|
|
465
|
-
|
|
|
466
|
-
|
|
|
606
|
+
| 工作流 | `node tests/workflow.test.mjs` | 30 | **30 通过,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 失败** |
|
|
467
610
|
| 版本选择 | `node tools/verify-editions.mjs` | 14 | **14 通过,0 失败** |
|
|
468
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 主机。
|
|
469
618
|
|
|
470
619
|
### 两张表的一致性
|
|
471
620
|
|
package/client.js
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-os settings panel: the client half of the takeover switch.
|
|
3
|
+
*
|
|
4
|
+
* HOW THIS FILE IS LOADED, AND WHY IT LOOKS LIKE THIS
|
|
5
|
+
*
|
|
6
|
+
* The browser half of a DSH plugin is not an ordinary ES module. It is a self-registering lazy factory
|
|
7
|
+
* keyed by the package name, handed to the page's module table:
|
|
8
|
+
*
|
|
9
|
+
* window.__ModuleLoader__.load({ id: '<package name>', factory(require) { ... } })
|
|
10
|
+
*
|
|
11
|
+
* React is NOT imported and NOT bundled: it is taken from the module table with `require('react')`, and
|
|
12
|
+
* shipping a second copy would give the page two Reacts and break hooks. `<package.json>` therefore
|
|
13
|
+
* declares `dsh.client` and exposes this file as the `./client` subpath, which is what the loader reads.
|
|
14
|
+
*
|
|
15
|
+
* The file is plain ES5-flavoured JavaScript — no JSX, no TypeScript — deliberately. JSX would require a
|
|
16
|
+
* build step whose output format has to match the table's contract exactly, and a hand-written file in
|
|
17
|
+
* the documented format is verifiable by reading it. `../src/client/` remains the untouched Vite
|
|
18
|
+
* scaffold it always was; this is the artifact that ships.
|
|
19
|
+
*
|
|
20
|
+
* WHAT THE PANEL DOES
|
|
21
|
+
*
|
|
22
|
+
* It edits three things and shows one:
|
|
23
|
+
*
|
|
24
|
+
* * the master switch, which is the only control that turns interception on;
|
|
25
|
+
* * a per-tool switch for each of read / write / edit / pwsh;
|
|
26
|
+
* * the write convention (preserve the target's BOM and line endings, or reproduce the built-in
|
|
27
|
+
* UTF-8-without-BOM, LF behaviour);
|
|
28
|
+
* * an activity feed of what was actually taken over, which is the only view that can distinguish
|
|
29
|
+
* "configured" from "working".
|
|
30
|
+
*
|
|
31
|
+
* State lives on the host (see `settings.js`), reached over the same-origin endpoints
|
|
32
|
+
* `/api/dsh-os/settings` and `/api/dsh-os/set`. The panel is a pure view over that state, so a page
|
|
33
|
+
* reload costs nothing and two open windows cannot disagree.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
window.__ModuleLoader__.load({
|
|
37
|
+
id: 'deepseek-coordination',
|
|
38
|
+
factory(require) {
|
|
39
|
+
const React = require('react')
|
|
40
|
+
const h = React.createElement
|
|
41
|
+
|
|
42
|
+
/** Endpoints registered by the host half. Relative, so no port or origin is baked in. */
|
|
43
|
+
const STATE_URL = '/api/dsh-os/settings'
|
|
44
|
+
const SET_URL = '/api/dsh-os/set'
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Theme tokens only.
|
|
48
|
+
*
|
|
49
|
+
* Literal colours would be wrong in one of the two themes, and reaching into another plugin's
|
|
50
|
+
* stylesheet is forbidden by the UI rules. A renamed token degrades the appearance; it never
|
|
51
|
+
* breaks rendering, which is the property being bought here.
|
|
52
|
+
*/
|
|
53
|
+
const T = {
|
|
54
|
+
label: 'var(--dsw-alias-label-primary)',
|
|
55
|
+
labelSecondary: 'var(--dsw-alias-label-secondary)',
|
|
56
|
+
labelTertiary: 'var(--dsw-alias-label-tertiary)',
|
|
57
|
+
border: '1px solid var(--dsw-alias-border-l)',
|
|
58
|
+
brand: 'var(--dsw-alias-brand-primary)',
|
|
59
|
+
error: 'var(--dsw-alias-state-error-primary)',
|
|
60
|
+
warn: 'var(--dsw-alias-state-warn-primary)',
|
|
61
|
+
hover: 'var(--dsw-alias-interactive-bg-hover)',
|
|
62
|
+
module: 'var(--dsw-alias-bg-module-platform)',
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** One labelled row with a switch on the right, matching the host's settings row rhythm. */
|
|
66
|
+
function Switch(props) {
|
|
67
|
+
const { checked, disabled, onChange, label, hint } = props
|
|
68
|
+
return h('div', {
|
|
69
|
+
style: {
|
|
70
|
+
display: 'flex',
|
|
71
|
+
alignItems: 'flex-start',
|
|
72
|
+
gap: '12px',
|
|
73
|
+
padding: '10px 0',
|
|
74
|
+
borderBottom: T.border,
|
|
75
|
+
},
|
|
76
|
+
}, [
|
|
77
|
+
h('div', { key: 'text', style: { flex: '1 1 auto', minWidth: 0 } }, [
|
|
78
|
+
h('div', { key: 'label', style: { color: T.label, fontSize: '13px', lineHeight: '20px' } }, label),
|
|
79
|
+
hint === undefined ? null : h('div', {
|
|
80
|
+
key: 'hint',
|
|
81
|
+
style: { color: T.labelTertiary, fontSize: '12px', lineHeight: '18px', marginTop: '2px' },
|
|
82
|
+
}, hint),
|
|
83
|
+
]),
|
|
84
|
+
h('button', {
|
|
85
|
+
key: 'switch',
|
|
86
|
+
type: 'button',
|
|
87
|
+
role: 'switch',
|
|
88
|
+
'aria-checked': checked === true,
|
|
89
|
+
'aria-label': label,
|
|
90
|
+
disabled: disabled === true,
|
|
91
|
+
onClick: () => { if (disabled !== true) onChange(!checked) },
|
|
92
|
+
style: {
|
|
93
|
+
flex: '0 0 auto',
|
|
94
|
+
width: '38px',
|
|
95
|
+
height: '22px',
|
|
96
|
+
borderRadius: '11px',
|
|
97
|
+
border: 'none',
|
|
98
|
+
padding: '0',
|
|
99
|
+
cursor: disabled === true ? 'not-allowed' : 'pointer',
|
|
100
|
+
opacity: disabled === true ? 0.5 : 1,
|
|
101
|
+
background: checked === true ? T.brand : T.hover,
|
|
102
|
+
position: 'relative',
|
|
103
|
+
transition: 'background 120ms linear',
|
|
104
|
+
},
|
|
105
|
+
}, h('span', {
|
|
106
|
+
style: {
|
|
107
|
+
position: 'absolute',
|
|
108
|
+
top: '2px',
|
|
109
|
+
left: checked === true ? '18px' : '2px',
|
|
110
|
+
width: '18px',
|
|
111
|
+
height: '18px',
|
|
112
|
+
borderRadius: '50%',
|
|
113
|
+
background: '#fff',
|
|
114
|
+
transition: 'left 120ms ease',
|
|
115
|
+
},
|
|
116
|
+
})),
|
|
117
|
+
])
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** A small segmented control, used for the write convention. */
|
|
121
|
+
function Segmented(props) {
|
|
122
|
+
const { value, options, disabled, onChange } = props
|
|
123
|
+
return h('div', {
|
|
124
|
+
style: { display: 'inline-flex', border: T.border, borderRadius: '6px', overflow: 'hidden' },
|
|
125
|
+
}, options.map((option, index) => h('button', {
|
|
126
|
+
key: option.value,
|
|
127
|
+
type: 'button',
|
|
128
|
+
disabled: disabled === true,
|
|
129
|
+
'aria-pressed': value === option.value,
|
|
130
|
+
onClick: () => { if (disabled !== true) onChange(option.value) },
|
|
131
|
+
style: {
|
|
132
|
+
padding: '4px 10px',
|
|
133
|
+
fontSize: '12px',
|
|
134
|
+
lineHeight: '18px',
|
|
135
|
+
border: 'none',
|
|
136
|
+
borderLeft: index === 0 ? 'none' : T.border,
|
|
137
|
+
cursor: disabled === true ? 'not-allowed' : 'pointer',
|
|
138
|
+
background: value === option.value ? T.brand : 'transparent',
|
|
139
|
+
color: value === option.value ? '#fff' : T.labelSecondary,
|
|
140
|
+
},
|
|
141
|
+
}, option.label)))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Render one activity entry: what was taken over, or why a call fell back. */
|
|
145
|
+
function ActivityRow(props) {
|
|
146
|
+
const entry = props.entry
|
|
147
|
+
const took = entry.outcome === 'taken-over'
|
|
148
|
+
return h('li', {
|
|
149
|
+
style: {
|
|
150
|
+
display: 'flex',
|
|
151
|
+
gap: '8px',
|
|
152
|
+
alignItems: 'baseline',
|
|
153
|
+
padding: '4px 0',
|
|
154
|
+
fontSize: '12px',
|
|
155
|
+
lineHeight: '18px',
|
|
156
|
+
},
|
|
157
|
+
}, [
|
|
158
|
+
h('span', {
|
|
159
|
+
key: 'tool',
|
|
160
|
+
style: {
|
|
161
|
+
flex: '0 0 auto',
|
|
162
|
+
color: took ? T.brand : T.warn,
|
|
163
|
+
fontVariantNumeric: 'tabular-nums',
|
|
164
|
+
},
|
|
165
|
+
}, entry.tool),
|
|
166
|
+
h('span', {
|
|
167
|
+
key: 'subject',
|
|
168
|
+
title: entry.subject,
|
|
169
|
+
style: {
|
|
170
|
+
flex: '1 1 auto',
|
|
171
|
+
minWidth: 0,
|
|
172
|
+
overflow: 'hidden',
|
|
173
|
+
textOverflow: 'ellipsis',
|
|
174
|
+
whiteSpace: 'nowrap',
|
|
175
|
+
color: T.labelSecondary,
|
|
176
|
+
direction: 'rtl',
|
|
177
|
+
textAlign: 'left',
|
|
178
|
+
},
|
|
179
|
+
}, entry.subject === '' ? '—' : entry.subject),
|
|
180
|
+
h('span', {
|
|
181
|
+
key: 'outcome',
|
|
182
|
+
style: { flex: '0 0 auto', color: took ? T.labelTertiary : T.error },
|
|
183
|
+
}, took ? '接管' : '回退'),
|
|
184
|
+
])
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The panel.
|
|
189
|
+
*
|
|
190
|
+
* It owns exactly one piece of state (the last document fetched) plus a pending flag. Every control
|
|
191
|
+
* posts and then adopts the response, so the host is always the source of truth and an optimistic
|
|
192
|
+
* local edit cannot drift from what the interception actually reads.
|
|
193
|
+
*/
|
|
194
|
+
function SettingsSection() {
|
|
195
|
+
const [state, setState] = React.useState(null)
|
|
196
|
+
const [error, setError] = React.useState(null)
|
|
197
|
+
const [busy, setBusy] = React.useState(false)
|
|
198
|
+
|
|
199
|
+
const load = React.useCallback(() => {
|
|
200
|
+
fetch(STATE_URL, { headers: { accept: 'application/json' } })
|
|
201
|
+
.then((response) => response.json())
|
|
202
|
+
.then((body) => {
|
|
203
|
+
if (body && body.ok) { setState(body); setError(null) } else { setError((body && body.error) || '读取失败') }
|
|
204
|
+
})
|
|
205
|
+
.catch((cause) => setError(String(cause && cause.message ? cause.message : cause)))
|
|
206
|
+
}, [])
|
|
207
|
+
|
|
208
|
+
React.useEffect(() => {
|
|
209
|
+
load()
|
|
210
|
+
// The feed is the point of the panel, so it refreshes on a slow interval while it is open. Ten
|
|
211
|
+
// seconds is chosen to be far below the cost of noticing: a five-minute poll would leave a user
|
|
212
|
+
// convinced that nothing is happening.
|
|
213
|
+
const timer = setInterval(load, 10000)
|
|
214
|
+
return () => clearInterval(timer)
|
|
215
|
+
}, [load])
|
|
216
|
+
|
|
217
|
+
const save = React.useCallback((patch) => {
|
|
218
|
+
setBusy(true)
|
|
219
|
+
fetch(SET_URL, {
|
|
220
|
+
method: 'POST',
|
|
221
|
+
headers: { 'content-type': 'application/json' },
|
|
222
|
+
body: JSON.stringify(patch),
|
|
223
|
+
})
|
|
224
|
+
.then((response) => response.json())
|
|
225
|
+
.then((body) => {
|
|
226
|
+
if (body && body.ok) { setState(body); setError(null) } else { setError((body && body.error) || '保存失败') }
|
|
227
|
+
})
|
|
228
|
+
.catch((cause) => setError(String(cause && cause.message ? cause.message : cause)))
|
|
229
|
+
.then(() => setBusy(false))
|
|
230
|
+
}, [])
|
|
231
|
+
|
|
232
|
+
if (state === null) {
|
|
233
|
+
return h('div', { style: { padding: '12px 0', color: error === null ? T.labelTertiary : T.error } },
|
|
234
|
+
error === null ? '正在读取 dsh-os 设置…' : `无法读取 dsh-os 设置:${error}`)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const settings = state.settings
|
|
238
|
+
const tools = settings.tools || {}
|
|
239
|
+
const activity = settings.activity || []
|
|
240
|
+
const master = settings.enabled === true
|
|
241
|
+
const names = {
|
|
242
|
+
read: '读取文件(read)',
|
|
243
|
+
write: '写入文件(write)',
|
|
244
|
+
edit: '编辑文件(edit)',
|
|
245
|
+
pwsh: '执行命令(pwsh)',
|
|
246
|
+
}
|
|
247
|
+
const hints = {
|
|
248
|
+
read: '经 dsh-os 读取:显式探测编码,剥掉 BOM 后再按行返回。',
|
|
249
|
+
write: '经 dsh-os 写入:保留目标文件原有的 BOM 与换行;新建文件仍按内置约定(UTF-8 无 BOM、LF)。',
|
|
250
|
+
edit: '经 dsh-os 编辑:字面替换语义与内置一致,写回时沿用目标文件的编码约定。',
|
|
251
|
+
pwsh: '经 dsh-os 执行:argv 数组直接传参,不拼 shell 字符串。后台任务与沙箱升级仍走内置实现。',
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return h('div', { style: { padding: '4px 0' } }, [
|
|
255
|
+
h('div', { key: 'head', style: { color: T.labelSecondary, fontSize: '12px', lineHeight: '18px', marginBottom: '4px' } },
|
|
256
|
+
'让 dsh-os 接管内置的文件与命令操作,从而获得字节级编码、BOM 与换行保证。关闭时全部走内置实现。'),
|
|
257
|
+
|
|
258
|
+
h(Switch, {
|
|
259
|
+
key: 'master',
|
|
260
|
+
checked: master,
|
|
261
|
+
disabled: busy,
|
|
262
|
+
label: '接管内置操作',
|
|
263
|
+
hint: '总开关。关闭后下面的逐项设置保留,但不再生效。',
|
|
264
|
+
onChange: (next) => save({ enabled: next }),
|
|
265
|
+
}),
|
|
266
|
+
|
|
267
|
+
h('div', { key: 'tools', style: { opacity: master ? 1 : 0.55 } },
|
|
268
|
+
['read', 'write', 'edit', 'pwsh'].map((tool) => h(Switch, {
|
|
269
|
+
key: tool,
|
|
270
|
+
checked: tools[tool] === true,
|
|
271
|
+
disabled: busy || !master,
|
|
272
|
+
label: names[tool],
|
|
273
|
+
hint: hints[tool],
|
|
274
|
+
onChange: (next) => save({ tools: { [tool]: next } }),
|
|
275
|
+
}))),
|
|
276
|
+
|
|
277
|
+
h('div', { key: 'format', style: { display: 'flex', alignItems: 'center', gap: '12px', padding: '12px 0', borderBottom: T.border, opacity: master ? 1 : 0.55 } }, [
|
|
278
|
+
h('div', { key: 'label', style: { flex: '1 1 auto' } }, [
|
|
279
|
+
h('div', { key: 't', style: { color: T.label, fontSize: '13px', lineHeight: '20px' } }, '写入编码约定'),
|
|
280
|
+
h('div', { key: 'h', style: { color: T.labelTertiary, fontSize: '12px', lineHeight: '18px', marginTop: '2px' } },
|
|
281
|
+
'新建文件一律 UTF-8 无 BOM、LF;此项决定改写已有文件时是否沿用其原有约定。'),
|
|
282
|
+
]),
|
|
283
|
+
h(Segmented, {
|
|
284
|
+
key: 'control',
|
|
285
|
+
value: settings.writeFormat === 'builtin' ? 'builtin' : 'target',
|
|
286
|
+
disabled: busy || !master,
|
|
287
|
+
options: [
|
|
288
|
+
{ value: 'target', label: '沿用原文件' },
|
|
289
|
+
{ value: 'builtin', label: '同内置' },
|
|
290
|
+
],
|
|
291
|
+
onChange: (next) => save({ writeFormat: next }),
|
|
292
|
+
}),
|
|
293
|
+
]),
|
|
294
|
+
|
|
295
|
+
h('div', { key: 'activity', style: { paddingTop: '12px' } }, [
|
|
296
|
+
h('div', { key: 'title', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' } }, [
|
|
297
|
+
h('span', { key: 't', style: { color: T.label, fontSize: '13px' } }, '最近活动'),
|
|
298
|
+
h('span', { key: 'c', style: { color: T.labelTertiary, fontSize: '12px' } },
|
|
299
|
+
`${activity.length} 条 · ${master ? '接管中' : '未启用'}`),
|
|
300
|
+
]),
|
|
301
|
+
activity.length === 0
|
|
302
|
+
? h('div', { key: 'empty', style: { color: T.labelTertiary, fontSize: '12px', padding: '8px 0' } },
|
|
303
|
+
master ? '尚未拦截到调用。' : '启用后这里会显示实际被接管的调用。')
|
|
304
|
+
: h('ul', {
|
|
305
|
+
key: 'list',
|
|
306
|
+
style: { listStyle: 'none', margin: '4px 0 0', padding: '0', maxHeight: '220px', overflowY: 'auto' },
|
|
307
|
+
}, activity.slice().reverse().map((entry, index) => h(ActivityRow, { key: `${entry.at}-${index}`, entry }))),
|
|
308
|
+
]),
|
|
309
|
+
|
|
310
|
+
error === null ? null : h('div', {
|
|
311
|
+
key: 'error',
|
|
312
|
+
style: { marginTop: '10px', color: T.error, fontSize: '12px', lineHeight: '18px' },
|
|
313
|
+
}, error),
|
|
314
|
+
|
|
315
|
+
h('div', {
|
|
316
|
+
key: 'path',
|
|
317
|
+
style: { marginTop: '10px', color: T.labelTertiary, fontSize: '11px', lineHeight: '16px', wordBreak: 'break-all' },
|
|
318
|
+
}, `设置文件:${settings.path}`),
|
|
319
|
+
])
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
// `slots` is the only service the panel needs: it renders into the settings page and talks to the
|
|
324
|
+
// host over plain HTTP, so it does not need the session, the store, or the connection.
|
|
325
|
+
inject: ['slots'],
|
|
326
|
+
apply(ctx) {
|
|
327
|
+
// `inject` waits for the slot to exist; a settings page that is disabled in this profile leaves
|
|
328
|
+
// the panel simply unregistered rather than throwing during activation.
|
|
329
|
+
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
330
|
+
name: 'settings.section',
|
|
331
|
+
id: 'dsh-os',
|
|
332
|
+
order: 140,
|
|
333
|
+
label: () => 'DSH OS',
|
|
334
|
+
}, SettingsSection))
|
|
335
|
+
},
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
})
|
package/cordis.patch.yml
CHANGED
|
@@ -22,6 +22,30 @@
|
|
|
22
22
|
scriptDir: src/host/bin
|
|
23
23
|
# Default timeout for a single verb invocation, in milliseconds.
|
|
24
24
|
timeoutMs: 120000
|
|
25
|
+
# The settings service the panel edits and the takeover reads. It is its own row because BOTH of the
|
|
26
|
+
# other two need it without owning it: the takeover calls `enabled()` on every intercepted tool call,
|
|
27
|
+
# and the routes the browser panel talks to mutate the same object.
|
|
28
|
+
#
|
|
29
|
+
# It comes BEFORE the takeover row on purpose. The takeover declares `inject: ['dsh-os-settings']`,
|
|
30
|
+
# which is a hard dependency: a row that loads first would simply stay inactive until the service
|
|
31
|
+
# appears, and an inactive interception looks exactly like a broken one.
|
|
32
|
+
- id: dsh-os-settings
|
|
33
|
+
name: deepseek-coordination/settings
|
|
34
|
+
config:
|
|
35
|
+
# Defaults to $DSH_HOME/dsh-os/settings.json, or <package root>/.dsh-os-settings.json when
|
|
36
|
+
# DSH_HOME is unset. That order matters: the installed package directory is replaced on upgrade,
|
|
37
|
+
# so a setting stored there would not survive one.
|
|
38
|
+
# settingsPath: /absolute/path/to/settings.json
|
|
39
|
+
# The interception itself. It answers the built-in `read`/`write`/`edit`/`pwsh` calls through
|
|
40
|
+
# dsh-os while the user has the switch on, and calls through to the built-in implementation for
|
|
41
|
+
# everything else — including on any failure, so takeover can never make a call unanswerable.
|
|
42
|
+
- id: dsh-os-takeover
|
|
43
|
+
name: deepseek-coordination/takeover
|
|
44
|
+
config:
|
|
45
|
+
timeoutMs: 120000
|
|
46
|
+
# Must match the `tool-fs` row's `readLimit`: the interception must not apply a different read
|
|
47
|
+
# window than the built-in tool it replaces.
|
|
48
|
+
readLimit: 2000
|
|
25
49
|
# The permission consent delegate: the ONLY writer of permission.json. It is a separate row because
|
|
26
50
|
# the scripts must never write the file - there is no write verb in the verb table - so the row
|
|
27
51
|
# that can write it is deliberately a distinct component with its own tools.
|
|
@@ -32,3 +56,19 @@
|
|
|
32
56
|
# durable decisions outside the installed package.
|
|
33
57
|
# permissionPath: /absolute/path/to/permission.json
|
|
34
58
|
auditLimit: 200
|
|
59
|
+
# The intent-first workflow: start -> clarify -> design -> options -> choose -> execute -> finish.
|
|
60
|
+
# Its own row because it is its own concern: the rows above make an OS call correct, and this one
|
|
61
|
+
# makes a request decided before it is executed. It registers the `workflow_plan` tool and a system-prompt
|
|
62
|
+
# section that is empty until a workflow is started, so a profile that never uses it pays nothing.
|
|
63
|
+
- id: dsh-os-workflow
|
|
64
|
+
name: deepseek-coordination/workflow
|
|
65
|
+
config:
|
|
66
|
+
# Defaults to $DSH_HOME/dsh-os/workflows, or <package root>/.dsh-os-workflows when DSH_HOME is
|
|
67
|
+
# unset. The state and every artifact are per session, under their own subdirectory.
|
|
68
|
+
# workflowRoot: /absolute/path/to/workflows
|
|
69
|
+
# Records each phase boundary in state.json, so a compaction that follows has a record of what it
|
|
70
|
+
# may drop. The structural half - detail to disk, one line back - is not optional and not
|
|
71
|
+
# configurable: it is the reason the feature saves anything.
|
|
72
|
+
autoCompact: true
|
|
73
|
+
# Checkpoint entries kept per session.
|
|
74
|
+
maxCheckpoints: 20
|