mingdao-harness 0.6.0 → 0.6.2
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 +1 -1
- package/docs/AUDIT-v0.6.1-/347/254/254/344/270/211/346/226/271/346/212/245/345/221/212/347/231/273/350/256/260.md +647 -0
- package/docs/MIGRATION-DEYI-v0.5.md +18 -0
- package/docs/PACK-API.md +62 -5
- package/docs/RELEASE-CHECKLIST.md +210 -7
- package/docs/RELEASE-TRAIN.md +20 -1
- package/package.json +1 -1
- package/src/agent.js +69 -6
- package/src/atomic-write.js +22 -2
- package/src/audit.js +45 -15
- package/src/autostart.js +67 -16
- package/src/cachestats.js +66 -13
- package/src/cli.js +16 -7
- package/src/commands/desktop.js +2 -1
- package/src/commands/diagnose.js +11 -2
- package/src/commands/ledger.js +7 -3
- package/src/commands/pack.js +58 -4
- package/src/commands/repl.js +28 -13
- package/src/commands/schedule.js +15 -2
- package/src/commands/sync.js +102 -20
- package/src/commands/update.js +2 -1
- package/src/commands/workspace.js +8 -1
- package/src/config.js +4 -4
- package/src/constraints.js +31 -5
- package/src/cost-guard.js +4 -3
- package/src/hooks.js +5 -2
- package/src/ledger.js +110 -10
- package/src/mcp-presets.js +25 -4
- package/src/mcp.js +3 -1
- package/src/memory.js +64 -18
- package/src/model-discovery.js +38 -5
- package/src/models.js +57 -2
- package/src/net-guard.js +35 -11
- package/src/notify.js +4 -3
- package/src/packs.js +155 -9
- package/src/permissions.js +24 -0
- package/src/proc.js +94 -4
- package/src/prompts.js +36 -3
- package/src/redact.js +57 -0
- package/src/routing.js +14 -8
- package/src/safe-fetch.js +83 -0
- package/src/schedule.js +81 -12
- package/src/session-index.js +26 -2
- package/src/skill-lib.js +25 -46
- package/src/skill-registry.js +55 -30
- package/src/task-state.js +50 -5
- package/src/tasks/worker.js +2 -1
- package/src/tasks.js +74 -10
- package/src/tokenizer.js +76 -6
- package/src/tools/bash.js +3 -1
- package/src/tools/fs-tools.js +31 -13
- package/src/tools/git.js +31 -2
- package/src/tools/index.js +24 -1
- package/src/ui.js +33 -1
- package/src/web/app.js +10 -4
- package/src/web/routes/domains/config.js +10 -4
- package/src/web/routes/domains/workspace.js +3 -1
- package/src/web/server.js +31 -7
- package/src/web/util.js +20 -0
- package/src/workspace.js +46 -5
|
@@ -219,6 +219,24 @@ mingdao key set deepseek <你的 DeepSeek Key> # 写入 <MINGDAO_HOME>/crede
|
|
|
219
219
|
7. `mingdao pack verify ./layer/packs/tcm` 必须退出 0;
|
|
220
220
|
8. 在 Deyi CI 里加:`mingdao pack verify ./layer/packs/tcm`。
|
|
221
221
|
|
|
222
|
+
> ⚠ **v0.6.2 行为变更:项目级 Pack 默认不再挂载。**
|
|
223
|
+
> `<项目>/.mingdao/packs/` 里的 `pack.mjs` 会被 `await import()` 到**同一进程、完整 Node 权限**,
|
|
224
|
+
> 于是「clone 一个不可信仓库 + cd 进去 + 跑任意 mingdao 子命令」就能执行仓库里的任意代码,
|
|
225
|
+
> 且与 `permission` 模式无关。所以上游加了信任门(详见 `docs/PACK-API.md` §1.1)。
|
|
226
|
+
>
|
|
227
|
+
> **对 Deyi 的影响**:若 Pack 放在 `<Deyi 仓库>/.mingdao/packs/`,升级到 v0.6.2 后
|
|
228
|
+
> **不会自动加载**,启动时会打印「项目级 Pack 未挂载(未信任)」告警。一次性处理二选一:
|
|
229
|
+
>
|
|
230
|
+
> ```bash
|
|
231
|
+
> # A. 记一次内容指纹信任(之后改过代码需重新执行——这正是不被静默劫持的意义)
|
|
232
|
+
> mingdao pack trust <Deyi 仓库根目录>
|
|
233
|
+
>
|
|
234
|
+
> # B. 或写进用户配置(显式声明不受信任门限制,适合本来就装在别处的 Pack)
|
|
235
|
+
> # ~/.mingdao/config.json: { "packs": ["<Deyi 仓库>/layer/packs/tcm"] }
|
|
236
|
+
> ```
|
|
237
|
+
>
|
|
238
|
+
> 若 `install.sh` 把 Pack 装到 `$MINGDAO_HOME/packs/`(用户级),**不受此变更影响**。
|
|
239
|
+
|
|
222
240
|
**验收(DoD)**:
|
|
223
241
|
- 域内每次 DeepSeek 调用都出现在 `mingdao cost --by pack` 里;
|
|
224
242
|
- 三条红线可被测试**阻断**(缺项 / 结论性措辞 / 缺 patientId 各一条断言);
|
package/docs/PACK-API.md
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
安装位置(三级遮蔽,优先级高者胜):
|
|
38
|
-
1. `<项目>/.mingdao/packs/<name
|
|
38
|
+
1. `<项目>/.mingdao/packs/<name>/`(项目级,私有)—— **默认不挂载,需先信任**(见 §1.1)
|
|
39
39
|
2. `<MINGDAO_HOME>/packs/<name>/`(用户级)
|
|
40
40
|
3. 包内 `packs/`(内置,仅官方参考实现使用)
|
|
41
41
|
|
|
@@ -44,13 +44,51 @@
|
|
|
44
44
|
```jsonc
|
|
45
45
|
{
|
|
46
46
|
"packs": [
|
|
47
|
-
"./packs/tcm", //
|
|
47
|
+
"./packs/tcm", // ① 直接指向 Pack 目录(该目录下有 pack.json)
|
|
48
|
+
"./packs", // ② 或指向"装着若干 Pack 的根目录"(扫其子目录)
|
|
48
49
|
"npm:@mingdao/pack-legal", // npm 包(需 --allow-npm)
|
|
49
50
|
"https://example.com/pack-tcm.tgz" // 归档(需 --allow-remote + sha256)
|
|
50
51
|
]
|
|
51
52
|
}
|
|
52
53
|
```
|
|
53
54
|
|
|
55
|
+
> 两种形态**都支持**(v0.6.2 起)。此前实现只认形态②(一律当根目录扫子目录),
|
|
56
|
+
> 而文档与 `MIGRATION-DEYI-v0.5.md` 写的都是形态①——于是**按文档声明的 Pack 一个都发现不了**。
|
|
57
|
+
> 顺带修掉 `mingdao pack list` 硬传 `{}` 读不到 `config.packs` 的问题(代码审计 P2-4)。
|
|
58
|
+
|
|
59
|
+
### 1.1 项目级 Pack 的信任门(v0.6.2 起,**行为变更**)
|
|
60
|
+
|
|
61
|
+
**项目级 Pack 默认不挂载。** 必须先显式信任:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
mingdao pack trust <项目目录> # 记录当前内容指纹为已信任
|
|
65
|
+
mingdao pack untrust <项目目录> # 撤销
|
|
66
|
+
mingdao pack list # 未信任的显示「⛔ 未信任(不挂载)」+ 开启命令
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**为什么**:`pack.mjs` 是 `await import()` 进来的——**同进程、完整 Node 权限**。而项目级
|
|
70
|
+
目录来自工作目录,等于「`git clone` 一个不可信仓库 → `cd` 进去 → 跑任意 `mingdao` 子命令」
|
|
71
|
+
就会执行仓库里的任意代码:可读 `~/.mingdao/credentials.json`、可出网外传、可读 bash 工具
|
|
72
|
+
专门过滤掉的敏感环境变量(`bash.js` 的环境变量黑名单在**进程内代码**面前毫无意义),
|
|
73
|
+
而且**与 `permission` 模式(`ask` / `readonly`)完全无关,也不会询问用户**。
|
|
74
|
+
这不是假想威胁:它是一条只需一次 clone 就能完成的本地代码执行路径。
|
|
75
|
+
|
|
76
|
+
**信任模型**(与 `mingdao skill trust` 同一套「内容指纹」口径):
|
|
77
|
+
|
|
78
|
+
- 指纹 = 目录内全部文件的逐文件 sha256 再聚合(与技能复用同一实现 `skillDirHash`);
|
|
79
|
+
- **指纹变化即自动失效**,需重新 `trust`——改过一行就等于没信任过;
|
|
80
|
+
- 目录键按 `realpath` 归一。否则 macOS 上 `/tmp` → `/private/tmp` 这类符号链接会让
|
|
81
|
+
「trust 记一个路径、运行时查另一个路径」,信任看起来完全不生效
|
|
82
|
+
(这是 CLI 端到端实测抓到的,单元测试因两处用同一字符串而漏掉);
|
|
83
|
+
- 信任表在 `${MINGDAO_HOME}/pack-trust.json`,权限 `0600`。
|
|
84
|
+
|
|
85
|
+
`config.packs` 里**显式声明**的目录**不受此门限制**——那是用户自己写下的授权,
|
|
86
|
+
与「工作目录里恰好躺着的东西」性质不同。用户级(`${MINGDAO_HOME}/packs/`)与内置
|
|
87
|
+
Pack 同样不受限制:前者是用户自己装的,后者随内核分发。
|
|
88
|
+
|
|
89
|
+
> 未信任时**不会静默跳过**:启动会打印告警,说明目录、原因与开启命令。
|
|
90
|
+
> 静默执行与静默跳过同样糟糕——前者是漏洞,后者让人以为 Pack 生效了。
|
|
91
|
+
|
|
54
92
|
---
|
|
55
93
|
|
|
56
94
|
## 2. `pack.json`(manifest)
|
|
@@ -66,7 +104,7 @@
|
|
|
66
104
|
"author": "…",
|
|
67
105
|
"license": "private",
|
|
68
106
|
|
|
69
|
-
"permissions": { //
|
|
107
|
+
"permissions": { // 仅**声明**用途(见下方 ⚠:内核当前不据此强制)
|
|
70
108
|
"fs": ["<home>/intake/**", "<home>/patients.json"],
|
|
71
109
|
"net": ["https://dify.example.com"],
|
|
72
110
|
"env": ["DIFY_API_KEY"]
|
|
@@ -90,10 +128,29 @@
|
|
|
90
128
|
```
|
|
91
129
|
|
|
92
130
|
**校验规则(加载时即失败并告警,绝不崩启动):**
|
|
131
|
+
> ### ⚠ `permissions` 目前只是**声明**,内核不据此强制(v0.6.2 更正)
|
|
132
|
+
>
|
|
133
|
+
> v0.6.2 第三方审计(自评报告 P2-1)指出:`permissions` 全仓**没有任何执行点消费它**,
|
|
134
|
+
> 只有 `validateManifest()` 做形状校验(键名限 `fs`/`net`/`env`、值必须是字符串数组)。
|
|
135
|
+
> 而 `pack.mjs` 是 `await import()` 进宿主进程的——**同进程、完整 Node 权限**,
|
|
136
|
+
> 可读写任意文件、可任意出网、可读所有环境变量。
|
|
137
|
+
>
|
|
138
|
+
> **失真的安全叙事比没有声明更危险**:文档写着「最小权限」「越出即拒绝」,使用者就会放心
|
|
139
|
+
> 安装第三方 Pack。所以本版做了两件事:
|
|
140
|
+
>
|
|
141
|
+
> 1. **把话说准**:本节与 §5 里所有"声称强制"的措辞已标注「尚未实现」;
|
|
142
|
+
> 2. **把差距摆出来**:加载时做一次**静态对照**——若 `pack.mjs` 用到了未声明的能力
|
|
143
|
+
> (`fetch(` → net、`fs.xxx` → fs、`process.env` → env),启动即打印告警,并明确写出
|
|
144
|
+
> 「`permissions` 只是声明、内核不据此强制,安装第三方 Pack 前请人工审阅 `pack.mjs`」。
|
|
145
|
+
>
|
|
146
|
+
> **当前承担安全边界的是「信任门」**(见 §1.1:项目级 Pack 默认不挂载,需 `mingdao pack trust`
|
|
147
|
+
> 记录内容指纹并随内容变化失效)。真正的 `permissions` 强制是**未实现的计划项**——
|
|
148
|
+
> 选型时不要把它当成已存在的沙箱。
|
|
149
|
+
|
|
93
150
|
- `apiVersion` 不在内核支持列表 → 拒绝加载,提示升级内核或降级 Pack;
|
|
94
151
|
- `engines.mingdao` 与本内核版本不匹配 → 拒绝加载;
|
|
95
152
|
- `name` 冲突 / 保留名(`mcp`、`core`)→ 拒绝;
|
|
96
|
-
-
|
|
153
|
+
- ~~`permissions.fs` 越出 `config.fsAllowDirs` → 拒绝~~ ⚠ **尚未实现**(见下方 ⚠ 说明);
|
|
97
154
|
- 任何 contributions 声明但文件缺失 → 拒绝该条并告警,其余继续。
|
|
98
155
|
|
|
99
156
|
---
|
|
@@ -299,7 +356,7 @@ mingdao constraint test <name> # 单独跑约束反例
|
|
|
299
356
|
|
|
300
357
|
1. **Pack 不得覆盖内置同名 Provider**——只允许新增。理由:内置 Provider 名称是内核契约的一部分,允许覆盖会让「内核行为」变成 Pack 可劫持的对象,安全与可预测性都受损。同名冲突在 `pack verify` 与加载时都会被拒绝并给出明确原因。
|
|
301
358
|
2. **`block-and-rewrite` 的修正请求计费归 Pack**。理由:修正是 Pack 的约束触发的额外开销,归到 Pack 才能让垂域团队看到「自己的红线花了多少钱」,也让 Pack 级预算真实反映其成本。
|
|
302
|
-
3. **Pack 内 `fetch` 允许,但必须**:①
|
|
359
|
+
3. **Pack 内 `fetch` 允许,但必须**:① ~~目标在 `permissions.net` 白名单内~~ ⚠ **尚未实现**;② ~~调用强制入账(`purpose` 标记)并可审计~~ ⚠ **尚未实现**(内核只做静态提示,不强制);③ `mingdao pack verify` 对「直连模型端点」的 `fetch` 给出静态告警(引导改用 `ctx.llm()`)——**这一条已实现**。理由:部分垂域必须直连业务系统(HIS/ERP),一刀切禁止会把 Pack 逼回 Provider 里写 `chat()` 的老路——那正是本设计要消灭的形态。
|
|
303
360
|
|
|
304
361
|
### 仍未定(不阻塞 v1,随 v0.5.x 落地)
|
|
305
362
|
|
|
@@ -4,11 +4,41 @@
|
|
|
4
4
|
> npm 首次发布/补发见该文件第 2 步(必须**从 tag** 发布,并用 `--tag backfill` 避免 latest 回退)。
|
|
5
5
|
|
|
6
6
|
> 起因:2026-09-11 明确发版纪律。**本清单是发版的唯一准入流程**——任何一步没做,就不算发布完成。
|
|
7
|
-
>
|
|
8
|
-
> 不允许「GitHub
|
|
7
|
+
> 纪律的核心是两条:**发布前必须由人验收**,以及**发布是「四平台 + 官网」的同一批产物**,
|
|
8
|
+
> 不允许「GitHub 发了、镜像/npm/官网还是旧版本」这种半发布状态。
|
|
9
|
+
|
|
10
|
+
### 0.1 「四平台」的定义(2026-09-13 负责人明确)
|
|
11
|
+
|
|
12
|
+
| 渠道 | 接收什么 | 怎么发 |
|
|
13
|
+
| --- | --- | --- |
|
|
14
|
+
| **GitHub** | 代码(main + tag)+ Release(安装包长期保留) | `git push origin main` + tag 触发 `desktop.yml` |
|
|
15
|
+
| **Gitee** | 代码(main + tag)+ Release(**不附安装包**) | `bash scripts/publish-mirror-releases.sh` |
|
|
16
|
+
| **GitCode** | 同上 | 同上(同一脚本) |
|
|
17
|
+
| **npm** | **版本化包**(不是代码分支) | `npm publish`(见 RELEASE-TRAIN §2;补发用 `--tag backfill`) |
|
|
18
|
+
|
|
19
|
+
> ⚠ **npm 不能「推分支」**:它只接收带版本号的包。所以「代码改动」推的是三个 git 平台,
|
|
20
|
+
> 而**每次发版必须四个渠道齐全**——npm 只在发版时加入。别把这两件事混为一谈。
|
|
21
|
+
>
|
|
22
|
+
> 这条纪律此前只停留在口头,于是负责人不得不专门提醒「应该推送四平台,要包括 npm,以后记住」。
|
|
23
|
+
> **口头约定记不住,脚本才记得住**:发版最后一步必须跑下面这条,它会把四个渠道逐项验一遍。
|
|
9
24
|
|
|
10
25
|
---
|
|
11
26
|
|
|
27
|
+
## 〇.0 最近一次发布复盘(v0.6.0,2026-09-12)
|
|
28
|
+
|
|
29
|
+
本次发布**我自己犯了两个错**,都记在这里:
|
|
30
|
+
|
|
31
|
+
1. **升版本时漏了 `desktop/package.json`** —— 被 smoke 里那条「桌面版版本必须与根一致」的
|
|
32
|
+
静态护栏当场拦下(v0.4.6 审计加的版本漂移防护)。护栏起了作用,说明它值得保留。
|
|
33
|
+
2. **更该记的是操作失误**:我把 `node test/smoke.js | tail -1` 串进了 `&&` 链——
|
|
34
|
+
**管道取的是 `tail` 的退出码**,于是 smoke 明明失败、链条照跑,我对着坏提交打了 tag,
|
|
35
|
+
随后不得不删 tag 重打。教训:**判成败要看 `$?`,不要用管道尾部命令代替**;
|
|
36
|
+
写成 `cmd > out 2>&1; echo $?` 或 `set -o pipefail`。
|
|
37
|
+
|
|
38
|
+
另外新增了 `MingDao-Harness-Site/scripts/update-downloads.mjs`:下载区的机械字段(版本/链接/体积/
|
|
39
|
+
sha256/apt 命令/平台标签)自动更新,替代手工逐个替换;数据源取 GitHub Release 的 `digest`,
|
|
40
|
+
保证 GitHub / 服务器 / 官网三方一致。
|
|
41
|
+
|
|
12
42
|
## 〇、铁律(三条,先记住)
|
|
13
43
|
|
|
14
44
|
1. **未自检不发版**:全门禁必须绿(见 §1)。
|
|
@@ -133,7 +163,8 @@ MINGDAO_HOME=~/.mingdao node src/cli.js web 3820 # 或后台托管:见下
|
|
|
133
163
|
mingdao --version
|
|
134
164
|
mingdao ledger list # 每跑完一次对话就应出现一条账本
|
|
135
165
|
mingdao ledger show # 人读明细:run.start/model.round/tool.call/…/run.end
|
|
136
|
-
mingdao ledger verify #
|
|
166
|
+
mingdao ledger verify # 完整性校验(改一行 → 报「前序哈希不匹配」;删尾部若干行 → 报「账本被截断」)
|
|
167
|
+
# v0.6.2 行为变更:无封条时退 1 并报「完整性无法确认」,不再显示 ✅
|
|
137
168
|
mingdao ledger export --format md --out /tmp/led.md # 导出物应搜不到密钥/私网 IP/家目录
|
|
138
169
|
mingdao ledger replay # 按当前规则重判历史调用(输出四类差异)
|
|
139
170
|
mingdao net policy # 未配置时应显示「闸门未安装」——零影响
|
|
@@ -148,6 +179,140 @@ mingdao audit # 原审计日志行为不变
|
|
|
148
179
|
|
|
149
180
|
> 验收通过 = 明确回复确认。**确认之前不创建 tag、不推 tag、不建 Release。**
|
|
150
181
|
|
|
182
|
+
### 3.0 已暴露的**流程缺口**——均已补进本清单
|
|
183
|
+
|
|
184
|
+
v0.6.0 / v0.6.1 上线后负责人与用户发现下面几个问题,都是**流程漏了一步**,不是代码缺陷:
|
|
185
|
+
|
|
186
|
+
**① 桌面版无法在线更新** —— 我把自动更新的 feed 收割到了 `/downloads/`,
|
|
187
|
+
但桌面版读的是官网 **`/updates/`**(`desktop/main.js` 里写明 `feed: 官网 /updates`)。
|
|
188
|
+
`/updates/*.yml` 还停在 0.4.5,于是应用永远认为「已是最新」。
|
|
189
|
+
两者格式也不同:`/downloads/` 用 GitHub 直链(CI 生成),`/updates/` 用**官网直链**(发布流程生成)。
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
# 已在官网仓库提供生成器,**别再手写**(手写过两次,两次都错):
|
|
193
|
+
ssh mingdao-server 'bash /tmp/gen-update-feeds.sh <版本> <downloads 目录> <updates 目录> https://harness.mingdao.ai/downloads'
|
|
194
|
+
# 源文件:MingDao-Harness-Site/scripts/gen-update-feeds.sh(scp 到服务器再跑)
|
|
195
|
+
#
|
|
196
|
+
# /updates/latest.yml → Windows NSIS exe
|
|
197
|
+
# /updates/latest-linux.yml → Linux AppImage(deb 不支持 electron-updater)
|
|
198
|
+
# /updates/latest-mac.yml → macOS **两个 zip**(arm64 + x64 合并成一份)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
> ⚠ **macOS 必须是 zip,不能是 dmg**。v0.6.0 上线后用户报
|
|
202
|
+
> 「`ZIP file not provided`」,根因就是 mac feed 指向了 dmg——electron-updater 在 macOS 上
|
|
203
|
+
> 要下载 **zip** 解包替换 .app,dmg 只能手动安装。**0.4.5 的 feed 也是 dmg,所以这个错误一直存在**
|
|
204
|
+
> (该步由「服务器侧发布流程」生成,长期没人写对)。相应地在**收割步骤**里也要把两个
|
|
205
|
+
> `MingDao.Harness-<版本>-{arm64-,}mac.zip` 一并放进 downloads,否则 feed 指向的文件不存在。
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
# 验收(三个平台各自取 feed → 下载 → 核对字节 sha512 与 size):
|
|
209
|
+
for f in latest.yml latest-linux.yml latest-mac.yml; do
|
|
210
|
+
curl -s https://harness.mingdao.ai/updates/$f | head -1 # 必须是新版本
|
|
211
|
+
done
|
|
212
|
+
# 注意:从 HTTP 头取 content-length 要用 `tr -d '\r'` 去掉 CR,否则 "$size" = "$act" 会假失败(我踩过)
|
|
213
|
+
```
|
|
214
|
+
**验收**:三份 `/updates/*.yml` 的 `version` 都是新版本,且 `curl https://harness.mingdao.ai/updates/latest.yml | head -1` 即为新版本。
|
|
215
|
+
|
|
216
|
+
**② 代码只推了 tag、没推分支** —— gitee/gitcode 的 `main` 仍停在旧提交(只有 tag 是新的),
|
|
217
|
+
用户在镜像上看到的是过期代码;同时 Gitee 的「最新版」标注也因此对不上。
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
# 发布时必须把分支与 tag 一起推(tag 触发 CI 构建,分支供人阅读/克隆)
|
|
221
|
+
git push <mirror> main:main
|
|
222
|
+
git push <mirror> refs/tags/v<版本>:refs/tags/v<版本>
|
|
223
|
+
```
|
|
224
|
+
**验收**:三平台的 `git ls-remote <url> refs/heads/main` 与本地 `main` 同 SHA;
|
|
225
|
+
`git ls-remote <url> 'refs/tags/v<版本>^{}'` 解析到的提交也一致。
|
|
226
|
+
|
|
227
|
+
> **2026-09-12 又犯了一次,所以改成脚本硬约束。** 现象:GitHub `main` 到了 `d986d1c`,
|
|
228
|
+
> 两个镜像还停在 `f6b7d92` 的 `chore: release v0.6.1`。两个原因叠加:
|
|
229
|
+
> ① 本机**只配了 `origin`**,没有 `gitee`/`gitcode` 两个 remote,于是 `git push origin main`
|
|
230
|
+
> 之后镜像毫无变化;② 发布脚本当时**只推 tag**(`git push gitee "$TAG"`),从不推分支。
|
|
231
|
+
> 文档写了要求,但脚本没落实——**文档约束不住流程,脚本才能**。现在
|
|
232
|
+
> `scripts/publish-mirror-releases.sh` 会:推 tag 前先 `git push <mirror> main:main`;
|
|
233
|
+
> remote 缺失时**直接报错退出**(不再静默只推 tag);并做「main 对齐检查」,
|
|
234
|
+
> 三处 SHA 与本地不一致就退出非 0。
|
|
235
|
+
>
|
|
236
|
+
> 一次性环境准备(SSH,不需要 token;`~/.ssh/config` 里 gitee.com / gitcode.com
|
|
237
|
+
> 已指向 `~/.ssh/mingdao_git`):
|
|
238
|
+
> ```bash
|
|
239
|
+
> git remote add gitee git@gitee.com:MingDaoTCM/MingDao-Harness.git
|
|
240
|
+
> git remote add gitcode git@gitcode.com:MingDaoTCM/MingDao-Harness.git
|
|
241
|
+
> ```
|
|
242
|
+
> 全量核对(**别只比 `main`**——要连全部 tag 一起比,否则漏推的 tag 看不出来):
|
|
243
|
+
> ```bash
|
|
244
|
+
> for r in origin gitee gitcode; do git ls-remote "$r" | sort > /tmp/refs_$r.txt; done
|
|
245
|
+
> diff /tmp/refs_origin.txt /tmp/refs_gitee.txt && diff /tmp/refs_origin.txt /tmp/refs_gitcode.txt
|
|
246
|
+
> ```
|
|
247
|
+
> 2026-09-12 实测:三平台各 105 个 ref(分支 + 全部 tag)名称与 SHA 完全一致。
|
|
248
|
+
|
|
249
|
+
**③ 差量更新素材漏采(v0.6.2 起)** —— 此前**三个平台的自动更新每次都整包重下**
|
|
250
|
+
(exe 76MB / mac zip 93–101MB / AppImage 104MB)。根因不是功能没做,而是发布链路把
|
|
251
|
+
差量素材丢了:CI 的产物 glob 和 Release 上传的 `find` 都没有 `*.blockmap`,
|
|
252
|
+
自研的 feed 生成器也没写 `blockMapSize`。三平台机制**各不相同**,别按一个套路处理:
|
|
253
|
+
|
|
254
|
+
| 平台 | 差量素材 | feed 里需要的字段 |
|
|
255
|
+
| --- | --- | --- |
|
|
256
|
+
| Windows NSIS `exe` | **独立文件** `<exe URL>.blockmap` | 无 |
|
|
257
|
+
| macOS `zip` | **独立文件** `<zip URL>.blockmap` | 无 |
|
|
258
|
+
| Linux `AppImage` | **内嵌在文件尾部** | **必须** `blockMapSize` |
|
|
259
|
+
|
|
260
|
+
依据 electron-updater `providers/Provider.js#getBlockMapFiles`(按 `<安装包 URL>.blockmap`
|
|
261
|
+
取;旧版本块映射按「把 URL 里的新版本号替换成当前版本号」取)与
|
|
262
|
+
`differentialDownloader/FileWithEmbeddedBlockMapDifferentialDownloader.js`
|
|
263
|
+
(`偏移 = size - (blockMapSize + 4)`,**末尾 4 字节是大端 uint32 的块映射长度**)。
|
|
264
|
+
缺素材**不会让更新失败**——electron-updater 捕获异常后回退整包下载——所以这是一个
|
|
265
|
+
**静默**降级,不特意查就发现不了。
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
# 采集:用脚本,别再手写 /tmp/harvest-<版本>.sh(手写漏过 mac zip,见 ①)
|
|
269
|
+
# scp MingDao-Harness-Site/scripts/harvest-release.mjs mingdao-server:/tmp/
|
|
270
|
+
ssh mingdao-server 'MINGDAO_GITHUB_TOKEN=xxx node /tmp/harvest-release.mjs <版本> /opt/1panel/www/sites/mingdao-site/downloads'
|
|
271
|
+
# · 采安装包 + *.blockmap + latest.yml/latest-linux.yml,跳过 latest-mac.yml 与 builder-debug.yml
|
|
272
|
+
# · 每个文件按 GitHub 的 sha256(digest) + size 双校验,不符即删并退出非 0
|
|
273
|
+
# · 末尾会打印「差量更新素材」自查(缺哪个平台的 .blockmap 会直接点名)
|
|
274
|
+
|
|
275
|
+
# 生成 feed(生成器自身也会打印同一份自查)
|
|
276
|
+
ssh mingdao-server 'bash /tmp/gen-update-feeds.sh <版本> <downloads> <updates> https://harness.mingdao.ai/downloads'
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
**验收**:生成器输出里 Windows/mac 三项 `.blockmap` 都是 `✓`,
|
|
280
|
+
且 `grep blockMapSize /updates/latest-linux.yml` 有值;`/updates/latest-linux.yml` 的分区
|
|
281
|
+
应形如 `size: …` 后紧跟 `blockMapSize: …`。
|
|
282
|
+
|
|
283
|
+
> 收益的时间线要说清楚:差量下载需要**本机已缓存上一版的安装包**,且需要**上一版的块映射**
|
|
284
|
+
> 在服务器上。而 0.6.1 及以前从未发布过 `.blockmap`,所以第一个真正吃到差量的是
|
|
285
|
+
> **从 v0.6.2 升到 v0.6.3** 的「一路自动更新上来的」用户;手工下载安装的用户没有缓存,
|
|
286
|
+
> 仍然整包下载。别在发布说明里写成「更新体积立刻变小」。
|
|
287
|
+
|
|
288
|
+
**④ 发行版标题退化成裸版本号(v0.6.2 起)** —— 负责人问「为什么 Gitee 发行版只显示 `v0.6.1`」。
|
|
289
|
+
查下来是三件事,别混为一谈:
|
|
290
|
+
|
|
291
|
+
1. **首页「发行版」组件本来就不渲染「最新版」徽标** —— 这是 Gitee 平台行为,不是我们配错:
|
|
292
|
+
对比 `y_project/RuoYi` 等仓库,首页组件用的是**完全相同的 markup**,同样没有徽标。
|
|
293
|
+
发行版**列表页**与**详情页**都有「最新版」,且 API `GET /releases/latest` 返回的就是 v0.6.1。
|
|
294
|
+
GitHub 首页组件会显示 `Latest` 徽标,Gitee 不会——**这个差异改不了**。
|
|
295
|
+
2. **真正能修的是标题**:Gitee/GitCode 的 `name` 被 `scripts/publish-mirror-releases.sh`
|
|
296
|
+
直接设成了 tag(`"name": sys.argv[1]`),GitHub 侧是 `gh release create --title "$TAG"`。
|
|
297
|
+
于是 v0.4.5 起三个平台的发行版标题**全是裸版本号**,而 v0.4.4 及更早(当年在 Gitee 网页上
|
|
298
|
+
手工建、标题照抄 CHANGELOG)是有描述的——用户看到的「只有一个 v0.6.1」就是这么来的。
|
|
299
|
+
既然是首页组件里唯一能读的信息,标题必须带描述。
|
|
300
|
+
3. **回填历史**:v0.4.5–v0.6.1 共 5 个版本 × 3 平台已按 `## vX.Y.Z(日期)— 描述` 回填标题
|
|
301
|
+
(GitHub `PATCH /releases/{id}`;Gitee 必须带 `tag_name`+`body` 且 token 放 **query**;
|
|
302
|
+
GitCode 用 `PATCH /releases/{tag}` 且载荷必须是 `{tag_name,name,body}`)。
|
|
303
|
+
回填时逐项核对:正文长度与附件数**前后一致**才算成功。
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
# 标题取自 CHANGELOG 的 `## vX.Y.Z(日期)— 描述`,脚本已自动带上,无需手填。
|
|
307
|
+
# 回填历史可用既有通道(复用 CI 的 GITHUB_TOKEN,不需要本地 PAT):
|
|
308
|
+
# Actions → Desktop → Run workflow → normalize_tag 填 v0.6.0(只改正文与标题,不构建)
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
> ⚠ 提取标题**不要用 grep/sed 匹配全角「(」**:本机回填时踩到
|
|
312
|
+
> `grep: illegal byte sequence`(locale 相关),一旦命中就得到**空标题**,
|
|
313
|
+
> 而写空标题会把发行版标题真清掉(v0.6.1 当场被清空过一次,已恢复)。
|
|
314
|
+
> 一律用 `python3` 显式 `encoding='utf8'` 读,且**写之前必须断言标题非空**。
|
|
315
|
+
|
|
151
316
|
## 三、发布(拿到确认后)
|
|
152
317
|
|
|
153
318
|
### 3.1 打 tag 并推送(触发桌面版构建与 GitHub Release)
|
|
@@ -167,14 +332,52 @@ git tag v<版本> && git push origin v<版本>
|
|
|
167
332
|
```bash
|
|
168
333
|
cd MingDao-Harness-Site
|
|
169
334
|
# 1) 服务器 downloads/ 已收到本版本 7 个包(官网收割流程)
|
|
170
|
-
# 2)
|
|
171
|
-
|
|
335
|
+
# 2) 机械字段交给脚本,别手改(scripts/update-downloads.mjs 已覆盖下载区 **和首页标语**):
|
|
336
|
+
node scripts/update-downloads.mjs <版本> --check # 先查:标语或下载区过期会退出非 0
|
|
337
|
+
node scripts/update-downloads.mjs <版本> # 再改:链接/体积/sha256/平台标签/首页标语
|
|
338
|
+
# 3) 更新日志 / 版本号相关内容同步(展示区卡片、版本历史表属于**内容**,脚本不碰,要人写)
|
|
172
339
|
bash deploy.sh
|
|
173
340
|
```
|
|
174
341
|
|
|
175
342
|
- [ ] `site/index.html` 中版本号、每个包的 `href`、体积、`sha256` 均为**本版本**
|
|
176
|
-
- [ ]
|
|
177
|
-
|
|
343
|
+
- [ ] **首页顶部标语「零依赖 · 开放内核 vX.Y.Z」是本版本** —— 它是一句*当前版本*声明,
|
|
344
|
+
最容易漏:v0.6.1 上线后它还停在 `v0.4.6`(负责人报障"版本号没更新")。
|
|
345
|
+
脚本此前只动 `#downloads` 区,现已把标语纳入机械字段,`--check` 也会查它。
|
|
346
|
+
- [ ] 首页无残留旧版本号——注意区分:**「vX.Y.Z 起 /(vX.Y.Z 已修复)」是历史叙述,不要改**;
|
|
347
|
+
版本历史表与功能展示区的旧版本锚点也是故意的。脚本每次会列出所有旧版本号位置供扫一眼。
|
|
348
|
+
- [ ] `bash deploy.sh` 成功,线上页面可访问(`curl -s https://harness.mingdao.ai/ | grep 开放内核`)
|
|
349
|
+
|
|
350
|
+
### 3.35 查 CI 必须带令牌(否则会把「限流」看成「还在排队」)
|
|
351
|
+
|
|
352
|
+
查 Actions 运行状态时,**匿名调用有 60 次/小时的额度**。用完之后 `GET /actions/runs` 会返回
|
|
353
|
+
空结果(不是报错),脚本里很容易被当成"没有该提交的运行"或"还在排队"——**得出"CI 还在跑"的
|
|
354
|
+
错误结论**(本仓实测:轮询了 9 分钟一直显示"排队中",实际那次运行早已 completed success)。
|
|
355
|
+
|
|
356
|
+
```bash
|
|
357
|
+
set -a && . ./.env && set +a # 用 MINGDAO_GITHUB_TOKEN(已认证 = 5000 次/小时)
|
|
358
|
+
curl -s -H "Authorization: Bearer $MINGDAO_GITHUB_TOKEN" \
|
|
359
|
+
"https://api.github.com/repos/MingDaoTCM/MingDao-Harness/actions/runs?per_page=3"
|
|
360
|
+
curl -s "https://api.github.com/rate_limit" # 自查额度:core.remaining 为 0 时上面的结果不可信
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### 3.4 四平台一致性验收(**最后一步,缺一不可**)
|
|
364
|
+
|
|
365
|
+
```bash
|
|
366
|
+
cd MingDao-Harness
|
|
367
|
+
set -a && . ./.env && set +a # 需要三个 token 才能读到镜像的 Release
|
|
368
|
+
node scripts/verify-release.mjs <版本> # 四个渠道逐项核对,任一缺失即非 0 退出
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
它逐项核对:
|
|
372
|
+
|
|
373
|
+
- GitHub / Gitee / GitCode 的 **main 与 tag 是否同一个 commit**(只比 main 会漏掉没推的 tag);
|
|
374
|
+
- 三个平台的 **Release 是否存在**(Gitee 对不存在的 Release 会返回 `HTTP 200 + null 体`,
|
|
375
|
+
只看状态码会把「没发布」判成「已发布」——脚本已按 body 判定);
|
|
376
|
+
- **npm 是否已发布该版本**、`dist-tags.latest` 指向哪里(回填版会用 `--tag backfill`,
|
|
377
|
+
latest 不指向它属正常,脚本只警告不判失败)。
|
|
378
|
+
|
|
379
|
+
- [ ] `node scripts/verify-release.mjs <版本>` **退出 0**
|
|
380
|
+
- [ ] 若输出 `⚠ npm/dist-tags.latest` 不是本版本,确认这是有意的回填(否则 latest 需要修正)
|
|
178
381
|
|
|
179
382
|
### 3.3 Gitee / GitCode 同步发行(含附件)
|
|
180
383
|
|
package/docs/RELEASE-TRAIN.md
CHANGED
|
@@ -157,4 +157,23 @@
|
|
|
157
157
|
| 3 v0.5.1 | ❌ **已取消**(负责人 2026-09-11:不发 v0.5.1,确认后直接发完整 v0.6.0) |
|
|
158
158
|
| 4 v0.6.0(C1–C4) | 🚧 C1–C4 均已开发完成;待负责人验收后发布 |
|
|
159
159
|
|
|
160
|
-
>
|
|
160
|
+
> **状态(2026-09-12 更新):全部完成。** 负责人提供了私钥与主机(`8.210.103.117`),
|
|
161
|
+
> 并决定「官网直接发布 v0.6.0」(不再补发 v0.4.6 / v0.5.0 的官网内容)。v0.6.0 已四平台 + 官网发布完成。
|
|
162
|
+
|
|
163
|
+
## 六、v0.6.0 发布结果(2026-09-12)
|
|
164
|
+
|
|
165
|
+
| 渠道 | 结果 |
|
|
166
|
+
| --- | --- |
|
|
167
|
+
| GitHub Release | ✅ tag `v0.6.0`;9 个附件(5 安装包 + 2 mac zip + 2 自动更新 feed);正文已按新政策(附件长期保留) |
|
|
168
|
+
| Gitee / GitCode Release | ✅ 均已创建;正文 6112 字符含官网直链;**无安装包附件**(符合政策) |
|
|
169
|
+
| npm | ✅ 已发布,`latest` = 0.6.0;实测 `--version` / `ledger list` / `net policy` 可用 |
|
|
170
|
+
| 官网 | ✅ 线上即 v0.6.0;5 个下载链接全部 200、体积正确、sha256 与 GitHub 产物**逐项一致**;无 0.4.5 残留 |
|
|
171
|
+
| 桌面自动更新 | ✅ `latest.yml` / `latest-linux.yml` 指向 v0.6.0 |
|
|
172
|
+
| 三平台 tag | ✅ 同一 tag 对象 `3b1cc0f1aa` → 同一提交 `a9167d1` |
|
|
173
|
+
| CI | ✅ 10 绿 + 1 有意跳过(`normalize-release-notes` 仅在手动触发时运行) |
|
|
174
|
+
|
|
175
|
+
**三方一致性交叉核对**(GitHub digest / 服务器实际文件 / 官网页面):5/5 安装包完全一致。
|
|
176
|
+
收割在服务器侧直接拉 GitHub(~10MB/s),无需绕经本机。
|
|
177
|
+
|
|
178
|
+
**遗留提醒**:服务器 `downloads/` 已 15G(9 个历史版本 × 约 630M),磁盘 80%。
|
|
179
|
+
清理旧版本是负责人的决定,**未擅自删除**。
|
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -101,7 +101,13 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
|
|
|
101
101
|
const undo = undoStore || { backups: new Map() };
|
|
102
102
|
const stepLimit = maxSteps || MAX_STEPS;
|
|
103
103
|
// 只读工具集合(子代理只读模式 + 并行批次共用)。v0.3.1 起含 git/fetch(只读、审计常用)
|
|
104
|
-
|
|
104
|
+
// v0.6.2(代码审计 P2-11):**不要另写一份集合**——从只读档派生,把刻意差异写清楚。
|
|
105
|
+
// 此前这里是手写的第二份副本(与模块顶部的 READONLY_TIER_SET 内容相近但不同),
|
|
106
|
+
// 两处各自演化必然漂移(本仓已多次吃过"同一逻辑多份副本"的亏)。
|
|
107
|
+
// 差异(刻意,不是遗漏):
|
|
108
|
+
// · 去掉 task:子代理不再派子代理,避免无限嵌套与成本失控;
|
|
109
|
+
// · 去掉 todo:子代理的任务清单不与主线程共享(主线程 todo 由主代理维护)。
|
|
110
|
+
const READONLY_TOOLS_SET = new Set([...READONLY_TIER_SET].filter((n) => n !== 'task' && n !== 'todo'));
|
|
105
111
|
// 精确 token 计数:DeepSeek 词表,其他模型回退启发式
|
|
106
112
|
const count = makeTokenCounter(modelName);
|
|
107
113
|
// MCP 工具集(每次取,服务器晚就绪也能在后续轮次出现)
|
|
@@ -296,10 +302,16 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
|
|
|
296
302
|
return { text, data, usage: res?.usage || null, model: usedModel, purpose: opts.purpose || null, durationMs: Date.now() - t0 };
|
|
297
303
|
}
|
|
298
304
|
|
|
305
|
+
// v0.6.2(第三方审计 P1-2):读取去重缓存**每个代理实例一份**。
|
|
306
|
+
// 之前是模块级共享,导致子代理读过的文件在主代理里返回「内容与上次读取一致」占位串
|
|
307
|
+
// ——主代理上下文里根本没有内容。子代理由 createAgent 另建实例,因此天然隔离。
|
|
308
|
+
const agentReadCache = new Map();
|
|
309
|
+
|
|
299
310
|
function makeCtx() {
|
|
300
311
|
return {
|
|
301
312
|
cwd: workingDir,
|
|
302
313
|
io,
|
|
314
|
+
readCache: agentReadCache,
|
|
303
315
|
workingDir,
|
|
304
316
|
modelName,
|
|
305
317
|
provider,
|
|
@@ -1020,7 +1032,8 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
|
|
|
1020
1032
|
messages.push({
|
|
1021
1033
|
role: 'user',
|
|
1022
1034
|
content:
|
|
1023
|
-
'
|
|
1035
|
+
'(系统提示)已达到工具调用步数上限(下一步是最后一步),请停止调用工具,直接输出最终总结:' +
|
|
1036
|
+
'① 已完成的工作;② 已落盘的交付物(文件路径);③ **尚未完成的部分**;④ 下一步建议。不要声称任务已全部完成。',
|
|
1024
1037
|
});
|
|
1025
1038
|
}
|
|
1026
1039
|
} else {
|
|
@@ -1114,16 +1127,40 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
|
|
|
1114
1127
|
});
|
|
1115
1128
|
} catch {}
|
|
1116
1129
|
const art = deliverables.length ? '已交付文件:' + deliverables.join('、') + '。' : '';
|
|
1130
|
+
// v0.6.2(用户实测:长任务「步数受限未产出交付物」):续跑前必须明确要求**落盘**。
|
|
1131
|
+
// 原因:兜底总结是无工具请求(tools: []),预算一旦用尽就再也写不了文件——
|
|
1132
|
+
// 只留在对话里的结论等于没有交付物。所以在还有工具的轮次里就得把结论写进文件。
|
|
1133
|
+
// 同时告知剩余轮次,让模型自己按预算收口,而不是一路做「未完成」直到被硬停。
|
|
1134
|
+
const roundsLeft = maxRounds - round - 1;
|
|
1117
1135
|
messages.push({
|
|
1118
1136
|
role: 'user',
|
|
1119
|
-
content:
|
|
1137
|
+
content:
|
|
1138
|
+
`(系统提示)已连续执行 ${stepLimit} 步工具操作,任务尚未完成,请继续完成剩余工作。${art}` +
|
|
1139
|
+
`还剩 ${roundsLeft} 轮(每轮 ${stepLimit} 步)预算:请**优先把已完成的结论写入交付物文件**` +
|
|
1140
|
+
`(只留在对话里的内容不算交付物),再做未完成的部分。先核对已完成部分(勿重复)。`,
|
|
1120
1141
|
});
|
|
1121
|
-
io.print(style(`♻ 步数上限,自动续跑第 ${round + 2}
|
|
1142
|
+
io.print(style(`♻ 步数上限,自动续跑第 ${round + 2} 轮…(还剩 ${roundsLeft} 轮)`, C.dim));
|
|
1122
1143
|
continue;
|
|
1123
1144
|
}
|
|
1124
1145
|
io.endTurn();
|
|
1125
1146
|
// 步数上限:清掉未执行的 tool_calls,避免下一轮/恢复后 API 400
|
|
1126
1147
|
stripOrphanCalls();
|
|
1148
|
+
// v0.6.2(用户实测:「步数受限未能产出交付物,需要继续追问方能继续下一步」):
|
|
1149
|
+
// 预算用尽必须**明确告诉用户**发生了什么、以及怎么继续。此前这里一行都不打印,
|
|
1150
|
+
// 用户只看到一段总结,很容易误判为「任务已完成」,只能自己追问。
|
|
1151
|
+
try {
|
|
1152
|
+
io.print(style(
|
|
1153
|
+
`⚠ 步数预算已用尽(${maxRounds} 轮 × ${stepLimit} 步,本轮实际执行 ${steps} 步),任务可能尚未完成。\n` +
|
|
1154
|
+
// v0.6.2:这里**不能**再写「已保存检查点」——检查点由调用方在 runTurn 返回**之后**
|
|
1155
|
+
// 才写(cli/web/repl 各自落盘),此刻一个字都还没写;写失败时的提示也在那边。
|
|
1156
|
+
// 一个未经核实的断言会让用户以为断点已经安全,这正是 B-WS-1/2 那类「看起来有、其实没有」。
|
|
1157
|
+
` 检查点会在本回合收尾时保存(保存失败会另行提示),继续方式:\n` +
|
|
1158
|
+
` · REPL / WebUI / 桌面版:直接发送「继续」即可(会自动注入断点续跑提示)\n` +
|
|
1159
|
+
` · 命令行:加 --continue(如 mingdao -c "接着往下做")\n` +
|
|
1160
|
+
` · 想让单次跑得更久:调高 config.maxRounds(当前 ${maxRounds},每轮 ${stepLimit} 步),或改用更大预算的预设`,
|
|
1161
|
+
C.yellow
|
|
1162
|
+
));
|
|
1163
|
+
} catch {}
|
|
1127
1164
|
// v0.2.8 兜底总结(对齐 DSH):跑满步数/末轮无正文且未中断时,补一次 no-tool 小输出请求,
|
|
1128
1165
|
// 让任务以「总结文字 + 交付物清单」收尾,而非静默结束;失败则回退旧行为(text:null)。
|
|
1129
1166
|
// v0.4.1 修复:输入轻量化——此前用 trimMessages(messages, budget) 全量历史,本地 q8 量化模型
|
|
@@ -1137,7 +1174,14 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
|
|
|
1137
1174
|
...(deliverables.length
|
|
1138
1175
|
? [{ role: 'user', content: '已交付文件:\n' + deliverables.map((/** @type {string} */ f) => `- ${f}`).join('\n') }]
|
|
1139
1176
|
: []),
|
|
1140
|
-
|
|
1177
|
+
// v0.6.2:原话是「任务已执行完毕」——在**步数上限**这条路径上那是假话,
|
|
1178
|
+
// 模型据此汇报「已完成」,用户就以为交付完成了。改为如实说明是被迫中断。
|
|
1179
|
+
{
|
|
1180
|
+
role: 'user',
|
|
1181
|
+
content:
|
|
1182
|
+
'(系统提示)步数预算已用尽,本轮**被迫中断,任务可能尚未完成**(不要声称已全部完成)。' +
|
|
1183
|
+
'请如实总结:① 已完成的工作;② 已落盘的交付物(文件路径);③ **尚未完成的部分**;④ 下一步该做什么(供继续时使用)。',
|
|
1184
|
+
},
|
|
1141
1185
|
];
|
|
1142
1186
|
currentAc = new AbortController();
|
|
1143
1187
|
lastRequestStartAt = Date.now();
|
|
@@ -1219,7 +1263,26 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
|
|
|
1219
1263
|
truncated: Boolean(finish === 'length'),
|
|
1220
1264
|
aborted,
|
|
1221
1265
|
});
|
|
1222
|
-
|
|
1266
|
+
// v0.6.2(B-WS-1/2):记账降级必须让用户看见。
|
|
1267
|
+
// 此前写账本失败只把 alive 置 false,用户拿到一段「正常」总结却不知道这次运行
|
|
1268
|
+
// **没有任何账本**——而「被静默截断的合规账本比不记账更糟」正是账本自己的纪律。
|
|
1269
|
+
// 仍然不抛异常(记账失败绝不能影响正常执行),但必须留下可见痕迹与补救方式。
|
|
1270
|
+
if (turnLedger.degraded) {
|
|
1271
|
+
io.print(
|
|
1272
|
+
style(
|
|
1273
|
+
`⚠ 本回合执行账本未能完整写入:${turnLedger.lastError ?? '未知原因'}(失败 ${turnLedger.failures} 次)。\n` +
|
|
1274
|
+
` 这意味着**这次运行在 ~/.mingdao/ledger 里可能不完整**,不能作为完整审计依据。\n` +
|
|
1275
|
+
` 常见原因:磁盘写满、目录权限、MINGDAO_HOME 指向不可写位置。可用 mingdao ledger list 核对。`,
|
|
1276
|
+
C.yellow
|
|
1277
|
+
)
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
} catch (err) {
|
|
1281
|
+
// 收尾自身失败也不能静默:账本是合规物,用户在意的正是「有没有记全」
|
|
1282
|
+
try {
|
|
1283
|
+
io.print(style(`⚠ 执行账本收尾失败:${/** @type {any} */ (err)?.message ?? err}`, C.yellow));
|
|
1284
|
+
} catch {}
|
|
1285
|
+
}
|
|
1223
1286
|
turnLedger = null;
|
|
1224
1287
|
}
|
|
1225
1288
|
offEgressSink();
|
package/src/atomic-write.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import fs from 'node:fs';
|
|
11
11
|
import path from 'node:path';
|
|
12
12
|
import crypto from 'node:crypto';
|
|
13
|
+
import { procAlive } from './proc.js';
|
|
13
14
|
|
|
14
15
|
export function atomicWriteFileSync(/** @type {string} */ target, /** @type {string|Buffer} */ data, /** @type {any} */ options = {}) {
|
|
15
16
|
const dir = path.dirname(target);
|
|
@@ -37,7 +38,11 @@ const sleepBuf = new Int32Array(new SharedArrayBuffer(4));
|
|
|
37
38
|
const sleepMs = (/** @type {number} */ ms) => Atomics.wait(sleepBuf, 0, 0, ms);
|
|
38
39
|
const heldLocks = new Set(); // 本进程当前持有的锁路径(可重入判定)
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
// v0.6.2(自评报告 P2-7):默认值必须 **timeoutMs > staleMs**。
|
|
42
|
+
// 原为 5000 / 15000——等待方 5 秒就抛「获取文件锁超时」,而陈旧锁要 15 秒才允许回收,
|
|
43
|
+
// 中间 10 秒是纯粹的**死区**:持锁方一旦崩溃,这段时间内所有写方必然全部失败
|
|
44
|
+
// (cachestats 静默跳过轮转丢计费明细;tasks/workspace/schedule/sync 未捕获时直接报错)。
|
|
45
|
+
export function withFileLockSync(/** @type {string} */ lockPath, /** @type {() => any} */ fn, { timeoutMs = 20000, staleMs = 15000 } = {}) {
|
|
41
46
|
// 可重入:同一调用栈内已持该锁 → 直接执行,不再二次抢锁
|
|
42
47
|
if (heldLocks.has(lockPath)) {
|
|
43
48
|
return fn();
|
|
@@ -71,7 +76,22 @@ export function withFileLockSync(/** @type {string} */ lockPath, /** @type {() =
|
|
|
71
76
|
if (/** @type {any} */ (err).code !== 'EEXIST') throw err;
|
|
72
77
|
try {
|
|
73
78
|
const st = fs.statSync(lockPath);
|
|
74
|
-
|
|
79
|
+
// v0.6.2(P2-7):陈旧判据改为「**持有者 pid 已死**」优先。
|
|
80
|
+
//
|
|
81
|
+
// 原判据只看 mtime 超过 staleMs,带来两个反向问题:
|
|
82
|
+
// ① 崩溃后要白等满 15 秒才允许回收(配上面 5 秒的 timeout 就是死区);
|
|
83
|
+
// ② 若某个 fn 本身耗时超过 15 秒(大文件重写、慢盘),别人会把**仍然活着**的锁
|
|
84
|
+
// 判成陈旧并回收——互斥直接失效,而且没有任何补救。
|
|
85
|
+
// 锁内容本来就写着 {pid, at}(此前只用于 TOCTOU 比对),现成的判据没用上。
|
|
86
|
+
let holderPid = 0;
|
|
87
|
+
try {
|
|
88
|
+
holderPid = Number(JSON.parse(fs.readFileSync(lockPath, 'utf8'))?.pid) || 0;
|
|
89
|
+
} catch {}
|
|
90
|
+
const holderAlive = holderPid > 0 ? procAlive(holderPid) : null;
|
|
91
|
+
// 回收条件:持有者已死(立刻回收,不留死区);或读不到持有者信息且确实超过 staleMs。
|
|
92
|
+
// 持有者仍活着(哪怕 fn 跑了很久)**绝不回收**——那会破坏互斥。
|
|
93
|
+
const reclaimable = holderAlive === false || (holderAlive === null && Date.now() - st.mtimeMs > staleMs);
|
|
94
|
+
if (reclaimable) {
|
|
75
95
|
// TOCTOU 防护(OfficeACE 报告):unlink 前读锁内容并二次 stat 比对,
|
|
76
96
|
// 防两个进程同时判定陈旧、后者误删前者刚创建的新锁(互斥失效)
|
|
77
97
|
try {
|