mingdao-harness 0.4.5 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -0
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/AUDIT-v0.4.6.md +179 -0
- package/docs/CHANGELOG-PACK.md +21 -0
- package/docs/DEVELOPER.md +46 -0
- package/docs/MIGRATION-DEYI-v0.5.md +175 -0
- package/docs/PACK-API.md +267 -0
- package/docs/PLAN-v0.5.0.md +169 -0
- package/docs/SAVINGS-BENCHMARK.md +12 -5
- package/docs/STRATEGY-0.5.md +262 -0
- package/docs/STRATEGY-NEXT.md +1 -1
- package/package.json +3 -1
- package/packs/example-hello/pack.json +18 -0
- package/packs/example-hello/pack.mjs +47 -0
- package/packs/example-hello/prompts/domain.md +1 -0
- package/skills-lib/bash/SKILL.md +26 -0
- package/skills-lib/changelog/SKILL.md +24 -0
- package/skills-lib/ci-cd/SKILL.md +25 -0
- package/skills-lib/data-analysis/SKILL.md +25 -0
- package/skills-lib/database-design/SKILL.md +25 -0
- package/skills-lib/docs/SKILL.md +25 -0
- package/skills-lib/email/SKILL.md +25 -0
- package/skills-lib/file-organize/SKILL.md +27 -0
- package/skills-lib/git-workflow/SKILL.md +26 -0
- package/skills-lib/i18n/SKILL.md +24 -0
- package/skills-lib/markdown/SKILL.md +26 -0
- package/skills-lib/meeting-notes/SKILL.md +25 -0
- package/skills-lib/nodejs/SKILL.md +26 -0
- package/skills-lib/performance/SKILL.md +25 -0
- package/skills-lib/python/SKILL.md +26 -0
- package/skills-lib/readme/SKILL.md +25 -0
- package/skills-lib/regex/SKILL.md +25 -0
- package/skills-lib/report/SKILL.md +25 -0
- package/skills-lib/resume/SKILL.md +24 -0
- package/skills-lib/security-audit/SKILL.md +28 -0
- package/skills-lib/sql/SKILL.md +25 -0
- package/skills-lib/translation/SKILL.md +25 -0
- package/src/agent.js +291 -14
- package/src/autostart.js +37 -3
- package/src/batch.js +9 -1
- package/src/cachestats.js +64 -5
- package/src/cli.js +11 -0
- package/src/commands/pack.js +196 -0
- package/src/commands/update.js +19 -1
- package/src/compact.js +16 -1
- package/src/constraints.js +255 -0
- package/src/context.js +8 -0
- package/src/hooks.js +20 -4
- package/src/index.js +17 -0
- package/src/log-writer.js +19 -7
- package/src/mcp.js +4 -0
- package/src/model-caps.js +7 -2
- package/src/packs.js +451 -0
- package/src/permissions.js +10 -4
- package/src/presets.js +13 -3
- package/src/pricing.js +62 -13
- package/src/prompts.js +26 -0
- package/src/providers/index.js +25 -4
- package/src/redact.js +5 -0
- package/src/session.js +14 -1
- package/src/sync-server.js +29 -1
- package/src/sync.js +6 -3
- package/src/tasks/worker.js +6 -0
- package/src/tokenizer.js +48 -11
- package/src/tools/bash.js +9 -2
- package/src/tools/fetch.js +95 -7
- package/src/tools/fs-tools.js +48 -3
- package/src/tools/git.js +9 -1
- package/src/tools/index.js +12 -5
- package/src/web/constants.js +10 -0
- package/src/web/routes/api.js +3 -1
- package/src/web/routes/domains/misc.js +6 -0
- package/src/web/server.js +24 -22
package/README.md
CHANGED
|
@@ -203,6 +203,29 @@ mingdao sync conflicts # 跨设备冲突三选
|
|
|
203
203
|
|
|
204
204
|
多设备自动同步(会话结束静默推送);冲突绝不丢数据(自动 `.server-*` / `.remote-*` 备份 + 图形化选择);WebUI 设置面板含完整同步/分享/冲突区块。
|
|
205
205
|
|
|
206
|
+
### 垂域 Pack(v0.5.0 · Pack API v1)
|
|
207
|
+
|
|
208
|
+
把**某个行业的智能体**打包成一个可安装、可校验、可版本化的单元——不改内核源码:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
mingdao pack new tcm # 生成脚手架(manifest + 入口 + 领域提示词)
|
|
212
|
+
mingdao pack verify ./packs/tcm # 静态校验 + 运行时契约校验(CI 门禁,非 0 退出即失败)
|
|
213
|
+
mingdao pack list / info tcm # 查看已加载 Pack 与贡献面
|
|
214
|
+
mingdao cost --by pack # 垂域费用分账
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
一个 Pack 可以贡献四类东西:
|
|
218
|
+
|
|
219
|
+
| 贡献 | 作用 |
|
|
220
|
+
| --- | --- |
|
|
221
|
+
| **工具** | 领域工具,注册为 `pack__<pack>__<tool>`,与内置工具同走权限 / 审计 / schema 瘦身链路 |
|
|
222
|
+
| **约束(领域红线)** | `tool-deny` / `tool-arg-require` / `arg-forbid` / `output-forbid` / `completeness` / `confirm`,在内核三个时机**强制**(不是提示词里的一句话),命中写审计 |
|
|
223
|
+
| **领域提示词段** | 注入系统提示(确定性排序、字节稳定,不破坏前缀缓存) |
|
|
224
|
+
| **费用归因** | Pack 内模型调用走 `ctx.llm()`,自动入账 + 四维归因(`pack`/`tool`/`purpose`/`model`) |
|
|
225
|
+
|
|
226
|
+
三级遮蔽:`<项目>/.mingdao/packs/` > `~/.mingdao/packs/` > 内置 `packs/`;坏 Pack 只告警、不阻塞启动。
|
|
227
|
+
契约与示例见 [docs/PACK-API.md](docs/PACK-API.md)、内置中立示例 `packs/example-hello/`。
|
|
228
|
+
|
|
206
229
|
### 模型与 Key
|
|
207
230
|
|
|
208
231
|
- 内置:DeepSeek(v4-pro / v4-flash / v4-flash-vision-exp)、OpenAI(GPT-5 系列)、Qwen(qwen3.7-max)、GLM(GLM-5)、Kimi(kimi-latest)
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -89,7 +89,7 @@ PreToolUse(stdin 收 JSON,stdout 输出 `{decision:"block", reason}` 可阻
|
|
|
89
89
|
|
|
90
90
|
## 5. 面向 DeepSeek-V4 的优化设计
|
|
91
91
|
|
|
92
|
-
1. **预设调优**:v4-pro(推理/规划:温度 0.4
|
|
92
|
+
1. **预设调优**:v4-pro(推理/规划:温度 0.4、单次输出 `maxOutputTokens` 64k、预算 200k);v4-flash(日常:温度 0.6、输出 8k、预算 128k);两者 `contextWindow` 均为 1M(官方规格)。`maxOutputCeiling`=384K 是官方单次最大输出规格,作为用户显式调大 `config.maxOutputTokens` 时的硬上限(v0.4.6 起生效,此前该字段只定义未使用)。预算可随时调高。
|
|
93
93
|
2. **推理内容流式展示**:`reasoning_content` 以暗色增量渲染,与正文同流。
|
|
94
94
|
3. **峰谷定价适配**:每轮后展示 prompt/completion tokens,便于用户把批处理放在谷时段(v4 系列 2026-08-17 起峰谷定价,高峰=北京工作日 9:00–12:00、14:00–18:00,闲时价=高峰一半)。官方同时提供 Responses API 与 Anthropic 兼容接口;MingDao 默认走 OpenAI 兼容 chat/completions,如需原生协议可写自定义 Provider 模块。
|
|
95
95
|
4. **模型路由(路线图)**:规划用 v4-pro、执行用 v4-flash 的自动分工;Provider 抽象已支持任意切换。
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# MingDao-Harness 全量代码审计报告(v0.4.6)
|
|
2
|
+
|
|
3
|
+
> 审计日期:2026-09-11 · 迁移至 macOS 后首次全量审计
|
|
4
|
+
> 审计对象:`MingDao-Harness`(v0.4.5 发布态基线 `5db5ef2`;本报告即 v0.4.6 的发布前自检)
|
|
5
|
+
> 方法:六路并行只读深度审计(工具/权限、WebUI、同步/调度、经济学、CLI/TUI/技能、官网/打包)+ 主智能体独立复核与修复
|
|
6
|
+
> 结论:**已修复 50 处缺陷与口径问题(含 1 项 P0、19 项 P1)**,另有 14 项登记待办;修复后 6 套测试全绿(smoke 81 组断言)、strict 棘轮 0/0、tsc 0 错误。
|
|
7
|
+
>
|
|
8
|
+
> 本轮最重要的一条不是某个 bug,而是**一次口径自纠**:省钱基准「综合省 64%」实为虚高(详见 §2.3)。
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 一、环境与基线(迁移后首次验证)
|
|
13
|
+
|
|
14
|
+
| 项 | 结果 |
|
|
15
|
+
| --- | --- |
|
|
16
|
+
| 平台 | macOS 26.6 (Darwin 25.6.0) · arm64 · Apple Silicon |
|
|
17
|
+
| Node / npm | v24.20.0 / 12.0.2(`engines` 要求 ≥18.17,CI 覆盖 18/20/22) |
|
|
18
|
+
| 依赖安装 | `npm install` ✅ 仅 4 个 devDependencies(typescript + @types/node),运行时仍零依赖 |
|
|
19
|
+
| 类型门禁 | `npm run typecheck` → **0 错误** |
|
|
20
|
+
| strict 棘轮 | `scripts/strict-ratchet.mjs` → 当前 0 / 基线 0 ✅ |
|
|
21
|
+
| 测试 | `test/run-all.mjs` → **6/6 套通过**(smoke / e2e-local / e2e-web / e2e-schedule / api-contracts / bench) |
|
|
22
|
+
| 基准 | tokenizer 16 · routing 118 · compaction 46 · cost 14 · savings 20 = **214 断言全绿**,综合省 **51%**(v0.4.6 口径自纠:此前报 208 断言 / 64%,含启发式计数与过期只读档副本) |
|
|
23
|
+
| 断言总数 | smoke 由 74 组提升至 **78 组**(新增回归断言) |
|
|
24
|
+
|
|
25
|
+
> 说明:本机为 Beijing 时区(UTC+8)。已额外用 `TZ=UTC/America/New_York/Europe/London/Australia/Sydney` 交叉验证峰谷计价与宿主时区无关 —— 主张成立。
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 二、已修复(50 处:主线 22 项 + 第二轮 10 项 + 第三轮 9 项 + 官网 3 项 + 口径自纠 6 项)
|
|
30
|
+
|
|
31
|
+
### P0 / P1 —— 安全与正确性
|
|
32
|
+
|
|
33
|
+
| # | 级别 | 缺陷 | 位置 | 修复 |
|
|
34
|
+
| --- | --- | --- | --- | --- |
|
|
35
|
+
| 1 | **P1** | **`git` 工具 100% 失效**:`await execFile(...)` 中 execFile 是回调式 API、返回 ChildProcess(非 thenable),解构出的 stdout/stderr 是两个可读流 → 恒返回 `ok:true exitCode:0 output:"[object Object][object Object]"`,不等待、退出码/ENOENT/maxBuffer/timeout 全被吞。git 属免确认只读工具且 schema 反复推荐 → 模型把「不是仓库/版本不存在」全当成功 | `src/tools/git.js:36` | `promisify(execFile)`;实测 `log` 返回真实提交、坏 revision 返回 exitCode 128 |
|
|
36
|
+
| 2 | **P1** | **WebUI 权限确认可被任意客户端代答**:`/api/permission` 只用可枚举的 `body.taskId` 取 pendingAsk,**从不校验 `body.id`**(ask nonce),也不校验调用方归属 → 任何能访问 API 的一方都能替他人挂起的写文件/执行命令确认直接答「允许」,绕过默认 ask 档的唯一人工闸门 | `src/web/routes/domains/misc.js:59`、`src/web/server.js:493` | 服务端强制校验 ask id;id 由 `crypto.randomBytes(16)` 生成(原 `Math.random` 非加密随机);恢复 `options` 传递 |
|
|
37
|
+
| 3 | **P1** | **任意环境变量外泄**:自定义模型的 `envKey` 由 WebUI 接受任意输入、`baseUrl` 亦由调用方指定,密钥解析链 `[cm.envKey, 'MINGDAO_API_KEY']` 会把宿主环境里**任意变量**(AWS_SECRET_ACCESS_KEY/GITHUB_TOKEN…)当 Bearer 发到攻击者端点;不填 envKey 也会回落主密钥 | `src/providers/index.js:23-34` | envKey 收敛为「形如模型密钥」的白名单模式且排除 SECRET/TOKEN/PASSWORD/…;未声明(或被拒)时**不读任何环境变量**,只认凭证库 |
|
|
38
|
+
| 4 | **P1** | **SSRF 绕过(IPv4-mapped IPv6 十六进制形态)**:URL 解析器把 `[::ffff:127.0.0.1]` 规范化为 `[::ffff:7f00:1]`,旧判定只处理点分四段 → 判为公网;DNS 复检又对带方括号的 IPv6 字面量 lookup 失败后放行 → 可抓回环 WebUI(3820,默认无令牌)/内网服务/云元数据 | `src/tools/fetch.js:7-21`、`src/web/server.js:279-294`(副本) | 按数值展开 IPv6 八组判定,覆盖 IPv4-mapped / IPv4-compatible / NAT64 64:ff9b::/96 / fe80::/10 / fc00::/7;删除 server.js 副本改为单一来源;22 条 IP 形态断言全绿 |
|
|
39
|
+
| 5 | **P1** | **hooks / MCP 子进程 stdin 无 error 监听** → 子进程先退出且载荷 >64KB 时 EPIPE 异步未捕获异常,**整个进程崩溃**(WebUI 下所有并发会话一起死);仓库无 `uncaughtException` 兜底 | `src/hooks.js:92`、`src/mcp.js:140,151` | 两处 `child.stdin.on('error', () => {})` |
|
|
40
|
+
| 6 | **P1** | **`edit` 静默写坏文件**:单处替换用 `String.replace(old, new)`,`new_string` 里的 `$&`/`` $` ``/`$'`/`$$` 被当替换模式 → 写 shell/模板字符串/正则/sed 时静默产生错误内容甚至复制文件尾部,仍回报「已编辑成功」 | `src/tools/fs-tools.js:294` | 改用函数式替换值 `replace(old, () => newString)`,与 `replace_all` 路径语义统一 |
|
|
41
|
+
| 7 | **P1** | **`deny` 规则 fail-open**:防 allow 提权的「白名单字符校验」被同一函数用于 deny,命令含 `& ; \| @ = ' " Tab` 等元字符即失配 → 回落 mode;auto 档下 deny 是唯一防线,等于不存在(`deny:['bash:rm *']` 放过 `rm -rf /x; echo done`、`curl -d @/etc/passwd …`) | `src/permissions.js:37` | deny 与 allow 分开匹配:deny 一律按命令原文匹配,不设字符白名单(allow 白名单保留) |
|
|
42
|
+
| 8 | **P0** | **费用护栏对 9/12 内置模型完全失效**:`effectivePricing` 在读 `pricing.overrides` **之前**就因无内置价 `return null` → hasPricing=false → estimateCost=null → cache-stats 记 cost=null(当 0 累计)→ todayCost 看不到消费 → 护栏永不拦截;而护栏给出的补救办法恰是这条不生效的路径。gpt-5/qwen/glm/kimi/本地/动态发现模型全部中招 | `src/pricing.js:231` | overrides 与 ext/preset 等价作为价格来源;仅靠 overrides 供价时要求 input+output 均为有限正数(防半张价格表把缺失侧当 0)。实测:配 overrides 后 hasPricing=true、todayCost 由 ¥0.15 → ¥100.21、护栏正确 block |
|
|
43
|
+
| 9 | **P1** | **read-after-write 命中旧缓存**:v0.4.1 把 `turnToolCache` 提到轮内让去重跨步生效,却没有失效点 → 同回合「read a → write a → read a」第 3 步返回**写入前**内容,模型误判写入未生效 | `src/agent.js:227,574` | 有副作用工具执行(含失败)后整片作废只读缓存;只读工具与 `task(readOnly)` 保留缓存 |
|
|
44
|
+
| 10 | **P1** | **子代理 token 消耗完全不计费**:`spawnTask` 只用子代理返回文本,其 usage 从不并入父回合 → CLI/REPL(无 onUsage)恒漏计,README 主推的「多方向并行调研」漏计最重 | `src/agent.js:104-116` | 子代理 usage 并入父回合累加器;同时不再向子代理透传 onUsage(避免 WebUI 对同一笔重复入账) |
|
|
45
|
+
| 11 | **P1** | **步数上限兜底总结永远不显示**:进入兜底总结前已多次 `io.endTurn()`,TUI 的 renderer 被置空,而 `onDelta → writeText` 只做 `renderer?.push()` → 总结被静默丢弃。跑满 24 步的长任务(审计/重构/调研)在终端只看到一屏工具调用、没有最终答复 | `src/agent.js:823,840` | 兜底总结前重新 `io.beginTurn()`、结束后 `io.endTurn()`(web-io 的 beginTurn 为空实现,不受影响) |
|
|
46
|
+
| 12 | **P1** | **自动压缩吞掉整段会话**:`compactTrigger < 0.6`(文档化的 0–1 可调项)或 `force` 时,保留循环不 break,boundary 保持 messages.length → 除 system 外**全部**压成摘要(连最新用户指令只剩转述),WebUI 的 onCompact 还会 rewriteSession 永久写回会话文件 | `src/compact.js:67-80` | 触发线夹紧到 ≥ TARGET_RATIO;并在「无可丢前缀」时改为保留最后 2 条原文(至少覆盖最近一轮问答),不再产出「system + 摘要」 |
|
|
47
|
+
| 13 | **P1** | **`reasoning_content` 完全不计入上下文预算**:带 tool_calls 的 assistant 消息会原样回传完整 reasoning(DeepSeek thinking 硬要求),但 messageTokens 只算 content+tool_calls → 实测单条 4400 字 reasoning=2000 token 只算 33(低估 61 倍),预算/压缩/护栏预检同源低估 | `src/context.js:13-19` | `partTokens` 计入 `reasoning_content` |
|
|
48
|
+
| 14 | **P1** | **冲突备份对产品不可见**:producer 写 `<名>.server-<时间戳>-<随机>.jsonl`,consumer 正则只认 `<名>.server-<纯数字>.jsonl` → `listSyncConflicts` 恒空、`resolveSyncConflict` 恒报「没有找到」,「冲突三选一」100% 失效;备份还会被当成普通会话推送到其他设备变成幽灵会话 | `src/sync.js:222` vs `:440,467`、`src/session.js:11` | `session.js` 导出共享 `CONFLICT_BACKUP_RE` + `isConflictBackupName`;三处共用;`listSessions` 排除备份(同步/会话列表/搜索一并修复);补真实 producer 的端到端回归断言 |
|
|
49
|
+
| 15 | **P1** | **sync-server 限流可被查询串绕过**:桶键用 `req.url`(含 query)而路由用 pathname → 每个请求加随机 `?x=` 即每次落进新桶,登录/配对/改密限流整体失效(scrypt 18ms/次 → 无限速爆破 + 阻塞事件循环的 DoS) | `src/sync-server.js:152` | 桶键改用解析后的 pathname;新增独立服务器的限流回归断言 |
|
|
50
|
+
|
|
51
|
+
### P2 / P3 —— 正确性、健壮性与打包
|
|
52
|
+
|
|
53
|
+
| # | 级别 | 缺陷 | 位置 | 修复 |
|
|
54
|
+
| --- | --- | --- | --- | --- |
|
|
55
|
+
| 16 | P1 | **npm 包与桌面版都缺 `skills-lib/`**:22 个可安装技能库既不在 `package.json#files` 也不在 Electron `extraResources`,而 `skill-lib.js` 运行时按 `../skills-lib` 读取(ENOENT 被 try/catch 静默吞)→ README/官网主打的「36 个技能」在两条主分发渠道只剩 14 个 | `package.json:14`、`desktop/electron-builder.yml:20` | 两处补 `skills-lib/`;新增静态护栏断言「src 里所有 `new URL('../dir')` 资源目录必须同时出现在 npm files 与 extraResources」 |
|
|
56
|
+
| 17 | P1 | **桌面版版本漂移**:`desktop/package.json`=0.2.0(根 0.4.5),而 `npm run dist:linux\|win\|mac`(README 教用户的入口)**不跑同步脚本** → 本地按文档打包产出 0.2.0 安装包/应用 | `desktop/package.json:3,13` | 为 4 个 `dist:*` 增加 `predist:*` 前置同步;当前已同步为 0.4.6;新增版本一致性断言 |
|
|
57
|
+
| 18 | P2 | **日志轮转写放大**:保留区恰好等于上限 → 达上限后**每次追加都整文件重写**(实测 2000 次追加 ≈1GB I/O);且 `raw.length`(UTF-16 码元)与 `st.size`(字节)单位混用,中文日志只砍一行 | `src/log-writer.js:16-25` | 按字节定位行边界(`Buffer.lastIndexOf(0x0a)`)+ 轮转到 `maxBytes/2` 低水位;实测 3000 次追加仅 5 次轮转 |
|
|
58
|
+
| 19 | P3 | 已存在的 644 日志永不收权(mode 只在创建/轮转生效) | `src/log-writer.js:13` | 每次追加后 `chmodSync(0o600)` |
|
|
59
|
+
| 20 | P2 | TLS 静默降级:只设 `SYNC_CERT`/`SYNC_KEY` 之一时启动明文 HTTP(默认端口 443),部署方以为在跑 HTTPS | `src/sync-server.js:674` | 两者必须同时提供,否则拒绝启动;非回环明文绑定追加醒目告警 |
|
|
60
|
+
| 21 | P2 | `estimateBatchCost` 对无价模型返 0 → `--max-cost` 预算拦截静默失效、/cost 把未知费用显示成「免费」 | `src/pricing.js:185`、`src/batch.js:141` | 返 null(与 `estimateCost` 的 P0-4 同口径);`--max-cost` 遇未知即 fail-closed 中止并给配置指引;CLI 显示「未知」而非 ¥0 |
|
|
61
|
+
| 22 | P3 | 桌面版 `files`/`extraResources` 与运行时资源目录的一致性无任何测试守护(正是 #16 长期未被发现的原因) | `test/smoke.js` | 见 #16 的静态护栏 |
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
### 第二轮:安全与健壮性(T1–T13 中的低风险项,全部已修)
|
|
66
|
+
|
|
67
|
+
| # | 级别 | 缺陷 | 位置 | 修复 |
|
|
68
|
+
| --- | --- | --- | --- | --- |
|
|
69
|
+
| 23 | P2 | **`undo` 越界回落**:显式传越界 path 时静默落到「撤销最近一次」分支,回滚**无关文件**(指定 `/etc/hosts` 却还原了 `important.txt`)并回报成功 | `src/tools/fs-tools.js:126` | 指定路径与省略路径语义分开:越界直接返回边界错误 |
|
|
70
|
+
| 24 | P2 | **hook `matcher` 不支持 `\|`**:CONFIG.md 官方示例写 `"write\|edit\|bash"` 并声明支持 `\|`,实现只按 `,` 切分 → 按文档写的策略钩子永不触发(fail-open) | `src/hooks.js:22` | 同时支持 `,` 与 `\|` |
|
|
71
|
+
| 25 | P2 | **URL 内嵌凭据未脱敏**:文件头注释一直声称覆盖,规则里却没有 → `git clone https://oauth2:glpat-…@host`、`postgres://user:pw@host` 原样落入 `audit.jsonl` 与诊断包 | `src/redact.js:15` | 新增 `://user:pass@` 掩码规则(保留用户名/主机便于排查) |
|
|
72
|
+
| 26 | P2 | **预设提权防护对对象形态失效**:`permission` 为文档推荐的 `{mode,allow,deny}` 时对象做 `in` 运算键名变 `[object Object]` → 当前 `{mode:'readonly'}` 被判为 `ask`,项目级预设可把只读**静默提权**为 ask | `src/presets.js:160` | 归一化 mode;识别不出的输入取最保守 readonly(fail-closed);拦截时保留原对象结构 |
|
|
73
|
+
| 27 | P2 | **深嵌套 schema 崩溃**:`stripDescriptions` 无深度上限,恶意 MCP `inputSchema`(20 万层)抛 RangeError,且 `buildToolSchemas` 每轮都跑 → 该会话此后每轮报错 | `src/tools/index.js:388` | 深度上限 32,超深原样保留 |
|
|
74
|
+
| 28 | P3 | **`config.tools` 子进程不筛敏感环境变量**(bash/hooks/MCP 都筛)——唯一不一致的子进程入口,可读到 `MINGDAO_API_KEY`/`AWS_SECRET_ACCESS_KEY` | `src/tools/index.js:339` | 复用 `buildChildEnv`(从 bash.js 导出) |
|
|
75
|
+
| 29 | P3 | **`SSH_AUTH_SOCK` 被误判为敏感变量剥离** → bash 内 git-over-SSH / ssh-agent 失效(macOS 常态) | `src/tools/bash.js:17` | 连接句柄显式放行,其余过滤语义不变 |
|
|
76
|
+
| 30 | P3 | **缺点击劫持防护**:CSP 的 `frame-ancestors` 不支持 meta 标签,而服务端未下发 HTTP 头 → 任意网页可 iframe 嵌 WebUI 并叠透明层把点击导向权限弹窗「允许」 | `src/web/constants.js`、`api.js`、`server.js` | 统一下发 `frame-ancestors 'none'` / `X-Frame-Options` / `nosniff` / `Referrer-Policy` |
|
|
77
|
+
| 31 | P2 | **`fetch` 上限在整包下载后才判**:实测服务端写满 30MB 才报错(15s abort 只限时不限字节)→ 内存 DoS | `src/tools/fetch.js:131` | 先看 Content-Length,再边读边累计,超限立即 `cancel()`(实测 1MB 内中止) |
|
|
78
|
+
| 32 | P2 | **`grep` ReDoS 可绕过**:`(a\|aa)+$` 的括号内无量词,逃过「嵌套量词」启发式 → 20KB 行同步回溯 >180s,冻结整个 Node 进程 | `src/tools/fs-tools.js:445` | 新增「同前缀歧义分支 + 量词」精确判定(不误伤 `(foo\|bar)+`)+ 5s 总时间预算 |
|
|
79
|
+
|
|
80
|
+
### 第三轮:成本口径与平台正确性
|
|
81
|
+
|
|
82
|
+
| # | 级别 | 缺陷 | 位置 | 修复 |
|
|
83
|
+
| --- | --- | --- | --- | --- |
|
|
84
|
+
| 33 | **P1** | **启发式计数不是「保守上界」**:纯标点低估 3 倍、单字母词/随机字母数字 2 倍、纯数字 1.3 倍(10 类样本 6 类偏低)→ 非 DeepSeek 模型预算/压缩/批量预检系统性偏小 | `src/tokenizer.js:137` | 改为按字符类别 + 连续串估算(标点 1:1、数字 1/2、字母串 ≥1、空白不重复计);实测 11 类样本 3 类轻微低估(≤9%)、平均比值 1.14。**并修正文档中「普适上界」的虚假表述** |
|
|
85
|
+
| 34 | **P1** | **省钱基准虚高(口径自纠)**:④⑤ 用启发式计数测「面向 DeepSeek 的省钱主张」(JSON 结构字符多,偏差 1.1–1.8 倍);⑤ 还维护了一份**过期 6 工具副本**(实现已在 v0.4.4 加入 `task`)→ 报出「综合省 64%」 | `test/bench/bench-cost.mjs`、`bench-savings.mjs`、`test/smoke.js` | 改用随包官方词表精确计数;只读档集合从 `agent.js` 单源导出;新增类别化断言。**真实值:④48.9%、⑤28.9%、综合 51%**,并同步修正 `SAVINGS-BENCHMARK.md` / `STRATEGY-NEXT.md` |
|
|
86
|
+
| 35 | P3 | `maxOutputCeiling`(官方 384K 单次输出规格)**只定义零引用** → README「单次输出上限 384K」在框架里拿不到 | `src/models.js:57`、`model-caps.js`、`agent.js:47` | 纳入能力面并作为显式 `maxOutputTokens` 的硬上限 |
|
|
87
|
+
| 36 | P2 | **日界/避峰时区错位**:`beijingParts` 用可配置时区,`beijingToDate` 却硬编码 UTC+8 → 覆盖 `pricing.timezone` 后日界与 `--offpeak` 顺延错 12 小时(美东实测) | `src/pricing.js:145` | 按目标时区真实偏移换算(两遍法处理夏令时) |
|
|
88
|
+
| 37 | P2 | **峰谷单价按落账时刻判定**:跨 12:00/18:00 边界的请求错记一档(1M prompt 的 pro 调用 ¥9 vs ¥4.5) | `src/agent.js`、`src/cachestats.js` | 记录请求**发起**时刻并作为计价锚点 |
|
|
89
|
+
| 38 | P2 | **cache-stats 轮转可丢当天早期费用**:只保留最后 1 万行 → todayCost 变小、日费用护栏被静默重置 | `src/cachestats.js:47` | 轮转保留「当天全部 + 最近 KEEP_LINES」并保持原顺序 |
|
|
90
|
+
| 39 | P2 | **macOS 自启必然失败**:plist 用 `/bin/sh -c "mingdao web 3820"`,launchd 极简 PATH 下找不到命令,且从不 `launchctl` 注册 → 静默不自启 | `src/autostart.js:41` | 改用 `process.execPath` + `cli.js` 绝对路径 + 注入 PATH + `launchctl bootstrap/bootout`(plutil 校验通过) |
|
|
91
|
+
| 40 | P2 | **CI strict 棘轮空转**:`npm ci` 排在棘轮之后,`npx tsc` 找不到 typescript 时按「0 错误」报 ✅ | `.github/workflows/ci.yml`、`scripts/strict-ratchet.mjs` | `npm ci` 前置;棘轮在 tsc 不可用时以退出码 2 失败(实测) |
|
|
92
|
+
| 41 | — | 测试本身编码了缺陷行为:smoke 断言 `git status` 在非仓库中**成功**(只有坏实现才成立)、tokenizer 断言锁定旧口径 | `test/smoke.js` | 改为断言真实语义(含 git 退出码 128 透传、真实 log 输出) |
|
|
93
|
+
|
|
94
|
+
### 第四轮:官网仓库(独立仓库,已单独提交)
|
|
95
|
+
|
|
96
|
+
| # | 级别 | 缺陷 | 位置 | 修复 |
|
|
97
|
+
| --- | --- | --- | --- | --- |
|
|
98
|
+
| 42 | P2 | 论坛**板块名未转义**进列表页 `<h2>`(仅搜索分支转了义)→ 管理员/首位注册者可持久化 XSS | `bbs/index.html:246` | 板块名同样 `esc()` |
|
|
99
|
+
| 43 | P2 | 论坛限速在 openresty 反代下按 `127.0.0.1` 聚合 → 退化为**全站共享**配额(11 次登录/分钟即锁死全站) | `bbs/bbs-server.js:95` | 来自本机代理时信任 `X-Forwarded-For`(格式校验)+ 桶按时间淘汰(原先整表 clear 可自解限速) |
|
|
100
|
+
| 44 | P3 | `deploy.sh` 不部署 `site/site-stats.mjs`,与 README「服务器以本仓库为唯一事实来源」矛盾 | `deploy.sh` | 纳入部署范围 |
|
|
101
|
+
|
|
102
|
+
## 三、登记待办(14 项,未在本轮修复)
|
|
103
|
+
|
|
104
|
+
> 均已定位到 `file:line` 并有可复现路径;按优先级排列,建议 v0.4.7 / v0.5.0 消化。
|
|
105
|
+
> (第二轮已消化 T4–T9、T11–T13、T16;下表为**剩余**项。)
|
|
106
|
+
|
|
107
|
+
### 安全 / 隔离
|
|
108
|
+
|
|
109
|
+
| # | 级别 | 问题 | 位置 |
|
|
110
|
+
| --- | --- | --- | --- |
|
|
111
|
+
| T1 | P2 | `/api/workspaces` 对用户给的绝对路径 `mkdirSync(recursive)`;登记 `/` 后 `fs-browse` 围栏自我解除(可枚举全盘目录树) | `web/routes/domains/workspace.js:36-49,98-101` |
|
|
112
|
+
| T2 | P2 | `/api/abort`、`/api/tasks` 无归属校验:任意客户端可中断他人任务、读取他人任务消息与会话名(taskId 可枚举)。根因是共享 token 下缺少「每客户端作用域」,需引入客户端 cookie 作用域后统一收口 | `web/routes/domains/misc.js:76-97`、`schedule.js:27-34` |
|
|
113
|
+
| T3 | P2 | 项目级技能 sha256「防仓库投毒」可自签/可缺失(指纹就在被检查目录内),对声称威胁零收益却给出虚假安全感。最小诚实修法:改措辞 + 加载项目级技能时显式提示来源不可验证 | `src/skills.js:42-50,96-100` |
|
|
114
|
+
|
|
115
|
+
### 正确性 / 健壮性
|
|
116
|
+
|
|
117
|
+
| # | 级别 | 问题 | 位置 |
|
|
118
|
+
| --- | --- | --- | --- |
|
|
119
|
+
| T10 | P2 | 同一会话并发回合未串行化(`withSessionLock` 只锁单次写,不覆盖 load→推理→写回整段)→ 会话记录交错;若触发自动压缩会整文件覆盖丢另一路消息 | `src/web/server.js:391,487,590,552` |
|
|
120
|
+
| T14 | P2 | 调度:daemon 模式下 `job.pid` 恒 null、`lastTaskId` 仅在跑完后写 → pause/remove 无法停止在途运行;`--offpeak` 等待期间 pause/remove 后仍会启动 | `src/schedule.js:165-177,389-415` |
|
|
121
|
+
| T15 | P2 | 重复 daemon → 同一调度任务被**并发执行两次**(lease 自检只 break 监督循环,未取消已启动的协程;pidfile 在 spawn 后才写、无 `O_EXCL` 认领)。这是剩余项里影响最大的一条 | `src/cli.js:279-338`、`schedule.js:319-335` |
|
|
122
|
+
| T17 | P2 | 辅助模型调用(路由分类器 / 自动标题 / 记忆提炼)从不 `recordUsage` → 「自动路由省钱」在本框架自己的账本里无法验证,且护栏少计这部分消费 | `src/routing.js:95-110`、`titles.js:31-57`、`memory.js:183,321` |
|
|
123
|
+
| T18 | P3 | `withFileLockSync` 会把 `fn` 抛出的 `EEXIST` 误判为「锁被占」→ 潜在同步死循环(当前调用方暂无必然触发路径,但属共享锁原语的隐患) | `src/atomic-write.js:64-65` |
|
|
124
|
+
| T19 | P3 | `workspaces.json` / `session-workspaces.json` / `sync-state.json` 的 read-modify-write 未加锁(WebUI 每次建会话都会 touch 工作空间) | `src/workspace.js:32-81`、`src/sync.js:227-242` |
|
|
125
|
+
| T20 | P3 | 调度/任务的生命周期边角:僵尸任务不回收(守护可能空转到 2h)、`killed` 被 worker 的终态写覆盖、无 `/proc` 平台(macOS)无法校验 PID 归属 → 存在 PID 复用误杀风险 | `src/tasks.js:18-43,95-101`、`src/schedule.js:286-318` |
|
|
126
|
+
| T21 | P3 | WebUI 边角:草稿槽 `draftTexts` 无上限(实测 150 请求 +40MB 不回收)、`/api/config` 不校验模型名、`updateCustom` 实为 upsert、非法 JSON body 被当 `{}` 并落盘、`/api/session-finalize` 缺文件返回 500 并回显绝对路径、`HEAD` 被当写方法返回 415 | `web/routes/domains/{sessions,config,misc}.js`、`web/server.js:125`、`routes/api.js:43` |
|
|
127
|
+
| T22 | P3 | TUI/CLI 边角:模型/工具输出中的 ANSI/OSC 转义直通终端(可清屏/改标题/污染管道)、`box()` 不看终端宽度、隐藏输入把提示语一起隐藏、`key set` 经 argv 传密钥、`batch` 清空全进程 SIGINT 监听、HELP_LINES 两份已分叉 | `src/ui.js`、`src/notify.js`、`src/commands/{key,update}.js`、`src/cli.js` |
|
|
128
|
+
| T23 | P3 | 技能/安装链边角:技能 `description` 无长度上限(可撑大每轮系统提示)、技能「安装/信任/重装」三入口不校验名称(纵深防御缺口)、`install.sh` 的 `curl \| bash` 失败静默成功且 Node 门槛查 ≥18.0(文档写 ≥18.17) | `src/skills.js:26-39`、`src/skill-lib.js:80`、`install.sh:71,84` |
|
|
129
|
+
| T24 | P3 | 官网/IDE 边角:openresty 对不存在路径返回 200+首页(死链接不可发现、污染下载计数,需服务器侧 `try_files`)、VS Code「发送选中代码」写全局槽而 WebUI 只读会话槽(功能不生效)、JetBrains 文档要 `./gradlew` 但仓库无 wrapper | 官网 nginx 配置、`ide/vscode/extension.js:65-78`、`src/web/app.js:695`、`ide/jetbrains` |
|
|
130
|
+
|
|
131
|
+
## 四、已确认无问题(避免过度修复)
|
|
132
|
+
|
|
133
|
+
以下机制经实测或逐行追踪确认**可靠**,本轮审计中未被列为缺陷:
|
|
134
|
+
|
|
135
|
+
- **文件边界防护**:`..`/绝对路径越界、软链目录逃逸、写软链目录全部拒绝;`fsAllowDirs` 生效;macOS `/tmp → /private/tmp` 场景正确;`walkFiles` 不跟随软链。22 条越界探针(含 `~/.ssh/id_rsa`、`credentials.json`、`/etc/passwd`、URL 编码与双点花招)全部拦住。
|
|
136
|
+
- **bash 沙箱**:档位不可被模型降级;macOS 无 bwrap 时显式 note 降级(不假装);超时整进程组清理无残留;敏感环境变量默认剥离。
|
|
137
|
+
- **MCP**:子进程环境默认过滤;`readOnlyHint` 仅在 `trusted:true` 时被信任;子进程输出 20MB 缓冲上限 + 组杀。
|
|
138
|
+
- **WebUI 认证**:非回环强制 token(三通道 + `timingSafeEqual`)、Host 头校验(DNS rebinding 403)、CSRF Origin/Content-Type、全部路径穿越防护、body 按字节分级限制、并发 8 上限后 429、SSE 中断无 inflight 泄漏、XSS 汇聚点全部 `esc`/`textContent` + CSP `script-src 'self'`。
|
|
139
|
+
- **sync-server 认证与隔离**:scrypt + 盐 + `timingSafeEqual`、192-bit 设备 token 只存哈希、改密吊销全部设备、用户名/会话名白名单挡穿越、跨用户访问 404。
|
|
140
|
+
- **同步数据安全(常规路径)**:pull 不覆盖非空本地文件(差异写 `.remote-*`);push 备份已知的远端版本;冲突副本原子写。
|
|
141
|
+
- **峰谷计价**:单价表与「命中=未命中/30」「闲时=高峰/2」完全自洽;边界(09:00 含 / 12:00 不含 / 14:00 含 / 18:00 不含 / 周末闲时)正确;**宿主时区无关**(6 个时区交叉验证一致)。
|
|
142
|
+
- **tokenizer**:BPE 实现经独立参考实现交叉验证 330/330 一致;词表完整(127,741 merges + 818 added);黄金值在两套独立实现下均成立。
|
|
143
|
+
- **裁剪**:不产生孤儿 `tool` 消息、不越预算、system 恒保留。
|
|
144
|
+
- **原子写与文件锁**:tmp 名含 pid+随机、rename 原子替换;锁可重入、异常释放、超时保护、陈旧锁 TOCTOU 防护。
|
|
145
|
+
- **凭证隔离**:`credentials.json` 独立于 config、0600、`maskKey` 只露首 6 末 4;`redactSecrets` 对 `sk-`/`ghp_`/Bearer/`api_key=` 等有效。
|
|
146
|
+
- **桌面壳边界**:`contextIsolation:true` / `nodeIntegration:false` / `sandbox:true`;外链走系统浏览器;`will-navigate` 白名单。
|
|
147
|
+
- **官网发布面**:5 个安装包 sha256/sha512/大小与线上**逐字节一致**;6 个下载 URL 全部有效;技能 registry 三镜像索引与 22 个技能文件 sha256 全部吻合;两仓库无密钥入库。
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## 五、审计方法说明与局限
|
|
152
|
+
|
|
153
|
+
**方法**:六路并行只读审计(每路覆盖独立子系统,要求给出 `file:line` 证据 + 可复现路径 + 实测确认),主智能体独立复核关键结论并用最小复现脚本验证,再集中修复。所有复现脚本位于 `/tmp`,未污染仓库。
|
|
154
|
+
|
|
155
|
+
**局限(诚实边界)**:
|
|
156
|
+
|
|
157
|
+
1. **无法离线验证词表出处**:本机 bash 无外网、`web_fetch` 取不到官方 `tokenizer.json`、环境无 transformers/HF 缓存 → 12 条黄金值只能证明「与随包词表自洽」,不能证明独立来自官方词表(但已用独立参考实现交叉验证实现正确性)。
|
|
158
|
+
2. **官方价格数字与 Batch 折扣语义无法离线核对**:只验证了内部一致性与算术正确性。
|
|
159
|
+
3. **macOS `launchctl` 真实加载行为**未实测(未触碰真实 `~/Library/LaunchAgents`)。
|
|
160
|
+
4. **Windows 特定语义**(进程组、detached、NSIS)未在本机验证。
|
|
161
|
+
5. **线上服务器侧的发布/收割流程与 nginx 配置不在任何仓库内**,无法从代码确证。
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## 六、验证方式
|
|
166
|
+
|
|
167
|
+
修复后执行(全部通过):
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
npm install # devDependencies(运行时仍零依赖)
|
|
171
|
+
npm run typecheck # tsc --checkJs 全量:0 错误
|
|
172
|
+
node scripts/strict-ratchet.mjs # strict 棘轮:0 / 0
|
|
173
|
+
node test/run-all.mjs # 6 套:smoke / e2e-local / e2e-web / e2e-schedule / api-contracts / bench
|
|
174
|
+
npm run bench # 214 断言(省钱基准:综合 51%,v0.4.6 口径自纠后)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
新增回归断言覆盖:写后再读不吃旧缓存、子代理 token 并入父回合、日志低水位轮转 + 收权、限流不可被查询串绕过、随包资源目录齐全 + 桌面版本一致、冲突备份可见性 + 会话列表排除、`git` 工具真实语义(含退出码透传)、IPv4-mapped IPv6 SSRF 拦截、无价模型 Batch 费用为 null、URL 凭据脱敏、`undo` 越界报错、hook `|` matcher、预设对象形态提权拦截、`SSH_AUTH_SOCK` 保留、深嵌套 schema 不崩、`grep` ReDoS 拦截、时区感知日界/避峰、峰谷锚定请求发起时刻、输出上限封顶、自启绝对路径。
|
|
178
|
+
|
|
179
|
+
**测试本身的两处「编码缺陷行为」也一并修正**:smoke 曾断言 `git status` 在非 git 目录**成功**(只有 `await execFile` 的坏实现才成立),以及 tokenizer 断言锁定旧口径——两者都会让回归测试反过来保护 bug。
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Pack API 变更日志(Changelog-PACK)
|
|
2
|
+
|
|
3
|
+
只记录 **Pack API**(`pack.json` / `pack.mjs` / 约束引擎 / `ctx.llm()` / CLI)的契约变更。
|
|
4
|
+
产品功能变更见根目录 `CHANGELOG.md`。
|
|
5
|
+
|
|
6
|
+
兼容策略:同一 major 内向后兼容(minor 只增不改);支持窗口 = 最近 2 个 minor。
|
|
7
|
+
任何改动必须同步更新 `PACK-API.md` 与兼容性矩阵。
|
|
8
|
+
|
|
9
|
+
## v1(2026-09-11,随内核 v0.5.0 冻结)
|
|
10
|
+
|
|
11
|
+
**首次冻结。** 范围:
|
|
12
|
+
|
|
13
|
+
- `pack.json` manifest:`apiVersion` / `name` / `version` / `engines.mingdao` / `permissions` / `contributes`
|
|
14
|
+
- `pack.mjs` contributions:`tools` / `promptSections` / `constraints` / `memorySchema`
|
|
15
|
+
- 约束引擎 kind:`tool-deny` / `tool-arg-require` / `arg-forbid` / `output-forbid` / `completeness` / `confirm`
|
|
16
|
+
- 约束执行时机:PreToolUse / PostToolUse / 输出前
|
|
17
|
+
- `ctx.llm()` 统一模型出口(自动入账 + 四维归因)
|
|
18
|
+
- CLI:`mingdao pack list|verify|new|info|test`
|
|
19
|
+
- 兼容窗口:内核 0.5.x / 0.6.x 支持 `apiVersion: 1`
|
|
20
|
+
|
|
21
|
+
**已拍板的三条约束**(见 `PACK-API.md` §9):Pack 不得覆盖内置 Provider;`block-and-rewrite` 计费归 Pack;Pack 内 `fetch` 允许但需白名单 + 入账 + 静态告警。
|
package/docs/DEVELOPER.md
CHANGED
|
@@ -83,6 +83,52 @@ registerTool({
|
|
|
83
83
|
不做字符串拼接(防注入),由命令自行解析;执行受权限引擎门控(与 bash 同权重)。
|
|
84
84
|
改 config.tools 需重启生效(与 MCP 预设一致)。
|
|
85
85
|
|
|
86
|
+
## 二之补、垂域 Pack(v0.5.0,Pack API v1)
|
|
87
|
+
|
|
88
|
+
Preset 定制的是「提示词 + 工具白名单 + 权限」;**Pack 定制的是「一个行业的智能体」**——
|
|
89
|
+
领域工具、领域红线、领域提示词、领域费用归因,全部作为可安装单元打包,且**不改内核源码**。
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
mingdao pack new tcm # 脚手架:pack.json + pack.mjs + prompts/domain.md
|
|
93
|
+
mingdao pack verify ./packs/tcm # 契约校验(下游 CI 门禁:非 0 退出即失败)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
最小 `pack.mjs`:
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
export const apiVersion = 1;
|
|
100
|
+
export function createPack(ctx) {
|
|
101
|
+
return {
|
|
102
|
+
tools: [{
|
|
103
|
+
name: 'intake_collect',
|
|
104
|
+
description: '采集并落盘;缺项必须继续追问',
|
|
105
|
+
parameters: { type: 'object', properties: { patientId: { type: 'string' } }, required: ['patientId'] },
|
|
106
|
+
readOnly: false,
|
|
107
|
+
async run(args, toolCtx) {
|
|
108
|
+
// 统一模型出口:usage 自动入账 + 受日费用护栏约束 + Pack 归因
|
|
109
|
+
const r = await toolCtx.llm({ model: 'deepseek-v4-flash', system: '…', user: '…', purpose: 'patient-extract' });
|
|
110
|
+
return { ok: true, output: r.text, data: { /* 供 completeness 约束校验的字段 */ } };
|
|
111
|
+
},
|
|
112
|
+
}],
|
|
113
|
+
constraints: [
|
|
114
|
+
{ id: 'no-cross-patient', kind: 'tool-arg-require', tool: 'intake_collect', requireArg: 'patientId' },
|
|
115
|
+
{ id: 'ten-questions', kind: 'completeness', tool: 'intake_collect', fields: ['zhushu', 'zhendan'] },
|
|
116
|
+
{ id: 'no-conclusion', kind: 'output-forbid', pattern: '好转|治愈|确诊为', action: 'block-and-rewrite' },
|
|
117
|
+
],
|
|
118
|
+
promptSections: [{ id: 'domain', order: 100, content: '…' }],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
三条要点:
|
|
124
|
+
|
|
125
|
+
1. **领域红线由内核强制**,不是提示词建议。三个时机:调用工具前(工具/参数)、工具返回后(缺项则拒绝该结果)、正文输出前(回填会话历史**之前**改写/拦截)。命中写审计事件。
|
|
126
|
+
2. **Pack 内模型调用必须走 `ctx.llm()`**。自己 `fetch` 模型接口会让费用隐身、日费用护栏失效、`--by pack` 看不到——`pack verify` 会对此给出静态告警。
|
|
127
|
+
3. **只收紧、不放松**:约束不授予任何权限,也不改变 `permissions.js` 的判定。
|
|
128
|
+
|
|
129
|
+
完整契约(manifest 字段、约束 kind、`ctx.llm` 语义、版本兼容窗口)见 [PACK-API.md](PACK-API.md);
|
|
130
|
+
Pack API 变更史见 [CHANGELOG-PACK.md](CHANGELOG-PACK.md)。
|
|
131
|
+
|
|
86
132
|
## 三、库嵌入:最小示例
|
|
87
133
|
|
|
88
134
|
```js
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Deyi-TCM-Harness 回迁指南(v0.5.0 → Pack API v1)
|
|
2
|
+
|
|
3
|
+
> 面向:下游 Line B(中医垂域层,Linux 原机开发)
|
|
4
|
+
> 上游契约:`PACK-API.md`(v1 已冻结)· 变更史 `CHANGELOG-PACK.md`
|
|
5
|
+
> 触发:**v0.5.0 发布即回迁**(决策已确认)。目标:把 3 个域工具从 `providers/dify.mjs` 的 `chat()` 里搬出来。
|
|
6
|
+
> 纪律:下游**只通过扩展点接入,绝不修改上游源码**;内核 bug 在上游修。
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 一、为什么必须迁(迁移前的真实损失)
|
|
11
|
+
|
|
12
|
+
当前 `Deyi-TCM-Harness/layer/providers/dify.mjs` 把整个中医域逻辑写进了 Provider 的 `chat()`。这是上游缺抽象导致的,不是下游的问题。具体损失:
|
|
13
|
+
|
|
14
|
+
| 应有能力 | 迁移前现状 |
|
|
15
|
+
| --- | --- |
|
|
16
|
+
| 权限引擎门控 | 域内「工具」是 `chat()` 里的正则匹配(`/^(回访\|随访)\s*(.*)$/`),绕过 `permissions.js` |
|
|
17
|
+
| 审计追溯 | 不写 `audit.jsonl`——无法回答「谁在何时读了哪位患者的病历」 |
|
|
18
|
+
| **费用与护栏** | 域内每次 DeepSeek 调用硬编码 `usage: { prompt_tokens: 0, completion_tokens: 0 }` → **完全不计费、不触发日费用护栏** |
|
|
19
|
+
| UI 工具卡片 / 流式 | 只能手工 `opts.onDelta` |
|
|
20
|
+
| 独立版本与兼容 | 整文件覆盖,无 `apiVersion`,无法 CI 校验 |
|
|
21
|
+
| 记忆 / 技能 / 预设复用 | 全部用不上 |
|
|
22
|
+
|
|
23
|
+
迁移后这六项全部进入内核的既有链路。
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 二、目标结构
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
Deyi-TCM-Harness/
|
|
31
|
+
layer/
|
|
32
|
+
packs/
|
|
33
|
+
tcm/
|
|
34
|
+
pack.json # manifest(apiVersion / engines / permissions / contributes / budget)
|
|
35
|
+
pack.mjs # createPack(ctx) → tools / constraints / promptSections
|
|
36
|
+
prompts/domain.md # 中医领域提示词(从 README 的职责描述落成正式文本)
|
|
37
|
+
constraints.json # 三条领域红线(也可内联在 pack.mjs)
|
|
38
|
+
examples/config.example.json
|
|
39
|
+
install.sh # 改为安装 packs/ 而不是 providers/
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`providers/dify.mjs` 里的**Dify 调用本身**保留为 Provider(协议适配是 Provider 的职责);
|
|
43
|
+
**三个域工具、四条流程、患者注册表、四态对比、回访看板**迁到 `pack-tcm`。
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 三、逐项迁移
|
|
48
|
+
|
|
49
|
+
### 3.1 三个「命令式工具」→ 真工具
|
|
50
|
+
|
|
51
|
+
迁移前(`chat()` 内字符串匹配,无权限、无审计、无卡片):
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
const fu = /^(回访|随访)\s*(.*)$/.exec(query);
|
|
55
|
+
if (fu) { /* …直接产出文本,手工 opts.onDelta… */ }
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
迁移后:
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
// packs/tcm/pack.mjs
|
|
62
|
+
export function createPack(ctx) {
|
|
63
|
+
return {
|
|
64
|
+
tools: [
|
|
65
|
+
{ name: 'intake_collect', description: '首诊十问采集与病历落盘(缺项必须继续追问)', parameters: {…}, readOnly: false, run: intakeCollect },
|
|
66
|
+
{ name: 'visit_compare', description: '复诊四态对比(消失/减轻/无变化/加重,只陈述事实)', parameters: {…}, readOnly: false, run: visitCompare },
|
|
67
|
+
{ name: 'followup_board', description: '回访看板与单患者随访(趋势 + 预警 + 话术草稿)', parameters: {…}, readOnly: true, run: followupBoard },
|
|
68
|
+
],
|
|
69
|
+
// …
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
注册后内核自动加前缀:`pack__tcm__intake_collect` 等,与内置工具走**同一条**权限 / 审计 / schema 瘦身 / 费用链路。
|
|
75
|
+
|
|
76
|
+
### 3.2 三条红线 → 约束引擎(从提示词升级为内核强制)
|
|
77
|
+
|
|
78
|
+
| 现有红线(写在 prompt 里) | 迁移后的约束声明 |
|
|
79
|
+
| --- | --- |
|
|
80
|
+
| 缺项绝不编造 | `{ id:'ten-questions', kind:'completeness', tool:'intake_collect', fields:['zhushu','zhenduan','hanre','han','toushen','erbian','yinshi','xiongfu','kouke','jiubing'], onMissing:'reject' }` |
|
|
81
|
+
| 不输出诊疗结论 | `{ id:'no-diagnosis', kind:'output-forbid', pattern:'有效\|好转\|治愈\|确诊为', action:'block-and-rewrite' }` |
|
|
82
|
+
| 不得跨患者串病历 | `{ id:'no-cross-patient', kind:'tool-arg-require', tool:'intake_collect', requireArg:'patientId' }` |
|
|
83
|
+
|
|
84
|
+
执行时机:调用工具前(工具/参数)、工具返回后(缺项则**拒绝该结果**并回填「请继续采集」)、正文输出前(**回填会话历史之前**改写/拦截)。命中写审计事件。
|
|
85
|
+
|
|
86
|
+
> 注意:`output-forbid` 的 `block` / `block-and-rewrite` **不回显**命中的措辞——回显会把违规表述重新写进正文与历史。
|
|
87
|
+
|
|
88
|
+
### 3.3 域内模型调用 → `ctx.llm()`
|
|
89
|
+
|
|
90
|
+
迁移前(费用隐身):
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
const res = await fetch(`${deepseekBaseUrl}/chat/completions`, { … });
|
|
94
|
+
return { text, usage: { prompt_tokens: 0, completion_tokens: 0 } }; // ← 这笔钱谁也看不到
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
迁移后:
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
async function deepseekJson(system, user, maxTokens = 2000) {
|
|
101
|
+
const r = await ctx.llm({ model: 'deepseek-v4-flash', system, user, maxTokens, json: true, purpose: 'patient-extract' });
|
|
102
|
+
return r.data; // json:true 时内核已解析
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
收益:usage 并入当前回合 → 今日费用 / 缓存命中率 / 峰谷 / 日费用护栏**同时生效**;并写一条 Pack 归因记录(`cost=null` 标记,与回合级总账不重复计费),`mingdao cost --by pack` 可见。
|
|
107
|
+
|
|
108
|
+
`purpose` 建议取值:`patient-extract`(患者识别)、`intake-extract`(结构化落盘)、`visit-compare`(四态对比)、`followup-script`(随访话术)。
|
|
109
|
+
|
|
110
|
+
### 3.4 患者注册表与快照落盘 → 受权限约束的 IO
|
|
111
|
+
|
|
112
|
+
`patients.json` / `intake/**` 的读写改用 `permissions.fs` 声明的路径(内核据此校验越界):
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
"permissions": { "fs": ["<MINGDAO_HOME>/patients.json", "<MINGDAO_HOME>/intake/**"] }
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
同时建议把 `saveRegistry` 的 `writeFileSync` 换成上游的原子写语义(避免崩溃时半写)——这是**下游自己的代码**,上游只提供契约。
|
|
119
|
+
|
|
120
|
+
### 3.5 领域提示词 → `promptSections`
|
|
121
|
+
|
|
122
|
+
把现在散在 `chat()` 里的角色/边界说明提炼成 `prompts/domain.md`,以 `promptSections` 注入:
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
"promptSections": ["prompts/domain.md"]
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
内核按 `order` + `pack/id` **确定性排序**,字节稳定 → 不破坏 DeepSeek 前缀缓存。
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## 四、迁移步骤(建议顺序)
|
|
133
|
+
|
|
134
|
+
1. `mingdao pack new tcm`(在 Deyi 仓库里生成 `packs/tcm/` 脚手架);
|
|
135
|
+
2. 把 `dify.mjs` 的 `intakeCollect` / `visitCompare` / `followupDashboard` / `patientFollowup` 搬到 `pack.mjs`,
|
|
136
|
+
函数体基本不动,只把「读凭据 + 直接 fetch」换成 `ctx.llm` + `ctx` 提供的只读信息;
|
|
137
|
+
3. 把患者的 JSON 读写挂到 `permissions.fs` 声明的路径;
|
|
138
|
+
4. 把三条红线写成 `constraints`;
|
|
139
|
+
5. 把领域提示词抽成 `prompts/domain.md`;
|
|
140
|
+
6. `layer/providers/dify.mjs` 只保留 **Dify 协议适配**(`createProvider` → `chat`),
|
|
141
|
+
域逻辑全部移出;`install.sh` 改为安装 `layer/packs/` 到 `$MINGDAO_HOME/packs/`;
|
|
142
|
+
7. `mingdao pack verify ./layer/packs/tcm` 必须退出 0;
|
|
143
|
+
8. 在 Deyi CI 里加:`mingdao pack verify ./layer/packs/tcm`。
|
|
144
|
+
|
|
145
|
+
**验收(DoD)**:
|
|
146
|
+
- 域内每次 DeepSeek 调用都出现在 `mingdao cost --by pack` 里;
|
|
147
|
+
- 三条红线可被测试**阻断**(缺项 / 结论性措辞 / 缺 patientId 各一条断言);
|
|
148
|
+
- 三个工具在 WebUI 显示为正常工具卡片;
|
|
149
|
+
- 同名多命中仍返回候选列表(不静默合并)——行为不回归。
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## 五、兼容性与版本
|
|
154
|
+
|
|
155
|
+
| 项 | 约定 |
|
|
156
|
+
| --- | --- |
|
|
157
|
+
| 声明 | `pack.json` 写 `apiVersion: 1` + `engines.mingdao: ">=0.5 <0.7"` |
|
|
158
|
+
| 支持窗口 | 上游承诺支持最近 2 个 minor(0.5 / 0.6 支持 v1) |
|
|
159
|
+
| 上游变更 | 任何 Pack API 变更同步更新 `PACK-API.md` + `CHANGELOG-PACK.md` + 兼容性矩阵 |
|
|
160
|
+
| 下游义务 | 每个上游 minor 发布后跑一次 `pack verify`,并入 CI |
|
|
161
|
+
| 破坏性变更 | 走 major + 迁移指南(如可行再配 codemod) |
|
|
162
|
+
|
|
163
|
+
**内核 bug 一律在上游修**:下游遇到的问题如果是「扩展点不够用/行为不对」,直接反馈上游,
|
|
164
|
+
不要在下游 fork 内核——这条边界是上下游能长期并行的前提。
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## 六、上游仍需补齐(下游可先按本节设计,勿依赖)
|
|
169
|
+
|
|
170
|
+
- `ctx.storage`:Pack 私有持久化命名空间(当前用 `permissions.fs` 显式路径替代);
|
|
171
|
+
- `ctx.provider`:由 Pack 贡献非 OpenAI 兼容 Provider(当前 Dify 适配仍放 `<home>/providers/`);
|
|
172
|
+
- Pack 私有存储加密(医疗 PII)→ v0.6.0「合规与确定性」;
|
|
173
|
+
- 执行账本导出 / 可回放(确定性③)→ v0.6.0。
|
|
174
|
+
|
|
175
|
+
以上四项在 v0.5.0 **不可用**,请勿在回迁中依赖。
|