@huicai/cli 2026.710.97950 → 2026.711.24616

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/LICENSE +43 -21
  2. package/bin/huicai.js +93 -2
  3. package/package.json +4 -3
  4. package/src/advisor.js +1 -0
  5. package/src/agent.js +1 -385
  6. package/src/api.js +1 -326
  7. package/src/autofix.js +1 -0
  8. package/src/banner.js +1 -83
  9. package/src/checkpoints.js +1 -0
  10. package/src/clipboard.js +1 -0
  11. package/src/codeindex.js +1 -0
  12. package/src/colors.js +1 -22
  13. package/src/commands.js +1 -339
  14. package/src/compaction.js +1 -140
  15. package/src/config.js +1 -66
  16. package/src/consult.js +1 -65
  17. package/src/context-report.js +1 -0
  18. package/src/context.js +1 -144
  19. package/src/cron.js +1 -0
  20. package/src/daemon.js +1 -0
  21. package/src/device.js +1 -88
  22. package/src/diffview.js +1 -0
  23. package/src/doctor.js +1 -0
  24. package/src/executors.js +1 -391
  25. package/src/forks.js +1 -0
  26. package/src/gitflow.js +1 -0
  27. package/src/hooks.js +1 -0
  28. package/src/ide.js +1 -53
  29. package/src/input.js +1 -182
  30. package/src/insights.js +1 -0
  31. package/src/kbsync.js +1 -0
  32. package/src/markdown.js +0 -0
  33. package/src/markup-filter.js +1 -85
  34. package/src/mcp.js +1 -207
  35. package/src/memory.js +1 -110
  36. package/src/permissions.js +1 -82
  37. package/src/render.js +1 -134
  38. package/src/repl.js +1 -205
  39. package/src/scan.js +1 -0
  40. package/src/schedule.js +1 -0
  41. package/src/session.js +1 -140
  42. package/src/settings.js +0 -0
  43. package/src/skills.js +1 -248
  44. package/src/subagent.js +1 -82
  45. package/src/todo.js +1 -75
  46. package/src/tools.js +1 -132
  47. package/src/transcript.js +1 -0
  48. package/src/tui/app.js +1 -566
  49. package/src/usage.js +1 -43
  50. package/src/watch.js +1 -0
package/LICENSE CHANGED
@@ -1,21 +1,43 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 HuiCai AI
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ HuiCai AI — 专有软件许可协议 / Proprietary Software License
2
+
3
+ 版权所有 © 2026 HuiCai 保留所有权利。
4
+ Copyright © 2026 HuiCai. All rights reserved.
5
+
6
+ 本软件(包括 HuiCai AI 命令行工具、VS Code 扩展、JetBrains 插件及其捆绑的
7
+ 核心代码、资源与文档,下称"本软件")为汇材及其许可方的专有财产,受著作权法
8
+ 及国际条约保护。本软件按许可授权使用,而非出售。
9
+
10
+ 1. 授权。在您遵守本协议的前提下,汇材授予您一项有限的、非独占的、不可转让、
11
+ 不可再许可的权利,以目标代码形式安装并使用本软件,用于您的内部业务用途。
12
+
13
+ 2. 限制。除适用法律明确允许或经汇材事先书面同意外,您不得:
14
+ (a) 复制、分发、出售、出租、出借、再许可或以其他方式向第三方提供本软件;
15
+ (b) 修改、翻译、改编本软件,或基于本软件创作衍生作品;
16
+ (c) 反向工程、反编译、反汇编本软件,或以其他方式尝试获取其源代码或内部结构
17
+ (适用法律明确不可排除者除外);
18
+ (d) 去除或更改本软件中的任何专有声明、标识或标记。
19
+
20
+ 3. 所有权。本软件的一切权利、所有权及知识产权均归汇材及其许可方所有。本协议
21
+ 未明确授予的权利均予保留。
22
+
23
+ 4. 无担保。本软件按"现状"提供,不附带任何明示或默示担保。
24
+
25
+ 5. 责任限制。在法律允许的最大范围内,汇材不对因使用或无法使用本软件而产生的
26
+ 任何间接、附带、特殊或后果性损害承担责任。
27
+
28
+ 6. 终止。若您违反本协议,本授权自动终止;届时您须停止使用并销毁本软件的全部
29
+ 副本。
30
+
31
+ 如需商业授权、再分发或源代码相关事宜,请联系 1253007701@qq.com。
32
+
33
+ ------------------------------------------------------------------------------
34
+
35
+ This software (the "Software", including the HuiCai AI CLI, the VS Code
36
+ extension, the JetBrains plugin, and their bundled core code, resources and
37
+ documentation) is the proprietary property of HuiCai and its licensors. It is
38
+ licensed, not sold. Except as expressly permitted above or by applicable law,
39
+ you may not copy, distribute, sell, rent, sublicense, modify, translate, create
40
+ derivative works of, reverse-engineer, decompile or disassemble the Software,
41
+ nor remove any proprietary notices. All rights not expressly granted are
42
+ reserved. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. The
43
+ Chinese text above governs. Contact: 1253007701@qq.com
package/bin/huicai.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import readline from 'node:readline';
5
5
  import path from 'node:path';
6
6
  import { createRequire } from 'node:module';
7
- import { c } from '../src/colors.js';
7
+ import { c, setColorMode, setAccent } from '../src/colors.js';
8
8
  import { loadConfig, saveConfig, CONFIG_PATH, DEFAULT_MODEL } from '../src/config.js';
9
9
  import { listModels, getAgentPersona, fetchUsage, listPlatformSkills, getPlatformSkill, pushPlatformSkill } from '../src/api.js';
10
10
  import { deviceLogin } from '../src/device.js';
@@ -129,6 +129,10 @@ ${c.bold('huicai')} — 终端智能体,以 HuiCai_AI 平台为模型后端,
129
129
  huicai models 列出可用模型
130
130
  huicai usage 查看 Token 用量窗口(5小时/7天,按会员等级)
131
131
  huicai skills 列出本地 + 平台技能(Agent Skills)
132
+ huicai daemon 调度守护:按 cron 真正运行 /schedule 登记的定时任务
133
+ huicai watch ["*.js"] [-p] 文件监听:变更即触发一个智能体任务(如保存即跑测试)
134
+ huicai pr [commit] 自动提交并开 PR(生成规范 commit→建分支→push→gh 开 PR)
135
+ huicai kb-sync <id> [ref] 配合 git diff 增量同步知识库,保鲜 RAG(--dry 试运行)
132
136
  huicai skills pull [name] 平台技能 → 本地(默认 ~/.huicai/skills;--project 放入项目)
133
137
  huicai skills push <name> 本地技能 → 平台(默认同名跳过;--overwrite 覆盖)
134
138
  huicai help 显示本帮助
@@ -441,6 +445,8 @@ async function main() {
441
445
  if (flags.key) config.apiKey = flags.key;
442
446
  if (flags.model) config.model = flags.model;
443
447
  if (Number.isFinite(flags.maxTurns) && flags.maxTurns > 0) config.maxTurns = flags.maxTurns;
448
+ setColorMode(config.theme); // /theme:无色/彩色
449
+ setAccent(config.accentColor); // /color:强调色
444
450
  const cwd = flags.cwd ? path.resolve(flags.cwd) : process.cwd();
445
451
 
446
452
  // 会话列表:纯本地,无需鉴权
@@ -464,6 +470,89 @@ async function main() {
464
470
  return;
465
471
  }
466
472
 
473
+ if (sub === 'daemon') {
474
+ // 内置调度守护:按 cron 真正运行 /schedule 登记的任务(Ctrl-C 退出)
475
+ const { runDaemon } = await import('../src/daemon.js');
476
+ try {
477
+ await runDaemon({});
478
+ } catch (e) {
479
+ process.stderr.write(c.red(e.message) + '\n');
480
+ process.exit(1);
481
+ }
482
+ return;
483
+ }
484
+
485
+ if (sub === 'watch') {
486
+ // 文件监听:变更即触发一个智能体任务。用法:huicai watch ["*.js"] [-p "跑测试"]
487
+ const { runWatch } = await import('../src/watch.js');
488
+ const pattern = positional[1] && !positional[1].startsWith('-') ? positional[1] : null;
489
+ const prompt = (flags.prompt || '').trim()
490
+ || '刚有文件变更。请审查这些改动是否有问题(bug/风格/遗漏),必要时跑相关测试,简要汇报;无需大改。';
491
+ try {
492
+ await runWatch({ cwd, prompt, pattern });
493
+ } catch (e) {
494
+ process.stderr.write(c.red(e.message) + '\n');
495
+ process.exit(1);
496
+ }
497
+ return;
498
+ }
499
+
500
+ if (sub === 'pr') {
501
+ // 自动提交并开 PR:生成规范 commit → 建分支 → push → gh 开 PR(`huicai pr commit` 仅提交不推送)
502
+ const authProblem = validateAuth(config);
503
+ if (authProblem) {
504
+ process.stderr.write(authProblem + '\n');
505
+ process.exit(1);
506
+ }
507
+ const { runPrFlow } = await import('../src/gitflow.js');
508
+ const commitOnly = positional[1] === 'commit';
509
+ const r = await runPrFlow({ cwd, config, emit: (t) => process.stdout.write(c.gray(t) + '\n'), push: !commitOnly, openPr: !commitOnly });
510
+ if (!r.ok) {
511
+ process.stderr.write(c.red('✗ ' + r.error) + '\n');
512
+ process.exit(1);
513
+ }
514
+ process.stdout.write(c.green('✅ ') + (r.pr ? 'PR: ' + r.pr : '分支 ' + r.branch + (r.note ? '(' + r.note + ')' : '')) + '\n');
515
+ return;
516
+ }
517
+
518
+ if (sub === 'kb-sync') {
519
+ // 配合 git diff 增量同步知识库:huicai kb-sync <知识库ID> [ref] [--dry]
520
+ // 无 ref → 同步工作区相对 HEAD 的改动 + 未跟踪新文件
521
+ // 给 ref → 同步相对该提交/分支的改动(如 origin/main、HEAD~3、abc123;适合 CI)
522
+ const kbId = positional[1] && !positional[1].startsWith('-') ? positional[1] : null;
523
+ if (!kbId) {
524
+ process.stderr.write(c.red('用法:huicai kb-sync <知识库ID> [ref] [--dry]') + '\n'
525
+ + c.gray(' 例:huicai kb-sync 12 # 同步本地未提交改动到知识库 12') + '\n'
526
+ + c.gray(' huicai kb-sync 12 origin/main # 同步相对 origin/main 的改动(CI 常用)') + '\n');
527
+ process.exit(1);
528
+ }
529
+ const authProblem = validateAuth(config);
530
+ if (authProblem) {
531
+ process.stderr.write(authProblem + '\n');
532
+ process.exit(1);
533
+ }
534
+ const base = positional[2] && !positional[2].startsWith('-') ? positional[2] : null;
535
+ const dry = positional.includes('--dry') || positional.includes('-n');
536
+ const { runKbSync } = await import('../src/kbsync.js');
537
+ try {
538
+ const r = await runKbSync({ cwd, config, kbId, base, dry, emit: (t) => process.stdout.write(c.gray(t) + '\n') });
539
+ if (r.empty) {
540
+ process.stdout.write(c.gray(r.message) + '\n');
541
+ return;
542
+ }
543
+ if (r.dry) {
544
+ for (const f of r.preview) process.stdout.write((f.deleted ? c.red(' − ') : c.green(' + ')) + f.path + '\n');
545
+ process.stdout.write(c.gray(`试运行:${r.added} 新增/修改,${r.removed} 删除${r.skipped ? `,跳过 ${r.skipped}` : ''}(加 --dry 之外的调用即实际同步)`) + '\n');
546
+ return;
547
+ }
548
+ process.stdout.write(c.green('✅ 知识库已保鲜:') + r.message + '\n');
549
+ } catch (e) {
550
+ process.stderr.write(c.red('✗ kb-sync 失败:' + e.message) + '\n');
551
+ process.exit(1);
552
+ }
553
+ return;
554
+ }
555
+
467
556
  const problem = validateAuth(config);
468
557
  if (problem) {
469
558
  process.stderr.write(problem + '\n');
@@ -567,7 +656,9 @@ async function main() {
567
656
  }
568
657
 
569
658
  // 交互式:默认富 TUI(ink,需已安装依赖 + TTY);--plain 强制普通 REPL;TUI 不可用则自动回退
570
- const wantTui = flags.tui || (!flags.plain && process.stdout.isTTY && process.stdin.isTTY);
659
+ // 偏好顺序:命令行 --tui/--plain > 配置 tui(rich/plain) > auto(TTY 下用 TUI)
660
+ const wantTui = flags.tui || config.tui === 'rich'
661
+ || (!flags.plain && config.tui !== 'plain' && process.stdout.isTTY && process.stdin.isTTY);
571
662
  if (wantTui) {
572
663
  try {
573
664
  const { runInk } = await import('../src/tui/app.js');
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@huicai/cli",
3
- "version": "2026.710.97950",
4
- "versionDisplay": "2026.07.10.97950",
3
+ "version": "2026.711.24616",
4
+ "versionDisplay": "2026.07.11.24616",
5
5
  "description": "在终端运行的智能体 CLI:以 HuiCai_AI 平台的 OpenAI 兼容 /v1 为模型后端,在用户本机执行文件/命令工具。",
6
6
  "type": "module",
7
7
  "bin": {
@@ -22,6 +22,7 @@
22
22
  "check": "node --check bin/huicai.js",
23
23
  "test": "node --test --test-concurrency=1 test/*.test.mjs",
24
24
  "version:next": "node scripts/next-version.cjs",
25
+ "pack:release": "node scripts/pack-release.cjs",
25
26
  "prepublishOnly": "node --check bin/huicai.js"
26
27
  },
27
28
  "keywords": [
@@ -34,7 +35,7 @@
34
35
  "coding-agent"
35
36
  ],
36
37
  "author": "ChenBo",
37
- "license": "MIT",
38
+ "license": "SEE LICENSE IN LICENSE",
38
39
  "homepage": "https://github.com/SJHT-SDK/huicai-cli/tree/master#readme",
39
40
  "repository": {
40
41
  "type": "git",
package/src/advisor.js ADDED
@@ -0,0 +1 @@
1
+ import{streamChat as e}from"./api.js";export function makeAdvisorTool({config:r}){if(!r?.advisorModel)return null;return{readOnly:!0,spec:{type:"function",function:{name:"ask_advisor",description:"遇到难题、复杂决策或反复卡住时,向一个更强的「技术顾问」模型请教独立的第二意见。把问题背景、已知信息、已尝试过什么、待决策点写清楚(自足),它会给出判断/方案与关键风险。用于真正的难点或高风险决策,不要滥用。",parameters:{type:"object",properties:{question:{type:"string",description:"自足的难题描述(含背景、已尝试、待决策点)"}},required:["question"]}}},run:async t=>{const n=String(t?.question||"").trim();if(!n)return"缺少 question";try{const t=await e({baseUrl:r.baseUrl,apiKey:r.apiKey,model:r.advisorModel,messages:[{role:"system",content:"你是资深技术顾问,正被一个编码智能体咨询一个难题。基于它提供的信息,给出清晰、具体、可执行的判断或方案:指出关键风险、推荐做法、以及为何。简洁直接,不要寒暄。"},{role:"user",content:n}]});return`【顾问(${r.advisorModel})意见】\n`+(t.content||"(顾问无回复)").trim()}catch(e){return"咨询顾问失败:"+e.message}}}}
package/src/agent.js CHANGED
@@ -1,385 +1 @@
1
- // 客户端 Agent 循环:以平台 /v1 为模型后端,模型决定调哪个工具,工具在本机执行,结果回灌,直至无工具调用。
2
- // 核心机制:循环与工具都在客户端,平台只做模型推理与计费。
3
- //
4
- // 两种运行形态(按 config.model 自动判定):
5
- // - 原始模型:本机工具循环(内置文件/命令工具 + todo_write + memory_write + extraTools[MCP/子智能体]);
6
- // - 伪模型 agent-/workflow-/datahuman-:平台编排直连(不传 tools,纯流式多轮)——即智能体/数字人/工作流
7
- // 分享页的对话能力,CLI/扩展/REPL/TUI 全部可用。
8
- //
9
- // 关键机制:
10
- // - 连续的只读工具调用并行执行(结果仍按原顺序回灌);
11
- // - 工具输出截断保头尾注入上下文;工具参数 JSON 解析失败时把错误回灌模型(而非静默空参数);
12
- // - 中断时为未执行的 tool_calls 合成占位结果(协议配对完整);
13
- // - pullQueued:用户回合中排队的消息在工具轮间隙/回合末注入;
14
- // - 思考过程(reasoning)流式上抛(events.onReasoning / REPL 灰字),config.showReasoning=false 关闭;
15
- // - 上下文压缩为**非破坏式**:摘要存 compaction 状态(随会话持久化),原消息/页面显示不受影响,
16
- // 仅发给模型时经 buildModelMessages 组装视图;
17
- // - 文本化工具调用防泄漏(与平台侧同款):模型把工具调用写成纯文本标记时(DeepSeek 系退化)
18
- // 过滤不显示、不进会话,并注入纠正指令重试(一次要求改走 tool_calls 通道,二次禁工具强制作答)。
19
-
20
- import fs from 'node:fs';
21
- import path from 'node:path';
22
- import { streamChat } from './api.js';
23
- import { MarkupFilter, stripMarkup } from './markup-filter.js';
24
- import { TOOL_SPECS, READONLY_TOOLS, runTool } from './tools.js';
25
- import { toolLabel, indent, renderFileDiff } from './render.js';
26
- import { compactIfNeeded, buildModelMessages } from './compaction.js';
27
- import { makeTodoTool, formatTodos, TODO_TOOL_NAME } from './todo.js';
28
- import { makeMemoryTool } from './memory.js';
29
- import * as ide from './ide.js';
30
- import { c } from './colors.js';
31
-
32
- const DEFAULT_MAX_ITERS = 120; // 中大型项目需更多工具轮(读多文件/多次检索/多处编辑);仍可 config.maxTurns 或 --max-turns 覆盖
33
- const TOOL_CONCURRENCY = 8; // 并行只读工具的并发上限:大仓库里模型一次触发大量 grep/glob 时限并发,降争用、提稳定与总吞吐
34
- const MAX_TOOL_RESULT_CHARS = 20_000;
35
- const CAP_HEAD = 16_000;
36
- const CAP_TAIL = 3_000;
37
-
38
- // 「文本化工具调用」被拦截后的纠正指令:与平台侧 ToolCallingOrchestrator 同款两级策略。
39
- // 第一次:要求改走规范 tool_calls 通道重试;第二次:禁用工具、强制直接作答(保证用户拿到干净答复)。
40
- const MARKUP_RETRY_PROMPT =
41
- '你刚才的输出把工具调用写成了文本标记,工具并未被执行,该内容也不会展示给用户。'
42
- + '请重新处理:若需调用工具,必须使用规范的函数调用(tool_calls)通道发起,'
43
- + '绝不要以任何文本形式书写工具调用标记;若无需工具,请直接用中文输出面向用户的最终答复。';
44
- const MARKUP_FORCE_ANSWER_PROMPT =
45
- '请停止调用任何工具。基于目前已获得的全部信息,直接用中文输出面向用户的完整最终答复,'
46
- + '不要输出任何工具调用或协议标记。';
47
- // 预算收敛:临近工具轮数上限时注入,禁用工具并要求直接作答,保证中大型项目里到达上限也能给出干净的最终答复(而非被硬截断)。
48
- const BUDGET_FINAL_PROMPT =
49
- '已接近本回合的工具调用轮数上限。请不要再调用任何工具,基于目前已获得的信息,直接用中文输出面向用户的完整最终答复。';
50
-
51
- /** 有序的有限并发 map:并发不超过 limit,结果按输入下标回填(与 Promise.all 相同的顺序语义,但并发受限)。 */
52
- async function mapLimit(items, limit, fn) {
53
- const results = new Array(items.length);
54
- let next = 0;
55
- const worker = async () => {
56
- while (next < items.length) {
57
- const i = next++;
58
- results[i] = await fn(items[i], i);
59
- }
60
- };
61
- await Promise.all(Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, worker));
62
- return results;
63
- }
64
-
65
- /** 伪模型(平台编排)判定:agent-/workflow-/datahuman- 前缀。 */
66
- export function isPlatformModel(model) {
67
- return /^(agent|workflow|datahuman)-/.test(String(model || ''));
68
- }
69
-
70
- function shortLine(s, max = 200) {
71
- const first = String(s).split('\n')[0];
72
- return first.length > max ? first.slice(0, max) + '…' : first;
73
- }
74
-
75
- /** 工具输出超长时截断(保头尾),防止单条结果撑爆上下文。 */
76
- export function capToolResult(text) {
77
- const s = String(text);
78
- if (s.length <= MAX_TOOL_RESULT_CHARS) return s;
79
- const omitted = s.length - CAP_HEAD - CAP_TAIL;
80
- return s.slice(0, CAP_HEAD) + `\n…【工具输出过长,中间已省略 ${omitted} 字符】…\n` + s.slice(-CAP_TAIL);
81
- }
82
-
83
- /**
84
- * 跑一个用户回合:mutate messages(追加 assistant/tool 消息),流式打印正文与工具进展。
85
- *
86
- * @param {object} p
87
- * @param {{spec:object,run:Function,readOnly:boolean}[]} [p.extraTools] 动态外部工具(MCP / 子智能体)
88
- * @param {number} [p.depth] 子智能体递归深度(0=顶层;todo/memory 仅顶层提供)
89
- * @param {{write:(s:string)=>void}} [p.io] 输出通道(子智能体传静默通道)
90
- * @param {object} [p.events] 结构化事件:onDelta/onReasoning/onAssistantText/onToolStart/onToolResult/onFileDiff/onSkip/onInfo/onTodo/onMemory/onTurnEnd
91
- * @param {{list:any[]}} [p.todoState] 任务清单状态(调用方持有可跨轮保留)
92
- * @param {{upTo?:number, summary?:string}} [p.compaction] 压缩状态(随会话持久化;原消息不被修改)
93
- * @param {() => (string|null)} [p.pullQueued] 取一条用户排队消息(无则 null)
94
- * @param {number} [p.maxIters] 本回合最大工具轮数(默认 config.maxTurns 或 60)
95
- * @returns {Promise<{aborted?:boolean, error?:string, usage:{prompt:number,completion:number}, lastPromptTokens:number}>}
96
- */
97
- export async function runTurn({
98
- config, messages, permissions, cwd, ask, signal,
99
- extraTools = [], depth = 0, io, events, todoState, compaction, pullQueued, maxIters,
100
- }) {
101
- const out = (io && io.write) || ((s) => process.stdout.write(s));
102
- const ev = events || null; // 结构化事件(ink TUI / 扩展用);为空则走 out() 字符串输出(普通 REPL)
103
- const info = (t) => {
104
- if (ev) ev.onInfo?.(t);
105
- else out(c.gray(' · ' + t) + '\n');
106
- };
107
- let prompt = 0;
108
- let completion = 0;
109
- let lastPromptTokens = 0;
110
- const iterMax = Number(maxIters || config.maxTurns) || DEFAULT_MAX_ITERS;
111
- const platformMode = isPlatformModel(config.model);
112
- const compState = compaction || {}; // 未传则本回合内独立生效(不持久化)
113
-
114
- // 顶层注入 todo_write / memory_write(平台编排模式无本机工具,均不注入)
115
- const dynamicTools = [...extraTools];
116
- if (depth === 0 && !platformMode && !dynamicTools.some((t) => t.spec?.function?.name === TODO_TOOL_NAME)) {
117
- const state = todoState || { list: [] };
118
- dynamicTools.push(
119
- makeTodoTool({
120
- state,
121
- emit: (list) => {
122
- if (ev) {
123
- if (ev.onTodo) ev.onTodo(list.map((t) => ({ ...t })));
124
- else ev.onInfo?.('任务清单:\n' + formatTodos(list));
125
- } else {
126
- out(c.gray(indent(formatTodos(list), ' ')) + '\n');
127
- }
128
- },
129
- }),
130
- );
131
- }
132
- if (depth === 0 && !platformMode && config.memoryEnabled !== false
133
- && !dynamicTools.some((t) => t.spec?.function?.name === 'memory_write')) {
134
- dynamicTools.push(
135
- makeMemoryTool({
136
- cwd,
137
- emit: (t) => {
138
- if (ev) {
139
- if (ev.onMemory) ev.onMemory(t);
140
- else ev.onInfo?.('已记入长期记忆:' + shortLine(t, 80));
141
- } else {
142
- out(c.gray(' ✎ 已记入长期记忆:' + shortLine(t, 80)) + '\n');
143
- }
144
- },
145
- }),
146
- );
147
- }
148
-
149
- const extraByName = new Map();
150
- for (const t of dynamicTools) extraByName.set(t.spec.function.name, t);
151
- const specs = platformMode ? undefined : [...TOOL_SPECS, ...dynamicTools.map((t) => t.spec)];
152
-
153
- let markupRetries = 0; // 「文本化工具调用」纠正次数(首次要求改走 tool_calls 通道,二次禁工具收敛)
154
- let toolsDisabled = false;
155
- let correction = null; // 待注入的纠正指令:仅随本轮请求发给模型,不进会话记录(与平台侧临时 convo 等价)
156
-
157
- for (let iter = 0; iter < iterMax; iter++) {
158
- // 预算收敛:最后一轮禁用工具并提示直接作答,保证中大型项目里到达上限也能给出干净的最终答复(而非被硬截断)
159
- const lastIter = iter === iterMax - 1;
160
- if (lastIter && !platformMode && !toolsDisabled && !correction) correction = BUDGET_FINAL_PROMPT;
161
- // 非破坏式上下文压缩:仅更新 compState(摘要独立存储),messages/页面内容不动
162
- const compacted = await compactIfNeeded(messages, config, { state: compState, lastPromptTokens });
163
- if (compacted > 0) info(`已压缩上下文(${compacted} 条较早消息已摘要,页面与会话记录不受影响)`);
164
-
165
- let streamedAny = false;
166
- let streamedReason = false;
167
- const mf = new MarkupFilter(); // 文本化工具标记防泄漏:每轮模型输出独立实例
168
- const emitDelta = (t) => {
169
- if (!t) return;
170
- if (!streamedAny && streamedReason && !ev) out('\n'); // 思考→正文换行
171
- streamedAny = true;
172
- if (ev) ev.onDelta?.(t);
173
- else out(t);
174
- };
175
- const modelMessages = buildModelMessages(messages, compState);
176
- let res;
177
- try {
178
- res = await streamChat(
179
- {
180
- baseUrl: config.baseUrl,
181
- apiKey: config.apiKey,
182
- model: config.model,
183
- messages: correction ? [...modelMessages, { role: 'user', content: correction }] : modelMessages,
184
- tools: (toolsDisabled || lastIter) ? undefined : specs,
185
- // 对话落库审计:来源与会话号(由 config 透传;扩展侧为 vscode)
186
- client: config.client,
187
- sessionId: config.sessionId,
188
- },
189
- {
190
- onContent: (t) => emitDelta(mf.feed(t)),
191
- onReasoning: (t) => {
192
- if (config.showReasoning === false) return;
193
- if (ev) {
194
- ev.onReasoning?.(t);
195
- } else {
196
- if (!streamedReason) out(c.gray('\n[思考] '));
197
- out(c.gray(t));
198
- }
199
- streamedReason = true;
200
- },
201
- onRetry: (n, waitMs, reason) => info(`模型响应异常,${Math.round(waitMs / 1000)}s 后自动重试(第 ${n} 次):${reason}`),
202
- },
203
- signal,
204
- );
205
- } catch (e) {
206
- if (e.name === 'AbortError') {
207
- if (ev) ev.onInfo?.('已中断');
208
- else out(c.gray('\n[已中断]\n'));
209
- return { aborted: true, usage: { prompt, completion }, lastPromptTokens };
210
- }
211
- if (ev) ev.onInfo?.('✗ 模型调用失败:' + e.message);
212
- else out('\n' + c.red('✗ 模型调用失败:') + e.message + '\n');
213
- return { error: e.message, usage: { prompt, completion }, lastPromptTokens };
214
- }
215
- emitDelta(mf.flush()); // 放行轮末仍持有的未确认内容(吞噬中则丢弃)
216
-
217
- if (res.usage) {
218
- prompt += res.usage.prompt_tokens || 0;
219
- completion += res.usage.completion_tokens || 0;
220
- lastPromptTokens = res.usage.prompt_tokens || lastPromptTokens;
221
- }
222
-
223
- // 全量内容按同一状态机二次清洗(与流式放行结果一致):协议标记绝不进入会话记录与后续上下文
224
- const { text: cleanText, sawMarkup: sawInFull } = stripMarkup(res.content);
225
- const markupRound = sawInFull || mf.sawMarkup;
226
- correction = null; // 上一轮注入的纠正指令已被消费
227
-
228
- const toolCalls = platformMode ? [] : res.toolCalls;
229
- if (!toolCalls.length) {
230
- // 「文本化工具调用」拦截后的纠正:本轮"正文"实为协议标记(已被过滤,未显示,工具也未执行)。
231
- // 平台编排模式(伪模型)由服务端过滤与纠正,客户端不再重试。
232
- if (markupRound && !cleanText.trim() && !platformMode) {
233
- if (markupRetries < 2) {
234
- markupRetries++;
235
- if (markupRetries === 2) toolsDisabled = true;
236
- correction = markupRetries === 1 ? MARKUP_RETRY_PROMPT : MARKUP_FORCE_ANSWER_PROMPT;
237
- info(`已拦截模型的文本化工具调用标记(工具未被执行),纠正重试中(第 ${markupRetries} 次)`);
238
- continue;
239
- }
240
- info('模型持续输出无效的工具调用标记,已全部拦截;本轮无有效答复(可换个说法重试或更换模型)');
241
- }
242
- if (streamedAny && !ev) out('\n');
243
- // 记录最终答复进上下文,使后续用户回合能看到助手上一次回复(多轮对话记忆)
244
- if (cleanText) messages.push({ role: 'assistant', content: cleanText });
245
- if (ev) ev.onTurnEnd?.(cleanText);
246
- // 用户在回合中排队了新消息 → 直接续跑,无需重新发起回合
247
- const queued = pullQueued?.();
248
- if (queued) {
249
- messages.push({ role: 'user', content: queued });
250
- continue;
251
- }
252
- return { usage: { prompt, completion }, lastPromptTokens };
253
- }
254
-
255
- if (streamedAny && !ev) out('\n');
256
- if (ev && cleanText) ev.onAssistantText?.(cleanText); // 工具轮前的正文先落定
257
- messages.push({ role: 'assistant', content: cleanText, tool_calls: toolCalls });
258
-
259
- // 预解析全部调用(名称/参数/只读性);参数 JSON 解析失败记录错误(回灌模型重试,而非静默空参数)
260
- const parsed = toolCalls.map((tc, idx) => {
261
- const name = tc.function?.name || '';
262
- let args = {};
263
- let argsError = null;
264
- const raw = tc.function?.arguments || '';
265
- if (raw) {
266
- try {
267
- args = JSON.parse(raw);
268
- } catch (e) {
269
- argsError = `工具参数不是合法 JSON(${e.message}):${String(raw).slice(0, 200)}…请修正参数后重新调用。`;
270
- }
271
- }
272
- const extra = extraByName.get(name);
273
- // IDE 集成下,write_file/edit_file 的批准发生在编辑器的内联 diff 审阅里,跳过 CLI 自身的 y/n(避免双重确认)
274
- const deferToIde = ide.isActive() && (name === 'write_file' || name === 'edit_file');
275
- const readOnly = extra ? !!extra.readOnly : READONLY_TOOLS.has(name);
276
- return { tc, idx, name, args, argsError, extra, readOnly, deferToIde };
277
- });
278
-
279
- // 中断时为剩余未执行的 tool_calls 合成占位结果,保证 assistant.tool_calls 配对完整(协议要求)
280
- const abortRest = (from) => {
281
- for (let k = from; k < parsed.length; k++) {
282
- messages.push({ role: 'tool', tool_call_id: parsed[k].tc.id || `call_${k}`, content: '(用户中断,该工具未执行)' });
283
- }
284
- if (ev) ev.onInfo?.('已中断');
285
- else out(c.gray('\n[已中断]\n'));
286
- return { aborted: true, usage: { prompt, completion }, lastPromptTokens };
287
- };
288
-
289
- const execOne = async (item) => {
290
- if (item.argsError) return item.argsError;
291
- let result;
292
- try {
293
- result = item.extra ? await item.extra.run(item.args) : await runTool(item.name, item.args, cwd);
294
- } catch (e) {
295
- result = `工具执行失败:${e.message}`;
296
- }
297
- return capToolResult(String(result));
298
- };
299
- const emitStart = (item) => {
300
- if (ev) ev.onToolStart?.(item.name, item.args);
301
- else out(c.cyan(' ⏺ ') + toolLabel(item.name, item.args) + '\n');
302
- };
303
- const emitResult = (item, result) => {
304
- if (ev) ev.onToolResult?.(item.name, String(result));
305
- else out(c.gray(' ' + shortLine(result)) + '\n');
306
- messages.push({ role: 'tool', tool_call_id: item.tc.id, content: String(result) });
307
- };
308
- const emitSkip = (item, reason) => {
309
- if (ev) ev.onSkip?.(item.name, item.args);
310
- else out(c.gray(` ⊘ ${toolLabel(item.name, item.args)} — 已跳过\n`));
311
- messages.push({ role: 'tool', tool_call_id: item.tc.id, content: reason || '未授权,未执行。' });
312
- };
313
- // 写文件类工具(edit_file/write_file)成功后向 UI 展示「更改的行」(display-only,不进模型上下文)。
314
- // 以「执行前后文件内容对比」判定:未改动/失败/被拒 → before==after → 空 diff,天然不显示。
315
- const isFileWrite = (name) => name === 'edit_file' || name === 'write_file';
316
- const readSafe = (abs) => { try { return fs.readFileSync(abs, 'utf8'); } catch { return ''; } };
317
- const emitFileDiff = (item, before) => {
318
- const abs = path.resolve(cwd, String(item.args.path));
319
- const diff = renderFileDiff(before, readSafe(abs), { path: String(item.args.path) });
320
- if (!diff) return;
321
- if (ev) ev.onFileDiff?.(diff);
322
- else out(diff + '\n');
323
- };
324
-
325
- let i = 0;
326
- while (i < parsed.length) {
327
- if (signal?.aborted) return abortRest(i);
328
- const cur = parsed[i];
329
-
330
- // 参数损坏:不请求授权、不执行,直接把解析错误回灌(模型可修正后重试)
331
- if (cur.argsError) {
332
- emitStart(cur);
333
- emitResult(cur, cur.argsError);
334
- i++;
335
- continue;
336
- }
337
-
338
- if (cur.readOnly) {
339
- // 连续只读段:并行执行(权限对只读即时放行),结果按原顺序回灌
340
- let j = i;
341
- while (j < parsed.length && parsed[j].readOnly && !parsed[j].argsError) j++;
342
- const batch = parsed.slice(i, j);
343
- // 只读段并行执行,但并发受限(大仓库里模型可能一次触发很多 grep/glob,无限并发会抢占磁盘/CPU 反而更慢);结果仍按原顺序回灌
344
- const settled = await mapLimit(batch, TOOL_CONCURRENCY, async (item) => {
345
- const perm = await permissions.request(item.name, item.args, cwd, true);
346
- if (!perm.allow) return { item, denied: perm.reason || '未授权,未执行。' };
347
- emitStart(item);
348
- return { item, result: await execOne(item) };
349
- });
350
- for (const s of settled) {
351
- if (s.denied != null) emitSkip(s.item, s.denied);
352
- else emitResult(s.item, s.result);
353
- }
354
- i = j;
355
- if (signal?.aborted) return abortRest(i);
356
- continue;
357
- }
358
-
359
- // 写/执行类:顺序执行,逐个审批
360
- const perm = await permissions.request(cur.name, cur.args, cwd, cur.readOnly || cur.deferToIde);
361
- if (signal?.aborted) return abortRest(i);
362
- if (!perm.allow) {
363
- emitSkip(cur, perm.reason);
364
- i++;
365
- continue;
366
- }
367
- emitStart(cur);
368
- const before = isFileWrite(cur.name) && cur.args?.path
369
- ? readSafe(path.resolve(cwd, String(cur.args.path)))
370
- : null;
371
- const result = await execOne(cur);
372
- emitResult(cur, result);
373
- if (before != null) emitFileDiff(cur, before);
374
- i++;
375
- }
376
-
377
- // 工具轮之间注入用户排队消息(协议合法:tool 配对已完整),模型下一轮即可看到
378
- const queued = pullQueued?.();
379
- if (queued) messages.push({ role: 'user', content: queued });
380
- }
381
-
382
- if (ev) ev.onInfo?.('已达最大工具轮数,停止');
383
- else out(c.yellow('\n[已达最大工具轮数,停止]\n'));
384
- return { usage: { prompt, completion }, lastPromptTokens };
385
- }
1
+ import t from"node:fs";import o from"node:path";import{streamChat as e}from"./api.js";import{MarkupFilter as n,stripMarkup as r}from"./markup-filter.js";import{TOOL_SPECS as s,READONLY_TOOLS as a,runTool as l}from"./tools.js";import{toolLabel as i,indent as c,renderFileDiff as u}from"./render.js";import{compactIfNeeded as m,buildModelMessages as g}from"./compaction.js";import{makeTodoTool as p,formatTodos as f,TODO_TOOL_NAME as d}from"./todo.js";import{makeMemoryTool as h}from"./memory.js";import{loadHooks as y,runHooks as w}from"./hooks.js";import*as S from"./ide.js";import{c as _}from"./colors.js";function T(t){const o=String(t);return o.startsWith("工具执行失败")||o.startsWith("命令启动失败")||o.includes("不是合法 JSON")||o.includes("未找到要替换的内容")||o.includes("路径超出项目目录")||o.includes("是目录而非文件")}function b(t){return"\n\n# 当前目标(用户设定)\n"+String(t).trim()+"\n在本回合结束、停止作答前,检查该目标是否已达成:若未达成,应继续推进(补充调研/改动/验证),不要过早停止;达成后用一句话说明已达成。"}function k(t){return"string"==typeof t?t:Array.isArray(t)?t.filter(t=>"text"===t?.type).map(t=>t.text||"").join("\n"):""}async function x(t,o,e){const n=new Array(t.length);let r=0;return await Promise.all(Array.from({length:Math.max(1,Math.min(o,t.length))},async()=>{for(;r<t.length;){const o=r++;n[o]=await e(t[o],o)}})),n}export function isPlatformModel(t){return/^(agent|workflow|datahuman)-/.test(String(t||""))}function P(t,o=200){const e=String(t).split("\n")[0];return e.length>o?e.slice(0,o)+"…":e}export function capToolResult(t){const o=String(t);if(o.length<=2e4)return o;const e=o.length-16e3-3e3;return o.slice(0,16e3)+`\n…【工具输出过长,中间已省略 ${e} 字符】…\n`+o.slice(-3e3)}export async function runTurn({config:$,messages:j,permissions:M,cwd:I,ask:E,signal:U,extraTools:v=[],depth:A=0,io:O,events:C,todoState:R,compaction:N,pullQueued:D,maxIters:J,fileJournal:q}){const F=O&&O.write||(t=>process.stdout.write(t)),K=C||null,W=t=>{K?K.onInfo?.(t):F(_.gray(" · "+t)+"\n")};let L=0,Q=0,z=0;const B=Number(J||$.maxTurns)||120,G=isPlatformModel($.model);let H=$.model;if(0===A&&$.autoModelStrong&&!G){const t=[...j].reverse().find(t=>"user"===t.role),o=k(t?.content);($.planMode||o.length>400||/重构|架构|设计|复杂|排查|调试|debug|优化|性能|并发|死锁|重写|迁移|安全审查|根因/i.test(o))&&$.autoModelStrong!==$.model&&(H=$.autoModelStrong,$.debug&&W(`[automodel] 复杂回合升配 → ${H}`))}const V=N||{},X=[...v];if(0===A&&!G&&!X.some(t=>t.spec?.function?.name===d)){const t=R||{list:[]};X.push(p({state:t,emit:t=>{K?K.onTodo?K.onTodo(t.map(t=>({...t}))):K.onInfo?.("任务清单:\n"+f(t)):F(_.gray(c(f(t)," "))+"\n")}}))}0!==A||G||!1===$.memoryEnabled||X.some(t=>"memory_write"===t.spec?.function?.name)||X.push(h({cwd:I,emit:t=>{K?K.onMemory?K.onMemory(t):K.onInfo?.("已记入长期记忆:"+P(t,80)):F(_.gray(" ✎ 已记入长期记忆:"+P(t,80))+"\n")}}));const Y=new Map;for(const t of X)Y.set(t.spec.function.name,t);const Z=G?void 0:[...s,...X.map(t=>t.spec)];let tt=0,ot=!1,et=0,nt=null;const rt=0!==A||G?null:y(I),st=!(!rt||!rt.all.length);if(st){const t=[];if(j.filter(t=>"user"===t.role).length<=1){const o=w("SessionStart",{cwd:I},rt);o.injectContext&&t.push(o.injectContext);for(const e of o.outputs)t.push(e.stdout)}const o=[...j].reverse().find(t=>"user"===t.role),e=w("UserPromptSubmit",{cwd:I,prompt:k(o?.content)},rt);if(e.block)return W("提交被 UserPromptSubmit 钩子阻断:"+e.reason),{error:"提交被钩子阻断:"+e.reason,usage:{prompt:L,completion:Q},lastPromptTokens:z};e.injectContext&&t.push(e.injectContext);for(const o of e.outputs)t.push(o.stdout);t.length&&(nt="【钩子提供的附加上下文】\n"+t.join("\n"))}for(let s=0;s<B;s++){const c=s===B-1;!c||G||ot||nt||(nt="已接近本回合的工具调用轮数上限。请不要再调用任何工具,基于目前已获得的信息,直接用中文输出面向用户的完整最终答复。");const p=await m(j,$,{state:V,lastPromptTokens:z});p>0&&W(`已压缩上下文(${p} 条较早消息已摘要,页面与会话记录不受影响)`);let f=!1,d=!1;const h=new n,y=t=>{t&&(f||!d||K||F("\n"),f=!0,K?K.onDelta?.(t):F(t))},k=g(j,V);let E=k;if(!G&&k.length&&"system"===k[0]?.role){let t="";$.planMode&&(t+="\n\n# 计划模式(当前开启)\n你现在处于计划模式:只能用只读工具(read_file/glob/grep/list_files/fetch_url)调研,不得做任何改动——write_file/edit_file/run_bash 等写或执行操作都会被拒绝,不要反复尝试。请先充分理解需求与现状,然后产出一份清晰、分步的实施方案:涉及哪些文件、每步改动要点、如何验证、风险点,用 todo_write 列出步骤,随后停下等待用户确认。用户关闭计划模式后你再执行。"),$.sandbox&&(t+="\n\n# 沙箱模式(当前开启)\n禁止执行 shell 命令(run_bash)与联网(fetch_url),调用会被拒绝,不要反复尝试。只做本地文件读写与分析。"),$.goal&&String($.goal).trim()&&(t+=b($.goal)),t&&(E=[{...k[0],content:String(k[0].content||"")+t},...k.slice(1)])}const v=Date.now();let A;try{A=await e({baseUrl:$.baseUrl,apiKey:$.apiKey,model:H,messages:nt?[...E,{role:"user",content:nt}]:E,tools:ot||c?void 0:Z,reasoningEffort:$.reasoningEffort,client:$.client,sessionId:$.sessionId},{onContent:t=>y(h.feed(t)),onReasoning:t=>{!1!==$.showReasoning&&(K?K.onReasoning?.(t):(d||F(_.gray("\n[思考] ")),F(_.gray(t))),d=!0)},onRetry:(t,o,e)=>W(`模型响应异常,${Math.round(o/1e3)}s 后自动重试(第 ${t} 次):${e}`)},U)}catch(t){return"AbortError"===t.name?(K?K.onInfo?.("已中断"):F(_.gray("\n[已中断]\n")),{aborted:!0,usage:{prompt:L,completion:Q},lastPromptTokens:z}):(K?K.onInfo?.("✗ 模型调用失败:"+t.message):F("\n"+_.red("✗ 模型调用失败:")+t.message+"\n"),{error:t.message,usage:{prompt:L,completion:Q},lastPromptTokens:z})}y(h.flush()),A.usage&&(L+=A.usage.prompt_tokens||0,Q+=A.usage.completion_tokens||0,z=A.usage.prompt_tokens||z),$.debug&&W(`[debug] 轮${s+1} · ${$.model} · in ${A.usage?.prompt_tokens||0} / out ${A.usage?.completion_tokens||0} tok · ${Date.now()-v}ms · ${(A.toolCalls||[]).length} 工具调用`);const{text:O,sawMarkup:C}=r(A.content),R=C||h.sawMarkup;nt=null;const N=G?[]:A.toolCalls;if(!N.length){if(R&&!O.trim()&&!G){if(tt<2){tt++,2===tt&&(ot=!0),nt=1===tt?"你刚才的输出把工具调用写成了文本标记,工具并未被执行,该内容也不会展示给用户。请重新处理:若需调用工具,必须使用规范的函数调用(tool_calls)通道发起,绝不要以任何文本形式书写工具调用标记;若无需工具,请直接用中文输出面向用户的最终答复。":"请停止调用任何工具。基于目前已获得的全部信息,直接用中文输出面向用户的完整最终答复,不要输出任何工具调用或协议标记。",W(`已拦截模型的文本化工具调用标记(工具未被执行),纠正重试中(第 ${tt} 次)`);continue}W("模型持续输出无效的工具调用标记,已全部拦截;本轮无有效答复(可换个说法重试或更换模型)")}f&&!K&&F("\n"),O&&j.push({role:"assistant",content:O}),K&&K.onTurnEnd?.(O);const t=D?.();if(t){j.push({role:"user",content:t});continue}if(st){const t=w("Stop",{cwd:I,result:O},rt);for(const o of t.outputs)W("钩子·Stop:"+P(o.stdout,160))}return{usage:{prompt:L,completion:Q},lastPromptTokens:z}}f&&!K&&F("\n"),K&&O&&K.onAssistantText?.(O),j.push({role:"assistant",content:O,tool_calls:N});const J=N.map((t,o)=>{const e=t.function?.name||"";let n={},r=null;const s=t.function?.arguments||"";if(s)try{n=JSON.parse(s)}catch(t){r=`工具参数不是合法 JSON(${t.message}):${String(s).slice(0,200)}…请修正参数后重新调用。`}const l=Y.get(e),i=S.isActive()&&("write_file"===e||"edit_file"===e);return{tc:t,idx:o,name:e,args:n,argsError:r,extra:l,readOnly:l?!!l.readOnly:a.has(e),deferToIde:i}}),X=t=>{for(let o=t;o<J.length;o++)j.push({role:"tool",tool_call_id:J[o].tc.id||`call_${o}`,content:"(用户中断,该工具未执行)"});return K?K.onInfo?.("已中断"):F(_.gray("\n[已中断]\n")),{aborted:!0,usage:{prompt:L,completion:Q},lastPromptTokens:z}},at=async t=>{if(t.argsError)return t.argsError;let o;try{o=t.extra?await t.extra.run(t.args):await l(t.name,t.args,I,$)}catch(t){o=`工具执行失败:${t.message}`}return capToolResult(String(o))},lt=t=>{K?K.onToolStart?.(t.name,t.args):F(_.cyan(" ⏺ ")+i(t.name,t.args)+"\n")},it=(t,o)=>{T(o)&&et++,K?K.onToolResult?.(t.name,String(o)):F(_.gray(" "+P(o))+"\n"),j.push({role:"tool",tool_call_id:t.tc.id,content:String(o)})},ct=(t,o)=>{K?K.onSkip?.(t.name,t.args):F(_.gray(` ⊘ ${i(t.name,t.args)} — 已跳过\n`)),j.push({role:"tool",tool_call_id:t.tc.id,content:o||"未授权,未执行。"})},ut=t=>"edit_file"===t||"write_file"===t,mt=o=>{try{return t.readFileSync(o,"utf8")}catch{return""}},gt=(t,e)=>{const n=o.resolve(I,String(t.args.path)),r=u(e,mt(n),{path:String(t.args.path)});r&&(K?K.onFileDiff?.(r):F(r+"\n"))};let pt=0;for(;pt<J.length;){if(U?.aborted)return X(pt);const e=J[pt];if(e.argsError){lt(e),it(e,e.argsError),pt++;continue}if(e.readOnly){let t=pt;for(;t<J.length&&J[t].readOnly&&!J[t].argsError;)t++;const o=J.slice(pt,t),e=await x(o,8,async t=>{const o=await M.request(t.name,t.args,I,!0);return o.allow?(lt(t),{item:t,result:await at(t)}):{item:t,denied:o.reason||"未授权,未执行。"}});for(const t of e)null!=t.denied?ct(t.item,t.denied):it(t.item,t.result);if(pt=t,U?.aborted)return X(pt);continue}const n=await M.request(e.name,e.args,I,e.readOnly||e.deferToIde);if(U?.aborted)return X(pt);if(!n.allow){ct(e,n.reason),pt++;continue}if(st){const t=w("PreToolUse",{cwd:I,toolName:e.name,args:e.args},rt);for(const o of t.outputs)W("钩子·PreToolUse:"+P(o.stdout,120));if(t.block){ct(e,"PreToolUse 钩子阻断:"+t.reason),pt++;continue}t.modifiedArgs&&(e.args={...e.args,...t.modifiedArgs},W("钩子·PreToolUse 已修改工具参数"))}lt(e);const r=ut(e.name)&&e.args?.path?mt(o.resolve(I,String(e.args.path))):null;if(q&&null!=r&&e.args?.path){const n=t.existsSync(o.resolve(I,String(e.args.path)));q.push(r.length>2097152?{path:String(e.args.path),tooLarge:!0}:{path:String(e.args.path),before:r,existed:n})}let s=await at(e);if(st){const t=w("PostToolUse",{cwd:I,toolName:e.name,args:e.args,result:String(s)},rt);if(t.outputs.length){for(const o of t.outputs)W("钩子·PostToolUse:"+P(o.stdout,120));s=String(s)+"\n\n[PostToolUse 钩子输出]\n"+t.outputs.map(t=>t.stdout).join("\n")}}it(e,s),null!=r&&gt(e,r),pt++}et>=3&&!ot&&!nt&&(nt="你刚才连续多次工具调用失败。请停下来重新审视:是不是文件路径/内容/前提假设错了?先用 read_file / grep / list_files 核实现状,再换一种更可靠的方法,绝不要重复同样会失败的调用。",et=0,W("检测到连续工具失败,已提示智能体换一种更可靠的方法"));const ft=D?.();ft&&j.push({role:"user",content:ft})}return K?K.onInfo?.("已达最大工具轮数,停止"):F(_.yellow("\n[已达最大工具轮数,停止]\n")),{usage:{prompt:L,completion:Q},lastPromptTokens:z}}