mingdao-harness 0.6.2 → 0.6.4
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/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 +300 -32
- package/docs/CONFIG.md +32 -6
- package/docs/DEVELOPER.md +1 -1
- package/docs/PACK-API.md +26 -3
- package/docs/PROVIDERS.md +22 -0
- package/docs/RELEASE-CHECKLIST.md +13 -2
- package/package.json +1 -1
- package/src/agent.js +133 -15
- package/src/atomic-write.js +247 -71
- package/src/audit.js +23 -18
- package/src/cachestats.js +57 -47
- package/src/cli.js +25 -4
- package/src/commands/diagnose.js +3 -0
- package/src/commands/key.js +23 -1
- package/src/commands/ledger.js +41 -3
- package/src/commands/net.js +5 -1
- package/src/commands/pack.js +23 -6
- package/src/commands/repl.js +9 -1
- package/src/commands/skill.js +30 -1
- package/src/commands/sync.js +7 -0
- package/src/commands/workspace.js +3 -3
- package/src/compact.js +15 -1
- package/src/config.js +71 -6
- package/src/constraints.js +55 -6
- package/src/credentials.js +36 -5
- package/src/hooks.js +37 -2
- package/src/ledger.js +3 -3
- package/src/log-writer.js +22 -17
- package/src/mcp.js +19 -1
- package/src/memory.js +8 -8
- package/src/packs.js +57 -8
- package/src/permissions.js +35 -9
- package/src/presets.js +46 -1
- package/src/providers/index.js +133 -7
- package/src/providers/openai-compatible.js +24 -3
- package/src/redact.js +38 -2
- package/src/replay.js +15 -3
- package/src/safe-fetch.js +8 -2
- package/src/schedule.js +22 -4
- package/src/session-index.js +2 -2
- package/src/session.js +2 -2
- package/src/skill-lib.js +43 -14
- package/src/skill-registry.js +31 -2
- package/src/sync-server.js +47 -28
- package/src/sync.js +17 -3
- package/src/task-state.js +10 -1
- package/src/tasks.js +22 -10
- package/src/tools/bash.js +96 -18
- package/src/tools/fetch.js +24 -0
- package/src/web/app.js +7 -7
- package/src/web/attachments.js +7 -1
- package/src/web/index.html +2 -2
- package/src/web/routes/api.js +27 -2
- package/src/web/routes/domains/sessions.js +3 -3
- package/src/web/routes/domains/sync.js +2 -1
- package/src/web/routes/domains/workspace.js +45 -15
- package/src/web/server.js +22 -5
- package/src/workspace.js +31 -18
package/docs/PROVIDERS.md
CHANGED
|
@@ -80,6 +80,28 @@ export async function createProvider(cfg) {
|
|
|
80
80
|
|
|
81
81
|
规则:`provider` 名不在内置预设中且存在同名模块文件时,优先加载该模块;否则按 OpenAI 兼容方式使用 `baseUrl`。
|
|
82
82
|
|
|
83
|
+
### 声明能力(可选,v0.6.3 起)
|
|
84
|
+
|
|
85
|
+
模块可以**静态声明**能力,内核据此决定界面行为(目前用于图片输入门控):
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
// ~/.mingdao/providers/dify.mjs
|
|
89
|
+
export const supportsVision = true; // 或 export const capabilities = { vision: true }
|
|
90
|
+
export async function createProvider(cfg) { /* … */ }
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
- 只读**静态导出**,不调用 `createProvider()`(后者可能有副作用:起进程、建连接);按文件 mtime 缓存。
|
|
94
|
+
- 声明缺失时该 Provider 视为不支持图片——**门控保守**,不会把图发给看不懂的端点。
|
|
95
|
+
- 为什么需要它:**为了打开图片门控而往 `config.customModels` 里加条目,会改变"请求发给谁"**。
|
|
96
|
+
自 v0.6.3 起 `customModels` 的条目分两类,只有写了传输字段的才算自定义端点:
|
|
97
|
+
|
|
98
|
+
| 条目内容 | 含义 | 效果 |
|
|
99
|
+
| --- | --- | --- |
|
|
100
|
+
| 含 `baseUrl` / `apiKey` / `envKey` / `headers` / `path` / `kind` | **声明式端点** | 走 `custom:<模型名>` 的 OpenAI 兼容直连(优先于内置预设,原行为) |
|
|
101
|
+
| 只含 `vision` / `tokenizer` / `contextWindow` / `maxOutputTokens` / `local` / `provider` | **纯能力覆盖** | **不改变 transport**;`provider` 字段只作路由提示(可指向你的自定义模块) |
|
|
102
|
+
|
|
103
|
+
也就是说:「打开一个能力开关」不再改变「请求发给谁」。
|
|
104
|
+
|
|
83
105
|
## 模型预设(src/models.js)
|
|
84
106
|
|
|
85
107
|
每个模型预设定义:`provider` / `contextWindow` / `budgetTokens`(默认注入预算)/ `maxOutputTokens` / `temperature` / `supportsReasoning`。自定义模型名没有预设时使用安全默认值(预算 128k、输出 8k、温度 0.6),都可以在 `config.json` 用 `contextBudget` / `maxOutputTokens` / `temperature` 覆盖。
|
|
@@ -266,8 +266,13 @@ git push <mirror> refs/tags/v<版本>:refs/tags/v<版本>
|
|
|
266
266
|
|
|
267
267
|
```bash
|
|
268
268
|
# 采集:用脚本,别再手写 /tmp/harvest-<版本>.sh(手写漏过 mac zip,见 ①)
|
|
269
|
-
|
|
270
|
-
|
|
269
|
+
scp MingDao-Harness-Site/scripts/harvest-release.mjs mingdao-server:/tmp/
|
|
270
|
+
# ⚠ token **不要**写进 ssh 的命令串(那等于放进 argv,本地与服务器的 ps 都能读到几小时,
|
|
271
|
+
# 与 §3.22 修掉的是同一个毛病)。经 stdin 落到 umask 077 的临时文件,用完即删:
|
|
272
|
+
umask 077; printf 'MINGDAO_GITHUB_TOKEN=%s\n' "$MINGDAO_GITHUB_TOKEN" > /tmp/hr.env
|
|
273
|
+
ssh mingdao-server 'umask 077; cat > /tmp/harvest.env' < /tmp/hr.env && rm -f /tmp/hr.env
|
|
274
|
+
ssh mingdao-server 'set -a; . /tmp/harvest.env; set +a; rm -f /tmp/harvest.env; \
|
|
275
|
+
node /tmp/harvest-release.mjs <版本> /opt/1panel/www/sites/mingdao-site/downloads'
|
|
271
276
|
# · 采安装包 + *.blockmap + latest.yml/latest-linux.yml,跳过 latest-mac.yml 与 builder-debug.yml
|
|
272
277
|
# · 每个文件按 GitHub 的 sha256(digest) + size 双校验,不符即删并退出非 0
|
|
273
278
|
# · 末尾会打印「差量更新素材」自查(缺哪个平台的 .blockmap 会直接点名)
|
|
@@ -379,6 +384,12 @@ node scripts/verify-release.mjs <版本> # 四个渠道逐项核对,任一缺
|
|
|
379
384
|
- [ ] `node scripts/verify-release.mjs <版本>` **退出 0**
|
|
380
385
|
- [ ] 若输出 `⚠ npm/dist-tags.latest` 不是本版本,确认这是有意的回填(否则 latest 需要修正)
|
|
381
386
|
|
|
387
|
+
> **v0.6.2 发布实测**:该脚本第一次跑报 `✗ GitHub/main 取不到`,第二次即通过——
|
|
388
|
+
> 是一次 `git ls-remote` 的瞬时失败。**一次网络抖动不该在发版收尾时喊狼来了**:
|
|
389
|
+
> 误报会训练人忽略这条告警,而它恰恰是防半发布的最后一道闸。现已加**三次有界重试**,
|
|
390
|
+
> 并把「取不到」(网络/限流)与「读到了但不一样」(真的没推)在输出里区分开。
|
|
391
|
+
> 若仍显示「取不到」,先确认网络与额度再重跑,**不要**跳过这一步。
|
|
392
|
+
|
|
382
393
|
### 3.3 Gitee / GitCode 同步发行(含附件)
|
|
383
394
|
|
|
384
395
|
```bash
|
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -33,6 +33,57 @@ const SUBAGENT_MAX_STEPS = 24;
|
|
|
33
33
|
// 导致基准测的不是真实只读档。导出后基准与实现共用同一集合。
|
|
34
34
|
export const READONLY_TIER_SET = new Set(['read', 'ls', 'glob', 'grep', 'skill', 'todo', 'git', 'fetch', 'task']);
|
|
35
35
|
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// 写意图 / 疑问句 / 未达成意图的判定(v0.6.3)
|
|
38
|
+
//
|
|
39
|
+
// 这几个判定决定「本回合暴露哪些工具」,因此**放到模块级并导出**:判定错一次,
|
|
40
|
+
// 用户看到的就是"模型说做不到"而完全不知道原因(下游 Deyi-TCM 随访的「回访」正是如此)。
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
const WRITE_INTENT_RE = /写|建|创|改|修|删|装|加|添|增|补|换|移|部署|执行|运行|实现|重构|生成|迁移|安装|更新|升级|发布|调整|优化|修复|提交|推送|打包|编译|测试|implement|fix|create|modify|update|delete|deploy|build|make|generate|install|write|refactor|migrate|test|run|commit|push|remove|add|change|patch/i;
|
|
43
|
+
const hasWriteIntent = (/** @type {any} */ text) => WRITE_INTENT_RE.test(String(text || ''));
|
|
44
|
+
// v0.6.3(下游 Deyi-TCM 随访实测的上游缺口):只读档的**判定方向必须反过来**。
|
|
45
|
+
//
|
|
46
|
+
// 原逻辑:命中写意图关键词 → 给全量工具;否则整回合只读(write/edit/bash 对模型**不可见**)。
|
|
47
|
+
// 但域内任务用的是**域内动词/名词**——「回访」「排班」「盘点」「对账」「随访」「归档」……
|
|
48
|
+
// 这是**开集**,永远枚举不完。下游实测:「请生成回访看板」命中"生成"→✅ 能用;
|
|
49
|
+
// 而「回访」不命中任何关键词 → 只读档 → 工具不可见 → ❌ 高频入口直接废掉。
|
|
50
|
+
//
|
|
51
|
+
// 现在改为:**只有"看起来是纯提问、且不含写意图"才进只读档**,其余一律给全量工具。
|
|
52
|
+
// 省 token 的初衷不变(真正的问答仍然是只读档),但默认从"任务态"出发——
|
|
53
|
+
// 少省一点 token 远好过"任务做不了且用户看不出原因"。
|
|
54
|
+
const QUESTION_RE = /[??]|什么|怎么|怎样|如何|为何|为什么|是否|能否|可否|多少|几个|哪些|哪个|哪里|哪个|谁|何时|吗$|吗[。!!]|呢[。!!]?$/i;
|
|
55
|
+
const QUESTION_RE_EN = /\b(what|how|why|is|are|does|do|can|could|should|when|where|which|who)\b/i;
|
|
56
|
+
const looksLikeQuestion = (/** @type {any} */ text) => {
|
|
57
|
+
const t = String(text || '').trim();
|
|
58
|
+
if (!t) return false;
|
|
59
|
+
return QUESTION_RE.test(t) || QUESTION_RE_EN.test(t);
|
|
60
|
+
};
|
|
61
|
+
// 自愈信号:只读档下模型"想做但做不了 / 要用户来做"时的典型措辞(中英)。
|
|
62
|
+
const UNMET_INTENT_RE = /无法|不能|没法|做不到|没有权限|未提供|需要你|需要先|请先|建议你|请确认|请提供|如需|无法直接|不能直接|请告知|需要我|我可以帮你|unable to|can'?t|cannot|need you|need to know|please confirm|please provide/i;
|
|
63
|
+
const looksLikeUnmetIntent = (/** @type {any} */ text) => UNMET_INTENT_RE.test(String(text || ''));
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 本回合是否从「只读档」起步。
|
|
67
|
+
*
|
|
68
|
+
* **判定方向是故意反的**:原实现要命中写意图关键词才给全量工具,而域内任务用的是
|
|
69
|
+
* **开集**动词/名词——「回访」「排班」「盘点」「对账」……永远枚举不完。下游实测:
|
|
70
|
+
* 「请生成回访看板」命中"生成"→能用;「回访」不命中→只读档→write/edit/bash 不可见→废掉。
|
|
71
|
+
* 现在只有"看起来是纯提问且不含写意图"才进只读档(真正的问答照样省 token),其余一律全量。
|
|
72
|
+
*
|
|
73
|
+
* 另外两条防线(都写在这里,保证判定只有一处):
|
|
74
|
+
* · **域内 Pack 在场时不进只读档**:这类部署的价值在"任务能做",不在省 schema token;
|
|
75
|
+
* · 只读档若以纯文本收尾且透出"做不到/需要…",`runTurn` 会**自愈**放开一次(与词表无关)。
|
|
76
|
+
*
|
|
77
|
+
* @param {any} cfg @param {any} lastUserText @param {boolean} packActive
|
|
78
|
+
* @returns {boolean}
|
|
79
|
+
*/
|
|
80
|
+
export function startsInReadOnlyPhase(cfg, lastUserText, packActive) {
|
|
81
|
+
if (cfg?.schemaTier === false) return false;
|
|
82
|
+
if (packActive) return false;
|
|
83
|
+
const t = lastUserText;
|
|
84
|
+
return !hasWriteIntent(t) && looksLikeQuestion(t);
|
|
85
|
+
}
|
|
86
|
+
|
|
36
87
|
/**
|
|
37
88
|
* 创建 Agent 循环(调用方只需传 provider/permission/io/modelName/workingDir,其余可选)
|
|
38
89
|
* @param {{ provider: any, permission: any, io: any, modelName: any, workingDir: any,
|
|
@@ -121,8 +172,7 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
|
|
|
121
172
|
// (readOnly 子代理只读,权限引擎仍门控写操作,无越权)。
|
|
122
173
|
// 只读档工具集:单一来源见模块顶部导出的 READONLY_TIER_SET
|
|
123
174
|
// 中英双语写意图(CodeArts 报告:纯中文正则让英文会话整回合只读死锁)
|
|
124
|
-
|
|
125
|
-
const hasWriteIntent = (/** @type {any} */ text) => WRITE_INTENT_RE.test(String(text || ''));
|
|
175
|
+
|
|
126
176
|
// A1(前缀稳定):剥描述集合按「回合冻结快照」——回合内恒定(至多两态:只读档/全量档),
|
|
127
177
|
// 新使用的工具只在下一回合才进入剥描述集合;回合边界本身就有新 user 消息,schema 变化免费。
|
|
128
178
|
// v0.4.0 Agent Preset:cfg.presetTools 白名单恒生效(在只读档过滤之后收紧——预设只减不增)。
|
|
@@ -330,6 +380,9 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
|
|
|
330
380
|
async function runTurn(/** @type {any} */ messages) {
|
|
331
381
|
let steps = 0;
|
|
332
382
|
let finish = null;
|
|
383
|
+
/** v0.6.3(M-16):本回合是否出现过"上游提前关流"(回答可能不完整)。
|
|
384
|
+
* 刻意不叫 truncated——既有 `truncated` 的含义是"步数兜底总结也失败了",两者必须分开。 */
|
|
385
|
+
let upstreamTruncated = false;
|
|
333
386
|
// v0.3.1 自动续跑(长程执行):跑满 stepLimit 步后不再直接中断,而是注入进度摘要再续跑,
|
|
334
387
|
// 最多 maxRounds 轮(默认 3,可用 cfg.maxRounds 调);审计/重构等大任务不再「一步中断」。
|
|
335
388
|
const maxRounds = Math.max(1, Number(cfg.maxRounds) || 3);
|
|
@@ -401,11 +454,14 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
|
|
|
401
454
|
// 省钱 B1:本回合只读阶段判定——最新用户消息无写意图则先只发只读工具,
|
|
402
455
|
// 模型明确表达写意图后(下一轮)注入全量。cfg.schemaTier=false 可关。
|
|
403
456
|
let readOnlyPhase = true;
|
|
404
|
-
|
|
457
|
+
// 自愈只允许一次(否则与 stepLimit 形成"放开→再收起"的循环)
|
|
458
|
+
let readOnlyHealed = false;
|
|
459
|
+
{
|
|
405
460
|
const lastUser = [...messages].reverse().find((m) => m?.role === 'user');
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
461
|
+
// 域内 Pack 在场时**不进只读档**(域内词是开集,靠关键词判定必然漏)
|
|
462
|
+
const packCtx0 = getActivePackContext();
|
|
463
|
+
const packActive = Array.isArray(packCtx0?.mounted) && packCtx0.mounted.length > 0;
|
|
464
|
+
readOnlyPhase = startsInReadOnlyPhase(cfg, String(lastUser?.content ?? ''), packActive);
|
|
409
465
|
}
|
|
410
466
|
// A1:本回合的剥描述冻结快照(会话级 usedToolNames 的副本)——回合内 schema 字节不变
|
|
411
467
|
const turnStrippedSet = new Set(usedToolNames);
|
|
@@ -535,6 +591,11 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
|
|
|
535
591
|
windowPressure = false;
|
|
536
592
|
pressureWarned = false;
|
|
537
593
|
}
|
|
594
|
+
// v0.6.3(M-1):压缩把早期消息(含文件正文)换成了摘要,但**读取去重缓存**还记着
|
|
595
|
+
// "这个文件你看过(mtime+size 未变)"——模型随后再读同一文件只会拿到「内容与上次读取一致」
|
|
596
|
+
// 的占位串,而正文已经不在上下文里了,于是它可能凭残缺信息继续改文件。
|
|
597
|
+
// 压缩是"上下文被替换"的事件,缓存必须跟着失效(代价只是重新读一次文件)。
|
|
598
|
+
agentReadCache.clear();
|
|
538
599
|
try {
|
|
539
600
|
onCompact?.(messages);
|
|
540
601
|
} catch {}
|
|
@@ -708,6 +769,15 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
|
|
|
708
769
|
}
|
|
709
770
|
|
|
710
771
|
finish = res.finish ?? finish;
|
|
772
|
+
// v0.6.3(M-16):上游**提前关流**(没有 [DONE]、也没有 finish_reason)说明这次回答很可能是半截的。
|
|
773
|
+
// 此前它与正常结束完全无法区分:用户拿到半句、账本记 done、也没有任何告警。
|
|
774
|
+
// 这里至少做到"说出来"——置标记并在界面上提示,让人知道该复核输出而不是直接采信。
|
|
775
|
+
if (res.truncated) {
|
|
776
|
+
upstreamTruncated = true;
|
|
777
|
+
try {
|
|
778
|
+
io.print(style('⚠ 上游提前结束响应(未收到结束标记),本次回答可能不完整——请复核输出后再采用。', C.yellow));
|
|
779
|
+
} catch {}
|
|
780
|
+
}
|
|
711
781
|
// v0.6.0 C1:模型轮次事件(耗时/首 token/用量/完成原因)——「这一步花了多少钱、等了多久」的最小依据
|
|
712
782
|
turnLedger.modelRound({
|
|
713
783
|
round,
|
|
@@ -716,6 +786,8 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
|
|
|
716
786
|
firstTokenMs: firstTokenAt ? firstTokenAt - llmT0 : null,
|
|
717
787
|
requestStartAt: lastRequestStartAt,
|
|
718
788
|
finish: res.finish ?? null,
|
|
789
|
+
// 提前关流是"这次回答不可信"的事实,必须进账本(否则事后无法区分它和正常完成)
|
|
790
|
+
...(res.truncated ? { truncated: true } : {}),
|
|
719
791
|
usage: res.usage ?? null,
|
|
720
792
|
});
|
|
721
793
|
// 省钱 B1:只读阶段中模型文字明确表达写意图 → 下一轮注入全量工具(多一轮,几乎无感)
|
|
@@ -750,7 +822,7 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
|
|
|
750
822
|
if (res.text) {
|
|
751
823
|
const ap = await applyOutputConstraints(res.text, { messages, provider, activeModel, temperature, maxOutput, usage, signal: currentAc?.signal });
|
|
752
824
|
messages.push({ role: 'assistant', content: ap.text });
|
|
753
|
-
return { text: ap.text, reasoning: res.reasoning || '', usage, steps, finish, truncated: false, aborted: false, note: ap.note || undefined, durationMs: Date.now() - startedAt, perf: perf() };
|
|
825
|
+
return { text: ap.text, reasoning: res.reasoning || '', usage, steps, finish, truncated: false, aborted: false, upstreamTruncated, note: ap.note || undefined, durationMs: Date.now() - startedAt, perf: perf() };
|
|
754
826
|
}
|
|
755
827
|
break;
|
|
756
828
|
}
|
|
@@ -851,6 +923,25 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
|
|
|
851
923
|
// v0.5.0 A3 ①:领域约束(PreToolUse)——权限放行之后、执行之前强制。
|
|
852
924
|
// 与权限引擎的分工:权限回答「用户是否允许」,约束回答「领域是否允许」;两者都通过才执行。
|
|
853
925
|
const cv = checkPreTool(constraints, name, args);
|
|
926
|
+
// v0.6.3(P1-2):confirm 类约束——权限放行之后**仍要人工确认**。
|
|
927
|
+
// 它存在的意义正是「权限档位是 auto 时也要问一次」;拿不到确认通道(非交互:
|
|
928
|
+
// -p / 调度 / 子 Agent)或用户答否,一律按阻断处理,与权限引擎在同一情形下的
|
|
929
|
+
// 「按拒绝处理」保持同一口径(fail-closed,见下方 permission.check 的 catch)。
|
|
930
|
+
let confirmFailure = /** @type {string|null} */ (null);
|
|
931
|
+
if (cv?.needsConfirm) {
|
|
932
|
+
let granted = false;
|
|
933
|
+
try {
|
|
934
|
+
granted = await io.confirm(`【领域约束】${cv.reason}。是否继续执行?(y/N)`);
|
|
935
|
+
} catch {
|
|
936
|
+
granted = false;
|
|
937
|
+
}
|
|
938
|
+
if (granted) {
|
|
939
|
+
if (auditOn) auditEntry({ confirmRequired: true, granted: true, reason: cv.reason });
|
|
940
|
+
turnLedger.permission({ name, mode: permission?.mode ?? cfg.permission ?? null, decision: 'confirm-granted', source: 'constraint.confirm', rule: cv.event?.id ?? null });
|
|
941
|
+
} else {
|
|
942
|
+
confirmFailure = `领域约束「${cv.event?.id ?? ''}」要求人工确认,未获确认(拒绝或当前环境无法交互)——已按 fail-closed 阻止执行`;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
854
945
|
// v0.6.0 C1:工具调用事件——参数留「脱敏明细 + 原文指纹」两份,指纹用于比对/防篡改;
|
|
855
946
|
// 权限与约束**两者都记**:受监管场景要能回答「权限放行了,但领域红线拦住了」这类问题。
|
|
856
947
|
turnLedger.toolCall({
|
|
@@ -860,15 +951,16 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
|
|
|
860
951
|
rawArgs: args,
|
|
861
952
|
args,
|
|
862
953
|
permission: { decision: 'allow', source: 'permission.check' },
|
|
863
|
-
constraint: cv?.blocked ? { blocked: true, id: cv
|
|
954
|
+
constraint: cv?.blocked || confirmFailure ? { blocked: true, id: cv?.event?.id ?? null, kind: cv?.event?.kind ?? null } : null,
|
|
864
955
|
});
|
|
865
|
-
if (cv?.blocked) {
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
956
|
+
if (cv?.blocked || confirmFailure) {
|
|
957
|
+
const reason = confirmFailure ?? cv?.reason ?? '领域约束拦截';
|
|
958
|
+
io.renderToolDenied(name, args, reason);
|
|
959
|
+
if (auditOn) auditEntry({ denied: true, reason });
|
|
960
|
+
if (cv?.event) auditConstraint(cv.event);
|
|
869
961
|
// 被约束拦下的调用不会有 tool.result,这里补一条终态,避免账本出现「只有调用没有结果」的悬空步
|
|
870
962
|
turnLedger.toolResult({ callId: tc.id, name, ok: false, blocked: true, ms: 0, error: '领域约束拦截' });
|
|
871
|
-
messages.push({ role: 'tool', tool_call_id: tc.id, content: `【领域约束】${
|
|
963
|
+
messages.push({ role: 'tool', tool_call_id: tc.id, content: `【领域约束】${reason}` });
|
|
872
964
|
return null;
|
|
873
965
|
}
|
|
874
966
|
return { tc, name, args, isMcp };
|
|
@@ -1100,6 +1192,23 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
|
|
|
1100
1192
|
});
|
|
1101
1193
|
continue;
|
|
1102
1194
|
}
|
|
1195
|
+
// v0.6.3 自愈:只读档下 write/edit/bash 不可见,域内任务只会收到一段
|
|
1196
|
+
// "我做不到/需要你提供…"的文字,而**根本不会进第二轮**——原释放条件
|
|
1197
|
+
// (模型文字里出现写意图)因此永远等不到。这里补一条与词表无关的兜底。
|
|
1198
|
+
if (readOnlyPhase && !readOnlyHealed && looksLikeUnmetIntent(finalText)) {
|
|
1199
|
+
readOnlyHealed = true;
|
|
1200
|
+
readOnlyPhase = false;
|
|
1201
|
+
try {
|
|
1202
|
+
io.print(style('⚠ 上一轮处于只读档、未暴露写入类工具;已放开工具集并重试一次', C.dim));
|
|
1203
|
+
} catch {}
|
|
1204
|
+
messages.push({
|
|
1205
|
+
role: 'user',
|
|
1206
|
+
content:
|
|
1207
|
+
'(系统提示)你上一轮可能因为没有可用的写入/执行工具而未能完成任务。' +
|
|
1208
|
+
'现在工具集已放开:如果用户的要求需要写文件或执行命令,**请直接调用相应工具去做**,不要只描述计划或让用户自己完成。',
|
|
1209
|
+
});
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1103
1212
|
return {
|
|
1104
1213
|
text: finalText,
|
|
1105
1214
|
reasoning: res.reasoning || '',
|
|
@@ -1239,10 +1348,16 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
|
|
|
1239
1348
|
if (turnLedger) {
|
|
1240
1349
|
try {
|
|
1241
1350
|
const costDate = lastRequestStartAt ? new Date(lastRequestStartAt) : new Date();
|
|
1242
|
-
|
|
1351
|
+
// v0.6.3(BUG-024):计价必须用**本回合实际使用**的模型。
|
|
1352
|
+
// 此前用 modelName(用户配置的模型),而成本护栏降级后请求实际发给 activeModel——
|
|
1353
|
+
// 两者单价不同(flash 便宜数倍),于是账本与日费用护栏系统性偏移,
|
|
1354
|
+
// 而"降级省钱"恰恰是靠这里体现的。
|
|
1355
|
+
const yuan = estimateCost(activeModel, usage.prompt_tokens, usage.completion_tokens, cacheSplit(usage), costDate);
|
|
1243
1356
|
const priced = typeof yuan === 'number' && Number.isFinite(yuan);
|
|
1244
1357
|
turnLedger.cost({
|
|
1245
|
-
|
|
1358
|
+
// BUG-024:归属同样要用实际计费的模型(run.start 里已记过"用户请求的模型",
|
|
1359
|
+
// 这里再写 modelName 会让"花了钱的那次调用"归属到一个没被调用的模型)
|
|
1360
|
+
model: activeModel,
|
|
1246
1361
|
usage,
|
|
1247
1362
|
yuan: priced ? yuan : null,
|
|
1248
1363
|
priced,
|
|
@@ -1298,5 +1413,8 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
|
|
|
1298
1413
|
runTurn,
|
|
1299
1414
|
spawnTask: (/** @type {any} */ prompt, /** @type {any} */ opts) => spawnTask(prompt, opts),
|
|
1300
1415
|
getTodos: () => todos.slice(),
|
|
1416
|
+
// v0.6.3(M-1):凡是"上下文被整体替换"的地方(/compact、/clear、手动重写历史)都要能
|
|
1417
|
+
// 让读取去重缓存一起失效,否则模型会被"你已读过"的占位串误导。
|
|
1418
|
+
clearReadCache: () => agentReadCache.clear(),
|
|
1301
1419
|
};
|
|
1302
1420
|
}
|