@huicai/cli 2026.1019.0 → 2026.1021.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/bin/huicai.js +10 -2
- package/bin/update.js +97 -0
- package/package.json +2 -2
- package/skills/ai-coding/SKILL.md +2 -0
- package/skills/eng-develop/SKILL.md +9 -1
- package/skills/eng-develop/references/dod-checklist.md +3 -0
- package/skills/eng-develop/references/engineering-workflow.md +37 -0
- package/src/agent.js +1 -1
- package/src/api.js +1 -1
- package/src/config.js +1 -1
- package/src/context.js +1 -1
- package/src/engineering.js +1 -0
- package/src/executors.js +1 -1
- package/src/ide.js +1 -1
- package/src/repl.js +1 -1
- package/src/session.js +1 -1
- package/src/test-runner.js +1 -1
- package/src/toolpipe.js +1 -1
- package/src/tui/app.js +1 -1
- package/src/vision.js +1 -1
package/README.md
CHANGED
|
@@ -18,6 +18,8 @@ npm link # 安装全局 huicai 命令(或:node bin/huicai.js)
|
|
|
18
18
|
核心链路**零第三方运行时依赖**(`--plain` 或未装 ink 时,只用 Node 内置模块即可跑)。
|
|
19
19
|
TTY 默认全屏,退出恢复终端;`--plain` 普通模式。缺依赖时回退。见[操作指南](docs/terminal-workbench.md)。
|
|
20
20
|
|
|
21
|
+
支持[启动提醒与自动更新](docs/terminal-workbench.md#版本更新)。
|
|
22
|
+
|
|
21
23
|
## 配置
|
|
22
24
|
|
|
23
25
|
```bash
|
package/bin/huicai.js
CHANGED
|
@@ -17,6 +17,7 @@ import { refreshPlan, isPlanLimitError } from '../src/plan-limits.js';
|
|
|
17
17
|
import fs from 'node:fs';
|
|
18
18
|
import { lintSkillLibrary } from '../src/skill-lint.js';
|
|
19
19
|
import { tokenize } from '../src/codeindex.js';
|
|
20
|
+
import { createUpdater, displayVersion } from './update.js';
|
|
20
21
|
|
|
21
22
|
// 版本从 package.json 读取(versionDisplay 年月日展示式,与 VSCode 扩展、IDEA 插件一致);勿手改,用 scripts/next-version.cjs
|
|
22
23
|
const _pkg = createRequire(import.meta.url)('../package.json');
|
|
@@ -846,6 +847,11 @@ async function main() {
|
|
|
846
847
|
}
|
|
847
848
|
|
|
848
849
|
// 交互式:默认富 TUI(ink,需已安装依赖 + TTY);--plain 强制普通 REPL;TUI 不可用则自动回退
|
|
850
|
+
const updater = process.stdin.isTTY && process.stdout.isTTY ? createUpdater({ current: _pkg.version }) : null;
|
|
851
|
+
const finishUpdate = (result) => {
|
|
852
|
+
updater?.dispose();
|
|
853
|
+
if (result?.updated) process.stdout.write(c.green(`已更新到 v${displayVersion(result.updated)},请重新运行 huicai。登录、历史与配置已保留。\n`));
|
|
854
|
+
};
|
|
849
855
|
// 偏好顺序:命令行 --tui/--plain > 配置 tui(rich/plain) > auto(TTY 下用 TUI)
|
|
850
856
|
const wantTui = !flags.plain && process.stdout.isTTY && process.stdin.isTTY && process.env.TERM !== 'dumb'
|
|
851
857
|
&& (flags.tui || config.tui !== 'plain');
|
|
@@ -859,12 +865,14 @@ async function main() {
|
|
|
859
865
|
// Only dependency loading may fall back. A runtime failure must never replay
|
|
860
866
|
// the initial prompt after it may already have edited files or run commands.
|
|
861
867
|
if (runInk) {
|
|
862
|
-
const ran = await runInk({ config, cwd, session, yolo: flags.yolo, initialPrompt: prompt || null, version: VERSION });
|
|
868
|
+
const ran = await runInk({ config, cwd, session, yolo: flags.yolo, initialPrompt: prompt || null, version: VERSION, updater });
|
|
869
|
+
finishUpdate(ran);
|
|
863
870
|
await reloginIfNeeded(ran, flags);
|
|
864
871
|
return;
|
|
865
872
|
}
|
|
866
873
|
}
|
|
867
|
-
const r = await startRepl({ config, cwd, yolo: flags.yolo, initialPrompt: prompt || null, session, version: VERSION });
|
|
874
|
+
const r = await startRepl({ config, cwd, yolo: flags.yolo, initialPrompt: prompt || null, session, version: VERSION, updater });
|
|
875
|
+
finishUpdate(r);
|
|
868
876
|
await reloginIfNeeded(r, flags);
|
|
869
877
|
}
|
|
870
878
|
|
package/bin/update.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// CLI 更新只由入口启用;IDE 内嵌核心不会自行安装全局软件。
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
const exec = promisify(execFile);
|
|
8
|
+
const PACKAGE = '@huicai/cli';
|
|
9
|
+
const stable = (v) => typeof v === 'string' && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(v);
|
|
10
|
+
export function newer(a, b) {
|
|
11
|
+
if (!stable(a) || !stable(b)) return false;
|
|
12
|
+
const aa = a.split('.').map(BigInt), bb = b.split('.').map(BigInt);
|
|
13
|
+
for (let i = 0; i < 3; i++) if (aa[i] !== bb[i]) return aa[i] > bb[i];
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
export function displayVersion(v) {
|
|
17
|
+
const [year, md, patch] = v.split('.').map(Number);
|
|
18
|
+
const month = Math.floor(md / 100), day = md % 100;
|
|
19
|
+
return year >= 2020 && month >= 1 && month <= 12 && day >= 1 && day <= 31
|
|
20
|
+
? `${year}.${String(month).padStart(2, '0')}.${String(day).padStart(2, '0')}${patch ? '.' + patch : ''}` : v;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 直接执行 npm 的 JS 入口,兼容 Windows,避免 cmd/shell 插值及命令注入。
|
|
24
|
+
export function npmExecutable(env = process.env) {
|
|
25
|
+
const dirs = [path.dirname(process.execPath), ...(env.PATH || '').split(path.delimiter)];
|
|
26
|
+
for (const dir of dirs) {
|
|
27
|
+
for (const file of ['npm', 'node_modules/npm/bin/npm-cli.js', '../lib/node_modules/npm/bin/npm-cli.js']) {
|
|
28
|
+
try {
|
|
29
|
+
const resolved = fs.realpathSync(path.resolve(dir, file));
|
|
30
|
+
if (resolved.endsWith('npm-cli.js')) return resolved;
|
|
31
|
+
} catch { /* 继续查找同一 Node 安装或 PATH 中的 npm */ }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
throw new Error('未找到 npm,请安装 Node.js 附带的 npm 后重试。');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createUpdater({ current, packageRoot = fileURLToPath(new URL('..', import.meta.url)), env = process.env, run = exec } = {}) {
|
|
38
|
+
const controller = new AbortController();
|
|
39
|
+
let installing;
|
|
40
|
+
let installAbort;
|
|
41
|
+
const npm = async (args, timeout = 10000, signal = controller.signal) => {
|
|
42
|
+
const result = await run(process.execPath, [npmExecutable(env), ...args], {
|
|
43
|
+
env: { ...env, HUICAI_UPDATING: '1', npm_config_update_notifier: 'false' },
|
|
44
|
+
cwd: path.dirname(process.execPath), timeout, maxBuffer: 1024 * 1024,
|
|
45
|
+
signal, windowsHide: true,
|
|
46
|
+
});
|
|
47
|
+
return result.stdout.trim();
|
|
48
|
+
};
|
|
49
|
+
const check = npm(['view', PACKAGE + '@latest', 'version', '--json', '--fetch-retries=0', '--fetch-timeout=6000'])
|
|
50
|
+
.then((raw) => JSON.parse(raw)).then((version) => newer(version, current) ? version : null)
|
|
51
|
+
.catch(() => null); // 后台检测断网、未发布或损坏响应时静默,不影响启动。
|
|
52
|
+
return {
|
|
53
|
+
check,
|
|
54
|
+
dispose: () => { controller.abort(); installAbort?.abort(); },
|
|
55
|
+
menu: (version) => ({ title: `HuiCai AI 有新版本 v${displayVersion(version)}`, current: 'later', searchable: false,
|
|
56
|
+
options: [{ value: 'update', label: '更新到最新版本', description: '自动安装;保留登录、历史与配置,完成后重新运行 huicai' },
|
|
57
|
+
{ value: 'later', label: '稍后', description: '继续当前会话,下次启动时再提醒' }] }),
|
|
58
|
+
install(version, { signal } = {}) {
|
|
59
|
+
if (installing) return installing;
|
|
60
|
+
installAbort = new AbortController();
|
|
61
|
+
const abort = () => installAbort?.abort();
|
|
62
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
63
|
+
if (signal?.aborted || controller.signal.aborted) installAbort.abort();
|
|
64
|
+
const installSignal = installAbort.signal;
|
|
65
|
+
installing = (async () => {
|
|
66
|
+
if (!newer(version, current)) throw new Error('无有效的新版本,已停止安装。');
|
|
67
|
+
// npm 7+ 不执行卸载钩子;旧 npm 可能在替换包时误删 ~/.huicai。
|
|
68
|
+
const npmVersion = await npm(['--version'], 10000, installSignal);
|
|
69
|
+
if (!/^\d+\./.test(npmVersion) || Number(npmVersion.split('.')[0]) < 7) throw new Error('自动更新需要 npm 7 或更新版本,请先升级 npm。');
|
|
70
|
+
let prefix;
|
|
71
|
+
const root = fs.realpathSync(packageRoot);
|
|
72
|
+
const suffix = path.join('lib', 'node_modules', '@huicai', 'cli');
|
|
73
|
+
const windowsSuffix = path.join('node_modules', '@huicai', 'cli');
|
|
74
|
+
// 已全局安装时固定原安装位置,避免 PATH 上另一个 npm 更新到错误目录。
|
|
75
|
+
if (root.endsWith(path.sep + suffix)) prefix = root.slice(0, -suffix.length);
|
|
76
|
+
else if (process.platform === 'win32' && root.endsWith(path.sep + windowsSuffix)) prefix = root.slice(0, -windowsSuffix.length);
|
|
77
|
+
else prefix = await npm(['prefix', '--global'], 10000, installSignal);
|
|
78
|
+
await npm(['install', '--global', '--prefix', prefix, PACKAGE + '@' + version,
|
|
79
|
+
'--no-audit', '--no-fund', '--fetch-retries=1', '--fetch-timeout=30000'], 300000, installSignal);
|
|
80
|
+
const installedRoot = path.join(prefix, ...(process.platform === 'win32' ? [] : ['lib']), 'node_modules', '@huicai', 'cli');
|
|
81
|
+
const installed = JSON.parse(fs.readFileSync(path.join(installedRoot, 'package.json'), 'utf8'));
|
|
82
|
+
if (installed.version !== version) throw new Error('安装后的版本校验未通过,请重试。');
|
|
83
|
+
// 用真实的新入口确认可启动,而非仅凭 npm 的退出码报告成功。
|
|
84
|
+
await run(process.execPath, [path.join(installedRoot, 'bin', 'huicai.js'), '--version'], {
|
|
85
|
+
env, timeout: 15000, maxBuffer: 65536, windowsHide: true, signal: installSignal,
|
|
86
|
+
});
|
|
87
|
+
return version;
|
|
88
|
+
})().catch((error) => {
|
|
89
|
+
installing = null;
|
|
90
|
+
if (installSignal.aborted) throw new Error('更新已中断,可输入 /update 重试。');
|
|
91
|
+
const denied = /EACCES|EPERM/.test(String(error.stderr || error.message));
|
|
92
|
+
throw new Error(denied ? '安装目录没有写入权限,请使用可写的 npm 全局目录后重试。' : '更新未完成,请检查网络或 npm 配置后重试。' + (error.message?.startsWith('Command failed') ? '' : ` ${error.message}`));
|
|
93
|
+
}).finally(() => { signal?.removeEventListener('abort', abort); installAbort = null; });
|
|
94
|
+
return installing;
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
package/package.json
CHANGED
|
@@ -22,6 +22,8 @@ eng-ai 与 eng-develop 共同的孙链:用 AI 加速**工程本身**。eng-devel
|
|
|
22
22
|
|
|
23
23
|
## 上下文工程(垃圾进垃圾出的解法)
|
|
24
24
|
|
|
25
|
+
实际执行编码任务时,先载入 **eng-develop** 的工程分析与验证流程:先通读上下文并用 engineering_plan 登记,再修改,再用 engineering_verify 留下单测/启动/冒烟/功能/客户端分项运行证据。工具不可用时手工执行同等流程并记录限制;不得把工具名的出现当成验证通过。
|
|
26
|
+
|
|
25
27
|
- **项目说明文件**:把团队惯例沉淀成 AI 可读文档(架构导读/命名与分层规范/常用命令/禁区清单)放仓库固定位置——**这是 eng-docs 的 AI 化投资,一次编写全员受益**;新人文档与 AI 说明高度重合,一起维护;
|
|
26
28
|
- 示例引导:给一个本仓库的好例子("参照 OrderService 的写法实现 RefundService")胜过十句抽象描述——**仓库里最好的代码就是最好的提示**;
|
|
27
29
|
- 仓库导航:让 AI 先读相关文件再动手(接口定义/被调用方/相邻实现),禁"凭空写一个"(与 explain-codebase 摸落点同构);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: eng-develop
|
|
3
3
|
description: 开发子链总控(领任务→分支→编码→自测→自评→提交 PR 全流程、语言无关编码规范通则、DoD 完成定义、AI 结对纪律;按技术栈路由 13 条孙链:通用工具链/Java/Python/C#/前端/Node/Go/Rust/C++/移动端/跨端/鸿蒙/PHP)。工程链开发环节入口:动手写码前、跨语言协作、不确定用哪条语言链时载入。
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# 开发子链总控
|
|
@@ -9,6 +9,14 @@ version: 1.0.0
|
|
|
9
9
|
engineering-chain 的开发环节子链,也是 12 条语言/工具孙链的路由入口。
|
|
10
10
|
输入:验收标准(eng-requirements)+方案与落点(eng-architecture);产出:可评审的 PR。
|
|
11
11
|
|
|
12
|
+
## 编码前分析与真实验证(四端统一)
|
|
13
|
+
|
|
14
|
+
任何代码正式编写前,必须通读相关源码、调用方和被调用方、项目规范/约定/文档、已有测试以及需求中的表格和图片;必要时先分析全库。搜索只用来定位,不能把片段当成完整上下文。用现有解析或视觉工具读取附件,缺失或读不懂的材料明确记录,禁止凭空假定。
|
|
15
|
+
|
|
16
|
+
先读 [工程分析与验证流程](references/engineering-workflow.md)。用 `engineering_plan` 登记已读材料、调用链、改动文件与分项验收命令,再修改;shell、脚本、自动修复和 MCP 写入也遵守此流程。无需另行请用户批准常规计划。
|
|
17
|
+
|
|
18
|
+
修改后必须运行单测、真实启动项目、冒烟及受影响接口/功能测试。涉及客户端(含共享核心改动)必须在真实宿主或设备模拟器执行交互测试;每项通过 `engineering_verify` 留下实际结果。构建、mock、DOM 单测、浏览器视口模拟均不能冒充原生客户端验收。缺环境时报告未验证及原因,不得宣称完成。
|
|
19
|
+
|
|
12
20
|
## 语言/工具孙链路由表
|
|
13
21
|
|
|
14
22
|
| 技术栈 | 孙链 | 一句定位 |
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
- [ ] 功能过全部验收标准(对着 story-templates 的 Gherkin 逐条勾)
|
|
6
6
|
- [ ] 新逻辑有单测且全绿;关键路径先写失败用例再实现
|
|
7
|
+
- [ ] 编码前已通读上下文/调用链/规范/文档/附件并登记分析与验收计划
|
|
8
|
+
- [ ] 真实启动、冒烟、涉及接口与功能逐项验证,结果对应最后一次改动
|
|
9
|
+
- [ ] 客户端在真实宿主或设备模拟器内完成交互验证,记录环境和证据;未验证项如实列出
|
|
7
10
|
- [ ] 静态检查零新增告警;格式化通过(工具管,评审不吵空格)
|
|
8
11
|
- [ ] 涉接口:契约文档已更新(api-design 同源)
|
|
9
12
|
- [ ] 涉配置/部署:变更说明已写(部署说明进 PR 描述)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# 从上下文到可运行交付
|
|
2
|
+
|
|
3
|
+
## 编码前
|
|
4
|
+
|
|
5
|
+
1. 读取适用的 AGENTS、README、贡献指南、规范、架构文档、依赖清单与启动说明;查看工作区已有改动,不覆盖用户未提交工作。
|
|
6
|
+
2. 用 glob/grep/search_code 建立目录与符号地图;沿入口、调用方、实现、数据模型、外部依赖、异常处理和测试追踪调用链。LSP 可用时查定义与引用,索引缺失明确降级为全文搜索。
|
|
7
|
+
3. 通读将改动的文件及必要上下文,长文件分段读全;接口变动要读所有消费者。表格检查多个 sheet、口径和约束;图片查看设计、标注和状态,不只读文件名。跨模块或边界不明时扩大至全库分析。
|
|
8
|
+
4. 记录需求、验收断言、规范、风险与替代方案;先复现 bug 或运行基线测试。新项目先核实目录、需求素材与运行环境。
|
|
9
|
+
5. `engineering_plan(action="plan")` 提交 summary/callChain/conventions/ux/references/targets/client/checks。已有目标文件必须实际读取;新增目标也登记。扩大改动范围时补读并更新计划,不能让计划成为事后补填的表格。
|
|
10
|
+
|
|
11
|
+
## 选择工具
|
|
12
|
+
|
|
13
|
+
优先复用项目已有 lint、类型检查、格式化、构建、测试与 CI。代码体检用 code_inspect;测试设计用 test_plan;单测用 run_tests。只在实际任务需要且环境支持时使用覆盖率、契约测试、属性测试、变异测试、依赖审计和性能剖析,不为工具数量堆依赖。
|
|
14
|
+
|
|
15
|
+
每个 check 有唯一 id、kind 和明确 expectation;unit 指定 path/framework/filter,其余指定有实际断言且失败非零退出的 command。工程验证在原有权限管线内运行,不能借验证跳过审批或访问生产数据。
|
|
16
|
+
|
|
17
|
+
## 验证必须分项
|
|
18
|
+
|
|
19
|
+
| kind | 要证明的事实 |
|
|
20
|
+
|---|---|
|
|
21
|
+
| unit | 新逻辑及回归的实际断言运行通过;0 用例、全跳过或非零退出不算通过 |
|
|
22
|
+
| startup | 本次代码对应进程成功启动且就绪;构建成功、打印版本、起了一个 PID 都不够 |
|
|
23
|
+
| smoke | 真实启动后的核心路径、依赖连通、退出与重启基本可用 |
|
|
24
|
+
| behavior | 变更涉及接口/功能的正常、错误、边界、取消/超时/重试等适用分支,断言响应内容和实际副作用 |
|
|
25
|
+
| client | 真实 Electron/VSCode Extension Host/IDEA JCEF 或 Android/iOS 模拟器内打开、操作并断言;记录宿主/设备、场景和截图或日志 |
|
|
26
|
+
|
|
27
|
+
一个类别可有多项检查,四端公共代码的 client 检查应覆盖四个宿主。CLI 用真实子进程/PTY 交互。浏览器可覆盖网页响应式和交互,但不是 WebKit/iOS 或原生插件运行证据。mock 用于确定性协议回归,应与真实模型/服务联调结果分开标注。
|
|
28
|
+
|
|
29
|
+
按 id 调 `engineering_verify`。失败先定位修复,再重跑受影响验证。工具保存实际命令、时间、输出和状态;计划材料或目标文件变化会使旧验证失效。测试数据、端口、账号配置隔离,只清理自己启动的进程,不关闭用户既有应用。
|
|
30
|
+
|
|
31
|
+
台账验证的是执行事实,不自动判断命令是否覆盖了全部需求。必须人工或模型复核断言质量,禁止用 echo、true、--help、构建脚本充当功能验证。任意 shell/MCP 在计划外写入仍需主动补充 targets 和验证;不能把工具门禁当成完备安全沙箱。
|
|
32
|
+
|
|
33
|
+
## 提前做体验设计
|
|
34
|
+
|
|
35
|
+
编码前检查加载/空态/失败/恢复、长任务进度、取消与重试、重复提交、草稿保留、键盘焦点、可访问性、窄屏/高 DPI/浅深主题以及性能预算。选择与需求有关的改进,不擅自扩展产品范围。运行时用实际交互验证这些状态,面向用户的错误信息给出可执行下一步,避免暴露内部实现。
|
|
36
|
+
|
|
37
|
+
交付附上改动与调用链影响、实际通过项、失败/跳过/阻塞项、证据位置和剩余风险。缺凭据、设备或依赖属于“未验证”;不得用静态契约或替身截图写成“模拟真机通过”。
|
package/src/agent.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"node:fs";import t from"node:os";import n from"node:path";import{streamChat as o,getPlatformConfig as s}from"./api.js";import{classifyError as r}from"./errkind.js";import{MarkupFilter as i,stripMarkup as a}from"./markup-filter.js";import{TOOL_SPECS as l,READONLY_TOOLS as c}from"./tools.js";import{toolLabel as u,indent as d,renderFileDiff as m}from"./render.js";import{compactIfNeeded as f,buildModelMessages as g,contextGauge as p}from"./compaction.js";import{catalogTokenBudget as h,noteMemberLimitFromError as b}from"./context-window.js";import{syncSkillCatalog as y,catalogDigestOf as w}from"./skill-catalog.js";import{makeTodoTool as $,formatTodos as S,TODO_TOOL_NAME as x}from"./todo.js";import{makeDecisionTool as k,DECISION_TOOL_NAME as _,recordDecision as M,extractDecisionFromTurn as j}from"./decisions.js";import{makeMemoryTool as I}from"./memory.js";import{loadHooks as T,runHooks as E}from"./hooks.js";import{capToolResult as C,capOrSpill as v,MAX_TOOL_RESULT_CHARS as A}from"./spill.js";import{runToolCall as O,isBinaryFile as R}from"./toolpipe.js";import{inspectForTurn as P,isCodeFile as N}from"./code-tools.js";import{inspectWebForTurn as U,isWebFile as J,turnCorrection as D}from"./web-inspect.js";import{inspectDocsForTurn as z,docTurnCorrection as K}from"./docx-tools.js";import{isFileWriteTool as F}from"./permissions.js";import{decideUpgrade as W}from"./routing.js";import{renderSdk as B,CODE_MODE_HIDDEN as q}from"./codemode.js";import*as L from"./ide.js";import{c as Q}from"./colors.js";export{C as capToolResult,v as capOrSpill,A as MAX_TOOL_RESULT_CHARS};export function toolConcurrency(e){const n=Number(e?.toolConcurrency);if(Number.isFinite(n)&&n>0)return Math.min(24,Math.floor(n));let o=4;try{o=t.cpus()?.length||4}catch{}return Math.max(8,Math.min(24,o-2))}const G=/context[_\s-]?length|maximum context|context window|too many tokens|reduce the length|prompt is too long|上下文.*(超|过长|超出|溢出)|输入.*过长|token.*(超出|超过).*(上限|限制)/i;export function isContextOverflowError(e){return G.test(String(e||""))}const H=32768,V=new Map;function X(e,t){e&&(V.delete(e),V.set(e,t),V.size>200&&V.delete(V.keys().next().value))}function Y(e,t){let n="";try{n=JSON.stringify(t??{},(e,t)=>t&&"object"==typeof t&&!Array.isArray(t)?Object.fromEntries(Object.keys(t).sort().map(e=>[e,t[e]])):t)}catch{n=String(t)}return e+"|"+n.slice(0,4e3)}const Z=/(现在|接下来|下面|这就|马上|我来|让我|我要|我先|我这就|咱们|让我们|稍等|待我)/,ee=/(做|进行|生成|编写|写(一|个|完|出)?|制作|计算|统计|核对|汇总|整理|分析|梳理|输出|列出|准备|运行|执行|开始)/,te=/\b(let me|now let me|i.?ll|i will|let.?s|next,? i)\b[^.!?\n]*\b(write|create|generat|calculat|comput|do|produc|compil|analyz|summar|prepar|run|make|list)\b/i;export function jsonErrorContext(e,t){const n=String(e||""),o=String(t?.message||"").match(/position (\d+)/);if(!o)return`:${n.slice(0,200)}…`;const s=Number(o[1]);return`,出错位置附近:…${n.slice(Math.max(0,s-60),s)+"⟦此处⟧"+n.slice(s,s+30)}…${/[^\\]"[^,:}\]\s]/.test(n.slice(Math.max(0,s-60),s+1))?'看起来是字符串里夹了没转义的英文双引号(中文里引述请用「」或“”,或写成 \\")。':""}`}export function truncatedArgs(e,t,n){if("length"===n)return!0;const o=String(t?.message||"");if(!/Unterminated string|Unexpected end of JSON|Expected .* after .* in JSON at position/.test(o))return!1;const s=o.match(/position (\d+)/),r=s?Number(s[1]):String(e).length;return String(e).length>40&&r>=String(e).length-2}function ne(e){const t=String(e||"").trim().split(/\n+/).map(e=>e.trim()).filter(Boolean),n=t[t.length-1]||"";return!(!n||n.length>120)&&(!!te.test(n)||Z.test(n)&&ee.test(n))}function oe(e){const t=String(e);return t.startsWith("工具执行失败")||t.startsWith("命令启动失败")||t.startsWith("【工具超时】")||t.includes("不是合法JSON")||t.includes("不是合法 JSON")||t.includes("未找到要替换的内容")||t.includes("路径超出项目目录")||t.includes("是目录而非文件")}function se(e){return"\n\n# 当前目标(用户设定)\n"+String(e).trim()+"\n在本回合结束、停止作答前,检查该目标是否已达成:若未达成,应继续推进(补充调研/改动/验证),不要过早停止;达成后用一句话说明已达成。"}const re="【用户中断】",ie="\n\n# 上一轮被用户中断(重要)\n上一回合被用户主动中断:会话里标注「"+re+"」的工具调用并未真正执行、没有任何结果。请勿臆测这些未完成工具的输出,也不要假设被中断的工作已完成或推断其结论;一切以用户最新一条消息为准——如与被中断的工作无关,就按新指令重新开始,需要时先与用户确认再继续。";function ae(e){if(!Array.isArray(e))return!1;let t=-1;for(let n=e.length-1;n>=0;n--)if("user"===e[n].role){t=n;break}if(t<=0)return!1;for(let n=t-1;n>=0;n--){const t=e[n];if("user"===t.role)break;if("tool"===t.role&&"string"==typeof t.content&&t.content.includes(re))return!0}return!1}function le(e){return"string"==typeof e?e:Array.isArray(e)?e.filter(e=>"text"===e?.type).map(e=>e.text||"").join("\n"):""}async function ce(e,t,n){const o=new Array(e.length);let s=0;return await Promise.all(Array.from({length:Math.max(1,Math.min(t,e.length))},async()=>{for(;s<e.length;){const t=s++;o[t]=await n(e[t],t)}})),o}export function isPlatformModel(e){return/^(agent|workflow|datahuman|squad)-/.test(String(e||""))}export function buildPlatformMessages(e){return e.filter(e=>("user"===e.role||"assistant"===e.role)&&!w(e)&&("string"==typeof e.content?e.content.trim().length>0:Array.isArray(e.content)&&e.content.length>0)).map(e=>({role:e.role,content:e.content}))}function ue(e,t=200){const n=String(e).split("\n")[0];return n.length>t?n.slice(0,t)+"…":n}export async function runTurn({config:t,messages:w,permissions:C,cwd:v,ask:A,signal:G,extraTools:Z=[],depth:ee=0,io:te,events:de,todoState:me,compaction:fe,pullQueued:ge,maxIters:pe,fileJournal:he}){const be=te&&te.write||(e=>process.stdout.write(e)),ye=de||null,we=e=>{ye?ye.onInfo?.(e):be(Q.gray(" · "+e)+"\n")};let $e=0,Se=0,xe=0,ke=null;const _e=Number(pe||t.maxTurns)||120,Me=isPlatformModel(t.model),je={low:24e4,medium:36e4,high:6e5}[t.reasoningEffort];let Ie=!1,Te=String(t.fallbackModel||"").trim(),Ee=0,Ce=t.model,ve=null;if(0===ee&&t.autoModelStrong&&!Me){const e=[...w].reverse().find(e=>"user"===e.role),n=le(e?.content);ve=W({text:n,planMode:t.planMode,attachmentCount:t.attachmentCount},{threshold:t.autoModelThreshold}),ve.upgrade&&t.autoModelStrong!==t.model&&(Ce=t.autoModelStrong),ye?.onRouteDecision?.({...ve,model:Ce,upgraded:Ce!==t.model}),t.debug&&we(`[automodel] ${ve.reason}`+(Ce!==t.model?` → 升配 ${Ce}`:" → 维持"))}const Ae=fe||{},Oe=[...Z];if(0===ee&&!Me&&!Oe.some(e=>e.spec?.function?.name===x)){const e=me||{list:[]};Oe.push($({state:e,emit:e=>{ye?ye.onTodo?ye.onTodo(e.map(e=>({...e}))):ye.onInfo?.("任务清单:\n"+S(e)):be(Q.gray(d(S(e)," "))+"\n")}}))}if(0!==ee||Me||!1===t.memoryEnabled||Oe.some(e=>"memory_write"===e.spec?.function?.name)||Oe.push(I({cwd:v,emit:e=>{ye?ye.onMemory?ye.onMemory(e):ye.onInfo?.("已记入长期记忆:"+ue(e,80)):be(Q.gray(" ✎ 已记入长期记忆:"+ue(e,80))+"\n")}})),0===ee&&!Me&&!1!==t.decisionsEnabled&&!Oe.some(e=>e.spec?.function?.name===_)){const e=w.filter(e=>"user"===e.role).length;Oe.push(k({cwd:v,sessionId:t.sessionId||"",turn:e,emit:e=>{it=!0,ye?ye.onDecision?ye.onDecision(e):ye.onInfo?.("已记录决策:"+ue(e.title,80)):be(Q.gray(" ◆ 已记录决策:"+ue(e.title,80))+"\n")}}))}const Re=new Map;let Pe=null,Ne=[],Ue=new Set,Je=0,De=null,ze=new Set,Ke=null;const Fe=e=>{if("function"!=typeof e?.enabled)return!1;try{return!1===e.enabled(t)}catch{return!1}},We=()=>{if(Array.isArray(Z)&&Z.length>Je){for(const e of Z.slice(Je)){const t=e?.spec?.function?.name;t&&!Re.has(t)&&Oe.push(e)}Je=Z.length,Pe=null}De!==t.toolMode&&(De=t.toolMode,Pe=null);const e=Me?[]:Oe.filter(Fe).map(e=>e.spec.function.name),n=e.join(",");if(Ke!==n&&(Ke=n,ze=new Set(e),Pe=null),null===Pe){Re.clear();for(const e of Oe)Re.set(e.spec.function.name,e);const e="both"===t.toolMode||"code"===t.toolMode;Ne=Me?[]:[...l,...Oe.filter(e=>!ze.has(e.spec.function.name)).map(e=>e.spec)].filter(t=>e||"run_program"!==t.function.name),Ue=new Set(Ne.map(e=>e.function.name));const n="code"===t.toolMode&&Ue.has("run_program");Pe=Me?void 0:n?Ne.filter(e=>!q.has(e.function.name)):Ne}};Je=Array.isArray(Z)?Z.length:0,We();let Be=0,qe=!1,Le=0,Qe="",Ge=0,He=0,Ve=null;const Xe=new Set;let Ye=0;const Ze=new Set;let et=0;const tt=new Set;let nt=0,ot=0,st=0,rt=!1,it=!1;const at=new Set;let lt="";const ct=t.maxTokens||8192;let ut=Math.max(ct,V.get(t.sessionId)||0),dt=0,mt=!1,ft=!1;const gt=0!==ee||Me?null:T(v),pt=!(!gt||!gt.all.length);if(pt){const e=[];if(w.filter(e=>"user"===e.role).length<=1){const n=E("SessionStart",{cwd:v,sessionId:t.sessionId||"",model:t.model||""},gt);n.injectContext&&e.push(n.injectContext);for(const t of n.outputs)e.push(t.stdout)}const n=[...w].reverse().find(e=>"user"===e.role),o=E("UserPromptSubmit",{cwd:v,prompt:le(n?.content),sessionId:t.sessionId||"",model:t.model||"",turn:w.filter(e=>"user"===e.role).length},gt);if(o.block)return we("提交被UserPromptSubmit钩子阻断:"+o.reason),{error:"提交被钩子阻断:"+o.reason,usage:{prompt:$e,completion:Se},lastPromptTokens:xe,lastUsage:ke};o.injectContext&&e.push(o.injectContext);for(const t of o.outputs)e.push(t.stdout);e.length&&(Ve="【钩子提供的附加上下文】\n"+e.join("\n"))}for(let l=0;l<_e;l++){We();const d=l===_e-1;!d||Me||qe||Ve||(Ve="已接近本回合的工具调用轮数上限。请不要再调用任何工具,基于目前已获得的信息,直接用中文输出面向用户的完整最终答复。");const $=Me?0:await f(w,t,{state:Ae,lastPromptTokens:xe});$>0&&t.debug&&we(`[debug] 已压缩上下文(${$} 条较早消息已摘要)`);let S=!1,x=!1;const k=new i,_=e=>{e&&(S||!x||ye||be("\n"),S=!0,ye?ye.onDelta?.(e):be(e))};if(0===ee&&!Me&&!1!==t.skillsEnabled){const e=g(w,Ae),n=y(w,e,v,{allowInsert:!(Ae.upTo>0),maxTokens:h(t)});"none"!==n.action&&t.debug&&we(`[skills] 目录${{initial:"首投",replace:"已刷新",restore:"压缩后重投"}[n.action]}(${n.count} 条,指纹 ${n.digest})`),"none"!==n.action&&ye?.onSkillCatalog?.({...n})}const I=Me?buildPlatformMessages(w):g(w,Ae);let T=I;if(!Me&&I.length&&"system"===I[0]?.role){let e="";t.planMode&&(e+="\n\n# 计划模式(当前开启)\n你现在处于计划模式:只能用只读工具(read_file/glob/grep/list_files/fetch_url)调研,不得做任何改动——write_file/edit_file/run_bash等写或执行操作都会被拒绝,不要反复尝试。请先充分理解需求与现状,然后产出一份清晰、分步的实施方案:涉及哪些文件、每步改动要点、如何验证、风险点,用todo_write列出步骤,随后停下等待用户确认。用户关闭计划模式后你再执行。"),t.sandbox&&(e+="\n\n# 沙箱模式(当前开启)\n禁止执行shell命令(run_bash)、联网(fetch_url)与环境/应用自动化工具(install_*/setup_environment/open_*),调用会被拒绝,不要反复尝试。只做本地文件读写与分析。"),t.goal&&String(t.goal).trim()&&(e+=se(t.goal)),ae(w)&&(e+=ie),Ue.has("run_program")&&(e+="\n\n# 用程序编排工具(run_program)\n"+B(Ne)),e&&(T=[{...I[0],content:String(I[0].content||"")+e},...I.slice(1)])}const A=[];lt?A.push({role:"assistant",content:lt},{role:"user",content:"你上一条回复因达到单轮输出长度上限而被截断,用户尚未看到剩余部分。请从截断处直接继续输出剩余内容:无缝衔接上文(若截断处在句子、代码块或表格中间,从中断字符处接着写),不要重复任何已输出的内容,不要道歉或解释,不要重新开头或总结前文。"}):Ve&&A.push({role:"user",content:Ve});const W=Date.now();let q,Z=!1,te="";try{q=await o({baseUrl:t.baseUrl,apiKey:t.apiKey,model:Ce,messages:A.length?[...T,...A]:T,tools:qe||d?void 0:Pe,maxTokens:ut,extraBody:mt?{enable_thinking:!1}:void 0,reasoningEffort:t.reasoningEffort,stallTimeoutMs:je,client:t.client,sessionId:t.sessionId,aux:t.aux||void 0,continuation:!!lt,tailCount:A.length},{onContent:e=>{Z=!0,te+=e,_(k.feed(e))},onReasoning:e=>{!1!==t.showReasoning&&(lt||(ye?ye.onReasoning?.(e):(x||be(Q.gray("\n[思考] ")),be(Q.gray(e))),x=!0))},onReasoningEnd:()=>ye?.onReasoningEnd?.(),onRetry:(e,n,o,s)=>{s?.network?we(`${o.replace(/[::].*$/,"")},${Math.round(n/1e3)} 秒后自动重试(第 ${e}/${s.max} 次)`):t.debug&&we(`[retry] 模型响应异常,${Math.round(n/1e3)}s后自动重试(第 ${e} 次):${o}`)}},G)}catch(e){if("AbortError"===e.name)return ye?ye.onInfo?.("已中断"):be(Q.gray("\n[已中断]\n")),{aborted:!0,usage:{prompt:$e,completion:Se},lastPromptTokens:xe,lastUsage:ke};if(!e.nonRetryable&&!e.partial&&ut>ct&&!ft&&!G?.aborted){ft=!0,dt=2,ut=ct,V.delete(t.sessionId),t.debug&&we(`[debug] 抬高输出预算后请求失败(${e.message}),已撤回到 ${ct} 重试`);continue}if(!e.nonRetryable&&!e.partial&&!Me&&!G?.aborted&&isContextOverflowError(e.message)&&Ee<3){Ee++,b(t,e.message);const n=await f(w,t,{state:Ae,force:!0});if(n>0){Z&&te&&(lt+=te),t.debug&&we(`[overflow] 上下文超出模型窗口,已强制压缩 ${n} 条较早消息后重试`);continue}t.debug&&we("[overflow] 上下文超限但无可压缩内容,转兜底模型")}const n=r(e);if(!(n||e.nonRetryable||e.partial||e.network||Me||Ie||G?.aborted)){if(!Te)try{const e=await s(t.baseUrl,t.apiKey);Te=String(e&&e.fallback_model||"").trim()}catch{}if(Te&&Te!==Ce){Ie=!0,Ce=Te,Z&&te&&(lt+=te),t.debug&&we(`[fallback] 主模型失败(${e.message}),静默切兜底模型 ${Te}${Z?"(续写)":""}`);continue}}return ye?ye.onInfo?.("✗ 模型调用失败:"+e.message):be("\n"+Q.red("✗ 模型调用失败:")+e.message+"\n"),{error:e.message,errorKind:n,usage:{prompt:$e,completion:Se},lastPromptTokens:xe,lastUsage:ke}}_(k.flush()),q.usage&&($e+=q.usage.prompt_tokens||0,Se+=q.usage.completion_tokens||0,xe=q.usage.prompt_tokens||xe,ke=q.usage,0===ee&&ye&&ye.onUsage?.({lastPromptTokens:xe,prompt:$e,completion:Se,ctx:p({messages:w,compaction:Ae,config:t,lastPromptTokens:xe})})),t.debug&&we(`[debug] 轮${l+1} · ${t.model} · in ${q.usage?.prompt_tokens||0} / out ${q.usage?.completion_tokens||0} tok · ${Date.now()-W}ms · ${(q.toolCalls||[]).length} 工具调用`);const{text:le,sawMarkup:de}=a(q.content),me=de||k.sawMarkup;Ve=null;const fe=Me?[]:q.toolCalls;if(!fe.length){if(me&&!le.trim()&&!Me){if(Be<2){Be++,2===Be&&(qe=!0),Ve=1===Be?"你刚才的输出把工具调用写成了文本标记,工具并未被执行,该内容也不会展示给用户。请重新处理:若需调用工具,必须使用规范的函数调用(tool_calls)通道发起,绝不要以任何文本形式书写工具调用标记;若无需工具,请直接用中文输出面向用户的最终答复。":"请停止调用任何工具。基于目前已获得的全部信息,直接用中文输出面向用户的完整最终答复,不要输出任何工具调用或协议标记。",we(`已拦截模型的文本化工具调用标记(工具未被执行),纠正重试中(第 ${Be} 次)`);continue}we("模型持续输出无效的工具调用标记,已全部拦截;本轮无有效答复(可换个说法重试或更换模型)")}if(!Me&&"length"===q.finishReason&&le&&ot<3){ot++,lt+=le,t.debug&&we(`[debug] 输出达单轮长度上限,自动续写(第 ${ot} 段)`);continue}if(!Me&&"length"===q.finishReason&&!le.trim()&&!lt){if(dt<2&&ut<H){dt++,ut=Math.min(3*ut,H),X(t.sessionId,ut),t.debug&&we(`[debug] 思考吃光输出预算(正文为空),抬高单轮上限到 ${ut} 重试(第 ${dt} 次)`);continue}if(!mt){mt=!0,Ve="你上一条把全部输出预算用在了思考上,没有输出任何正文,用户什么都没看到。请立刻直接输出面向用户的最终答复(结论、表格、结果放最前面),思考尽量精简。",t.debug&&we("[debug] 抬高预算后仍无正文,改为要求先出正文(并best-effort关思考)");continue}}S&&!ye&&be("\n");const e=lt?lt+le:le;lt="",e&&w.push({role:"assistant",content:e});const o=!e.trim();if(rt&&!o&&ne(e)&&"length"!==q.finishReason&&!qe&&st<2){st++,t.debug&&we(`[debug] 检测到「宣告未落地」预告式收尾,注入续跑纠偏(第 ${st} 次)`),w.push({role:"user",content:'你上一条只是说明"接下来要做什么",但既没有发起工具调用、也没有给出结果——用户还没拿到任何最终产物。请立即执行你刚才所说的步骤(需要就用run_python/工具,不需要就直接算),并直接输出面向用户的完整最终结果(例如完整的按月汇总表与逐月核对表、差异结论),不要再只描述你打算做什么。'});continue}if(o&&!Me&&"length"!==q.finishReason&&!qe&&st<2){st++,t.debug&&we(`[debug] 本回合正文为空,注入续跑纠偏(第 ${st} 次)`),Ve="你上一条回复没有输出任何面向用户的正文(内容可能全部留在了思考里),用户一个字都没拿到。请立刻用中文直接输出完整的最终答复,不要只在思考里推演。";continue}if(!Me&&0===ee&&!1!==t.codeSelfCheck&&Xe.size&&Ye<1&&!qe&&!o&&"length"!==q.finishReason){Ye++;const e=await P(v,[...Xe],t);if(e&&e.issues.length){const t=e.issues.slice(0,12),o=t.map(e=>{const t=n.relative(v,e.path)||e.path;return` [${e.rule}] ${t}:${e.line}${e.unit?` 〔${e.unit}〕`:""}\n 问题:${e.msg}\n 改法:${e.hint||"按上面的说明处理"}`}).join("\n"),s=e.issues.length>t.length?`\n …… 另有 ${e.issues.length-t.length} 条,用code_inspect看全量。`:"";we(`代码自检:本回合改动的 ${Xe.size} 个源码文件有 ${e.issues.length} 处必修问题,已要求修复`),Ve="【代码自检】你本回合改动的源码经体检发现以下**必修**问题,请逐条改完再结束:\n"+o+s+"\n\n改完用code_inspect(path) 复检确认必修清零。\n若确认某一条是误报,在那一行上方加 `// code-inspect-ignore: 规则名 简短理由`,并在回复里说明为什么判定它是误报——不要默默跳过,也不要在没修完时就宣布完成。";continue}}if(!Me&&0===ee&&!1!==t.webSelfCheck&&Ze.size&&et<1&&!qe&&!o&&"length"!==q.finishReason){et++;const e=await U(v,[...Ze],t).catch(()=>null);if(e&&e.errors.length){we(`网页自检:本回合改动的页面(${e.entries.join("、")})在浏览器里有 ${e.errors.length} 处必修问题,已要求修复`),Ve=D(e);continue}}if(!Me&&0===ee&&!1!==t.docSelfCheck&&tt.size&&nt<1&&!qe&&!o&&"length"!==q.finishReason){nt++;const e=await z(v,[...tt],t).catch(()=>null);if(e&&e.issues.length){we(`文档自检:本回合生成的 ${tt.size} 份文档有 ${e.issues.length} 处必修问题,已要求修复`),Ve=K(e,v);continue}}if(o&&!me){const e=dt>0||mt?",自动抬高上限并重试后仍未产出":"";we("length"===q.finishReason?`模型把本回合的输出预算(单轮上限 ${ut})全用在了思考上,没有产出正文${e}。建议把任务拆小一点分步做,或换个模型再试。`:`模型本回合没有返回任何正文${st>0?"(已自动重试)":""}。可以直接重发,或换个说法/换个模型再试。`)}ye&&ye.onTurnEnd?.(e);const s=ge?.();if(s){w.push({role:"user",content:s});continue}if(pt){const n=E("Stop",{cwd:v,result:e,sessionId:t.sessionId||"",model:t.model||"",turn:w.filter(e=>"user"===e.role).length},gt);for(const e of n.outputs)we("钩子·Stop:"+ue(e.stdout,160))}if(!Me&&t.decisionAutoExtract&&!1!==t.decisionsEnabled&&rt&&!it&&e&&e.trim())try{const n=[...w].reverse().find(e=>"user"===e.role),o=n&&"string"==typeof n.content?n.content:"",s=await j(t,{userText:o,assistantText:e,toolNames:[...at]}),r="number"==typeof t.decisionExtractMinConfidence?t.decisionExtractMinConfidence:.55;if(s&&(s.confidence??0)>=r){const e=w.filter(e=>"user"===e.role).length,n=M(v,{...s,title:s.what,status:"draft",origin:"agent",confidence:Math.min(s.confidence??.5,.7),sessionId:t.sessionId||"",turn:e},{origin:"agent"});ye?ye.onDecision?ye.onDecision(n):ye.onInfo?.("已抽取决策草稿(待确认):"+ue(n.title,80)):be(Q.gray(" ◇ 已抽取决策草稿(待确认):"+ue(n.title,80))+"\n")}}catch{}return{usage:{prompt:$e,completion:Se},lastPromptTokens:xe,lastUsage:ke}}rt=!0,S&&!ye&&be("\n"),ye&&le&&ye.onAssistantText?.(le),w.push({role:"assistant",content:lt?lt+le:le,tool_calls:fe,...q.nativeState?{huicai_anthropic:q.nativeState}:{}}),lt="";const pe=fe.map((e,n)=>{const o=e.function?.name||"";let s={},r=null,i=!1;const a=e.function?.arguments||"";if(a)try{s=JSON.parse(a)}catch(e){i=truncatedArgs(a,e,q.finishReason),r=i?`工具参数在第 ${String(a).length} 个字符处被截断:这次回复的输出触到了单次上限(约 ${ut} token),参数没写完——不是字符或转义的问题,原样重试还会在同一处断。常见成因是一段内容太长,或一条回复里并行发了好几个大参数的调用(上限是整条回复共用的)。请拆小:大参数的调用一次少发几个;write_file每段约 3000 字以内,后续段落用append=true追加;很长的JSON(测算假设、图表spec)先write_file落盘,再用对应的 *_path / spec_path参数传入。`:`工具参数不是合法JSON(${e.message})${jsonErrorContext(a,e)}请修正参数后重新调用。`}const l=Re.get(o);if(l&&ze.has(o)&&!r){const e="function"==typeof l.disabledMessage?l.disabledMessage(t):l.disabledMessage;r=String(e||`工具 ${o} 当前已停用(开关已关闭),未执行。`)}const u=L.isActive()&&("write_file"===o||"edit_file"===o);return{tc:e,idx:n,name:o,args:s,argsError:r,argsTruncated:i,extra:l,readOnly:l?!!l.readOnly:c.has(o),deferToIde:u}});!Me&&pe.some(e=>e.argsTruncated)&&dt<2&&ut<H&&(dt++,ut=Math.min(3*ut,H),X(t.sessionId,ut),t.debug&&we(`[debug] 工具参数被单轮输出上限截断,抬高到 ${ut}`));const ve=e=>{for(let t=e;t<pe.length;t++)w.push({role:"tool",tool_call_id:pe[t].tc.id||`call_${t}`,content:re+" 该工具未执行,无任何结果,请勿臆测其输出。"});return ye?ye.onInfo?.("已中断"):be(Q.gray("\n[已中断]\n")),{aborted:!0,usage:{prompt:$e,completion:Se},lastPromptTokens:xe,lastUsage:ke}},Oe={cwd:v,config:t,signal:G,permissions:C,hooks:gt,hooksActive:pt,fileJournal:he,info:we,shortLine:ue,turn:w.filter(e=>"user"===e.role).length,onStart:e=>e.nested?we("程序内调用:"+u(e.name,e.args)):De(e),listTools:()=>Ne,resolveTool:e=>{const t=Re.get(e);return t&&ze.has(e)?null:t?{readOnly:!!t.readOnly,extra:t}:Ue.has(e)?{readOnly:c.has(e),extra:null}:null}},Je=e=>O({name:e.name,args:e.args,readOnly:e.readOnly,extra:e.extra,callId:e.tc?.id,deferToIde:e.deferToIde,argsError:e.argsError,item:e},Oe),De=e=>{at.add(e.name),ye?ye.onToolStart?.(e.name,e.args,{callId:e.callId||e.tc?.id}):be(Q.cyan(" ⏺ ")+u(e.name,e.args)+"\n")},Ke=(e,t)=>{const n=e.item||e;oe(t)&&Le++;const o=Y(e.name,e.args);o===Qe?Ge++:(Qe=o,Ge=1,He=0),ye?ye.onToolResult?.(e.name,String(t),{callId:n.tc?.id,failed:oe(t)}):be(Q.gray(" "+ue(t))+"\n"),w.push({role:"tool",tool_call_id:n.tc.id,content:String(t)})},Fe=(e,t)=>{const n=e.item||e;ye?ye.onSkip?.(e.name,e.args,{callId:n.tc?.id,reason:t}):be(Q.gray(` ⊘ ${u(e.name,e.args)} — 已跳过\n`)),w.push({role:"tool",tool_call_id:n.tc.id,content:t||"未授权,未执行。"})},ht=e=>"render_diagram"!==e,bt=t=>{try{return e.readFileSync(t,"utf8")}catch{return""}},yt=(e,t)=>{const o=n.resolve(v,String(e.args.path));if(R(o))return;const s=m(t,bt(o),{path:String(e.args.path)});s&&(N(o)&&Xe.add(o),J(o)&&Ze.add(o),ye?ye.onFileDiff?.(s):be(s+"\n"))};let wt=0;for(;wt<pe.length;){if(G?.aborted)return ve(wt);const o=pe[wt];if(o.readOnly&&!o.argsError){let e=wt;for(;e<pe.length&&pe[e].readOnly&&!pe[e].argsError;)e++;const n=pe.slice(wt,e),o=await ce(n,toolConcurrency(t),e=>Je(e));for(const e of o)null!=e.denied?Fe(e,e.denied):Ke(e,e.result);if(wt=e,G?.aborted)return ve(wt);continue}const s=await Je(o);if(G?.aborted&&null==s.denied&&!s.settled)return ve(wt);if(null==s.denied)if(s.settled)De(s),Ke(s,s.result),wt++;else{if(Ke(s,s.result),null!=s.meta.before&&ht(s.name)&&yt(s,s.meta.before),F(s.name)&&/\.docx$/i.test(String(s.args?.path||""))){const t=n.resolve(v,String(s.args.path));e.existsSync(t)&&tt.add(t)}wt++}else Fe(s,s.denied),wt++}Le>=3&&!qe&&!Ve&&(Ve="你刚才连续多次工具调用失败。请停下来重新审视:是不是文件路径/内容/前提假设错了?先用read_file / grep / list_files核实现状,再换一种更可靠的方法,绝不要重复同样会失败的调用。",Le=0,we("检测到连续工具失败,已提示智能体换一种更可靠的方法")),qe||Ve||(Ge>=5&&He<2?(Ve="停止重复调用。你已经用完全相同的参数调用同一工具达 5 次,这不会产生任何新信息,只是在空转。现在必须二选一:① 基于已有结果直接给出结论/继续下一步;② 明确说明你缺少什么、为什么现有结果不够,然后换一种**明显不同**的方法去拿。",He=2,we(`检测到同一调用连续重复 ${Ge} 次,已要求智能体停止空转`)):Ge>=3&&He<1&&(Ve="注意:你刚刚用**完全相同的参数**连续调用了同一个工具多次,结果不会因为再调一次而改变。请先仔细重读上一次的返回内容——答案很可能已经在里面;若确实需要更多信息,请换不同的参数或换一个工具,不要重复同一次调用。",He=1,we(`检测到同一调用连续重复 ${Ge} 次,已提示智能体重读上次结果`)));const $t=ge?.();$t&&w.push({role:"user",content:$t})}return lt&&(w.push({role:"assistant",content:lt}),ye&&ye.onTurnEnd?.(lt)),ye?ye.onInfo?.("已达最大工具轮数,停止"):be(Q.yellow("\n[已达最大工具轮数,停止]\n")),{usage:{prompt:$e,completion:Se},lastPromptTokens:xe,lastUsage:ke}}
|
|
1
|
+
import e from"node:fs";import t from"node:os";import n from"node:path";import{streamChat as o,getPlatformConfig as s}from"./api.js";import{classifyError as r}from"./errkind.js";import{MarkupFilter as i,stripMarkup as a}from"./markup-filter.js";import{TOOL_SPECS as l,READONLY_TOOLS as c}from"./tools.js";import{toolLabel as u,indent as d,renderFileDiff as m}from"./render.js";import{compactIfNeeded as f,buildModelMessages as g,contextGauge as p}from"./compaction.js";import{catalogTokenBudget as h,noteMemberLimitFromError as b}from"./context-window.js";import{syncSkillCatalog as y,catalogDigestOf as w}from"./skill-catalog.js";import{makeTodoTool as $,formatTodos as S,TODO_TOOL_NAME as x}from"./todo.js";import{makeDecisionTool as k,DECISION_TOOL_NAME as _,recordDecision as M,extractDecisionFromTurn as j}from"./decisions.js";import{makeMemoryTool as I}from"./memory.js";import{loadHooks as T,runHooks as E}from"./hooks.js";import{capToolResult as C,capOrSpill as v,MAX_TOOL_RESULT_CHARS as A}from"./spill.js";import{runToolCall as O,isBinaryFile as R}from"./toolpipe.js";import{inspectForTurn as P,isCodeFile as N}from"./code-tools.js";import{EngineeringRun as U}from"./engineering.js";import{inspectWebForTurn as J,isWebFile as D,turnCorrection as z}from"./web-inspect.js";import{inspectDocsForTurn as K,docTurnCorrection as F}from"./docx-tools.js";import{isFileWriteTool as W}from"./permissions.js";import{decideUpgrade as B}from"./routing.js";import{renderSdk as q,CODE_MODE_HIDDEN as L}from"./codemode.js";import*as Q from"./ide.js";import{c as G}from"./colors.js";export{C as capToolResult,v as capOrSpill,A as MAX_TOOL_RESULT_CHARS};export function toolConcurrency(e){const n=Number(e?.toolConcurrency);if(Number.isFinite(n)&&n>0)return Math.min(24,Math.floor(n));let o=4;try{o=t.cpus()?.length||4}catch{}return Math.max(8,Math.min(24,o-2))}const H=/context[_\s-]?length|maximum context|context window|too many tokens|reduce the length|prompt is too long|上下文.*(超|过长|超出|溢出)|输入.*过长|token.*(超出|超过).*(上限|限制)/i;export function isContextOverflowError(e){return H.test(String(e||""))}const V=32768,X=new Map;function Y(e,t){e&&(X.delete(e),X.set(e,t),X.size>200&&X.delete(X.keys().next().value))}function Z(e,t){let n="";try{n=JSON.stringify(t??{},(e,t)=>t&&"object"==typeof t&&!Array.isArray(t)?Object.fromEntries(Object.keys(t).sort().map(e=>[e,t[e]])):t)}catch{n=String(t)}return e+"|"+n.slice(0,4e3)}const ee=/(现在|接下来|下面|这就|马上|我来|让我|我要|我先|我这就|咱们|让我们|稍等|待我)/,te=/(做|进行|生成|编写|写(一|个|完|出)?|制作|计算|统计|核对|汇总|整理|分析|梳理|输出|列出|准备|运行|执行|开始)/,ne=/\b(let me|now let me|i.?ll|i will|let.?s|next,? i)\b[^.!?\n]*\b(write|create|generat|calculat|comput|do|produc|compil|analyz|summar|prepar|run|make|list)\b/i;export function jsonErrorContext(e,t){const n=String(e||""),o=String(t?.message||"").match(/position (\d+)/);if(!o)return`:${n.slice(0,200)}…`;const s=Number(o[1]);return`,出错位置附近:…${n.slice(Math.max(0,s-60),s)+"⟦此处⟧"+n.slice(s,s+30)}…${/[^\\]"[^,:}\]\s]/.test(n.slice(Math.max(0,s-60),s+1))?'看起来是字符串里夹了没转义的英文双引号(中文里引述请用「」或“”,或写成 \\")。':""}`}export function truncatedArgs(e,t,n){if("length"===n)return!0;const o=String(t?.message||"");if(!/Unterminated string|Unexpected end of JSON|Expected .* after .* in JSON at position/.test(o))return!1;const s=o.match(/position (\d+)/),r=s?Number(s[1]):String(e).length;return String(e).length>40&&r>=String(e).length-2}function oe(e){const t=String(e||"").trim().split(/\n+/).map(e=>e.trim()).filter(Boolean),n=t[t.length-1]||"";return!(!n||n.length>120)&&(!!ne.test(n)||ee.test(n)&&te.test(n))}function se(e){const t=String(e);return t.startsWith("工具执行失败")||t.startsWith("命令启动失败")||t.startsWith("【工具超时】")||t.includes("不是合法JSON")||t.includes("不是合法 JSON")||t.includes("未找到要替换的内容")||t.includes("路径超出项目目录")||t.includes("是目录而非文件")}function re(e){return"\n\n# 当前目标(用户设定)\n"+String(e).trim()+"\n在本回合结束、停止作答前,检查该目标是否已达成:若未达成,应继续推进(补充调研/改动/验证),不要过早停止;达成后用一句话说明已达成。"}const ie="【用户中断】",ae="\n\n# 上一轮被用户中断(重要)\n上一回合被用户主动中断:会话里标注「"+ie+"」的工具调用并未真正执行、没有任何结果。请勿臆测这些未完成工具的输出,也不要假设被中断的工作已完成或推断其结论;一切以用户最新一条消息为准——如与被中断的工作无关,就按新指令重新开始,需要时先与用户确认再继续。";function le(e){if(!Array.isArray(e))return!1;let t=-1;for(let n=e.length-1;n>=0;n--)if("user"===e[n].role){t=n;break}if(t<=0)return!1;for(let n=t-1;n>=0;n--){const t=e[n];if("user"===t.role)break;if("tool"===t.role&&"string"==typeof t.content&&t.content.includes(ie))return!0}return!1}function ce(e){return"string"==typeof e?e:Array.isArray(e)?e.filter(e=>"text"===e?.type).map(e=>e.text||"").join("\n"):""}async function ue(e,t,n){const o=new Array(e.length);let s=0;return await Promise.all(Array.from({length:Math.max(1,Math.min(t,e.length))},async()=>{for(;s<e.length;){const t=s++;o[t]=await n(e[t],t)}})),o}export function isPlatformModel(e){return/^(agent|workflow|datahuman|squad)-/.test(String(e||""))}export function buildPlatformMessages(e){return e.filter(e=>("user"===e.role||"assistant"===e.role)&&!w(e)&&("string"==typeof e.content?e.content.trim().length>0:Array.isArray(e.content)&&e.content.length>0)).map(e=>({role:e.role,content:e.content}))}function de(e,t=200){const n=String(e).split("\n")[0];return n.length>t?n.slice(0,t)+"…":n}export async function runTurn({config:t,messages:w,permissions:C,cwd:v,ask:A,signal:H,extraTools:ee=[],depth:te=0,io:ne,events:me,todoState:fe,compaction:ge,pullQueued:pe,maxIters:he,fileJournal:be}){const ye=ne&&ne.write||(e=>process.stdout.write(e)),we=me||null,$e=e=>{we?we.onInfo?.(e):ye(G.gray(" · "+e)+"\n")};let Se=0,xe=0,ke=0,_e=null;const Me=Number(he||t.maxTurns)||120,je=isPlatformModel(t.model),Ie={low:24e4,medium:36e4,high:6e5}[t.reasoningEffort];let Te=!1,Ee=String(t.fallbackModel||"").trim(),Ce=0,ve=t.model,Ae=null;if(0===te&&t.autoModelStrong&&!je){const e=[...w].reverse().find(e=>"user"===e.role),n=ce(e?.content);Ae=B({text:n,planMode:t.planMode,attachmentCount:t.attachmentCount},{threshold:t.autoModelThreshold}),Ae.upgrade&&t.autoModelStrong!==t.model&&(ve=t.autoModelStrong),we?.onRouteDecision?.({...Ae,model:ve,upgraded:ve!==t.model}),t.debug&&$e(`[automodel] ${Ae.reason}`+(ve!==t.model?` → 升配 ${ve}`:" → 维持"))}const Oe=ge||{},Re=[...ee],Pe=!je&&Re.some(e=>"engineering_plan"===e.spec?.function?.name)?new U(v):null;let Ne=0;if(0===te&&!je&&!Re.some(e=>e.spec?.function?.name===x)){const e=fe||{list:[]};Re.push($({state:e,emit:e=>{we?we.onTodo?we.onTodo(e.map(e=>({...e}))):we.onInfo?.("任务清单:\n"+S(e)):ye(G.gray(d(S(e)," "))+"\n")}}))}if(0!==te||je||!1===t.memoryEnabled||Re.some(e=>"memory_write"===e.spec?.function?.name)||Re.push(I({cwd:v,emit:e=>{we?we.onMemory?we.onMemory(e):we.onInfo?.("已记入长期记忆:"+de(e,80)):ye(G.gray(" ✎ 已记入长期记忆:"+de(e,80))+"\n")}})),0===te&&!je&&!1!==t.decisionsEnabled&&!Re.some(e=>e.spec?.function?.name===_)){const e=w.filter(e=>"user"===e.role).length;Re.push(k({cwd:v,sessionId:t.sessionId||"",turn:e,emit:e=>{ct=!0,we?we.onDecision?we.onDecision(e):we.onInfo?.("已记录决策:"+de(e.title,80)):ye(G.gray(" ◆ 已记录决策:"+de(e.title,80))+"\n")}}))}const Ue=new Map;let Je=null,De=[],ze=new Set,Ke=0,Fe=null,We=new Set,Be=null;const qe=e=>{if("function"!=typeof e?.enabled)return!1;try{return!1===e.enabled(t)}catch{return!1}},Le=()=>{if(Array.isArray(ee)&&ee.length>Ke){for(const e of ee.slice(Ke)){const t=e?.spec?.function?.name;t&&!Ue.has(t)&&Re.push(e)}Ke=ee.length,Je=null}Fe!==t.toolMode&&(Fe=t.toolMode,Je=null);const e=je?[]:Re.filter(qe).map(e=>e.spec.function.name),n=e.join(",");if(Be!==n&&(Be=n,We=new Set(e),Je=null),null===Je){Ue.clear();for(const e of Re)Ue.set(e.spec.function.name,e);const e="both"===t.toolMode||"code"===t.toolMode;De=je?[]:[...l,...Re.filter(e=>!We.has(e.spec.function.name)).map(e=>e.spec)].filter(t=>e||"run_program"!==t.function.name),ze=new Set(De.map(e=>e.function.name));const n="code"===t.toolMode&&ze.has("run_program");Je=je?void 0:n?De.filter(e=>!L.has(e.function.name)):De}};Ke=Array.isArray(ee)?ee.length:0,Le();let Qe=0,Ge=!1,He=0,Ve="",Xe=0,Ye=0,Ze=null;const et=new Set;let tt=0;const nt=new Set;let ot=0;const st=new Set;let rt=0,it=0,at=0,lt=!1,ct=!1;const ut=new Set;let dt="";const mt=t.maxTokens||8192;let ft=Math.max(mt,X.get(t.sessionId)||0),gt=0,pt=!1,ht=!1;const bt=0!==te||je?null:T(v),yt=!(!bt||!bt.all.length);if(yt){const e=[];if(w.filter(e=>"user"===e.role).length<=1){const n=E("SessionStart",{cwd:v,sessionId:t.sessionId||"",model:t.model||""},bt);n.injectContext&&e.push(n.injectContext);for(const t of n.outputs)e.push(t.stdout)}const n=[...w].reverse().find(e=>"user"===e.role),o=E("UserPromptSubmit",{cwd:v,prompt:ce(n?.content),sessionId:t.sessionId||"",model:t.model||"",turn:w.filter(e=>"user"===e.role).length},bt);if(o.block)return $e("提交被UserPromptSubmit钩子阻断:"+o.reason),{error:"提交被钩子阻断:"+o.reason,usage:{prompt:Se,completion:xe},lastPromptTokens:ke,lastUsage:_e};o.injectContext&&e.push(o.injectContext);for(const t of o.outputs)e.push(t.stdout);e.length&&(Ze="【钩子提供的附加上下文】\n"+e.join("\n"))}for(let l=0;l<Me;l++){Le();const d=l===Me-1;!d||je||Ge||Ze||(Ze="已接近本回合的工具调用轮数上限。请不要再调用任何工具,基于目前已获得的信息,直接用中文输出面向用户的完整最终答复。");const $=je?0:await f(w,t,{state:Oe,lastPromptTokens:ke});$>0&&t.debug&&$e(`[debug] 已压缩上下文(${$} 条较早消息已摘要)`);let S=!1,x=!1;const k=new i,_=e=>{e&&(S||!x||we||ye("\n"),S=!0,we?we.onDelta?.(e):ye(e))};if(0===te&&!je&&!1!==t.skillsEnabled){const e=g(w,Oe),n=y(w,e,v,{allowInsert:!(Oe.upTo>0),maxTokens:h(t)});"none"!==n.action&&t.debug&&$e(`[skills] 目录${{initial:"首投",replace:"已刷新",restore:"压缩后重投"}[n.action]}(${n.count} 条,指纹 ${n.digest})`),"none"!==n.action&&we?.onSkillCatalog?.({...n})}const I=je?buildPlatformMessages(w):g(w,Oe);let T=I;if(!je&&I.length&&"system"===I[0]?.role){let e="";t.planMode&&(e+="\n\n# 计划模式(当前开启)\n你现在处于计划模式:只能用只读工具(read_file/glob/grep/list_files/fetch_url)调研,不得做任何改动——write_file/edit_file/run_bash等写或执行操作都会被拒绝,不要反复尝试。请先充分理解需求与现状,然后产出一份清晰、分步的实施方案:涉及哪些文件、每步改动要点、如何验证、风险点,用todo_write列出步骤,随后停下等待用户确认。用户关闭计划模式后你再执行。"),t.sandbox&&(e+="\n\n# 沙箱模式(当前开启)\n禁止执行shell命令(run_bash)、联网(fetch_url)与环境/应用自动化工具(install_*/setup_environment/open_*),调用会被拒绝,不要反复尝试。只做本地文件读写与分析。"),t.goal&&String(t.goal).trim()&&(e+=re(t.goal)),le(w)&&(e+=ae),ze.has("run_program")&&(e+="\n\n# 用程序编排工具(run_program)\n"+q(De)),e&&(T=[{...I[0],content:String(I[0].content||"")+e},...I.slice(1)])}const A=[];dt?A.push({role:"assistant",content:dt},{role:"user",content:"你上一条回复因达到单轮输出长度上限而被截断,用户尚未看到剩余部分。请从截断处直接继续输出剩余内容:无缝衔接上文(若截断处在句子、代码块或表格中间,从中断字符处接着写),不要重复任何已输出的内容,不要道歉或解释,不要重新开头或总结前文。"}):Ze&&A.push({role:"user",content:Ze});const U=Date.now();let B,L=!1,ee="";try{B=await o({baseUrl:t.baseUrl,apiKey:t.apiKey,model:ve,supportsImages:t.imageCapabilities?.[ve],messages:A.length?[...T,...A]:T,tools:Ge||d?void 0:Je,maxTokens:ft,extraBody:pt?{enable_thinking:!1}:void 0,reasoningEffort:t.reasoningEffort,stallTimeoutMs:Ie,client:t.client,sessionId:t.sessionId,aux:t.aux||void 0,continuation:!!dt,tailCount:A.length},{onContent:e=>{L=!0,ee+=e,_(k.feed(e))},onReasoning:e=>{!1!==t.showReasoning&&(dt||(we?we.onReasoning?.(e):(x||ye(G.gray("\n[思考] ")),ye(G.gray(e))),x=!0))},onReasoningEnd:()=>we?.onReasoningEnd?.(),onRetry:(e,n,o,s)=>{s?.network?$e(`${o.replace(/[::].*$/,"")},${Math.round(n/1e3)} 秒后自动重试(第 ${e}/${s.max} 次)`):t.debug&&$e(`[retry] 模型响应异常,${Math.round(n/1e3)}s后自动重试(第 ${e} 次):${o}`)}},H)}catch(e){if("AbortError"===e.name)return we?we.onInfo?.("已中断"):ye(G.gray("\n[已中断]\n")),{aborted:!0,usage:{prompt:Se,completion:xe},lastPromptTokens:ke,lastUsage:_e};if(!e.nonRetryable&&!e.partial&&ft>mt&&!ht&&!H?.aborted){ht=!0,gt=2,ft=mt,X.delete(t.sessionId),t.debug&&$e(`[debug] 抬高输出预算后请求失败(${e.message}),已撤回到 ${mt} 重试`);continue}if(!e.nonRetryable&&!e.partial&&!je&&!H?.aborted&&isContextOverflowError(e.message)&&Ce<3){Ce++,b(t,e.message);const n=await f(w,t,{state:Oe,force:!0});if(n>0){L&&ee&&(dt+=ee),t.debug&&$e(`[overflow] 上下文超出模型窗口,已强制压缩 ${n} 条较早消息后重试`);continue}t.debug&&$e("[overflow] 上下文超限但无可压缩内容,转兜底模型")}const n=r(e);if(!(n||e.nonRetryable||e.partial||e.network||je||Te||H?.aborted)){if(!Ee)try{const e=await s(t.baseUrl,t.apiKey);Ee=String(e&&e.fallback_model||"").trim()}catch{}if(Ee&&Ee!==ve){Te=!0,ve=Ee,L&&ee&&(dt+=ee),t.debug&&$e(`[fallback] 主模型失败(${e.message}),静默切兜底模型 ${Ee}${L?"(续写)":""}`);continue}}return we?we.onInfo?.("✗ 模型调用失败:"+e.message):ye("\n"+G.red("✗ 模型调用失败:")+e.message+"\n"),{error:e.message,errorKind:n,usage:{prompt:Se,completion:xe},lastPromptTokens:ke,lastUsage:_e}}_(k.flush()),B.usage&&(Se+=B.usage.prompt_tokens||0,xe+=B.usage.completion_tokens||0,ke=B.usage.prompt_tokens||ke,_e=B.usage,0===te&&we&&we.onUsage?.({lastPromptTokens:ke,prompt:Se,completion:xe,ctx:p({messages:w,compaction:Oe,config:t,lastPromptTokens:ke})})),t.debug&&$e(`[debug] 轮${l+1} · ${t.model} · in ${B.usage?.prompt_tokens||0} / out ${B.usage?.completion_tokens||0} tok · ${Date.now()-U}ms · ${(B.toolCalls||[]).length} 工具调用`);const{text:ne,sawMarkup:ce}=a(B.content),me=ce||k.sawMarkup;Ze=null;const fe=je?[]:B.toolCalls;if(!fe.length){if(me&&!ne.trim()&&!je){if(Qe<2){Qe++,2===Qe&&(Ge=!0),Ze=1===Qe?"你刚才的输出把工具调用写成了文本标记,工具并未被执行,该内容也不会展示给用户。请重新处理:若需调用工具,必须使用规范的函数调用(tool_calls)通道发起,绝不要以任何文本形式书写工具调用标记;若无需工具,请直接用中文输出面向用户的最终答复。":"请停止调用任何工具。基于目前已获得的全部信息,直接用中文输出面向用户的完整最终答复,不要输出任何工具调用或协议标记。",$e(`已拦截模型的文本化工具调用标记(工具未被执行),纠正重试中(第 ${Qe} 次)`);continue}$e("模型持续输出无效的工具调用标记,已全部拦截;本轮无有效答复(可换个说法重试或更换模型)")}if(!je&&"length"===B.finishReason&&ne&&it<3){it++,dt+=ne,t.debug&&$e(`[debug] 输出达单轮长度上限,自动续写(第 ${it} 段)`);continue}if(!je&&"length"===B.finishReason&&!ne.trim()&&!dt){if(gt<2&&ft<V){gt++,ft=Math.min(3*ft,V),Y(t.sessionId,ft),t.debug&&$e(`[debug] 思考吃光输出预算(正文为空),抬高单轮上限到 ${ft} 重试(第 ${gt} 次)`);continue}if(!pt){pt=!0,Ze="你上一条把全部输出预算用在了思考上,没有输出任何正文,用户什么都没看到。请立刻直接输出面向用户的最终答复(结论、表格、结果放最前面),思考尽量精简。",t.debug&&$e("[debug] 抬高预算后仍无正文,改为要求先出正文(并best-effort关思考)");continue}}S&&!we&&ye("\n");let e=dt?dt+ne:ne;dt="",e&&w.push({role:"assistant",content:e});const o=!e.trim();if(lt&&!o&&oe(e)&&"length"!==B.finishReason&&!Ge&&at<2){at++,t.debug&&$e(`[debug] 检测到「宣告未落地」预告式收尾,注入续跑纠偏(第 ${at} 次)`),w.push({role:"user",content:'你上一条只是说明"接下来要做什么",但既没有发起工具调用、也没有给出结果——用户还没拿到任何最终产物。请立即执行你刚才所说的步骤(需要就用run_python/工具,不需要就直接算),并直接输出面向用户的完整最终结果(例如完整的按月汇总表与逐月核对表、差异结论),不要再只描述你打算做什么。'});continue}if(o&&!je&&"length"!==B.finishReason&&!Ge&&at<2){at++,t.debug&&$e(`[debug] 本回合正文为空,注入续跑纠偏(第 ${at} 次)`),Ze="你上一条回复没有输出任何面向用户的正文(内容可能全部留在了思考里),用户一个字都没拿到。请立刻用中文直接输出完整的最终答复,不要只在思考里推演。";continue}if(!je&&0===te&&!1!==t.codeSelfCheck&&et.size&&tt<1&&!Ge&&!o&&"length"!==B.finishReason){tt++;const e=await P(v,[...et],t);if(e&&e.issues.length){const t=e.issues.slice(0,12),o=t.map(e=>{const t=n.relative(v,e.path)||e.path;return` [${e.rule}] ${t}:${e.line}${e.unit?` 〔${e.unit}〕`:""}\n 问题:${e.msg}\n 改法:${e.hint||"按上面的说明处理"}`}).join("\n"),s=e.issues.length>t.length?`\n …… 另有 ${e.issues.length-t.length} 条,用code_inspect看全量。`:"";$e(`代码自检:本回合改动的 ${et.size} 个源码文件有 ${e.issues.length} 处必修问题,已要求修复`),Ze="【代码自检】你本回合改动的源码经体检发现以下**必修**问题,请逐条改完再结束:\n"+o+s+"\n\n改完用code_inspect(path) 复检确认必修清零。\n若确认某一条是误报,在那一行上方加 `// code-inspect-ignore: 规则名 简短理由`,并在回复里说明为什么判定它是误报——不要默默跳过,也不要在没修完时就宣布完成。";continue}}if(!je&&0===te&&!1!==t.webSelfCheck&&nt.size&&ot<1&&!Ge&&!o&&"length"!==B.finishReason){ot++;const e=await J(v,[...nt],t).catch(()=>null);if(e&&e.errors.length){$e(`网页自检:本回合改动的页面(${e.entries.join("、")})在浏览器里有 ${e.errors.length} 处必修问题,已要求修复`),Ze=z(e);continue}}if(!je&&0===te&&!1!==t.docSelfCheck&&st.size&&rt<1&&!Ge&&!o&&"length"!==B.finishReason){rt++;const e=await K(v,[...st],t).catch(()=>null);if(e&&e.issues.length){$e(`文档自检:本回合生成的 ${st.size} 份文档有 ${e.issues.length} 处必修问题,已要求修复`),Ze=F(e,v);continue}}if(o&&!me){const e=gt>0||pt?",自动抬高上限并重试后仍未产出":"";$e("length"===B.finishReason?`模型把本回合的输出预算(单轮上限 ${ft})全用在了思考上,没有产出正文${e}。建议把任务拆小一点分步做,或换个模型再试。`:`模型本回合没有返回任何正文${at>0?"(已自动重试)":""}。可以直接重发,或换个说法/换个模型再试。`)}const s=Pe?.pendingNote();if(s){if(!Ge&&Ne<2&&!H?.aborted){Ne++,Ze=s,$e(s);continue}e+="\n\n【未完成验证】"+s,"assistant"===w.at(-1)?.role?w.at(-1).content=e:w.push({role:"assistant",content:e}),$e("【未完成验证】"+s)}we&&we.onTurnEnd?.(e);const r=pe?.();if(r){w.push({role:"user",content:r});continue}if(yt){const n=E("Stop",{cwd:v,result:e,sessionId:t.sessionId||"",model:t.model||"",turn:w.filter(e=>"user"===e.role).length},bt);for(const e of n.outputs)$e("钩子·Stop:"+de(e.stdout,160))}if(!je&&t.decisionAutoExtract&&!1!==t.decisionsEnabled&<&&!ct&&e&&e.trim())try{const n=[...w].reverse().find(e=>"user"===e.role),o=n&&"string"==typeof n.content?n.content:"",s=await j(t,{userText:o,assistantText:e,toolNames:[...ut]}),r="number"==typeof t.decisionExtractMinConfidence?t.decisionExtractMinConfidence:.55;if(s&&(s.confidence??0)>=r){const e=w.filter(e=>"user"===e.role).length,n=M(v,{...s,title:s.what,status:"draft",origin:"agent",confidence:Math.min(s.confidence??.5,.7),sessionId:t.sessionId||"",turn:e},{origin:"agent"});we?we.onDecision?we.onDecision(n):we.onInfo?.("已抽取决策草稿(待确认):"+de(n.title,80)):ye(G.gray(" ◇ 已抽取决策草稿(待确认):"+de(n.title,80))+"\n")}}catch{}return{usage:{prompt:Se,completion:xe},lastPromptTokens:ke,lastUsage:_e}}lt=!0,S&&!we&&ye("\n"),we&&ne&&we.onAssistantText?.(ne),w.push({role:"assistant",content:dt?dt+ne:ne,tool_calls:fe,...B.nativeState?{huicai_anthropic:B.nativeState}:{}}),dt="";const ge=fe.map((e,n)=>{const o=e.function?.name||"";let s={},r=null,i=!1;const a=e.function?.arguments||"";if(a)try{s=JSON.parse(a)}catch(e){i=truncatedArgs(a,e,B.finishReason),r=i?`工具参数在第 ${String(a).length} 个字符处被截断:这次回复的输出触到了单次上限(约 ${ft} token),参数没写完——不是字符或转义的问题,原样重试还会在同一处断。常见成因是一段内容太长,或一条回复里并行发了好几个大参数的调用(上限是整条回复共用的)。请拆小:大参数的调用一次少发几个;write_file每段约 3000 字以内,后续段落用append=true追加;很长的JSON(测算假设、图表spec)先write_file落盘,再用对应的 *_path / spec_path参数传入。`:`工具参数不是合法JSON(${e.message})${jsonErrorContext(a,e)}请修正参数后重新调用。`}const l=Ue.get(o);if(l&&We.has(o)&&!r){const e="function"==typeof l.disabledMessage?l.disabledMessage(t):l.disabledMessage;r=String(e||`工具 ${o} 当前已停用(开关已关闭),未执行。`)}const u=Q.isActive()&&("write_file"===o||"edit_file"===o);return{tc:e,idx:n,name:o,args:s,argsError:r,argsTruncated:i,extra:l,readOnly:l?!!l.readOnly:c.has(o),deferToIde:u}});!je&&ge.some(e=>e.argsTruncated)&><2&&ft<V&&(gt++,ft=Math.min(3*ft,V),Y(t.sessionId,ft),t.debug&&$e(`[debug] 工具参数被单轮输出上限截断,抬高到 ${ft}`));const he=e=>{for(let t=e;t<ge.length;t++)w.push({role:"tool",tool_call_id:ge[t].tc.id||`call_${t}`,content:ie+" 该工具未执行,无任何结果,请勿臆测其输出。"});return we?we.onInfo?.("已中断"):ye(G.gray("\n[已中断]\n")),{aborted:!0,usage:{prompt:Se,completion:xe},lastPromptTokens:ke,lastUsage:_e}},Ae={cwd:v,config:t,signal:H,permissions:C,hooks:bt,hooksActive:yt,fileJournal:be,info:$e,shortLine:de,engineering:Pe,turn:w.filter(e=>"user"===e.role).length,onStart:e=>e.nested?$e("程序内调用:"+u(e.name,e.args)):Ke(e),listTools:()=>De,resolveTool:e=>{const t=Ue.get(e);return t&&We.has(e)?null:t?{readOnly:!!t.readOnly,extra:t}:ze.has(e)?{readOnly:c.has(e),extra:null}:null}},Re=e=>O({name:e.name,args:e.args,readOnly:e.readOnly,extra:e.extra,callId:e.tc?.id,deferToIde:e.deferToIde,argsError:e.argsError,item:e},Ae),Ke=e=>{ut.add(e.name),we?we.onToolStart?.(e.name,e.args,{callId:e.callId||e.tc?.id}):ye(G.cyan(" ⏺ ")+u(e.name,e.args)+"\n")},Fe=(e,t)=>{const n=e.item||e;se(t)&&He++;const o=Z(e.name,e.args);o===Ve?Xe++:(Ve=o,Xe=1,Ye=0),we?we.onToolResult?.(e.name,String(t),{callId:n.tc?.id,failed:se(t)}):ye(G.gray(" "+de(t))+"\n"),w.push({role:"tool",tool_call_id:n.tc.id,content:String(t)})},Be=(e,t)=>{const n=e.item||e;we?we.onSkip?.(e.name,e.args,{callId:n.tc?.id,reason:t}):ye(G.gray(` ⊘ ${u(e.name,e.args)} — 已跳过\n`)),w.push({role:"tool",tool_call_id:n.tc.id,content:t||"未授权,未执行。"})},qe=e=>"render_diagram"!==e,wt=t=>{try{return e.readFileSync(t,"utf8")}catch{return""}},$t=(e,t)=>{const o=n.resolve(v,String(e.args.path));if(R(o))return;const s=m(t,wt(o),{path:String(e.args.path)});s&&(N(o)&&et.add(o),D(o)&&nt.add(o),we?we.onFileDiff?.(s):ye(s+"\n"))};let St=0;for(;St<ge.length;){if(H?.aborted)return he(St);const o=ge[St];if(o.readOnly&&!o.argsError){let e=St;for(;e<ge.length&&ge[e].readOnly&&!ge[e].argsError;)e++;const n=ge.slice(St,e),o=await ue(n,toolConcurrency(t),e=>Re(e));for(const e of o)null!=e.denied?Be(e,e.denied):Fe(e,e.result);if(St=e,H?.aborted)return he(St);continue}const s=await Re(o);if(H?.aborted&&null==s.denied&&!s.settled)return he(St);if(null==s.denied)if(s.settled)Ke(s),Fe(s,s.result),St++;else{if(Fe(s,s.result),null!=s.meta.before&&qe(s.name)&&$t(s,s.meta.before),W(s.name)&&/\.docx$/i.test(String(s.args?.path||""))){const t=n.resolve(v,String(s.args.path));e.existsSync(t)&&st.add(t)}St++}else Be(s,s.denied),St++}He>=3&&!Ge&&!Ze&&(Ze="你刚才连续多次工具调用失败。请停下来重新审视:是不是文件路径/内容/前提假设错了?先用read_file / grep / list_files核实现状,再换一种更可靠的方法,绝不要重复同样会失败的调用。",He=0,$e("检测到连续工具失败,已提示智能体换一种更可靠的方法")),Ge||Ze||(Xe>=5&&Ye<2?(Ze="停止重复调用。你已经用完全相同的参数调用同一工具达 5 次,这不会产生任何新信息,只是在空转。现在必须二选一:① 基于已有结果直接给出结论/继续下一步;② 明确说明你缺少什么、为什么现有结果不够,然后换一种**明显不同**的方法去拿。",Ye=2,$e(`检测到同一调用连续重复 ${Xe} 次,已要求智能体停止空转`)):Xe>=3&&Ye<1&&(Ze="注意:你刚刚用**完全相同的参数**连续调用了同一个工具多次,结果不会因为再调一次而改变。请先仔细重读上一次的返回内容——答案很可能已经在里面;若确实需要更多信息,请换不同的参数或换一个工具,不要重复同一次调用。",Ye=1,$e(`检测到同一调用连续重复 ${Xe} 次,已提示智能体重读上次结果`)));const xt=pe?.();xt&&w.push({role:"user",content:xt})}return dt&&(w.push({role:"assistant",content:dt}),we&&we.onTurnEnd?.(dt)),we?we.onInfo?.("已达最大工具轮数,停止"):ye(G.yellow("\n[已达最大工具轮数,停止]\n")),{usage:{prompt:Se,completion:xe},lastPromptTokens:ke,lastUsage:_e}}
|
package/src/api.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{sanitizeMessagesForModel as t}from"./vision.js";import{isWebSearchOn as e}from"./config.js";const n=new Set([429,500,502,503,504,529]),o=15e3;function r(t){return{"Content-Type":"application/json",Authorization:`Bearer ${t}`}}function a(t,e){try{const e=JSON.parse(t),n=e?.error?.type||e?.type||"";if(e?.error?.message)return{message:e.error.message,type:n};if(e?.message)return{message:e.message,type:n};if(e?.msg)return{message:e.msg,type:n}}catch{}return{message:t||e,type:""}}async function s(t){let e="";try{e=await t.text()}catch{}return{...a(e,`HTTP ${t.status}`),status:t.status}}function i(t){return Object.assign(new Error(t.message),{status:t.status,type:t.type,nonRetryable:/upstream_(protocol|configuration)_error/.test(t.type||"")})}function c(t,e){return new Promise((n,o)=>{if(e?.aborted)return o(u());const r=setTimeout(()=>{e?.removeEventListener?.("abort",a),n()},t),a=()=>{clearTimeout(r),o(u())};e?.addEventListener?.("abort",a,{once:!0})})}function u(){const t=new Error("已中断");return t.name="AbortError",t}function f(t,e){const n=new AbortController;let o=!1;const r=()=>n.abort();t&&(t.aborted?n.abort():t.addEventListener("abort",r,{once:!0}));const a=setTimeout(()=>{o=!0,n.abort()},e);return{signal:n.signal,ms:e,get timedOut(){return o},done(){clearTimeout(a),t?.removeEventListener?.("abort",r)}}}function l(t){const e=String(t?.message||t||""),n=t?.cause?.code||t?.code||"";return"ECONNREFUSED"===n||/ECONNREFUSED|connection refused/i.test(e)?"无法连接模型服务(连接被拒绝):请检查网络或稍后重试。":"ENOTFOUND"===n||"EAI_AGAIN"===n||/getaddrinfo|ENOTFOUND|EAI_AGAIN/i.test(e)?"无法解析模型服务地址(DNS异常):请检查网络连接后重试。":"ETIMEDOUT"===n||/ETIMEDOUT|timeout|timed out/i.test(e)?"连接模型服务超时:网络较慢或服务繁忙,请稍后重试。":"ECONNRESET"===n||/ECONNRESET|socket hang up|network|fetch failed/i.test(e)?"与模型服务的连接中断:网络波动,请稍后重试。":"网络错误:"+e}const h=/ENOTFOUND|EAI_AGAIN|ECONNRESET|ETIMEDOUT|EPIPE|ENETUNREACH|EHOSTUNREACH|ENETDOWN|UND_ERR_SOCKET|UND_ERR_CONNECT_TIMEOUT|socket hang up/i;export function isTransientNetError(t){const e=String(t?.cause?.code||t?.code||""),n=`${t?.cause?.message||""} ${t?.message||""}`;return!/ECONNREFUSED/i.test(e+n)&&(h.test(e)||h.test(n))}const m=[2e3,5e3,1e4,2e4,3e4];function d(t,e){const n=e?.headers?.get?.("retry-after"),o=null!=n?Number(n):NaN;if(Number.isFinite(o)&&o>=0)return Math.min(1e3*o,3e4);const r=n?Date.parse(n):NaN;return Number.isFinite(r)?Math.max(0,Math.min(r-Date.now(),3e4)):Math.min(500*2**t+Math.floor(250*Math.random()),8e3)}export async function streamChat(o,a={},h){const y=`${o.baseUrl}/v1/chat/completions`,{messages:p}=t(o.messages,o.model),w={model:o.model,messages:p,stream:!0,stream_options:{include_usage:!0}};o.tools&&o.tools.length&&(w.tools=o.tools,w.tool_choice="auto"),"number"==typeof o.temperature&&(w.temperature=o.temperature),Number.isFinite(o.maxTokens)&&o.maxTokens>0&&(w.max_tokens=Math.floor(o.maxTokens)),o.reasoningEffort&&"off"!==o.reasoningEffort&&(w.reasoning_effort=o.reasoningEffort),o.extraBody&&"object"==typeof o.extraBody&&Object.assign(w,o.extraBody);const b=r(o.apiKey);o.client&&(b["X-Huicai-Client"]=o.client),o.sessionId&&(b["X-Huicai-Session"]=o.sessionId),o.aux&&(b["X-Huicai-Aux"]="1"),o.continuation&&(b["X-Huicai-Continue"]="1"),o.tailCount>0&&(b["X-Huicai-Tail"]=String(o.tailCount)),e()||(b["X-Huicai-Web-Search"]="off");const E=JSON.stringify(w),S=o.netRetryWaits||m;let N=0;for(let t=0;;t++){let e;const r=f(h,o.headersTimeoutMs??12e4);try{e=await fetch(y,{method:"POST",headers:b,body:E,signal:r.signal})}catch(e){if(h?.aborted)throw u();if(r.timedOut){const e=`等待模型服务响应超时(${Math.round(r.ms/1e3)}s内未返回响应头)`;if(t<2){const n=d(t,null);a.onRetry?.(t+1,n,e),await c(n,h);continue}throw new Error(e+",请稍后重试。")}if("AbortError"===e?.name)throw u();const n=isTransientNetError(e);if(n&&N<S.length){const t=S[N++];a.onRetry?.(N,t,l(e),{network:!0,max:S.length}),await c(t,h);continue}if(!n&&t<2){const n=d(t,null);a.onRetry?.(t+1,n,l(e)),await c(n,h);continue}const o=new Error(l(e));throw n&&(o.network=!0),o}finally{r.done()}if(!e.ok||!e.body){const o=await s(e);if(!("usage_limit_exceeded"===o.type)&&n.has(e.status)&&t<2){const n=d(t,e);a.onRetry?.(t+1,n,`HTTP ${e.status}:${o.message}`),await c(n,h);continue}throw i(o)}return g(e,a,h,o.stallTimeoutMs??18e4)}}async function g(t,e,n,o){const r=t.body.getReader(),a=new TextDecoder;let s="",c="";const f=[];let l,h=null,m=null,d=!1,g=!1,y=!1,p=!1;const w=()=>{p&&(p=!1,e.onReasoningEnd?.())},b=(t.headers.get("content-type")||"").toLowerCase(),E=/application\/(?:[\w.-]+\+)?json/.test(b),S=t=>Object.assign(new Error(t),{type:"upstream_protocol_error",nonRetryable:!0});if(!E&&!b.includes("text/event-stream"))throw await r.cancel().catch(()=>{}),S("模型服务未返回SSE/JSON,请检查渠道API地址和协议。");let N=[];const T=t=>{if(!t.trim())return;if("[DONE]"===t.trim())return w(),g=!0,void(y=!0);let n;try{n=JSON.parse(t)}catch{throw S("模型响应包含无效JSON,无法解析。")}if(!n||"object"!=typeof n)throw S("模型返回了无效响应。");if(n.error){const t="object"==typeof n.error?n.error:{message:String(n.error)};throw i({message:"模型返回错误:"+(t.message||t.msg||"未知错误"),type:t.type||"",status:t.code})}if(/^(message|content_block)/.test(n.type||""))throw S("渠道配置为OpenAI,但上游返回Anthropic事件,请检查渠道协议。");if(!Object.keys(n).length||"ping"===n.type)return;if(!Array.isArray(n.choices))throw S("模型响应不符合OpenAI Chat Completions格式,请检查渠道地址和协议。");n.usage&&(m=n.usage,v());const o=n.choices[0];if(!o)return;d=!0;const r=o.delta||o.message;if(!r||"object"!=typeof r)throw S("模型响应缺少delta/message。");const a=r.reasoning_content??r.reasoning;if(a&&(v(),p=!0,e.onReasoning?.(a)),null!=r.content&&"string"!=typeof r.content)throw S("模型正文不是字符串,响应格式不兼容。");r.content&&(w(),v(),c+=r.content,e.onContent?.(r.content)),Array.isArray(r.tool_calls)&&r.tool_calls.length&&(w(),v(),function(t,e){for(const n of e){const e="number"==typeof n.index?n.index:t.length;t[e]||(t[e]={id:"",type:"function",function:{name:"",arguments:""}}),n.id&&(t[e].id=n.id),n.type&&(t[e].type=n.type),n.function?.name&&(t[e].function.name+=n.function.name),n.function?.arguments&&(t[e].function.arguments+=n.function.arguments)}}(f,r.tool_calls)),r.huicai_anthropic&&(l=r.huicai_anthropic),o.finish_reason&&(w(),v(),h=o.finish_reason,g=!0)},O=t=>{if(!y){if(""===t)return N.length&&T(N.join("\n")),void(N=[]);if(t.startsWith("data:")&&(N.push(t.slice(5).replace(/^ /,"")),N.reduce((t,e)=>t+e.length,0)>8388608))throw S("模型SSE事件过大。")}},A=(t=!1)=>{for(;s.length;){const e=s.search(/[\r\n]/);if(e<0||!t&&e===s.length-1&&"\r"===s[e])break;const n="\r"===s[e]&&"\n"===s[e+1]?2:1;O(s.slice(0,e)),s=s.slice(e+n)}t&&(s&&O(s),s="",O(""))};let x=!1,C=null,$=!1;const k=()=>{C&&clearTimeout(C),C=setTimeout(()=>{x=!0,r.cancel().catch(()=>{})},o)},v=()=>{$=!0,k()},j=()=>new Error($?`响应停滞超过 ${Math.round(o/1e3)}s,已中止(可重试)`:`模型 ${Math.round(o/1e3)}s内没有任何输出,已中止(可重试)`),M=()=>r.cancel().catch(()=>{});n?.addEventListener?.("abort",M,{once:!0});try{for(k();;){let t;try{t=await r.read()}catch(t){if(n?.aborted)throw u();if(x)throw j();throw t}if(n?.aborted)throw u();if(x)throw j();const{done:e,value:o}=t;if(e)break;if(s+=a.decode(o,{stream:!0}),s.length>8388608)throw S("模型响应超过解析大小限制。");if(E||A(),y)break}if(s+=a.decode(),E?(T(s),g=!0):A(!0),!d)throw S("模型响应没有有效的对话事件,请检查渠道地址和协议。");if(!g)throw S("模型响应流提前结束,未收到结束事件。")}catch(t){throw(c||f.length)&&(t.partial=!0),t}finally{C&&clearTimeout(C),w(),await r.cancel().catch(()=>{}),n?.removeEventListener?.("abort",M)}return{content:c,toolCalls:f.filter(Boolean),finishReason:h,usage:m,...l?{nativeState:l}:{}}}export async function embed(t,e,n,o){const a=await fetch(`${t}/v1/embeddings`,{method:"POST",headers:r(e),body:JSON.stringify({model:n,input:Array.isArray(o)?o:[o]}),signal:AbortSignal.timeout(3e4)});if(!a.ok)throw i(await s(a));const c=await a.json();return Array.isArray(c?.data)?c.data.map(t=>t.embedding):null}export async function syncKnowledge(t,e,n,o){const a=await fetch(`${t}/v1/knowledge/${encodeURIComponent(n)}/sync`,{method:"POST",headers:r(e),body:JSON.stringify({files:o}),signal:AbortSignal.timeout(12e4)});if(!a.ok)throw i(await s(a));return a.json()}export async function pushDecisions(t,e,n,o){const a=await fetch(`${t}/v1/decisions/sync`,{method:"POST",headers:r(e),body:JSON.stringify({projectKey:n,nodes:o}),signal:AbortSignal.timeout(6e4)});if(!a.ok)throw i(await s(a));const c=await a.json();return Number(c?.accepted||0)}export async function pullDecisions(t,e,n,o){const a=new URLSearchParams({projectKey:n});o&&a.set("since",String(o));const c=await fetch(`${t}/v1/decisions?${a.toString()}`,{headers:r(e),signal:AbortSignal.timeout(6e4)});if(!c.ok)throw i(await s(c));const u=await c.json();return Array.isArray(u?.data)?u.data:[]}export async function listModels(t,e){const n=await fetch(`${t}/v1/models`,{headers:r(e),signal:AbortSignal.timeout(o)});if(!n.ok)throw i(await s(n));const a=await n.json();return Array.isArray(a?.data)?a.data:[]}export async function getPlatformConfig(t,e,{timeoutMs:n=15e3}={}){try{const o=await fetch(`${t}/v1/config`,{headers:r(e),signal:AbortSignal.timeout(n)});if(!o.ok)return{};const a=await o.json();return a&&"object"==typeof a?a:{}}catch{return{}}}export async function webSearch(t,e,{query:n,count:o,client:a}={},c){const l=r(e);a&&(l["X-Huicai-Client"]=a);const h={query:String(n??"")};Number.isFinite(Number(o))&&Number(o)>0&&(h.count=Math.floor(Number(o)));const m=f(c,75e3);try{let e;try{e=await fetch(`${t}/v1/web/search`,{method:"POST",headers:l,body:JSON.stringify(h),signal:m.signal})}catch(t){if(c?.aborted)throw u();if(m.timedOut)throw new Error(`平台联网搜索超时(${Math.round(m.ms/1e3)}s内无响应)`);throw new Error("无法连接平台:"+(t?.cause?.code||t?.cause?.message||t?.message||String(t)))}if(!e.ok)throw i(await s(e));const n=await e.json();return n&&"object"==typeof n?n:{}}finally{m.done()}}export async function getAgentPersona(t,e,n){const a=await fetch(`${t}/v1/agents/${encodeURIComponent(n)}/persona`,{headers:r(e),signal:AbortSignal.timeout(o)});if(!a.ok)throw i(await s(a));return a.json()}export async function fetchUsage(t,e){const n=await fetch(`${t}/v1/usage`,{headers:r(e),signal:AbortSignal.timeout(o)});if(!n.ok)throw i(await s(n));return n.json()}export async function extractDocument(t,e,n,o){const a=n.bytes||(n.data?Buffer.from(String(n.data),"base64"):null);if(a&&!1!==p.get(t))try{const{body:s,contentType:i}=function(t,e,n,o){const r="----huicai"+Date.now().toString(36)+Math.random().toString(36).slice(2,10),a=String(t||"file").replace(/[\r\n"]/g,"_"),s=[];s.push(Buffer.from(`--${r}\r\nContent-Disposition: form-data; name="file"; filename="${a}"\r\nContent-Type: application/octet-stream\r\n\r\n`)),s.push(e,Buffer.from("\r\n"));for(const[t,e]of[["maxChars",n],["offset",o]])null!=e&&s.push(Buffer.from(`--${r}\r\nContent-Disposition: form-data; name="${t}"\r\n\r\n${e}\r\n`));return s.push(Buffer.from(`--${r}--\r\n`)),{body:Buffer.concat(s),contentType:`multipart/form-data; boundary=${r}`}}(n.filename,a,n.maxChars,n.offset),c=y(await S(`${t}/v1/files/extract`,{...r(e),"content-type":i},s,o),!0);return p.set(t,!0),c}catch(e){if(!e||!e._noMultipart)throw e;p.set(t,!1)}const c=n.bytes?{...n,bytes:void 0,data:n.bytes.toString("base64")}:n,u=JSON.stringify(c);if("function"==typeof o){return y(await S(`${t}/v1/files/extract`,{...r(e)},Buffer.from(u),o),!1)}const f=await fetch(`${t}/v1/files/extract`,{method:"POST",headers:r(e),body:u,signal:AbortSignal.timeout(12e4)});if(!f.ok)throw i(await s(f));return y(await f.text(),!1)}function y(t,e){let n=null;try{n=JSON.parse(t)}catch{}if(n&&"string"==typeof n.text)return n;const o=n&&(n.message||n.msg||n.error?.message)||"解析服务返回异常响应",r=new Error(o);throw e&&(r._noMultipart=!0),r}const p=new Map;const w=12e4,b=25,E=9e5;async function S(t,e,n,o){const{request:r}=await import("http:"===new URL(t).protocol?"node:http":"node:https"),s={};for(const[t,n]of Object.entries(e||{}))s[String(t).toLowerCase()]=n;return s["content-type"]||(s["content-type"]="application/json"),s["content-length"]=n.length,new Promise((e,i)=>{const c=r(t,{method:"POST",headers:s,agent:!1},t=>{const n=[];t.on("data",t=>n.push(t)),t.on("error",i),t.on("end",()=>{const o=Buffer.concat(n).toString("utf8");t.statusCode>=200&&t.statusCode<300?e(o):415===t.statusCode||405===t.statusCode?i(Object.assign(new Error(a(o,`HTTP ${t.statusCode}`).message),{_noMultipart:!0})):i(new Error(a(o,`HTTP ${t.statusCode}`).message))})}),u=Math.min(E,w+Math.floor(n.length/b));c.setTimeout(u,()=>c.destroy(new Error(`网络传输/服务端处理超时(已等待 ${Math.round(u/1e3)} 秒无响应),请检查网络后重试`))),c.on("error",i);let f=0;const l=()=>{for(;f<n.length;){const t=Math.min(f+262144,n.length),e=c.write(n.subarray(f,t));f=t;try{o&&o(f,n.length)}catch{}if(!e)return void c.once("drain",l)}c.end()};l()})}export function startParseTicker(t,e,n=200){const o=Math.max(600,400+t/15360),r=Date.now(),a=setInterval(()=>{const t=Date.now()-r,n=Math.min(99,Math.round(100*(1-Math.exp(-t/o))));try{e(n,t)}catch{}},n);return()=>clearInterval(a)}export async function listPlatformSkills(t,e){const n=await fetch(`${t}/v1/skills`,{headers:r(e),signal:AbortSignal.timeout(o)});if(!n.ok)throw i(await s(n));const a=await n.json();return Array.isArray(a?.data)?a.data:[]}export async function getPlatformSkill(t,e,n){const a=await fetch(`${t}/v1/skills/${encodeURIComponent(n)}`,{headers:r(e),signal:AbortSignal.timeout(o)});if(!a.ok)throw i(await s(a));return a.json()}export async function pushPlatformSkill(t,e,n){const a=await fetch(`${t}/v1/skills/import`,{method:"POST",headers:{...r(e),"Content-Type":"application/json"},body:JSON.stringify(n),signal:AbortSignal.timeout(o)});if(!a.ok)throw i(await s(a));return a.json()}export async function listNotifyChannels(t,e){const n=await fetch(`${t}/v1/notify/channels`,{headers:r(e),signal:AbortSignal.timeout(o)});if(!n.ok)throw i(await s(n));const a=await n.json();return Array.isArray(a?.data)?a.data:[]}export async function pushNotifyChannel(t,e,n,a,c){const u=await fetch(`${t}/v1/notify/push`,{method:"POST",headers:{...r(e),"Content-Type":"application/json"},body:JSON.stringify({channel_id:n,title:a,content:c}),signal:AbortSignal.timeout(o)});if(!u.ok)throw i(await s(u));const f=await u.json();return!!f?.delivered}
|
|
1
|
+
import{sanitizeMessagesForModel as t}from"./vision.js";import{isWebSearchOn as e}from"./config.js";const n=new Set([429,500,502,503,504,529]),o=15e3;function r(t){return{"Content-Type":"application/json",Authorization:`Bearer ${t}`}}function a(t,e){try{const e=JSON.parse(t),n=e?.error?.type||e?.type||"";if(e?.error?.message)return{message:e.error.message,type:n};if(e?.message)return{message:e.message,type:n};if(e?.msg)return{message:e.msg,type:n}}catch{}return{message:t||e,type:""}}async function s(t){let e="";try{e=await t.text()}catch{}return{...a(e,`HTTP ${t.status}`),status:t.status}}function i(t){return Object.assign(new Error(t.message),{status:t.status,type:t.type,nonRetryable:/upstream_(protocol|configuration)_error/.test(t.type||"")})}function c(t,e){return new Promise((n,o)=>{if(e?.aborted)return o(u());const r=setTimeout(()=>{e?.removeEventListener?.("abort",a),n()},t),a=()=>{clearTimeout(r),o(u())};e?.addEventListener?.("abort",a,{once:!0})})}function u(){const t=new Error("已中断");return t.name="AbortError",t}function f(t,e){const n=new AbortController;let o=!1;const r=()=>n.abort();t&&(t.aborted?n.abort():t.addEventListener("abort",r,{once:!0}));const a=setTimeout(()=>{o=!0,n.abort()},e);return{signal:n.signal,ms:e,get timedOut(){return o},done(){clearTimeout(a),t?.removeEventListener?.("abort",r)}}}function l(t){const e=String(t?.message||t||""),n=t?.cause?.code||t?.code||"";return"ECONNREFUSED"===n||/ECONNREFUSED|connection refused/i.test(e)?"无法连接模型服务(连接被拒绝):请检查网络或稍后重试。":"ENOTFOUND"===n||"EAI_AGAIN"===n||/getaddrinfo|ENOTFOUND|EAI_AGAIN/i.test(e)?"无法解析模型服务地址(DNS异常):请检查网络连接后重试。":"ETIMEDOUT"===n||/ETIMEDOUT|timeout|timed out/i.test(e)?"连接模型服务超时:网络较慢或服务繁忙,请稍后重试。":"ECONNRESET"===n||/ECONNRESET|socket hang up|network|fetch failed/i.test(e)?"与模型服务的连接中断:网络波动,请稍后重试。":"网络错误:"+e}const h=/ENOTFOUND|EAI_AGAIN|ECONNRESET|ETIMEDOUT|EPIPE|ENETUNREACH|EHOSTUNREACH|ENETDOWN|UND_ERR_SOCKET|UND_ERR_CONNECT_TIMEOUT|socket hang up/i;export function isTransientNetError(t){const e=String(t?.cause?.code||t?.code||""),n=`${t?.cause?.message||""} ${t?.message||""}`;return!/ECONNREFUSED/i.test(e+n)&&(h.test(e)||h.test(n))}const m=[2e3,5e3,1e4,2e4,3e4];function d(t,e){const n=e?.headers?.get?.("retry-after"),o=null!=n?Number(n):NaN;if(Number.isFinite(o)&&o>=0)return Math.min(1e3*o,3e4);const r=n?Date.parse(n):NaN;return Number.isFinite(r)?Math.max(0,Math.min(r-Date.now(),3e4)):Math.min(500*2**t+Math.floor(250*Math.random()),8e3)}export async function streamChat(o,a={},h){const y=`${o.baseUrl}/v1/chat/completions`,{messages:p}=t(o.messages,o.model,o.supportsImages),w={model:o.model,messages:p,stream:!0,stream_options:{include_usage:!0}};o.tools&&o.tools.length&&(w.tools=o.tools,w.tool_choice="auto"),"number"==typeof o.temperature&&(w.temperature=o.temperature),Number.isFinite(o.maxTokens)&&o.maxTokens>0&&(w.max_tokens=Math.floor(o.maxTokens)),o.reasoningEffort&&"off"!==o.reasoningEffort&&(w.reasoning_effort=o.reasoningEffort),o.extraBody&&"object"==typeof o.extraBody&&Object.assign(w,o.extraBody);const b=r(o.apiKey);o.client&&(b["X-Huicai-Client"]=o.client),o.sessionId&&(b["X-Huicai-Session"]=o.sessionId),o.aux&&(b["X-Huicai-Aux"]="1"),o.continuation&&(b["X-Huicai-Continue"]="1"),o.tailCount>0&&(b["X-Huicai-Tail"]=String(o.tailCount)),e()||(b["X-Huicai-Web-Search"]="off");const E=JSON.stringify(w),S=o.netRetryWaits||m;let N=0;for(let t=0;;t++){let e;const r=f(h,o.headersTimeoutMs??12e4);try{e=await fetch(y,{method:"POST",headers:b,body:E,signal:r.signal})}catch(e){if(h?.aborted)throw u();if(r.timedOut){const e=`等待模型服务响应超时(${Math.round(r.ms/1e3)}s内未返回响应头)`;if(t<2){const n=d(t,null);a.onRetry?.(t+1,n,e),await c(n,h);continue}throw new Error(e+",请稍后重试。")}if("AbortError"===e?.name)throw u();const n=isTransientNetError(e);if(n&&N<S.length){const t=S[N++];a.onRetry?.(N,t,l(e),{network:!0,max:S.length}),await c(t,h);continue}if(!n&&t<2){const n=d(t,null);a.onRetry?.(t+1,n,l(e)),await c(n,h);continue}const o=new Error(l(e));throw n&&(o.network=!0),o}finally{r.done()}if(!e.ok||!e.body){const o=await s(e);if(!("usage_limit_exceeded"===o.type)&&n.has(e.status)&&t<2){const n=d(t,e);a.onRetry?.(t+1,n,`HTTP ${e.status}:${o.message}`),await c(n,h);continue}throw i(o)}return g(e,a,h,o.stallTimeoutMs??18e4)}}async function g(t,e,n,o){const r=t.body.getReader(),a=new TextDecoder;let s="",c="";const f=[];let l,h=null,m=null,d=!1,g=!1,y=!1,p=!1;const w=()=>{p&&(p=!1,e.onReasoningEnd?.())},b=(t.headers.get("content-type")||"").toLowerCase(),E=/application\/(?:[\w.-]+\+)?json/.test(b),S=t=>Object.assign(new Error(t),{type:"upstream_protocol_error",nonRetryable:!0});if(!E&&!b.includes("text/event-stream"))throw await r.cancel().catch(()=>{}),S("模型服务未返回SSE/JSON,请检查渠道API地址和协议。");let N=[];const T=t=>{if(!t.trim())return;if("[DONE]"===t.trim())return w(),g=!0,void(y=!0);let n;try{n=JSON.parse(t)}catch{throw S("模型响应包含无效JSON,无法解析。")}if(!n||"object"!=typeof n)throw S("模型返回了无效响应。");if(n.error){const t="object"==typeof n.error?n.error:{message:String(n.error)};throw i({message:"模型返回错误:"+(t.message||t.msg||"未知错误"),type:t.type||"",status:t.code})}if(/^(message|content_block)/.test(n.type||""))throw S("渠道配置为OpenAI,但上游返回Anthropic事件,请检查渠道协议。");if(!Object.keys(n).length||"ping"===n.type)return;if(!Array.isArray(n.choices))throw S("模型响应不符合OpenAI Chat Completions格式,请检查渠道地址和协议。");n.usage&&(m=n.usage,v());const o=n.choices[0];if(!o)return;d=!0;const r=o.delta||o.message;if(!r||"object"!=typeof r)throw S("模型响应缺少delta/message。");const a=r.reasoning_content??r.reasoning;if(a&&(v(),p=!0,e.onReasoning?.(a)),null!=r.content&&"string"!=typeof r.content)throw S("模型正文不是字符串,响应格式不兼容。");r.content&&(w(),v(),c+=r.content,e.onContent?.(r.content)),Array.isArray(r.tool_calls)&&r.tool_calls.length&&(w(),v(),function(t,e){for(const n of e){const e="number"==typeof n.index?n.index:t.length;t[e]||(t[e]={id:"",type:"function",function:{name:"",arguments:""}}),n.id&&(t[e].id=n.id),n.type&&(t[e].type=n.type),n.function?.name&&(t[e].function.name+=n.function.name),n.function?.arguments&&(t[e].function.arguments+=n.function.arguments)}}(f,r.tool_calls)),r.huicai_anthropic&&(l=r.huicai_anthropic),o.finish_reason&&(w(),v(),h=o.finish_reason,g=!0)},O=t=>{if(!y){if(""===t)return N.length&&T(N.join("\n")),void(N=[]);if(t.startsWith("data:")&&(N.push(t.slice(5).replace(/^ /,"")),N.reduce((t,e)=>t+e.length,0)>8388608))throw S("模型SSE事件过大。")}},A=(t=!1)=>{for(;s.length;){const e=s.search(/[\r\n]/);if(e<0||!t&&e===s.length-1&&"\r"===s[e])break;const n="\r"===s[e]&&"\n"===s[e+1]?2:1;O(s.slice(0,e)),s=s.slice(e+n)}t&&(s&&O(s),s="",O(""))};let x=!1,C=null,$=!1;const k=()=>{C&&clearTimeout(C),C=setTimeout(()=>{x=!0,r.cancel().catch(()=>{})},o)},v=()=>{$=!0,k()},j=()=>new Error($?`响应停滞超过 ${Math.round(o/1e3)}s,已中止(可重试)`:`模型 ${Math.round(o/1e3)}s内没有任何输出,已中止(可重试)`),M=()=>r.cancel().catch(()=>{});n?.addEventListener?.("abort",M,{once:!0});try{for(k();;){let t;try{t=await r.read()}catch(t){if(n?.aborted)throw u();if(x)throw j();throw t}if(n?.aborted)throw u();if(x)throw j();const{done:e,value:o}=t;if(e)break;if(s+=a.decode(o,{stream:!0}),s.length>8388608)throw S("模型响应超过解析大小限制。");if(E||A(),y)break}if(s+=a.decode(),E?(T(s),g=!0):A(!0),!d)throw S("模型响应没有有效的对话事件,请检查渠道地址和协议。");if(!g)throw S("模型响应流提前结束,未收到结束事件。")}catch(t){throw(c||f.length)&&(t.partial=!0),t}finally{C&&clearTimeout(C),w(),await r.cancel().catch(()=>{}),n?.removeEventListener?.("abort",M)}return{content:c,toolCalls:f.filter(Boolean),finishReason:h,usage:m,...l?{nativeState:l}:{}}}export async function embed(t,e,n,o){const a=await fetch(`${t}/v1/embeddings`,{method:"POST",headers:r(e),body:JSON.stringify({model:n,input:Array.isArray(o)?o:[o]}),signal:AbortSignal.timeout(3e4)});if(!a.ok)throw i(await s(a));const c=await a.json();return Array.isArray(c?.data)?c.data.map(t=>t.embedding):null}export async function syncKnowledge(t,e,n,o){const a=await fetch(`${t}/v1/knowledge/${encodeURIComponent(n)}/sync`,{method:"POST",headers:r(e),body:JSON.stringify({files:o}),signal:AbortSignal.timeout(12e4)});if(!a.ok)throw i(await s(a));return a.json()}export async function pushDecisions(t,e,n,o){const a=await fetch(`${t}/v1/decisions/sync`,{method:"POST",headers:r(e),body:JSON.stringify({projectKey:n,nodes:o}),signal:AbortSignal.timeout(6e4)});if(!a.ok)throw i(await s(a));const c=await a.json();return Number(c?.accepted||0)}export async function pullDecisions(t,e,n,o){const a=new URLSearchParams({projectKey:n});o&&a.set("since",String(o));const c=await fetch(`${t}/v1/decisions?${a.toString()}`,{headers:r(e),signal:AbortSignal.timeout(6e4)});if(!c.ok)throw i(await s(c));const u=await c.json();return Array.isArray(u?.data)?u.data:[]}export async function listModels(t,e){const n=await fetch(`${t}/v1/models`,{headers:r(e),signal:AbortSignal.timeout(o)});if(!n.ok)throw i(await s(n));const a=await n.json();return Array.isArray(a?.data)?a.data:[]}export async function getPlatformConfig(t,e,{timeoutMs:n=15e3}={}){try{const o=await fetch(`${t}/v1/config`,{headers:r(e),signal:AbortSignal.timeout(n)});if(!o.ok)return{};const a=await o.json();return a&&"object"==typeof a?a:{}}catch{return{}}}export async function webSearch(t,e,{query:n,count:o,client:a}={},c){const l=r(e);a&&(l["X-Huicai-Client"]=a);const h={query:String(n??"")};Number.isFinite(Number(o))&&Number(o)>0&&(h.count=Math.floor(Number(o)));const m=f(c,75e3);try{let e;try{e=await fetch(`${t}/v1/web/search`,{method:"POST",headers:l,body:JSON.stringify(h),signal:m.signal})}catch(t){if(c?.aborted)throw u();if(m.timedOut)throw new Error(`平台联网搜索超时(${Math.round(m.ms/1e3)}s内无响应)`);throw new Error("无法连接平台:"+(t?.cause?.code||t?.cause?.message||t?.message||String(t)))}if(!e.ok)throw i(await s(e));const n=await e.json();return n&&"object"==typeof n?n:{}}finally{m.done()}}export async function getAgentPersona(t,e,n){const a=await fetch(`${t}/v1/agents/${encodeURIComponent(n)}/persona`,{headers:r(e),signal:AbortSignal.timeout(o)});if(!a.ok)throw i(await s(a));return a.json()}export async function fetchUsage(t,e){const n=await fetch(`${t}/v1/usage`,{headers:r(e),signal:AbortSignal.timeout(o)});if(!n.ok)throw i(await s(n));return n.json()}export async function extractDocument(t,e,n,o){const a=n.bytes||(n.data?Buffer.from(String(n.data),"base64"):null);if(a&&!1!==p.get(t))try{const{body:s,contentType:i}=function(t,e,n,o){const r="----huicai"+Date.now().toString(36)+Math.random().toString(36).slice(2,10),a=String(t||"file").replace(/[\r\n"]/g,"_"),s=[];s.push(Buffer.from(`--${r}\r\nContent-Disposition: form-data; name="file"; filename="${a}"\r\nContent-Type: application/octet-stream\r\n\r\n`)),s.push(e,Buffer.from("\r\n"));for(const[t,e]of[["maxChars",n],["offset",o]])null!=e&&s.push(Buffer.from(`--${r}\r\nContent-Disposition: form-data; name="${t}"\r\n\r\n${e}\r\n`));return s.push(Buffer.from(`--${r}--\r\n`)),{body:Buffer.concat(s),contentType:`multipart/form-data; boundary=${r}`}}(n.filename,a,n.maxChars,n.offset),c=y(await S(`${t}/v1/files/extract`,{...r(e),"content-type":i},s,o),!0);return p.set(t,!0),c}catch(e){if(!e||!e._noMultipart)throw e;p.set(t,!1)}const c=n.bytes?{...n,bytes:void 0,data:n.bytes.toString("base64")}:n,u=JSON.stringify(c);if("function"==typeof o){return y(await S(`${t}/v1/files/extract`,{...r(e)},Buffer.from(u),o),!1)}const f=await fetch(`${t}/v1/files/extract`,{method:"POST",headers:r(e),body:u,signal:AbortSignal.timeout(12e4)});if(!f.ok)throw i(await s(f));return y(await f.text(),!1)}function y(t,e){let n=null;try{n=JSON.parse(t)}catch{}if(n&&"string"==typeof n.text)return n;const o=n&&(n.message||n.msg||n.error?.message)||"解析服务返回异常响应",r=new Error(o);throw e&&(r._noMultipart=!0),r}const p=new Map;const w=12e4,b=25,E=9e5;async function S(t,e,n,o){const{request:r}=await import("http:"===new URL(t).protocol?"node:http":"node:https"),s={};for(const[t,n]of Object.entries(e||{}))s[String(t).toLowerCase()]=n;return s["content-type"]||(s["content-type"]="application/json"),s["content-length"]=n.length,new Promise((e,i)=>{const c=r(t,{method:"POST",headers:s,agent:!1},t=>{const n=[];t.on("data",t=>n.push(t)),t.on("error",i),t.on("end",()=>{const o=Buffer.concat(n).toString("utf8");t.statusCode>=200&&t.statusCode<300?e(o):415===t.statusCode||405===t.statusCode?i(Object.assign(new Error(a(o,`HTTP ${t.statusCode}`).message),{_noMultipart:!0})):i(new Error(a(o,`HTTP ${t.statusCode}`).message))})}),u=Math.min(E,w+Math.floor(n.length/b));c.setTimeout(u,()=>c.destroy(new Error(`网络传输/服务端处理超时(已等待 ${Math.round(u/1e3)} 秒无响应),请检查网络后重试`))),c.on("error",i);let f=0;const l=()=>{for(;f<n.length;){const t=Math.min(f+262144,n.length),e=c.write(n.subarray(f,t));f=t;try{o&&o(f,n.length)}catch{}if(!e)return void c.once("drain",l)}c.end()};l()})}export function startParseTicker(t,e,n=200){const o=Math.max(600,400+t/15360),r=Date.now(),a=setInterval(()=>{const t=Date.now()-r,n=Math.min(99,Math.round(100*(1-Math.exp(-t/o))));try{e(n,t)}catch{}},n);return()=>clearInterval(a)}export async function listPlatformSkills(t,e){const n=await fetch(`${t}/v1/skills`,{headers:r(e),signal:AbortSignal.timeout(o)});if(!n.ok)throw i(await s(n));const a=await n.json();return Array.isArray(a?.data)?a.data:[]}export async function getPlatformSkill(t,e,n){const a=await fetch(`${t}/v1/skills/${encodeURIComponent(n)}`,{headers:r(e),signal:AbortSignal.timeout(o)});if(!a.ok)throw i(await s(a));return a.json()}export async function pushPlatformSkill(t,e,n){const a=await fetch(`${t}/v1/skills/import`,{method:"POST",headers:{...r(e),"Content-Type":"application/json"},body:JSON.stringify(n),signal:AbortSignal.timeout(o)});if(!a.ok)throw i(await s(a));return a.json()}export async function listNotifyChannels(t,e){const n=await fetch(`${t}/v1/notify/channels`,{headers:r(e),signal:AbortSignal.timeout(o)});if(!n.ok)throw i(await s(n));const a=await n.json();return Array.isArray(a?.data)?a.data:[]}export async function pushNotifyChannel(t,e,n,a,c){const u=await fetch(`${t}/v1/notify/push`,{method:"POST",headers:{...r(e),"Content-Type":"application/json"},body:JSON.stringify({channel_id:n,title:a,content:c}),signal:AbortSignal.timeout(o)});if(!u.ok)throw i(await s(u));const f=await u.json();return!!f?.delivered}
|
package/src/config.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"node:fs";import r from"node:path";import o from"node:os";export const CONFIG_DIR=r.join(o.homedir(),".huicai");export const CONFIG_PATH=r.join(CONFIG_DIR,"config.json");export const DEFAULT_BASE_URL="https://huicai-ai.sjht.com";export const DEFAULT_MODEL="deepseek-v4-flash";function
|
|
1
|
+
import e from"node:fs";import r from"node:path";import o from"node:os";export const CONFIG_DIR=process.env.HUICAI_CONFIG_DIR?r.resolve(process.env.HUICAI_CONFIG_DIR):r.join(o.homedir(),".huicai");export const CONFIG_PATH=r.join(CONFIG_DIR,"config.json");export const DEFAULT_BASE_URL="https://huicai-ai.sjht.com";export const DEFAULT_MODEL="deepseek-v4-flash";function s(){try{return JSON.parse(e.readFileSync(CONFIG_PATH,"utf8"))}catch{return{}}}export function loadConfig(){const e=s(),r={baseUrl:(process.env.HUICAI_BASE_URL||e.baseUrl||DEFAULT_BASE_URL).trim(),apiKey:(process.env.HUICAI_API_KEY||e.apiKey||"").trim(),model:(process.env.HUICAI_MODEL||e.model||DEFAULT_MODEL).trim(),maxTurns:Number(process.env.HUICAI_MAX_TURNS||e.maxTurns)||0,maxTokens:Number(process.env.HUICAI_MAX_TOKENS||e.maxTokens)||0,autoCompactTokens:Number(process.env.HUICAI_AUTOCOMPACT_TOKENS||e.autoCompactTokens)||0,toolConcurrency:Number(process.env.HUICAI_TOOL_CONCURRENCY||e.toolConcurrency)||0,subagentConcurrency:Number(process.env.HUICAI_SUBAGENT_CONCURRENCY||e.subagentConcurrency)||0,reasoningEffort:(process.env.HUICAI_EFFORT||e.reasoningEffort||"").trim(),compactModel:(process.env.HUICAI_COMPACT_MODEL||e.compactModel||"").trim(),memoryEnabled:"0"!==process.env.HUICAI_MEMORY&&!1!==e.memory,showReasoning:"0"!==process.env.HUICAI_REASONING&&!1!==e.showReasoning,markdown:"0"!==process.env.HUICAI_MARKDOWN&&!1!==e.markdown,client:(process.env.HUICAI_CLIENT||"cli").trim().toLowerCase(),theme:(process.env.HUICAI_THEME||e.theme||"default").trim(),accentColor:(e.accentColor||"cyan").trim(),tui:(process.env.HUICAI_TUI||e.tui||"auto").trim(),debug:"1"===process.env.HUICAI_DEBUG||!0===e.debug,statusLine:(e.statusLine||"").trim(),advisorModel:(process.env.HUICAI_ADVISOR||e.advisorModel||"").trim(),autoModelStrong:(process.env.HUICAI_AUTOMODEL||e.autoModelStrong||"").trim(),autoModelThreshold:Number(process.env.HUICAI_AUTOMODEL_THRESHOLD||e.autoModelThreshold)||0,subagentModel:(process.env.HUICAI_SUBAGENT_MODEL||e.subagentModel||"").trim(),subagentMaxIters:Number(process.env.HUICAI_SUBAGENT_MAXITERS||e.subagentMaxIters)||0,budgetTokens:Number(e.budgetTokens)||0,crewBudget:"1"===process.env.HUICAI_CREW_BUDGET||!0===e.crewBudget,crewBudgetPlan:e.crewBudgetPlan&&"object"==typeof e.crewBudgetPlan?e.crewBudgetPlan:{},embeddingModel:(process.env.HUICAI_EMBED_MODEL||e.embeddingModel||"").trim(),pyUvMirror:(process.env.HUICAI_UV_MIRROR||e.pyUvMirror||"").trim(),pyUvVersion:(process.env.HUICAI_UV_VERSION||e.pyUvVersion||"").trim(),pyPythonMirror:(process.env.HUICAI_PYTHON_MIRROR||e.pyPythonMirror||"").trim(),pyPypiMirror:(process.env.HUICAI_PYPI_MIRROR||e.pyPypiMirror||"").trim(),pyBundleUrl:(process.env.HUICAI_PYRT_BUNDLE||e.pyBundleUrl||"").trim(),pyPrewarm:!1!==e.pyPrewarm,webSearch:"0"!==process.env.HUICAI_WEB_SEARCH&&!1!==e.webSearch,computerUse:"0"!==process.env.HUICAI_COMPUTER_USE&&!1!==e.computerUse,notifications:"0"!==process.env.HUICAI_NOTIFY&&!1!==e.notifications,autoSkillSync:"0"!==process.env.HUICAI_SKILL_SYNC&&!1!==e.autoSkillSync,wakeLockInAuto:"0"!==process.env.HUICAI_WAKELOCK&&!1!==e.wakeLockInAuto,visionModel:(process.env.HUICAI_VISION_MODEL||e.visionModel||"").trim(),spill:"0"!==process.env.HUICAI_SPILL&&!1!==e.spill,crush:"0"!==process.env.HUICAI_CRUSH&&!1!==e.crush,execSandbox:(process.env.HUICAI_EXEC_SANDBOX||e.execSandbox||"off").trim(),toolMode:(process.env.HUICAI_TOOL_MODE||e.toolMode||"native").trim(),user:e.user&&"object"==typeof e.user?e.user:null};return r.baseUrl=r.baseUrl.replace(/\/+$/,""),r}export function saveConfig(r){const o={...s(),...r};e.mkdirSync(CONFIG_DIR,{recursive:!0}),e.writeFileSync(CONFIG_PATH,JSON.stringify(o,null,2)+"\n",{mode:384});try{e.chmodSync(CONFIG_PATH,384)}catch{}return t=null,o}let n=null,t=null;export function setWebSearchHost(e){n=e&&"function"==typeof e.get?e:null}export function isWebSearchOn(){if("0"===process.env.HUICAI_WEB_SEARCH)return!1;if(n)try{return!1!==n.get()}catch{}let r;try{r=e.statSync(CONFIG_PATH)}catch{return!0}if(t&&t.mtimeMs===r.mtimeMs&&t.size===r.size)return t.on;const o=!1!==s().webSearch;return t={mtimeMs:r.mtimeMs,size:r.size,on:o},o}export async function setWebSearchOn(e){const r=!!e;return n&&"function"==typeof n.set?(await n.set(r),r):(saveConfig({webSearch:r}),r)}
|
package/src/context.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import e from"node:fs";import n from"node:path";import t from"node:os";import{execSync as r}from"node:child_process";import{loadMemory as i}from"./memory.js";import{buildRepoMap as o}from"./codeindex.js";import{loadSquads as s}from"./squad.js";import{loadAgents as a}from"./subagent.js";import{COMPUTER_PROMPT_HINT as c,computerUseEnabled as l}from"./computer.js";import{CREW_GUIDELINE as p}from"./crew.js";const m=["- 先了解现状再动手:并行使用read_file/glob/grep/list_files收集信息(一轮可同时调多个只读工具),不要臆测文件内容。","- 编码前必须通读需求涉及的源码上下文、调用链、项目规范、文档与约定;表格用read_document或file-ingest脚本解析,图片用可用视觉工具查看。必要时先分析全库;不得以搜索片段代替通读。",'- 编程任务先load_skill("eng-develop"),用engineering_plan登记实际读过的references、拟改targets、调用链、规范、体验考虑及验收checks;新建项目也先调研目录。工具未挂载时照此执行并如实留证。',"- 任何代码改动后必须真实跑单测、启动项目、冒烟和受影响接口/功能(含正常、错误、边界路径);用engineering_verify逐项运行,构建通过不等于启动成功。涉及客户端即使只改共享核心也要client=true,在真实宿主或设备模拟器走交互并保留证据,模拟视口不等于原生设备验证。","- shell、脚本、终端、MCP和自动修复同样遵守先分析后修改;将其涉及的文件列入targets,不得绕过流程。修改后重跑受影响验证;环境/账号/设备缺失属于未验证,不得用mock、静态检查或口头声明替代。","- read_file输出的「行号→」前缀仅供定位,不是文件内容;写edit_file的old_string/new_string时绝不能包含行号前缀。","- 多步骤任务先用todo_write列出计划,并随进度更新状态(同一时刻至多一项in_progress);单步小事不必用。","- 大而可拆的任务优先并行铺开,别一个人串行啃:互相独立的调研/检索/分析子任务用dispatch_agents一次派出多路(每个子任务须自足,多路共用的背景放shared_context,不要逐条重复抄);单个独立子任务用dispatch_agent;跨角色的复杂任务用dispatch_member按专家团分工派发,独立任务同一轮多次调用即并行。子智能体只读、在隔离上下文中执行、只回传结论,因此主线上下文保持精简、总耗时按最慢的一路算。(这些工具未挂载时就自己做;简单任务直接做,不要为拆而拆——派发本身也有开销。)",p,"- 收到多路子任务结论后,先交叉核对再采信:结论之间矛盾、或与你已知事实冲突时,用本机工具亲自复核关键点,不要把子智能体的说法当既成事实直接写进产物。","- 改代码优先用edit_file做最小精确改动;新建文件才用write_file。遵循项目既有风格与命名。","- 用run_bash执行构建/git等;改动后要运行测试自检,失败要定位修复而不是忽略。","- **跑单元测试用run_tests,不要用run_bash**:它自动探测框架(Maven/Gradle/pytest/Node)并把结果解析成「通过/失败数 + 每个失败用例的期望值与实际值 + 堆栈里第一个非测试非框架的位置(也就是你哪一行业务代码出的问题)」。run_bash只会给你几千行原始输出,要你自己从噪声里找——那正是「测试失败了,可能是X的问题」这类猜测结论的来源。定位问题时用filter只跑相关用例,比全量跑快得多。",'- 写完或改完代码后用code_inspect自查(逻辑/注释与实现是否一致/可读性/结构/测试/安全/一致性七类),**必修项清零再说完成**;肉眼扫一遍不算自查——空捕获、注释里写着早已改名的形参、测试里的 .only残留,这些都是"看起来很对"的,读第二遍也发现不了。fix=true只能修五类改法唯一的问题,逻辑与安全类它不碰、只报给你,要你自己判断怎么改。','- 要补单元测试时先test_plan算清该测什么(哪些函数没覆盖、每个有几条路、参数边界在哪、项目用什么框架放哪个目录),再照着写用例。直接开写的产出多半是"调一次、断言不为空"——覆盖率上去了,缺陷一个也拦不住。',"- 处理数据/表格/文档/各类文件时,用run_python(内置pandas/openpyxl/xlrd/pypdf/python-docx/python-pptx/matplotlib等)在本机精确处理,绝不要只凭肉眼或纯文本摘要臆断数字;先摸清结构(表头在第几行/多Sheet/合并单元格/金额是否文本/日期格式/编码)再算,关键中间量print出来核对。","- 复杂数据任务优先载入并复用现成技能脚本,别从零手搓易错的解析:读任意文件(Excel/PDF/Word/PPT/OFD/RTF/图片…)用file-ingest的read_any;Excel分析/透视/对账用xlsx-analysis(smart_read/sheet_map/reconcile);多表关联匹配用data-wrangling(safe_join)、维度透视交叉用pivot-crosstab、统计检验用statistical-testing、回归预测用regression-forecast。跨文档/跨表交叉分析务必口径一致、逐步可复核。",'- 需要在**对话里**呈现**看板/仪表盘/指标卡/数据表格+图表/检查清单/时间线/摘要卡**时用render_ui:你只给组件树与数据(靠 {"path":"字段"} 绑定),排版与呈现由引擎负责,桌面端渲染为可视化面板、其它终端自动降级为等价文本。**不要手写HTML/CSS糊一个界面,也不要拿Markdown表格凑数**。组件只能用注册过的那些——组件名与必填字段先调ui_catalog看清单,别猜。(这条只管对话内的数据呈现;用户要的交付物本身就是网页/H5/前端项目时,照常写真实的HTML/CSS/JS文件,见下一条。)','- 做网页/H5/落地页/邀请函/活动页/前端界面这类「用户会直接打开、直接用」的交付物:先load_skill("web-craft"),按它定下设计方向与设计令牌(配色/字体/间距/动效基调)再写码。交付要达到**作品级**:真实得体的文案(不写Lorem、不把「把照片放到assets/img」这种开发说明露给访客)、每个按钮都真能用、空/加载/出错态齐全、手机优先、不依赖国内打不开的外链、没有素材就用CSS/SVG画而不是留破图。写完用 **web_inspect** 在手机与桌面视口真跑一遍(关键交互用actions走一遍),**必修清零再交付**,并把截图路径告诉用户。(本机没有Chrome/Edge时没有这件工具——那就如实告诉用户页面未经浏览器验证,不要假装查过。)',"- 交付即可用:新建项目要能照着说明直接跑起来(依赖写进清单,启动/构建/测试命令你亲自跑通);用户一定会碰到的基本面——出错了怎么办、没有数据时显示什么、手机上能不能用、输入非法值会怎样——要做全,而不是只做字面要求的最小版本;超出需求的新功能先问再做。没验证过的不要说「完成」,说清验证了什么、还有什么没验证。",'- 需要出图(系统总体架构图/技术架构图/业务架构图、流程图与数据流、接口时序图、数据模型图、功能结构图、演进路线图)时一律用render_diagram:你只负责给出结构化spec,版式/对齐/配色/连线走向由引擎负责,产出矢量SVG(放大不失真)+ 高清PNG。**不要手写SVG/HTML/mermaid,也不要用run_python/matplotlib画架构图**——那些方式必然重叠错位。字段拿不准先调diagram_schema(type)。图要画到能直接进评审材料的程度:先读透源码/设计文档再画,节点名精确(用系统里真实的模块与服务名),每个节点补一句desc与技术栈tags,层/域的职责写进desc,必要时加legend与notes;不要只画几个空方块。画**专利附图**时置theme:"patent" 并给figure:{no,title}(黑白线条、直角、无阴影,符合附图形式要求)。',"- 写要过评审的报告(可研/项目建议书/立项/结题/技术方案/商业计划书)先载入report-craft,按「取证→测算→配图→成文→自审」走:project_facts从代码库数出规模事实(条条带来源);财务数字一律用fin_model算(NPV/IRR/回收期/敏感性,不心算);数据图用render_chart(甘特/投资构成/累计现金流/龙卷风/风险矩阵),不用run_python手写;插进文档的结构图加header:false,图表用 {#fig:x} 标注、正文写 @fig:x交叉引用;成稿后docx_inspect必修清零。**时态**:编制日期之后才是计划,已完成的写进现有工作基础;拿不到的人名/资质/合同写「[待补:…]」,不编造。","- 需要在线资料(文档/API说明)可用fetch_url;带明确目的使用,不要漫游。","- 环境与应用自动化:install_packages装Node.js包、setup_environment一键配置运行环境、install_plugin装VS Code/JetBrains插件、open_browser/open_path/open_app打开浏览器/文件管理器/任意应用。这些操作会先征询用户「是否继续」;被拒绝时不要重试,改为说明手动做法。","- 破坏性或不可逆操作(删除、强推、重置等)前先说明意图;写/执行类工具会请用户授权,被拒绝时据反馈调整方案。","- 需要用户选择方案或范围时,调用 ask_ui 的 single_choice / multi_choice,提供带取舍说明的 options,让用户在选择视图中确认;不要要求在聊天输入框中回复编号或方案名称。只有开放式补充信息才用 text。","- 不要把密钥、令牌等敏感值回显到输出或写进代码。","- 简洁直接,用中文回答;完成后用一两句话总结改了什么、如何验证的。","- 深度思考/推理过程(reasoning、思维链)默认也用简体中文书写,除非用户明确要求其他语言。"];export const VOLATILE_HEADING="# 当前状态(每轮可能变化)";export function stablePrefixOf(e){const n=String(e).indexOf("# 当前状态(每轮可能变化)");return n<0?String(e):String(e).slice(0,n)}export function buildSystemPrompt(s,a={}){const c=function(e){const n=n=>r(n,{cwd:e,stdio:["ignore","pipe","ignore"]}).toString().trim();try{const e=n("git rev-parse --abbrev-ref HEAD");let t="";try{t=n("git status --short")}catch{}return{branch:e,status:t}}catch{return null}}(s),l=function(t){for(const r of["HUICAI.md","AGENTS.md","CLAUDE.md"])try{const i=e.readFileSync(n.join(t,r),"utf8");if(i.trim())return{name:r,content:i.slice(0,8e3)}}catch{}return null}(s),p=function(){try{const r=e.readFileSync(n.join(t.homedir(),".huicai","HUICAI.md"),"utf8");return r.trim()?r.slice(0,4e3):null}catch{return null}}();let d="";if(a.persona&&a.persona.trim()?(d+="# 你的角色设定\n"+a.persona.trim()+"\n\n",d+="# 运行方式\n",d+="你现在运行在用户终端里,可通过工具在用户本机的项目里读写文件、搜索、执行shell命令来真正完成任务。请在保持上述角色设定的前提下遵循以下准则:\n"):(d+="你是huicai,一个运行在用户终端里的专业自治编码与自动化智能体。\n",d+="你能通过工具在用户本机的项目里读写文件、搜索、执行shell命令,从而真正完成任务,而不仅是给建议。\n\n",d+="# 工作准则\n"),d+=m.join("\n")+"\n\n",d+="# 运行环境\n",d+=`- 工作目录: ${s}\n`,d+=`- 操作系统: ${t.platform()} ${t.release()} (${t.arch()})\n`,p&&(d+=`\n# 全局用户说明(~/.huicai/HUICAI.md)\n${p}\n`),l&&(d+=`\n# 项目说明(${l.name})\n${l.content}\n`),d+=capabilityCatalog(s,a),!1!==a.memory){const e=i(s);e&&(d+=`\n# 长期记忆(此前会话经memory_write记录,跨会话生效)\n${e}\n`,d+="(发现新的、值得跨会话记住的稳定事实/偏好/结论时,用memory_write记录。)\n")}d+=`\n# 当前状态(每轮可能变化)\n- 日期: ${(new Date).toISOString().slice(0,10)}\n`;const _=function(n){try{return e.readdirSync(n,{withFileTypes:!0}).filter(e=>".git"!==e.name&&"node_modules"!==e.name).slice(0,60).map(e=>e.isDirectory()?e.name+"/":e.name).join(" ")}catch{return""}}(s);if(_&&(d+=`- 顶层条目: ${_}\n`),c&&(d+=`- Git分支: ${c.branch}\n`,c.status)){d+=`- 未提交改动:\n${c.status.split("\n").slice(0,30).map(e=>" "+e).join("\n")}\n`}if(!1!==a.repoMap)try{const e=o(s);e&&(d+=`\n## 项目地图(关键文件与主要符号;用search_code按概念检索相关实现)\n${e}\n`)}catch{}const g=function(){const e=process.env.HUICAI_IDE_FILE,n=process.env.HUICAI_IDE_SELECTION,t=process.env.HUICAI_IDE_DIAGNOSTICS;if(!e&&!n&&!t)return"";let r="";e&&(r+=`- 用户当前正在编辑的文件: ${e}\n`);if(n){r+="- 用户当前选中的代码片段:\n```\n"+(n.length>4e3?n.slice(0,4e3)+"\n…(选区已截断)":n)+"\n```\n"}if(t){r+="- 编辑器诊断(错误/警告,来自Problems面板):\n```\n"+(t.length>4e3?t.slice(0,4e3)+"\n…(诊断已截断)":t)+"\n```\n"}return r}();return g&&(d+="\n# 编辑器上下文(来自IDE集成)\n"+g),a.append&&String(a.append).trim()&&(d+="\n# 附加指示\n"+String(a.append).trim()+"\n"),d}export function capabilityCatalog(e,n={}){let t="",r=[];try{r=s(e)}catch{}r.length&&(t+="\n# 可召唤的专家团(本地,dispatch_member派发成员)\n",t+=r.slice(0,20).map(e=>{const n=String(e.when||e.description||"").replace(/\s+/g," ").slice(0,100),t=(e.members||[]).map(e=>e.callName||e.id).filter(Boolean).slice(0,6).join("、");return`- ${e.name}${n?":"+n:""}${t?`(成员:${t})`:""}`}).join("\n")+"\n",t+="跨角色、要分工协作的复杂任务,直接用dispatch_member把子任务派给对应成员,不必等用户点名。\n");let i=[];try{i=a(e)}catch{}return i.length&&(t+="\n# 可调用的子智能体人格(本地,dispatch_agent指定agent参数)\n",t+=i.slice(0,20).map(e=>`- ${e.name}${e.description?":"+String(e.description).replace(/\s+/g," ").slice(0,100):""}`).join("\n")+"\n"),Array.isArray(n.experts)&&n.experts.length&&(t+="\n# 可咨询的平台专家(consult_expert / consult_experts)\n",t+=n.experts.slice(0,20).map(e=>`- [${e.type||""}]${e.name}${e.description?":"+String(e.description).slice(0,80):""}`).join("\n")+"\n"),n.mcpCatalog&&String(n.mcpCatalog).trim()&&(t+="\n# 外部能力连接器(MCP)\n"+String(n.mcpCatalog).trim()+"\n需要当前工具覆盖不到的能力时,先list_mcp_servers看有什么,再connect_mcp连上——连接后新工具下一轮即可调用,无需重开会话。\n"),!1!==n.computerUse&&l(n.config||{})&&(t+=c+"\n"),!1!==n.automation&&(t+="\n# 自动化与提醒(主动使用,不必等用户明说)\n- 用户表达出「以后每天/每周/每隔多久…」这类周期性意图时,直接用create_automation把它落成定时任务,再向用户复述频率与内容确认;要改/停/删已有任务先list_automations定位。\n- 当前任务需要「等一会儿再做」(等构建完成、等对方回复、明早再跑)时,用schedule_followup给自己排后续,不要让用户干等,也不要假装已经完成。\n- 长任务跑完、或遇到必须由人拍板的岔路时,用send_notification把用户叫回来(用户可能已经离开电脑)。\n- 预计要跑很久的任务,开始前set_wake_lock(on=true) 防止电脑休眠导致半途中断,结束后释放。\n"),t}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import e from"node:fs";import t from"node:path";import{createHash as n}from"node:crypto";const s=/\.(?:[cm]?[jt]sx?|vue|svelte|py|java|kt|kts|go|rs|cs|php|c|h|cpp|hpp|swift|rb|scala|html?|css|scss|sql|sh|ps1)$/i,r=["unit","startup","smoke","behavior","client"],i=new Set(["read_file","read_document","read_any","view_image"]);function a(t){try{return n("sha256").update(e.readFileSync(t)).digest("hex")}catch{return null}}export class EngineeringRun{constructor(n){this.cwd=t.resolve(n),this.realCwd=e.realpathSync(this.cwd),this.reads=new Map,this.readLines=new Map,this.readDocuments=new Map,this.browsed=!1,this.plan=null,this.baseline=new Map,this.results=new Map,this.changed=!1,this.revision=0,this.blocked=""}resolve(n){if("string"!=typeof n||!n.trim())throw new Error("必须提供工作区相对路径");const s=t.resolve(this.cwd,n);if(!(e=>e===this.cwd||e.startsWith(this.cwd+t.sep))(s))throw new Error("路径越出工作目录:"+n);let r=s;for(;!e.existsSync(r)&&r!==this.cwd;)r=t.dirname(r);const i=e.realpathSync(r);if(i!==this.realCwd&&!i.startsWith(this.realCwd+t.sep))throw new Error("符号链接越出工作目录:"+n);return t.resolve(i,t.relative(r,s))}observe(t){const n=String(t.result??"");if(!(t.denied||t.settled||/^(?:工具执行失败|读取失败|命令启动失败|路径不存在|文件不存在|【超时|【已中断)/.test(n))&&(["list_files","glob","search_code","grep"].includes(t.name)&&(this.browsed=!0),i.has(t.name)&&t.args?.path&&n.trim()))try{const s=this.resolve(t.args.path),r=a(s);if("read_file"===t.name){const t=e.readFileSync(s,"utf8");if(t.includes("\0"))return;const i=t.split(/\r?\n/);""===i.at(-1)&&i.pop();let a=this.readLines.get(s);a?.hash!==r&&(a={hash:r,lines:new Set},this.readLines.set(s,a),this.reads.delete(s));for(const e of n.matchAll(/^\s*(\d+)→(.*)$/gm)){const t=Number(e[1])-1;t>=0&&t<i.length&&e[2]===i[t]&&a.lines.add(t)}if(a.lines.size!==i.length)return}if("read_document"===t.name){const e=/\n(?:…)?(全文共 (\d+) 字符,本段为第 (\d+)–(\d+) 字符[^\n]*)$/.exec(n);if(!e)return;const[t,i,a]=e.slice(1).map(Number),o=n.slice(n.indexOf("\n")+1,e.index);if(a<i||a>t||o.length!==a-i)return;let c=this.readDocuments.get(s);c?.hash===r&&c?.total===t||(c={hash:r,total:t,ranges:[]},this.readDocuments.set(s,c),this.reads.delete(s)),c.ranges.push([i,a]),c.ranges.sort((e,t)=>e[0]-t[0]);let d=0;for(const[e,t]of c.ranges){if(e>d)break;d=Math.max(d,t)}if(d!==t)return}r&&this.reads.set(s,r)}catch{}}prepare(n){if(!this.browsed)throw new Error("先用 list_files/glob/grep/search_code 梳理项目结构和调用位置");for(const e of["summary","callChain","conventions","ux"])if("string"!=typeof n[e]||!n[e].trim())throw new Error("缺少分析:"+e);if(!Array.isArray(n.references)||!Array.isArray(n.targets)||!n.targets.length)throw new Error("references 必须列出已读材料,targets 必须列出拟改文件");const s=n.references.map(e=>this.resolve(e)),i=n.targets.map(e=>this.resolve(e)),o=new Set(["AGENTS.md","README.md","CONTRIBUTING.md",".editorconfig","package.json","pom.xml","pyproject.toml"].map(e=>t.join(this.realCwd,e)).filter(t=>e.existsSync(t)&&e.statSync(t).isFile()));for(const n of i)for(let s=t.dirname(n);s.startsWith(this.realCwd+t.sep);s=t.dirname(s))for(const n of["AGENTS.md","README.md"]){const r=t.join(s,n);e.existsSync(r)&&e.statSync(r).isFile()&&o.add(r)}s.push(...[...o].filter(e=>!s.includes(e)));for(const n of[...new Set([...s,...i.filter(t=>e.existsSync(t))])])if(!this.reads.has(n)||this.reads.get(n)!==a(n))throw new Error("请先读取或重新读取:"+t.relative(this.cwd,n));if("boolean"!=typeof n.client)throw new Error("必须声明是否涉及客户端 client");let c={};try{c=JSON.parse(e.readFileSync(t.join(this.realCwd,"package.json"),"utf8"))}catch{}if((i.some(e=>/\.(?:vue|svelte|jsx|tsx|html?|css|scss|swift)$/i.test(e))||!!(c.bin||c.engines?.vscode||c.dependencies?.electron||c.devDependencies?.electron))&&!n.client)throw new Error("当前项目或改动涉及客户端,必须 client=true 并提供 client 验证");const d=n.checks;if(!Array.isArray(d)||d.length>40)throw new Error("checks 必须是 1–40 项验证计划");const l=new Set;for(const e of d){if(!e||!/^[\w-]{1,64}$/.test(e.id||"")||l.has(e.id))throw new Error("验证 id 必须唯一且只含字母、数字、下划线或横线");if(l.add(e.id),!r.includes(e.kind)||"string"!=typeof e.expectation||!e.expectation.trim())throw new Error("每项验证须有 kind 和可断言的 expectation");if("unit"!==e.kind&&("string"!=typeof e.command||!e.command.trim()))throw new Error("运行验证缺少 command:"+e.id);"unit"===e.kind&&this.resolve(e.path||".")}for(const e of r.filter(e=>"client"!==e||n.client))if(!d.some(t=>t.kind===e))throw new Error("缺少验证类别:"+e);return this.refresh(),this.plan=structuredClone({...n,references:s,targets:i}),this.baseline=new Map([...new Set([...s,...i])].map(e=>[e,a(e)])),this.results.clear(),this.blocked="",this.status()}refresh(){let e=!1;for(const[t,n]of this.baseline){const s=a(t);s!==n&&(e=!0,this.baseline.set(t,s))}return e&&(this.changed=!0,this.revision++,this.results.clear()),e}guard(e){const t=String(e.args?.path||""),n=["write_file","edit_file"].includes(e.name)&&(s.test(t)||/(?:^|[/\\])(?:package(?:-lock)?\.json|tsconfig[^/\\]*\.json|pom\.xml|Dockerfile|Makefile)$|\.(?:ya?ml|toml)$/i.test(t)),r="code_inspect"===e.name&&!0===e.args?.fix;if(!n&&!r)return null;if(!this.plan)return"编码前分析尚未完成:先通读相关源码、调用方、项目规范、文档及需求附件,再调用 engineering_plan 提交分析与验证计划。";if(r)try{if(!this.plan.targets.includes(this.resolve(e.args.path||".")))return"自动修复也必须限定为 engineering_plan.targets 中的具体文件,目录请逐文件检查后修复。"}catch(e){return e.message}if(n)try{const t=this.resolve(e.args.path);if(!this.plan.targets.includes(t))return"改动超出分析范围:先读取关联上下文并更新 engineering_plan.targets:"+e.args.path}catch(e){return e.message}return null}status(){this.refresh();const e=(this.plan?.checks||[]).map(e=>({id:e.id,kind:e.kind,expectation:e.expectation,...this.results.get(e.id)||{status:"pending"}}));return{phase:this.blocked||!this.plan?"analysis":e.every(e=>"passed"===e.status)?"verified":"verification",changed:this.changed,blocked:this.blocked,checks:e}}pendingNote(){const e=this.status();if(e.blocked)return"工程改动尚未完成:"+e.blocked+" 请完成分析后继续;不能将未执行的改动报告为已完成。";if(!e.changed)return"";const t=e.checks.filter(e=>"passed"!==e.status);return t.length?"工程验证尚未完成:"+t.map(e=>`${e.id}(${e.kind}:${e.status})`).join("、")+"。请用 engineering_verify 逐项真实执行;单测、启动、冒烟、功能与客户端验证不可互相替代。环境阻塞须如实报告,不得宣称全部完成。":""}}export const engineeringStage={name:"engineering",before(e,t){const n=t.engineering?.guard(e);n&&(e.denied=n,t.engineering.blocked=n)},after(e,t){t.engineering?.observe(e),t.engineering?.refresh()}};export function makeEngineeringTools(){return[{readOnly:!0,spec:{type:"function",function:{name:"engineering_plan",description:"编码前提交已读上下文、调用链、项目约定、用户体验考虑和验证计划;action=status 查询各项进展。先读材料再提交,已有目标文件必须实际读过。相关表格/图片/文档也须解析;摘要不代替完整上下文。",parameters:{type:"object",properties:{action:{type:"string",enum:["plan","status"]},summary:{type:"string",description:"需求、影响范围、已读材料与分析结论"},callChain:{type:"string",description:"入口→调用方→实现→依赖→测试,跨端同步点"},conventions:{type:"string",description:"项目规范与文档约定,缺失材料明确说明"},ux:{type:"string",description:"提前考虑加载/空态/错误/重试/取消/键盘/响应式/性能;不涉及时说明理由"},references:{type:"array",items:{type:"string"},description:"已通过读取工具读过的源码、文档、规范与附件路径"},targets:{type:"array",items:{type:"string"},description:"拟创建或修改的文件,含测试与配置"},client:{type:"boolean",description:"涉及任何客户端功能必须 true,即使只改共享核心"},checks:{type:"array",items:{type:"object",properties:{id:{type:"string"},kind:{type:"string",enum:r},expectation:{type:"string",description:"具体验收断言;不得只检查退出码、echo、--help 或构建成功"},command:{type:"string",description:"startup/smoke/behavior/client 的真实验证命令,须失败非零退出、清理自己的进程;客户端启动真实宿主或模拟器并交互断言"},path:{type:"string",description:"unit 的项目目录"},framework:{type:"string"},filter:{type:"string"}},required:["id","kind","expectation"]}}},required:["action"]}}},runWithEnv:async(e,t)=>{if(!t.engineering)return"当前宿主未建立工程流程台账";try{if(!["plan","status"].includes(e.action))throw new Error("action 必须是 plan 或 status");const n="status"===e.action?t.engineering.status():t.engineering.prepare(e);return"plan"===e.action&&t.info?.("编码分析已登记;后续验证按单测、启动、冒烟、功能与客户端分别记录。"),JSON.stringify(n,null,2)}catch(e){return"工程计划未就绪:"+e.message}}},{readOnly:!1,timeoutMs:66e4,spec:{type:"function",function:{name:"engineering_verify",description:"执行 engineering_plan 中一项真实验证并记录命令、耗时与结果。unit 走 run_tests,其余走 run_bash,沿用各工具的权限、钩子、超时和取消能力。最后一次改动之前的结果不计入通过。无法执行就报告阻塞,禁止自报成功。",parameters:{type:"object",properties:{id:{type:"string"},timeout:{type:"number",description:"毫秒,最多600000"}},required:["id"]}}},runWithEnv:async(e,t,n)=>{const s=t.engineering,r=s?.plan?.checks.find(t=>t.id===e.id);if(!r)return"请先用 engineering_plan 登记该验证项";s.refresh();const i=s.revision,a=Date.now(),o=Math.max(1,Math.min(Number(e.timeout)||12e4,6e5)),{makeDispatch:c}=await import("./toolpipe.js");let d=null,l=null;const h=c({...t,signal:n,onTestResult:e=>{d=e},onStart:e=>{l={...e.args},t.onStart?.(e)}}),p="unit"===r.kind,u=await h(p?"run_tests":"run_bash",p?{path:r.path||".",framework:r.framework,filter:r.filter,timeout:o}:{command:r.command,timeout:o}),g=String(u.result||u.denied||"未返回结果");s.refresh();const f=s.revision!==i,m=u.ok&&!n?.aborted&&!f&&(p?0===d?.code&&d?.stats?.passed>0&&0===d?.stats?.failed:/^退出码 0\n/.test(g)),y={status:f?"stale":m?"passed":"failed",command:p?d?.command||`run_tests(${JSON.stringify(l||{path:r.path||"."})})`:l?.command||r.command,durationMs:Date.now()-a,at:(new Date).toISOString(),output:g.slice(-12e3)};return s.results.set(r.id,y),t.info?.(`工程验证 ${r.id}:${"passed"===y.status?"通过":"未通过"}(${y.durationMs}ms)`),JSON.stringify({id:r.id,...y},null,2)}}]}
|
package/src/executors.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"node:fs";import t from"node:path";import{StringDecoder as n}from"node:string_decoder";import{spawn as r,execFileSync as i}from"node:child_process";import*as o from"./ide.js";import{extractDocument as s}from"./api.js";import{pythonEnvForBash as l}from"./pyruntime.js";import*as c from"./jobs.js";import{confineArgv as a,sandboxNote as f}from"./sandbox.js";import{cleanForModel as u}from"./terminal.js";import{isWebSearchOn as p}from"./config.js";import{isSearchEngineQueryUrl as h,FETCH_SEARCH_BLOCKED_MESSAGE as d}from"./websearch.js";const g=16777216,m=536870912,$=2e3,w=2e3,y=3e4,b=new Set([".git","node_modules","dist","build","target","out",".next",".nuxt",".venv","venv","__pycache__",".idea",".gradle",".mvn","coverage",".cache","vendor","bower_components","Pods","DerivedData",".turbo",".parcel-cache",".terraform",".pytest_cache",".mypy_cache",".tox",".angular",".svelte-kit",".output",".serverless",".svn",".hg"]);function S(e,n,r){if(!e||"string"!=typeof e)throw new Error("工作目录未设置:请在项目目录中运行(VS Code中需先「打开文件夹」再对话)");if(!n||"string"!=typeof n)throw new Error("缺少路径path(工具参数里必须提供path字段)");const i=t.resolve(e,n);if(r){const r=t.relative(e,i);if(""===r||r.startsWith("..")||t.isAbsolute(r))throw new Error(`路径超出项目目录(写操作仅限当前项目内):${n}`)}return i}function _(e,n){const r=t.relative(e,n);return""===r?".":r}function x(e){const t=Math.min(e.length,4096);for(let n=0;n<t;n++)if(0===e[n])return!0;return!1}function*v(n,r=0,i=14){let o;try{o=e.readdirSync(n,{withFileTypes:!0})}catch{return}for(const e of o){if(b.has(e.name))continue;const o=t.join(n,e.name);e.isDirectory()?r<i&&(yield*v(o,r+1,i)):e.isFile()&&(yield o)}}function E(e){let t="";for(let n=0;n<e.length;n++){const r=e[n];"*"===r?"*"===e[n+1]?(t+=".*",n++,"/"===e[n+1]&&n++):t+="[^/]*":"?"===r?t+="[^/]":"\\^$+.()|{}[]".includes(r)?t+="\\"+r:t+=r}return new RegExp("^"+t+"$")}export function readFile(t,r){const i=S(t,r.path,!1),o=e.statSync(i);if(o.isDirectory())throw new Error(`是目录而非文件:${r.path}`);if(o.size>4194304)return function(t,r,i,o){const s=Buffer.allocUnsafe(4096),l=e.openSync(r,"r"),c=e.readSync(l,s,0,4096,0);if(e.closeSync(l),x(s.subarray(0,c)))return`(二进制文件,${o.size} 字节,未展示内容)`;const a=Math.max(0,(Number.isFinite(i.offset)?i.offset:1)-1),f=Number.isFinite(i.limit)&&i.limit>0?i.limit:$,{lines:u,lastLine:p,hitEof:h,truncatedScan:d}=function(t,r,i,o,s){const l=r+i,c=e=>e.length>o?e.slice(0,o)+"…(行过长已截断)":e,a=e.openSync(t,"r");try{const t=65536,i=Buffer.allocUnsafe(t),o=new n("utf8"),f=[];let u="",p=0,h=0,d=!1,g=!1,m=!1;for(;!m;){const n=e.readSync(a,i,0,t,null);if(0===n){d=!0;break}h+=n;let $,w=u+o.write(i.subarray(0,n)),y=0;for(;-1!==($=w.indexOf("\n",y));)if(p>=r&&p<l&&f.push(c(w.slice(y,$))),p++,y=$+1,p>=l){m=!0;break}if(u=m?"":w.slice(y),!m&&h>=s){g=!0;break}}return d&&(u+=o.end(),u.length&&p>=r&&p<l?(f.push(c(u)),p++):u.length&&p++),{lines:f,lastLine:p,hitEof:d,truncatedScan:g}}finally{e.closeSync(a)}}(r,a,f,w,m),g=(o.size/1024/1024).toFixed(1);if(!u.length)return d?`(大文件 ${g}MB:起始行 ${a+1} 超出已扫描的前 ${Math.round(512)}MB;请改用grep精确定位)`:`(起始行 ${a+1} 超出文件总行数 ${p})`;const y=a+u.length,b=String(y).length,S=u.map((e,t)=>`${String(a+t+1).padStart(b)}→${e}`).join("\n");return S+`\n…(大文件 ${g}MB,${h?`共 ${p} 行`:`${p}+ 行(大文件未全扫)`},本次第 ${a+1}-${y} 行;用offset/limit翻页,或用grep精确定位)`}(0,i,r,o);const s=e.readFileSync(i);if(x(s))return`(二进制文件,${o.size} 字节,未展示内容)`;const l=s.toString("utf8");if(""===l)return"(空文件)";const c=l.split("\n");c.length>1&&""===c[c.length-1]&&c.pop();const a=c.length,f=Math.max(0,(Number.isFinite(r.offset)?r.offset:1)-1),u=Number.isFinite(r.limit)&&r.limit>0?r.limit:$,p=Math.min(a,f+u);if(f>=a)return`(起始行 ${f+1} 超出文件总行数 ${a})`;const h=String(p).length,d=[];for(let e=f;e<p;e++){let t=c[e];t.length>w&&(t=t.slice(0,w)+"…(行过长已截断)"),d.push(`${String(e+1).padStart(h)}→${t}`)}let g=d.join("\n");return(f>0||p<a)&&(g+=`\n…(文件共 ${a} 行,本次显示第 ${f+1}-${p} 行,可用offset/limit继续读取)`),g}const k=20971520;export async function readDocument(n,r,i){const o=S(n,r.path,!1),l=e.statSync(o);if(l.isDirectory())throw new Error(`是目录而非文件:${r.path}`);if(l.size>k)throw new Error(`文档超过 ${Math.round(20)}MB解析上限`);if(!i||!i.baseUrl||!i.apiKey)throw new Error("解析文档需登录平台(先执行huicai login)");const c=Number.isFinite(Number(r.offset))&&Number(r.offset)>0?Math.floor(Number(r.offset)):0,a=Number.isFinite(Number(r.max_chars))&&Number(r.max_chars)>0?Math.min(18e3,Math.max(1e3,Math.floor(Number(r.max_chars)))):16e3,f=await s(i.baseUrl,i.apiKey,{filename:t.basename(o),data:e.readFileSync(o).toString("base64"),maxChars:a,offset:c}),u=f.chars??0,p=f.text||"";if(!p)return`(第 ${c} 字符起已无内容;全文共 ${u} 字符)`;const h=c+p.length,d=f.truncated?`\n…(全文共 ${u} 字符,本段为第 ${c}–${h} 字符;继续调用read_document并传offset=${h} 读取后续)`:`\n(全文共 ${u} 字符,本段为第 ${c}–${h} 字符,已到结尾)`,g=f.pages?`${f.pages}页·`:"";return`【文档 ${_(n,o)}(${g}共 ${u} 字符)】\n${p}${d}`}export function looksLikeNumberedDump(e){const t=String(e||"").split("\n").filter(e=>e.trim());if(t.length<3)return!1;return t.filter(e=>/^\s*\d+→/.test(e)).length/t.length>=.8}const L="拒绝写入:内容看起来是read_file的**带行号输出**(每行以「行号→」开头),不是文件原文。原样写回会把行号写进文件,把它毁掉。\n改法:① 局部替换用edit_file(old_string→new_string,不必读全文);② 确需重写全文,先去掉前缀:content.replace(/^\\s*\\d+→/gm, '')。";export async function writeFile(n,r){const i=S(n,r.path,!0),s=null==r.content?"":String(r.content);if(looksLikeNumberedDump(s))throw new Error(L);if(!0===r.append){let r="";try{r=e.readFileSync(i,"utf8")}catch{}const l=r+s;if(Buffer.byteLength(l)>g)throw new Error(`追加后文件将超过 ${Math.round(16)}MB上限,请拆分为多个文件`);if(o.isActive()){const e=await o.proposeWrite(i,l,n);if(e.decided)return e.applied?`已追加到 ${_(n,i)}(经IDE审阅应用,本段 ${Buffer.byteLength(s)} 字节)`:`用户在IDE中拒绝了对 ${_(n,i)} 的追加${e.reason?":"+e.reason:""}`}return e.mkdirSync(t.dirname(i),{recursive:!0}),e.writeFileSync(i,l,"utf8"),`已追加到 ${_(n,i)}(本段 ${Buffer.byteLength(s)} 字节,文件现 ${Buffer.byteLength(l)} 字节)`+(o.isActive()?"(IDE未响应,已直接写入)":"")}if(o.isActive()){const e=await o.proposeWrite(i,s,n);if(e.decided)return e.applied?`已写入 ${_(n,i)}(经IDE审阅应用)`:`用户在IDE中拒绝了对 ${_(n,i)} 的修改${e.reason?":"+e.reason:""}`}return e.mkdirSync(t.dirname(i),{recursive:!0}),e.writeFileSync(i,s,"utf8"),`已写入 ${_(n,i)}(${Buffer.byteLength(s)} 字节)`+(o.isActive()?"(IDE未响应,已直接写入)":"")}export async function editFile(t,n){const r=S(t,n.path,!0),i=null==n.old_string?"":String(n.old_string),s=null==n.new_string?"":String(n.new_string);if(""===i)throw new Error("edit_file需要非空old_string");if(looksLikeNumberedDump(s))throw new Error(L);if(!e.existsSync(r))throw new Error(`目标文件不存在:${n.path}。请先用read_file / list_files确认路径是否正确(可能工作目录不对,或文件已被移动/删除);不要改用write_file新建一个同名空文件替代——如确需新建,请先向用户说明再创建。`);const l=e.readFileSync(r,"utf8"),c=l.split(i).length-1;if(0===c)throw new Error("未找到要替换的内容(old_string未出现在文件中)。注意:old_string必须是文件原文,不能包含read_file显示的「行号→」前缀");if(c>1&&!n.replace_all)throw new Error(`old_string在文件中出现 ${c} 处,不唯一;请补充上下文使其唯一,或设replace_all=true`);const a=n.replace_all?l.split(i).join(s):l.replace(i,s),f=n.replace_all?c:1;if(o.isActive()){const e=await o.proposeWrite(r,a,t);if(e.decided)return e.applied?`已编辑 ${_(t,r)}(经IDE审阅应用,替换 ${f} 处)`:`用户在IDE中拒绝了对 ${_(t,r)} 的修改${e.reason?":"+e.reason:""}`}return e.writeFileSync(r,a,"utf8"),`已编辑 ${_(t,r)},替换 ${f} 处`+(o.isActive()?"(IDE未响应,已直接写入)":"")}export function listFiles(n,r){const i=S(n,r.path||".",!1),o=[];let s=0;const l=(n,r)=>{if(s>=400)return;let c;try{c=e.readdirSync(n,{withFileTypes:!0})}catch{return}c.sort((e,t)=>e.name.localeCompare(t.name));for(const e of c){if(s>=400)break;if(b.has(e.name))continue;const c=t.join(n,e.name),a=t.relative(i,c);e.isDirectory()?(o.push(a+"/"),s++,r<6&&l(c,r+1)):(o.push(a),s++)}};return l(i,0),0===o.length?"(空目录)":o.join("\n")+(s>=400?"\n…(条目已截断)":"")}export function listWorkspaceFiles(e,n=2e3){const r=[];for(const i of v(e))if(r.push(t.relative(e,i).split(t.sep).join("/")),r.length>=n)break;return r}export function glob(e,n){if(!n.pattern)throw new Error("缺少pattern");const r=E(n.pattern),i=[];for(const n of v(e)){const o=t.relative(e,n).split(t.sep).join("/");if(r.test(o)&&(i.push(o),i.length>=300))break}return 0===i.length?"(无匹配文件)":i.join("\n")}let j=null;export function grep(e,t){if(!t.pattern)throw new Error("缺少pattern");const n=function(){if(null!==j)return j;try{i("rg",["--version"],{stdio:"ignore"}),j=!0}catch{j=!1}return j}()?function(e,t){const n=Number.isFinite(t.max_results)&&t.max_results>0?Math.min(t.max_results,500):200,r=t.path?String(t.path):".",o=[...b].flatMap(e=>["-g",`!**/${e}/**`,"-g",`!${e}/**`]),s=["--color=never","--no-messages","--max-columns","2000"];t.ignore_case&&s.push("-i");t.glob&&s.push("-g",String(t.glob));s.push(...o),t.files_only?s.push("-l"):s.push("-n","--no-heading");let l;s.push("-e",String(t.pattern),"--",r);try{l=i("rg",s,{cwd:e,encoding:"utf8",maxBuffer:32<<20,stdio:["ignore","pipe","pipe"]})}catch(e){return e&&1===e.status?"(无匹配)":null}const c=e=>e.replace(/\\/g,"/").replace(/^\.\//,""),a=l.split("\n").filter(Boolean);if(t.files_only)return a.length?a.slice(0,n).map(c).join("\n"):"(无匹配)";const f=[];for(const e of a){const t=/^(.*?):(\d+):(.*)$/.exec(e);if(t&&(f.push(`${c(t[1])}:${t[2]}: ${t[3].slice(0,240)}`),f.length>=n))break}return f.length?f.join("\n")+(f.length>=n?"\n…(结果已截断)":""):"(无匹配)"}(e,t):null;return null!==n?n:grepJs(e,t)}export function grepJs(n,r){if(!r.pattern)throw new Error("缺少pattern");let i;try{i=new RegExp(r.pattern,r.ignore_case?"i":void 0)}catch(e){throw new Error(`无效的正则:${e.message}`)}const o=r.glob?E(r.glob):null,s=S(n,r.path||".",!1),l=Number.isFinite(r.max_results)&&r.max_results>0?Math.min(r.max_results,500):200,c=[],a=[];for(const f of v(s)){if(c.length>=l)break;const s=t.relative(n,f).split(t.sep).join("/");if(o&&!o.test(s))continue;let u;try{u=e.statSync(f)}catch{continue}let p=!1;if(u.size<=262144){let t;try{t=e.readFileSync(f)}catch{continue}if(x(t))continue;const n=t.toString("utf8").split("\n");for(let e=0;e<n.length;e++)if(i.test(n[e])){if(p=!0,r.files_only)break;if(c.push(`${s}:${e+1}: ${n[e].slice(0,240)}`),c.length>=l)break}}else p=F(f,i,s,!!r.files_only,c,l);if(p&&(a.push(s),r.files_only&&a.length>=l))break}return r.files_only?a.length?a.join("\n"):"(无匹配)":0===c.length?"(无匹配)":c.join("\n")+(c.length>=l?"\n…(结果已截断)":"")}function F(t,r,i,o,s,l){let c;try{c=e.openSync(t,"r")}catch{return!1}try{const t=65536,a=Buffer.allocUnsafe(t),f=new n("utf8");let u="",p=0,h=0,d=!1,g=!0,m=!1;for(;;){const n=e.readSync(c,a,0,t,null);if(0===n)break;if(g){if(x(a.subarray(0,n)))return!1;g=!1}h+=n;const $=u+f.write(a.subarray(0,n));let w,y=0;for(;-1!==(w=$.indexOf("\n",y));){p++;const e=$.slice(y,w);if(r.test(e)){if(d=!0,o)return!0;if(s.push(`${i}:${p}: ${e.slice(0,240)}`),s.length>=l)return!0}y=w+1}if(u=$.slice(y),h>=16777216){m=!0;break}}return m||(u+=f.end(),u.length&&(p++,r.test(u)&&(d=!0,!o&&s.length<l&&s.push(`${i}:${p}: ${u.slice(0,240)}`)))),d}finally{e.closeSync(c)}}function M(e,t,i){const o=String(t.command),s="win32"===process.platform,a={...process.env,...l()},f=s?[process.env.COMSPEC||"cmd.exe","/d","/s","/c",o]:[process.env.SHELL||"/bin/sh","-c",o],{argv:u,sandbox:p,note:h}=I(f,i,e),d=r(u[0],u.slice(1),s?{cwd:e,env:a,windowsVerbatimArguments:!0}:{cwd:e,env:a,detached:!0}),g=c.start({kind:"bash",label:o.replace(/\s+/g," ").slice(0,120),sessionId:i?.sessionId||"",cwd:e,kill:()=>(()=>{try{!s&&d.pid?process.kill(-d.pid,"SIGKILL"):d.kill("SIGKILL")}catch{try{d.kill("SIGKILL")}catch{}}})(),meta:{pid:d.pid}}),m=new n("utf8"),$=new n("utf8");return d.stdout.on("data",e=>g.append(m.write(e))),d.stderr.on("data",e=>g.append($.write(e))),d.on("error",e=>{g.append(`\n命令启动失败:${e.message}\n`),g.settle("failed","启动失败")}),d.on("close",(e,t)=>{g.append(m.end()),g.append($.end()),t?g.settle("killed",`被信号 ${t} 终止`):g.settle(0===e?"done":"failed",`退出码 ${e}`)}),Promise.resolve(`已在后台启动,作业id:${g.id}\n命令:${o.replace(/\s+/g," ").slice(0,200)}\n用job_output读它的输出(默认只回新增;加wait=true可等它结束),job_kill可终止它。不要为了等它而空转或反复轮询——它结束时会主动通知你。`+h)}const I=(e,t,n)=>a(e,t,n);export function runBash(e,t,i,o){const s=null==t.command?"":String(t.command);if(""===s.trim())return Promise.resolve("(未提供命令)");if(t.run_in_background)return M(e,t,i);const c=Number.isFinite(t.timeout)?Math.min(t.timeout,6e5):12e4,a="win32"===process.platform;return new Promise(t=>{let p="",h=!1,d=!1;const g={...process.env,...l()},m=a?[process.env.COMSPEC||"cmd.exe","/d","/s","/c",s]:[process.env.SHELL||"/bin/sh","-c",s],{argv:$,sandbox:w,note:b}=I(m,i,e),S=r($[0],$.slice(1),a?{cwd:e,env:g,windowsVerbatimArguments:!0}:{cwd:e,env:g,detached:!0}),_=()=>{try{!a&&S.pid?process.kill(-S.pid,"SIGKILL"):S.kill("SIGKILL")}catch{try{S.kill("SIGKILL")}catch{}}},x=setTimeout(()=>{h=!0,_()},c),v=()=>{d=!0,h=!0,_()};o&&(o.aborted?v():o.addEventListener("abort",v,{once:!0}));const E=()=>{clearTimeout(x),o&&o.removeEventListener("abort",v)},k=new n("utf8"),L=new n("utf8"),j=e=>{p.length<y&&(p+=e)};S.stdout.on("data",e=>j(k.write(e))),S.stderr.on("data",e=>j(L.write(e))),S.on("error",e=>{E(),t(`命令启动失败:${e.message}`)}),S.on("close",e=>{E(),j(k.end()),j(L.end()),p=u(p);let n=p.length>y?p.slice(0,y)+"\n…(输出已截断)":p;""===n.trim()&&(n="(无输出)");const r=d?"【已中断】":h?"【超时被终止】":`退出码 ${e}`,i=0===e?"":((e,t)=>f(e,t))(w,n);t(`${r}\n${n}${b}${i}`)})})}export async function fetchUrl(e,t){const n=String(t.url||"").trim();if(!/^https?:\/\//i.test(n))throw new Error("仅支持http/https URL");const r=!p();if(r&&h(n))throw new Error(d);let i;try{i=await fetch(n,{redirect:"follow",signal:AbortSignal.timeout(Number.isFinite(t.timeout)?Math.min(t.timeout,12e4):3e4),headers:{"User-Agent":"huicai-cli",Accept:"text/html,application/json;q=0.9,text/plain;q=0.8,*/*;q=0.5"}})}catch(e){throw new Error(`请求失败:${e?.cause?.message||e.message}`)}if(r&&i.url&&i.url!==n&&h(i.url)){try{await(i.body?.cancel())}catch{}throw new Error(d)}const o=await async function(e,t){if(!e.body)return Buffer.alloc(0);const n=e.body.getReader(),r=[];let i=0;for(;;){const{done:e,value:o}=await n.read();if(e)break;if(r.push(o),i+=o.length,i>=t){n.cancel().catch(()=>{});break}}return Buffer.concat(r.map(e=>Buffer.from(e)),Math.min(i,t))}(i,2097152),s=(i.headers.get("content-type")||"").toLowerCase();if(x(o)&&!/text|json|xml|html/.test(s))return`HTTP ${i.status} · ${s||"未知类型"} · ${o.length} 字节(二进制内容未展示)`;let l=function(e,t){let n=(/charset=([\w-]+)/i.exec(t||"")||[])[1];if(!n){const t=e.slice(0,4096).toString("latin1");n=(/<meta[^>]+charset\s*=\s*["']?([\w-]+)/i.exec(t)||[])[1]}const r=String(n||"utf-8").toLowerCase();if("utf-8"===r||"utf8"===r)return e.toString("utf8");try{return new TextDecoder(r).decode(e)}catch{return e.toString("utf8")}}(o,s),c="",a="";if(s.includes("html")||/^\s*<(!doctype|html)/i.test(l)){if(c=((/<title[^>]*>([\s\S]*?)<\/title>/i.exec(l)||[])[1]||"").replace(/\s+/g," ").trim().slice(0,120),t.links){const e=function(e,t,n=60){const r=[],i=new Set,o=/<a\b[^>]*href\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))[^>]*>([\s\S]*?)<\/a>/gi;let s;for(;(s=o.exec(e))&&r.length<n;){const e=(s[1]??s[2]??s[3]??"").trim();if(!e||/^(javascript:|mailto:|tel:|#)/i.test(e))continue;let n;try{n=new URL(e,t).href}catch{continue}if(!/^https?:/i.test(n)||i.has(n))continue;i.add(n);const o=s[4].replace(/<[^>]+>/g,"").replace(/\s+/g," ").trim().slice(0,80);r.push(`- ${o||"(无文本)"} → ${n}`)}return r}(l,i.url||n);e.length&&(a=`\n\n—— 页面链接(${e.length} 条)——\n`+e.join("\n"))}l=function(e){let t=String(e);return t=t.replace(/<(script|style|noscript|svg|head)[\s\S]*?<\/\1>/gi,""),t=t.replace(/<!--[\s\S]*?-->/g,""),t=t.replace(/<br\s*\/?>/gi,"\n"),t=t.replace(/<\/(p|div|li|tr|h[1-6]|section|article|blockquote|pre|ul|ol|table)>/gi,"\n"),t=t.replace(/<li[^>]*>/gi,"- "),t=t.replace(/<[^>]+>/g,""),t=t.replace(/ /g," ").replace(/</g,"<").replace(/>/g,">").replace(/"/g,'"').replace(/'|'/g,"'").replace(/&/g,"&"),t=t.replace(/[ \t]+\n/g,"\n").replace(/\n{3,}/g,"\n\n"),t}(l)}l=l.trim(),l.length>8e4&&(l=l.slice(0,8e4)+"\n…(内容过长已截断)");return`${`HTTP ${i.status}${c?` · ${c}`:""}${i.url&&i.url!==n?` · 重定向至 ${i.url}`:""}`}\n${l||"(无内容)"}${a}`}
|
|
1
|
+
import e from"node:fs";import t from"node:path";import{StringDecoder as n}from"node:string_decoder";import{spawn as r,execFileSync as i}from"node:child_process";import*as o from"./ide.js";import{extractDocument as s}from"./api.js";import{pythonEnvForBash as l}from"./pyruntime.js";import*as c from"./jobs.js";import{confineArgv as a,sandboxNote as f}from"./sandbox.js";import{cleanForModel as u}from"./terminal.js";import{isWebSearchOn as p}from"./config.js";import{isSearchEngineQueryUrl as h,FETCH_SEARCH_BLOCKED_MESSAGE as d}from"./websearch.js";const g=16777216,m=536870912,$=2e3,w=2e3,y=3e4,b=new Set([".git","node_modules","dist","build","target","out",".next",".nuxt",".venv","venv","__pycache__",".idea",".gradle",".mvn","coverage",".cache","vendor","bower_components","Pods","DerivedData",".turbo",".parcel-cache",".terraform",".pytest_cache",".mypy_cache",".tox",".angular",".svelte-kit",".output",".serverless",".svn",".hg"]);function S(e,n,r){if(!e||"string"!=typeof e)throw new Error("工作目录未设置:请在项目目录中运行(VS Code中需先「打开文件夹」再对话)");if(!n||"string"!=typeof n)throw new Error("缺少路径path(工具参数里必须提供path字段)");const i=t.resolve(e,n);if(r){const r=t.relative(e,i);if(""===r||r.startsWith("..")||t.isAbsolute(r))throw new Error(`路径超出项目目录(写操作仅限当前项目内):${n}`)}return i}function x(e,n){const r=t.relative(e,n);return""===r?".":r}function _(e){const t=Math.min(e.length,4096);for(let n=0;n<t;n++)if(0===e[n])return!0;return!1}function*v(n,r=0,i=14){let o;try{o=e.readdirSync(n,{withFileTypes:!0})}catch{return}for(const e of o){if(b.has(e.name))continue;const o=t.join(n,e.name);e.isDirectory()?r<i&&(yield*v(o,r+1,i)):e.isFile()&&(yield o)}}function E(e){let t="";for(let n=0;n<e.length;n++){const r=e[n];"*"===r?"*"===e[n+1]?(t+=".*",n++,"/"===e[n+1]&&n++):t+="[^/]*":"?"===r?t+="[^/]":"\\^$+.()|{}[]".includes(r)?t+="\\"+r:t+=r}return new RegExp("^"+t+"$")}export function readFile(t,r){const i=S(t,r.path,!1),o=e.statSync(i);if(o.isDirectory())throw new Error(`是目录而非文件:${r.path}`);if(o.size>4194304)return function(t,r,i,o){const s=Buffer.allocUnsafe(4096),l=e.openSync(r,"r"),c=e.readSync(l,s,0,4096,0);if(e.closeSync(l),_(s.subarray(0,c)))return`(二进制文件,${o.size} 字节,未展示内容)`;const a=Math.max(0,(Number.isFinite(i.offset)?i.offset:1)-1),f=Number.isFinite(i.limit)&&i.limit>0?i.limit:$,{lines:u,lastLine:p,hitEof:h,truncatedScan:d}=function(t,r,i,o,s){const l=r+i,c=e=>e.length>o?e.slice(0,o)+"…(行过长已截断)":e,a=e.openSync(t,"r");try{const t=65536,i=Buffer.allocUnsafe(t),o=new n("utf8"),f=[];let u="",p=0,h=0,d=!1,g=!1,m=!1;for(;!m;){const n=e.readSync(a,i,0,t,null);if(0===n){d=!0;break}h+=n;let $,w=u+o.write(i.subarray(0,n)),y=0;for(;-1!==($=w.indexOf("\n",y));)if(p>=r&&p<l&&f.push(c(w.slice(y,$))),p++,y=$+1,p>=l){m=!0;break}if(u=m?"":w.slice(y),!m&&h>=s){g=!0;break}}return d&&(u+=o.end(),u.length&&p>=r&&p<l?(f.push(c(u)),p++):u.length&&p++),{lines:f,lastLine:p,hitEof:d,truncatedScan:g}}finally{e.closeSync(a)}}(r,a,f,w,m),g=(o.size/1024/1024).toFixed(1);if(!u.length)return d?`(大文件 ${g}MB:起始行 ${a+1} 超出已扫描的前 ${Math.round(512)}MB;请改用grep精确定位)`:`(起始行 ${a+1} 超出文件总行数 ${p})`;const y=a+u.length,b=String(y).length,S=u.map((e,t)=>`${String(a+t+1).padStart(b)}→${e}`).join("\n");return S+`\n…(大文件 ${g}MB,${h?`共 ${p} 行`:`${p}+ 行(大文件未全扫)`},本次第 ${a+1}-${y} 行;用offset/limit翻页,或用grep精确定位)`}(0,i,r,o);const s=e.readFileSync(i);if(_(s))return`(二进制文件,${o.size} 字节,未展示内容)`;const l=s.toString("utf8");if(""===l)return"(空文件)";const c=l.split("\n");c.length>1&&""===c[c.length-1]&&c.pop();const a=c.length,f=Math.max(0,(Number.isFinite(r.offset)?r.offset:1)-1),u=Number.isFinite(r.limit)&&r.limit>0?r.limit:$,p=Math.min(a,f+u);if(f>=a)return`(起始行 ${f+1} 超出文件总行数 ${a})`;const h=String(p).length,d=[];for(let e=f;e<p;e++){let t=c[e];t.length>w&&(t=t.slice(0,w)+"…(行过长已截断)"),d.push(`${String(e+1).padStart(h)}→${t}`)}let g=d.join("\n");return(f>0||p<a)&&(g+=`\n…(文件共 ${a} 行,本次显示第 ${f+1}-${p} 行,可用offset/limit继续读取)`),g}const k=20971520;export async function readDocument(n,r,i){const o=S(n,r.path,!1),l=e.statSync(o);if(l.isDirectory())throw new Error(`是目录而非文件:${r.path}`);if(l.size>k)throw new Error(`文档超过 ${Math.round(20)}MB解析上限`);if(!i||!i.baseUrl||!i.apiKey)throw new Error("解析文档需登录平台(先执行huicai login)");const c=Number.isFinite(Number(r.offset))&&Number(r.offset)>0?Math.floor(Number(r.offset)):0,a=Number.isFinite(Number(r.max_chars))&&Number(r.max_chars)>0?Math.min(18e3,Math.max(1e3,Math.floor(Number(r.max_chars)))):16e3,f=await s(i.baseUrl,i.apiKey,{filename:t.basename(o),data:e.readFileSync(o).toString("base64"),maxChars:a,offset:c}),u=f.chars??0,p=f.text||"";if(!p)return`(第 ${c} 字符起已无内容;全文共 ${u} 字符)`;const h=c+p.length,d=f.truncated?`\n…(全文共 ${u} 字符,本段为第 ${c}–${h} 字符;继续调用read_document并传offset=${h} 读取后续)`:`\n(全文共 ${u} 字符,本段为第 ${c}–${h} 字符,已到结尾)`,g=f.pages?`${f.pages}页·`:"";return`【文档 ${x(n,o)}(${g}共 ${u} 字符)】\n${p}${d}`}export function looksLikeNumberedDump(e){const t=String(e||"").split("\n").filter(e=>e.trim());if(t.length<3)return!1;return t.filter(e=>/^\s*\d+→/.test(e)).length/t.length>=.8}const L="拒绝写入:内容看起来是read_file的**带行号输出**(每行以「行号→」开头),不是文件原文。原样写回会把行号写进文件,把它毁掉。\n改法:① 局部替换用edit_file(old_string→new_string,不必读全文);② 确需重写全文,先去掉前缀:content.replace(/^\\s*\\d+→/gm, '')。";export async function writeFile(n,r){const i=S(n,r.path,!0),s=null==r.content?"":String(r.content);if(looksLikeNumberedDump(s))throw new Error(L);if(!0===r.append){let r="";try{r=e.readFileSync(i,"utf8")}catch{}const l=r+s;if(Buffer.byteLength(l)>g)throw new Error(`追加后文件将超过 ${Math.round(16)}MB上限,请拆分为多个文件`);if(o.isActive()){const t=await o.proposeWrite(i,l,n,{exists:e.existsSync(i),content:r});if(t.decided)return t.applied?`已追加到 ${x(n,i)}(经IDE审阅应用,本段 ${Buffer.byteLength(s)} 字节)`:`用户在IDE中拒绝了对 ${x(n,i)} 的追加${t.reason?":"+t.reason:""}`}return e.mkdirSync(t.dirname(i),{recursive:!0}),e.writeFileSync(i,l,"utf8"),`已追加到 ${x(n,i)}(本段 ${Buffer.byteLength(s)} 字节,文件现 ${Buffer.byteLength(l)} 字节)`+(o.isActive()?"(IDE未响应,已直接写入)":"")}if(o.isActive()){const e=await o.proposeWrite(i,s,n);if(e.decided)return e.applied?`已写入 ${x(n,i)}(经IDE审阅应用)`:`用户在IDE中拒绝了对 ${x(n,i)} 的修改${e.reason?":"+e.reason:""}`}return e.mkdirSync(t.dirname(i),{recursive:!0}),e.writeFileSync(i,s,"utf8"),`已写入 ${x(n,i)}(${Buffer.byteLength(s)} 字节)`+(o.isActive()?"(IDE未响应,已直接写入)":"")}export async function editFile(t,n){const r=S(t,n.path,!0),i=null==n.old_string?"":String(n.old_string),s=null==n.new_string?"":String(n.new_string);if(""===i)throw new Error("edit_file需要非空old_string");if(looksLikeNumberedDump(s))throw new Error(L);if(!e.existsSync(r))throw new Error(`目标文件不存在:${n.path}。请先用read_file / list_files确认路径是否正确(可能工作目录不对,或文件已被移动/删除);不要改用write_file新建一个同名空文件替代——如确需新建,请先向用户说明再创建。`);const l=e.readFileSync(r,"utf8"),c=l.split(i).length-1;if(0===c)throw new Error("未找到要替换的内容(old_string未出现在文件中)。注意:old_string必须是文件原文,不能包含read_file显示的「行号→」前缀");if(c>1&&!n.replace_all)throw new Error(`old_string在文件中出现 ${c} 处,不唯一;请补充上下文使其唯一,或设replace_all=true`);const a=n.replace_all?l.split(i).join(s):l.replace(i,s),f=n.replace_all?c:1;if(o.isActive()){const e=await o.proposeWrite(r,a,t,{exists:!0,content:l});if(e.decided)return e.applied?`已编辑 ${x(t,r)}(经IDE审阅应用,替换 ${f} 处)`:`用户在IDE中拒绝了对 ${x(t,r)} 的修改${e.reason?":"+e.reason:""}`}return e.writeFileSync(r,a,"utf8"),`已编辑 ${x(t,r)},替换 ${f} 处`+(o.isActive()?"(IDE未响应,已直接写入)":"")}export function listFiles(n,r){const i=S(n,r.path||".",!1),o=[];let s=0;const l=(n,r)=>{if(s>=400)return;let c;try{c=e.readdirSync(n,{withFileTypes:!0})}catch{return}c.sort((e,t)=>e.name.localeCompare(t.name));for(const e of c){if(s>=400)break;if(b.has(e.name))continue;const c=t.join(n,e.name),a=t.relative(i,c);e.isDirectory()?(o.push(a+"/"),s++,r<6&&l(c,r+1)):(o.push(a),s++)}};return l(i,0),0===o.length?"(空目录)":o.join("\n")+(s>=400?"\n…(条目已截断)":"")}export function listWorkspaceFiles(e,n=2e3){const r=[];for(const i of v(e))if(r.push(t.relative(e,i).split(t.sep).join("/")),r.length>=n)break;return r}export function glob(e,n){if(!n.pattern)throw new Error("缺少pattern");const r=E(n.pattern),i=[];for(const n of v(e)){const o=t.relative(e,n).split(t.sep).join("/");if(r.test(o)&&(i.push(o),i.length>=300))break}return 0===i.length?"(无匹配文件)":i.join("\n")}let j=null;export function grep(e,t){if(!t.pattern)throw new Error("缺少pattern");const n=function(){if(null!==j)return j;try{i("rg",["--version"],{stdio:"ignore"}),j=!0}catch{j=!1}return j}()?function(e,t){const n=Number.isFinite(t.max_results)&&t.max_results>0?Math.min(t.max_results,500):200,r=t.path?String(t.path):".",o=[...b].flatMap(e=>["-g",`!**/${e}/**`,"-g",`!${e}/**`]),s=["--color=never","--no-messages","--max-columns","2000"];t.ignore_case&&s.push("-i");t.glob&&s.push("-g",String(t.glob));s.push(...o),t.files_only?s.push("-l"):s.push("-n","--no-heading");let l;s.push("-e",String(t.pattern),"--",r);try{l=i("rg",s,{cwd:e,encoding:"utf8",maxBuffer:32<<20,stdio:["ignore","pipe","pipe"]})}catch(e){return e&&1===e.status?"(无匹配)":null}const c=e=>e.replace(/\\/g,"/").replace(/^\.\//,""),a=l.split("\n").filter(Boolean);if(t.files_only)return a.length?a.slice(0,n).map(c).join("\n"):"(无匹配)";const f=[];for(const e of a){const t=/^(.*?):(\d+):(.*)$/.exec(e);if(t&&(f.push(`${c(t[1])}:${t[2]}: ${t[3].slice(0,240)}`),f.length>=n))break}return f.length?f.join("\n")+(f.length>=n?"\n…(结果已截断)":""):"(无匹配)"}(e,t):null;return null!==n?n:grepJs(e,t)}export function grepJs(n,r){if(!r.pattern)throw new Error("缺少pattern");let i;try{i=new RegExp(r.pattern,r.ignore_case?"i":void 0)}catch(e){throw new Error(`无效的正则:${e.message}`)}const o=r.glob?E(r.glob):null,s=S(n,r.path||".",!1),l=Number.isFinite(r.max_results)&&r.max_results>0?Math.min(r.max_results,500):200,c=[],a=[];for(const f of v(s)){if(c.length>=l)break;const s=t.relative(n,f).split(t.sep).join("/");if(o&&!o.test(s))continue;let u;try{u=e.statSync(f)}catch{continue}let p=!1;if(u.size<=262144){let t;try{t=e.readFileSync(f)}catch{continue}if(_(t))continue;const n=t.toString("utf8").split("\n");for(let e=0;e<n.length;e++)if(i.test(n[e])){if(p=!0,r.files_only)break;if(c.push(`${s}:${e+1}: ${n[e].slice(0,240)}`),c.length>=l)break}}else p=F(f,i,s,!!r.files_only,c,l);if(p&&(a.push(s),r.files_only&&a.length>=l))break}return r.files_only?a.length?a.join("\n"):"(无匹配)":0===c.length?"(无匹配)":c.join("\n")+(c.length>=l?"\n…(结果已截断)":"")}function F(t,r,i,o,s,l){let c;try{c=e.openSync(t,"r")}catch{return!1}try{const t=65536,a=Buffer.allocUnsafe(t),f=new n("utf8");let u="",p=0,h=0,d=!1,g=!0,m=!1;for(;;){const n=e.readSync(c,a,0,t,null);if(0===n)break;if(g){if(_(a.subarray(0,n)))return!1;g=!1}h+=n;const $=u+f.write(a.subarray(0,n));let w,y=0;for(;-1!==(w=$.indexOf("\n",y));){p++;const e=$.slice(y,w);if(r.test(e)){if(d=!0,o)return!0;if(s.push(`${i}:${p}: ${e.slice(0,240)}`),s.length>=l)return!0}y=w+1}if(u=$.slice(y),h>=16777216){m=!0;break}}return m||(u+=f.end(),u.length&&(p++,r.test(u)&&(d=!0,!o&&s.length<l&&s.push(`${i}:${p}: ${u.slice(0,240)}`)))),d}finally{e.closeSync(c)}}function M(e,t,i){const o=String(t.command),s="win32"===process.platform,a={...process.env,...l()},f=s?[process.env.COMSPEC||"cmd.exe","/d","/s","/c",o]:[process.env.SHELL||"/bin/sh","-c",o],{argv:u,sandbox:p,note:h}=I(f,i,e),d=r(u[0],u.slice(1),s?{cwd:e,env:a,windowsVerbatimArguments:!0}:{cwd:e,env:a,detached:!0}),g=c.start({kind:"bash",label:o.replace(/\s+/g," ").slice(0,120),sessionId:i?.sessionId||"",cwd:e,kill:()=>(()=>{try{!s&&d.pid?process.kill(-d.pid,"SIGKILL"):d.kill("SIGKILL")}catch{try{d.kill("SIGKILL")}catch{}}})(),meta:{pid:d.pid}}),m=new n("utf8"),$=new n("utf8");return d.stdout.on("data",e=>g.append(m.write(e))),d.stderr.on("data",e=>g.append($.write(e))),d.on("error",e=>{g.append(`\n命令启动失败:${e.message}\n`),g.settle("failed","启动失败")}),d.on("close",(e,t)=>{g.append(m.end()),g.append($.end()),t?g.settle("killed",`被信号 ${t} 终止`):g.settle(0===e?"done":"failed",`退出码 ${e}`)}),Promise.resolve(`已在后台启动,作业id:${g.id}\n命令:${o.replace(/\s+/g," ").slice(0,200)}\n用job_output读它的输出(默认只回新增;加wait=true可等它结束),job_kill可终止它。不要为了等它而空转或反复轮询——它结束时会主动通知你。`+h)}const I=(e,t,n)=>a(e,t,n);export function runBash(e,t,i,o){const s=null==t.command?"":String(t.command);if(""===s.trim())return Promise.resolve("(未提供命令)");if(t.run_in_background)return M(e,t,i);const c=Number.isFinite(t.timeout)?Math.min(t.timeout,6e5):12e4,a="win32"===process.platform;return new Promise(t=>{let p="",h=!1,d=!1;const g={...process.env,...l()},m=a?[process.env.COMSPEC||"cmd.exe","/d","/s","/c",s]:[process.env.SHELL||"/bin/sh","-c",s],{argv:$,sandbox:w,note:b}=I(m,i,e),S=r($[0],$.slice(1),a?{cwd:e,env:g,windowsVerbatimArguments:!0}:{cwd:e,env:g,detached:!0}),x=()=>{try{!a&&S.pid?process.kill(-S.pid,"SIGKILL"):S.kill("SIGKILL")}catch{try{S.kill("SIGKILL")}catch{}}},_=setTimeout(()=>{h=!0,x()},c),v=()=>{d=!0,h=!0,x()};o&&(o.aborted?v():o.addEventListener("abort",v,{once:!0}));const E=()=>{clearTimeout(_),o&&o.removeEventListener("abort",v)},k=new n("utf8"),L=new n("utf8"),j=e=>{p.length<y&&(p+=e)};S.stdout.on("data",e=>j(k.write(e))),S.stderr.on("data",e=>j(L.write(e))),S.on("error",e=>{E(),t(`命令启动失败:${e.message}`)}),S.on("close",e=>{E(),j(k.end()),j(L.end()),p=u(p);let n=p.length>y?p.slice(0,y)+"\n…(输出已截断)":p;""===n.trim()&&(n="(无输出)");const r=d?"【已中断】":h?"【超时被终止】":`退出码 ${e}`,i=0===e?"":((e,t)=>f(e,t))(w,n);t(`${r}\n${n}${b}${i}`)})})}export async function fetchUrl(e,t){const n=String(t.url||"").trim();if(!/^https?:\/\//i.test(n))throw new Error("仅支持http/https URL");const r=!p();if(r&&h(n))throw new Error(d);let i;try{i=await fetch(n,{redirect:"follow",signal:AbortSignal.timeout(Number.isFinite(t.timeout)?Math.min(t.timeout,12e4):3e4),headers:{"User-Agent":"huicai-cli",Accept:"text/html,application/json;q=0.9,text/plain;q=0.8,*/*;q=0.5"}})}catch(e){throw new Error(`请求失败:${e?.cause?.message||e.message}`)}if(r&&i.url&&i.url!==n&&h(i.url)){try{await(i.body?.cancel())}catch{}throw new Error(d)}const o=await async function(e,t){if(!e.body)return Buffer.alloc(0);const n=e.body.getReader(),r=[];let i=0;for(;;){const{done:e,value:o}=await n.read();if(e)break;if(r.push(o),i+=o.length,i>=t){n.cancel().catch(()=>{});break}}return Buffer.concat(r.map(e=>Buffer.from(e)),Math.min(i,t))}(i,2097152),s=(i.headers.get("content-type")||"").toLowerCase();if(_(o)&&!/text|json|xml|html/.test(s))return`HTTP ${i.status} · ${s||"未知类型"} · ${o.length} 字节(二进制内容未展示)`;let l=function(e,t){let n=(/charset=([\w-]+)/i.exec(t||"")||[])[1];if(!n){const t=e.slice(0,4096).toString("latin1");n=(/<meta[^>]+charset\s*=\s*["']?([\w-]+)/i.exec(t)||[])[1]}const r=String(n||"utf-8").toLowerCase();if("utf-8"===r||"utf8"===r)return e.toString("utf8");try{return new TextDecoder(r).decode(e)}catch{return e.toString("utf8")}}(o,s),c="",a="";if(s.includes("html")||/^\s*<(!doctype|html)/i.test(l)){if(c=((/<title[^>]*>([\s\S]*?)<\/title>/i.exec(l)||[])[1]||"").replace(/\s+/g," ").trim().slice(0,120),t.links){const e=function(e,t,n=60){const r=[],i=new Set,o=/<a\b[^>]*href\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))[^>]*>([\s\S]*?)<\/a>/gi;let s;for(;(s=o.exec(e))&&r.length<n;){const e=(s[1]??s[2]??s[3]??"").trim();if(!e||/^(javascript:|mailto:|tel:|#)/i.test(e))continue;let n;try{n=new URL(e,t).href}catch{continue}if(!/^https?:/i.test(n)||i.has(n))continue;i.add(n);const o=s[4].replace(/<[^>]+>/g,"").replace(/\s+/g," ").trim().slice(0,80);r.push(`- ${o||"(无文本)"} → ${n}`)}return r}(l,i.url||n);e.length&&(a=`\n\n—— 页面链接(${e.length} 条)——\n`+e.join("\n"))}l=function(e){let t=String(e);return t=t.replace(/<(script|style|noscript|svg|head)[\s\S]*?<\/\1>/gi,""),t=t.replace(/<!--[\s\S]*?-->/g,""),t=t.replace(/<br\s*\/?>/gi,"\n"),t=t.replace(/<\/(p|div|li|tr|h[1-6]|section|article|blockquote|pre|ul|ol|table)>/gi,"\n"),t=t.replace(/<li[^>]*>/gi,"- "),t=t.replace(/<[^>]+>/g,""),t=t.replace(/ /g," ").replace(/</g,"<").replace(/>/g,">").replace(/"/g,'"').replace(/'|'/g,"'").replace(/&/g,"&"),t=t.replace(/[ \t]+\n/g,"\n").replace(/\n{3,}/g,"\n\n"),t}(l)}l=l.trim(),l.length>8e4&&(l=l.slice(0,8e4)+"\n…(内容过长已截断)");return`${`HTTP ${i.status}${c?` · ${c}`:""}${i.url&&i.url!==n?` · 重定向至 ${i.url}`:""}`}\n${l||"(无内容)"}${a}`}
|
package/src/ide.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"node:http";export function isActive(){return!!process.env.HUICAI_IDE_PORT}export function proposeWrite(
|
|
1
|
+
import e from"node:http";import t from"node:fs";import n from"node:crypto";const o=()=>process.env.HUICAI_IDEA_VERSION||process.env.HUICAI_VSCODE_VERSION?{decided:!0,applied:!1,reason:"IDE审阅连接不可用,未自动写入;请恢复连接后重新审阅。"}:{decided:!1};export function isActive(){return!!(process.env.HUICAI_IDE_PORT||process.env.HUICAI_IDEA_VERSION||process.env.HUICAI_VSCODE_VERSION)}export function proposeWrite(r,s,p,i){return new Promise(c=>{const d=process.env.HUICAI_IDE_PORT;if(!d)return void c(o());const a=process.env.HUICAI_IDE_TOKEN||"";let I=i;if(!I)try{I={exists:!0,content:t.readFileSync(r,"utf8")}}catch(e){if("ENOENT"!==e.code)return void c({decided:!0,applied:!1,reason:"无法读取文件版本,未写入"});I={exists:!1,content:""}}const E=n.createHash("sha256").update((I.exists?"present\0":"missing\0")+I.content.replace(/\r\n/g,"\n")).digest("hex"),h=JSON.stringify({path:r,content:s,cwd:p,token:a,baseHash:E}),_=e.request({host:"127.0.0.1",port:d,path:"/apply",method:"POST",headers:{"Content-Type":"application/json","Content-Length":Buffer.byteLength(h)}},e=>{let t="";e.on("data",e=>t+=e),e.on("end",()=>{try{const e=JSON.parse(t);c({decided:!0,applied:!!e.applied,reason:e.reason})}catch{c(o())}})});_.on("error",()=>c(o())),_.setTimeout(12e4,()=>{_.destroy(),c(o())}),_.write(h),_.end()})}
|
package/src/repl.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{terminalSelect as s}from"./terminal-select.js";import{questionSelection as o}from"./selection.js";import{parseAskAnswer as e}from"./ask-ui.js";import t from"node:readline";import{syncPlanUser as r}from"./plan-limits.js";import{c as n}from"./colors.js";import{createPermissions as i}from"./permissions.js";import{runTurn as c}from"./agent.js";import{createSession as a,saveSession as l,turnCount as u}from"./session.js";import{syncSessionLog as p,rewindSessionLog as m}from"./sessionlog.js";import{ensureSessionTitle as d}from"./title.js";import{createCheckpoints as f}from"./checkpoints.js";import{buildSystemPrompt as g}from"./context.js";import{runVerifyLoop as w}from"./autofix.js";import{runAutoFlow as h}from"./autorun.js";import{loadMcpTools as j,closeMcp as y,makeMcpConnectTools as b}from"./mcp.js";import{makeJobTools as k,onSettled as x,describe as I}from"./jobs.js";import{makeTerminalTools as T,closeAllTerminals as S,dropSessionTerminals as v}from"./terminal.js";import{makeGoalTools as $,runGoalRounds as q,setRoundKind as O,getGoal as P,viewGoal as U}from"./goalround.js";import{makeCodeModeTool as _}from"./codemode.js";import{makeWorkflowTool as C}from"./workflow.js";import{makeLspTool as M,setProvider as A}from"./lsp.js";import{ensureVerified as E}from"./sandbox.js";import{makeAutomationTools as R}from"./automation-tools.js";import{makeNotifyTool as L,notifyTurnDone as N}from"./notify.js";import{makePresenceTools as W,acquireWakeLock as B,releaseWakeLock as G}from"./presence.js";import{makeComputerTools as H}from"./computer.js";import{makeDiagramTools as V}from"./diagram.js";import{makePptxTools as D}from"./pptx-tools.js";import{makeDocxTools as Y}from"./docx-tools.js";import{makeReportTools as J}from"./report-tools.js";import{makeXlsxTools as K}from"./xlsx-tools.js";import{makeCodeTools as Q}from"./code-tools.js";import{makeTestTools as z}from"./test-runner.js";import{makeWebTools as F}from"./web-inspect.js";import{browserAvailable as X}from"./web-cdp.js";import{makeUiTools as Z}from"./ui-spec.js";import{makeAskUiTool as ss}from"./ask-ui.js";import{makeSurfaceTools as os}from"./surface-state.js";import{makeVoiceFormTool as es}from"./voice-form.js";import{makeDepAuditTool as ts}from"./depaudit.js";import{makeScopeCheckTool as rs}from"./scopecreep.js";import{makeArchaeologyTool as ns}from"./archaeology.js";import{streamChat as is}from"./api.js";import{execSync as cs}from"node:child_process";import{createVisualBridge as as}from"./visual.js";import{autoSyncPlatformSkills as ls}from"./skills.js";import{makeSubagentTool as us,makeDispatchAgentsTool as ps,makeContinuableAgentTools as ms}from"./subagent.js";import{makeDispatchMemberTool as ds}from"./squad.js";import{makeConsultTool as fs}from"./consult.js";import{makeExpertTools as gs}from"./experts.js";import{listWorkspaceFiles as ws}from"./executors.js";import{makeAdvisorTool as hs}from"./advisor.js";import{makeRecallTool as js,readMemoryRaw as ys}from"./memory.js";import{detectScanners as bs,makeScanTool as ks}from"./scan.js";import{runSlash as xs,listCommands as Is}from"./commands.js";import{handleAccountError as Ts,accountErrorKind as Ss}from"./account.js";import{expandAtMentions as vs,buildUserContent as $s,runBangCommand as qs,loadHistory as Os,appendHistory as Ps}from"./input.js";import{makeSkillTools as Us}from"./skills.js";import{makeSessionEventTools as _s}from"./session-events.js";import{makeExternalAgentTools as Cs}from"./external-agents.js";import{makeWebSearchTools as Ms}from"./websearch.js";import{renderWelcome as As}from"./banner.js";import{createCrewBudget as Es,withBudget as Rs,makePlanBudgetTool as Ls,makeBudgetStatusTool as Ns}from"./crew.js";import{makeEvidenceTools as Ws}from"./evidence.js";import{makeTapdTools as Bs}from"./tapd.js";import{makeWeeklyTools as Gs}from"./weekly.js";export async function buildExtraTools(s,o,e={}){const t="function"==typeof e.onSub?e.onSub:null;let r={tools:[],clients:[],registry:{servers:{}}};try{r=await j(o)}catch(s){process.stdout.write(n.yellow("MCP初始化失败(忽略):"+s.message)+"\n")}const i=[...Us(o),...r.tools],c=s.crewBudget?Es(s.crewBudgetPlan||{}):null,a=(s,o,e)=>c?Rs(s,c,o,e):s,l=us({config:s,cwd:o,extraTools:r.tools,depth:0,onSub:t});l&&i.push(a(l,"dispatch"));const u=ps({config:s,cwd:o,extraTools:r.tools,depth:0,onSub:t});u&&i.push(a(u,"dispatch",s=>(s?.tasks||[]).length||1)),i.push(...ms({config:s,cwd:o,extraTools:r.tools,depth:0,onSub:t,queueInject:"function"==typeof e.queueInject?e.queueInject:null}));const p=ds({config:s,cwd:o,extraTools:r.tools,onSub:t});p&&i.push(p),i.push(...Ms({config:s})),i.push(...Cs({config:s,cwd:o,onSub:t}));const m=fs({config:s});m&&i.push(m);try{i.push(...await gs({config:s}))}catch{}const d=hs({config:s});if(d?i.push(a(d,"advisor")):c&&c.degrade("顾问","未配置advisorModel"),c&&(i.push(Ls(c,e.onBudgetDeclared||null)),i.push(Ns(c))),!1!==s.memoryEnabled&&ys(o)&&i.push(js({cwd:o})),bs(o).length&&i.push(ks({cwd:o})),i.push(...k({sessionId:()=>s.sessionId||""})),"function"==typeof e.queueInject){x(o=>{if(o.sessionId!==String(s.sessionId||""))return;const t="agent"===o.kind?`它已转为空闲,可用send_message继续对话;想看这一段的细节用job_output("${o.id}")。`:`用job_output("${o.id}") 读它的输出(默认只回新增)。`;e.queueInject(`【后台作业完成】${I(o)}\n${t}`)})}i.push(C({config:s,cwd:o,extraTools:r.tools,depth:0,onSub:t})),i.push(_()),"function"==typeof e.sessionOf&&i.push(...$({sessionOf:e.sessionOf})),e.lspProvider&&A(e.lspProvider),i.push(M({cwd:o})),i.push(...await T({sessionId:()=>s.sessionId||"",cwd:o,config:s})),i.push(...b({registry:r.registry,clients:r.clients,cwd:o,register:s=>i.push(...s)})),i.push(...R({cwd:o,selfSchedule:!1!==e.selfSchedule,runnerHint:e.runnerHint})),i.push(...V({cwd:o})),i.push(...D({cwd:o,config:s})),i.push(...Y({cwd:o,config:s})),i.push(...J({cwd:o,config:s})),i.push(...K({cwd:o,config:s})),i.push(...Q({cwd:o,config:s})),i.push(...z({cwd:o,config:s})),i.push(...Ws({cwd:o})),i.push(...Bs({cwd:o})),i.push(...Gs({cwd:o})),i.push(...Z({cwd:o})),i.push(ss({ask:e.ask,askUi:e.askUi})),e.sessionId&&i.push(...os({sessionId:e.sessionId,emit:e.onSurface})),i.push(...ts({cwd:o})),i.push(..._s({cwd:o,config:s})),i.push(...ns({cwd:o})),i.push(...rs({cwd:o,runGit:(s,o)=>{try{return cs(`git ${o.join(" ")}`,{cwd:s,encoding:"utf8",stdio:["ignore","pipe","ignore"],maxBuffer:33554432})}catch{return null}},goalOf:()=>{try{const s="function"==typeof e.sessionOf?P(e.sessionOf()):null;return s&&"active"===s.phase?s.objective:""}catch{return""}}})),i.push(...es(async(o,e)=>{let t="";try{await is({baseUrl:s.baseUrl,apiKey:s.apiKey,model:s.model,messages:[{role:"system",content:o},{role:"user",content:e}],temperature:0,client:s.client,extraBody:{enable_thinking:!1},aux:!0},{onContent:s=>{t+=s}})}catch{return""}return t}));const f=L({enabled:!1!==s.notifications});f&&i.push(f),i.push(...W({signalRef:e.signalRef}));const g="function"==typeof e.injectVisual?e.injectVisual:"function"==typeof e.queueInject?as({config:s,queue:e.queueInject,currentModel:e.currentModel}):null;return i.push(...H({config:s,injectVisual:g,cwd:o,omit:e.omitComputer||[],enabled:e.computerUse})),X()&&i.push(...F({cwd:o,config:s,injectVisual:g})),ls(s,{onInfo:o=>{s.debug&&process.stdout.write(n.gray(" · "+o)+"\n")}}).catch(()=>{}),{extraTools:i,clients:r.clients,mcpRegistry:r.registry,crew:c}}export async function startRepl({config:u,cwd:j,yolo:b,initialPrompt:k,session:x,version:I,...T}){E().catch(()=>{}),r(u).catch(()=>{});const $=t.createInterface({input:process.stdin,output:process.stdout,history:Os(j).reverse(),historySize:200,completer:s=>{if(/^\/\S*$/.test(s)){const o=Is(j).flatMap(s=>[s.name,...s.aliases||[]]);return[o.map(s=>"/"+s).filter(o=>o.startsWith(s)),s]}const o=/(?:^|\s)@([^\s@]*)$/.exec(s);if(!o)return[[],s];const e="@"+o[1],t=e.toLowerCase();return[ws(j).map(s=>"@"+s).filter(s=>s.toLowerCase().startsWith(t)).slice(0,40),e]}});let U,_=!1;const C=s=>new Promise((o,e)=>{if($.closed)return e(new Error("终端已关闭"));const t=new AbortController,r=()=>{U=null,e(new Error("终端已关闭"))},n=s=>{$.off("close",r),U=null,o(s)};U=()=>{t.abort(),n("")},$.once("close",r),$.question(s,{signal:t.signal},n)});let M=Promise.resolve();const A=process.stdin.isTTY&&process.stdout.isTTY?o=>{const e=Y?.signal,t=M.then(async()=>{_=!0;try{return await s(o,{rl:$,signal:e,onInterrupt:()=>Y?.abort()})}finally{_=!1}});return M=t.catch(()=>null),t}:void 0,R=T.askUi||(A?async s=>{const t=await A(o(s));return"date_range"===s.kind&&t?.value?e(s,t.value):t||{value:null}}:void 0);let L=!1,W=i({yolo:b,ask:C,select:A,cwd:j});const H={prompt:0,completion:0},V={list:[]},D=f();let Y=null,J=null;const K={config:u,cwd:j,permissions:W,usage:H,session:x,version:I,crewOf:()=>Z};$.on("SIGINT",()=>{_?(Y?.abort(),U?.(),process.stdout.write("\n")):Y?Y.abort():process.stdout.write("\n"+n.gray("输入 /exit退出。")+"\n")}),function(s,o,e,t){process.stdout.write(As({model:s.model,cwd:o,version:t,platform:/^(agent|workflow|datahuman|squad)-/.test(String(s.model||"")),yolo:e.isYolo(),consult:s.consult?s.consultLabel||s.consult:null})+"\n")}(u,j,W,I),Vs(x);const Q=[],z={current:null};let{extraTools:F,clients:X,crew:Z}=await buildExtraTools(u,j,{sessionOf:()=>K.session,queueInject:s=>Q.push(s),signalRef:z,ask:C,askUi:R,sessionId:K.session?.id,onSurface:T.onSurface,runnerHint:"到点由调度器在该目录自动执行(终端里需有 `huicai daemon` 常驻;桌面端保持应用运行亦可)。"});const ss=()=>{d(K.session,u).then(s=>{s&&l(K.session)}).catch(()=>{})},os=async s=>{const{text:o,mentions:e,images:t}=await vs(s,j,u,{onDocStart:(s,o,e)=>process.stdout.write(n.gray(` · 正在解析文档 @${s}${e>1?`(${o}/${e})`:""}…`)+"\n")});e.length&&process.stdout.write(n.gray(" · 已展开引用:"+e.join("、")+(t.length?`(含 ${t.length} 张图片)`:""))+"\n");const r=D.snapshot({session:K.session,label:s});K.session.messages.push({role:"user",content:$s(o,t)}),ss(),Y=new AbortController,z.current=Y.signal,K.session.compaction||(K.session.compaction={}),u.sessionId=K.session.id,u.goal=P(K.session)?.objective||"";const i="auto"===W.getMode?.()&&!1!==u.wakeLockInAuto?B({reason:"自动模式任务执行中"}):null,a=Date.now();let m;try{m=await c({config:u,messages:K.session.messages,permissions:W,cwd:j,signal:Y.signal,extraTools:F,todoState:V,compaction:K.session.compaction,fileJournal:r,pullQueued:()=>Q.shift()||null})}finally{i?.ok&&G()}return m?.usage&&(H.prompt+=m.usage.prompt,H.completion+=m.usage.completion),m?.lastPromptTokens&&(K.session.lastPromptTokens=m.lastPromptTokens),m?.lastUsage&&(K.session.lastUsage=m.lastUsage),Y=null,z.current=null,m?.aborted||m?.error||N({durationMs:Date.now()-a,cwd:j,summary:String(s).replace(/\s+/g," ").slice(0,80),enabled:!1!==u.notifications}).catch(()=>{}),p(K.session),l(K.session),ss(),m?.error&&"auth"===Ts(m,u)&&(L=!0),m},es=async s=>{O(K.session.id,"human");const o=await os(s);return o?.aborted||o?.error||L?o:P(K.session)?(await q({session:K.session,doTurn:os,stats:()=>({usedTokens:H.prompt+H.completion,budgetTokens:Number(u.budgetTokens)||0}),onInfo:s=>process.stdout.write(n.gray(" ◆ "+s)+"\n"),aborted:()=>L}),l(K.session),o):o},ts=()=>({...K,signal:Y?.signal,select:A,resumeSession:s=>{J&&(clearInterval(J),J=null),v(K.session.id),K.session=s,u.sessionId=s.id,s.model&&(u.model=s.model);const o=W.getMode();W=i({yolo:!1,ask:C,select:A,cwd:j}),W.setMode(o),K.permissions=W,Q.length=0,V.list=[],D.list.length=0,Vs(s)},session:K.session,extraTools:F,checkpoints:D,onRewind:s=>m(K.session,s),print:s=>process.stdout.write(String(s).endsWith("\n")?String(s):String(s)+"\n")});for(k&&(process.stdout.write(n.cyan("\nhuicai> ")+k+"\n"),await es(k));!L;){let s;try{s=(await C(n.cyan("\nhuicai> "))).trim()}catch{break}if(""!==s){if(Ps(j,s),"/exit"===s||"/quit"===s)break;if(s.startsWith("!")&&s.length>1){const o=s.slice(1).trim(),{output:e,record:t}=await qs(o,j);process.stdout.write(n.gray(e.split("\n").map(s=>" "+s).join("\n"))+"\n"),K.session.messages.push({role:"user",content:t}),l(K.session);continue}if(s.startsWith("/")){let o;Y=new AbortController;try{o=await xs(s,ts())}catch(s){process.stdout.write((Y.signal.aborted?"操作已取消":"命令失败:"+s.message)+"\n");continue}finally{Y=null}if($.closed)break;if("exit"===o.action)break;if("clear"===o.action){J&&(clearInterval(J),J=null);const s=W.getMode();W=i({yolo:!1,ask:C,select:A,cwd:j}),W.setMode(s),K.permissions=W,l(K.session),v(K.session.id),K.session=a({cwd:j,model:u.model,messages:[K.session.messages[0]]}),u.sessionId=K.session.id,Q.length=0,V.list=[],D.list.length=0,process.stdout.write(n.gray("已开启新会话(旧会话已保存,可用huicai --resume恢复)。\n"))}"refresh"===o.action&&Vs(K.session),"cd"===o.action&&(j=o.cwd,K.cwd=j,W=i({yolo:W.isYolo(),ask:C,select:A,cwd:j}),K.permissions=W,y(X),({extraTools:F,clients:X,crew:Z}=await buildExtraTools(u,j,{ask:C,askUi:R,sessionOf:()=>K.session,queueInject:s=>Q.push(s),signalRef:z,runnerHint:"到点由调度器在该目录自动执行(终端里需有 `huicai daemon` 常驻;桌面端保持应用运行亦可)。"})),K.session=a({cwd:j,model:u.model,messages:[{role:"system",content:g(j,{memory:!1!==u.memoryEnabled,config:u})}]}),V.list=[],D.list.length=0,u.sessionId=K.session.id,process.stdout.write(n.gray("已切换到 "+j+"(新会话,旧会话已保存)。\n"))),"autofix"===o.action&&await w({command:o.command,maxIters:o.maxIters,cwd:j,runTurnOnce:s=>os(s),emit:s=>process.stdout.write(n.gray(s)+"\n")}),"autorun"===o.action&&await h({goal:o.goal,config:u,permissions:W,cwd:j,runTurnOnce:s=>os(s),emit:s=>process.stdout.write(n.gray(" · "+s)+"\n")}),"loop"===o.action&&(J&&(clearInterval(J),J=null),o.stop?process.stdout.write(n.gray("已停止循环。\n")):(process.stdout.write(n.gray(`已开始循环:每 ${o.seconds}s运行「${o.prompt}」(/loop off停止)。\n`)),J=setInterval(()=>{Y||os(o.prompt)},1e3*o.seconds))),o.prompt&&await es(o.prompt);continue}await es(s)}}return J&&clearInterval(J),$.close(),y(X),S(),L?{relogin:!0}:(process.stdout.write(n.gray("\n再见。\n")),{})}function Hs(s){return"string"==typeof s?s:Array.isArray(s)?s.map(s=>"string"==typeof s?s:s&&"text"===s.type&&s.text||"").join(""):""}function Vs(s){const o=(s.messages||[]).filter(s=>"user"===s.role||"assistant"===s.role),e=u(s.messages);if(0===e)return;process.stdout.write(n.gray(` 已恢复会话「${s.title||""}」(${e} 轮)——历史对话如下:`)+"\n");const t=o.length>40?o.length-40:0;t&&process.stdout.write(n.gray(` (较早 ${t} 条已省略,完整记录在 ~/.huicai/sessions)`)+"\n");for(const s of o.slice(-40)){const o=Hs(s.content).trim();if(!o)continue;const e=o.length>800?o.slice(0,800)+"…":o,t="user"===s.role?n.bold(" 你 › "):n.gray(" 助手 › ");process.stdout.write(t+e.replace(/\n/g,"\n ")+"\n")}process.stdout.write(n.gray(" ——以上为历史,可继续对话——")+"\n")}export async function runOnce({config:s,cwd:o,yolo:e,prompt:t,session:r,persist:a,outputFormat:m,maxTurns:d}){t||(process.stderr.write('用法: huicai -p "你的指令"\n'),process.exit(1));const f="json"===m,g=i({yolo:e,ask:null,cwd:o}),w=[],h={current:null},j=new AbortController;h.current=j.signal;let b=!1;const k=()=>{b&&process.exit(130),b=!0,process.stderr.write(n.gray("\n已请求中断(再按一次Ctrl-C强制退出)…\n")),j.abort()};process.on("SIGINT",k);const{extraTools:x,clients:I}=await buildExtraTools(s,o,{sessionOf:()=>r,queueInject:s=>w.push(s),signalRef:h,runnerHint:"到点由调度器在该目录自动执行(终端里需有 `huicai daemon` 常驻;桌面端保持应用运行亦可)。"});let T=t;if(t.trim().startsWith("/")){const{runSlash:e}=await import("./commands.js"),n=[],i=await e(t.trim(),{cwd:o,config:s,session:r,permissions:g,print:s=>n.push(String(s))}).catch(s=>({handled:!1,error:s}));if(n.length&&process.stdout.write(n.join("\n")+"\n"),i&&i.prompt)T=i.prompt;else if(i&&i.handled)return y(I),process.off("SIGINT",k),{text:n.join("\n")}}const{text:v,images:$}=await vs(T,o,s);r.messages.push({role:"user",content:$s(v,$)}),r.compaction||(r.compaction={}),s.sessionId=r.id;const q=e&&!1!==s.wakeLockInAuto?B({reason:"无人值守任务执行中"}):null;let O;try{O=await c({config:s,messages:r.messages,permissions:g,cwd:o,extraTools:x,signal:j.signal,maxIters:d,compaction:r.compaction,io:f?{write:()=>{}}:void 0,pullQueued:()=>w.shift()||null})}finally{q?.ok&&G()}O?.lastPromptTokens&&(r.lastPromptTokens=O.lastPromptTokens),O?.lastUsage&&(r.lastUsage=O.lastUsage),a&&(l(r),p(r)),process.removeListener("SIGINT",k),y(I),S();const P=O?.error?Ss(O):"";if(P&&(Ts(O,s,s=>process.stderr.write(s)),"auth"===P&&process.stderr.write(n.gray("请重新运行 `huicai login` 登录后重试。")+"\n")),f){const o=[...r.messages].reverse().find(s=>"assistant"===s.role&&s.content);process.stdout.write(JSON.stringify({type:"result",is_error:!!O?.error,...O?.error?{error:O.error}:{},...P?{error_kind:P}:{},result:o?String(o.content):"",usage:{input_tokens:O?.usage?.prompt??0,output_tokens:O?.usage?.completion??0},session_id:r.id,model:s.model,num_turns:u(r.messages)})+"\n")}O?.error&&process.exit(1)}
|
|
1
|
+
import{terminalSelect as s}from"./terminal-select.js";import{questionSelection as o}from"./selection.js";import{parseAskAnswer as e}from"./ask-ui.js";import t from"node:readline";import{syncPlanUser as r}from"./plan-limits.js";import{c as n}from"./colors.js";import{createPermissions as i}from"./permissions.js";import{runTurn as a}from"./agent.js";import{createSession as c,saveSession as l,turnCount as u}from"./session.js";import{syncSessionLog as p,rewindSessionLog as m}from"./sessionlog.js";import{ensureSessionTitle as d}from"./title.js";import{createCheckpoints as f}from"./checkpoints.js";import{buildSystemPrompt as g}from"./context.js";import{runVerifyLoop as w}from"./autofix.js";import{runAutoFlow as h}from"./autorun.js";import{loadMcpTools as j,closeMcp as y,makeMcpConnectTools as b}from"./mcp.js";import{makeJobTools as k,onSettled as x,describe as I}from"./jobs.js";import{makeTerminalTools as T,closeAllTerminals as S,dropSessionTerminals as v}from"./terminal.js";import{makeGoalTools as $,runGoalRounds as q,setRoundKind as O,getGoal as P,viewGoal as U}from"./goalround.js";import{makeCodeModeTool as C}from"./codemode.js";import{makeWorkflowTool as _}from"./workflow.js";import{makeLspTool as M,setProvider as A}from"./lsp.js";import{ensureVerified as E}from"./sandbox.js";import{makeAutomationTools as R}from"./automation-tools.js";import{makeNotifyTool as H,notifyTurnDone as L}from"./notify.js";import{makePresenceTools as N,acquireWakeLock as W,releaseWakeLock as B}from"./presence.js";import{makeComputerTools as G}from"./computer.js";import{makeDiagramTools as V}from"./diagram.js";import{makePptxTools as D}from"./pptx-tools.js";import{makeDocxTools as Y}from"./docx-tools.js";import{makeReportTools as J}from"./report-tools.js";import{makeXlsxTools as K}from"./xlsx-tools.js";import{makeCodeTools as Q}from"./code-tools.js";import{makeTestTools as z}from"./test-runner.js";import{makeEngineeringTools as F}from"./engineering.js";import{makeWebTools as X}from"./web-inspect.js";import{browserAvailable as Z}from"./web-cdp.js";import{makeUiTools as ss}from"./ui-spec.js";import{makeAskUiTool as os}from"./ask-ui.js";import{makeSurfaceTools as es}from"./surface-state.js";import{makeVoiceFormTool as ts}from"./voice-form.js";import{makeDepAuditTool as rs}from"./depaudit.js";import{makeScopeCheckTool as ns}from"./scopecreep.js";import{makeArchaeologyTool as is}from"./archaeology.js";import{streamChat as as}from"./api.js";import{execSync as cs}from"node:child_process";import{createVisualBridge as ls}from"./visual.js";import{autoSyncPlatformSkills as us}from"./skills.js";import{makeSubagentTool as ps,makeDispatchAgentsTool as ms,makeContinuableAgentTools as ds}from"./subagent.js";import{makeDispatchMemberTool as fs}from"./squad.js";import{makeConsultTool as gs}from"./consult.js";import{makeExpertTools as ws}from"./experts.js";import{listWorkspaceFiles as hs}from"./executors.js";import{makeAdvisorTool as js}from"./advisor.js";import{makeRecallTool as ys,readMemoryRaw as bs}from"./memory.js";import{detectScanners as ks,makeScanTool as xs}from"./scan.js";import{runSlash as Is,listCommands as Ts}from"./commands.js";import{handleAccountError as Ss,accountErrorKind as vs}from"./account.js";import{expandAtMentions as $s,buildUserContent as qs,runBangCommand as Os,loadHistory as Ps,appendHistory as Us}from"./input.js";import{makeSkillTools as Cs}from"./skills.js";import{makeSessionEventTools as _s}from"./session-events.js";import{makeExternalAgentTools as Ms}from"./external-agents.js";import{makeWebSearchTools as As}from"./websearch.js";import{renderWelcome as Es}from"./banner.js";import{createCrewBudget as Rs,withBudget as Hs,makePlanBudgetTool as Ls,makeBudgetStatusTool as Ns}from"./crew.js";import{makeEvidenceTools as Ws}from"./evidence.js";import{makeTapdTools as Bs}from"./tapd.js";import{makeWeeklyTools as Gs}from"./weekly.js";export async function buildExtraTools(s,o,e={}){const t="function"==typeof e.onSub?e.onSub:null;let r={tools:[],clients:[],registry:{servers:{}}};try{r=await j(o)}catch(s){process.stdout.write(n.yellow("MCP初始化失败(忽略):"+s.message)+"\n")}const i=[...Cs(o),...r.tools],a=s.crewBudget?Rs(s.crewBudgetPlan||{}):null,c=(s,o,e)=>a?Hs(s,a,o,e):s,l=ps({config:s,cwd:o,extraTools:r.tools,depth:0,onSub:t});l&&i.push(c(l,"dispatch"));const u=ms({config:s,cwd:o,extraTools:r.tools,depth:0,onSub:t});u&&i.push(c(u,"dispatch",s=>(s?.tasks||[]).length||1)),i.push(...ds({config:s,cwd:o,extraTools:r.tools,depth:0,onSub:t,queueInject:"function"==typeof e.queueInject?e.queueInject:null}));const p=fs({config:s,cwd:o,extraTools:r.tools,onSub:t});p&&i.push(p),i.push(...As({config:s})),i.push(...Ms({config:s,cwd:o,onSub:t}));const m=gs({config:s});m&&i.push(m);try{i.push(...await ws({config:s}))}catch{}const d=js({config:s});if(d?i.push(c(d,"advisor")):a&&a.degrade("顾问","未配置advisorModel"),a&&(i.push(Ls(a,e.onBudgetDeclared||null)),i.push(Ns(a))),!1!==s.memoryEnabled&&bs(o)&&i.push(ys({cwd:o})),ks(o).length&&i.push(xs({cwd:o})),i.push(...k({sessionId:()=>s.sessionId||""})),"function"==typeof e.queueInject){x(o=>{if(o.sessionId!==String(s.sessionId||""))return;const t="agent"===o.kind?`它已转为空闲,可用send_message继续对话;想看这一段的细节用job_output("${o.id}")。`:`用job_output("${o.id}") 读它的输出(默认只回新增)。`;e.queueInject(`【后台作业完成】${I(o)}\n${t}`)})}i.push(_({config:s,cwd:o,extraTools:r.tools,depth:0,onSub:t})),i.push(C()),"function"==typeof e.sessionOf&&i.push(...$({sessionOf:e.sessionOf})),e.lspProvider&&A(e.lspProvider),i.push(M({cwd:o})),i.push(...await T({sessionId:()=>s.sessionId||"",cwd:o,config:s})),i.push(...b({registry:r.registry,clients:r.clients,cwd:o,register:s=>i.push(...s)})),i.push(...R({cwd:o,selfSchedule:!1!==e.selfSchedule,runnerHint:e.runnerHint})),i.push(...V({cwd:o})),i.push(...D({cwd:o,config:s})),i.push(...Y({cwd:o,config:s})),i.push(...J({cwd:o,config:s})),i.push(...K({cwd:o,config:s})),i.push(...Q({cwd:o,config:s})),i.push(...z({cwd:o,config:s})),i.push(...F()),i.push(...Ws({cwd:o})),i.push(...Bs({cwd:o})),i.push(...Gs({cwd:o})),i.push(...ss({cwd:o})),i.push(os({ask:e.ask,askUi:e.askUi})),e.sessionId&&i.push(...es({sessionId:e.sessionId,emit:e.onSurface})),i.push(...rs({cwd:o})),i.push(..._s({cwd:o,config:s})),i.push(...is({cwd:o})),i.push(...ns({cwd:o,runGit:(s,o)=>{try{return cs(`git ${o.join(" ")}`,{cwd:s,encoding:"utf8",stdio:["ignore","pipe","ignore"],maxBuffer:33554432})}catch{return null}},goalOf:()=>{try{const s="function"==typeof e.sessionOf?P(e.sessionOf()):null;return s&&"active"===s.phase?s.objective:""}catch{return""}}})),i.push(...ts(async(o,e)=>{let t="";try{await as({baseUrl:s.baseUrl,apiKey:s.apiKey,model:s.model,messages:[{role:"system",content:o},{role:"user",content:e}],temperature:0,client:s.client,extraBody:{enable_thinking:!1},aux:!0},{onContent:s=>{t+=s}})}catch{return""}return t}));const f=H({enabled:!1!==s.notifications});f&&i.push(f),i.push(...N({signalRef:e.signalRef}));const g="function"==typeof e.injectVisual?e.injectVisual:"function"==typeof e.queueInject?ls({config:s,queue:e.queueInject,currentModel:e.currentModel}):null;return i.push(...G({config:s,injectVisual:g,cwd:o,omit:e.omitComputer||[],enabled:e.computerUse})),Z()&&i.push(...X({cwd:o,config:s,injectVisual:g})),us(s,{onInfo:o=>{s.debug&&process.stdout.write(n.gray(" · "+o)+"\n")}}).catch(()=>{}),{extraTools:i,clients:r.clients,mcpRegistry:r.registry,crew:a}}export async function startRepl({config:u,cwd:j,yolo:b,initialPrompt:k,session:x,version:I,...T}){E().catch(()=>{}),r(u).catch(()=>{});const $=t.createInterface({input:process.stdin,output:process.stdout,history:Ps(j).reverse(),historySize:200,completer:s=>{if(/^\/\S*$/.test(s)){const o=Ts(j).flatMap(s=>[s.name,...s.aliases||[]]);return[o.map(s=>"/"+s).filter(o=>o.startsWith(s)),s]}const o=/(?:^|\s)@([^\s@]*)$/.exec(s);if(!o)return[[],s];const e="@"+o[1],t=e.toLowerCase();return[hs(j).map(s=>"@"+s).filter(s=>s.toLowerCase().startsWith(t)).slice(0,40),e]}});let U,C=null,_=!1,M=null,A=!1;T.updater?.check.then(s=>{C=s,s&&A&&!$.line&&U?.()});let R=!1;const H=s=>new Promise((o,e)=>{if($.closed)return e(new Error("终端已关闭"));const t=new AbortController,r=()=>{U=null,e(new Error("终端已关闭"))},n=s=>{$.off("close",r),U=null,o(s)};U=()=>{t.abort(),n("")},$.once("close",r),$.question(s,{signal:t.signal},n)});let N=Promise.resolve();const G=process.stdin.isTTY&&process.stdout.isTTY?o=>{const e=z?.signal,t=N.then(async()=>{R=!0;try{return await s(o,{rl:$,signal:e,onInterrupt:()=>z?.abort()})}finally{R=!1}});return N=t.catch(()=>null),t}:void 0,V=T.askUi||(G?async s=>{const t=await G(o(s));return"date_range"===s.kind&&t?.value?e(s,t.value):t||{value:null}}:void 0);let D=!1,Y=i({yolo:b,ask:H,select:G,cwd:j});const J={prompt:0,completion:0},K={list:[]},Q=f();let z=null,F=null;const X={config:u,cwd:j,permissions:Y,usage:J,session:x,version:I,crewOf:()=>ts};$.on("SIGINT",()=>{R?(z?.abort(),U?.(),process.stdout.write("\n")):z?z.abort():process.stdout.write("\n"+n.gray("输入 /exit退出。")+"\n")}),function(s,o,e,t){process.stdout.write(Es({model:s.model,cwd:o,version:t,platform:/^(agent|workflow|datahuman|squad)-/.test(String(s.model||"")),yolo:e.isYolo(),consult:s.consult?s.consultLabel||s.consult:null})+"\n")}(u,j,Y,I),Ds(x);const Z=[],ss={current:null};let{extraTools:os,clients:es,crew:ts}=await buildExtraTools(u,j,{sessionOf:()=>X.session,queueInject:s=>Z.push(s),signalRef:ss,ask:H,askUi:V,sessionId:X.session?.id,onSurface:T.onSurface,runnerHint:"到点由调度器在该目录自动执行(终端里需有 `huicai daemon` 常驻;桌面端保持应用运行亦可)。"});const rs=()=>{d(X.session,u).then(s=>{s&&l(X.session)}).catch(()=>{})},ns=async s=>{const{text:o,mentions:e,images:t}=await $s(s,j,u,{onDocStart:(s,o,e)=>process.stdout.write(n.gray(` · 正在解析文档 @${s}${e>1?`(${o}/${e})`:""}…`)+"\n")});e.length&&process.stdout.write(n.gray(" · 已展开引用:"+e.join("、")+(t.length?`(含 ${t.length} 张图片)`:""))+"\n");const r=Q.snapshot({session:X.session,label:s});X.session.messages.push({role:"user",content:qs(o,t)}),rs(),z=new AbortController,ss.current=z.signal,X.session.compaction||(X.session.compaction={}),u.sessionId=X.session.id,u.goal=P(X.session)?.objective||"";const i="auto"===Y.getMode?.()&&!1!==u.wakeLockInAuto?W({reason:"自动模式任务执行中"}):null,c=Date.now();let m;try{m=await a({config:u,messages:X.session.messages,permissions:Y,cwd:j,signal:z.signal,extraTools:os,todoState:K,compaction:X.session.compaction,fileJournal:r,pullQueued:()=>Z.shift()||null})}finally{i?.ok&&B()}return m?.usage&&(J.prompt+=m.usage.prompt,J.completion+=m.usage.completion),m?.lastPromptTokens&&(X.session.lastPromptTokens=m.lastPromptTokens),m?.lastUsage&&(X.session.lastUsage=m.lastUsage),z=null,ss.current=null,m?.aborted||m?.error||L({durationMs:Date.now()-c,cwd:j,summary:String(s).replace(/\s+/g," ").slice(0,80),enabled:!1!==u.notifications}).catch(()=>{}),p(X.session),l(X.session),rs(),m?.error&&"auth"===Ss(m,u)&&(D=!0),m},is=async s=>{O(X.session.id,"human");const o=await ns(s);return o?.aborted||o?.error||D?o:P(X.session)?(await q({session:X.session,doTurn:ns,stats:()=>({usedTokens:J.prompt+J.completion,budgetTokens:Number(u.budgetTokens)||0}),onInfo:s=>process.stdout.write(n.gray(" ◆ "+s)+"\n"),aborted:()=>D}),l(X.session),o):o},as=()=>({...X,signal:z?.signal,select:G,resumeSession:s=>{F&&(clearInterval(F),F=null),v(X.session.id),X.session=s,u.sessionId=s.id,s.model&&(u.model=s.model);const o=Y.getMode();Y=i({yolo:!1,ask:H,select:G,cwd:j}),Y.setMode(o),X.permissions=Y,Z.length=0,K.list=[],Q.list.length=0,Ds(s)},session:X.session,extraTools:os,checkpoints:Q,onRewind:s=>m(X.session,s),print:s=>process.stdout.write(String(s).endsWith("\n")?String(s):String(s)+"\n")});for(k&&(process.stdout.write(n.cyan("\nhuicai> ")+k+"\n"),await is(k));!D;){if(C&&!_&&G){_=!0;if("update"===await G(T.updater.menu(C))){process.stdout.write("正在更新 HuiCai AI,请稍候…\n"),z=new AbortController;try{M=await T.updater.install(C,{signal:z.signal});break}catch(s){process.stdout.write(s.message+" 输入 /update 可重试。\n")}finally{z=null}}}let s;try{A=!0,s=(await H(n.cyan("\nhuicai> "))).trim()}catch{break}finally{A=!1}if(""!==s){if(Us(j,s),"/exit"===s||"/quit"===s)break;if("/update"===s&&C&&T.updater)_=!1;else{if(s.startsWith("!")&&s.length>1){const o=s.slice(1).trim(),{output:e,record:t}=await Os(o,j);process.stdout.write(n.gray(e.split("\n").map(s=>" "+s).join("\n"))+"\n"),X.session.messages.push({role:"user",content:t}),l(X.session);continue}if(s.startsWith("/")){let o;z=new AbortController;try{o=await Is(s,as())}catch(s){process.stdout.write((z.signal.aborted?"操作已取消":"命令失败:"+s.message)+"\n");continue}finally{z=null}if($.closed)break;if("exit"===o.action)break;if("clear"===o.action){F&&(clearInterval(F),F=null);const s=Y.getMode();Y=i({yolo:!1,ask:H,select:G,cwd:j}),Y.setMode(s),X.permissions=Y,l(X.session),v(X.session.id),X.session=c({cwd:j,model:u.model,messages:[X.session.messages[0]]}),u.sessionId=X.session.id,Z.length=0,K.list=[],Q.list.length=0,process.stdout.write(n.gray("已开启新会话(旧会话已保存,可用huicai --resume恢复)。\n"))}"refresh"===o.action&&Ds(X.session),"cd"===o.action&&(j=o.cwd,X.cwd=j,Y=i({yolo:Y.isYolo(),ask:H,select:G,cwd:j}),X.permissions=Y,y(es),({extraTools:os,clients:es,crew:ts}=await buildExtraTools(u,j,{ask:H,askUi:V,sessionOf:()=>X.session,queueInject:s=>Z.push(s),signalRef:ss,runnerHint:"到点由调度器在该目录自动执行(终端里需有 `huicai daemon` 常驻;桌面端保持应用运行亦可)。"})),X.session=c({cwd:j,model:u.model,messages:[{role:"system",content:g(j,{memory:!1!==u.memoryEnabled,config:u})}]}),K.list=[],Q.list.length=0,u.sessionId=X.session.id,process.stdout.write(n.gray("已切换到 "+j+"(新会话,旧会话已保存)。\n"))),"autofix"===o.action&&await w({command:o.command,maxIters:o.maxIters,cwd:j,runTurnOnce:s=>ns(s),emit:s=>process.stdout.write(n.gray(s)+"\n")}),"autorun"===o.action&&await h({goal:o.goal,config:u,permissions:Y,cwd:j,runTurnOnce:s=>ns(s),emit:s=>process.stdout.write(n.gray(" · "+s)+"\n")}),"loop"===o.action&&(F&&(clearInterval(F),F=null),o.stop?process.stdout.write(n.gray("已停止循环。\n")):(process.stdout.write(n.gray(`已开始循环:每 ${o.seconds}s运行「${o.prompt}」(/loop off停止)。\n`)),F=setInterval(()=>{z||ns(o.prompt)},1e3*o.seconds))),o.prompt&&await is(o.prompt);continue}await is(s)}}}return F&&clearInterval(F),$.close(),y(es),S(),T.updater?.dispose(),M?{updated:M}:D?{relogin:!0}:(process.stdout.write(n.gray("\n再见。\n")),{})}function Vs(s){return"string"==typeof s?s:Array.isArray(s)?s.map(s=>"string"==typeof s?s:s&&"text"===s.type&&s.text||"").join(""):""}function Ds(s){const o=(s.messages||[]).filter(s=>"user"===s.role||"assistant"===s.role),e=u(s.messages);if(0===e)return;process.stdout.write(n.gray(` 已恢复会话「${s.title||""}」(${e} 轮)——历史对话如下:`)+"\n");const t=o.length>40?o.length-40:0;t&&process.stdout.write(n.gray(` (较早 ${t} 条已省略,完整记录在 ~/.huicai/sessions)`)+"\n");for(const s of o.slice(-40)){const o=Vs(s.content).trim();if(!o)continue;const e=o.length>800?o.slice(0,800)+"…":o,t="user"===s.role?n.bold(" 你 › "):n.gray(" 助手 › ");process.stdout.write(t+e.replace(/\n/g,"\n ")+"\n")}process.stdout.write(n.gray(" ——以上为历史,可继续对话——")+"\n")}export async function runOnce({config:s,cwd:o,yolo:e,prompt:t,session:r,persist:c,outputFormat:m,maxTurns:d}){t||(process.stderr.write('用法: huicai -p "你的指令"\n'),process.exit(1));const f="json"===m,g=i({yolo:e,ask:null,cwd:o}),w=[],h={current:null},j=new AbortController;h.current=j.signal;let b=!1;const k=()=>{b&&process.exit(130),b=!0,process.stderr.write(n.gray("\n已请求中断(再按一次Ctrl-C强制退出)…\n")),j.abort()};process.on("SIGINT",k);const{extraTools:x,clients:I}=await buildExtraTools(s,o,{sessionOf:()=>r,queueInject:s=>w.push(s),signalRef:h,runnerHint:"到点由调度器在该目录自动执行(终端里需有 `huicai daemon` 常驻;桌面端保持应用运行亦可)。"});let T=t;if(t.trim().startsWith("/")){const{runSlash:e}=await import("./commands.js"),n=[],i=await e(t.trim(),{cwd:o,config:s,session:r,permissions:g,print:s=>n.push(String(s))}).catch(s=>({handled:!1,error:s}));if(n.length&&process.stdout.write(n.join("\n")+"\n"),i&&i.prompt)T=i.prompt;else if(i&&i.handled)return y(I),process.off("SIGINT",k),{text:n.join("\n")}}const{text:v,images:$}=await $s(T,o,s);r.messages.push({role:"user",content:qs(v,$)}),r.compaction||(r.compaction={}),s.sessionId=r.id;const q=e&&!1!==s.wakeLockInAuto?W({reason:"无人值守任务执行中"}):null;let O;try{O=await a({config:s,messages:r.messages,permissions:g,cwd:o,extraTools:x,signal:j.signal,maxIters:d,compaction:r.compaction,io:f?{write:()=>{}}:void 0,pullQueued:()=>w.shift()||null})}finally{q?.ok&&B()}O?.lastPromptTokens&&(r.lastPromptTokens=O.lastPromptTokens),O?.lastUsage&&(r.lastUsage=O.lastUsage),c&&(l(r),p(r)),process.removeListener("SIGINT",k),y(I),S();const P=O?.error?vs(O):"";if(P&&(Ss(O,s,s=>process.stderr.write(s)),"auth"===P&&process.stderr.write(n.gray("请重新运行 `huicai login` 登录后重试。")+"\n")),f){const o=[...r.messages].reverse().find(s=>"assistant"===s.role&&s.content);process.stdout.write(JSON.stringify({type:"result",is_error:!!O?.error,...O?.error?{error:O.error}:{},...P?{error_kind:P}:{},result:o?String(o.content):"",usage:{input_tokens:O?.usage?.prompt??0,output_tokens:O?.usage?.completion??0},session_id:r.id,model:s.model,num_turns:u(r.messages)})+"\n")}O?.error&&process.exit(1)}
|
package/src/session.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"node:fs";import e from"node:path";import{
|
|
1
|
+
import t from"node:fs";import e from"node:path";import{randomUUID as n}from"node:crypto";import{CONFIG_DIR as r}from"./config.js";import{dropSessionSpill as o}from"./spill.js";import{dropSessionJobs as i}from"./jobs.js";import{dropSessionObservations as s}from"./fs-observe.js";import{dropSessionTerminals as c}from"./terminal.js";import{dropSessionLog as l}from"./sessionlog.js";export const SESSIONS_DIR=e.join(r,"sessions");function a(){t.mkdirSync(SESSIONS_DIR,{recursive:!0})}function u(t){return e.join(SESSIONS_DIR,t+".json")}export function userKeyOf(t){const e=t?.user;if(!e||"object"!=typeof e)return"";const n=e.id??e.userId??e.username??"";return null==n?"":String(n)}export function turnCount(t){return(t||[]).filter(t=>"user"===t.role).length}export function createSession({cwd:t,model:e,messages:n,parentSession:r,origin:o,delegationDepth:i,label:s,user:c}){const l=Date.now(),a={id:Date.now().toString(36)+"-"+Math.random().toString(36).slice(2,6),cwd:t,model:e,createdAt:l,updatedAt:l,title:"",messages:n||[],compaction:{}};return r&&(a.parentSession=String(r)),o&&(a.origin=String(o)),Number.isFinite(i)&&(a.delegationDepth=Math.max(0,Math.floor(i))),s&&(a.label=String(s).replace(/\s+/g," ").slice(0,120)),c&&(a.user=String(c)),a}export function saveSession(e){if(!e||!e.id)return;let r;try{a(),e.updatedAt=Date.now();const o=JSON.stringify(e),i=u(e.id);r=i+"."+n()+".tmp",t.writeFileSync(r,o,{mode:384,flag:"wx"}),t.renameSync(r,i)}catch(t){process.stderr.write(`(会话保存失败,不影响对话:${t.message})\n`)}finally{if(r)try{t.unlinkSync(r)}catch{}}}export function loadSession(e){if(!e)return null;try{const n=JSON.parse(t.readFileSync(u(e),"utf8"));return n&&Array.isArray(n.messages)?(n.id||(n.id=e),n.compaction&&"object"==typeof n.compaction||(n.compaction={}),n):null}catch{return null}}export function listSessions(n){let r;try{a(),r=t.readdirSync(SESSIONS_DIR).filter(t=>t.endsWith(".json"))}catch{return[]}const o=[];for(const i of r)try{const r=JSON.parse(t.readFileSync(e.join(SESSIONS_DIR,i),"utf8"));if(n&&r.cwd!==n)continue;o.push({id:r.id||i.replace(/\.json$/,""),title:r.title||"(未命名会话)",cwd:r.cwd,model:r.model,updatedAt:r.updatedAt||0,turns:turnCount(r.messages),user:r.user?String(r.user):"",origin:r.origin||"",parentSession:r.parentSession||"",delegationDepth:Number.isFinite(r.delegationDepth)?r.delegationDepth:0,label:r.label||""})}catch{}return o.sort((t,e)=>e.updatedAt-t.updatedAt),o}export function mostRecent(t){const e=listSessions(t);return e.length?e[0].id:null}export function deleteSession(e,n){const r=n||new Set;if(r.has(e))return!1;r.add(e);for(const t of listChildSessions(e))deleteSession(t.id,r);let a=!1;try{t.unlinkSync(u(e)),a=!0}catch{a=!1}return o(e),i(e),c(e),l(e),s(e),a}export function listChildSessions(t){return t?listSessions(null).filter(e=>e.parentSession===String(t)):[]}export function relTime(t){if(!t)return"未知时间";const e=Math.max(0,Math.floor((Date.now()-t)/1e3));if(e<60)return e+" 秒前";const n=Math.floor(e/60);if(n<60)return n+" 分钟前";const r=Math.floor(n/60);if(r<24)return r+" 小时前";return Math.floor(r/24)+" 天前"}
|
package/src/test-runner.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"node:fs";import t from"node:path";import s from"node:os";import{captureCommand as n}from"./autofix.js";const r=[{id:"maven",detect:s=>e.existsSync(t.join(s,"pom.xml")),command:e=>"mvn -q -B test"+(e?` -Dtest=${o(e)}`:""),reports:["**/target/surefire-reports","**/target/failsafe-reports"],format:"junit-xml"},{id:"gradle",detect:s=>e.existsSync(t.join(s,"build.gradle"))||e.existsSync(t.join(s,"build.gradle.kts")),command:e=>("win32"===process.platform?"gradlew.bat":"./gradlew")+" test"+(e?` --tests ${o(e)}`:""),reports:["**/build/test-results/test"],format:"junit-xml"},{id:"pytest",detect:s=>e.existsSync(t.join(s,"pytest.ini"))||e.existsSync(t.join(s,"tox.ini"))||e.existsSync(t.join(s,"conftest.py"))||e.existsSync(t.join(s,"pyproject.toml"))&&/\[tool\.pytest/.test(i(t.join(s,"pyproject.toml")))||a(s,e=>/^test_.*\.py$|_test\.py$/.test(e)),command:(e,t)=>`pytest -q --junit-xml=${o(t)}`+(e?` -k ${o(e)}`:""),format:"junit-xml",ownXml:!0},{id:"node",detect:s=>{const n=function(t){try{return JSON.parse(e.readFileSync(t,"utf8"))}catch{return null}}(t.join(s,"package.json"));return!!(n&&n.scripts&&n.scripts.test)},command:e=>"npm test --silent"+(e?` -- --test-name-pattern=${o(e)}`:""),format:"tap"}];function i(t){try{return e.readFileSync(t,"utf8")}catch{return""}}function a(s,n,r=2){try{for(const i of e.readdirSync(s,{withFileTypes:!0}))if(!i.name.startsWith(".")&&"node_modules"!==i.name){if(i.isFile()&&n(i.name))return!0;if(i.isDirectory()&&r>0&&a(t.join(s,i.name),n,r-1))return!0}}catch{}return!1}function o(e){const t=String(e);return/^[\w./:@=-]+$/.test(t)?t:`'${t.replace(/'/g,"'\\''")}'`}export function detectRunner(e,t){return t?r.find(e=>e.id===t)||null:r.find(t=>{try{return t.detect(e)}catch{return!1}})||null}const c=/<testcase\b([^>]*?)(\/>|>([\s\S]*?)<\/testcase>)/g,l=/(\w[\w:.-]*)\s*=\s*"([^"]*)"/g,p=/<(failure|error)\b([^>]*?)(?:\/>|>([\s\S]*?)<\/\1>)/;function u(e){const t={};let s;for(l.lastIndex=0;s=l.exec(e);)t[s[1]]=d(s[2]);return t}function d(e){return String(e).replace(/</g,"<").replace(/>/g,">").replace(/"/g,'"').replace(/'/g,"'").replace(/ /g,"\n").replace(/	/g,"\t").replace(/&/g,"&")}export function parseJUnitXml(e,t){let s;for(c.lastIndex=0;s=c.exec(e);){const e=u(s[1]),n=s[3]||"";if(t.total+=1,t.timeMs+=Math.round(1e3*Number(e.time||0)),/<skipped\b/.test(n)){t.skipped+=1;continue}const r=p.exec(n);if(!r){t.passed+=1;continue}const i=u(r[2]||"");t.failed+=1,t.failures.push({suite:e.classname||e.class||"",name:e.name||"(未命名用例)",kind:"error"===r[1]?"报错":"断言失败",message:(i.message||"").trim(),type:i.type||"",stack:d(r[3]||"").trim()})}return t}function m(s,n=[],r=6){let i=[];try{i=e.readdirSync(s,{withFileTypes:!0})}catch{return n}for(const e of i){const i=t.join(s,e.name);if(e.isDirectory()){if("node_modules"===e.name||e.name.startsWith("."))continue;r>0&&m(i,n,r-1)}else(/^TEST-.*\.xml$/.test(e.name)||/\.xml$/.test(e.name))&&n.push(i)}return n}function f(s,n=4){const r=[],i=(s,n)=>{let a=[];try{a=e.readdirSync(s,{withFileTypes:!0})}catch{return}for(const e of a){if(!e.isDirectory()||e.name.startsWith(".")||"node_modules"===e.name)continue;const a=t.join(s,e.name);"surefire-reports"===e.name||"failsafe-reports"===e.name||"test-results"===e.name?r.push(a):n>0&&i(a,n-1)}};return i(s,n),r}const y=/^[✔✓]\s+(.+?)\s*\(\d[\d.]*\s*ms\)\s*$/,x=/^[✖✗×]\s+(.+?)\s*\(\d[\d.]*\s*ms\)\s*$/;export function parseNodeSpec(e,t){const s=String(e||""),n=s.split("\n"),r=e=>{const t=new RegExp(`^\\s*[ℹi]\\s+${e}\\s+(\\d+)\\s*$`,"m").exec(s);return t?Number(t[1]):null},i=r("pass"),a=r("fail"),o=r("skipped");if(null!==i||null!==a)t.passed+=i||0,t.failed+=a||0,t.skipped+=o||0,t.total+=(i||0)+(a||0)+(o||0);else for(const e of n){const s=e.trim();y.test(s)?(t.total+=1,t.passed+=1):x.test(s)&&(t.total+=1,t.failed+=1)}const c=n.findIndex(e=>/failing tests:/.test(e));if(c<0)return t;let l=null;const p=new Set;for(const e of n.slice(c+1)){const s=e.trim(),n=x.exec(s);if(n){if(p.has(n[1])){l=null;continue}p.add(n[1]),l={suite:"",name:n[1],kind:"断言失败",message:"",type:"",stack:""},t.failures.push(l);continue}if(!l)continue;const r=/^actual:\s*(.+?),?$/.exec(s),i=/^expected:\s*(.+?),?$/.exec(s);r?l.actual=r[1]:i?l.expected=i[1]:/Error|AssertionError/.test(s)&&!l.message?l.message=s:(/^at\s+/.test(s)||/^test at /.test(s))&&(l.stack+=(l.stack?"\n":"")+s)}return t}export function parseTap(e,t){const s=String(e||"").split("\n");let n=null;for(const e of s){const s=e.replace(/^\s*/,""),r=/^ok\s+\d+\s*-?\s*(.*)$/.exec(s),i=/^not ok\s+\d+\s*-?\s*(.*)$/.exec(s);if(r)t.total+=1,/#\s*SKIP/i.test(r[1])?t.skipped+=1:t.passed+=1,n=null;else if(i)t.total+=1,t.failed+=1,n={suite:"",name:i[1].trim(),kind:"断言失败",message:"",type:"",stack:""},t.failures.push(n);else if(n){/^(?:error|expected|actual|operator):\s*(.*)$/i.exec(s)&&!n.message?n.message=s.trim():/^at\s+/.test(s)&&(n.stack+=(n.stack?"\n":"")+s.trim())}const a=/^#\s*(pass|fail|skipped)\s+(\d+)$/.exec(s);a&&0===t.total&&("pass"===a[1]&&(t.passed=Number(a[2])),"fail"===a[1]&&(t.failed=Number(a[2])),"skipped"===a[1]&&(t.skipped=Number(a[2])),t.total=t.passed+t.failed+t.skipped)}return t}const g=new RegExp(["at\\s+(java|javax|jdk|sun|com\\.sun|kotlin)\\.","org\\.(junit|mockito|hamcrest|assertj|testng|springframework|apache\\.maven|gradle)","surefire|failsafe|\\breflect\\b|jdk\\.internal","pytest|_pytest|site-packages|unittest[/\\\\.]","node:internal|node_modules|internal/test_runner","System\\.(Runtime|Threading|Reflection)|xunit|nunit|Microsoft\\.VisualStudio\\.TestPlatform"].join("|"),"i"),$=/([Tt]ests?\.(java|py|cs)|_test\.py|test_\w+\.py|\.test\.[cm]?[jt]s|\.spec\.[cm]?[jt]s)/;export function locateSourceFrame(e){let t=null;for(const s of String(e||"").split("\n")){const e=s.trim();if(!e||g.test(e))continue;let n=/\(([\w./$-]+\.(?:java|kt)):(\d+)\)/.exec(e);if(n||(n=/File\s+"([^"]+\.py)",\s+line\s+(\d+)/.exec(e)),n||(n=/^([\w./$-]+\.py):(\d+):/.exec(e)),n||(n=/\(?([\w./$-]+\.[cm]?[jt]sx?):(\d+)(?::\d+)?\)?$/.exec(e)),!n)continue;const r={file:n[1],line:Number(n[2])};if(!$.test(n[1]))return{...r,isTest:!1};t||(t={...r,isTest:!0})}return t}export function extractAssertion(e){const t=String(e||"").split("\n")[0];let s=/expected:?\s*<?(.+?)>?\s+but\s+(?:was|got):?\s*<?(.+?)>?\s*$/is.exec(t);const n=e=>String(e).trim().replace(/\s+/g," ").replace(/[.,;]$/,"").slice(0,160);return s?{expected:n(s[1]),actual:n(s[2])}:(s=/Expected:\s*(.+?)[\r\n]+\s*(?:Actual|but was):\s*(.+?)$/is.exec(t),s?{expected:n(s[1]),actual:n(s[2])}:(s=/\bassert\s+(.+?)\s*==\s*(.+?)\s*$/is.exec(t),s?{expected:n(s[2]),actual:n(s[1])}:null))}export async function runTests(r,a={}){const o=detectRunner(r,a.framework);if(!o)return{error:"没有探测到测试框架。支持:Maven(pom.xml) / Gradle(build.gradle) / pytest(pytest.ini·conftest.py·test_*.py) / Node(package.json的test脚本)。\n可以用framework参数强制指定,或直接用run_bash跑你自己的命令。"};const c=o.ownXml?t.join(s.tmpdir(),`huicai-tests-${process.pid}-${Date.now()}.xml`):"",l=o.ownXml?[]:f(r),p=new Map;for(const t of l)for(const s of m(t))try{p.set(s,e.statSync(s).mtimeMs)}catch{}const u=o.command(a.filter||"",c),d=Date.now(),{code:y,output:x}=await n(r,u,Number(a.timeoutMs)||6e5),g=Date.now()-d,$={total:0,passed:0,failed:0,skipped:0,timeMs:0,failures:[]};let h="";if("junit-xml"===o.format){const t=o.ownXml?e.existsSync(c)?[c]:[]:f(r).flatMap(e=>m(e)).filter(t=>{try{return!p.has(t)||e.statSync(t).mtimeMs>p.get(t)}catch{return!1}});for(const e of t)parseJUnitXml(i(e),$);if(h=t.length?`${t.length} 份JUnit XML报告`:"",o.ownXml)try{e.unlinkSync(c)}catch{}}else"tap"===o.format&&(parseTap(x,$),0===$.total?(parseNodeSpec(x,$),h=$.total?"node:test输出":""):h="TAP输出");const k=$.total>0;return{runner:o.id,command:u,code:y,wallMs:g,stats:{...$,failures:void 0},failures:$.failures,parsedFrom:h,compiledOk:k,raw:x}}export function renderTestReport(e,s="."){if(e.error)return e.error;const n=e.stats,r=`【测试结果】${e.runner} · ${(e.wallMs/1e3).toFixed(1)}s`;if(!e.compiledOk){const t=String(e.raw||"").split("\n").filter(Boolean).slice(-40).join("\n");return`${r}\n✗ **一个用例都没跑起来**(退出码 ${e.code})——通常是编译失败、依赖缺失或用例收集报错。\n命令:${e.command}\n\n原始输出(末 40 行):\n${t}\n\n先把它跑起来再谈测试结果:编译错误看上面第一个ERROR的文件与行号。`}const i=[`${r} · 解析自${e.parsedFrom}`,`通过 ${n.passed} · 失败 ${n.failed} · 跳过 ${n.skipped} · 共 ${n.total}`];if(0===n.failed)return i.push("✅ 全部通过。"),n.skipped&&i.push(`ℹ 有 ${n.skipped} 个用例被跳过——确认是有意跳过,而不是被skip标记忘了去掉。`),i.join("\n");i.push("");const a=e.failures.slice(0,12);return a.forEach((e,r)=>{const a=e.suite?`${e.suite}.${e.name}`:e.name;i.push(`── 失败 ${r+1}/${n.failed}:${a}`);const o=void 0!==e.expected||void 0!==e.actual?{expected:String(e.expected??"(未给出)"),actual:String(e.actual??"(未给出)")}:extractAssertion(e.message);o?(i.push(` 期望:${o.expected}`),i.push(` 实际:${o.actual}`)):e.message?i.push(` ${e.kind}:${e.message.split("\n")[0].slice(0,200)}`):i.push(` ${e.kind}(无消息)`);const c=locateSourceFrame(e.stack)||locateSourceFrame(e.message);if(c){const e=t.isAbsolute(c.file)?t.relative(s,c.file):c.file;i.push(c.isTest?` → 用例位置:${e}:${c.line}(堆栈里没有业务代码帧)`:` → 被测代码:${e}:${c.line}`)}}),e.failures.length>a.length&&i.push(`…… 另有 ${e.failures.length-a.length} 个失败用例未展开(用filter缩小范围逐个看)。`),i.push(""),i.push("先读「期望vs实际」——两者的差异通常直接指出是业务逻辑错了还是用例的预期写错了;再看「被测代码」那一行(它是堆栈里第一个既非测试、也非框架的位置)。"),i.join("\n")}export function makeTestTools({cwd:s,config:n={}}){return[{readOnly:!1,spec:{type:"function",function:{name:"run_tests",description:"跑单元测试并**把结果解析成结构化摘要**——改完代码验证、排查失败用例时用它,不要用run_bash跑测试。\n两者的差别在于你拿到什么:run_bash给你几千行原始输出(Maven的下载与编译日志、pytest堆栈里十几层框架调用),要你自己从噪声里找哪个用例失败、为什么;本工具直接给出「通过/失败/跳过的数量 + 每个失败用例的期望值与实际值 + **堆栈里第一个非测试非框架的位置**(也就是你的哪一行业务代码出的问题)」。\n自动探测框架:Maven(pom.xml) · Gradle(build.gradle) · pytest(pytest.ini/conftest.py/test_*.py) · Node(package.json的test脚本)。Maven/Gradle/pytest都走JUnit XML报告,结果精确到用例级。\n用filter只跑相关用例(Maven是 -Dtest、Gradle是 --tests、pytest是 -k、Node是用例名模式)——定位问题时比全量跑快得多。\n**一个用例都没跑起来时会如实说明**(编译失败/依赖缺失/收集报错),并给出原始输出末尾,不会伪装成「0 个用例、全部通过」。",parameters:{type:"object",properties:{path:{type:"string",description:"项目目录(相对工作目录),默认当前目录"},filter:{type:"string",description:"只跑匹配的用例。Maven:`OrderServiceTest` 或 `OrderServiceTest#某方法`;Gradle:`*.OrderServiceTest`;pytest:`-k` 表达式如 `settle and not slow`;Node:用例名片段"},framework:{type:"string",description:"强制指定框架(maven / gradle / pytest / node)。不给则自动探测"},timeout:{type:"number",description:"超时毫秒,默认 600000(10 分钟)"}},required:[]}}},run:async n=>{const r=t.resolve(s,String(n?.path||".").trim()||".");if(r!==s&&!r.startsWith(s+t.sep))return"路径越出工作目录:"+n.path;if(!e.existsSync(r))return"路径不存在:"+n.path;try{return renderTestReport(await runTests(r,{filter:n?.filter,framework:n?.framework,timeoutMs:n?.timeout}),r)}catch(e){return"测试未能运行:"+(e.message||String(e))}}}]}
|
|
1
|
+
import e from"node:fs";import t from"node:path";import s from"node:os";import{captureCommand as n}from"./autofix.js";const r=[{id:"maven",detect:s=>e.existsSync(t.join(s,"pom.xml")),command:e=>"mvn -q -B test"+(e?` -Dtest=${o(e)}`:""),reports:["**/target/surefire-reports","**/target/failsafe-reports"],format:"junit-xml"},{id:"gradle",detect:s=>e.existsSync(t.join(s,"build.gradle"))||e.existsSync(t.join(s,"build.gradle.kts")),command:e=>("win32"===process.platform?"gradlew.bat":"./gradlew")+" test"+(e?` --tests ${o(e)}`:""),reports:["**/build/test-results/test"],format:"junit-xml"},{id:"pytest",detect:s=>e.existsSync(t.join(s,"pytest.ini"))||e.existsSync(t.join(s,"tox.ini"))||e.existsSync(t.join(s,"conftest.py"))||e.existsSync(t.join(s,"pyproject.toml"))&&/\[tool\.pytest/.test(i(t.join(s,"pyproject.toml")))||a(s,e=>/^test_.*\.py$|_test\.py$/.test(e)),command:(e,t)=>`pytest -q --junit-xml=${o(t)}`+(e?` -k ${o(e)}`:""),format:"junit-xml",ownXml:!0},{id:"node",detect:s=>{const n=function(t){try{return JSON.parse(e.readFileSync(t,"utf8"))}catch{return null}}(t.join(s,"package.json"));return!!(n&&n.scripts&&n.scripts.test)},command:e=>"npm test --silent"+(e?` -- --test-name-pattern=${o(e)}`:""),format:"tap"}];function i(t){try{return e.readFileSync(t,"utf8")}catch{return""}}function a(s,n,r=2){try{for(const i of e.readdirSync(s,{withFileTypes:!0}))if(!i.name.startsWith(".")&&"node_modules"!==i.name){if(i.isFile()&&n(i.name))return!0;if(i.isDirectory()&&r>0&&a(t.join(s,i.name),n,r-1))return!0}}catch{}return!1}function o(e){const t=String(e);return/^[\w./:@=-]+$/.test(t)?t:`'${t.replace(/'/g,"'\\''")}'`}export function detectRunner(e,t){return t?r.find(e=>e.id===t)||null:r.find(t=>{try{return t.detect(e)}catch{return!1}})||null}const c=/<testcase\b([^>]*?)(\/>|>([\s\S]*?)<\/testcase>)/g,l=/(\w[\w:.-]*)\s*=\s*"([^"]*)"/g,p=/<(failure|error)\b([^>]*?)(?:\/>|>([\s\S]*?)<\/\1>)/;function u(e){const t={};let s;for(l.lastIndex=0;s=l.exec(e);)t[s[1]]=d(s[2]);return t}function d(e){return String(e).replace(/</g,"<").replace(/>/g,">").replace(/"/g,'"').replace(/'/g,"'").replace(/ /g,"\n").replace(/	/g,"\t").replace(/&/g,"&")}export function parseJUnitXml(e,t){let s;for(c.lastIndex=0;s=c.exec(e);){const e=u(s[1]),n=s[3]||"";if(t.total+=1,t.timeMs+=Math.round(1e3*Number(e.time||0)),/<skipped\b/.test(n)){t.skipped+=1;continue}const r=p.exec(n);if(!r){t.passed+=1;continue}const i=u(r[2]||"");t.failed+=1,t.failures.push({suite:e.classname||e.class||"",name:e.name||"(未命名用例)",kind:"error"===r[1]?"报错":"断言失败",message:(i.message||"").trim(),type:i.type||"",stack:d(r[3]||"").trim()})}return t}function m(s,n=[],r=6){let i=[];try{i=e.readdirSync(s,{withFileTypes:!0})}catch{return n}for(const e of i){const i=t.join(s,e.name);if(e.isDirectory()){if("node_modules"===e.name||e.name.startsWith("."))continue;r>0&&m(i,n,r-1)}else(/^TEST-.*\.xml$/.test(e.name)||/\.xml$/.test(e.name))&&n.push(i)}return n}function f(s,n=4){const r=[],i=(s,n)=>{let a=[];try{a=e.readdirSync(s,{withFileTypes:!0})}catch{return}for(const e of a){if(!e.isDirectory()||e.name.startsWith(".")||"node_modules"===e.name)continue;const a=t.join(s,e.name);"surefire-reports"===e.name||"failsafe-reports"===e.name||"test-results"===e.name?r.push(a):n>0&&i(a,n-1)}};return i(s,n),r}const y=/^[✔✓]\s+(.+?)\s*\(\d[\d.]*\s*ms\)\s*$/,g=/^[✖✗×]\s+(.+?)\s*\(\d[\d.]*\s*ms\)\s*$/;export function parseNodeSpec(e,t){const s=String(e||""),n=s.split("\n"),r=e=>{const t=new RegExp(`^\\s*[ℹi]\\s+${e}\\s+(\\d+)\\s*$`,"m").exec(s);return t?Number(t[1]):null},i=r("pass"),a=r("fail"),o=r("skipped");if(null!==i||null!==a)t.passed+=i||0,t.failed+=a||0,t.skipped+=o||0,t.total+=(i||0)+(a||0)+(o||0);else for(const e of n){const s=e.trim();y.test(s)?(t.total+=1,t.passed+=1):g.test(s)&&(t.total+=1,t.failed+=1)}const c=n.findIndex(e=>/failing tests:/.test(e));if(c<0)return t;let l=null;const p=new Set;for(const e of n.slice(c+1)){const s=e.trim(),n=g.exec(s);if(n){if(p.has(n[1])){l=null;continue}p.add(n[1]),l={suite:"",name:n[1],kind:"断言失败",message:"",type:"",stack:""},t.failures.push(l);continue}if(!l)continue;const r=/^actual:\s*(.+?),?$/.exec(s),i=/^expected:\s*(.+?),?$/.exec(s);r?l.actual=r[1]:i?l.expected=i[1]:/Error|AssertionError/.test(s)&&!l.message?l.message=s:(/^at\s+/.test(s)||/^test at /.test(s))&&(l.stack+=(l.stack?"\n":"")+s)}return t}export function parseTap(e,t){const s=String(e||"").split("\n");let n=null;for(const e of s){const s=e.replace(/^\s*/,""),r=/^ok\s+\d+\s*-?\s*(.*)$/.exec(s),i=/^not ok\s+\d+\s*-?\s*(.*)$/.exec(s);if(r)t.total+=1,/#\s*SKIP/i.test(r[1])?t.skipped+=1:t.passed+=1,n=null;else if(i)t.total+=1,t.failed+=1,n={suite:"",name:i[1].trim(),kind:"断言失败",message:"",type:"",stack:""},t.failures.push(n);else if(n){/^(?:error|expected|actual|operator):\s*(.*)$/i.exec(s)&&!n.message?n.message=s.trim():/^at\s+/.test(s)&&(n.stack+=(n.stack?"\n":"")+s.trim())}const a=/^#\s*(pass|fail|skipped)\s+(\d+)$/.exec(s);a&&0===t.total&&("pass"===a[1]&&(t.passed=Number(a[2])),"fail"===a[1]&&(t.failed=Number(a[2])),"skipped"===a[1]&&(t.skipped=Number(a[2])),t.total=t.passed+t.failed+t.skipped)}return t}const x=new RegExp(["at\\s+(java|javax|jdk|sun|com\\.sun|kotlin)\\.","org\\.(junit|mockito|hamcrest|assertj|testng|springframework|apache\\.maven|gradle)","surefire|failsafe|\\breflect\\b|jdk\\.internal","pytest|_pytest|site-packages|unittest[/\\\\.]","node:internal|node_modules|internal/test_runner","System\\.(Runtime|Threading|Reflection)|xunit|nunit|Microsoft\\.VisualStudio\\.TestPlatform"].join("|"),"i"),$=/([Tt]ests?\.(java|py|cs)|_test\.py|test_\w+\.py|\.test\.[cm]?[jt]s|\.spec\.[cm]?[jt]s)/;export function locateSourceFrame(e){let t=null;for(const s of String(e||"").split("\n")){const e=s.trim();if(!e||x.test(e))continue;let n=/\(([\w./$-]+\.(?:java|kt)):(\d+)\)/.exec(e);if(n||(n=/File\s+"([^"]+\.py)",\s+line\s+(\d+)/.exec(e)),n||(n=/^([\w./$-]+\.py):(\d+):/.exec(e)),n||(n=/\(?([\w./$-]+\.[cm]?[jt]sx?):(\d+)(?::\d+)?\)?$/.exec(e)),!n)continue;const r={file:n[1],line:Number(n[2])};if(!$.test(n[1]))return{...r,isTest:!1};t||(t={...r,isTest:!0})}return t}export function extractAssertion(e){const t=String(e||"").split("\n")[0];let s=/expected:?\s*<?(.+?)>?\s+but\s+(?:was|got):?\s*<?(.+?)>?\s*$/is.exec(t);const n=e=>String(e).trim().replace(/\s+/g," ").replace(/[.,;]$/,"").slice(0,160);return s?{expected:n(s[1]),actual:n(s[2])}:(s=/Expected:\s*(.+?)[\r\n]+\s*(?:Actual|but was):\s*(.+?)$/is.exec(t),s?{expected:n(s[1]),actual:n(s[2])}:(s=/\bassert\s+(.+?)\s*==\s*(.+?)\s*$/is.exec(t),s?{expected:n(s[2]),actual:n(s[1])}:null))}export async function runTests(r,a={}){const o=detectRunner(r,a.framework);if(!o)return{error:"没有探测到测试框架。支持:Maven(pom.xml) / Gradle(build.gradle) / pytest(pytest.ini·conftest.py·test_*.py) / Node(package.json的test脚本)。\n可以用framework参数强制指定,或直接用run_bash跑你自己的命令。"};const c=o.ownXml?t.join(s.tmpdir(),`huicai-tests-${process.pid}-${Date.now()}.xml`):"",l=o.ownXml?[]:f(r),p=new Map;for(const t of l)for(const s of m(t))try{p.set(s,e.statSync(s).mtimeMs)}catch{}const u=o.command(a.filter||"",c),d=Date.now(),{code:y,output:g}=await n(r,u,Number(a.timeoutMs)||6e5,a.signal),x=Date.now()-d,$={total:0,passed:0,failed:0,skipped:0,timeMs:0,failures:[]};let h="";if("junit-xml"===o.format){const t=o.ownXml?e.existsSync(c)?[c]:[]:f(r).flatMap(e=>m(e)).filter(t=>{try{return!p.has(t)||e.statSync(t).mtimeMs>p.get(t)}catch{return!1}});for(const e of t)parseJUnitXml(i(e),$);if(h=t.length?`${t.length} 份JUnit XML报告`:"",o.ownXml)try{e.unlinkSync(c)}catch{}}else"tap"===o.format&&(parseTap(g,$),0===$.total?(parseNodeSpec(g,$),h=$.total?"node:test输出":""):h="TAP输出");const k=$.total>0;return{runner:o.id,command:u,code:y,wallMs:x,stats:{...$,failures:void 0},failures:$.failures,parsedFrom:h,compiledOk:k,raw:g}}export function renderTestReport(e,s="."){if(e.error)return e.error;const n=e.stats,r=`【测试结果】${e.runner} · ${(e.wallMs/1e3).toFixed(1)}s`;if(!e.compiledOk){const t=String(e.raw||"").split("\n").filter(Boolean).slice(-40).join("\n");return`${r}\n✗ **一个用例都没跑起来**(退出码 ${e.code})——通常是编译失败、依赖缺失或用例收集报错。\n命令:${e.command}\n\n原始输出(末 40 行):\n${t}\n\n先把它跑起来再谈测试结果:编译错误看上面第一个ERROR的文件与行号。`}const i=[`${r} · 解析自${e.parsedFrom}`,`通过 ${n.passed} · 失败 ${n.failed} · 跳过 ${n.skipped} · 共 ${n.total}`];if(0===n.failed&&0===e.code&&n.passed>0)return i.push("✅ 全部通过。"),n.skipped&&i.push(`ℹ 有 ${n.skipped} 个用例被跳过——确认是有意跳过,而不是被skip标记忘了去掉。`),i.join("\n");0===e.code&&0!==n.passed||(i.push(`✗ 验证未通过:退出码 ${e.code},实际通过 ${n.passed} 个用例。`),i.push(String(e.raw||"").split("\n").slice(-20).join("\n"))),i.push("");const a=e.failures.slice(0,12);return a.forEach((e,r)=>{const a=e.suite?`${e.suite}.${e.name}`:e.name;i.push(`── 失败 ${r+1}/${n.failed}:${a}`);const o=void 0!==e.expected||void 0!==e.actual?{expected:String(e.expected??"(未给出)"),actual:String(e.actual??"(未给出)")}:extractAssertion(e.message);o?(i.push(` 期望:${o.expected}`),i.push(` 实际:${o.actual}`)):e.message?i.push(` ${e.kind}:${e.message.split("\n")[0].slice(0,200)}`):i.push(` ${e.kind}(无消息)`);const c=locateSourceFrame(e.stack)||locateSourceFrame(e.message);if(c){const e=t.isAbsolute(c.file)?t.relative(s,c.file):c.file;i.push(c.isTest?` → 用例位置:${e}:${c.line}(堆栈里没有业务代码帧)`:` → 被测代码:${e}:${c.line}`)}}),e.failures.length>a.length&&i.push(`…… 另有 ${e.failures.length-a.length} 个失败用例未展开(用filter缩小范围逐个看)。`),i.push(""),i.push("先读「期望vs实际」——两者的差异通常直接指出是业务逻辑错了还是用例的预期写错了;再看「被测代码」那一行(它是堆栈里第一个既非测试、也非框架的位置)。"),i.join("\n")}export function makeTestTools({cwd:s,config:n={}}){const r=async(n,r,i)=>{const a=t.resolve(s,String(n?.path||".").trim()||".");if(a!==s&&!a.startsWith(s+t.sep))return"路径越出工作目录:"+n.path;if(!e.existsSync(a))return"路径不存在:"+n.path;try{const e=await runTests(a,{filter:n?.filter,framework:n?.framework,timeoutMs:n?.timeout,signal:i});return r?.(e),renderTestReport(e,a)}catch(e){return"测试未能运行:"+(e.message||String(e))}};return[{readOnly:!1,timeoutMs:66e4,spec:{type:"function",function:{name:"run_tests",description:"跑单元测试并**把结果解析成结构化摘要**——改完代码验证、排查失败用例时用它,不要用run_bash跑测试。\n两者的差别在于你拿到什么:run_bash给你几千行原始输出(Maven的下载与编译日志、pytest堆栈里十几层框架调用),要你自己从噪声里找哪个用例失败、为什么;本工具直接给出「通过/失败/跳过的数量 + 每个失败用例的期望值与实际值 + **堆栈里第一个非测试非框架的位置**(也就是你的哪一行业务代码出的问题)」。\n自动探测框架:Maven(pom.xml) · Gradle(build.gradle) · pytest(pytest.ini/conftest.py/test_*.py) · Node(package.json的test脚本)。Maven/Gradle/pytest都走JUnit XML报告,结果精确到用例级。\n用filter只跑相关用例(Maven是 -Dtest、Gradle是 --tests、pytest是 -k、Node是用例名模式)——定位问题时比全量跑快得多。\n**一个用例都没跑起来时会如实说明**(编译失败/依赖缺失/收集报错),并给出原始输出末尾,不会伪装成「0 个用例、全部通过」。",parameters:{type:"object",properties:{path:{type:"string",description:"项目目录(相对工作目录),默认当前目录"},filter:{type:"string",description:"只跑匹配的用例。Maven:`OrderServiceTest` 或 `OrderServiceTest#某方法`;Gradle:`*.OrderServiceTest`;pytest:`-k` 表达式如 `settle and not slow`;Node:用例名片段"},framework:{type:"string",description:"强制指定框架(maven / gradle / pytest / node)。不给则自动探测"},timeout:{type:"number",description:"超时毫秒,默认 600000(10 分钟)"}},required:[]}}},run:e=>r(e),runWithEnv:(e,t,s)=>r(e,t.onTestResult,s)}]}
|
package/src/toolpipe.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"node:fs";import r from"node:path";import{runTool as t,toolTimeoutMs as n,toolTimeoutText as o}from"./tools.js";import{runHooks as s}from"./hooks.js";import{isFileWriteTool as i}from"./permissions.js";import{capOrSpill as a,MAX_TOOL_RESULT_CHARS as l}from"./spill.js";import{toolResultCharBudget as c}from"./context-window.js";import{queryOf as u}from"./crush.js";import{guardOverwrite as f,noteAfterCall as d}from"./fs-observe.js";export function looksBinary(e){return!!e&&e.subarray(0,8e3).includes(0)}export function isBinaryFile(r){let t;try{t=e.openSync(r,"r");const n=Buffer.alloc(8e3),o=e.readSync(t,n,0,n.length,0);return looksBinary(n.subarray(0,o))}catch{return!1}finally{if(void 0!==t)try{e.closeSync(t)}catch{}}}export async function withToolDeadline(e,r,t,n){if(!(r>0))return e(n);const s=new AbortController,i=()=>s.abort();n&&(n.aborted?s.abort():n.addEventListener("abort",i,{once:!0}));let a=null;const l=new Promise(e=>{a=setTimeout(()=>{s.abort(),e({timedOut:!0})},r)});try{const n=await Promise.race([e(s.signal).then(e=>({value:e}),e=>({err:e})),l]);if(n.timedOut)return o(t,r);if(n.err)throw n.err;return n.value}finally{a&&clearTimeout(a),n?.removeEventListener?.("abort",i)}}const
|
|
1
|
+
import e from"node:fs";import r from"node:path";import{runTool as t,toolTimeoutMs as n,toolTimeoutText as o}from"./tools.js";import{runHooks as s}from"./hooks.js";import{isFileWriteTool as i}from"./permissions.js";import{capOrSpill as a,MAX_TOOL_RESULT_CHARS as l}from"./spill.js";import{toolResultCharBudget as c}from"./context-window.js";import{queryOf as u}from"./crush.js";import{guardOverwrite as f,noteAfterCall as d}from"./fs-observe.js";import{engineeringStage as m}from"./engineering.js";export function looksBinary(e){return!!e&&e.subarray(0,8e3).includes(0)}export function isBinaryFile(r){let t;try{t=e.openSync(r,"r");const n=Buffer.alloc(8e3),o=e.readSync(t,n,0,n.length,0);return looksBinary(n.subarray(0,o))}catch{return!1}finally{if(void 0!==t)try{e.closeSync(t)}catch{}}}export async function withToolDeadline(e,r,t,n){if(!(r>0))return e(n);const s=new AbortController,i=()=>s.abort();n&&(n.aborted?s.abort():n.addEventListener("abort",i,{once:!0}));let a=null;const l=new Promise(e=>{a=setTimeout(()=>{s.abort(),e({timedOut:!0})},r)});try{const n=await Promise.race([e(s.signal).then(e=>({value:e}),e=>({err:e})),l]);if(n.timedOut)return o(t,r);if(n.err)throw n.err;return n.value}finally{a&&clearTimeout(a),n?.removeEventListener?.("abort",i)}}const g={name:"validate",before(e){e.argsError&&(e.result=e.argsError,e.settled=!0)}},p={name:"authorize",async before(e,r){const t=await r.permissions.request(e.name,e.args,r.cwd,e.readOnly||e.deferToIde);t.policyRule&&(e.policyRule=t.policyRule),t.allow||(e.denied=t.reason||"未授权,未执行。")}};function h(e,r){return{cwd:r.cwd,toolName:e.name,args:e.args,callId:e.callId||"",sessionId:r.config?.sessionId||"",model:r.config?.model||"",turn:r.turn??""}}const y={name:"hooks",skipReadOnly:!0,before(e,r){if(!r.hooksActive)return;const t=s("PreToolUse",{...h(e,r)},r.hooks);for(const e of t.outputs)r.info("钩子·PreToolUse:"+r.shortLine(e.stdout,120));t.block?e.denied="PreToolUse钩子阻断:"+t.reason:t.modifiedArgs&&(e.args={...e.args,...t.modifiedArgs},r.info("钩子·PreToolUse已修改工具参数"))},after(e,r){if(!r.hooksActive)return;const t=s("PostToolUse",{...h(e,r),result:String(e.result)},r.hooks);if(t.outputs.length){for(const e of t.outputs)r.info("钩子·PostToolUse:"+r.shortLine(e.stdout,120));e.result=String(e.result)+"\n\n[PostToolUse钩子输出]\n"+t.outputs.map(e=>e.stdout).join("\n")}}},w={name:"journal",skipReadOnly:!0,before(t,n){if(!i(t.name)||!t.args?.path)return;const o=String(t.args.path),s=r.resolve(n.cwd,o);let a=null;try{a=e.readFileSync(s)}catch{}const l=looksBinary(a);if(t.meta.before=l?null:a?a.toString("utf8"):"",!n.fileJournal)return;const c=!!a||e.existsSync(s);a&&a.length>2097152?n.fileJournal.push({path:o,tooLarge:!0}):l?n.fileJournal.push({path:o,before:a.toString("base64"),encoding:"base64",existed:c}):n.fileJournal.push({path:o,before:t.meta.before,existed:c})}},b={name:"inbound-scan",async after(e,r){if(!1===r.config?.inboundScan)return;const t=function(e){const r=e.extra;if(r?.mcpServer)return r.mcpOrigin?`${r.mcpServer}@${r.mcpOrigin}`:`MCP:${r.mcpServer}`;if(r?.networkSource)return String(r.networkSource);if(S.has(e.name)){const r=e.args?.url||e.args?.uri||"";let t="";try{t=r?new URL(String(r)).host:""}catch{t=""}return t||e.name}return null}(e);if(!t)return;const n=String(e.result??"");if(!n.trim())return;const{scanUntrustedText:o,fenceUntrusted:s}=await import("./skill-scan.js"),i=o(n,{source:t,label:e.name});if(i.ok){if(e.meta.inboundScan={source:t,verdict:i.verdict,counts:i.counts},i.counts.critical){const o=i.findings.filter(e=>"critical"===e.severity).slice(0,3).map(e=>e.message).join(";");return r.info(`已给来自 ${t} 的结果加不可信围栏:命中 ${i.counts.critical} 处注入话术(${o})`),void(e.result=s(n,t))}i.counts.warn&&r.config?.debug&&r.info(`来自 ${t} 的结果含 ${i.counts.warn} 处可疑表述(未拦截,仅记录)`)}}};const S=new Set(["fetch_url"]),v={name:"observe",before(e,r){if(!1===r.config?.readBeforeWrite)return;const t=f({sessionId:r.config?.sessionId||"",cwd:r.cwd,name:e.name,args:e.args});t&&(e.denied=t)},after(e,r){const t=/^工具执行失败|^读取失败|不存在/.test(String(e.result??""));d({sessionId:r.config?.sessionId||"",cwd:r.cwd,name:e.name,args:e.args,failed:t})}};export const DEFAULT_STAGES=[g,v,p,y,m,w,b];export async function runToolCall(e,r){e.meta=e.meta||{},e.denied=null,e.settled=!1;const o=(r.stages||DEFAULT_STAGES).filter(r=>!(r.skipReadOnly&&e.readOnly)),s=[];for(const t of o){if(t.before){if(await t.before(e,r),null!=e.denied)return e;if(e.settled)return e}s.push(t)}r.onStart?.(e),e.result=await async function(e,r){const{config:o,cwd:s,signal:i}=r;let f;try{f=await withToolDeadline(n=>e.extra?"function"==typeof e.extra.runWithEnv?e.extra.runWithEnv(e.args,{...r,toolCallId:e.callId},n):e.extra.run(e.args):t(e.name,e.args,s,o,n),n(e.name,e.extra),e.name,i)}catch(e){f=`工具执行失败:${e.message}`}return e.nested?String(f):a(String(f),{sessionId:o.sessionId||"",toolName:e.name,callId:e.callId,enabled:!1!==o.spill,maxChars:c(o,l),query:u(e.name,e.args),crush:!1!==o.crush})}(e,r);for(let t=s.length-1;t>=0;t--)s[t].after&&await s[t].after(e,r);return e}export function makeDispatch(e){return async(r,t)=>{const n=e.resolveTool?e.resolveTool(r):null;if(!n)return{ok:!1,denied:`未知工具:${r}`};const o=await runToolCall({name:r,args:t||{},readOnly:!!n.readOnly,extra:n.extra,callId:`nested-${r}-${Date.now().toString(36)}`,nested:!0},e);return null!=o.denied?{ok:!1,denied:o.denied}:{ok:!0,result:String(o.result??"")}}}
|
package/src/tui/app.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import r,{useState as e,useRef as t,useReducer as n,useEffect as o,useMemo as s}from"react";import{syncPlanUser as c}from"../plan-limits.js";import{exec as u}from"node:child_process";import{render as i,Box as l,Text as a,useInput as m,usePaste as f,useApp as d,useStdout as p,useStdin as g}from"ink";import{runTurn as h}from"../agent.js";import{fetchUsage as x}from"../api.js";import{toolLabel as w}from"../render.js";import{renderMarkdown as v,looksLikeMarkdown as b}from"../markdown.js";import{createSession as k,saveSession as y}from"../session.js";import{ensureSessionTitle as j}from"../title.js";import{createCheckpoints as I}from"../checkpoints.js";import{buildExtraTools as T}from"../repl.js";import{buildSystemPrompt as M}from"../context.js";import{closeMcp as $}from"../mcp.js";import{runSlash as A,listCommands as S}from"../commands.js";import{matchCommands as P}from"../command-ui.js";import{menuOptions as U}from"./command-menu.js";import{questionSelection as E,approvalSelection as R,initialSelection as D,selectionKey as q,insertSelectionText as L}from"../selection.js";import{parseAskAnswer as C}from"../ask-ui.js";import{usageStatus as O,fmtReset as G}from"../usage.js";import{handleAccountError as W}from"../account.js";import{pricingUrl as N}from"../errkind.js";import{expandAtMentions as Y,buildUserContent as F,loadHistory as H,appendHistory as Q}from"../input.js";import{listWorkspaceFiles as z,runBash as K}from"../executors.js";import{loadAllowRules as V,loadDenyRules as _,matchAllowRules as B,suggestRule as J,addProjectAllowRule as X,riskWarning as Z}from"../settings.js";import{PLAN_DENY_REASON as rr,SANDBOX_DENY_REASON as er,sandboxBlocked as tr,isFileWriteTool as nr,AUTOMATION_TOOLS as or,automationActionLabel as sr,automationDetail as cr,PERMISSION_MODES as ur,MODE_ICON as ir,MODE_LABEL as lr}from"../permissions.js";import{runVerifyLoop as ar}from"../autofix.js";import{runAutoFlow as mr}from"../autorun.js";import{acquireWakeLock as fr,releaseWakeLock as dr}from"../presence.js";import{notifyTurnDone as pr}from"../notify.js";import{closeAllTerminals as gr,dropSessionTerminals as hr}from"../terminal.js";import{shutdownLsp as xr}from"../lsp.js";import{ensureVerified as wr}from"../sandbox.js";import{runGoalRounds as vr,getGoal as br,setRoundKind as kr}from"../goalround.js";import{Workbench as yr,moveCursor as jr}from"./workbench.js";import{WelcomeBanner as Ir}from"./welcome.js";import{enableMouseTracking as Tr,mouseReport as Mr}from"./scroll.js";const $r=r.createElement;function Ar(r){return(r=Number(r)||0)>=1e3?(r/1e3).toFixed(1)+"k":String(r)}export function App({config:r,cwd:c,session:i,extraTools:l,mcp:a,yolo:gr,initialPrompt:xr,version:wr,onExit:Ir,exitState:Sr={},injectQueue:Pr=[],bridge:Ur={}}){const Er=d(),{stdout:Rr}=p(),{stdin:Dr}=g();o(()=>Dr.isTTY?Tr(Rr):void 0,[Dr,Rr]);const[qr,Lr]=e({columns:Rr.columns||80,rows:Rr.rows||24});o(()=>{const r=()=>Lr({columns:Rr.columns||80,rows:Rr.rows||24});return Rr.on("resize",r),()=>Rr.off("resize",r)},[Rr]);const[Cr,Or]=e({offset:0,revision:0}),Gr=r=>{Wr.current.offset=r,Or(e=>({offset:r,revision:e.revision+1}))},Wr=t({max:0,page:10,offset:0}),Nr=r=>Gr(Math.max(0,Math.min(Wr.current.max,Wr.current.offset+r))),Yr=t(null),Fr=t(!1),Hr=t(null),[Qr,zr]=e(null),Kr=t([]),[Vr,_r]=e(""),Br=t(0),[Jr,Xr]=e(c),Zr=t(l),re=t(null);re.current||(re.current=i.messages.filter(r=>["user","assistant"].includes(r.role)&&r.content).map((r,e)=>({id:e,kind:r.role,text:"string"==typeof r.content?r.content:r.content.filter(r=>"text"===r.type).map(r=>r.text).join("\n")})));const ee=t(re.current.length),te=t(""),ne=t(i),oe=t(null),se=t(new Set),ce=t(V(Jr)),ue=t(_(Jr)),ie=t(!1),le=t(!1),ae=t(!1),me=t(I()),fe=t(!!gr),de=t(!1),pe=t("idle"),ge=t([]),he=t({list:[]}),xe=t(H(Jr)),we=t(null),ve=t(""),[,be]=n(r=>r+1,0),[ke,ye]=n(r=>r+1,0),[je,Ie]=e("idle"),[Te,Me]=e({text:"",cursor:0}),$e=t({prompt:0,completion:0}),[Ae,Se]=e({prompt:0,completion:0}),[Pe,Ue]=e(0),[Ee,Re]=e([]),[De,qe]=e("hidden"),[Le,Ce]=e(0),[Oe,Ge]=e(gr?"auto":"manual"),We=()=>ie.current?"plan":le.current?"sandbox":fe.current?"auto":ae.current?"acceptEdits":"manual",[Ne,Ye]=e(""),Fe=t(null),[He,Qe]=e(null),ze=t(null),[Ke,Ve]=e(0),[_e,Be]=e(!1),Je=s(()=>S(Jr),[Jr,je]),[Xe,Ze]=e(0),[rt,et]=e(!1),tt=s(()=>{try{return z(Jr)}catch{return[]}},[Jr,je]),[nt,ot]=e(!1),[st,ct]=e(0),ut=t(0),it=(r,e)=>{re.current.push({id:ee.current++,kind:r,text:e}),ye()},lt=()=>{if(te.current){const e=te.current,t=!1!==r.markdown&&b(e);it("assistant",t?v(e):e),te.current=""}},at=r=>{pe.current=r?"busy":"idle",Ie(pe.current)};Ur.signalRef||={current:null},Ur.sessionOf=()=>ne.current;const mt=(r=null)=>{const e=Hr.current,t=Kr.current.shift()||null;Hr.current=t,zr(t),e?.resolve(r)},ft=r=>new Promise(e=>{if(Yr.current?.signal.aborted||Fr.current)return e(null);const t={...D(r),resolve:e};Hr.current?Kr.current.push(t):(Hr.current=t,zr(t),Gr(0))});Ur.askUi=async r=>{const e=await ft(E(r));return"date_range"===r.kind&&e?.value?C(r,e.value):e||{value:null}},Ur.ask=async r=>(await Ur.askUi({kind:"text",question:r}))?.value||"已取消";const dt=()=>{const r=[Hr.current,...Kr.current.splice(0)];Hr.current=null,zr(null);for(const e of r)e?.resolve(null)},pt=()=>{qe("hidden"),Yr.current?.abort(),oe.current?.abort(),dt()};o(()=>()=>{Yr.current?.abort(),Hr.current?.resolve(null),Hr.current=null;for(const r of Kr.current.splice(0))r.resolve(null);Ur.askUi=null,Ur.ask=null},[]),o(()=>{if("busy"!==je)return;const r=setInterval(be,250);return()=>clearInterval(r)},[je]);const gt=async()=>{if(r.apiKey)try{const e=await x(r.baseUrl,r.apiKey);ze.current=e,Qe(e)}catch{}};o(()=>{gt();const r=setInterval(()=>{gt()},18e4);return()=>clearInterval(r)},[]),o(()=>{if(!r.statusLine)return void Ye("");let e=!1,t=null;const n=()=>{t||(t=u(r.statusLine,{cwd:Jr,timeout:3e3,maxBuffer:8192},(r,n)=>{t=null,e||Ye(r?"":n.split("\n")[0].slice(0,80))}))};n();const o=setInterval(n,5e3);return()=>{e=!0,clearInterval(o),t?.kill()}},[r.statusLine,Jr]),o(()=>()=>{Fe.current&&clearInterval(Fe.current)},[]);const ht=t(""),xt={onDelta:r=>{ht.current&&(ht.current=""),te.current+=r,be()},onReasoning:r=>{ht.current=(ht.current+r).slice(-400),be()},onToolStart:(r,e)=>{ht.current="",lt(),_r(w(r,e)),it("tool",w(r,e))},onToolResult:(r,e)=>{_r("整理结果"),it("toolresult",String(e))},onFileDiff:r=>it("diff",r),onSkip:(r,e)=>it("skip",w(r,e)+" — 已跳过"),onInfo:r=>{lt(),it("info",r)},onTodo:r=>{Re(r),Yr.current?.signal.aborted||qe(e=>r.length?"hidden"===e?"expanded":e:"hidden")},onDecision:r=>{lt();const e=(r.alternatives||[]).length?` · ${r.alternatives.length} 备选`:"",t="draft"===r.status?"(待确认草稿)":"";it("info",`◆ 已记录决策:${r.title}${e}${t}`)},onTurnEnd:()=>{qe("hidden"),ht.current="",lt()}},wt={request:(r,e,t,n)=>{if(Yr.current?.signal.aborted)return Promise.resolve({allow:!1,reason:"操作已中断。"});if(ue.current.length&&B(ue.current,r,e))return Promise.resolve({allow:!1,reason:"该操作被拒绝规则阻止(/permissions查看或移除)。"});if(ie.current&&!n)return Promise.resolve({allow:!1,reason:rr});if(le.current&&tr(r))return Promise.resolve({allow:!1,reason:er});if(n||fe.current||se.current.has(r)||B(ce.current,r,e))return Promise.resolve({allow:!0});if(ae.current&&nr(r))return Promise.resolve({allow:!0});const o=or.has(r);return ft(R({name:r,args:e,consent:o,label:o?sr(r,e):w(r,e),detail:o?cr(r,e):"",risk:Z(r,e)})).then(t=>{if(!t||"deny"===t||Yr.current?.signal.aborted)return{allow:!1,reason:"用户未批准此操作。"};if("session"!==t&&"project"!==t||se.current.add(r),"project"===t){const t=J(r,e);try{X(Jr,t),ce.current=V(Jr),it("info",`已写入允许规则:${t} → .huicai/settings.json`)}catch(r){it("info",`规则写入失败(本会话仍生效):${r.message}`)}}return{allow:!0}})},isYolo:()=>fe.current,setYolo:r=>{fe.current=!!r,Ge(We())},setPlanMode:r=>{ie.current=!!r,Ge(We())},isPlanMode:()=>ie.current,setSandbox:r=>{le.current=!!r,Ge(We())},isSandbox:()=>le.current,setAcceptEdits:r=>{ae.current=!!r,Ge(We())},isAcceptEdits:()=>ae.current,setMode:r=>{fe.current="auto"===r,ae.current="acceptEdits"===r,ie.current="plan"===r,le.current="sandbox"===r,Ge(r)},getMode:()=>We(),reloadRules:()=>{ce.current=V(Jr),ue.current=_(Jr)}},vt=()=>{if(Pr.length)return Pr.shift();const r=ge.current.shift();return Ce(ge.current.length),null==r?null:r},bt=()=>{j(ne.current,r).then(r=>{r&&y(ne.current)}).catch(()=>{})},kt=async e=>{qe("hidden");const t=O(ze.current);if(t&&"exhausted"===t.level){const e=N(r.baseUrl);return it("info",`✗ Token用量窗口(${t.label})已用尽,暂无法继续。${G(t.resetAtEpochMs)}`+(e?`\n 升级会员提高上限 → ${e}`:"")),{error:"Token用量已用尽"}}if(Yr.current?.signal.aborted)return{aborted:!0};_r("准备上下文");const{text:n,images:o}=await Y(e,Jr,r);if(Yr.current?.signal.aborted)return{aborted:!0};const s=me.current.snapshot({session:ne.current,label:e});ne.current.messages.push({role:"user",content:F(n,o)}),bt(),at(!0),oe.current=Yr.current||new AbortController,Ur.signalRef.current=oe.current.signal,ne.current.compaction||(ne.current.compaction={});const c="auto"===We()&&!1!==r.wakeLockInAuto?fr({reason:"自动模式任务执行中"}):null,u=Date.now();let i;try{i=await h({config:r,messages:ne.current.messages,permissions:wt,cwd:Jr,extraTools:Zr.current,events:xt,signal:oe.current.signal,todoState:he.current,compaction:ne.current.compaction,pullQueued:vt,fileJournal:s})}finally{qe("hidden"),c?.ok&&dr()}if(i?.aborted||i?.error||pr({durationMs:Date.now()-u,cwd:Jr,summary:String(e).replace(/\s+/g," ").slice(0,80),enabled:!1!==r.notifications}).catch(()=>{}),lt(),i?.usage&&($e.current={prompt:$e.current.prompt+(i.usage.prompt||0),completion:$e.current.completion+(i.usage.completion||0)},Se({...$e.current})),i?.lastPromptTokens&&(Ue(i.lastPromptTokens),ne.current.lastPromptTokens=i.lastPromptTokens),y(ne.current),bt(),(i?.aborted||i?.error)&&ge.current.length&&(ge.current=[],Ce(0),it("info","已清空排队消息")),i?.error){let e="";const t=W(i,r,r=>{e+=r});if(e.trim()&&it("info",e.trim()),"auth"===t)return Sr.relogin=!0,yt(),i}return gt(),i},yt=()=>{Fr.current=!0,pt(),Yr.current||(Ir?.(),Er.exit())},jt=async e=>{kr(ne.current.id,"human");const t=await kt(e);return t?.aborted||t?.error||Fr.current||Yr.current?.signal.aborted||br(ne.current)&&(await vr({session:ne.current,doTurn:kt,stats:()=>({usedTokens:$e.current.prompt+$e.current.completion,budgetTokens:Number(r.budgetTokens)||0}),onInfo:r=>it("info",r),aborted:()=>Fr.current||!!Yr.current?.signal.aborted}),y(ne.current)),t},It=async e=>{const t=e.trim();if(t){if(Yr.current)return/^\/(exit|quit)$/.test(t)?yt():t.startsWith("/")||t.startsWith("!")?void it("info","任务执行中,请先按 Esc 中断,再执行此命令。"):(Me({text:"",cursor:0}),it("user",t),ge.current.push(t),void Ce(ge.current.length));Me({text:"",cursor:0}),Gr(0),we.current=null,Q(Jr,t),xe.current.at(-1)!==t&&xe.current.push(t),Yr.current=new AbortController,oe.current=Yr.current,Ur.signalRef.current=Yr.current.signal,Br.current=Date.now(),qe("hidden"),at(!0);try{for(t.startsWith("/")?await(async e=>{const t=br(ne.current)?.rev,n={version:wr,config:r,cwd:Jr,select:r=>ft({...r,command:e.trim()}),signal:Yr.current?.signal,resumeSession:e=>{Fe.current&&(clearInterval(Fe.current),Fe.current=null),hr(ne.current.id),ne.current=e,r.sessionId=e.id,e.model&&(r.model=e.model),Pr.length=0,se.current.clear(),he.current.list=[],me.current.list.length=0,Re([]),Ue(e.lastPromptTokens||0),re.current=e.messages.filter(r=>["user","assistant"].includes(r.role)&&r.content).map(r=>({id:ee.current++,kind:r.role,text:"string"==typeof r.content?r.content:r.content.filter(r=>"text"===r.type).map(r=>r.text).join("\n")})),te.current="",Gr(0),ye()},usage:$e.current,permissions:wt,extraTools:Zr.current,session:ne.current,checkpoints:me.current,print:r=>it("info",String(r))},o=await A(e,n);if("exit"===o.action)return yt();if("clear"===o.action&&(y(ne.current),Fe.current&&(clearInterval(Fe.current),Fe.current=null),hr(ne.current.id),ne.current=k({cwd:Jr,model:r.model,messages:[ne.current.messages[0]]}),r.sessionId=ne.current.id,Pr.length=0,se.current.clear(),he.current.list=[],me.current.list.length=0,Re([]),Ue(0),re.current=[],te.current="",it("info","已开启新会话(旧会话仍可 --resume恢复)")),"refresh"===o.action&&(he.current.list=[],Re([]),Ue(0)),"autofix"===o.action&&await ar({command:o.command,maxIters:o.maxIters,signal:Yr.current?.signal,cwd:Jr,runTurnOnce:r=>kt(r),emit:r=>it("info",r)}),"autorun"===o.action&&await mr({goal:o.goal,config:r,permissions:wt,cwd:Jr,runTurnOnce:async r=>{const e=await kt(r);if(e?.aborted||e?.error||Yr.current?.signal.aborted)throw new Error("自动执行已停止");return e},emit:r=>it("info",r)}),"cd"===o.action){Fe.current&&(clearInterval(Fe.current),Fe.current=null);const e=await T(r,o.cwd,{queueInject:r=>Pr.push(r),ask:r=>Ur.ask(r),askUi:r=>Ur.askUi(r),sessionOf:()=>ne.current,signalRef:Ur.signalRef});ce.current=V(o.cwd),ue.current=_(o.cwd),hr(ne.current.id),$(a?.clients),Zr.current=e.extraTools,a&&(a.clients=e.clients),ne.current=k({cwd:o.cwd,model:r.model,messages:[{role:"system",content:M(o.cwd,{memory:!1!==r.memoryEnabled,config:r})}]}),r.sessionId=ne.current.id,Pr.length=0,se.current.clear(),xe.current=H(o.cwd),we.current=null,he.current.list=[],me.current.list.length=0,Re([]),Ue(0),re.current=[],te.current="",Xr(o.cwd),it("info","已切换到 "+o.cwd+"(新会话,旧会话已保存)")}if("loop"===o.action&&(Fe.current&&(clearInterval(Fe.current),Fe.current=null),o.stop?it("info","已停止循环。"):(it("info",`已开始循环:每 ${o.seconds}s运行「${o.prompt}」(/loop off停止)`),Fe.current=setInterval(()=>{"busy"!==pe.current&&It(o.prompt)},1e3*o.seconds))),o.prompt)it("user",o.prompt),await jt(o.prompt);else if(/^\/goal\s+/.test(e)&&!/^\/goal\s+(?:status|pause|clear|limit)(?:\s|$)/.test(e)&&"active"===br(ne.current)?.phase&&br(ne.current)?.rev!==t){const r="继续完成用户明确设定的目标:"+br(ne.current).objective;it("user",e),await jt(r)}})(t):t.startsWith("!")&&t.length>1?await(async e=>{it("user","! "+e),_r("$ "+e);const t=await K(Jr,{command:e},r,Yr.current?.signal);it("toolresult",t),ne.current.messages.push({role:"user",content:`我在终端直接执行了命令:$ ${e}\n${t.slice(0,8e3)}`}),y(ne.current)})(t.slice(1).trim()):(it("user",t),await jt(t));ge.current.length&&!Yr.current.signal.aborted&&!Fr.current;){const r=ge.current.shift();Ce(ge.current.length),await jt(r)}}catch(r){lt(),it("info",Yr.current?.signal.aborted?"操作已中断":"执行失败:"+r.message)}finally{dt(),ge.current=[],Ce(0),Yr.current=null,oe.current=null,Ur.signalRef.current=null,at(!1),qe("hidden"),_r(""),Fr.current&&(Ir?.(),Er.exit())}}};o(()=>{!de.current&&xr&&(de.current=!0,It(xr))},[]);const Tt=Qr?null:/^\/(\S*)$/.exec(Te.text),Mt=Tt?Tt[1]:null,$t=null==Mt?[]:P(Je,Mt),At=$t.length>0&&!_e;o(()=>{Ve(0),Be(!1)},[Mt]);const St=Qr||null!=Mt?null:/(?:^|\s)@([^\s@]*)$/.exec(Te.text),Pt=St?St[1]:null,Ut=null==Pt?[]:(()=>{const r=Pt.toLowerCase(),e=[];for(const t of tt){const n=t.toLowerCase(),o=n.split("/").pop();let s=-1;if(r?o.startsWith(r)?s=3:o.includes(r)?s=2:n.includes(r)&&(s=1):s=0,s>=0&&e.push({f:t,rank:s}),!r&&e.length>=50)break}return e.sort((r,e)=>e.rank-r.rank||r.f.length-e.f.length||r.f.localeCompare(e.f)),e.slice(0,50).map(r=>r.f)})(),Et=Ut.length>0&&!rt;o(()=>{Ze(0),et(!1)},[Pt]);const Rt=s(()=>{const r=new Set,e=[];for(let t=xe.current.length-1;t>=0&&e.length<50;t--){const n=xe.current[t];n&&!r.has(n)&&(r.add(n),e.push(n))}return e},[nt]);f(r=>{if(Hr.current){if(Hr.current.editing){const e=L(Hr.current,r);Hr.current=e,zr(e)}return}const e=r.replace(/\r\n?/g,"\n");we.current=null,Me(r=>({text:r.text.slice(0,r.cursor)+e+r.text.slice(r.cursor),cursor:r.cursor+e.length}))}),m((e,t)=>{const n=Mr(e);if(n)if(n.delta&&Hr.current){const r=Hr.current,e={...r,index:Math.max(0,Math.min(U(r).length-1,r.index-Math.sign(n.delta)))};Hr.current=e,zr(e)}else n.delta&&Nr(n.delta);else if(t.ctrl&&"c"===e)Yr.current?(pt(),it("info","已请求中断")):yt();else{if(t.ctrl&&t.end)return Gr(0);if(t.ctrl&&t.home)return Gr(Wr.current.max);if(t.ctrl&&(t.upArrow||t.downArrow))return Nr(t.upArrow?1:-1);if(!t.ctrl||"t"!==e||Hr.current){if(Hr.current){const r=Hr.current;if(r.kind){const n=q(r,e,t);return"result"in n?mt(n.result):(Hr.current=n.menu,void zr(n.menu))}const n=U(r),o=e=>{const t={...r,...e};Hr.current=t,zr(t)};if(t.escape)return mt();if(t.return)return n[r.index]?mt(n[r.index].value):void 0;if(t.upArrow||t.downArrow||t.pageUp||t.pageDown||"segments"===r.layout&&(t.leftArrow||t.rightArrow||t.tab)){const e=t.pageUp||t.pageDown?Math.max(1,Wr.current.menuPage||4):1,s=t.upArrow||t.pageUp||t.leftArrow||t.tab&&t.shift?-e:e;return o({index:Math.max(0,Math.min(n.length-1,r.index+s)),..."segments"===r.layout?{query:""}:{}})}const s=e=>{const t="segments"===r.layout?n.findIndex(r=>[r.label,r.value].join(" ").toLowerCase().includes(e.toLowerCase())):0;o({query:e,index:Math.max(0,t)})};return t.ctrl&&"u"===e?s(""):t.backspace||t.delete?s(r.query.slice(0,jr(r.query,r.query.length,-1))):!e||t.ctrl||t.meta||t.tab?void 0:s(r.query+e.replace(/[\x00-\x1f\x7f]/g,""))}if((t.pageUp||t.pageDown)&&!At&&!Et&&!nt)return Nr((t.pageUp?1:-1)*Math.max(1,Wr.current.page-1));if(t.ctrl&&"p"===e)return Me({text:"/",cursor:1}),void Be(!1);if(nt){if(!Rt.length)return void ot(!1);if(t.upArrow)return ct(r=>(r-1+Rt.length)%Rt.length);if(t.downArrow)return ct(r=>(r+1)%Rt.length);if(t.pageUp)return ct(r=>Math.max(0,r-4));if(t.pageDown)return ct(r=>Math.min(Rt.length-1,r+4));if(t.return){const r=Rt[Math.min(st,Rt.length-1)]||"";return ot(!1),we.current=null,Me({text:r,cursor:r.length})}return t.escape?ot(!1):void 0}if(t.tab&&t.shift&&!Yr.current){const e=ur[(ur.indexOf(wt.getMode())+1)%ur.length];return wt.setMode(o=e),r.planMode="plan"===o,r.sandbox="sandbox"===o,void it("info",`对话模式 → ${ir[o]} ${lr[o]}`+("auto"===o?"(已开放所有权限,任何命令免审批)":""))}var o;if(At){const r=()=>$t[Math.min(Ke,$t.length-1)];if(t.upArrow)return Ve(r=>(r-1+$t.length)%$t.length);if(t.downArrow)return Ve(r=>(r+1)%$t.length);if(t.pageUp)return Ve(r=>Math.max(0,r-4));if(t.pageDown)return Ve(r=>Math.min($t.length-1,r+4));if(t.tab){const e=r().name;return Me({text:"/"+e+" ",cursor:e.length+2})}if(t.return){const e=r();return!e.menu&&e.args?.startsWith("<")?Me({text:"/"+e.name+" ",cursor:e.name.length+2}):void It("/"+e.name)}if(t.escape)return Be(!0)}if(Et){const r=()=>Ut[Math.min(Xe,Ut.length-1)];if(t.upArrow)return Ze(r=>(r-1+Ut.length)%Ut.length);if(t.downArrow)return Ze(r=>(r+1)%Ut.length);if(t.pageUp)return Ze(r=>Math.max(0,r-4));if(t.pageDown)return Ze(r=>Math.min(Ut.length-1,r+4));if(t.tab||t.return){const e=r(),t=Te.text.length-(Pt?Pt.length:0)-1,n=Te.text.slice(0,t+1)+e+" ";return Me({text:n,cursor:n.length})}if(t.escape)return et(!0)}if(t.escape){if("busy"===pe.current&&oe.current)return pt(),void it("info","已请求中断(Esc)");const r=Date.now();return r-ut.current<600&&xe.current.length?(ut.current=0,ct(0),void ot(!0)):void(ut.current=r)}if(t.ctrl&&"c"===e)"busy"===pe.current&&oe.current?(pt(),it("info","已请求中断")):yt();else{if(t.return&&(t.shift||t.meta)||t.ctrl&&"j"===e)return Me(r=>({text:r.text.slice(0,r.cursor)+"\n"+r.text.slice(r.cursor),cursor:r.cursor+1}));if(t.return)It(Te.text);else{if(t.upArrow)return(()=>{const r=xe.current;if(!r.length)return;null==we.current?(ve.current=Te.text,we.current=r.length-1):we.current>0&&we.current--;const e=r[we.current]??"";Me({text:e,cursor:e.length})})();if(t.downArrow)return(()=>{if(null==we.current)return;const r=xe.current;if(we.current<r.length-1){we.current++;const e=r[we.current];Me({text:e,cursor:e.length})}else we.current=null,Me({text:ve.current,cursor:ve.current.length})})();if(t.leftArrow)return Me(r=>({...r,cursor:jr(r.text,r.cursor,-1)}));if(t.rightArrow)return Me(r=>({...r,cursor:jr(r.text,r.cursor,1)}));if(t.ctrl&&"a"===e)return Me(r=>({...r,cursor:0}));if(t.ctrl&&"e"===e)return Me(r=>({...r,cursor:r.text.length}));if(t.ctrl&&"u"===e)return Me(r=>({text:r.text.slice(r.cursor),cursor:0}));if(t.ctrl&&"k"===e)return Me(r=>({text:r.text.slice(0,r.cursor),cursor:r.cursor}));if(t.ctrl&&"w"===e)return Me(r=>{const e=r.text.slice(0,r.cursor).replace(/\S+\s*$/,"");return{text:e+r.text.slice(r.cursor),cursor:e.length}});if(t.backspace||t.delete)return we.current=null,Me(r=>{const e=t.backspace?jr(r.text,r.cursor,-1):r.cursor,n=t.backspace?r.cursor:jr(r.text,r.cursor,1);return{text:r.text.slice(0,e)+r.text.slice(n),cursor:e}});if(e&&!t.ctrl&&!t.meta){we.current=null;const r=e.replace(/\r\n?/g,"\n");Me(e=>({text:e.text.slice(0,e.cursor)+r+e.text.slice(e.cursor),cursor:e.cursor+r.length}))}}}}else he.current.list.length&&qe(r=>"expanded"===r?"busy"===pe.current?"collapsed":"hidden":"expanded")}});const Dt=s(()=>re.current.slice(),[ke]),qt=O(He),Lt=qt?"exhausted"===qt.level?`⛔ ${qt.label}用量已用尽${G(qt.resetAtEpochMs)?" · "+G(qt.resetAtEpochMs):""}`:`⚠ ${qt.label}用量 ${Math.round(100*qt.pct)}%(${Ar(qt.used)}/${Ar(qt.limit)})`:"";return $r(yr,{columns:qr.columns,rows:qr.rows,version:wr,cwd:Jr,model:r.model,theme:r.theme,accent:r.accentColor,mode:`${ir[Oe]} ${lr[Oe]}`,logItems:Dt,live:te.current,liveId:ee.current,thinking:ht.current,todos:Ee,taskView:De,busy:"busy"===je,activity:Ne||Vr,elapsed:Math.floor((Date.now()-Br.current)/1e3),pending:null,question:null,menu:Qr,input:Te,candidates:nt?Rt:At?$t.map(r=>"/"+r.name+(r.args?" "+r.args:"")+" ["+r.category+"] "+r.desc):Et?Ut.map(r=>"@"+r):[],selected:nt?st:At?Ke:Xe,candidateTitle:nt?"输入历史":At?"命令与技能":"引用文件",offset:Cr.offset,scrollRevision:Cr.revision,scrollRef:Wr,usage:{context:Ar(Pe),total:Ar(Ae.prompt+Ae.completion)},queueLen:Le,warning:Lt,toolCount:Zr.current.length})}export async function runInk({config:r,cwd:e,session:t,yolo:n,initialPrompt:o,version:s}){wr().catch(()=>{}),c(r).catch(()=>{}),r.sessionId=t.id;const u=[],m={signalRef:{current:null},sessionOf:()=>t},f={clients:[]},d={};let p,g=!1;const h=()=>{g=!0,p?.unmount()};try{p=i($r(l,{flexDirection:"column"},$r(Ir,{columns:process.stdout.columns,rows:process.stdout.rows,version:s}),$r(a,{dimColor:!0},"正在加载技能与工具…")),{alternateScreen:!0,interactive:!(!process.stdout.isTTY||!process.stdin.isTTY),exitOnCtrlC:!1,maxFps:20}),process.once("SIGTERM",h),process.once("SIGHUP",h),process.once("SIGINT",h);const c=await T(r,e,{queueInject:r=>u.push(r),sessionOf:()=>m.sessionOf(),signalRef:m.signalRef,ask:r=>m.ask?m.ask(r):Promise.resolve("终端尚未就绪"),askUi:r=>m.askUi?m.askUi(r):Promise.resolve({value:null}),runnerHint:"到点由调度器在该目录自动执行(终端里需有 `huicai daemon` 常驻;桌面端保持应用运行亦可)。"});return f.clients=c.clients,g?d:(process.removeListener("SIGINT",h),p.rerender($r(App,{config:r,cwd:e,session:t,extraTools:c.extraTools,mcp:f,yolo:n,initialPrompt:o,version:s,exitState:d,injectQueue:u,bridge:m})),await p.waitUntilExit(),d)}finally{process.removeListener("SIGINT",h),process.removeListener("SIGTERM",h),process.removeListener("SIGHUP",h),p?.unmount(),$(f.clients),gr(),xr(),dr()}}
|
|
1
|
+
import r,{useState as e,useRef as t,useReducer as n,useEffect as o,useMemo as s}from"react";import{syncPlanUser as c}from"../plan-limits.js";import{exec as u}from"node:child_process";import{render as i,Box as l,Text as a,useInput as m,usePaste as f,useApp as d,useStdout as p,useStdin as g}from"ink";import{runTurn as h}from"../agent.js";import{fetchUsage as x}from"../api.js";import{toolLabel as w}from"../render.js";import{renderMarkdown as v,looksLikeMarkdown as b}from"../markdown.js";import{createSession as k,saveSession as y}from"../session.js";import{ensureSessionTitle as j}from"../title.js";import{createCheckpoints as I}from"../checkpoints.js";import{buildExtraTools as T}from"../repl.js";import{buildSystemPrompt as M}from"../context.js";import{closeMcp as A}from"../mcp.js";import{runSlash as $,listCommands as S}from"../commands.js";import{matchCommands as P}from"../command-ui.js";import{menuOptions as U}from"./command-menu.js";import{questionSelection as E,approvalSelection as R,initialSelection as D,selectionKey as C,insertSelectionText as L}from"../selection.js";import{parseAskAnswer as q}from"../ask-ui.js";import{usageStatus as O,fmtReset as G}from"../usage.js";import{handleAccountError as W}from"../account.js";import{pricingUrl as N}from"../errkind.js";import{expandAtMentions as Y,buildUserContent as H,loadHistory as F,appendHistory as Q}from"../input.js";import{listWorkspaceFiles as z,runBash as K}from"../executors.js";import{loadAllowRules as V,loadDenyRules as _,matchAllowRules as B,suggestRule as J,addProjectAllowRule as X,riskWarning as Z}from"../settings.js";import{PLAN_DENY_REASON as rr,SANDBOX_DENY_REASON as er,sandboxBlocked as tr,isFileWriteTool as nr,AUTOMATION_TOOLS as or,automationActionLabel as sr,automationDetail as cr,PERMISSION_MODES as ur,MODE_ICON as ir,MODE_LABEL as lr}from"../permissions.js";import{runVerifyLoop as ar}from"../autofix.js";import{runAutoFlow as mr}from"../autorun.js";import{acquireWakeLock as fr,releaseWakeLock as dr}from"../presence.js";import{notifyTurnDone as pr}from"../notify.js";import{closeAllTerminals as gr,dropSessionTerminals as hr}from"../terminal.js";import{shutdownLsp as xr}from"../lsp.js";import{ensureVerified as wr}from"../sandbox.js";import{runGoalRounds as vr,getGoal as br,setRoundKind as kr}from"../goalround.js";import{Workbench as yr,moveCursor as jr}from"./workbench.js";import{WelcomeBanner as Ir}from"./welcome.js";import{enableMouseTracking as Tr,mouseReport as Mr}from"./scroll.js";const Ar=r.createElement;function $r(r){return(r=Number(r)||0)>=1e3?(r/1e3).toFixed(1)+"k":String(r)}export function App({config:r,cwd:c,session:i,extraTools:l,mcp:a,yolo:gr,initialPrompt:xr,version:wr,updater:Ir,onExit:Sr,exitState:Pr={},injectQueue:Ur=[],bridge:Er={}}){const Rr=d(),{stdout:Dr}=p(),{stdin:Cr}=g();o(()=>Cr.isTTY?Tr(Dr):void 0,[Cr,Dr]);const[Lr,qr]=e({columns:Dr.columns||80,rows:Dr.rows||24});o(()=>{const r=()=>qr({columns:Dr.columns||80,rows:Dr.rows||24});return Dr.on("resize",r),()=>Dr.off("resize",r)},[Dr]);const[Or,Gr]=e({offset:0,revision:0}),Wr=r=>{Nr.current.offset=r,Gr(e=>({offset:r,revision:e.revision+1}))},Nr=t({max:0,page:10,offset:0}),Yr=r=>Wr(Math.max(0,Math.min(Nr.current.max,Nr.current.offset+r))),Hr=t(null),Fr=t(!1),Qr=t(null),[zr,Kr]=e(null),Vr=t([]),[_r,Br]=e(""),Jr=t(0),[Xr,Zr]=e(c),re=t(l),ee=t(null);ee.current||(ee.current=i.messages.filter(r=>["user","assistant"].includes(r.role)&&r.content).map((r,e)=>({id:e,kind:r.role,text:"string"==typeof r.content?r.content:r.content.filter(r=>"text"===r.type).map(r=>r.text).join("\n")})));const te=t(ee.current.length),ne=t(""),oe=t(i),se=t(null),ce=t(new Set),ue=t(V(Xr)),ie=t(_(Xr)),le=t(!1),ae=t(!1),me=t(!1),fe=t(I()),de=t(!!gr),pe=t(!1),ge=t("idle"),he=t([]),xe=t({list:[]}),we=t(F(Xr)),ve=t(null),be=t(""),[,ke]=n(r=>r+1,0),[ye,je]=n(r=>r+1,0),[Ie,Te]=e("idle"),[Me,Ae]=e({text:"",cursor:0}),[$e,Se]=e(null),Pe=t(!1);o(()=>{let r=!0;return Ir?.check.then(e=>{r&&Se(e)}),()=>{r=!1,Ir?.dispose()}},[Ir]);const Ue=t({prompt:0,completion:0}),[Ee,Re]=e({prompt:0,completion:0}),[De,Ce]=e(0),[Le,qe]=e([]),[Oe,Ge]=e("hidden"),[We,Ne]=e(0),[Ye,He]=e(gr?"auto":"manual"),Fe=()=>le.current?"plan":ae.current?"sandbox":de.current?"auto":me.current?"acceptEdits":"manual",[Qe,ze]=e(""),Ke=t(null),[Ve,_e]=e(null),Be=t(null),[Je,Xe]=e(0),[Ze,rt]=e(!1),et=s(()=>[...S(Xr),...Ir&&$e?[{name:"update",description:"安装已检测到的 CLI 新版本",menu:!0}]:[]],[Xr,Ie,$e,Ir]),[tt,nt]=e(0),[ot,st]=e(!1),ct=s(()=>{try{return z(Xr)}catch{return[]}},[Xr,Ie]),[ut,it]=e(!1),[lt,at]=e(0),mt=t(0),ft=(r,e)=>{ee.current.push({id:te.current++,kind:r,text:e}),je()},dt=()=>{if(ne.current){const e=ne.current,t=!1!==r.markdown&&b(e);ft("assistant",t?v(e):e),ne.current=""}},pt=r=>{ge.current=r?"busy":"idle",Te(ge.current)};Er.signalRef||={current:null},Er.sessionOf=()=>oe.current;const gt=(r=null)=>{const e=Qr.current,t=Vr.current.shift()||null;Qr.current=t,Kr(t),e?.resolve(r)},ht=r=>new Promise(e=>{if(Hr.current?.signal.aborted||Fr.current)return e(null);const t={...D(r),resolve:e};Qr.current?Vr.current.push(t):(Qr.current=t,Kr(t),Wr(0))});Er.askUi=async r=>{const e=await ht(E(r));return"date_range"===r.kind&&e?.value?q(r,e.value):e||{value:null}},Er.ask=async r=>(await Er.askUi({kind:"text",question:r}))?.value||"已取消";const xt=()=>{const r=[Qr.current,...Vr.current.splice(0)];Qr.current=null,Kr(null);for(const e of r)e?.resolve(null)},wt=()=>{Ge("hidden"),Hr.current?.abort(),se.current?.abort(),xt()};o(()=>()=>{Hr.current?.abort(),Qr.current?.resolve(null),Qr.current=null;for(const r of Vr.current.splice(0))r.resolve(null);Er.askUi=null,Er.ask=null},[]),o(()=>{if("busy"!==Ie)return;const r=setInterval(ke,250);return()=>clearInterval(r)},[Ie]);const vt=async()=>{if(r.apiKey)try{const e=await x(r.baseUrl,r.apiKey);Be.current=e,_e(e)}catch{}};o(()=>{vt();const r=setInterval(()=>{vt()},18e4);return()=>clearInterval(r)},[]),o(()=>{if(!r.statusLine)return void ze("");let e=!1,t=null;const n=()=>{t||(t=u(r.statusLine,{cwd:Xr,timeout:3e3,maxBuffer:8192},(r,n)=>{t=null,e||ze(r?"":n.split("\n")[0].slice(0,80))}))};n();const o=setInterval(n,5e3);return()=>{e=!0,clearInterval(o),t?.kill()}},[r.statusLine,Xr]),o(()=>()=>{Ke.current&&clearInterval(Ke.current)},[]);const bt=t(""),kt={onDelta:r=>{bt.current&&(bt.current=""),ne.current+=r,ke()},onReasoning:r=>{bt.current=(bt.current+r).slice(-400),ke()},onToolStart:(r,e)=>{bt.current="",dt(),Br(w(r,e)),ft("tool",w(r,e))},onToolResult:(r,e)=>{Br("整理结果"),ft("toolresult",String(e))},onFileDiff:r=>ft("diff",r),onSkip:(r,e)=>ft("skip",w(r,e)+" — 已跳过"),onInfo:r=>{dt(),ft("info",r)},onTodo:r=>{qe(r),Hr.current?.signal.aborted||Ge(e=>r.length?"hidden"===e?"expanded":e:"hidden")},onDecision:r=>{dt();const e=(r.alternatives||[]).length?` · ${r.alternatives.length} 备选`:"",t="draft"===r.status?"(待确认草稿)":"";ft("info",`◆ 已记录决策:${r.title}${e}${t}`)},onTurnEnd:()=>{Ge("hidden"),bt.current="",dt()}},yt={request:(r,e,t,n)=>{if(Hr.current?.signal.aborted)return Promise.resolve({allow:!1,reason:"操作已中断。"});if(ie.current.length&&B(ie.current,r,e))return Promise.resolve({allow:!1,reason:"该操作被拒绝规则阻止(/permissions查看或移除)。"});if(le.current&&!n)return Promise.resolve({allow:!1,reason:rr});if(ae.current&&tr(r))return Promise.resolve({allow:!1,reason:er});if(n||de.current||ce.current.has(r)||B(ue.current,r,e))return Promise.resolve({allow:!0});if(me.current&&nr(r))return Promise.resolve({allow:!0});const o=or.has(r);return ht(R({name:r,args:e,consent:o,label:o?sr(r,e):w(r,e),detail:o?cr(r,e):"",risk:Z(r,e)})).then(t=>{if(!t||"deny"===t||Hr.current?.signal.aborted)return{allow:!1,reason:"用户未批准此操作。"};if("session"!==t&&"project"!==t||ce.current.add(r),"project"===t){const t=J(r,e);try{X(Xr,t),ue.current=V(Xr),ft("info",`已写入允许规则:${t} → .huicai/settings.json`)}catch(r){ft("info",`规则写入失败(本会话仍生效):${r.message}`)}}return{allow:!0}})},isYolo:()=>de.current,setYolo:r=>{de.current=!!r,He(Fe())},setPlanMode:r=>{le.current=!!r,He(Fe())},isPlanMode:()=>le.current,setSandbox:r=>{ae.current=!!r,He(Fe())},isSandbox:()=>ae.current,setAcceptEdits:r=>{me.current=!!r,He(Fe())},isAcceptEdits:()=>me.current,setMode:r=>{de.current="auto"===r,me.current="acceptEdits"===r,le.current="plan"===r,ae.current="sandbox"===r,He(r)},getMode:()=>Fe(),reloadRules:()=>{ue.current=V(Xr),ie.current=_(Xr)}},jt=()=>{if(Ur.length)return Ur.shift();const r=he.current.shift();return Ne(he.current.length),null==r?null:r},It=()=>{j(oe.current,r).then(r=>{r&&y(oe.current)}).catch(()=>{})},Tt=async e=>{Ge("hidden");const t=O(Be.current);if(t&&"exhausted"===t.level){const e=N(r.baseUrl);return ft("info",`✗ Token用量窗口(${t.label})已用尽,暂无法继续。${G(t.resetAtEpochMs)}`+(e?`\n 升级会员提高上限 → ${e}`:"")),{error:"Token用量已用尽"}}if(Hr.current?.signal.aborted)return{aborted:!0};Br("准备上下文");const{text:n,images:o}=await Y(e,Xr,r);if(Hr.current?.signal.aborted)return{aborted:!0};const s=fe.current.snapshot({session:oe.current,label:e});oe.current.messages.push({role:"user",content:H(n,o)}),It(),pt(!0),se.current=Hr.current||new AbortController,Er.signalRef.current=se.current.signal,oe.current.compaction||(oe.current.compaction={});const c="auto"===Fe()&&!1!==r.wakeLockInAuto?fr({reason:"自动模式任务执行中"}):null,u=Date.now();let i;try{i=await h({config:r,messages:oe.current.messages,permissions:yt,cwd:Xr,extraTools:re.current,events:kt,signal:se.current.signal,todoState:xe.current,compaction:oe.current.compaction,pullQueued:jt,fileJournal:s})}finally{Ge("hidden"),c?.ok&&dr()}if(i?.aborted||i?.error||pr({durationMs:Date.now()-u,cwd:Xr,summary:String(e).replace(/\s+/g," ").slice(0,80),enabled:!1!==r.notifications}).catch(()=>{}),dt(),i?.usage&&(Ue.current={prompt:Ue.current.prompt+(i.usage.prompt||0),completion:Ue.current.completion+(i.usage.completion||0)},Re({...Ue.current})),i?.lastPromptTokens&&(Ce(i.lastPromptTokens),oe.current.lastPromptTokens=i.lastPromptTokens),y(oe.current),It(),(i?.aborted||i?.error)&&he.current.length&&(he.current=[],Ne(0),ft("info","已清空排队消息")),i?.error){let e="";const t=W(i,r,r=>{e+=r});if(e.trim()&&ft("info",e.trim()),"auth"===t)return Pr.relogin=!0,Mt(),i}return vt(),i},Mt=()=>{Fr.current=!0,wt(),Hr.current||(Sr?.(),Rr.exit())},At=async e=>{kr(oe.current.id,"human");const t=await Tt(e);return t?.aborted||t?.error||Fr.current||Hr.current?.signal.aborted||br(oe.current)&&(await vr({session:oe.current,doTurn:Tt,stats:()=>({usedTokens:Ue.current.prompt+Ue.current.completion,budgetTokens:Number(r.budgetTokens)||0}),onInfo:r=>ft("info",r),aborted:()=>Fr.current||!!Hr.current?.signal.aborted}),y(oe.current)),t},$t=async e=>{const t=e.trim();if(t){if(Hr.current)return/^\/(exit|quit)$/.test(t)?Mt():t.startsWith("/")||t.startsWith("!")?void ft("info","任务执行中,请先按 Esc 中断,再执行此命令。"):(Ae({text:"",cursor:0}),ft("user",t),he.current.push(t),void Ne(he.current.length));Ae({text:"",cursor:0}),Wr(0),ve.current=null,Q(Xr,t),we.current.at(-1)!==t&&we.current.push(t),Hr.current=new AbortController,se.current=Hr.current,Er.signalRef.current=Hr.current.signal,Jr.current=Date.now(),Ge("hidden"),pt(!0);try{for("/update"===t&&Ir&&$e?(Br("正在更新 HuiCai AI,请稍候"),Pr.updated=await Ir.install($e,{signal:Hr.current.signal}),y(oe.current),Fr.current=!0):t.startsWith("/")?await(async e=>{const t=br(oe.current)?.rev,n={version:wr,config:r,cwd:Xr,select:r=>ht({...r,command:e.trim()}),signal:Hr.current?.signal,resumeSession:e=>{Ke.current&&(clearInterval(Ke.current),Ke.current=null),hr(oe.current.id),oe.current=e,r.sessionId=e.id,e.model&&(r.model=e.model),Ur.length=0,ce.current.clear(),xe.current.list=[],fe.current.list.length=0,qe([]),Ce(e.lastPromptTokens||0),ee.current=e.messages.filter(r=>["user","assistant"].includes(r.role)&&r.content).map(r=>({id:te.current++,kind:r.role,text:"string"==typeof r.content?r.content:r.content.filter(r=>"text"===r.type).map(r=>r.text).join("\n")})),ne.current="",Wr(0),je()},usage:Ue.current,permissions:yt,extraTools:re.current,session:oe.current,checkpoints:fe.current,print:r=>ft("info",String(r))},o=await $(e,n);if("exit"===o.action)return Mt();if("clear"===o.action&&(y(oe.current),Ke.current&&(clearInterval(Ke.current),Ke.current=null),hr(oe.current.id),oe.current=k({cwd:Xr,model:r.model,messages:[oe.current.messages[0]]}),r.sessionId=oe.current.id,Ur.length=0,ce.current.clear(),xe.current.list=[],fe.current.list.length=0,qe([]),Ce(0),ee.current=[],ne.current="",ft("info","已开启新会话(旧会话仍可 --resume恢复)")),"refresh"===o.action&&(xe.current.list=[],qe([]),Ce(0)),"autofix"===o.action&&await ar({command:o.command,maxIters:o.maxIters,signal:Hr.current?.signal,cwd:Xr,runTurnOnce:r=>Tt(r),emit:r=>ft("info",r)}),"autorun"===o.action&&await mr({goal:o.goal,config:r,permissions:yt,cwd:Xr,runTurnOnce:async r=>{const e=await Tt(r);if(e?.aborted||e?.error||Hr.current?.signal.aborted)throw new Error("自动执行已停止");return e},emit:r=>ft("info",r)}),"cd"===o.action){Ke.current&&(clearInterval(Ke.current),Ke.current=null);const e=await T(r,o.cwd,{queueInject:r=>Ur.push(r),ask:r=>Er.ask(r),askUi:r=>Er.askUi(r),sessionOf:()=>oe.current,signalRef:Er.signalRef});ue.current=V(o.cwd),ie.current=_(o.cwd),hr(oe.current.id),A(a?.clients),re.current=e.extraTools,a&&(a.clients=e.clients),oe.current=k({cwd:o.cwd,model:r.model,messages:[{role:"system",content:M(o.cwd,{memory:!1!==r.memoryEnabled,config:r})}]}),r.sessionId=oe.current.id,Ur.length=0,ce.current.clear(),we.current=F(o.cwd),ve.current=null,xe.current.list=[],fe.current.list.length=0,qe([]),Ce(0),ee.current=[],ne.current="",Zr(o.cwd),ft("info","已切换到 "+o.cwd+"(新会话,旧会话已保存)")}if("loop"===o.action&&(Ke.current&&(clearInterval(Ke.current),Ke.current=null),o.stop?ft("info","已停止循环。"):(ft("info",`已开始循环:每 ${o.seconds}s运行「${o.prompt}」(/loop off停止)`),Ke.current=setInterval(()=>{"busy"!==ge.current&&$t(o.prompt)},1e3*o.seconds))),o.prompt)ft("user",o.prompt),await At(o.prompt);else if(/^\/goal\s+/.test(e)&&!/^\/goal\s+(?:status|pause|clear|limit)(?:\s|$)/.test(e)&&"active"===br(oe.current)?.phase&&br(oe.current)?.rev!==t){const r="继续完成用户明确设定的目标:"+br(oe.current).objective;ft("user",e),await At(r)}})(t):t.startsWith("!")&&t.length>1?await(async e=>{ft("user","! "+e),Br("$ "+e);const t=await K(Xr,{command:e},r,Hr.current?.signal);ft("toolresult",t),oe.current.messages.push({role:"user",content:`我在终端直接执行了命令:$ ${e}\n${t.slice(0,8e3)}`}),y(oe.current)})(t.slice(1).trim()):(ft("user",t),await At(t));he.current.length&&!Hr.current.signal.aborted&&!Fr.current;){const r=he.current.shift();Ne(he.current.length),await At(r)}}catch(r){dt(),ft("info",Hr.current?.signal.aborted?"操作已中断":"执行失败:"+r.message+("/update"===t?" 输入 /update 可重试。":""))}finally{xt(),he.current=[],Ne(0),Hr.current=null,se.current=null,Er.signalRef.current=null,pt(!1),Ge("hidden"),Br(""),Fr.current&&(Sr?.(),Rr.exit())}}};o(()=>{!$e||Pe.current||"idle"!==Ie||Me.text||zr||Fr.current||(Pe.current=!0,ht(Ir.menu($e)).then(r=>{"update"===r&&$t("/update")}))},[$e,Ie,Me.text,zr]),o(()=>{!pe.current&&xr&&(pe.current=!0,$t(xr))},[]);const St=zr?null:/^\/(\S*)$/.exec(Me.text),Pt=St?St[1]:null,Ut=null==Pt?[]:P(et,Pt),Et=Ut.length>0&&!Ze;o(()=>{Xe(0),rt(!1)},[Pt]);const Rt=zr||null!=Pt?null:/(?:^|\s)@([^\s@]*)$/.exec(Me.text),Dt=Rt?Rt[1]:null,Ct=null==Dt?[]:(()=>{const r=Dt.toLowerCase(),e=[];for(const t of ct){const n=t.toLowerCase(),o=n.split("/").pop();let s=-1;if(r?o.startsWith(r)?s=3:o.includes(r)?s=2:n.includes(r)&&(s=1):s=0,s>=0&&e.push({f:t,rank:s}),!r&&e.length>=50)break}return e.sort((r,e)=>e.rank-r.rank||r.f.length-e.f.length||r.f.localeCompare(e.f)),e.slice(0,50).map(r=>r.f)})(),Lt=Ct.length>0&&!ot;o(()=>{nt(0),st(!1)},[Dt]);const qt=s(()=>{const r=new Set,e=[];for(let t=we.current.length-1;t>=0&&e.length<50;t--){const n=we.current[t];n&&!r.has(n)&&(r.add(n),e.push(n))}return e},[ut]);f(r=>{if(Qr.current){if(Qr.current.editing){const e=L(Qr.current,r);Qr.current=e,Kr(e)}return}const e=r.replace(/\r\n?/g,"\n");ve.current=null,Ae(r=>({text:r.text.slice(0,r.cursor)+e+r.text.slice(r.cursor),cursor:r.cursor+e.length}))}),m((e,t)=>{const n=Mr(e);if(n)if(n.delta&&Qr.current){const r=Qr.current,e={...r,index:Math.max(0,Math.min(U(r).length-1,r.index-Math.sign(n.delta)))};Qr.current=e,Kr(e)}else n.delta&&Yr(n.delta);else if(t.ctrl&&"c"===e)Hr.current?(wt(),ft("info","已请求中断")):Mt();else{if(t.ctrl&&t.end)return Wr(0);if(t.ctrl&&t.home)return Wr(Nr.current.max);if(t.ctrl&&(t.upArrow||t.downArrow))return Yr(t.upArrow?1:-1);if(!t.ctrl||"t"!==e||Qr.current){if(Qr.current){const r=Qr.current;if(r.kind){const n=C(r,e,t);return"result"in n?gt(n.result):(Qr.current=n.menu,void Kr(n.menu))}const n=U(r),o=e=>{const t={...r,...e};Qr.current=t,Kr(t)};if(t.escape)return gt();if(t.return)return n[r.index]?gt(n[r.index].value):void 0;if(t.upArrow||t.downArrow||t.pageUp||t.pageDown||"segments"===r.layout&&(t.leftArrow||t.rightArrow||t.tab)){const e=t.pageUp||t.pageDown?Math.max(1,Nr.current.menuPage||4):1,s=t.upArrow||t.pageUp||t.leftArrow||t.tab&&t.shift?-e:e;return o({index:Math.max(0,Math.min(n.length-1,r.index+s)),..."segments"===r.layout?{query:""}:{}})}const s=e=>{const t="segments"===r.layout?n.findIndex(r=>[r.label,r.value].join(" ").toLowerCase().includes(e.toLowerCase())):0;o({query:e,index:Math.max(0,t)})};return t.ctrl&&"u"===e?s(""):t.backspace||t.delete?s(r.query.slice(0,jr(r.query,r.query.length,-1))):!e||t.ctrl||t.meta||t.tab?void 0:s(r.query+e.replace(/[\x00-\x1f\x7f]/g,""))}if((t.pageUp||t.pageDown)&&!Et&&!Lt&&!ut)return Yr((t.pageUp?1:-1)*Math.max(1,Nr.current.page-1));if(t.ctrl&&"p"===e)return Ae({text:"/",cursor:1}),void rt(!1);if(ut){if(!qt.length)return void it(!1);if(t.upArrow)return at(r=>(r-1+qt.length)%qt.length);if(t.downArrow)return at(r=>(r+1)%qt.length);if(t.pageUp)return at(r=>Math.max(0,r-4));if(t.pageDown)return at(r=>Math.min(qt.length-1,r+4));if(t.return){const r=qt[Math.min(lt,qt.length-1)]||"";return it(!1),ve.current=null,Ae({text:r,cursor:r.length})}return t.escape?it(!1):void 0}if(t.tab&&t.shift&&!Hr.current){const e=ur[(ur.indexOf(yt.getMode())+1)%ur.length];return yt.setMode(o=e),r.planMode="plan"===o,r.sandbox="sandbox"===o,void ft("info",`对话模式 → ${ir[o]} ${lr[o]}`+("auto"===o?"(已开放所有权限,任何命令免审批)":""))}var o;if(Et){const r=()=>Ut[Math.min(Je,Ut.length-1)];if(t.upArrow)return Xe(r=>(r-1+Ut.length)%Ut.length);if(t.downArrow)return Xe(r=>(r+1)%Ut.length);if(t.pageUp)return Xe(r=>Math.max(0,r-4));if(t.pageDown)return Xe(r=>Math.min(Ut.length-1,r+4));if(t.tab){const e=r().name;return Ae({text:"/"+e+" ",cursor:e.length+2})}if(t.return){const e=r();return!e.menu&&e.args?.startsWith("<")?Ae({text:"/"+e.name+" ",cursor:e.name.length+2}):void $t("/"+e.name)}if(t.escape)return rt(!0)}if(Lt){const r=()=>Ct[Math.min(tt,Ct.length-1)];if(t.upArrow)return nt(r=>(r-1+Ct.length)%Ct.length);if(t.downArrow)return nt(r=>(r+1)%Ct.length);if(t.pageUp)return nt(r=>Math.max(0,r-4));if(t.pageDown)return nt(r=>Math.min(Ct.length-1,r+4));if(t.tab||t.return){const e=r(),t=Me.text.length-(Dt?Dt.length:0)-1,n=Me.text.slice(0,t+1)+e+" ";return Ae({text:n,cursor:n.length})}if(t.escape)return st(!0)}if(t.escape){if("busy"===ge.current&&se.current)return wt(),void ft("info","已请求中断(Esc)");const r=Date.now();return r-mt.current<600&&we.current.length?(mt.current=0,at(0),void it(!0)):void(mt.current=r)}if(t.ctrl&&"c"===e)"busy"===ge.current&&se.current?(wt(),ft("info","已请求中断")):Mt();else{if(t.return&&(t.shift||t.meta)||t.ctrl&&"j"===e)return Ae(r=>({text:r.text.slice(0,r.cursor)+"\n"+r.text.slice(r.cursor),cursor:r.cursor+1}));if(t.return)$t(Me.text);else{if(t.upArrow)return(()=>{const r=we.current;if(!r.length)return;null==ve.current?(be.current=Me.text,ve.current=r.length-1):ve.current>0&&ve.current--;const e=r[ve.current]??"";Ae({text:e,cursor:e.length})})();if(t.downArrow)return(()=>{if(null==ve.current)return;const r=we.current;if(ve.current<r.length-1){ve.current++;const e=r[ve.current];Ae({text:e,cursor:e.length})}else ve.current=null,Ae({text:be.current,cursor:be.current.length})})();if(t.leftArrow)return Ae(r=>({...r,cursor:jr(r.text,r.cursor,-1)}));if(t.rightArrow)return Ae(r=>({...r,cursor:jr(r.text,r.cursor,1)}));if(t.ctrl&&"a"===e)return Ae(r=>({...r,cursor:0}));if(t.ctrl&&"e"===e)return Ae(r=>({...r,cursor:r.text.length}));if(t.ctrl&&"u"===e)return Ae(r=>({text:r.text.slice(r.cursor),cursor:0}));if(t.ctrl&&"k"===e)return Ae(r=>({text:r.text.slice(0,r.cursor),cursor:r.cursor}));if(t.ctrl&&"w"===e)return Ae(r=>{const e=r.text.slice(0,r.cursor).replace(/\S+\s*$/,"");return{text:e+r.text.slice(r.cursor),cursor:e.length}});if(t.backspace||t.delete)return ve.current=null,Ae(r=>{const e=t.backspace?jr(r.text,r.cursor,-1):r.cursor,n=t.backspace?r.cursor:jr(r.text,r.cursor,1);return{text:r.text.slice(0,e)+r.text.slice(n),cursor:e}});if(e&&!t.ctrl&&!t.meta){ve.current=null;const r=e.replace(/\r\n?/g,"\n");Ae(e=>({text:e.text.slice(0,e.cursor)+r+e.text.slice(e.cursor),cursor:e.cursor+r.length}))}}}}else xe.current.list.length&&Ge(r=>"expanded"===r?"busy"===ge.current?"collapsed":"hidden":"expanded")}});const Ot=s(()=>ee.current.slice(),[ye]),Gt=O(Ve),Wt=Gt?"exhausted"===Gt.level?`⛔ ${Gt.label}用量已用尽${G(Gt.resetAtEpochMs)?" · "+G(Gt.resetAtEpochMs):""}`:`⚠ ${Gt.label}用量 ${Math.round(100*Gt.pct)}%(${$r(Gt.used)}/${$r(Gt.limit)})`:"";return Ar(yr,{columns:Lr.columns,rows:Lr.rows,version:wr,cwd:Xr,model:r.model,theme:r.theme,accent:r.accentColor,mode:`${ir[Ye]} ${lr[Ye]}`,logItems:Ot,live:ne.current,liveId:te.current,thinking:bt.current,todos:Le,taskView:Oe,busy:"busy"===Ie,activity:Qe||_r,elapsed:Math.floor((Date.now()-Jr.current)/1e3),pending:null,question:null,menu:zr,input:Me,candidates:ut?qt:Et?Ut.map(r=>"/"+r.name+(r.args?" "+r.args:"")+" ["+r.category+"] "+r.desc):Lt?Ct.map(r=>"@"+r):[],selected:ut?lt:Et?Je:tt,candidateTitle:ut?"输入历史":Et?"命令与技能":"引用文件",offset:Or.offset,scrollRevision:Or.revision,scrollRef:Nr,usage:{context:$r(De),total:$r(Ee.prompt+Ee.completion)},queueLen:We,warning:Wt,toolCount:re.current.length})}export async function runInk({config:r,cwd:e,session:t,yolo:n,initialPrompt:o,version:s,updater:u}){wr().catch(()=>{}),c(r).catch(()=>{}),r.sessionId=t.id;const m=[],f={signalRef:{current:null},sessionOf:()=>t},d={clients:[]},p={};let g,h=!1;const x=()=>{h=!0,g?.unmount()};try{g=i(Ar(l,{flexDirection:"column"},Ar(Ir,{columns:process.stdout.columns,rows:process.stdout.rows,version:s}),Ar(a,{dimColor:!0},"正在加载技能与工具…")),{alternateScreen:!0,interactive:!(!process.stdout.isTTY||!process.stdin.isTTY),exitOnCtrlC:!1,maxFps:20}),process.once("SIGTERM",x),process.once("SIGHUP",x),process.once("SIGINT",x);const c=await T(r,e,{queueInject:r=>m.push(r),sessionOf:()=>f.sessionOf(),signalRef:f.signalRef,ask:r=>f.ask?f.ask(r):Promise.resolve("终端尚未就绪"),askUi:r=>f.askUi?f.askUi(r):Promise.resolve({value:null}),runnerHint:"到点由调度器在该目录自动执行(终端里需有 `huicai daemon` 常驻;桌面端保持应用运行亦可)。"});return d.clients=c.clients,h?p:(process.removeListener("SIGINT",x),g.rerender(Ar(App,{config:r,cwd:e,session:t,extraTools:c.extraTools,mcp:d,yolo:n,initialPrompt:o,version:s,updater:u,exitState:p,injectQueue:m,bridge:f})),await g.waitUntilExit(),p)}finally{process.removeListener("SIGINT",x),process.removeListener("SIGTERM",x),process.removeListener("SIGHUP",x),g?.unmount(),A(d.clients),gr(),xr(),dr()}}
|
package/src/vision.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const PSEUDO_MODEL_RE=/^(agent|workflow|datahuman|squad)-/;export const VISION_MODEL_RE=/(vl|vision|gpt-4o|omni|multimodal|\dv\b)/i;export function modelSeesImages(t){const e=String(t||"").trim();return!(!e||"auto"===e)&&(!!PSEUDO_MODEL_RE.test(e)||VISION_MODEL_RE.test(e))}export function hasImageParts(t){return Array.isArray(t)&&t.some(t=>t&&"image_url"===t.type)}export function messagesHaveImages(t){return Array.isArray(t)&&t.some(t=>hasImageParts(t&&t.content))}export function stripImageParts(t,e="(图片已省略:当前模型不支持读取图片)"){if(!hasImageParts(t))return t;const s=[];let r=0;for(const e of t)e&&"image_url"===e.type?r++:s.push(e);r&&s.push({type:"text",text:e});const a=s.filter(t=>t&&"text"===t.type);return a.length===s.length?a.map(t=>t.text||"").join("\n\n"):s}export function sanitizeMessagesForModel(t,e){if(!Array.isArray(t)||
|
|
1
|
+
export const PSEUDO_MODEL_RE=/^(agent|workflow|datahuman|squad)-/;export const VISION_MODEL_RE=/(vl|vision|gpt-4o|omni|multimodal|\dv\b)/i;export function modelSeesImages(t){const e=String(t||"").trim();return!(!e||"auto"===e)&&(!!PSEUDO_MODEL_RE.test(e)||VISION_MODEL_RE.test(e))}export function hasImageParts(t){return Array.isArray(t)&&t.some(t=>t&&"image_url"===t.type)}export function messagesHaveImages(t){return Array.isArray(t)&&t.some(t=>hasImageParts(t&&t.content))}export function stripImageParts(t,e="(图片已省略:当前模型不支持读取图片)"){if(!hasImageParts(t))return t;const s=[];let r=0;for(const e of t)e&&"image_url"===e.type?r++:s.push(e);r&&s.push({type:"text",text:e});const a=s.filter(t=>t&&"text"===t.type);return a.length===s.length?a.map(t=>t.text||"").join("\n\n"):s}export function sanitizeMessagesForModel(t,e,s){const r="boolean"==typeof s?s:modelSeesImages(e);if(!Array.isArray(t)||r||!messagesHaveImages(t))return{messages:t,stripped:0};let a=0;return{messages:t.map(t=>hasImageParts(t&&t.content)?(a++,{...t,content:stripImageParts(t.content)}):t),stripped:a}}
|