mingdao-harness 0.4.6 → 0.6.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.
Files changed (63) hide show
  1. package/README.md +51 -4
  2. package/docs/AUDIT-v0.4.6.md +77 -15
  3. package/docs/CHANGELOG-PACK.md +42 -0
  4. package/docs/CONFIG.md +67 -0
  5. package/docs/DEVELOPER.md +46 -0
  6. package/docs/HANDOVER.md +51 -0
  7. package/docs/MIGRATION-DEYI-v0.5.md +271 -0
  8. package/docs/PACK-API.md +65 -8
  9. package/docs/PLAN-v0.5.0.md +171 -0
  10. package/docs/PLAN-v0.6.0.md +228 -0
  11. package/docs/PROVIDERS.md +3 -0
  12. package/docs/RELEASE-CHECKLIST.md +211 -0
  13. package/docs/RELEASE-TRAIN.md +160 -0
  14. package/docs/ROADMAP-NEXT.md +2 -2
  15. package/install.ps1 +49 -14
  16. package/install.sh +58 -17
  17. package/package.json +2 -1
  18. package/packs/example-hello/pack.json +18 -0
  19. package/packs/example-hello/pack.mjs +47 -0
  20. package/packs/example-hello/prompts/domain.md +1 -0
  21. package/src/agent.js +337 -12
  22. package/src/atomic-write.js +6 -0
  23. package/src/batch.js +39 -2
  24. package/src/cachestats.js +68 -0
  25. package/src/cli.js +60 -60
  26. package/src/commands/key.js +24 -4
  27. package/src/commands/ledger.js +141 -0
  28. package/src/commands/net.js +86 -0
  29. package/src/commands/pack.js +196 -0
  30. package/src/commands/repl.js +5 -53
  31. package/src/commands/update.js +23 -2
  32. package/src/constraints.js +336 -0
  33. package/src/cost-guard.js +10 -2
  34. package/src/help.js +95 -0
  35. package/src/index.js +17 -0
  36. package/src/ledger.js +423 -0
  37. package/src/memory.js +13 -1
  38. package/src/models.js +29 -0
  39. package/src/net-guard.js +251 -0
  40. package/src/net-policy.js +129 -0
  41. package/src/packs.js +469 -0
  42. package/src/permissions.js +46 -11
  43. package/src/proc.js +101 -0
  44. package/src/prompts.js +26 -0
  45. package/src/providers/openai-compatible.js +8 -1
  46. package/src/replay.js +132 -0
  47. package/src/routing.js +2 -0
  48. package/src/schedule.js +83 -34
  49. package/src/skill-lib.js +25 -9
  50. package/src/skills.js +50 -7
  51. package/src/sync.js +49 -12
  52. package/src/tasks/worker.js +21 -6
  53. package/src/tasks.js +84 -8
  54. package/src/titles.js +3 -0
  55. package/src/ui.js +50 -10
  56. package/src/update.js +79 -6
  57. package/src/web/app.js +12 -0
  58. package/src/web/routes/api.js +4 -2
  59. package/src/web/routes/domains/config.js +19 -0
  60. package/src/web/routes/domains/sessions.js +19 -1
  61. package/src/web/routes/domains/workspace.js +44 -6
  62. package/src/web/server.js +49 -4
  63. package/src/workspace.js +54 -36
@@ -0,0 +1,271 @@
1
+ # Deyi-TCM-Harness 回迁指南(v0.5.0 → Pack API v1)
2
+
3
+ > **已按下游真实代码核对**(核对对象:`Deyi-TCM-Harness@c6b4397`,文件
4
+ > `layer/providers/dify.mjs`、`docs/ARCHITECTURE.md`、`README.md`)。
5
+ > 核对后更正了两处会误导回迁的地方:§3.1.1 补上「同名多命中」这条**模型之前的安全闸门**
6
+ > (初版只举了 `随访` 命令,把安全关键的那条漏了);§6 更正「执行账本不可用」的过期说法
7
+ > (v0.6.0 已交付)。指南里凡是标注「按真实代码核对」的结论,都来自读代码而不是推测。
8
+
9
+
10
+ > 面向:下游 Line B(中医垂域层,Linux 原机开发)
11
+ > 上游契约:`PACK-API.md`(v1 已冻结)· 变更史 `CHANGELOG-PACK.md`
12
+ > 触发:**v0.5.0 发布即回迁**(决策已确认)。目标:把 3 个域工具从 `providers/dify.mjs` 的 `chat()` 里搬出来。
13
+ > 纪律:下游**只通过扩展点接入,绝不修改上游源码**;内核 bug 在上游修。
14
+
15
+ ---
16
+
17
+ ## 一、为什么必须迁(迁移前的真实损失)
18
+
19
+ 当前 `Deyi-TCM-Harness/layer/providers/dify.mjs` 把整个中医域逻辑写进了 Provider 的 `chat()`。这是上游缺抽象导致的,不是下游的问题。具体损失:
20
+
21
+ | 应有能力 | 迁移前现状 |
22
+ | --- | --- |
23
+ | 权限引擎门控 | 域内「工具」是 `chat()` 里的正则匹配(`/^(回访\|随访)\s*(.*)$/`),绕过 `permissions.js` |
24
+ | 审计追溯 | 不写 `audit.jsonl`——无法回答「谁在何时读了哪位患者的病历」 |
25
+ | **费用与护栏** | 域内每次 DeepSeek 调用硬编码 `usage: { prompt_tokens: 0, completion_tokens: 0 }` → **完全不计费、不触发日费用护栏** |
26
+ | UI 工具卡片 / 流式 | 只能手工 `opts.onDelta` |
27
+ | 独立版本与兼容 | 整文件覆盖,无 `apiVersion`,无法 CI 校验 |
28
+ | 记忆 / 技能 / 预设复用 | 全部用不上 |
29
+
30
+ 迁移后这六项全部进入内核的既有链路。
31
+
32
+ ---
33
+
34
+ ## 二、目标结构
35
+
36
+ ```
37
+ Deyi-TCM-Harness/
38
+ layer/
39
+ packs/
40
+ tcm/
41
+ pack.json # manifest(apiVersion / engines / permissions / contributes / budget)
42
+ pack.mjs # createPack(ctx) → tools / constraints / promptSections
43
+ prompts/domain.md # 中医领域提示词(从 README 的职责描述落成正式文本)
44
+ constraints.json # 三条领域红线(也可内联在 pack.mjs)
45
+ examples/config.example.json
46
+ install.sh # 改为安装 packs/ 而不是 providers/
47
+ ```
48
+
49
+ `providers/dify.mjs` 里的**Dify 调用本身**保留为 Provider(协议适配是 Provider 的职责);
50
+ **三个域工具、四条流程、患者注册表、四态对比、回访看板**迁到 `pack-tcm`。
51
+
52
+ ---
53
+
54
+ ## 三、逐项迁移
55
+
56
+ ### 3.1 三个「命令式工具」→ 真工具
57
+
58
+ 迁移前(`chat()` 内字符串匹配,无权限、无审计、无卡片):
59
+
60
+ ```js
61
+ const fu = /^(回访|随访)\s*(.*)$/.exec(query);
62
+ if (fu) { /* …直接产出文本,手工 opts.onDelta… */ }
63
+ ```
64
+
65
+ 迁移后:
66
+
67
+ ```js
68
+ // packs/tcm/pack.mjs
69
+ export function createPack(ctx) {
70
+ return {
71
+ tools: [
72
+ { name: 'intake_collect', description: '首诊十问采集与病历落盘(缺项必须继续追问)', parameters: {…}, readOnly: false, run: intakeCollect },
73
+ { name: 'visit_compare', description: '复诊四态对比(消失/减轻/无变化/加重,只陈述事实)', parameters: {…}, readOnly: false, run: visitCompare },
74
+ { name: 'followup_board', description: '回访看板与单患者随访(趋势 + 预警 + 话术草稿)', parameters: {…}, readOnly: true, run: followupBoard },
75
+ ],
76
+ // …
77
+ };
78
+ }
79
+ ```
80
+
81
+ 注册后内核自动加前缀:`pack__tcm__intake_collect` 等,与内置工具走**同一条**权限 / 审计 / schema 瘦身 / 费用链路。
82
+
83
+ #### 3.1.1 ⚠ 两条「模型之前」的短路,不能当成普通工具直接搬
84
+
85
+ > 本节是按**下游真实代码**核对后补写的(核对对象:`Deyi-TCM-Harness@c6b4397`,
86
+ > 文件 `layer/providers/dify.mjs` 的 `chat()`)。初版指南只举了 `随访` 一条,
87
+ > 漏掉了下面第 2 条——而它才是安全关键的那条。
88
+
89
+ 下游的 `chat()` 里有**两处**在调用模型**之前**就返回的短路分支:
90
+
91
+ | # | 触发 | 现状行为 | 直接改成工具会怎样 |
92
+ | --- | --- | --- | --- |
93
+ | 1 | `^(回访\|随访)\s*(.*)$` | 不调用 Dify、不走问诊,直接产出看板/随访话术(`usage: {0,0}`) | 多一次模型往返(延迟与 token 开销)。**若在意这点**:保留在下游 Provider 里是允许的——`<home>/providers/` 本就是下游的自留地(见 §6) |
94
+ | 2 | **同名多命中** `match.ambiguous` | **直接返回候选列表、请医师确认,不落盘、不问诊**(`dify.mjs` 中 `if (match.ambiguous)` 分支) | ⚠ **这是安全降级**:现在的语义是「**Provider 在模型之前就拒绝继续**」,改成普通工具后变成「模型自行决定要不要先问一句」。对「同名多命中不静默合并、避免混病历」这条医疗安全属性,**把决定权从内核交给模型是不可接受的** |
95
+
96
+ 第 2 条的正确迁移方式不是「做成工具」,而是**用约束引擎把它升级为内核强制**(这正是 v0.5.0 约束确定性存在的意义):
97
+
98
+ ```js
99
+ // packs/tcm/pack.mjs 的 constraints —— 让「没有确认病历号就不得写入」由内核强制,
100
+ // 且对**所有**写类工具生效,不依赖模型自觉、可审计、可被测试阻断。
101
+ constraints: [
102
+ { id: 'must-have-patient', kind: 'tool-arg-require', tool: 'intake_collect', requireArg: 'patientId' },
103
+ { id: 'must-have-patient', kind: 'tool-arg-require', tool: 'visit_compare', requireArg: 'patientId' },
104
+ // 采集缺项照样由 completeness 兜住(见 §3.2)
105
+ ]
106
+ ```
107
+
108
+ 这样得到的性质**比现状更强**:现状只在「同名多命中」这一种情况下拒绝,而 `tool-arg-require` 是
109
+ 「**任何**没有确认病历号的写入都拒绝」——把「避免混病历」从一条 if 分支变成一条内核不变量,
110
+ 并且进审计、可回放(§6 的账本已可用)。
111
+
112
+ ### 3.2 三条红线 → 约束引擎(从提示词升级为内核强制)
113
+
114
+ | 现有红线(写在 prompt 里) | 迁移后的约束声明 |
115
+ | --- | --- |
116
+ | 缺项绝不编造 | `{ id:'ten-questions', kind:'completeness', tool:'intake_collect', fields:['zhushu','zhenduan','hanre','han','toushen','erbian','yinshi','xiongfu','kouke','jiubing'], onMissing:'reject' }` |
117
+ | 不输出诊疗结论 | `{ id:'no-diagnosis', kind:'output-forbid', pattern:'有效\|好转\|治愈\|确诊为', action:'block-and-rewrite' }` |
118
+ | 不得跨患者串病历 | `{ id:'no-cross-patient', kind:'tool-arg-require', tool:'intake_collect', requireArg:'patientId' }` |
119
+
120
+ 执行时机:调用工具前(工具/参数)、工具返回后(缺项则**拒绝该结果**并回填「请继续采集」)、正文输出前(**回填会话历史之前**改写/拦截)。命中写审计事件。
121
+
122
+ > 注意:`output-forbid` 的 `block` / `block-and-rewrite` **不回显**命中的措辞——回显会把违规表述重新写进正文与历史。
123
+
124
+ ### 3.3 域内模型调用 → `ctx.llm()`
125
+
126
+ 迁移前(费用隐身):
127
+
128
+ ```js
129
+ const res = await fetch(`${deepseekBaseUrl}/chat/completions`, { … });
130
+ return { text, usage: { prompt_tokens: 0, completion_tokens: 0 } }; // ← 这笔钱谁也看不到
131
+ ```
132
+
133
+ 迁移后:
134
+
135
+ ```js
136
+ async function deepseekJson(system, user, maxTokens = 2000) {
137
+ const r = await ctx.llm({ model: 'deepseek-v4-flash', system, user, maxTokens, json: true, purpose: 'patient-extract' });
138
+ return r.data; // json:true 时内核已解析
139
+ }
140
+ ```
141
+
142
+ 收益:usage 并入当前回合 → 今日费用 / 缓存命中率 / 峰谷 / 日费用护栏**同时生效**;并写一条 Pack 归因记录(`cost=null` 标记,与回合级总账不重复计费),`mingdao cost --by pack` 可见。
143
+
144
+ `purpose` 建议取值:`patient-extract`(患者识别)、`intake-extract`(结构化落盘)、`visit-compare`(四态对比)、`followup-script`(随访话术)。
145
+
146
+ #### 3.3.1 逐参数核对结论(`deepseekJson` → `ctx.llm()`)
147
+
148
+ > 按下游真实代码逐参数核对过(`dify.mjs` 的 `deepseekJson`),**可以等价替换**:
149
+
150
+ | 下游 `deepseekJson` 的线上参数 | `ctx.llm()` 的写法 | 核对结果 |
151
+ | --- | --- | --- |
152
+ | `model: 'deepseek-v4-flash'`(硬编码) | `model` | ✅ |
153
+ | `temperature: 0` | `temperature: 0` | ✅ 支持(缺省回落到会话温度) |
154
+ | `max_tokens: maxTokens` | `maxTokens` | ✅(缺省 2048,受模型输出上限封顶) |
155
+ | **`thinking: { type: 'disabled' }`** | `reasoningEffort: 'off'` | ✅ **线上参数完全一致**——内核在 `reasoningEffort==='off'` 时正是发 `thinking:{type:'disabled'}` |
156
+ | 自己 `indexOf('{')…lastIndexOf('}')` 再 `JSON.parse` | `json: true` | ✅ 内核 `ctx.llm` 用**同一套**花括号切片解析,结果放在 `data`;解析失败为 `null` 而**不抛错**(与下游返回 `null` 同语义) |
157
+
158
+ 一个必须注意的差异:下游 `deepseekJson` 用 `deepseekKey` 直连,**usage 不入账**(这正是要迁的原因);
159
+ 改走 `ctx.llm()` 后 usage 并入当前回合 → 今日费用、缓存命中、峰谷、日护栏同时生效。
160
+
161
+ #### 3.3.2 密钥从哪来(一个必读的坑)
162
+
163
+ `ctx.llm()` 解析密钥走**内核**,顺序是(`src/credentials.js` 的 `resolveApiKey`,已按代码核对):
164
+
165
+ 1. `process.env[<服务商的 envKey>]`(DeepSeek 即 `DEEPSEEK_API_KEY`)
166
+ 2. **`process.env.MINGDAO_API_KEY`**(通用兜底)
167
+ 3. 凭证库 `<MINGDAO_HOME>/credentials.json` 里以**服务商名**为键的字段(如 `deepseek`)
168
+ 4. `cfg.apiKey`
169
+
170
+ 好消息:第 3 步读的就是下游现在写的那**同一个文件、同一个字段**(下游 `dify.mjs` 的
171
+ `creds.deepseek`)——只要 `MINGDAO_HOME` 是同一个目录,迁移后**不需要搬密钥**。
172
+
173
+ > ⚠ **坑(已实测复现)**:第 2 步的 `MINGDAO_API_KEY` **优先于**第 3 步。
174
+ > 机器上只要残留一个指向别的服务商的 `MINGDAO_API_KEY`,域内调用就会带着**那个** key
175
+ > 打到 DeepSeek 端点(或反之)——表现为 401 或「费用算到莫名其妙的账上」。
176
+ > 回迁时请检查环境变量:`env | grep -E 'MINGDAO_API_KEY|DEEPSEEK_API_KEY'`。
177
+
178
+ 另一个前置条件:`ctx.llm({ model })` 里的模型名必须是内核**能解析**的——
179
+ 要么是内置名(`deepseek-v4-flash`),要么在上游 `config.json` 的 `customModels` 里声明过
180
+ (含 `baseUrl`)。否则 `ctx.llm` 直接抛「未配置 API Key」,而下游原来的 `deepseekJson`
181
+ 是自己硬编码 URL 的、不会有这个问题。推荐显式写入凭证库:
182
+
183
+ ```bash
184
+ mingdao key set deepseek <你的 DeepSeek Key> # 写入 <MINGDAO_HOME>/credentials.json
185
+ ```
186
+
187
+ ### 3.4 患者注册表与快照落盘 → 受权限约束的 IO
188
+
189
+ `patients.json` / `intake/**` 的读写改用 `permissions.fs` 声明的路径(内核据此校验越界):
190
+
191
+ ```json
192
+ "permissions": { "fs": ["<MINGDAO_HOME>/patients.json", "<MINGDAO_HOME>/intake/**"] }
193
+ ```
194
+
195
+ 同时建议把 `saveRegistry` 的 `writeFileSync` 换成上游的原子写语义(避免崩溃时半写)——这是**下游自己的代码**,上游只提供契约。
196
+
197
+ ### 3.5 领域提示词 → `promptSections`
198
+
199
+ 把现在散在 `chat()` 里的角色/边界说明提炼成 `prompts/domain.md`,以 `promptSections` 注入:
200
+
201
+ ```json
202
+ "promptSections": ["prompts/domain.md"]
203
+ ```
204
+
205
+ 内核按 `order` + `pack/id` **确定性排序**,字节稳定 → 不破坏 DeepSeek 前缀缓存。
206
+
207
+ ---
208
+
209
+ ## 四、迁移步骤(建议顺序)
210
+
211
+ 1. `mingdao pack new tcm`(在 Deyi 仓库里生成 `packs/tcm/` 脚手架);
212
+ 2. 把 `dify.mjs` 的 `intakeCollect` / `visitCompare` / `followupDashboard` / `patientFollowup` 搬到 `pack.mjs`,
213
+ 函数体基本不动,只把「读凭据 + 直接 fetch」换成 `ctx.llm` + `ctx` 提供的只读信息;
214
+ 3. 把患者的 JSON 读写挂到 `permissions.fs` 声明的路径;
215
+ 4. 把三条红线写成 `constraints`;
216
+ 5. 把领域提示词抽成 `prompts/domain.md`;
217
+ 6. `layer/providers/dify.mjs` 只保留 **Dify 协议适配**(`createProvider` → `chat`),
218
+ 域逻辑全部移出;`install.sh` 改为安装 `layer/packs/` 到 `$MINGDAO_HOME/packs/`;
219
+ 7. `mingdao pack verify ./layer/packs/tcm` 必须退出 0;
220
+ 8. 在 Deyi CI 里加:`mingdao pack verify ./layer/packs/tcm`。
221
+
222
+ **验收(DoD)**:
223
+ - 域内每次 DeepSeek 调用都出现在 `mingdao cost --by pack` 里;
224
+ - 三条红线可被测试**阻断**(缺项 / 结论性措辞 / 缺 patientId 各一条断言);
225
+ - 三个工具在 WebUI 显示为正常工具卡片;
226
+ - 同名多命中仍返回候选列表(不静默合并)——行为不回归。
227
+
228
+ ---
229
+
230
+ ## 五、兼容性与版本
231
+
232
+ | 项 | 约定 |
233
+ | --- | --- |
234
+ | 声明 | `pack.json` 写 `apiVersion: 1` + `engines.mingdao: ">=0.5 <0.7"` |
235
+ | 支持窗口 | 上游承诺支持最近 2 个 minor(0.5 / 0.6 支持 v1) |
236
+ | 上游变更 | 任何 Pack API 变更同步更新 `PACK-API.md` + `CHANGELOG-PACK.md` + 兼容性矩阵 |
237
+ | 下游义务 | 每个上游 minor 发布后跑一次 `pack verify`,并入 CI |
238
+ | 破坏性变更 | 走 major + 迁移指南(如可行再配 codemod) |
239
+
240
+ **内核 bug 一律在上游修**:下游遇到的问题如果是「扩展点不够用/行为不对」,直接反馈上游,
241
+ 不要在下游 fork 内核——这条边界是上下游能长期并行的前提。
242
+
243
+ ---
244
+
245
+ ## 六、上游能力现状(回迁时按此判断能依赖什么)
246
+
247
+ > 本节已按 **v0.6.0** 实际交付情况更正。初版把「执行账本导出/回放」也列进了缺口,
248
+ > 但它在 v0.6.0 已经落地——照旧文办事会让下游白做一套替代方案。
249
+
250
+ **已可用(可以依赖)**
251
+
252
+ - **执行账本与决策回放(v0.6.0 确定性③)**:`mingdao ledger list/show/export/verify/replay`。
253
+ 对下游的意义:域内模型调用(经 `ctx.llm()`)与约束触发都会进账本,
254
+ 「这次结论是怎么来的」可离线回答;`ledger export` 脱敏后可交第三方复核。
255
+ 回放还能当**下游 CI 门禁**:`now-blocked > 0` 时退出码为 1。
256
+ - `permissions.fs` 显式路径 + `completeness` / `tool-deny` / `tool-arg-require` / `arg-forbid` /
257
+ `output-forbid` / `result-forbid`(后两者见 `PACK-API.md §4`,`result-forbid` 于 v0.6.0 补齐实现)。
258
+ - 出网白名单 `config.net` + `mingdao net report`(**注意边界**:只覆盖内核经 HTTP 出口与自更新
259
+ 联系的目标,不覆盖 MCP 服务器、以及工具自己起的子进程——见 `CONFIG.md` 出网白名单一节)。
260
+
261
+ **仍缺(请勿依赖)**
262
+
263
+ - `ctx.storage`:Pack 私有持久化命名空间(当前用 `permissions.fs` 显式路径替代);
264
+ - `ctx.provider`:由 Pack 贡献非 OpenAI 兼容 Provider(当前 Dify 适配仍放 `<home>/providers/`);
265
+ - **Pack 私有存储加密(医疗 PII)**:**不在 v0.6.0**(初版写「→ v0.6.0」是不准确的,特此更正)。
266
+ 下游若有 PII 落盘加密要求,请在上层自行处理(如加密文件系统 / 应用层加密);
267
+ - 账本的**可选签名**(`--sign-key`)未实现:当前只有哈希链完整性校验,
268
+ **不含可信时间戳**,不要当成审计级不可否认。
269
+
270
+ **能力缺口登记(契约里列了但没实现,别照文档写)**:`require-citation` 约束 kind、
271
+ `mingdao constraint test <pack>` —— 见 `PACK-API.md §4.1`。
package/docs/PACK-API.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # 垂域 Pack API v1(草案)
2
2
 
3
- > 状态:**草案 / 待实现**(目标版本 v0.5.0)。本文是上游与下游之间的接口契约。
3
+ > 状态:**v1 已冻结(2026-09-11,决策已确认)**,随 v0.5.0 发布生效。本文是上游与下游之间的接口契约。
4
+ > 变更纪律:v1 冻结后,minor 版本只增不改;任何改动必须同步更新本文 + `CHANGELOG-PACK.md` + 兼容性矩阵。
4
5
  > 定位:让垂域团队(中医、法律、教育、制造、政务…)**不修改内核源码**就能做出可私有化、可审计、受约束的智能体。
5
6
  > 关联:`STRATEGY-0.5.md`(战略)、`DEVELOPER.md`(现有扩展点)、`CONFIG.md`(配置)。
6
7
 
@@ -71,6 +72,11 @@
71
72
  "env": ["DIFY_API_KEY"]
72
73
  },
73
74
 
75
+ "budget": { // 可选:Pack 级预算(v0.5.0 A4.5)
76
+ "dailyYuan": 20, // 该 Pack 当日模型调用费用上限(元);经 ctx.llm 归因统计
77
+ "action": "block" // 超限处理:block(阻止调用,默认)/ warn(放行并提示)
78
+ },
79
+
74
80
  "contributes": {
75
81
  "tools": true, // 由 pack.mjs 提供
76
82
  "provider": "dify", // 复用/覆盖 Provider 名
@@ -168,11 +174,44 @@ export function createPack(ctx) {
168
174
  "matched": "好转", "session": "…", "model": "deepseek-v4-flash" }
169
175
  ```
170
176
 
177
+ **字段要求(v0.6.0 起在装载时强制校验,写错不会静默失效)**
178
+
179
+ | kind | 必填 | 说明 |
180
+ | --- | --- | --- |
181
+ | `tool-deny` | `tool` | |
182
+ | `tool-arg-require` | `tool`、`requireArg` | |
183
+ | `arg-forbid` | `tool`、`arg`、`pattern` | `pattern` 必须是**合法且非空**的正则 |
184
+ | `output-forbid` | `pattern`、`action` | 同上 |
185
+ | `result-forbid` | `tool`、`pattern` | 同上 |
186
+ | `completeness` | `tool`、`fields[]` | |
187
+
188
+ **pattern 写错会怎样(这一条值得单独读)**:v0.5.0 的行为是**静默放行**——
189
+ `arg-forbid` 给一个非法正则时求值失败、`re` 为 null,红线**永不命中**且不进 `invalid` 列表,
190
+ 作者以为自己有红线、实际没有;给一个缺失的 pattern 则退化成 `new RegExp('')`(匹配一切),
191
+ 比本意严得多。这与「fail-closed」的原则直接矛盾。v0.6.0 起:
192
+
193
+ 1. **装载即拒绝**:`pack.json`/`pack.mjs` 里 pattern 类约束的 pattern 缺失或非法 → Pack 装载失败
194
+ 并给出可操作的错误(`pack verify` 是下游 CI 门禁,因此拼写错误在 CI 就会被拦下);
195
+ 2. **运行期 fail-closed**:作用域限于某工具的 `arg-forbid` / `result-forbid` 若 pattern 仍然坏掉
196
+ (例如绕过装载校验直接传约束),只阻断**那个工具**并明说「配置有误」,而不是放行、也不是拦下全部;
197
+ 3. `output-forbid` 的 pattern 坏掉时不进生效集合(否则「一个正则写错」会变成「整个会话无法输出」),
198
+ 但会被计入 `invalid` 并在装载/校验时报告。
199
+
171
200
  **设计原则**
172
201
  - 约束**只能收紧、不能放松**权限(约束不授予任何权限);
173
202
  - 约束失败**默认 fail-closed**(拿不准就阻断并提示),与 hooks 同口径;
174
203
  - 每条约束必须有 `id`,便于审计与测试;
175
- - 提供 `mingdao constraint test <pack>`:对每条约束跑一遍内置反例样本(下游 CI 用)。
204
+ - 约束 kind 的**唯一来源**是引擎(`src/constraints.js` 的 `KINDS`)。此前 `packs.js` 另有一份副本,
205
+ 已经真实漂移并导致下游照契约写的 `result-forbid` 被判「kind 非法」而整包装载失败。
206
+
207
+ **§4.1 已知缺口(v1 契约中列出、但尚未实现——请勿依赖)**
208
+
209
+ | 项 | 状态 |
210
+ | --- | --- |
211
+ | `require-citation`(输出前 kind) | ❌ **未实现**。当前 `KINDS` 中没有它,写进 Pack 会被判 kind 非法。需要先定「什么算引用来源」的规格(与下游一起定),故不在此处擅自实现 |
212
+ | `mingdao constraint test <pack>` | ❌ **未实现**。目前请用 `pack verify`(静态校验)+ 自己写的反例测试 |
213
+
214
+ > 把缺口写在契约里而不是留在文档里当承诺:下游按本文档迁移时,能一眼看到哪些不能依赖。
176
215
 
177
216
  ---
178
217
 
@@ -186,7 +225,7 @@ const out = await ctx.llm({
186
225
  system: '…', user: '…',
187
226
  maxTokens: 2000,
188
227
  reasoningEffort: 'off', // 复用内核的模型能力表与参数校验
189
- json: true, // 结构化输出(内核负责解析与重试)
228
+ json: true, // 结构化输出:内核发 response_format=json_object,并把回复解析到 data
190
229
  purpose: 'patient-extract', // 归因标签(进账本与分账)
191
230
  });
192
231
  // → { text, data, usage: { prompt_tokens, completion_tokens, prompt_cache_hit_tokens, … } }
@@ -201,8 +240,22 @@ const out = await ctx.llm({
201
240
 
202
241
  > 这一条直接修复「域内调用 `usage: 0`」问题——**Pack 内不可能再有隐身花费**。
203
242
 
243
+ ### Pack 级预算
244
+
245
+ manifest 可声明 `budget.dailyYuan` + `budget.action`。超限时 `ctx.llm()` 在**发起调用前**拦截
246
+ (`action: block`)或放行并告警(`action: warn`),粒度到 Pack 而非整个进程——
247
+ 垂域团队能为自己包住的模型调用单独设上限,不必与宿主的日费用护栏争额度。
248
+ 查看某 Pack 今日花费:`mingdao cost --by pack`。
249
+
204
250
  ---
205
251
 
252
+ **`json: true` 的确切语义(v0.6.0 更正)**:内核会带上 `response_format: {type:'json_object'}`,
253
+ 并把回复解析到 `data`——解析方式与下游 `deepseekJson` 同款:取文本里**第一个 `{` 到最后一个 `}`**
254
+ 再 `JSON.parse`。两点请注意:
255
+ - **不重试**(此前文档写「负责解析与重试」,重试并不存在);
256
+ - 解析失败时 `data` 为 **null**,**不抛错**——因此调用方自己判断 `data` 是否为空,
257
+ 不要把「拿不到结构化结果」当成异常路径。
258
+
206
259
  ## 6. 兼容性与版本策略
207
260
 
208
261
  | 内核版本 | Pack API | 承诺 |
@@ -242,9 +295,13 @@ mingdao constraint test <name> # 单独跑约束反例
242
295
 
243
296
  ---
244
297
 
245
- ## 9. 待定问题(实现前需拍板)
298
+ ## 9. 关键决策(✅ 已拍板 2026-09-11,v1 冻结)
299
+
300
+ 1. **Pack 不得覆盖内置同名 Provider**——只允许新增。理由:内置 Provider 名称是内核契约的一部分,允许覆盖会让「内核行为」变成 Pack 可劫持的对象,安全与可预测性都受损。同名冲突在 `pack verify` 与加载时都会被拒绝并给出明确原因。
301
+ 2. **`block-and-rewrite` 的修正请求计费归 Pack**。理由:修正是 Pack 的约束触发的额外开销,归到 Pack 才能让垂域团队看到「自己的红线花了多少钱」,也让 Pack 级预算真实反映其成本。
302
+ 3. **Pack 内 `fetch` 允许,但必须**:① 目标在 `permissions.net` 白名单内;② 调用强制入账(`purpose` 标记)并可审计;③ `mingdao pack verify` 对「直连模型端点」的 `fetch` 给出静态告警(引导改用 `ctx.llm()`)。理由:部分垂域必须直连业务系统(HIS/ERP),一刀切禁止会把 Pack 逼回 Provider 里写 `chat()` 的老路——那正是本设计要消灭的形态。
303
+
304
+ ### 仍未定(不阻塞 v1,随 v0.5.x 落地)
246
305
 
247
- 1. **Pack 是否允许声明式 `provider` 覆盖内置同名 Provider?**(当前倾向:不允许覆盖内置,只允许新增)
248
- 2. **约束 `output-forbid` 的 `block-and-rewrite` 计费归属**:修正请求算 Pack 的费用还是内核的?(倾向:算 Pack)
249
- 3. **Pack 内 `fetch` 一律禁止,还是允许但需在 `permissions.net` 白名单内并强制入账?**(倾向:允许 + 白名单 + 入账,因为部分域需要直连业务系统)
250
- 4. **Pack 私有存储是否加密**(医疗/法律 PII)?v1 先不做,v0.6 「合规与确定性」阶段再定。
306
+ - Pack 私有存储是否加密(医疗/法律 PII)→ **v0.6.0「合规与确定性」阶段决定**。
307
+ - `npm:` 来源的离线缓存策略(内网无 npm 时的分发)→ v0.6.0 与离线安装包一并设计。
@@ -0,0 +1,171 @@
1
+ # PLAN-v0.5.0「垂域 Pack 契约」— 落地计划
2
+
3
+ > 依据:`STRATEGY-0.5.md` §七(阶段 A)+ §十(✅ 已确认 2026-09-11)。
4
+ > 决策:v0.5 只做 **确定性① 约束确定性 + ② 成本确定性**;③ 行为确定性(执行账本/可回放/合规导出)**放 v0.6.0**。
5
+ > 契约:接口定义见 `PACK-API.md`(v1 冻结目标)。
6
+ > 纪律:每步可独立验证;全绿门禁(6 套测试 + strict 0/0 + tsc 0)+ 新增回归断言;**v0.5.0 发布即触发 Deyi 回迁**。
7
+
8
+ ---
9
+
10
+ ## 〇、进度(滚动更新)
11
+
12
+ | 阶段 | 状态 | 产出 |
13
+ | --- | --- | --- |
14
+ | A0 契约冻结 | ✅ 完成 | `PACK-API.md` v1 冻结 + 三条决策拍板;`docs/CHANGELOG-PACK.md`(v1 条目) |
15
+ | A1 Pack 加载器 | ✅ 完成 | `src/packs.js`:三级遮蔽发现 / manifest 严格校验 / 极简 semver(npm 语义)/ 工具注册 / 坏 Pack 只告警 / 幂等挂载 |
16
+ | A2 CLI | ✅ 完成 | `src/commands/pack.js`:`list / verify / new / info`;`verify` 即下游 CI 门禁 |
17
+ | A3 约束引擎 | ✅ 完成(含接线) | `src/constraints.js` 三时机 + 6 kind + fail-closed;**已接入 agent**:PreToolUse(工具/参数)、PostToolUse(缺项拒绝结果)、输出前(回填历史之前改写/拦截);约束事件写审计;CLI/WebUI/worker 启动各挂载一次 |
18
+ | A4 成本确定性 | ✅ 完成 | `ctx.llm()` 统一模型出口(usage 并入当前回合 → 今日费用/缓存/峰谷/护栏全部生效);**Pack 归因记录**(cost=null 标记,不与回合级重复计费);`mingdao cost --by pack`;`pack verify` 对「自建模型调用」的静态告警(A4.6);**Pack 级预算**(A4.5:`budget.dailyYuan` + action,调用前拦截) |
19
+ | A5 提示词段 | ✅ 完成 | `buildSystemPrompt` 注入 `<pack_rules>`(按 order + pack/id 确定性排序,字节稳定不破坏前缀缓存) |
20
+ | A6 文档 | ✅ 完成 | `packs/example-hello/` 示例 + `PACK-API.md` + `CHANGELOG-PACK.md`;README「垂域 Pack」小节 + `DEVELOPER.md` Pack 章节(含最小 pack.mjs 与三条要点) |
21
+ | A7 发布 | ✅ 完成 | 版本 0.5.0;CHANGELOG + `RELEASE-NOTES-0.5.0.md`;tag `v0.5.0` |
22
+ | A7 下游回迁 | ⏳ 待下游执行 | 上游已交付 `docs/MIGRATION-DEYI-v0.5.md`;回迁在 Linux 原机进行 |
23
+
24
+ **已验证的端到端行为**(均有回归断言):
25
+
26
+ - `pack verify/list/info/new` 四条命令可用;`example-hello` 工具进 schema 且可执行;坏 Pack 只告警不崩启动;重复挂载幂等;
27
+ - 约束三时机在真实 agent 循环里生效:`tool-deny` 拦住已获权限的工具、`completeness` 缺项时**原始结果不进入模型上下文**、输出命中红线时在**回填会话历史之前**改写/拦截(否则违规措辞会被当既成事实喂回);
28
+ - 零约束时三处检查点全部惰性,正文原样返回(对既有行为零影响);
29
+ - `ctx.llm` 的子调用 usage **并入父回合**(实测 1000+500+1000=2500 prompt),并在账本留下 Pack 归因记录而不重复计费;`mingdao cost --by pack` 可见。
30
+
31
+ 断言规模:smoke 83 → **86 组**;6/6 套测试全绿;tsc 0 错误;strict 0/0。
32
+
33
+ **下一步**:
34
+ 0. **v0.6.0「合规与确定性」已立项**:落地计划见 `PLAN-v0.6.0.md`(确定性③:执行账本 / 决策回放 /
35
+ 出网白名单自证 / 离线安装)。上游在等待下游回迁反馈的窗口期,先推进这条不依赖下游的线。
36
+ 1. **下游执行回迁**(Linux 原机):按 `MIGRATION-DEYI-v0.5.md` 把 3 个域工具搬进 `pack-tcm`,
37
+ `pack verify` 进下游 CI;回迁中遇到的每个「别扭点」都反馈上游当契约缺陷修(dogfooding)。
38
+ 2. **v0.5.x**:Pack API 按回迁反馈做兼容性加固(minor 只增不改)。
39
+ 3. **v0.6.0「合规与确定性」**:确定性③——执行账本(脱敏可导出)+ 可回放 + 离线/内网安装包 + 出网白名单自证。
40
+
41
+ ---
42
+
43
+ ## 一、目标(一句话)
44
+
45
+ 让一个垂域团队(以中医为第一个参考实现)**不改内核源码**,就能把领域工具、领域红线、领域提示词、领域费用接进内核,并让它们全部进入权限 / 审计 / 费用 / UI 链路。
46
+
47
+ **验收的硬标准**:Deyi 的 3 个域工具从 `providers/dify.mjs` 的 `chat()` 里搬进 `pack-tcm`,且:
48
+ - 每次域内模型调用都出现在 `mingdao cost report` 里(当前硬编码 `usage: 0`,完全隐身);
49
+ - 中医三条红线由内核强制并可被测试阻断(当前只是提示词里的一句话);
50
+ - 域工具在 WebUI 里显示为正常工具卡片(当前只能在 `chat()` 里手工 `onDelta`)。
51
+
52
+ ---
53
+
54
+ ## 二、交付物与顺序
55
+
56
+ ### A0. 契约冻结(0.5 天)
57
+
58
+ | 项 | 内容 | 验收 |
59
+ | --- | --- | --- |
60
+ | A0.1 | 定稿 `PACK-API.md` 三个待定问题(§9):内置 Provider 不可覆盖 / `block-and-rewrite` 计费归 Pack / Pack 内 `fetch` 允许但需白名单+入账 | 文档更新并标注「v1 冻结」 |
61
+ | A0.2 | 新增 `docs/CHANGELOG-PACK.md`(Pack API 变更日志,从 v1 起) | 文件存在且有 v1 条目 |
62
+
63
+ ### A1. Pack 加载器 + manifest(2 天)
64
+
65
+ | 项 | 内容 | 验收 |
66
+ | --- | --- | --- |
67
+ | A1.1 | 新增 `src/packs.js`:`listPacks()` / `loadPack(dir)` / `validateManifest()` / `mountPacks(cfg)` | 单元断言:合法 manifest 通过、非法字段/版本不匹配/保留名被拒 |
68
+ | A1.2 | 三级遮蔽:`<项目>/.mingdao/packs/` > `<MINGDAO_HOME>/packs/` > 内置 `packs/` | 同名时高优先级胜出(断言) |
69
+ | A1.3 | `config.packs` 声明式加载(本地目录 / `npm:` / `https:` 归档 + sha256) | 三种来源各一条断言;坏 Pack 不阻塞启动(回归断言) |
70
+ | A1.4 | 工具注册进既有注册表:命名 `pack__<pack>__<tool>`,走 `registerTool` | `buildToolSchemas` 能看到;权限/审计链路复用(断言) |
71
+ | A1.5 | `permissions` 最小权限校验(fs 白名单越界即拒、net 白名单) | 越界 manifest 被拒(断言) |
72
+
73
+ ### A2. CLI(1 天)
74
+
75
+ | 命令 | 行为 | 验收 |
76
+ | --- | --- | --- |
77
+ | `mingdao pack list` | 已加载 Pack / 来源 / 版本 / 兼容状态 | 输出含名称与 apiVersion |
78
+ | `mingdao pack verify <dir>` | 静态校验:manifest + 文件齐全 + 约束合法 + 权限不越界 | 合法 Pack 退出 0;坏 Pack 退出非 0 **并给出可操作原因**(下游 CI 门禁) |
79
+ | `mingdao pack new <name>` | 脚手架(pack.json + pack.mjs + 示例工具 + 一条约束) | 生成的目录 `pack verify` 通过 |
80
+ | `mingdao pack info <name>` | 贡献面 + 费用统计(A4 完成后) | — |
81
+ | `mingdao pack test <name>` | 跑内置反例样本(约束 + 工具契约) | — |
82
+
83
+ ### A3. 约束引擎 v1(3 天,**本阶段核心**)
84
+
85
+ | 项 | 内容 | 验收 |
86
+ | --- | --- | --- |
87
+ | A3.1 | 约束声明解析 + 校验(`kind` ∈ tool-deny / tool-arg-require / arg-forbid / output-forbid / completeness / confirm) | 非法 kind 被 `pack verify` 拒绝 |
88
+ | A3.2 | **PreToolUse 时机**:`tool-deny` / `tool-arg-require` / `arg-forbid` → 阻止执行并回填错误给模型 | 每条 kind 一条阻断断言 |
89
+ | A3.3 | **PostToolUse 时机**:`completeness`(缺项则拒绝该工具结果并要求补采)/ `result-forbid` | 缺项场景断言:工具结果被拒、模型被要求补采 |
90
+ | A3.4 | **输出前时机**:`output-forbid` + `action` ∈ block / block-and-rewrite / warn | 命中「好转/治愈」时按 action 处理(断言) |
91
+ | A3.5 | 约束事件进审计(`pack`/`constraint`/`kind`/`stage`/`action`/`matched`) | 审计文件含该结构(断言) |
92
+ | A3.6 | 约束**只能收紧不能放松**权限(约束不授予任何权限) | 声明了约束也不改变权限判定(断言) |
93
+ | A3.7 | 失败 fail-closed | 约束求值异常时按阻断处理(断言) |
94
+
95
+ ### A4. 成本确定性(2.5 天)
96
+
97
+ | 项 | 内容 | 验收 |
98
+ | --- | --- | --- |
99
+ | A4.1 | `ctx.llm(opts)`:统一模型出口(复用 Provider 重试/超时/能力表;支持 `json:true`、`purpose` 标签) | Pack 内调用一次,`usage` 非 0(断言) |
100
+ | A4.2 | usage 自动并入当前回合 → 今日费用 / 缓存命中 / 峰谷 / 日护栏全部生效 | 记录前后 `todayCost()` 增加(断言) |
101
+ | A4.3 | 四维归因账本:`(pack, tool, purpose, model, session)` | `cost report --by pack` 可见 |
102
+ | A4.4 | `mingdao cost report --by pack` / WebUI 仪表盘加 Pack 维度 | 报告含 Pack 分账表 |
103
+ | A4.5 | Pack 级预算(日/任务)与护栏 action 联动 | 超预算按 action 处理(断言) |
104
+ | A4.6 | 禁止 Pack 内直连模型接口绕过(评审用:`pack verify` 静态告警 `fetch` 到模型端点) | 告警可复现 |
105
+
106
+ ### A5. 提示词段与分离(1 天)
107
+
108
+ | 项 | 内容 | 验收 |
109
+ | --- | --- | --- |
110
+ | A5.1 | `promptSections`(id + order + content)注入系统提示,位置在预设/记忆/技能之后 | 系统提示含该段(断言) |
111
+ | A5.2 | 快照语义:Pack 提示词段在会话内不变(不破坏前缀缓存) | 同回合两次构建字节一致(断言) |
112
+
113
+ ### A6. 文档与示例(1 天)
114
+
115
+ | 项 | 内容 |
116
+ | --- | --- |
117
+ | A6.1 | `docs/PACK-API.md` 定稿 + 「5 分钟做出第一个 Pack」教程 |
118
+ | A6.2 | `packs/example-hello/` 内置最小示例(1 工具 + 1 条 output-forbid 约束) |
119
+ | A6.3 | `docs/DEVELOPER.md` 增加 Pack 章节,指向 PACK-API |
120
+ | A6.4 | README 增加「垂域 Pack」卖点段(对齐新定位,不夸大) |
121
+
122
+ ### A7. 发布与回迁(1 天)
123
+
124
+ | 项 | 内容 | 验收 |
125
+ | --- | --- | --- |
126
+ | A7.1 | 版本 0.4.6 → **0.5.0**(minor:新增 Pack API);`CHANGELOG-PACK.md` 记录 v1 冻结 | 版本一致 + 全绿 |
127
+ | A7.2 | 发布(自检 → commit → tag → 推 origin → 官网/镜像按既有流程) | CI + Desktop 绿 |
128
+ | A7.3 | **Deyi 回迁**:`pack-tcm` 从 `providers/dify.mjs` 拆出 3 工具 + 3 条约束 + 提示词段;`mingdao pack verify` 进下游 CI | 域内费用可见、红线可阻断、工具卡片正常 |
129
+
130
+ ---
131
+
132
+ ## 三、排期(建议,合计 ≈ 11.5 个工作日)
133
+
134
+ ```
135
+ A0 契约冻结 ──▶ A1 Pack 加载器 ──▶ A2 CLI ──┬─▶ A3 约束引擎 ──┐
136
+ └─▶ A4 成本确定性 ─┴─▶ A5 提示词段 ──▶ A6 文档 ──▶ A7 发布+回迁
137
+ ```
138
+
139
+ A3 与 A4 可并行(不同模块),A5 依赖 A1。
140
+
141
+ ---
142
+
143
+ ## 四、风险与对策
144
+
145
+ | 风险 | 影响 | 对策 |
146
+ | --- | --- | --- |
147
+ | Pack API 设计不足,Deyi 迁不动 | v1 冻结即返工 | **A1/A3 完成后先用 `example-hello` 走通**;Deyi 回迁中发现的每个别扭点都当契约缺陷修 |
148
+ | 约束引擎改变 agent 主循环,伤到现有行为 | 回归风险 | 约束默认空集合 → 零 Pack 时行为与今天**完全一致**(用现有 6 套测试守住);有 Pack 才生效 |
149
+ | `ctx.llm()` 破坏已有的费用链路 | P0 级回归 | 复用 `recordUsage` 同一条路径,不新开记账;断言「Pack 调用与普通回合进同一账本」 |
150
+ | Pack 加载拖慢启动 | 体验 | 懒加载贡献面;manifest 静态校验与文件读取分离;启动只读 manifest |
151
+ | 零依赖被破坏 | 根基 | Pack 加载器只用 `node:fs`/`node:url`;`npm:` 来源走 `require.resolve` 语义但不引入依赖树;CI 加「运行时 0 依赖」断言 |
152
+
153
+ ---
154
+
155
+ ## 五、明确不做(本阶段)
156
+
157
+ - ❌ 不做 Pack 商店 / 在线市场(v0.7+ 议题)
158
+ - ❌ 不做行为确定性(执行账本 / 可回放 / 合规导出)→ **v0.6.0**
159
+ - ❌ 不引入 Cordis 或任何重依赖
160
+ - ❌ 不把中医逻辑写进内核(只在 `example-hello` 里放中立示例)
161
+ - ❌ 不动已有省钱链路的行为(只在其上增加 Pack 维度)
162
+
163
+ ---
164
+
165
+ ## 六、完成定义(DoD)
166
+
167
+ 1. `mingdao pack verify` 能对任意 Pack 目录给出**可操作**的通过/失败结论;
168
+ 2. 一个不含领域逻辑的中立示例 Pack(`example-hello`)端到端跑通(工具可用、约束可阻断、提示词段到达、费用入账);
169
+ 3. **Deyi 的 3 个域工具成功从 Provider 迁出**,域内费用出现在 `cost report` 里,中医红线可被测试阻断;
170
+ 4. 全绿门禁 + strict 0/0 + tsc 0 + 运行时 0 依赖;
171
+ 5. `PACK-API.md` 标注 v1 冻结,`CHANGELOG-PACK.md` 有 v1 条目,兼容性矩阵写下「0.5.x / 0.6.x 支持 v1」。