mingdao-harness 0.4.4 → 0.4.6
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/docs/ARCHITECTURE.md +1 -1
- package/docs/AUDIT-v0.4.6.md +179 -0
- package/docs/PACK-API.md +250 -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 +2 -1
- 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 +95 -9
- package/src/atomic-write.js +9 -0
- package/src/autostart.js +37 -3
- package/src/batch.js +9 -1
- package/src/cachestats.js +32 -8
- package/src/cli.js +29 -14
- package/src/commands/repl.js +5 -3
- package/src/commands/update.js +1 -1
- package/src/compact.js +16 -1
- package/src/context.js +8 -0
- package/src/cost-guard.js +26 -6
- package/src/hooks.js +20 -4
- package/src/log-writer.js +19 -7
- package/src/mcp.js +4 -0
- package/src/memory.js +6 -5
- package/src/model-caps.js +48 -4
- package/src/permissions.js +10 -4
- package/src/presets.js +13 -3
- package/src/pricing.js +78 -18
- package/src/providers/index.js +43 -13
- package/src/providers/openai-compatible.js +42 -1
- package/src/redact.js +5 -0
- package/src/schedule.js +62 -30
- package/src/session.js +14 -1
- package/src/skill-lib.js +23 -4
- package/src/skills.js +2 -2
- package/src/sync-server.js +29 -1
- package/src/sync.js +6 -3
- package/src/tasks/worker.js +4 -1
- package/src/tasks.js +4 -1
- package/src/tokenizer.js +48 -11
- package/src/tools/bash.js +9 -2
- package/src/tools/fetch.js +97 -8
- package/src/tools/fs-tools.js +48 -3
- package/src/tools/git.js +17 -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 +41 -31
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。
|
package/docs/PACK-API.md
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# 垂域 Pack API v1(草案)
|
|
2
|
+
|
|
3
|
+
> 状态:**草案 / 待实现**(目标版本 v0.5.0)。本文是上游与下游之间的接口契约。
|
|
4
|
+
> 定位:让垂域团队(中医、法律、教育、制造、政务…)**不修改内核源码**就能做出可私有化、可审计、受约束的智能体。
|
|
5
|
+
> 关联:`STRATEGY-0.5.md`(战略)、`DEVELOPER.md`(现有扩展点)、`CONFIG.md`(配置)。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 0. 为什么需要 Pack(下游实证)
|
|
10
|
+
|
|
11
|
+
`Deyi-TCM-Harness` 今天把整个中医域逻辑写进了 `<home>/providers/dify.mjs` 的 `chat()` 里。这不是下游的错——是上游缺抽象。由此造成的损失:
|
|
12
|
+
|
|
13
|
+
| 应有的能力 | 现状 |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| 权限引擎门控 | 域内「工具」是 `chat()` 里的正则匹配,绕过 `permissions.js` |
|
|
16
|
+
| 审计追溯 | 不写 `audit.jsonl` |
|
|
17
|
+
| 费用与护栏 | 域内模型调用硬编码 `usage: 0`,不计费、不触护栏 |
|
|
18
|
+
| UI 工具卡片 / 流式思考 | 只能手工 `onDelta({text})` |
|
|
19
|
+
| 独立版本与兼容 | 整文件覆盖,无 `apiVersion`、无 CI 校验 |
|
|
20
|
+
| 记忆 / 技能 / 预设复用 | 全部用不上 |
|
|
21
|
+
|
|
22
|
+
**Pack 就是把这个缺口补上**:把垂域能力变成内核的一等公民,与内置工具走同一条链路(权限 → 审计 → schema 瘦身 → 费用归因 → 约束校验)。
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 1. 目录与文件
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
<pack-dir>/
|
|
30
|
+
pack.json # manifest:身份、版本、兼容窗口、贡献声明(可静态校验)
|
|
31
|
+
pack.mjs # contributions:导出 createPack(ctx) → { tools, ... }(按需)
|
|
32
|
+
prompts/*.md # 可选:提示词段
|
|
33
|
+
skills/*/SKILL.md # 可选:随包技能(复用现有 SKILL.md 格式)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
安装位置(三级遮蔽,优先级高者胜):
|
|
37
|
+
1. `<项目>/.mingdao/packs/<name>/`(项目级,私有)
|
|
38
|
+
2. `<MINGDAO_HOME>/packs/<name>/`(用户级)
|
|
39
|
+
3. 包内 `packs/`(内置,仅官方参考实现使用)
|
|
40
|
+
|
|
41
|
+
`config.json` 声明:
|
|
42
|
+
|
|
43
|
+
```jsonc
|
|
44
|
+
{
|
|
45
|
+
"packs": [
|
|
46
|
+
"./packs/tcm", // 本地目录
|
|
47
|
+
"npm:@mingdao/pack-legal", // npm 包(需 --allow-npm)
|
|
48
|
+
"https://example.com/pack-tcm.tgz" // 归档(需 --allow-remote + sha256)
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 2. `pack.json`(manifest)
|
|
56
|
+
|
|
57
|
+
```jsonc
|
|
58
|
+
{
|
|
59
|
+
"apiVersion": 1, // 必填:本 Pack 面向的 Pack API 主版本
|
|
60
|
+
"name": "tcm", // 必填:唯一名([a-z0-9-]{2,32})
|
|
61
|
+
"displayName": "中医垂域层",
|
|
62
|
+
"version": "0.2.0", // 必填:Pack 自身版本(semver)
|
|
63
|
+
"engines": { "mingdao": ">=0.5 <0.7" },// 必填:兼容的内核版本窗口
|
|
64
|
+
"description": "中医问诊采集 / 复诊四态 / 回访追踪",
|
|
65
|
+
"author": "…",
|
|
66
|
+
"license": "private",
|
|
67
|
+
|
|
68
|
+
"permissions": { // Pack 声明它需要的宿主能力(最小权限,加载时校验)
|
|
69
|
+
"fs": ["<home>/intake/**", "<home>/patients.json"],
|
|
70
|
+
"net": ["https://dify.example.com"],
|
|
71
|
+
"env": ["DIFY_API_KEY"]
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
"contributes": {
|
|
75
|
+
"tools": true, // 由 pack.mjs 提供
|
|
76
|
+
"provider": "dify", // 复用/覆盖 Provider 名
|
|
77
|
+
"presets": ["presets/tcm.json"],
|
|
78
|
+
"promptSections": ["prompts/tcm-domain.md"],
|
|
79
|
+
"constraints": ["constraints.json"],
|
|
80
|
+
"skills": ["skills/tcm-intake"],
|
|
81
|
+
"commands": ["tcm:recall", "tcm:followup"]
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**校验规则(加载时即失败并告警,绝不崩启动):**
|
|
87
|
+
- `apiVersion` 不在内核支持列表 → 拒绝加载,提示升级内核或降级 Pack;
|
|
88
|
+
- `engines.mingdao` 与本内核版本不匹配 → 拒绝加载;
|
|
89
|
+
- `name` 冲突 / 保留名(`mcp`、`core`)→ 拒绝;
|
|
90
|
+
- `permissions.fs` 越出 `config.fsAllowDirs` → 拒绝;
|
|
91
|
+
- 任何 contributions 声明但文件缺失 → 拒绝该条并告警,其余继续。
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## 3. `pack.mjs`(contributions)
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
// 垂域 Pack 入口:纯 ESM,**不引入任何 npm 依赖**(与内核同约束)
|
|
99
|
+
export const apiVersion = 1;
|
|
100
|
+
|
|
101
|
+
export function createPack(ctx) {
|
|
102
|
+
// ctx(宿主注入,只读 + 受控能力):
|
|
103
|
+
// ctx.home MINGDAO_HOME
|
|
104
|
+
// ctx.workingDir 当前工作目录
|
|
105
|
+
// ctx.cfg 生效配置(只读快照)
|
|
106
|
+
// ctx.log(msg) 写入内核日志(自动脱敏)
|
|
107
|
+
// ctx.llm(opts) 统一模型出口(见 §5,自动计入费用/护栏/审计)
|
|
108
|
+
// ctx.readJson(p)/writeJsonAtomic(p, o) 受 permissions.fs 白名单约束的原子读写
|
|
109
|
+
// ctx.audit(entry) 写审计事件(自动带 pack 名)
|
|
110
|
+
// ctx.storage 包私有持久化命名空间(<home>/packs/<name>/data/)
|
|
111
|
+
return {
|
|
112
|
+
tools: [
|
|
113
|
+
{
|
|
114
|
+
name: 'intake_collect', // 内核自动加前缀:pack__tcm__intake_collect
|
|
115
|
+
description: '中医问诊采集:缺失必填项必须继续追问,不得编造。',
|
|
116
|
+
parameters: { type: 'object', properties: { patientId: { type: 'string' } }, required: ['patientId'] },
|
|
117
|
+
readOnly: false, // 进权限引擎;readOnly 的进只读档与只读子代理
|
|
118
|
+
// 声明本工具受哪些约束(约束引擎在 PreToolUse / PostToolUse 强制)
|
|
119
|
+
constraints: ['ten-questions-complete', 'no-cross-patient'],
|
|
120
|
+
async run(args, toolCtx) { // toolCtx = ctx + { signal, sessionRef }
|
|
121
|
+
const prev = await toolCtx.readJson(`intake/${args.patientId}/latest.json`).catch(() => null);
|
|
122
|
+
const summary = await toolCtx.llm({ model: 'deepseek-v4-flash', system: '…', user: '…' });
|
|
123
|
+
return { ok: true, output: '…', data: { prev, summary } }; // data 不进模型上下文,仅供 UI/约束
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
|
|
128
|
+
// 领域提示词段:注入系统提示的独立段落(预设/记忆/技能之后,可声明顺序)
|
|
129
|
+
promptSections: [{ id: 'tcm-domain', order: 100, content: '你是中医知识工作助手,不输出诊疗结论。' }],
|
|
130
|
+
|
|
131
|
+
// 领域约束(见 §4)
|
|
132
|
+
constraints: [
|
|
133
|
+
{ id: 'no-diagnosis-conclusion', kind: 'output-forbid', pattern: '有效|好转|治愈|确诊为', action: 'block' },
|
|
134
|
+
{ id: 'ten-questions-complete', kind: 'completeness', tool: 'intake_collect',
|
|
135
|
+
fields: ['zhushu','zhenduan','hanre','han','toushen','erbian','yinshi','xiongfu','kouke','jiubing'],
|
|
136
|
+
onMissing: 'reject' },
|
|
137
|
+
{ id: 'no-cross-patient', kind: 'tool-arg-require', tool: 'intake_read', requireArg: 'patientId', action: 'block' },
|
|
138
|
+
],
|
|
139
|
+
|
|
140
|
+
// 可选:领域记忆条目的结构(内核按此做语义检索与注入)
|
|
141
|
+
memorySchema: { fields: ['patientId', 'fact', 'source', 'at'] },
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**命名与隔离**
|
|
147
|
+
- 所有 Pack 工具在内核注册为 `pack__<packName>__<toolName>`,避免跨 Pack 与内置冲突;
|
|
148
|
+
- 权限规则、审计事件、费用分账、UI 卡片全部带 `pack` 维度;
|
|
149
|
+
- Pack 之间默认互不可见(除非 manifest 显式 `dependsOn`)。
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## 4. 约束引擎 v1
|
|
154
|
+
|
|
155
|
+
约束是**内核级强制**,不是提示词建议。执行时机三处:
|
|
156
|
+
|
|
157
|
+
| 时机 | 可用的 kind | 行为 |
|
|
158
|
+
| --- | --- | --- |
|
|
159
|
+
| PreToolUse | `tool-deny`、`tool-arg-require`、`arg-forbid` | 命中即阻止执行,回填工具错误给模型,写审计 |
|
|
160
|
+
| PostToolUse | `completeness`、`result-forbid` | `completeness` 缺项 → 拒绝该工具结果,要求模型补采;`result-forbid` → 屏蔽结果并提示 |
|
|
161
|
+
| 输出前 | `output-forbid`、`require-citation` | 命中 → 按 `action` 处理:`block`(改为固定合规文案)/ `block-and-rewrite`(再请求一次修正)/ `warn`(放行并标注) |
|
|
162
|
+
|
|
163
|
+
约束事件统一结构(进执行账本):
|
|
164
|
+
|
|
165
|
+
```jsonc
|
|
166
|
+
{ "at": 1757…, "pack": "tcm", "constraint": "no-diagnosis-conclusion",
|
|
167
|
+
"kind": "output-forbid", "stage": "pre-output", "action": "block-and-rewrite",
|
|
168
|
+
"matched": "好转", "session": "…", "model": "deepseek-v4-flash" }
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**设计原则**
|
|
172
|
+
- 约束**只能收紧、不能放松**权限(约束不授予任何权限);
|
|
173
|
+
- 约束失败**默认 fail-closed**(拿不准就阻断并提示),与 hooks 同口径;
|
|
174
|
+
- 每条约束必须有 `id`,便于审计与测试;
|
|
175
|
+
- 提供 `mingdao constraint test <pack>`:对每条约束跑一遍内置反例样本(下游 CI 用)。
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## 5. 统一模型出口 `ctx.llm()`
|
|
180
|
+
|
|
181
|
+
Pack 内**禁止**自己 `fetch` 模型接口(否则费用隐身、护栏失效、无法归因)。统一走:
|
|
182
|
+
|
|
183
|
+
```js
|
|
184
|
+
const out = await ctx.llm({
|
|
185
|
+
model: 'deepseek-v4-flash',
|
|
186
|
+
system: '…', user: '…',
|
|
187
|
+
maxTokens: 2000,
|
|
188
|
+
reasoningEffort: 'off', // 复用内核的模型能力表与参数校验
|
|
189
|
+
json: true, // 结构化输出(内核负责解析与重试)
|
|
190
|
+
purpose: 'patient-extract', // 归因标签(进账本与分账)
|
|
191
|
+
});
|
|
192
|
+
// → { text, data, usage: { prompt_tokens, completion_tokens, prompt_cache_hit_tokens, … } }
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
内核在 `ctx.llm()` 内自动完成:
|
|
196
|
+
1. 走 `resolveProviderConfig` 与 Provider 重试/超时策略;
|
|
197
|
+
2. usage 并入当前回合 → **今日费用、缓存命中率、峰谷、日费用护栏全部生效**;
|
|
198
|
+
3. 分账维度记录 `(pack, tool?, purpose, model, session)`;
|
|
199
|
+
4. 写审计事件(脱敏);
|
|
200
|
+
5. 受当前权限模式与预算约束(超预算按护栏 action 处理)。
|
|
201
|
+
|
|
202
|
+
> 这一条直接修复「域内调用 `usage: 0`」问题——**Pack 内不可能再有隐身花费**。
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## 6. 兼容性与版本策略
|
|
207
|
+
|
|
208
|
+
| 内核版本 | Pack API | 承诺 |
|
|
209
|
+
| --- | --- | --- |
|
|
210
|
+
| 0.5.x | v1 | 冻结;minor 只增不改;Pack 无需改动即可升级 |
|
|
211
|
+
| 0.6.x | v1 | 继续支持(窗口 = 最近 2 个 minor) |
|
|
212
|
+
| 1.0.x | v2(若需要) | major 升级,提供迁移指南 + codemod(可行时) |
|
|
213
|
+
|
|
214
|
+
**下游义务**:在 `pack.json` 声明 `apiVersion` 与 `engines.mingdao`,并把 `mingdao pack verify` 放进 CI。
|
|
215
|
+
|
|
216
|
+
**上游义务**:任何 Pack API 变更必须同步更新本文 + 兼容性矩阵 + `docs/CHANGELOG-PACK.md`。
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## 7. CLI
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
mingdao pack list # 已加载 Pack / 来源 / 版本 / 兼容状态
|
|
224
|
+
mingdao pack verify <dir> # 静态校验 manifest + 文件齐全 + 约束合法性(下游 CI 门禁)
|
|
225
|
+
mingdao pack new <name> # 脚手架
|
|
226
|
+
mingdao pack info <name> # 贡献面:工具/约束/提示词段/权限/费用统计
|
|
227
|
+
mingdao pack test <name> # 跑内置反例样本(约束 + 工具契约)
|
|
228
|
+
mingdao constraint test <name> # 单独跑约束反例
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## 8. 与现有扩展点的关系(不替代,只补位)
|
|
234
|
+
|
|
235
|
+
| 现有扩展点 | 定位 | Pack 的关系 |
|
|
236
|
+
| --- | --- | --- |
|
|
237
|
+
| 自定义 Provider(`providers/*.mjs`) | 接入非 OpenAI 兼容协议 | Pack 可**贡献** Provider,但域逻辑不应再写进 `chat()` |
|
|
238
|
+
| Agent Preset(JSON) | 声明式智能体(提示词+工具白名单+权限+模型) | Pack 可携带 Preset;Preset 仍是用户可选的「档位」 |
|
|
239
|
+
| `registerTool`(程序化) | 单工具注册 | Pack 是其**批量 + 声明式 + 可分发**的封装(内核内部仍走 `registerTool`) |
|
|
240
|
+
| `config.tools`(shell 包装) | 无代码的简单工具 | 保持不变(零代码场景);需要状态/约束/归因时用 Pack |
|
|
241
|
+
| Hooks | 生命周期拦截(外部进程) | 约束引擎是**进程内、可移植、可测试**的规则层;Hooks 仍可叠加 |
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## 9. 待定问题(实现前需拍板)
|
|
246
|
+
|
|
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 「合规与确定性」阶段再定。
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
- **离线零 API 成本**:全部用项目内置的计价函数(`pricing.js`)、精确词表(`tokenizer.js`)、工具 Schema 生成器(`tools/index.js`)、结果截断(`context.js clampText`)做「朴素基线 vs MingDao 优化」对照,不调用任何真实模型端点。
|
|
11
11
|
- **确定性**:计价时点固定为**闲时**(`2026-08-15T04:00:00Z`,周六 12:00 北京时间)与**高峰**(`2026-09-02T02:00:00Z`,周三 10:00 北京时间),避免真实时钟跨峰谷导致断言漂移。
|
|
12
12
|
- **回归门禁**:每个省钱杠杆都有阈值断言(省 X% 只降不升);未来任何改动若让省钱幅度跌破阈值,`npm run bench` 直接红灯。
|
|
13
|
-
- **诚实边界**:各杠杆是**正交可叠加**的,但不会全部同时生效;「综合
|
|
13
|
+
- **诚实边界**:各杠杆是**正交可叠加**的,但不会全部同时生效;「综合 51%」是简单平均(衡量各杠杆各自的省钱幅度),不是"总费用打 6 折"的承诺。实际一次请求同时命中几个杠杆,省多少,取决于任务形态。
|
|
14
14
|
|
|
15
15
|
## 二、任务集(8 项,随版本复测)
|
|
16
16
|
|
|
@@ -19,13 +19,20 @@
|
|
|
19
19
|
| ① | 缓存命中计价 | 90% 命中 vs 全未命中(命中价 1/30) | **省 84.5%**(¥0.15450 → ¥0.02400) |
|
|
20
20
|
| ② | 峰谷避峰 | 闲时 vs 高峰(闲时半价) | **省 50.0%**(¥0.11700 → ¥0.05850) |
|
|
21
21
|
| ③ | Batch 半价 | 批量通道 vs 标准(半价) | **省 50.0%** |
|
|
22
|
-
| ④ | 工具 Schema 瘦身 | 已用工具剥描述 vs 全量 | **省
|
|
23
|
-
| ⑤ | 只读阶段收缩 |
|
|
22
|
+
| ④ | 工具 Schema 瘦身 | 已用工具剥描述 vs 全量 | **省 48.9%**(1145 → 585 tokens,每轮 ≈¥0.00084) |
|
|
23
|
+
| ⑤ | 只读阶段收缩 | 真实只读档(9 工具,含 task) vs 全量 | **省 28.9%**(1145 → 814 tokens) |
|
|
24
24
|
| ⑥ | 工具结果截断 | clampText 上限 vs 原文 | **省 91.7%**(186150 → 15488 tokens) |
|
|
25
|
-
| ⑦ |
|
|
25
|
+
| ⑦ | 启发式计数类别化 | 标点/数字/单字母词为硬上界;自然语言误差 ≤15% | 中英混排 exact 740 ≤ heuristic 755 |
|
|
26
26
|
| ⑧ | 推理分级契约 | pro 支持 low/high/max(off 可关)、flash 不发 | 契约通过 |
|
|
27
27
|
|
|
28
|
-
**综合(简单平均)**:省 **
|
|
28
|
+
**综合(简单平均)**:省 **51%**(各杠杆独立口径,非叠加承诺)。
|
|
29
|
+
|
|
30
|
+
> **v0.4.6 口径修正(重要)**:此前本表写「综合 63%」「④省 52.1%」「⑤省 52.0%」,其中两处不实:
|
|
31
|
+
> ① ④⑤ 用的是**启发式**计数,而这两项是针对 DeepSeek 的省钱主张,应当用随包官方词表的**精确**计数
|
|
32
|
+
> (JSON 结构字符多,启发式偏差 1.1–1.8 倍);
|
|
33
|
+
> ② ⑤ 的基准里维护了一份**过期的只读档工具副本**(6 个工具),而实现的只读档在 v0.4.4 已加入 `task`
|
|
34
|
+
> (描述很长)——真实只读档为全量的 ~71%,即只省 29%,不是 52%。
|
|
35
|
+
> 现两项均改为精确计数 + 从 `agent.js` 单源导入只读档集合,并新增类别化断言防止再次虚高。
|
|
29
36
|
|
|
30
37
|
## 三、与其它 bench 的分工
|
|
31
38
|
|