@huicai/cli 2026.1019.0 → 2026.1022.59980

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 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
@@ -2,11 +2,12 @@
2
2
  // huicai —— 终端智能体入口:解析参数并分发(交互 / 一次性 / login / models / help)。
3
3
 
4
4
  import readline from 'node:readline';
5
+ import { accountSnapshot, assertAccountPath } from '../src/account-scope.js';
5
6
  import path from 'node:path';
6
7
  import { createRequire } from 'node:module';
7
8
  import { c, setColorMode, setAccent } from '../src/colors.js';
8
- import { loadConfig, saveConfig, CONFIG_PATH, DEFAULT_MODEL } from '../src/config.js';
9
- import { listModels, getAgentPersona, fetchUsage, listPlatformSkills, getPlatformSkill, pushPlatformSkill } from '../src/api.js';
9
+ import { loadConfig, saveConfig, setRuntimeCredentials, CONFIG_PATH, DEFAULT_MODEL } from '../src/config.js';
10
+ import { getPlatformConfig, listModels, getAgentPersona, fetchUsage, listPlatformSkills, getPlatformSkill, pushPlatformSkill } from '../src/api.js';
10
11
  import { deviceLogin } from '../src/device.js';
11
12
  import { startRepl, runOnce } from '../src/repl.js';
12
13
  import { buildSystemPrompt } from '../src/context.js';
@@ -17,6 +18,7 @@ import { refreshPlan, isPlanLimitError } from '../src/plan-limits.js';
17
18
  import fs from 'node:fs';
18
19
  import { lintSkillLibrary } from '../src/skill-lint.js';
19
20
  import { tokenize } from '../src/codeindex.js';
21
+ import { createUpdater, displayVersion } from './update.js';
20
22
 
21
23
  // 版本从 package.json 读取(versionDisplay 年月日展示式,与 VSCode 扩展、IDEA 插件一致);勿手改,用 scripts/next-version.cjs
22
24
  const _pkg = createRequire(import.meta.url)('../package.json');
@@ -210,7 +212,10 @@ async function login(flags = {}) {
210
212
  // 不适合 CLI 的本机工具循环——用 `huicai --agent <ref>` 才是采用智能体的正确方式)。
211
213
  let model = cur.model;
212
214
  let tokenOk = true;
215
+ let user = null;
213
216
  try {
217
+ const platform = await getPlatformConfig(normBase, key);
218
+ user = platform?.user || null;
214
219
  const models = await listModels(normBase, key);
215
220
  const raw = models.map((m) => m.id).filter((id) => !/^(agent|workflow|datahuman|squad)-/.test(id));
216
221
  if (models.length === 0) {
@@ -226,7 +231,7 @@ async function login(flags = {}) {
226
231
  process.stdout.write(c.red('⚠ 令牌校验失败:' + e.message) + '\n');
227
232
  }
228
233
 
229
- saveConfig({ baseUrl: normBase, apiKey: key, model });
234
+ saveConfig({ baseUrl: normBase, apiKey: key, model, user });
230
235
  if (tokenOk) {
231
236
  process.stdout.write(c.green('登录成功') + (model ? c.gray(` · 默认模型 ${model}`) : '') + '\n');
232
237
  process.stdout.write(c.gray(`已保存到 ${CONFIG_PATH},直接运行huicai开始。`) + '\n');
@@ -616,17 +621,40 @@ async function main() {
616
621
  return;
617
622
  }
618
623
 
624
+ setRuntimeCredentials({ ...(flags.base ? { baseUrl: flags.base } : {}), ...(flags.key ? { apiKey: flags.key } : {}) });
619
625
  const config = loadConfig();
626
+ if (config.apiKey && !config.user && !['help', 'version'].includes(sub)) {
627
+ const { syncPlanUser } = await import('../src/plan-limits.js');
628
+ await syncPlanUser(config);
629
+ Object.assign(config, loadConfig());
630
+ }
620
631
  if (flags.base) config.baseUrl = flags.base.replace(/\/+$/, '');
621
632
  if (flags.key) config.apiKey = flags.key;
622
633
  if (flags.model) config.model = flags.model;
623
634
  if (Number.isFinite(flags.maxTurns) && flags.maxTurns > 0) config.maxTurns = flags.maxTurns;
624
635
  setColorMode(config.theme); // /theme:无色/彩色
625
636
  setAccent(config.accentColor); // /color:强调色
626
- const cwd = flags.cwd ? path.resolve(flags.cwd) : process.cwd();
637
+ let cwd = flags.cwd ? path.resolve(flags.cwd) : process.cwd();
638
+ let scheduledTask = null;
639
+ if (sub === 'run-scheduled') {
640
+ const identity = accountSnapshot();
641
+ if (!identity.current() || identity.scope !== positional[2]) throw new Error('任务不属于当前登录用户');
642
+ const { syncAutomations } = await import('../src/automation-sync.js');
643
+ const schedule = await import('../src/schedule.js');
644
+ await syncAutomations();
645
+ const task = schedule.getTask(positional[1]);
646
+ if (!task || task.enabled === false || !identity.current()) return;
647
+ const { isDue } = await import('../src/schedule-freq.js');
648
+ if (!isDue(task, new Date())) return;
649
+ scheduledTask = task;
650
+ cwd = assertAccountPath(task.cwd || process.cwd());
651
+ flags.prompt = task.prompt; flags.print = true; flags.yolo = task.permission === 'full';
652
+ if (task.model && task.model !== 'auto') config.model = task.model;
653
+ }
627
654
 
628
655
  // 会话列表:纯本地,无需鉴权
629
656
  if (sub === 'sessions') {
657
+ if (!config.apiKey || !config.user) { process.stdout.write('请先登录并完成用户身份校验。\n'); return; }
630
658
  cmdSessions(cwd, flags.all || positional.includes('--all') || positional.includes('-a'));
631
659
  return;
632
660
  }
@@ -836,16 +864,29 @@ async function main() {
836
864
  if (piped) {
837
865
  prompt = prompt ? `${prompt}\n\n【以下为管道输入(stdin)】\n${piped}` : piped;
838
866
  }
839
- await runOnce({
840
- config, cwd, yolo: flags.yolo, prompt, session,
841
- persist: resuming,
842
- outputFormat: flags.outputFormat || 'text',
843
- maxTurns: config.maxTurns || undefined,
844
- });
867
+ const onceOptions = { config, cwd, yolo: flags.yolo, prompt, session, persist: resuming,
868
+ outputFormat: flags.outputFormat || 'text', maxTurns: config.maxTurns || undefined };
869
+ if (scheduledTask) {
870
+ const { runClaimedAutomation } = await import('../src/automation-sync.js');
871
+ await runClaimedAutomation(scheduledTask, async (task, signal) => {
872
+ // 以领取时的权威定义运行,不能使用同步到领取之间已被控制台修改的旧指令。
873
+ const runCwd = assertAccountPath(task.cwd || process.cwd());
874
+ const runConfig = { ...config, model: task.model && task.model !== 'auto' ? task.model : config.model };
875
+ const runSession = sessions.createSession({ cwd: runCwd, model: runConfig.model,
876
+ messages: [{ role: 'system', content: buildSystemPrompt(runCwd, { config: runConfig }) }] });
877
+ return runOnce({ ...onceOptions, config: runConfig, cwd: runCwd, session: runSession,
878
+ prompt: task.prompt, yolo: task.permission === 'full', signal, throwOnError: true });
879
+ });
880
+ } else await runOnce(onceOptions);
845
881
  return;
846
882
  }
847
883
 
848
884
  // 交互式:默认富 TUI(ink,需已安装依赖 + TTY);--plain 强制普通 REPL;TUI 不可用则自动回退
885
+ const updater = process.stdin.isTTY && process.stdout.isTTY ? createUpdater({ current: _pkg.version }) : null;
886
+ const finishUpdate = (result) => {
887
+ updater?.dispose();
888
+ if (result?.updated) process.stdout.write(c.green(`已更新到 v${displayVersion(result.updated)},请重新运行 huicai。登录、历史与配置已保留。\n`));
889
+ };
849
890
  // 偏好顺序:命令行 --tui/--plain > 配置 tui(rich/plain) > auto(TTY 下用 TUI)
850
891
  const wantTui = !flags.plain && process.stdout.isTTY && process.stdin.isTTY && process.env.TERM !== 'dumb'
851
892
  && (flags.tui || config.tui !== 'plain');
@@ -859,12 +900,14 @@ async function main() {
859
900
  // Only dependency loading may fall back. A runtime failure must never replay
860
901
  // the initial prompt after it may already have edited files or run commands.
861
902
  if (runInk) {
862
- const ran = await runInk({ config, cwd, session, yolo: flags.yolo, initialPrompt: prompt || null, version: VERSION });
903
+ const ran = await runInk({ config, cwd, session, yolo: flags.yolo, initialPrompt: prompt || null, version: VERSION, updater });
904
+ finishUpdate(ran);
863
905
  await reloginIfNeeded(ran, flags);
864
906
  return;
865
907
  }
866
908
  }
867
- const r = await startRepl({ config, cwd, yolo: flags.yolo, initialPrompt: prompt || null, session, version: VERSION });
909
+ const r = await startRepl({ config, cwd, yolo: flags.yolo, initialPrompt: prompt || null, session, version: VERSION, updater });
910
+ finishUpdate(r);
868
911
  await reloginIfNeeded(r, flags);
869
912
  }
870
913
 
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@huicai/cli",
3
- "version": "2026.1019.0",
4
- "versionDisplay": "2026.10.19",
3
+ "version": "2026.1022.59980",
4
+ "versionDisplay": "2026.10.22.59980",
5
5
  "description": "在终端运行的智能体CLI:以HuiCai_AI平台的OpenAI兼容 /v1 为模型后端,在用户本机执行文件/命令工具。",
6
6
  "type": "module",
7
7
  "bin": {
@@ -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.0.0
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
+ 交付附上改动与调用链影响、实际通过项、失败/跳过/阻塞项、证据位置和剩余风险。缺凭据、设备或依赖属于“未验证”;不得用静态契约或替身截图写成“模拟真机通过”。
@@ -0,0 +1 @@
1
+ import t from"node:crypto";import r from"node:fs";import o from"node:path";import{AsyncLocalStorage as n}from"node:async_hooks";import{CONFIG_DIR as e,loadConfig as c}from"./config.js";const a=new n;export function accountScope(r=c()){const o=r.user?.id??r.user?.userId??r.user?.username;return r.apiKey&&r.baseUrl&&null!=o&&""!==String(o)?t.createHash("sha256").update(`${r.baseUrl.replace(/\/+$/,"")}\n${o}`).digest("hex"):""}export function accountDataDir(){return assertAccountOperation(),o.join(e,"accounts",accountScope()||"_signed-out")}export function assertAccountOperation(){const t=a.getStore();if(t&&!t.current())throw new Error("登录状态已改变,请重试")}export function withAccountOperation(t,r){return a.run(t,r)}export function requireAccount(){assertAccountOperation();const t=accountScope();if(!t)throw new Error("请先登录并完成用户身份校验");return t}export function accountSnapshot(){const t=c(),r=accountScope(t);return{config:t,scope:r,current:()=>{const o=c();return!!r&&accountScope(o)===r&&o.apiKey===t.apiKey}}}export function assertAccountPath(t){if(assertAccountOperation(),!t)return t;const n=t=>{const e=o.resolve(t);try{return r.realpathSync(e)}catch{const t=o.dirname(e);return t===e?e:o.join(n(t),o.basename(e))}},c=n(t),a=n(e),s=n(accountDataDir());if((c===a||c.startsWith(a+o.sep))&&c!==s&&!c.startsWith(s+o.sep))throw new Error("无权访问其它账号或共享凭据目录");return t}
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{accountSnapshot as e,withAccountOperation as t,assertAccountOperation as n}from"./account-scope.js";import o from"node:fs";import s from"node:os";import r from"node:path";import{streamChat as i,getPlatformConfig as a}from"./api.js";import{classifyError as l}from"./errkind.js";import{MarkupFilter as c,stripMarkup as u}from"./markup-filter.js";import{TOOL_SPECS as d,READONLY_TOOLS as m}from"./tools.js";import{toolLabel as f,indent as g,renderFileDiff as p}from"./render.js";import{compactIfNeeded as h,buildModelMessages as b,contextGauge as y}from"./compaction.js";import{catalogTokenBudget as w,noteMemberLimitFromError as $}from"./context-window.js";import{syncSkillCatalog as S,catalogDigestOf as x}from"./skill-catalog.js";import{makeTodoTool as k,formatTodos as _,TODO_TOOL_NAME as M}from"./todo.js";import{makeDecisionTool as j,DECISION_TOOL_NAME as I,recordDecision as T,extractDecisionFromTurn as E}from"./decisions.js";import{makeMemoryTool as v}from"./memory.js";import{loadHooks as C,runHooks as A}from"./hooks.js";import{capToolResult as O,capOrSpill as R,MAX_TOOL_RESULT_CHARS as P}from"./spill.js";import{runToolCall as N,isBinaryFile as U}from"./toolpipe.js";import{inspectForTurn as J,isCodeFile as D}from"./code-tools.js";import{EngineeringRun as z}from"./engineering.js";import{inspectWebForTurn as K,isWebFile as F,turnCorrection as L}from"./web-inspect.js";import{inspectDocsForTurn as W,docTurnCorrection as B}from"./docx-tools.js";import{isFileWriteTool as q}from"./permissions.js";import{decideUpgrade as Q}from"./routing.js";import{renderSdk as G,CODE_MODE_HIDDEN as H}from"./codemode.js";import*as V from"./ide.js";import{c as X}from"./colors.js";export{O as capToolResult,R as capOrSpill,P as MAX_TOOL_RESULT_CHARS};export function toolConcurrency(e){const t=Number(e?.toolConcurrency);if(Number.isFinite(t)&&t>0)return Math.min(24,Math.floor(t));let n=4;try{n=s.cpus()?.length||4}catch{}return Math.max(8,Math.min(24,n-2))}const Y=/context[_\s-]?length|maximum context|context window|too many tokens|reduce the length|prompt is too long|上下文.*(超|过长|超出|溢出)|输入.*过长|token.*(超出|超过).*(上限|限制)/i;export function isContextOverflowError(e){return Y.test(String(e||""))}const Z=32768,ee=new Map;function te(e,t){e&&(ee.delete(e),ee.set(e,t),ee.size>200&&ee.delete(ee.keys().next().value))}function ne(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 oe=/(现在|接下来|下面|这就|马上|我来|让我|我要|我先|我这就|咱们|让我们|稍等|待我)/,se=/(做|进行|生成|编写|写(一|个|完|出)?|制作|计算|统计|核对|汇总|整理|分析|梳理|输出|列出|准备|运行|执行|开始)/,re=/\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 ie(e){const t=String(e||"").trim().split(/\n+/).map(e=>e.trim()).filter(Boolean),n=t[t.length-1]||"";return!(!n||n.length>120)&&(!!re.test(n)||oe.test(n)&&se.test(n))}function ae(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 le(e){return"\n\n# 当前目标(用户设定)\n"+String(e).trim()+"\n在本回合结束、停止作答前,检查该目标是否已达成:若未达成,应继续推进(补充调研/改动/验证),不要过早停止;达成后用一句话说明已达成。"}const ce="【用户中断】",ue="\n\n# 上一轮被用户中断(重要)\n上一回合被用户主动中断:会话里标注「"+ce+"」的工具调用并未真正执行、没有任何结果。请勿臆测这些未完成工具的输出,也不要假设被中断的工作已完成或推断其结论;一切以用户最新一条消息为准——如与被中断的工作无关,就按新指令重新开始,需要时先与用户确认再继续。";function de(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(ce))return!0}return!1}function me(e){return"string"==typeof e?e:Array.isArray(e)?e.filter(e=>"text"===e?.type).map(e=>e.text||"").join("\n"):""}async function fe(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)&&!x(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 ge(e,t=200){const n=String(e).split("\n")[0];return n.length>t?n.slice(0,t)+"…":n}export async function runTurn(o){n();const s=e();if(!s.scope)return pe(o);const r=new AbortController,i=()=>r.abort(o.signal?.reason);o.signal?.aborted?i():o.signal?.addEventListener("abort",i,{once:!0});const a=setInterval(()=>{s.current()||r.abort(new Error("登录状态已改变,已停止旧账号任务"))},100);a.unref();try{return await t(s,()=>pe({...o,signal:r.signal}))}finally{clearInterval(a),o.signal?.removeEventListener("abort",i)}}async function pe({config:e,messages:t,permissions:n,cwd:s,ask:x,signal:O,extraTools:R=[],depth:P=0,io:Y,events:oe,todoState:se,compaction:re,pullQueued:pe,maxIters:he,fileJournal:be}){const ye=Y&&Y.write||(e=>process.stdout.write(e)),we=oe||null,$e=e=>{we?we.onInfo?.(e):ye(X.gray(" · "+e)+"\n")};let Se=0,xe=0,ke=0,_e=null;const Me=Number(he||e.maxTurns)||120,je=isPlatformModel(e.model),Ie={low:24e4,medium:36e4,high:6e5}[e.reasoningEffort];let Te=!1,Ee=String(e.fallbackModel||"").trim(),ve=0,Ce=e.model,Ae=null;if(0===P&&e.autoModelStrong&&!je){const n=[...t].reverse().find(e=>"user"===e.role),o=me(n?.content);Ae=Q({text:o,planMode:e.planMode,attachmentCount:e.attachmentCount},{threshold:e.autoModelThreshold}),Ae.upgrade&&e.autoModelStrong!==e.model&&(Ce=e.autoModelStrong),we?.onRouteDecision?.({...Ae,model:Ce,upgraded:Ce!==e.model}),e.debug&&$e(`[automodel] ${Ae.reason}`+(Ce!==e.model?` → 升配 ${Ce}`:" → 维持"))}const Oe=re||{},Re=[...R],Pe=!je&&Re.some(e=>"engineering_plan"===e.spec?.function?.name)?new z(s):null;let Ne=0;if(0===P&&!je&&!Re.some(e=>e.spec?.function?.name===M)){const e=se||{list:[]};Re.push(k({state:e,emit:e=>{we?we.onTodo?we.onTodo(e.map(e=>({...e}))):we.onInfo?.("任务清单:\n"+_(e)):ye(X.gray(g(_(e)," "))+"\n")}}))}if(0!==P||je||!1===e.memoryEnabled||Re.some(e=>"memory_write"===e.spec?.function?.name)||Re.push(v({cwd:s,emit:e=>{we?we.onMemory?we.onMemory(e):we.onInfo?.("已记入长期记忆:"+ge(e,80)):ye(X.gray(" ✎ 已记入长期记忆:"+ge(e,80))+"\n")}})),0===P&&!je&&!1!==e.decisionsEnabled&&!Re.some(e=>e.spec?.function?.name===I)){const n=t.filter(e=>"user"===e.role).length;Re.push(j({cwd:s,sessionId:e.sessionId||"",turn:n,emit:e=>{ct=!0,we?we.onDecision?we.onDecision(e):we.onInfo?.("已记录决策:"+ge(e.title,80)):ye(X.gray(" ◆ 已记录决策:"+ge(e.title,80))+"\n")}}))}const Ue=new Map;let Je=null,De=[],ze=new Set,Ke=0,Fe=null,Le=new Set,We=null;const Be=t=>{if("function"!=typeof t?.enabled)return!1;try{return!1===t.enabled(e)}catch{return!1}},qe=()=>{if(Array.isArray(R)&&R.length>Ke){for(const e of R.slice(Ke)){const t=e?.spec?.function?.name;t&&!Ue.has(t)&&Re.push(e)}Ke=R.length,Je=null}Fe!==e.toolMode&&(Fe=e.toolMode,Je=null);const t=je?[]:Re.filter(Be).map(e=>e.spec.function.name),n=t.join(",");if(We!==n&&(We=n,Le=new Set(t),Je=null),null===Je){Ue.clear();for(const e of Re)Ue.set(e.spec.function.name,e);const t="both"===e.toolMode||"code"===e.toolMode;De=je?[]:[...d,...Re.filter(e=>!Le.has(e.spec.function.name)).map(e=>e.spec)].filter(e=>t||"run_program"!==e.function.name),ze=new Set(De.map(e=>e.function.name));const n="code"===e.toolMode&&ze.has("run_program");Je=je?void 0:n?De.filter(e=>!H.has(e.function.name)):De}};Ke=Array.isArray(R)?R.length:0,qe();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=e.maxTokens||8192;let ft=Math.max(mt,ee.get(e.sessionId)||0),gt=0,pt=!1,ht=!1;const bt=0!==P||je?null:C(s),yt=!(!bt||!bt.all.length);if(yt){const n=[];if(t.filter(e=>"user"===e.role).length<=1){const t=A("SessionStart",{cwd:s,sessionId:e.sessionId||"",model:e.model||""},bt);t.injectContext&&n.push(t.injectContext);for(const e of t.outputs)n.push(e.stdout)}const o=[...t].reverse().find(e=>"user"===e.role),r=A("UserPromptSubmit",{cwd:s,prompt:me(o?.content),sessionId:e.sessionId||"",model:e.model||"",turn:t.filter(e=>"user"===e.role).length},bt);if(r.block)return $e("提交被UserPromptSubmit钩子阻断:"+r.reason),{error:"提交被钩子阻断:"+r.reason,usage:{prompt:Se,completion:xe},lastPromptTokens:ke,lastUsage:_e};r.injectContext&&n.push(r.injectContext);for(const e of r.outputs)n.push(e.stdout);n.length&&(Ze="【钩子提供的附加上下文】\n"+n.join("\n"))}for(let d=0;d<Me;d++){qe();const g=d===Me-1;!g||je||Ge||Ze||(Ze="已接近本回合的工具调用轮数上限。请不要再调用任何工具,基于目前已获得的信息,直接用中文输出面向用户的完整最终答复。");const x=je?0:await h(t,e,{state:Oe,lastPromptTokens:ke});x>0&&e.debug&&$e(`[debug] 已压缩上下文(${x} 条较早消息已摘要)`);let k=!1,_=!1;const M=new c,j=e=>{e&&(k||!_||we||ye("\n"),k=!0,we?we.onDelta?.(e):ye(e))};if(0===P&&!je&&!1!==e.skillsEnabled){const n=b(t,Oe),o=S(t,n,s,{allowInsert:!(Oe.upTo>0),maxTokens:w(e)});"none"!==o.action&&e.debug&&$e(`[skills] 目录${{initial:"首投",replace:"已刷新",restore:"压缩后重投"}[o.action]}(${o.count} 条,指纹 ${o.digest})`),"none"!==o.action&&we?.onSkillCatalog?.({...o})}const I=je?buildPlatformMessages(t):b(t,Oe);let v=I;if(!je&&I.length&&"system"===I[0]?.role){let n="";e.planMode&&(n+="\n\n# 计划模式(当前开启)\n你现在处于计划模式:只能用只读工具(read_file/glob/grep/list_files/fetch_url)调研,不得做任何改动——write_file/edit_file/run_bash等写或执行操作都会被拒绝,不要反复尝试。请先充分理解需求与现状,然后产出一份清晰、分步的实施方案:涉及哪些文件、每步改动要点、如何验证、风险点,用todo_write列出步骤,随后停下等待用户确认。用户关闭计划模式后你再执行。"),e.sandbox&&(n+="\n\n# 沙箱模式(当前开启)\n禁止执行shell命令(run_bash)、联网(fetch_url)与环境/应用自动化工具(install_*/setup_environment/open_*),调用会被拒绝,不要反复尝试。只做本地文件读写与分析。"),e.goal&&String(e.goal).trim()&&(n+=le(e.goal)),de(t)&&(n+=ue),ze.has("run_program")&&(n+="\n\n# 用程序编排工具(run_program)\n"+G(De)),n&&(v=[{...I[0],content:String(I[0].content||"")+n},...I.slice(1)])}const C=[];dt?C.push({role:"assistant",content:dt},{role:"user",content:"你上一条回复因达到单轮输出长度上限而被截断,用户尚未看到剩余部分。请从截断处直接继续输出剩余内容:无缝衔接上文(若截断处在句子、代码块或表格中间,从中断字符处接着写),不要重复任何已输出的内容,不要道歉或解释,不要重新开头或总结前文。"}):Ze&&C.push({role:"user",content:Ze});const R=Date.now();let z,Q=!1,H="";try{z=await i({baseUrl:e.baseUrl,apiKey:e.apiKey,model:Ce,supportsImages:e.imageCapabilities?.[Ce],messages:C.length?[...v,...C]:v,tools:Ge||g?void 0:Je,maxTokens:ft,extraBody:pt?{enable_thinking:!1}:void 0,reasoningEffort:e.reasoningEffort,stallTimeoutMs:Ie,client:e.client,sessionId:e.sessionId,aux:e.aux||void 0,continuation:!!dt,tailCount:C.length},{onContent:e=>{Q=!0,H+=e,j(M.feed(e))},onReasoning:t=>{!1!==e.showReasoning&&(dt||(we?we.onReasoning?.(t):(_||ye(X.gray("\n[思考] ")),ye(X.gray(t))),_=!0))},onReasoningEnd:()=>we?.onReasoningEnd?.(),onRetry:(t,n,o,s)=>{s?.network?$e(`${o.replace(/[::].*$/,"")},${Math.round(n/1e3)} 秒后自动重试(第 ${t}/${s.max} 次)`):e.debug&&$e(`[retry] 模型响应异常,${Math.round(n/1e3)}s后自动重试(第 ${t} 次):${o}`)}},O)}catch(n){if("AbortError"===n.name)return we?we.onInfo?.("已中断"):ye(X.gray("\n[已中断]\n")),{aborted:!0,usage:{prompt:Se,completion:xe},lastPromptTokens:ke,lastUsage:_e};if(!n.nonRetryable&&!n.partial&&ft>mt&&!ht&&!O?.aborted){ht=!0,gt=2,ft=mt,ee.delete(e.sessionId),e.debug&&$e(`[debug] 抬高输出预算后请求失败(${n.message}),已撤回到 ${mt} 重试`);continue}if(!n.nonRetryable&&!n.partial&&!je&&!O?.aborted&&isContextOverflowError(n.message)&&ve<3){ve++,$(e,n.message);const o=await h(t,e,{state:Oe,force:!0});if(o>0){Q&&H&&(dt+=H),e.debug&&$e(`[overflow] 上下文超出模型窗口,已强制压缩 ${o} 条较早消息后重试`);continue}e.debug&&$e("[overflow] 上下文超限但无可压缩内容,转兜底模型")}const o=l(n);if(!(o||n.nonRetryable||n.partial||n.network||je||Te||O?.aborted)){if(!Ee)try{const t=await a(e.baseUrl,e.apiKey);Ee=String(t&&t.fallback_model||"").trim()}catch{}if(Ee&&Ee!==Ce){Te=!0,Ce=Ee,Q&&H&&(dt+=H),e.debug&&$e(`[fallback] 主模型失败(${n.message}),静默切兜底模型 ${Ee}${Q?"(续写)":""}`);continue}}return we?we.onInfo?.("✗ 模型调用失败:"+n.message):ye("\n"+X.red("✗ 模型调用失败:")+n.message+"\n"),{error:n.message,errorKind:o,usage:{prompt:Se,completion:xe},lastPromptTokens:ke,lastUsage:_e}}j(M.flush()),z.usage&&(Se+=z.usage.prompt_tokens||0,xe+=z.usage.completion_tokens||0,ke=z.usage.prompt_tokens||ke,_e=z.usage,0===P&&we&&we.onUsage?.({lastPromptTokens:ke,prompt:Se,completion:xe,ctx:y({messages:t,compaction:Oe,config:e,lastPromptTokens:ke})})),e.debug&&$e(`[debug] 轮${d+1} · ${e.model} · in ${z.usage?.prompt_tokens||0} / out ${z.usage?.completion_tokens||0} tok · ${Date.now()-R}ms · ${(z.toolCalls||[]).length} 工具调用`);const{text:Y,sawMarkup:oe}=u(z.content),se=oe||M.sawMarkup;Ze=null;const re=je?[]:z.toolCalls;if(!re.length){if(se&&!Y.trim()&&!je){if(Qe<2){Qe++,2===Qe&&(Ge=!0),Ze=1===Qe?"你刚才的输出把工具调用写成了文本标记,工具并未被执行,该内容也不会展示给用户。请重新处理:若需调用工具,必须使用规范的函数调用(tool_calls)通道发起,绝不要以任何文本形式书写工具调用标记;若无需工具,请直接用中文输出面向用户的最终答复。":"请停止调用任何工具。基于目前已获得的全部信息,直接用中文输出面向用户的完整最终答复,不要输出任何工具调用或协议标记。",$e(`已拦截模型的文本化工具调用标记(工具未被执行),纠正重试中(第 ${Qe} 次)`);continue}$e("模型持续输出无效的工具调用标记,已全部拦截;本轮无有效答复(可换个说法重试或更换模型)")}if(!je&&"length"===z.finishReason&&Y&&it<3){it++,dt+=Y,e.debug&&$e(`[debug] 输出达单轮长度上限,自动续写(第 ${it} 段)`);continue}if(!je&&"length"===z.finishReason&&!Y.trim()&&!dt){if(gt<2&&ft<Z){gt++,ft=Math.min(3*ft,Z),te(e.sessionId,ft),e.debug&&$e(`[debug] 思考吃光输出预算(正文为空),抬高单轮上限到 ${ft} 重试(第 ${gt} 次)`);continue}if(!pt){pt=!0,Ze="你上一条把全部输出预算用在了思考上,没有输出任何正文,用户什么都没看到。请立刻直接输出面向用户的最终答复(结论、表格、结果放最前面),思考尽量精简。",e.debug&&$e("[debug] 抬高预算后仍无正文,改为要求先出正文(并best-effort关思考)");continue}}k&&!we&&ye("\n");let n=dt?dt+Y:Y;dt="",n&&t.push({role:"assistant",content:n});const o=!n.trim();if(lt&&!o&&ie(n)&&"length"!==z.finishReason&&!Ge&&at<2){at++,e.debug&&$e(`[debug] 检测到「宣告未落地」预告式收尾,注入续跑纠偏(第 ${at} 次)`),t.push({role:"user",content:'你上一条只是说明"接下来要做什么",但既没有发起工具调用、也没有给出结果——用户还没拿到任何最终产物。请立即执行你刚才所说的步骤(需要就用run_python/工具,不需要就直接算),并直接输出面向用户的完整最终结果(例如完整的按月汇总表与逐月核对表、差异结论),不要再只描述你打算做什么。'});continue}if(o&&!je&&"length"!==z.finishReason&&!Ge&&at<2){at++,e.debug&&$e(`[debug] 本回合正文为空,注入续跑纠偏(第 ${at} 次)`),Ze="你上一条回复没有输出任何面向用户的正文(内容可能全部留在了思考里),用户一个字都没拿到。请立刻用中文直接输出完整的最终答复,不要只在思考里推演。";continue}if(!je&&0===P&&!1!==e.codeSelfCheck&&et.size&&tt<1&&!Ge&&!o&&"length"!==z.finishReason){tt++;const t=await J(s,[...et],e);if(t&&t.issues.length){const e=t.issues.slice(0,12),n=e.map(e=>{const t=r.relative(s,e.path)||e.path;return` [${e.rule}] ${t}:${e.line}${e.unit?` 〔${e.unit}〕`:""}\n 问题:${e.msg}\n 改法:${e.hint||"按上面的说明处理"}`}).join("\n"),o=t.issues.length>e.length?`\n …… 另有 ${t.issues.length-e.length} 条,用code_inspect看全量。`:"";$e(`代码自检:本回合改动的 ${et.size} 个源码文件有 ${t.issues.length} 处必修问题,已要求修复`),Ze="【代码自检】你本回合改动的源码经体检发现以下**必修**问题,请逐条改完再结束:\n"+n+o+"\n\n改完用code_inspect(path) 复检确认必修清零。\n若确认某一条是误报,在那一行上方加 `// code-inspect-ignore: 规则名 简短理由`,并在回复里说明为什么判定它是误报——不要默默跳过,也不要在没修完时就宣布完成。";continue}}if(!je&&0===P&&!1!==e.webSelfCheck&&nt.size&&ot<1&&!Ge&&!o&&"length"!==z.finishReason){ot++;const t=await K(s,[...nt],e).catch(()=>null);if(t&&t.errors.length){$e(`网页自检:本回合改动的页面(${t.entries.join("、")})在浏览器里有 ${t.errors.length} 处必修问题,已要求修复`),Ze=L(t);continue}}if(!je&&0===P&&!1!==e.docSelfCheck&&st.size&&rt<1&&!Ge&&!o&&"length"!==z.finishReason){rt++;const t=await W(s,[...st],e).catch(()=>null);if(t&&t.issues.length){$e(`文档自检:本回合生成的 ${st.size} 份文档有 ${t.issues.length} 处必修问题,已要求修复`),Ze=B(t,s);continue}}if(o&&!se){const e=gt>0||pt?",自动抬高上限并重试后仍未产出":"";$e("length"===z.finishReason?`模型把本回合的输出预算(单轮上限 ${ft})全用在了思考上,没有产出正文${e}。建议把任务拆小一点分步做,或换个模型再试。`:`模型本回合没有返回任何正文${at>0?"(已自动重试)":""}。可以直接重发,或换个说法/换个模型再试。`)}const i=Pe?.pendingNote();if(i){if(!Ge&&Ne<2&&!O?.aborted){Ne++,Ze=i,$e(i);continue}n+="\n\n【未完成验证】"+i,"assistant"===t.at(-1)?.role?t.at(-1).content=n:t.push({role:"assistant",content:n}),$e("【未完成验证】"+i)}we&&we.onTurnEnd?.(n);const a=pe?.();if(a){t.push({role:"user",content:a});continue}if(yt){const o=A("Stop",{cwd:s,result:n,sessionId:e.sessionId||"",model:e.model||"",turn:t.filter(e=>"user"===e.role).length},bt);for(const e of o.outputs)$e("钩子·Stop:"+ge(e.stdout,160))}if(!je&&e.decisionAutoExtract&&!1!==e.decisionsEnabled&&lt&&!ct&&n&&n.trim())try{const o=[...t].reverse().find(e=>"user"===e.role),r=o&&"string"==typeof o.content?o.content:"",i=await E(e,{userText:r,assistantText:n,toolNames:[...ut]}),a="number"==typeof e.decisionExtractMinConfidence?e.decisionExtractMinConfidence:.55;if(i&&(i.confidence??0)>=a){const n=t.filter(e=>"user"===e.role).length,o=T(s,{...i,title:i.what,status:"draft",origin:"agent",confidence:Math.min(i.confidence??.5,.7),sessionId:e.sessionId||"",turn:n},{origin:"agent"});we?we.onDecision?we.onDecision(o):we.onInfo?.("已抽取决策草稿(待确认):"+ge(o.title,80)):ye(X.gray(" ◇ 已抽取决策草稿(待确认):"+ge(o.title,80))+"\n")}}catch{}return{usage:{prompt:Se,completion:xe},lastPromptTokens:ke,lastUsage:_e}}lt=!0,k&&!we&&ye("\n"),we&&Y&&we.onAssistantText?.(Y),t.push({role:"assistant",content:dt?dt+Y:Y,tool_calls:re,...z.nativeState?{huicai_anthropic:z.nativeState}:{}}),dt="";const me=re.map((t,n)=>{const o=t.function?.name||"";let s={},r=null,i=!1;const a=t.function?.arguments||"";if(a)try{s=JSON.parse(a)}catch(e){i=truncatedArgs(a,e,z.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&&Le.has(o)&&!r){const t="function"==typeof l.disabledMessage?l.disabledMessage(e):l.disabledMessage;r=String(t||`工具 ${o} 当前已停用(开关已关闭),未执行。`)}const c=V.isActive()&&("write_file"===o||"edit_file"===o);return{tc:t,idx:n,name:o,args:s,argsError:r,argsTruncated:i,extra:l,readOnly:l?!!l.readOnly:m.has(o),deferToIde:c}});!je&&me.some(e=>e.argsTruncated)&&gt<2&&ft<Z&&(gt++,ft=Math.min(3*ft,Z),te(e.sessionId,ft),e.debug&&$e(`[debug] 工具参数被单轮输出上限截断,抬高到 ${ft}`));const he=e=>{for(let n=e;n<me.length;n++)t.push({role:"tool",tool_call_id:me[n].tc.id||`call_${n}`,content:ce+" 该工具未执行,无任何结果,请勿臆测其输出。"});return we?we.onInfo?.("已中断"):ye(X.gray("\n[已中断]\n")),{aborted:!0,usage:{prompt:Se,completion:xe},lastPromptTokens:ke,lastUsage:_e}},Ae={cwd:s,config:e,signal:O,permissions:n,hooks:bt,hooksActive:yt,fileJournal:be,info:$e,shortLine:ge,engineering:Pe,turn:t.filter(e=>"user"===e.role).length,onStart:e=>e.nested?$e("程序内调用:"+f(e.name,e.args)):Ke(e),listTools:()=>De,resolveTool:e=>{const t=Ue.get(e);return t&&Le.has(e)?null:t?{readOnly:!!t.readOnly,extra:t}:ze.has(e)?{readOnly:m.has(e),extra:null}:null}},Re=e=>N({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(X.cyan(" ⏺ ")+f(e.name,e.args)+"\n")},Fe=(e,n)=>{const o=e.item||e;ae(n)&&He++;const s=ne(e.name,e.args);s===Ve?Xe++:(Ve=s,Xe=1,Ye=0),we?we.onToolResult?.(e.name,String(n),{callId:o.tc?.id,failed:ae(n)}):ye(X.gray(" "+ge(n))+"\n"),t.push({role:"tool",tool_call_id:o.tc.id,content:String(n)})},We=(e,n)=>{const o=e.item||e;we?we.onSkip?.(e.name,e.args,{callId:o.tc?.id,reason:n}):ye(X.gray(` ⊘ ${f(e.name,e.args)} — 已跳过\n`)),t.push({role:"tool",tool_call_id:o.tc.id,content:n||"未授权,未执行。"})},Be=e=>"render_diagram"!==e,wt=e=>{try{return o.readFileSync(e,"utf8")}catch{return""}},$t=(e,t)=>{const n=r.resolve(s,String(e.args.path));if(U(n))return;const o=p(t,wt(n),{path:String(e.args.path)});o&&(D(n)&&et.add(n),F(n)&&nt.add(n),we?we.onFileDiff?.(o):ye(o+"\n"))};let St=0;for(;St<me.length;){if(O?.aborted)return he(St);const t=me[St];if(t.readOnly&&!t.argsError){let t=St;for(;t<me.length&&me[t].readOnly&&!me[t].argsError;)t++;const n=me.slice(St,t),o=await fe(n,toolConcurrency(e),e=>Re(e));for(const e of o)null!=e.denied?We(e,e.denied):Fe(e,e.result);if(St=t,O?.aborted)return he(St);continue}const n=await Re(t);if(O?.aborted&&null==n.denied&&!n.settled)return he(St);if(null==n.denied)if(n.settled)Ke(n),Fe(n,n.result),St++;else{if(Fe(n,n.result),null!=n.meta.before&&Be(n.name)&&$t(n,n.meta.before),q(n.name)&&/\.docx$/i.test(String(n.args?.path||""))){const e=r.resolve(s,String(n.args.path));o.existsSync(e)&&st.add(e)}St++}else We(n,n.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&&t.push({role:"user",content:xt})}return dt&&(t.push({role:"assistant",content:dt}),we&&we.onTurnEnd?.(dt)),we?we.onInfo?.("已达最大工具轮数,停止"):ye(X.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}
@@ -1 +1 @@
1
- import e from"node:fs";import s from"node:path";import t from"node:os";import{parseFrontmatter as i,countUserSkills as n}from"./skills.js";import{checkLimit as r,currentPlan as o,remainingOf as c}from"./plan-limits.js";export const EXTERNAL_SOURCES=[{cli:"Claude Code",agents:".claude/agents",skills:".claude/skills"},{cli:"Codex",agents:".codex/agents",skills:".codex/skills"},{cli:"Gemini CLI",agents:".gemini/agents",skills:".gemini/skills"},{cli:"Cursor",agents:".cursor/agents",skills:".cursor/skills"}];const l=new Set([".md",".txt",".py",".js",".ts",".sh",".json",".yaml",".yml",".csv",".sql",".toml",".ini"]),a=/^[a-zA-Z0-9_-]{1,64}$/;function m(){return s.join(t.homedir(),".huicai","agents")}function d(){return s.join(t.homedir(),".huicai","skills")}function p(){try{return new Set(e.readdirSync(m()).filter(e=>e.endsWith(".md")).map(e=>e.replace(/\.md$/,"")))}catch{return new Set}}export function discoverExternalAssets({base:n=t.homedir()}={}){const r=p(),o=function(){try{return new Set(e.readdirSync(d(),{withFileTypes:!0}).filter(e=>e.isDirectory()).map(e=>e.name))}catch{return new Set}}(),c=[],l=[],m=new Set,u=new Set;for(const t of EXTERNAL_SOURCES){const d=s.join(n,t.agents);let p=[];try{p=e.readdirSync(d).filter(e=>e.endsWith(".md"))}catch{}for(const n of p){const o=s.join(d,n);let l;try{l=i(e.readFileSync(o,"utf8"))}catch{continue}const p=String(l.name||n.replace(/\.md$/,"")).trim();a.test(p)&&!m.has(p)&&(m.add(p),c.push({name:p,description:(l.description||"").slice(0,200),cli:t.cli,src:o,exists:r.has(p)}))}const f=s.join(n,t.skills);let h=[];try{h=e.readdirSync(f,{withFileTypes:!0}).filter(e=>e.isDirectory())}catch{}for(const n of h){const r=n.name.trim(),c=s.join(f,r),m=s.join(c,"SKILL.md");if(!a.test(r)||u.has(r)||!e.existsSync(m))continue;u.add(r);let d="";try{d=(i(e.readFileSync(m,"utf8")).description||"").slice(0,200)}catch{}l.push({name:r,description:d,cli:t.cli,src:c,exists:o.has(r)})}}return{agents:c,skills:l}}function u(t,i,n=0){if(n>3)return 0;let r=0,o=[];try{o=e.readdirSync(t,{withFileTypes:!0})}catch{return 0}e.mkdirSync(i,{recursive:!0});for(const c of o){const o=s.join(t,c.name),a=s.join(i,c.name);if(c.isDirectory())r+=u(o,a,n+1);else if(c.isFile()&&("SKILL.md"===c.name||l.has(s.extname(c.name).toLowerCase())))try{e.copyFileSync(o,a),r++}catch{}}return r}export function importExternalAssets(t={},{overwrite:i=!1}={}){const l=[],f=[],h={agents:{imported:0,skipped:0},skills:{imported:0,skipped:0},details:l,limited:f},k=m(),y=d(),g=o(),S=p().size,x=n();let j=c("agents",S,g),$=c("skills",x,g);const w=(e,s)=>{const t=r(e,s,g)?.message;t&&!f.includes(t)&&f.push(t)};for(const n of t.agents||[]){if(!n||!a.test(String(n.name))||!n.src)continue;const t=s.join(k,n.name+".md"),r=e.existsSync(t);if(i||!r)if(!r&&j<=0)w("agents",S+h.agents.imported),h.agents.skipped++,l.push(`跳过agent(已达会员上限):${n.name}`);else try{e.mkdirSync(k,{recursive:!0}),e.copyFileSync(n.src,t),h.agents.imported++,r||j--,l.push(`导入agent:${n.name}`)}catch(e){l.push(`失败agent ${n.name}:${e.message}`)}else h.agents.skipped++,l.push(`跳过agent(已存在):${n.name}`)}for(const n of t.skills||[]){if(!n||!a.test(String(n.name))||!n.src)continue;const t=s.join(y,n.name),r=e.existsSync(s.join(t,"SKILL.md"));if(!i&&e.existsSync(t)){h.skills.skipped++,l.push(`跳过skill(已存在):${n.name}`);continue}if(!r&&$<=0){w("skills",x+h.skills.imported),h.skills.skipped++,l.push(`跳过skill(已达会员上限):${n.name}`);continue}const o=u(n.src,t);o>0?(h.skills.imported++,r||$--,l.push(`导入skill:${n.name}(${o} 文件)`)):(h.skills.skipped++,l.push(`跳过skill(无内容):${n.name}`))}return h}export function importAllExternalAssets(e={}){const s=discoverExternalAssets(e);return importExternalAssets({agents:s.agents.filter(e=>!e.exists),skills:s.skills.filter(e=>!e.exists)},e)}
1
+ import e from"node:fs";import{accountDataDir as s}from"./account-scope.js";import t from"node:path";import i from"node:os";import{parseFrontmatter as n,countUserSkills as r}from"./skills.js";import{checkLimit as o,currentPlan as c,remainingOf as l}from"./plan-limits.js";export const EXTERNAL_SOURCES=[{cli:"Claude Code",agents:".claude/agents",skills:".claude/skills"},{cli:"Codex",agents:".codex/agents",skills:".codex/skills"},{cli:"Gemini CLI",agents:".gemini/agents",skills:".gemini/skills"},{cli:"Cursor",agents:".cursor/agents",skills:".cursor/skills"}];const a=new Set([".md",".txt",".py",".js",".ts",".sh",".json",".yaml",".yml",".csv",".sql",".toml",".ini"]),m=/^[a-zA-Z0-9_-]{1,64}$/;function d(){return t.join(s(),"agents")}function p(){return t.join(s(),"skills")}function u(){try{return new Set(e.readdirSync(d()).filter(e=>e.endsWith(".md")).map(e=>e.replace(/\.md$/,"")))}catch{return new Set}}export function discoverExternalAssets({base:s=i.homedir()}={}){const r=u(),o=function(){try{return new Set(e.readdirSync(p(),{withFileTypes:!0}).filter(e=>e.isDirectory()).map(e=>e.name))}catch{return new Set}}(),c=[],l=[],a=new Set,d=new Set;for(const i of EXTERNAL_SOURCES){const p=t.join(s,i.agents);let u=[];try{u=e.readdirSync(p).filter(e=>e.endsWith(".md"))}catch{}for(const s of u){const o=t.join(p,s);let l;try{l=n(e.readFileSync(o,"utf8"))}catch{continue}const d=String(l.name||s.replace(/\.md$/,"")).trim();m.test(d)&&!a.has(d)&&(a.add(d),c.push({name:d,description:(l.description||"").slice(0,200),cli:i.cli,src:o,exists:r.has(d)}))}const f=t.join(s,i.skills);let k=[];try{k=e.readdirSync(f,{withFileTypes:!0}).filter(e=>e.isDirectory())}catch{}for(const s of k){const r=s.name.trim(),c=t.join(f,r),a=t.join(c,"SKILL.md");if(!m.test(r)||d.has(r)||!e.existsSync(a))continue;d.add(r);let p="";try{p=(n(e.readFileSync(a,"utf8")).description||"").slice(0,200)}catch{}l.push({name:r,description:p,cli:i.cli,src:c,exists:o.has(r)})}}return{agents:c,skills:l}}function f(s,i,n=0){if(n>3)return 0;let r=0,o=[];try{o=e.readdirSync(s,{withFileTypes:!0})}catch{return 0}e.mkdirSync(i,{recursive:!0});for(const c of o){const o=t.join(s,c.name),l=t.join(i,c.name);if(c.isDirectory())r+=f(o,l,n+1);else if(c.isFile()&&("SKILL.md"===c.name||a.has(t.extname(c.name).toLowerCase())))try{e.copyFileSync(o,l),r++}catch{}}return r}export function importExternalAssets(s={},{overwrite:i=!1}={}){const n=[],a=[],k={agents:{imported:0,skipped:0},skills:{imported:0,skipped:0},details:n,limited:a},y=d(),h=p(),g=c(),S=u().size,x=r();let j=l("agents",S,g),$=l("skills",x,g);const w=(e,s)=>{const t=o(e,s,g)?.message;t&&!a.includes(t)&&a.push(t)};for(const r of s.agents||[]){if(!r||!m.test(String(r.name))||!r.src)continue;const s=t.join(y,r.name+".md"),o=e.existsSync(s);if(i||!o)if(!o&&j<=0)w("agents",S+k.agents.imported),k.agents.skipped++,n.push(`跳过agent(已达会员上限):${r.name}`);else try{e.mkdirSync(y,{recursive:!0}),e.copyFileSync(r.src,s),k.agents.imported++,o||j--,n.push(`导入agent:${r.name}`)}catch(e){n.push(`失败agent ${r.name}:${e.message}`)}else k.agents.skipped++,n.push(`跳过agent(已存在):${r.name}`)}for(const r of s.skills||[]){if(!r||!m.test(String(r.name))||!r.src)continue;const s=t.join(h,r.name),o=e.existsSync(t.join(s,"SKILL.md"));if(!i&&e.existsSync(s)){k.skills.skipped++,n.push(`跳过skill(已存在):${r.name}`);continue}if(!o&&$<=0){w("skills",x+k.skills.imported),k.skills.skipped++,n.push(`跳过skill(已达会员上限):${r.name}`);continue}const c=f(r.src,s);c>0?(k.skills.imported++,o||$--,n.push(`导入skill:${r.name}(${c} 文件)`)):(k.skills.skipped++,n.push(`跳过skill(无内容):${r.name}`))}return k}export function importAllExternalAssets(e={}){const s=discoverExternalAssets(e);return importExternalAssets({agents:s.agents.filter(e=>!e.exists),skills:s.skills.filter(e=>!e.exists)},e)}