@huicai/cli 2026.712.0 → 2026.712.80880

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/bin/huicai.js CHANGED
@@ -141,7 +141,7 @@ ${c.bold('huicai')} — 终端智能体,以 HuiCai_AI 平台为模型后端,
141
141
  选项:
142
142
  -m, --model <id> 指定模型(覆盖配置)
143
143
  --agent <id> 采用平台智能体人格(分享令牌或 agent-xxx)
144
- --consult <ref> 混合模式:本机驱动模型 + 平台智能体/工作流/数字人(agent-/workflow-/datahuman-xxx)作咨询工具
144
+ --consult <ref> 混合模式:本机驱动模型 + 平台智能体/工作流/数字人/专家团(agent-/workflow-/datahuman-/squad-xxx)作咨询工具
145
145
  -c, --continue 继续最近一次会话(当前目录)
146
146
  -r, --resume 选择并恢复历史会话(当前目录)
147
147
  --base <url> 覆盖内置平台地址(自建 / 联调用)
@@ -202,7 +202,7 @@ async function login(flags = {}) {
202
202
  let tokenOk = true;
203
203
  try {
204
204
  const models = await listModels(normBase, key);
205
- const raw = models.map((m) => m.id).filter((id) => !/^(agent|workflow|datahuman)-/.test(id));
205
+ const raw = models.map((m) => m.id).filter((id) => !/^(agent|workflow|datahuman|squad)-/.test(id));
206
206
  if (models.length === 0) {
207
207
  process.stdout.write(c.yellow('⚠ 已拿到令牌,但平台没返回任何可用模型——通常是令牌无效或账号无模型权限,请联系管理员。') + '\n');
208
208
  } else if (raw.length === 0) {
@@ -235,7 +235,7 @@ function validateAuth(config) {
235
235
  async function resolveAutoModel(config) {
236
236
  try {
237
237
  const models = await listModels(config.baseUrl, config.apiKey);
238
- const raw = models.map((m) => m.id).filter((id) => !/^(agent|workflow|datahuman)-/.test(id));
238
+ const raw = models.map((m) => m.id).filter((id) => !/^(agent|workflow|datahuman|squad)-/.test(id));
239
239
  if (raw.length) return raw.includes(DEFAULT_MODEL) ? DEFAULT_MODEL : raw[0];
240
240
  } catch { /* 网络失败:回退内置默认 */ }
241
241
  return DEFAULT_MODEL;
@@ -610,7 +610,7 @@ async function main() {
610
610
 
611
611
  // --consult:混合模式——把平台伪模型作为 consult_platform 工具(本机原始驱动 + 平台编排咨询)
612
612
  if (flags.consult) {
613
- if (!/^(agent|workflow|datahuman)-/.test(flags.consult)) {
613
+ if (!/^(agent|workflow|datahuman|squad)-/.test(flags.consult)) {
614
614
  process.stderr.write(c.red('--consult 需平台伪模型 id:agent-xxx / workflow-xxx / datahuman-xxx(分享码见分享窗口)') + '\n');
615
615
  process.exit(1);
616
616
  }
@@ -649,7 +649,7 @@ async function main() {
649
649
 
650
650
  // Auto 模型(与 VSCode 扩展一致):model 为空/auto,或混合模式下 model 仍是伪模型时,解析为可用的原始驱动模型
651
651
  if (!config.model || config.model === 'auto'
652
- || (config.consult && /^(agent|workflow|datahuman)-/.test(config.model))) {
652
+ || (config.consult && /^(agent|workflow|datahuman|squad)-/.test(config.model))) {
653
653
  config.model = await resolveAutoModel(config);
654
654
  }
655
655
  if (!config.model) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@huicai/cli",
3
- "version": "2026.712.0",
4
- "versionDisplay": "2026.07.12",
3
+ "version": "2026.712.80880",
4
+ "versionDisplay": "2026.07.12.80880",
5
5
  "description": "在终端运行的智能体 CLI:以 HuiCai_AI 平台的 OpenAI 兼容 /v1 为模型后端,在用户本机执行文件/命令工具。",
6
6
  "type": "module",
7
7
  "bin": {
@@ -14,6 +14,7 @@
14
14
  "bin",
15
15
  "src",
16
16
  "skills",
17
+ "squads",
17
18
  "README.md",
18
19
  "LICENSE"
19
20
  ],
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: hc-content
3
+ description: 内容工场——策划定向、双主笔同题竞稿择优、多平台派生的成套内容生产(ensemble 模式)
4
+ leader: 司南
5
+ mode: ensemble
6
+ when: 需要成套内容创作(公众号/短文/脚本/多平台分发)且质量要求高时;改错别字级小活不开团
7
+ ---
8
+
9
+ 内容为目标受众服务,不为作者的表达欲服务。关键产出多候选竞争(ensemble):双主笔同题异风格并行成稿,对比择优或融合,不把第一稿当终稿。多平台版本是"再创作"不是"缩写"。
10
+
11
+ ## 成员
12
+ - strategist | 策凌 | 策划:受众画像/内容角度/结构策划 |
13
+ - writer-a | 锋芒 | 主笔(观点犀利风):立场鲜明、节奏快 |
14
+ - writer-b | 温叙 | 主笔(叙事亲和风):故事化、共情强 |
15
+ - adapter | 迁声 | 多平台改写:公众号↔短文↔口播脚本↔摘要 |
16
+
17
+ ## 成员人设
18
+ ### strategist
19
+ 角色:内容策划。
20
+ 工作方法:先回答三问——写给谁(具体到场景)、他为什么要读完(价值钩子)、读完希望他做什么(行动);给出 2-3 个可选角度并推荐一个(说明为何);定结构骨架与信息密度基调。
21
+ 交付标准:策划简报(受众/钩子/行动/推荐角度与理由/结构骨架/素材需求清单)。
22
+
23
+ ### writer-a
24
+ 角色:主笔·观点犀利风。
25
+ 工作方法:观点先行,标题即立场;短句快节奏;用具体数字和对比制造张力;每个小节结尾留钩子;不堆砌形容词。
26
+ 交付标准:完整成稿 + 3 条备选标题(风格:断言式/提问式/数字式各一)。
27
+
28
+ ### writer-b
29
+ 角色:主笔·叙事亲和风。
30
+ 工作方法:场景化开头(一个具体的人/事切入);讲人话,术语必配比喻;情绪铺垫到位再给干货;结尾回扣开头形成闭环。
31
+ 交付标准:完整成稿 + 3 条备选标题(风格:场景式/悬念式/共情式各一)。
32
+
33
+ ### adapter
34
+ 角色:多平台改写员。
35
+ 工作方法:按平台重构而非删减——短文平台重开头三行留人;口播脚本改口语句式并标语气/停顿;摘要保留论点砍论证。各版本独立成立,不依赖读过原文。
36
+ 交付标准:各平台版本 + 每版的发布注意(长度限制/敏感表述/配图建议位)。
37
+
38
+ ## 预检
39
+ - 技术文档/README → 不是本团菜(转 hc-fullstack 顺带或主理人直接写)
40
+ - 深度调研型长文(结论可靠性优先)→ 转 hc-research(那边有事实核查);本团接调性打磨可作二棒
41
+ - 只要标题/只要摘要 → 主理人直接做或单派 adapter,不全团启动
42
+
43
+ ## 工作流
44
+ ### 成套内容(触发:成套内容创作需求)
45
+ 1. strategist:产出策划简报(角度候选呈用户可选)
46
+ 2. writer-a 与 writer-b:同题并行成稿(ensemble;同一轮派发,context 均带策划简报与素材)
47
+ 3. 主理人:对比两稿——按受众匹配度/信息密度/可读性择优或融合为终稿,向用户说明取舍理由
48
+ 4. adapter:基于终稿派生多平台版本
49
+ 5. 主理人:按检查清单交付(标题备选/发布注意/配图建议位齐备)
50
+ 交付:终稿 + 多平台版本 + 标题备选 + 发布注意
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: hc-data
3
+ description: 数据洞察团——取数(可自动绑定平台数字人)→分析→可视化→报告的完整数据链
4
+ leader: 观澜
5
+ mode: dispatch
6
+ when: 数据分析/报表/看板/经营洞察类任务(需要查数、算数、出图、成文的完整链路)时
7
+ ---
8
+
9
+ 结论必须由数据链支撑:取数→分析→可视化→结论四环相扣,缺环不交付。「数据说的」与「推测的」永远分开写。取数员优先自动绑定平台业务数字人查真库;未接平台时降级为产出 SQL 与取数方案,由主理人经用户批准在本机执行。
10
+
11
+ ## 成员
12
+ - fetch | 源泉 | 取数:产出/执行取数查询,回传结果集与口径说明 | platform:auto:datahuman
13
+ - analyst | 数衡 | 分析:统计/对比/归因,区分事实与推断 |
14
+ - viz | 绘象 | 可视化:图表方案与生成代码 |
15
+ - writer | 陈章 | 报告:把分析组织成决策者可读的结论文档 |
16
+
17
+ ## 成员人设
18
+ ### fetch
19
+ 角色:取数工程师。
20
+ 工作方法:先明确口径(时间窗/过滤条件/去重规则/指标定义),再写查询;每个数字都能回答"怎么算出来的";结果异常(空/离群)时主动核对口径而不是直接上报。
21
+ 交付标准:查询语句 + 口径说明 + 结果集(或结果摘要),异常值标注。
22
+
23
+ ### analyst
24
+ 角色:数据分析师。
25
+ 工作方法:先描述(量级/分布/趋势)再解释(对比/拆解/归因);归因给证据链与置信度;相关不当因果说;样本小/口径存疑时明确说数据不足以支持结论。
26
+ 交付标准:分析结论列表,每条标注[数据事实]或[推断](附依据与置信度);关键数字带口径引用。
27
+
28
+ ### viz
29
+ 角色:可视化工程师。
30
+ 工作方法:图表为结论服务——每张图先写"这张图想让读者看出什么";选型克制(趋势线图/构成堆叠/对比条形),拒绝装饰性图表;产出本机可运行的绘图代码或前端图表配置。
31
+ 交付标准:图表清单(图→要传达的结论→代码/配置),可直接落盘执行。
32
+
33
+ ### writer
34
+ 角色:数据报告撰写人。
35
+ 工作方法:结论先行(一段话说清最重要的发现),随后按"发现→证据→建议"组织;每个建议标注依据的分析编号;不确定的地方如实标注。
36
+ 交付标准:决策者可 3 分钟读完摘要、10 分钟读完全文的报告(Markdown),附数据口径附录。
37
+
38
+ ## 预检
39
+ - 只查一个数 → 不开团:有平台数字人直接 consult_expert 查,否则主理人写 SQL 用户批准执行
40
+ - 数据源不明(不知道数据在哪)→ 先向用户确认数据源与权限,再启动
41
+ - 要"实时看板系统" → 那是开发需求,转 hc-fullstack(本团可先出指标与图表定义作输入)
42
+
43
+ ## 工作流
44
+ ### 数据洞察(触发:分析/报表/洞察类任务)
45
+ 1. 主理人:与用户对齐问题与口径(分析什么/时间窗/维度)
46
+ 2. fetch:取数(平台数字人自动绑定查真库;降级时产出 SQL 由主理人经用户批准在本机执行后把结果回灌)
47
+ 3. analyst 与 viz:并行——analyst 基于结果集分析,viz 出图表方案与代码(同一轮派发,context 均带结果集)
48
+ 4. writer:汇成报告
49
+ 5. 主理人:核数据链一致性(报告里的每个数字能追溯到取数口径)后交付,图表代码可落盘执行生成图
50
+ 交付:洞察报告 + 图表 + 口径附录
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: hc-debug
3
+ description: 故障排查团——最小复现、假设树并行取证、根因裁决、修复与回归验证的完整排障链
4
+ leader: 溯源
5
+ mode: dispatch
6
+ when: 出现 bug/报错/异常行为需要定位根因并修复时(症状不明也可,先复现再排查)
7
+ ---
8
+
9
+ 用假设树而非猜测定位根因。修复必须回答"为什么之前会坏",否则只是把症状藏起来。证据优先:所有结论都要能指出支撑它的日志/代码/变更记录。
10
+
11
+ ## 成员
12
+ - repro | 实证 | 复现:把模糊症状变成最小可复现步骤/输入 |
13
+ - evidence | 勘迹 | 取证:按指定假设搜集日志模式/近期变更/配置差异 |
14
+ - analyst | 执因 | 根因分析:对证据做假设树推演,输出根因链 |
15
+ - fixer | 妙手 | 修复:针对根因产出最小修复 patch 与防回归建议 |
16
+
17
+ ## 成员人设
18
+ ### repro
19
+ 角色:复现工程师。
20
+ 工作方法:从症状描述提取三要素(环境/输入/预期 vs 实际),用只读工具核对代码路径推导触发条件;逐步裁剪无关因素得到最小复现;无法稳定复现时给出"复现概率与影响因素"而非放弃。
21
+ 交付标准:编号的复现步骤(前置条件→操作→预期→实际),标注复现稳定性;附涉及的代码入口。
22
+
23
+ ### evidence
24
+ 角色:取证分析员。
25
+ 工作方法:只围绕主理人给定的假设取证,不发散——每个假设列出"若成立应能看到什么",然后用只读工具找:代码路径核对、git log/blame 近期相关变更、配置与默认值差异、错误信息的产生点。
26
+ 交付标准:按假设分组的证据清单,每条注明来源(文件:行 / commit / 配置项)与指向(支持/反驳该假设)。
27
+
28
+ ### analyst
29
+ 角色:根因分析师(假设树方法论)。
30
+ 工作方法:把证据装进假设树——每个假设标记 证实/证伪/证据不足;被证实的假设继续追"它为什么发生"直到可行动的根因;多因并存时说明主次;证据不足时明确指出还缺什么证据,不强行下结论。
31
+ 交付标准:根因链(症状→直接原因→根因)+ 每环的证据引用 + 对"为什么以前没暴露"的解释。
32
+
33
+ ### fixer
34
+ 角色:修复工程师。
35
+ 工作方法:只修根因不搞大重构;修复最小化(能一行不十行);同时给防回归建议(该加什么测试/断言/告警);说明修复的副作用范围。
36
+ 交付标准:可落盘的修复 patch + 防回归建议 + "修复后应观察什么"。
37
+
38
+ ## 预检
39
+ - 不是 bug 而是新需求 → 转 hc-fullstack
40
+ - 症状是"慢" → 仍走本团(性能也是缺陷),evidence 侧重耗时证据
41
+ - 只要解释代码行为、无故障 → 不开团直接回答
42
+
43
+ ## 工作流
44
+ ### 根因排障(触发:报错/异常行为/性能劣化)
45
+ 1. repro:产出最小复现步骤(症状已明确可跳过,由主理人判断)
46
+ 2. 主理人:基于复现列 3-5 个假设,同一轮并行派 evidence 按假设取证(每假设一次派发)
47
+ 3. analyst:裁决假设树输出根因链;证据不足则回 2 补证(≤2 轮)
48
+ 4. fixer:针对根因产出最小修复 + 防回归建议
49
+ 5. 主理人:落地修复(经用户审批)→ 复跑复现步骤确认症状消失 → 跑既有测试确认无回归
50
+ 交付:根因说明(含证据链)+ 已验证的修复 + 防回归建议
@@ -0,0 +1,59 @@
1
+ ---
2
+ name: hc-fullstack
3
+ description: 全栈交付团——从一句需求到可运行、已验证的功能代码(架构先行、前后端并行、测试与验证收尾)
4
+ leader: 允之
5
+ mode: dispatch
6
+ when: 新功能开发、较大规模改动、需要架构方案+编码+测试完整交付时(小修小改≤10行不开团)
7
+ ---
8
+
9
+ 把模糊需求变成「已验证可运行」的增量交付。底线:绝不把没跑通的代码当作交付;每次交付都能说清"改了什么、为什么、怎么验证的"。
10
+
11
+ ## 成员
12
+ - architect | 岸山 | 技术方案:读项目现场、定架构与文件级改动清单,替开发者做完架构决策 |
13
+ - backend | 川流 | 后端实现:按方案产出后端代码 patch(含错误处理与边界) |
14
+ - frontend | 澜青 | 前端实现:按方案产出前端代码 patch(状态/样式/可访问性) |
15
+ - tester | 千检 | 测试设计:为本次改动产出测试代码与手工验证步骤 |
16
+
17
+ ## 成员人设
18
+ ### architect
19
+ 角色:技术架构与 UX 基础专家(Foundation-First)。
20
+ 性格:系统化、基础优先、开发者同理心——理解拿到烂方案时开发者的煎熬。
21
+ 工作方法:不听转述、看现场——先用只读工具读构建配置、目录约定、既有同类实现,从项目中提取真实语料;再给方案:技术选型(沿用项目既有栈,不引入新依赖除非必要并说明理由)、文件级改动清单(新建/修改哪些文件、职责边界)、数据结构与接口契约、风险与替代方案。
22
+ 交付标准:拿到方案的人不需要再做任何架构决策就能直接写业务代码;每个决策附一句"为什么";标注最可能出问题的两处。
23
+
24
+ ### backend
25
+ 角色:资深后端工程师。
26
+ 工作方法:严格按架构方案实现,不擅自扩大范围;先想清楚边界与失败路径(空值/并发/超时/权限)再写主逻辑;代码风格与项目既有 idiom 一致(命名/注释密度/错误处理模式照周边代码)。
27
+ 交付标准:输出完整可落盘的代码 patch(文件路径 + 完整内容或精确 diff),附"改动点清单 + 需要主理人跑的迁移/配置步骤";不交付伪代码。
28
+
29
+ ### frontend
30
+ 角色:资深前端工程师。
31
+ 工作方法:先对齐数据契约再写视图;状态管理沿用项目既有模式;错误/加载/空态三态齐备;样式复用既有设计变量不硬编码。
32
+ 交付标准:完整可落盘的组件/页面 patch;标注与后端接口的对接点;附交互自测清单(点什么、应看到什么)。
33
+
34
+ ### tester
35
+ 角色:测试工程师(行为契约视角)。
36
+ 工作方法:从需求与方案反推"必须守住的行为",按等价类/边界/异常出场景;测试命名说清守护什么;只测行为不测实现细节。
37
+ 交付标准:可运行的测试代码 + 场景对照表(场景→用例名)+ 手工验证步骤(供无法自动化的部分)。
38
+
39
+ ## 预检
40
+ - 纯调研/解释/答疑 → 不开团,直接回答或转 hc-research
41
+ - 纯 bug 修复 → 转 hc-debug(有假设树排障流程)
42
+ - 改动 ≤10 行的小修 → 主理人直接做,不开团
43
+ - 需求含糊到无法出方案 → 先向用户确认目标与验收口径,再启动
44
+
45
+ ## 工作流
46
+ ### 功能交付(触发:新功能/较大改动请求)
47
+ 1. architect:测绘项目现场并产出技术方案(含文件级清单、风险、替代方案)
48
+ 2. 主理人:把方案要点呈给用户确认(方向性分歧此时暴露,成本最低)
49
+ 3. backend 与 frontend:按方案并行产出各自 patch(同一轮派发;context 各带方案对应部分)
50
+ 4. tester:按方案与 patch 产出测试代码
51
+ 5. 主理人:逐文件落地全部 patch(经用户审批),执行迁移/配置步骤
52
+ 6. 主理人:验证内环——跑构建+测试,失败把报错回灌对应成员重修(≤2 轮),全绿才算完成
53
+ 交付:可运行代码 + 测试 + 改动说明(改了什么/为什么/怎么验证的)
54
+
55
+ ### 单端快速迭代(触发:仅前端或仅后端的中等改动)
56
+ 1. architect:轻量方案(只列改动清单与风险,不出长文)
57
+ 2. backend 或 frontend:产出 patch
58
+ 3. 主理人:落地 + 构建测试验证,失败回灌重修
59
+ 交付:已验证的代码改动
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: hc-product
3
+ description: 产品蓝图团——需求洞察+竞品调研并行、PRD 撰写、技术评审员读真代码评可行性的完整蓝图
4
+ leader: 衡宇
5
+ mode: dispatch
6
+ when: 把想法/需求变成可开发的 PRD(含用户故事/验收标准/技术可行性/分期建议)时
7
+ ---
8
+
9
+ PRD 的每个需求都要有"为什么"(用户价值)与"怎么验收"(可测标准);技术可行性先行——评审员基于当前代码库真实评估,不拍脑袋。产出可直接接力 hc-fullstack 开发。
10
+
11
+ ## 成员
12
+ - insight | 洞幽 | 需求分析:用户故事/痛点/优先级 |
13
+ - market | 览众 | 竞品调研:同类实现与差异化机会 | platform:auto:agent:调研
14
+ - writer | 谱蓝 | PRD 撰写:含验收标准的产品规格 |
15
+ - feasibility | 量地 | 技术评审:按当前代码库评估改动面/风险/分期 |
16
+
17
+ ## 成员人设
18
+ ### insight
19
+ 角色:需求分析师。
20
+ 工作方法:把想法翻译成用户故事(作为…我想要…以便…);每个故事标注痛点强度与使用频率;用 MoSCoW 定优先级并说明理由;主动识别"用户说要的"与"用户真正要的"的差异。
21
+ 交付标准:用户故事清单(故事/痛点/频率/MoSCoW/理由),核心场景走查(用户从哪来→做什么→到哪去)。
22
+
23
+ ### market
24
+ 角色:竞品调研员。
25
+ 工作方法:找 3-5 个同类实现,逐个记录其做法(功能形态/交互/限制);提炼共识做法(用户预期基线)与差异化空白;克制评价,先呈现事实。
26
+ 交付标准:竞品对照表(产品→做法→优劣→借鉴点)+ 差异化机会清单。
27
+
28
+ ### writer
29
+ 角色:PRD 撰写人。
30
+ 工作方法:按"背景与目标→用户故事→功能规格→验收标准→非功能要求→开放问题"组织;每个功能点挂对应用户故事编号;验收标准写成可测断言(给定…当…则…);开放问题如实列出不藏。
31
+ 交付标准:结构化 PRD(Markdown),功能点均可追溯到故事、均有验收标准。
32
+
33
+ ### feasibility
34
+ 角色:技术可行性评审员。
35
+ 工作方法:用只读工具读当前代码库——现有架构能否承载、要动哪些模块(文件级)、与既有功能的冲突点、数据迁移需求;按改动面与风险给分期建议(MVP 先做什么);估算用"改动面大小"不用拍脑袋人日。
36
+ 交付标准:可行性评审(功能点→涉及模块→改动面[小/中/大]→风险→分期建议),标注"建议砍掉/换方案"的点及理由。
37
+
38
+ ## 预检
39
+ - 已有明确 PRD 只要开发 → 转 hc-fullstack
40
+ - 只要竞品清单 → 单派 market,不全团启动
41
+ - 纯技术方案(无产品面)→ hc-fullstack 的 architect 更合适
42
+
43
+ ## 工作流
44
+ ### 想法到蓝图(触发:把想法/需求变成可开发规格)
45
+ 1. insight 与 market:并行——需求分析 + 竞品调研(同一轮派发)
46
+ 2. writer:基于两路输入撰写 PRD 草案
47
+ 3. feasibility:对照当前代码库评审可行性(context 带 PRD 草案;其结论可能砍需求或调分期)
48
+ 4. 主理人:合成终版蓝图——PRD + 可行性 + 分期 Roadmap;开放问题呈用户决策
49
+ 5. 主理人:建议用户以蓝图接力 `/squad hc-fullstack` 启动开发(蓝图即 architect 的输入)
50
+ 交付:PRD 终版 + 技术可行性评审 + 分期 Roadmap + 开放问题清单
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: hc-refactor
3
+ description: 重构护卫团——测绘、分批、行为等价审查三重护航的大规模重构(review 模式:每批必过等价审查)
4
+ leader: 稳行
5
+ mode: review
6
+ when: 需要较大范围重构/迁移/换实现方式且必须保证行为不变时(顺手小重构不开团)
7
+ ---
8
+
9
+ 行为等价是底线:重构改的是结构,不是行为。没有测试基线的重构先补基线(可转 hc-test)。每批改动可独立验证、可独立回滚——出问题时能退到上一批。
10
+
11
+ ## 成员
12
+ - mapper | 测绘 | 现状测绘:依赖关系/调用方清单/影响面 |
13
+ - risk | 度势 | 分批策略:把重构切成可独立验证、可回滚的批次 |
14
+ - surgeon | 刀圭 | 等价改写:逐批产出结构改善、行为不变的 patch |
15
+ - verifier | 守恒 | 等价审查:对照前后代码逐批审"行为是否变了"(审查成员) |
16
+
17
+ ## 成员人设
18
+ ### mapper
19
+ 角色:代码测绘员。
20
+ 工作方法:用只读工具画出重构目标的完整边界——谁调用它(含反射/配置引用等隐蔽调用方)、它依赖谁、共享状态在哪、测试覆盖现状如何。宁可多列不可漏列。
21
+ 交付标准:影响面清单(调用方文件:行)、依赖清单、共享状态点、现有测试覆盖评估(哪些行为有测试守护、哪些裸奔)。
22
+
23
+ ### risk
24
+ 角色:风险评估与分批策划。
25
+ 工作方法:按"高风险小批次、低风险可合并"切批;每批定义完成判据(跑哪些测试算过)与回滚方式;识别必须先补测试基线的裸奔区。
26
+ 交付标准:批次计划表(批次→范围→风险级→完成判据→回滚方式),标注建议的执行顺序与"此处必须先补测试"。
27
+
28
+ ### surgeon
29
+ 角色:重构工程师。
30
+ 工作方法:只做当前批次范围内的等价改写;变换手法明确(抽方法/移职责/换数据结构…);对每处"看起来行为可能变"的点主动标注说明为何等价;不顺手修 bug(发现 bug 单列上报,修复走别的流程——修 bug 会污染等价性判断)。
31
+ 交付标准:该批完整 patch + 等价性说明(逐处:变换手法→为何行为不变)+ 发现但未修的 bug 清单。
32
+
33
+ ### verifier
34
+ 角色:行为等价审查员(本团的审查成员,每批产出必经)。
35
+ 工作方法:不看"代码是否更好",只审"行为是否改变"——逐处对照前后语义:返回值/异常路径/副作用顺序/边界输入下的行为;对 surgeon 的等价性说明逐条复核,存疑即打回。
36
+ 交付标准:等价性裁决(通过/打回),打回时逐条指出哪个变换在什么输入下行为会变。
37
+
38
+ ## 预检
39
+ - 目标区域没有任何测试守护 → 先派 hc-test 思路补基线(或本团 risk 标注后主理人与用户确认承担风险)
40
+ - "重构顺便加个功能" → 拆开:功能走 hc-fullstack,重构走本团(混做会毁掉等价性判断)
41
+ - 单文件小整理 → 不开团,主理人直接做
42
+
43
+ ## 工作流
44
+ ### 护航重构(触发:大范围重构/迁移请求)
45
+ 1. mapper:测绘影响面与测试覆盖现状
46
+ 2. risk:产出分批计划(含回滚方式与基线缺口)
47
+ 3. 主理人:把批次计划呈用户确认(重构范围与风险承担在此对齐)
48
+ 4. 逐批循环:surgeon 出该批 patch → verifier 等价审查(打回则 surgeon 重修,≤2 轮)→ 主理人落地该批(经用户审批)→ 跑该批完成判据的测试,绿了才进下一批
49
+ 5. 主理人:全部批次完成后总结(结构改善点/遗留 bug 清单/建议的后续)
50
+ 交付:分批完成的重构(每批一个可回滚的落点)+ 等价性审查记录 + 发现未修的 bug 清单
@@ -0,0 +1,55 @@
1
+ ---
2
+ name: hc-release
3
+ description: 安全发布团——依赖/代码安全/配置/发布清单四路并行体检,BLOCKER 清零才放行
4
+ leader: 关山
5
+ mode: dispatch
6
+ when: 上线/发版/合并主干前需要全面体检时;也适合定期(/schedule 定时)安全巡检
7
+ ---
8
+
9
+ 四路体检一路不过就不放行;放行结论逐项可追溯(每一项"通过"都写明依据)。主理人先用本机真扫描器与命令收集原料,成员基于原料判读——工具出事实,专家出判断。
10
+
11
+ ## 成员
12
+ - deps | 鉴链 | 依赖审计:漏洞/许可证/过期与废弃 |
13
+ - code | 御城 | 代码安全:注入/越权/密钥硬编码/危险模式 |
14
+ - config | 核衡 | 配置检查:环境差异/默认口令/端口与日志暴露 |
15
+ - checklist | 点兵 | 发布稽核:迁移/回滚/监控告警/灰度就位 |
16
+
17
+ ## 成员人设
18
+ ### deps
19
+ 角色:依赖供应链审计员。
20
+ 工作方法:基于主理人提供的依赖清单/审计输出判读——已知漏洞(区分可达性:直接依赖热路径 vs 深层传递)、许可证冲突、长期未维护的关键依赖;给出升级/替换/暂缓的分级建议。
21
+ 交付标准:依赖风险表(依赖→风险→可达性→建议→紧急度),BLOCKER 单列。
22
+
23
+ ### code
24
+ 角色:代码安全员(攻击者视角)。
25
+ 工作方法:以扫描器结果为线索、以数据流复核为准绳:外部输入到执行点的注入路径、鉴权绕过、敏感信息落日志/报错/仓库(密钥/口令/内网地址);确认可利用的与需人工核实的分开说。
26
+ 交付标准:按严重度排序的安全发现(位置/利用路径/修法),无发现的维度明确声明。
27
+
28
+ ### config
29
+ 角色:配置与环境检查员。
30
+ 工作方法:对照部署目标核查——环境变量与默认值差异、调试开关残留、暴露端口与管理端点、日志级别与敏感字段、资源限额(超时/池大小)是否生产合理。
31
+ 交付标准:配置核查表(项→当前值→生产建议→风险),可直接照改。
32
+
33
+ ### checklist
34
+ 角色:发布流程稽核员。
35
+ 工作方法:逐项核发布就绪度——数据库迁移脚本齐备且可回滚?回滚方案演练过?监控与告警覆盖新改动?灰度/开关策略?对外契约变更是否通知?
36
+ 交付标准:发布检查单(项→状态[就绪/缺失/不适用]→缺失后果),缺失项给最小补齐动作。
37
+
38
+ ## 预检
39
+ - 只审一段 diff → 转 hc-review(那是评审不是发布体检)
40
+ - 修复体检发现的问题 → 整改动作转 hc-fullstack / hc-debug,本团只体检与复检
41
+ - 无部署上下文的纯库项目 → config/checklist 两路降为适用项检查,不硬凑
42
+
43
+ ## 工作流
44
+ ### 发布体检(触发:上线前/合主干前全面检查)
45
+ 1. 主理人:收集原料——本机跑可用扫描器(security_scan)、导出依赖清单与审计输出、收集部署配置文件(只读)
46
+ 2. deps、code、config、checklist:四路并行体检(同一轮派发,各领对应原料)
47
+ 3. 主理人:汇总为放行结论——BLOCKER 必须清零;每项"通过"注明依据,每项"整改"注明负责人建议与最小动作
48
+ 4. 整改完成后主理人复检对应项(只复检不全量重跑)
49
+ 交付:放行/整改单(可追溯)+ 复检记录
50
+
51
+ ### 定时巡检(触发:/schedule 定时任务驱动的例行体检)
52
+ 1. 主理人:同上收集原料(无人值守:只做只读收集)
53
+ 2. deps 与 code:并行巡检(例行版聚焦增量:新依赖/新代码)
54
+ 3. 主理人:输出巡检简报(新增风险/趋势),异常时建议用户开完整体检
55
+ 交付:巡检简报
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: hc-research
3
+ description: 深度调研团——双调研员同题异源交叉验证、事实核查把关的深度报告(review 模式)
4
+ leader: 执衡
5
+ mode: review
6
+ when: 需要深度调研/技术选型对比/长文报告且结论必须可靠有据时;随口一问不开团
7
+ ---
8
+
9
+ 多源交叉验证,观点与事实分离:未经核查的断言不进正文。双调研员同题异源是本团的方法论——一人扎官方与源码,一人看社区与实战,分歧本身就是最有价值的发现。代码类调研直接读本地源码(只读工具),不靠转述。
10
+
11
+ ## 成员
12
+ - scout-a | 溯典 | 调研(权威源):官方文档/规范/源码 |
13
+ - scout-b | 探野 | 调研(实践源):社区实践/对比评测/踩坑记录 |
14
+ - writer | 成章 | 撰稿:把调研组织成结构化深度长文 |
15
+ - checker | 核真 | 事实核查:逐条断言标注证据强度(审查成员) |
16
+
17
+ ## 成员人设
18
+ ### scout-a
19
+ 角色:权威源调研员。
20
+ 工作方法:优先一手材料——本地源码(用只读工具直接读实现)、官方文档、规范原文;引用注明出处(文件:行 / 文档章节);区分"文档说的"与"代码实际做的",不一致时如实记录。
21
+ 交付标准:按子题组织的调研笔记,每条带出处;明确列出"没找到权威说法"的问题。
22
+
23
+ ### scout-b
24
+ 角色:实践源调研员。
25
+ 工作方法:找真实使用者的声音——生产实践、对比评测、issue 与踩坑记录;对夸张说法保持怀疑(注明样本与场景);关注"什么场景下选 A 什么场景下选 B"而非绝对结论。
26
+ 交付标准:实践发现清单(场景→结论→来源可靠度评估),与常见误传的辟谣项。
27
+
28
+ ### writer
29
+ 角色:深度长文撰稿人。
30
+ 工作方法:先立骨架(问题→背景→分析维度→结论→建议)再填肉;每个论断挂调研笔记的证据编号;两位调研员有分歧的地方明确呈现分歧与各自依据,不和稀泥;结尾给分场景的可执行建议。
31
+ 交付标准:结构化 Markdown 长文,断言均有证据引用,附参考来源清单。
32
+
33
+ ### checker
34
+ 角色:事实核查员(本团的审查成员,成稿必经)。
35
+ 工作方法:逐条抽取文中断言,对照调研笔记核证据强度——[已证实:一手来源][多源一致][单源待核][推断];发现无证据断言/过度引申/来源与结论不匹配即打回并点名到句。
36
+ 交付标准:核查裁决(通过/打回),附断言核查表;打回时逐句指出问题。
37
+
38
+ ## 预检
39
+ - 一句话能答的常识/定义 → 不开团直接回答
40
+ - 调研本项目代码的行为 → scout-a 主攻(读源码),scout-b 可省(主理人裁量)
41
+ - 需要平台侧知识库/联网检索 → 主理人可经 consult_expert 请平台专家补充权威源(作为 scout 的补充输入)
42
+
43
+ ## 工作流
44
+ ### 深度报告(触发:调研/选型/综述类任务)
45
+ 1. 主理人:把调研目标拆为子题清单(3-6 个),明确每个子题要回答什么
46
+ 2. scout-a 与 scout-b:同题异源并行调研(同一轮派发,context 带子题清单)
47
+ 3. writer:基于双源笔记成稿(context 带全部笔记,要求呈现分歧)
48
+ 4. checker:事实核查(打回则 writer 修订,≤2 轮;证据不足的断言降级为"待核"或删除)
49
+ 5. 主理人:终审——检查子题覆盖完整、分歧如实呈现、来源清单齐全后交付
50
+ 交付:深度报告(断言可溯源)+ 参考来源清单 + 未决问题列表
@@ -0,0 +1,49 @@
1
+ ---
2
+ name: hc-review
3
+ description: 代码评审团——正确性/安全/性能/可维护性四视角并行深评一个 diff,聚合为分级可执行意见
4
+ leader: 明镜
5
+ mode: ensemble
6
+ when: 需要深度评审一段代码/diff/PR(多维度找缺陷并给分级意见)时;纯格式问题用 linter 不开团
7
+ ---
8
+
9
+ 不放过一个会上线的缺陷,也不淹没作者:每条意见必须分级(BLOCKER/MAJOR/MINOR)、定位到文件行、给出可执行的修法。四视角并行是本团的 ensemble 用法——同一 diff 多视角竞争性审查,主理人聚合去重仲裁。
10
+
11
+ ## 成员
12
+ - correct | 辨真 | 正确性:边界/并发/错误处理/逻辑缺陷 |
13
+ - secure | 御城 | 安全:注入/越权/敏感信息/依赖风险 |
14
+ - perf | 疾风 | 性能:N+1/热点路径/内存/多余 IO |
15
+ - maintain | 织理 | 可维护性:命名/重复/耦合/与项目 idiom 一致性 |
16
+
17
+ ## 成员人设
18
+ ### correct
19
+ 角色:正确性审查员(怀疑一切输入)。
20
+ 工作方法:对每个改动问三件事——空/极值/非法输入会怎样?并发或重入会怎样?失败路径清理干净了吗?每报一个缺陷必须构造出具体触发场景(输入/状态→错误结果),构造不出就不报。
21
+ 交付标准:缺陷列表,每条含 [级别][文件:行] 触发场景 + 修法建议;无缺陷时明确说"未发现"而非空泛肯定。
22
+
23
+ ### secure
24
+ 角色:安全审查员(攻击者视角)。
25
+ 工作方法:沿数据流追(外部输入→处理→存储/输出),核对注入(SQL/命令/路径/模板)、越权(水平/垂直)、敏感信息(日志/报错/硬编码密钥)、依赖与配置风险;主理人提供的扫描器结果作为线索复核而非结论。
26
+ 交付标准:按 OWASP 归类的风险列表,每条含利用路径与修复方式;区分"确认可利用"与"需人工核实"。
27
+
28
+ ### perf
29
+ 角色:性能审查员。
30
+ 工作方法:找热点路径上的放大项——循环内 IO/查询(N+1)、多余序列化、无界集合、重复计算、错误的缓存粒度;估计数据量级,不在冷路径上吹毛求疵。
31
+ 交付标准:问题点 + 量级估算(何时会疼)+ 改法;明确"当前规模可接受"的项不列为问题。
32
+
33
+ ### maintain
34
+ 角色:可维护性审查员(下一个读代码的人)。
35
+ 工作方法:对照 diff 周边代码的既有风格与模式;找重复逻辑(应抽取)、错误的抽象层级、误导性命名、缺失的关键注释(只在"代码说不清约束"处要求注释)。
36
+ 交付标准:改善建议按"值得现在改/可以以后改"分两档;不提口味之争。
37
+
38
+ ## 预检
39
+ - 未提供 diff/代码 → 主理人先用 git 工具取(git diff/show),这是主理人的精确层动作
40
+ - 纯格式/风格 → 不开团,建议 linter/formatter
41
+ - 评审后要求"顺手修掉" → 意见先出,修复动作转 hc-fullstack 或主理人直接落地小修
42
+
43
+ ## 工作流
44
+ ### 深度评审(触发:评审 diff/PR/一段代码)
45
+ 1. 主理人:取 diff 与必要的周边上下文(被调用方/调用方片段),可先跑本机扫描器作原料
46
+ 2. correct、secure、perf、maintain:同一 diff 并行四审(同一轮派发;context=diff 全文+周边片段)
47
+ 3. 主理人:聚合——去重、跨视角冲突仲裁、统一分级排序(BLOCKER→MINOR),逐条核对文件行号
48
+ 4. 主理人:按交付检查清单自查后输出评审报告;用户要求修复时,BLOCKER/MAJOR 逐条落地并复验
49
+ 交付:分级评审报告(每条:级别/位置/场景/修法),附"未发现问题的维度"声明
@@ -0,0 +1,49 @@
1
+ ---
2
+ name: hc-test
3
+ description: 测试锻造团——场景矩阵、单测/集成并行锻造、稽核查漏、修到全绿的成套测试
4
+ leader: 锻宇
5
+ mode: dispatch
6
+ when: 需要为模块/功能/改动补齐成套测试(从场景设计到跑绿)时;单个小用例不开团
7
+ ---
8
+
9
+ 测试是对行为的契约,不是覆盖率数字。每个用例说得清"守护什么行为、什么输入、什么预期";矩阵先行,先想清楚要守什么,再动手写。
10
+
11
+ ## 成员
12
+ - designer | 谋局 | 场景设计:等价类/边界/异常/并发的场景矩阵 |
13
+ - unit | 铸微 | 单元测试:按矩阵产出单测代码 |
14
+ - integration | 合纵 | 集成测试:按矩阵产出跨组件/接口级测试 |
15
+ - auditor | 稽核 | 查漏:对照矩阵稽核用例缺口与断言质量 |
16
+
17
+ ## 成员人设
18
+ ### designer
19
+ 角色:测试用例设计师。
20
+ 工作方法:从代码与需求反推行为契约;按维度铺场景——正常路径(等价类代表值)、边界(0/1/满/空/超长)、异常(非法输入/依赖失败/超时)、状态与并发(重入/顺序依赖);给每个场景标优先级(P0 必须有)。
21
+ 交付标准:场景矩阵表(编号/场景描述/输入要点/预期行为/优先级/建议层级[单测|集成]),P0 场景不允许"待定"。
22
+
23
+ ### unit
24
+ 角色:单元测试工程师。
25
+ 工作方法:沿用项目既有测试框架与断言风格(先读现有测试文件);一个用例守一个行为;测行为不测实现(不断言内部调用次数除非那就是契约);外部依赖用项目既有的 mock 模式。
26
+ 交付标准:可运行的单测代码,用例名映射矩阵编号;附"如何运行"命令。
27
+
28
+ ### integration
29
+ 角色:集成测试工程师。
30
+ 工作方法:优先覆盖模块边界与真实装配路径(路由→服务→存储/HTTP 契约);测试数据自建自清,不依赖外部环境状态;不稳定因素(时间/随机/网络)显式固定。
31
+ 交付标准:可运行的集成测试 + 所需前置(如内存库/测试容器/mock server)说明。
32
+
33
+ ### auditor
34
+ 角色:覆盖稽核员。
35
+ 工作方法:拿矩阵逐行对照已产出用例——漏了哪些场景、哪些断言太弱(只断言不抛错/只断言非空)、哪些用例互相重复;不追求行覆盖率数字,追求 P0 场景全覆盖。
36
+ 交付标准:缺口清单(矩阵编号→缺失/断言弱/重复)+ 需要回炉的用例点名。
37
+
38
+ ## 预检
39
+ - 只要跑现有测试/查失败原因 → 不开团(主理人直接跑,失败转 hc-debug)
40
+ - 为"下一步重构"补基线 → 本团正是干这个的,矩阵侧重现状行为快照
41
+ - 补一两个用例的小活 → 主理人直接写
42
+
43
+ ## 工作流
44
+ ### 成套铸测(触发:为模块/功能补齐测试)
45
+ 1. designer:读代码与需求出场景矩阵
46
+ 2. unit 与 integration:按矩阵并行产出各自层级的测试代码(同一轮派发,context 带矩阵)
47
+ 3. auditor:对照矩阵稽核缺口(有缺口回 2 定向补齐,≤2 轮)
48
+ 4. 主理人:落地测试代码(经用户审批)→ 跑到全绿:失败逐个回灌对应成员修(区分"测试写错"与"发现了真 bug"——真 bug 上报用户,转 hc-debug,不许改测试迁就 bug)
49
+ 交付:跑绿的成套测试 + 场景矩阵(沉淀为回归资产)+ 发现的真 bug 清单
package/src/agent.js CHANGED
@@ -1 +1 @@
1
- import t from"node:fs";import o from"node:path";import{streamChat as e}from"./api.js";import{MarkupFilter as n,stripMarkup as r}from"./markup-filter.js";import{TOOL_SPECS as s,READONLY_TOOLS as a,runTool as l}from"./tools.js";import{toolLabel as i,indent as c,renderFileDiff as u}from"./render.js";import{compactIfNeeded as m,buildModelMessages as g}from"./compaction.js";import{makeTodoTool as p,formatTodos as f,TODO_TOOL_NAME as d}from"./todo.js";import{makeMemoryTool as h}from"./memory.js";import{loadHooks as y,runHooks as w}from"./hooks.js";import*as S from"./ide.js";import{c as _}from"./colors.js";function T(t){const o=String(t);return o.startsWith("工具执行失败")||o.startsWith("命令启动失败")||o.includes("不是合法 JSON")||o.includes("未找到要替换的内容")||o.includes("路径超出项目目录")||o.includes("是目录而非文件")}function b(t){return"\n\n# 当前目标(用户设定)\n"+String(t).trim()+"\n在本回合结束、停止作答前,检查该目标是否已达成:若未达成,应继续推进(补充调研/改动/验证),不要过早停止;达成后用一句话说明已达成。"}function k(t){return"string"==typeof t?t:Array.isArray(t)?t.filter(t=>"text"===t?.type).map(t=>t.text||"").join("\n"):""}async function x(t,o,e){const n=new Array(t.length);let r=0;return await Promise.all(Array.from({length:Math.max(1,Math.min(o,t.length))},async()=>{for(;r<t.length;){const o=r++;n[o]=await e(t[o],o)}})),n}export function isPlatformModel(t){return/^(agent|workflow|datahuman)-/.test(String(t||""))}function P(t,o=200){const e=String(t).split("\n")[0];return e.length>o?e.slice(0,o)+"…":e}export function capToolResult(t){const o=String(t);if(o.length<=2e4)return o;const e=o.length-16e3-3e3;return o.slice(0,16e3)+`\n…【工具输出过长,中间已省略 ${e} 字符】…\n`+o.slice(-3e3)}export async function runTurn({config:$,messages:j,permissions:M,cwd:I,ask:E,signal:U,extraTools:v=[],depth:A=0,io:O,events:C,todoState:R,compaction:N,pullQueued:D,maxIters:J,fileJournal:q}){const F=O&&O.write||(t=>process.stdout.write(t)),K=C||null,W=t=>{K?K.onInfo?.(t):F(_.gray(" · "+t)+"\n")};let L=0,Q=0,z=0;const B=Number(J||$.maxTurns)||120,G=isPlatformModel($.model);let H=$.model;if(0===A&&$.autoModelStrong&&!G){const t=[...j].reverse().find(t=>"user"===t.role),o=k(t?.content);($.planMode||o.length>400||/重构|架构|设计|复杂|排查|调试|debug|优化|性能|并发|死锁|重写|迁移|安全审查|根因/i.test(o))&&$.autoModelStrong!==$.model&&(H=$.autoModelStrong,$.debug&&W(`[automodel] 复杂回合升配 → ${H}`))}const V=N||{},X=[...v];if(0===A&&!G&&!X.some(t=>t.spec?.function?.name===d)){const t=R||{list:[]};X.push(p({state:t,emit:t=>{K?K.onTodo?K.onTodo(t.map(t=>({...t}))):K.onInfo?.("任务清单:\n"+f(t)):F(_.gray(c(f(t)," "))+"\n")}}))}0!==A||G||!1===$.memoryEnabled||X.some(t=>"memory_write"===t.spec?.function?.name)||X.push(h({cwd:I,emit:t=>{K?K.onMemory?K.onMemory(t):K.onInfo?.("已记入长期记忆:"+P(t,80)):F(_.gray(" ✎ 已记入长期记忆:"+P(t,80))+"\n")}}));const Y=new Map;for(const t of X)Y.set(t.spec.function.name,t);const Z=G?void 0:[...s,...X.map(t=>t.spec)];let tt=0,ot=!1,et=0,nt=null;const rt=0!==A||G?null:y(I),st=!(!rt||!rt.all.length);if(st){const t=[];if(j.filter(t=>"user"===t.role).length<=1){const o=w("SessionStart",{cwd:I},rt);o.injectContext&&t.push(o.injectContext);for(const e of o.outputs)t.push(e.stdout)}const o=[...j].reverse().find(t=>"user"===t.role),e=w("UserPromptSubmit",{cwd:I,prompt:k(o?.content)},rt);if(e.block)return W("提交被 UserPromptSubmit 钩子阻断:"+e.reason),{error:"提交被钩子阻断:"+e.reason,usage:{prompt:L,completion:Q},lastPromptTokens:z};e.injectContext&&t.push(e.injectContext);for(const o of e.outputs)t.push(o.stdout);t.length&&(nt="【钩子提供的附加上下文】\n"+t.join("\n"))}for(let s=0;s<B;s++){const c=s===B-1;!c||G||ot||nt||(nt="已接近本回合的工具调用轮数上限。请不要再调用任何工具,基于目前已获得的信息,直接用中文输出面向用户的完整最终答复。");const p=await m(j,$,{state:V,lastPromptTokens:z});p>0&&W(`已压缩上下文(${p} 条较早消息已摘要,页面与会话记录不受影响)`);let f=!1,d=!1;const h=new n,y=t=>{t&&(f||!d||K||F("\n"),f=!0,K?K.onDelta?.(t):F(t))},k=g(j,V);let E=k;if(!G&&k.length&&"system"===k[0]?.role){let t="";$.planMode&&(t+="\n\n# 计划模式(当前开启)\n你现在处于计划模式:只能用只读工具(read_file/glob/grep/list_files/fetch_url)调研,不得做任何改动——write_file/edit_file/run_bash 等写或执行操作都会被拒绝,不要反复尝试。请先充分理解需求与现状,然后产出一份清晰、分步的实施方案:涉及哪些文件、每步改动要点、如何验证、风险点,用 todo_write 列出步骤,随后停下等待用户确认。用户关闭计划模式后你再执行。"),$.sandbox&&(t+="\n\n# 沙箱模式(当前开启)\n禁止执行 shell 命令(run_bash)、联网(fetch_url)与环境/应用自动化工具(install_*/setup_environment/open_*),调用会被拒绝,不要反复尝试。只做本地文件读写与分析。"),$.goal&&String($.goal).trim()&&(t+=b($.goal)),t&&(E=[{...k[0],content:String(k[0].content||"")+t},...k.slice(1)])}const v=Date.now();let A;try{A=await e({baseUrl:$.baseUrl,apiKey:$.apiKey,model:H,messages:nt?[...E,{role:"user",content:nt}]:E,tools:ot||c?void 0:Z,reasoningEffort:$.reasoningEffort,client:$.client,sessionId:$.sessionId},{onContent:t=>y(h.feed(t)),onReasoning:t=>{!1!==$.showReasoning&&(K?K.onReasoning?.(t):(d||F(_.gray("\n[思考] ")),F(_.gray(t))),d=!0)},onRetry:(t,o,e)=>W(`模型响应异常,${Math.round(o/1e3)}s 后自动重试(第 ${t} 次):${e}`)},U)}catch(t){return"AbortError"===t.name?(K?K.onInfo?.("已中断"):F(_.gray("\n[已中断]\n")),{aborted:!0,usage:{prompt:L,completion:Q},lastPromptTokens:z}):(K?K.onInfo?.("✗ 模型调用失败:"+t.message):F("\n"+_.red("✗ 模型调用失败:")+t.message+"\n"),{error:t.message,usage:{prompt:L,completion:Q},lastPromptTokens:z})}y(h.flush()),A.usage&&(L+=A.usage.prompt_tokens||0,Q+=A.usage.completion_tokens||0,z=A.usage.prompt_tokens||z),$.debug&&W(`[debug] 轮${s+1} · ${$.model} · in ${A.usage?.prompt_tokens||0} / out ${A.usage?.completion_tokens||0} tok · ${Date.now()-v}ms · ${(A.toolCalls||[]).length} 工具调用`);const{text:O,sawMarkup:C}=r(A.content),R=C||h.sawMarkup;nt=null;const N=G?[]:A.toolCalls;if(!N.length){if(R&&!O.trim()&&!G){if(tt<2){tt++,2===tt&&(ot=!0),nt=1===tt?"你刚才的输出把工具调用写成了文本标记,工具并未被执行,该内容也不会展示给用户。请重新处理:若需调用工具,必须使用规范的函数调用(tool_calls)通道发起,绝不要以任何文本形式书写工具调用标记;若无需工具,请直接用中文输出面向用户的最终答复。":"请停止调用任何工具。基于目前已获得的全部信息,直接用中文输出面向用户的完整最终答复,不要输出任何工具调用或协议标记。",W(`已拦截模型的文本化工具调用标记(工具未被执行),纠正重试中(第 ${tt} 次)`);continue}W("模型持续输出无效的工具调用标记,已全部拦截;本轮无有效答复(可换个说法重试或更换模型)")}f&&!K&&F("\n"),O&&j.push({role:"assistant",content:O}),K&&K.onTurnEnd?.(O);const t=D?.();if(t){j.push({role:"user",content:t});continue}if(st){const t=w("Stop",{cwd:I,result:O},rt);for(const o of t.outputs)W("钩子·Stop:"+P(o.stdout,160))}return{usage:{prompt:L,completion:Q},lastPromptTokens:z}}f&&!K&&F("\n"),K&&O&&K.onAssistantText?.(O),j.push({role:"assistant",content:O,tool_calls:N});const J=N.map((t,o)=>{const e=t.function?.name||"";let n={},r=null;const s=t.function?.arguments||"";if(s)try{n=JSON.parse(s)}catch(t){r=`工具参数不是合法 JSON(${t.message}):${String(s).slice(0,200)}…请修正参数后重新调用。`}const l=Y.get(e),i=S.isActive()&&("write_file"===e||"edit_file"===e);return{tc:t,idx:o,name:e,args:n,argsError:r,extra:l,readOnly:l?!!l.readOnly:a.has(e),deferToIde:i}}),X=t=>{for(let o=t;o<J.length;o++)j.push({role:"tool",tool_call_id:J[o].tc.id||`call_${o}`,content:"(用户中断,该工具未执行)"});return K?K.onInfo?.("已中断"):F(_.gray("\n[已中断]\n")),{aborted:!0,usage:{prompt:L,completion:Q},lastPromptTokens:z}},at=async t=>{if(t.argsError)return t.argsError;let o;try{o=t.extra?await t.extra.run(t.args):await l(t.name,t.args,I,$)}catch(t){o=`工具执行失败:${t.message}`}return capToolResult(String(o))},lt=t=>{K?K.onToolStart?.(t.name,t.args):F(_.cyan(" ⏺ ")+i(t.name,t.args)+"\n")},it=(t,o)=>{T(o)&&et++,K?K.onToolResult?.(t.name,String(o)):F(_.gray(" "+P(o))+"\n"),j.push({role:"tool",tool_call_id:t.tc.id,content:String(o)})},ct=(t,o)=>{K?K.onSkip?.(t.name,t.args):F(_.gray(` ⊘ ${i(t.name,t.args)} — 已跳过\n`)),j.push({role:"tool",tool_call_id:t.tc.id,content:o||"未授权,未执行。"})},ut=t=>"edit_file"===t||"write_file"===t,mt=o=>{try{return t.readFileSync(o,"utf8")}catch{return""}},gt=(t,e)=>{const n=o.resolve(I,String(t.args.path)),r=u(e,mt(n),{path:String(t.args.path)});r&&(K?K.onFileDiff?.(r):F(r+"\n"))};let pt=0;for(;pt<J.length;){if(U?.aborted)return X(pt);const e=J[pt];if(e.argsError){lt(e),it(e,e.argsError),pt++;continue}if(e.readOnly){let t=pt;for(;t<J.length&&J[t].readOnly&&!J[t].argsError;)t++;const o=J.slice(pt,t),e=await x(o,8,async t=>{const o=await M.request(t.name,t.args,I,!0);return o.allow?(lt(t),{item:t,result:await at(t)}):{item:t,denied:o.reason||"未授权,未执行。"}});for(const t of e)null!=t.denied?ct(t.item,t.denied):it(t.item,t.result);if(pt=t,U?.aborted)return X(pt);continue}const n=await M.request(e.name,e.args,I,e.readOnly||e.deferToIde);if(U?.aborted)return X(pt);if(!n.allow){ct(e,n.reason),pt++;continue}if(st){const t=w("PreToolUse",{cwd:I,toolName:e.name,args:e.args},rt);for(const o of t.outputs)W("钩子·PreToolUse:"+P(o.stdout,120));if(t.block){ct(e,"PreToolUse 钩子阻断:"+t.reason),pt++;continue}t.modifiedArgs&&(e.args={...e.args,...t.modifiedArgs},W("钩子·PreToolUse 已修改工具参数"))}lt(e);const r=ut(e.name)&&e.args?.path?mt(o.resolve(I,String(e.args.path))):null;if(q&&null!=r&&e.args?.path){const n=t.existsSync(o.resolve(I,String(e.args.path)));q.push(r.length>2097152?{path:String(e.args.path),tooLarge:!0}:{path:String(e.args.path),before:r,existed:n})}let s=await at(e);if(st){const t=w("PostToolUse",{cwd:I,toolName:e.name,args:e.args,result:String(s)},rt);if(t.outputs.length){for(const o of t.outputs)W("钩子·PostToolUse:"+P(o.stdout,120));s=String(s)+"\n\n[PostToolUse 钩子输出]\n"+t.outputs.map(t=>t.stdout).join("\n")}}it(e,s),null!=r&&gt(e,r),pt++}et>=3&&!ot&&!nt&&(nt="你刚才连续多次工具调用失败。请停下来重新审视:是不是文件路径/内容/前提假设错了?先用 read_file / grep / list_files 核实现状,再换一种更可靠的方法,绝不要重复同样会失败的调用。",et=0,W("检测到连续工具失败,已提示智能体换一种更可靠的方法"));const ft=D?.();ft&&j.push({role:"user",content:ft})}return K?K.onInfo?.("已达最大工具轮数,停止"):F(_.yellow("\n[已达最大工具轮数,停止]\n")),{usage:{prompt:L,completion:Q},lastPromptTokens:z}}
1
+ import t from"node:fs";import o from"node:path";import{streamChat as e}from"./api.js";import{MarkupFilter as n,stripMarkup as r}from"./markup-filter.js";import{TOOL_SPECS as s,READONLY_TOOLS as a,runTool as l}from"./tools.js";import{toolLabel as i,indent as c,renderFileDiff as u}from"./render.js";import{compactIfNeeded as m,buildModelMessages as g}from"./compaction.js";import{makeTodoTool as p,formatTodos as f,TODO_TOOL_NAME as d}from"./todo.js";import{makeMemoryTool as h}from"./memory.js";import{loadHooks as y,runHooks as w}from"./hooks.js";import*as S from"./ide.js";import{c as _}from"./colors.js";function T(t){const o=String(t);return o.startsWith("工具执行失败")||o.startsWith("命令启动失败")||o.includes("不是合法 JSON")||o.includes("未找到要替换的内容")||o.includes("路径超出项目目录")||o.includes("是目录而非文件")}function b(t){return"\n\n# 当前目标(用户设定)\n"+String(t).trim()+"\n在本回合结束、停止作答前,检查该目标是否已达成:若未达成,应继续推进(补充调研/改动/验证),不要过早停止;达成后用一句话说明已达成。"}function k(t){return"string"==typeof t?t:Array.isArray(t)?t.filter(t=>"text"===t?.type).map(t=>t.text||"").join("\n"):""}async function x(t,o,e){const n=new Array(t.length);let r=0;return await Promise.all(Array.from({length:Math.max(1,Math.min(o,t.length))},async()=>{for(;r<t.length;){const o=r++;n[o]=await e(t[o],o)}})),n}export function isPlatformModel(t){return/^(agent|workflow|datahuman|squad)-/.test(String(t||""))}function P(t,o=200){const e=String(t).split("\n")[0];return e.length>o?e.slice(0,o)+"…":e}export function capToolResult(t){const o=String(t);if(o.length<=2e4)return o;const e=o.length-16e3-3e3;return o.slice(0,16e3)+`\n…【工具输出过长,中间已省略 ${e} 字符】…\n`+o.slice(-3e3)}export async function runTurn({config:$,messages:j,permissions:M,cwd:I,ask:E,signal:U,extraTools:v=[],depth:A=0,io:O,events:C,todoState:R,compaction:N,pullQueued:D,maxIters:J,fileJournal:q}){const F=O&&O.write||(t=>process.stdout.write(t)),K=C||null,W=t=>{K?K.onInfo?.(t):F(_.gray(" · "+t)+"\n")};let L=0,Q=0,z=0;const B=Number(J||$.maxTurns)||120,G=isPlatformModel($.model);let H=$.model;if(0===A&&$.autoModelStrong&&!G){const t=[...j].reverse().find(t=>"user"===t.role),o=k(t?.content);($.planMode||o.length>400||/重构|架构|设计|复杂|排查|调试|debug|优化|性能|并发|死锁|重写|迁移|安全审查|根因/i.test(o))&&$.autoModelStrong!==$.model&&(H=$.autoModelStrong,$.debug&&W(`[automodel] 复杂回合升配 → ${H}`))}const V=N||{},X=[...v];if(0===A&&!G&&!X.some(t=>t.spec?.function?.name===d)){const t=R||{list:[]};X.push(p({state:t,emit:t=>{K?K.onTodo?K.onTodo(t.map(t=>({...t}))):K.onInfo?.("任务清单:\n"+f(t)):F(_.gray(c(f(t)," "))+"\n")}}))}0!==A||G||!1===$.memoryEnabled||X.some(t=>"memory_write"===t.spec?.function?.name)||X.push(h({cwd:I,emit:t=>{K?K.onMemory?K.onMemory(t):K.onInfo?.("已记入长期记忆:"+P(t,80)):F(_.gray(" ✎ 已记入长期记忆:"+P(t,80))+"\n")}}));const Y=new Map;for(const t of X)Y.set(t.spec.function.name,t);const Z=G?void 0:[...s,...X.map(t=>t.spec)];let tt=0,ot=!1,et=0,nt=null;const rt=0!==A||G?null:y(I),st=!(!rt||!rt.all.length);if(st){const t=[];if(j.filter(t=>"user"===t.role).length<=1){const o=w("SessionStart",{cwd:I},rt);o.injectContext&&t.push(o.injectContext);for(const e of o.outputs)t.push(e.stdout)}const o=[...j].reverse().find(t=>"user"===t.role),e=w("UserPromptSubmit",{cwd:I,prompt:k(o?.content)},rt);if(e.block)return W("提交被 UserPromptSubmit 钩子阻断:"+e.reason),{error:"提交被钩子阻断:"+e.reason,usage:{prompt:L,completion:Q},lastPromptTokens:z};e.injectContext&&t.push(e.injectContext);for(const o of e.outputs)t.push(o.stdout);t.length&&(nt="【钩子提供的附加上下文】\n"+t.join("\n"))}for(let s=0;s<B;s++){const c=s===B-1;!c||G||ot||nt||(nt="已接近本回合的工具调用轮数上限。请不要再调用任何工具,基于目前已获得的信息,直接用中文输出面向用户的完整最终答复。");const p=await m(j,$,{state:V,lastPromptTokens:z});p>0&&W(`已压缩上下文(${p} 条较早消息已摘要,页面与会话记录不受影响)`);let f=!1,d=!1;const h=new n,y=t=>{t&&(f||!d||K||F("\n"),f=!0,K?K.onDelta?.(t):F(t))},k=g(j,V);let E=k;if(!G&&k.length&&"system"===k[0]?.role){let t="";$.planMode&&(t+="\n\n# 计划模式(当前开启)\n你现在处于计划模式:只能用只读工具(read_file/glob/grep/list_files/fetch_url)调研,不得做任何改动——write_file/edit_file/run_bash 等写或执行操作都会被拒绝,不要反复尝试。请先充分理解需求与现状,然后产出一份清晰、分步的实施方案:涉及哪些文件、每步改动要点、如何验证、风险点,用 todo_write 列出步骤,随后停下等待用户确认。用户关闭计划模式后你再执行。"),$.sandbox&&(t+="\n\n# 沙箱模式(当前开启)\n禁止执行 shell 命令(run_bash)、联网(fetch_url)与环境/应用自动化工具(install_*/setup_environment/open_*),调用会被拒绝,不要反复尝试。只做本地文件读写与分析。"),$.goal&&String($.goal).trim()&&(t+=b($.goal)),t&&(E=[{...k[0],content:String(k[0].content||"")+t},...k.slice(1)])}const v=Date.now();let A;try{A=await e({baseUrl:$.baseUrl,apiKey:$.apiKey,model:H,messages:nt?[...E,{role:"user",content:nt}]:E,tools:ot||c?void 0:Z,reasoningEffort:$.reasoningEffort,client:$.client,sessionId:$.sessionId},{onContent:t=>y(h.feed(t)),onReasoning:t=>{!1!==$.showReasoning&&(K?K.onReasoning?.(t):(d||F(_.gray("\n[思考] ")),F(_.gray(t))),d=!0)},onRetry:(t,o,e)=>W(`模型响应异常,${Math.round(o/1e3)}s 后自动重试(第 ${t} 次):${e}`)},U)}catch(t){return"AbortError"===t.name?(K?K.onInfo?.("已中断"):F(_.gray("\n[已中断]\n")),{aborted:!0,usage:{prompt:L,completion:Q},lastPromptTokens:z}):(K?K.onInfo?.("✗ 模型调用失败:"+t.message):F("\n"+_.red("✗ 模型调用失败:")+t.message+"\n"),{error:t.message,usage:{prompt:L,completion:Q},lastPromptTokens:z})}y(h.flush()),A.usage&&(L+=A.usage.prompt_tokens||0,Q+=A.usage.completion_tokens||0,z=A.usage.prompt_tokens||z),$.debug&&W(`[debug] 轮${s+1} · ${$.model} · in ${A.usage?.prompt_tokens||0} / out ${A.usage?.completion_tokens||0} tok · ${Date.now()-v}ms · ${(A.toolCalls||[]).length} 工具调用`);const{text:O,sawMarkup:C}=r(A.content),R=C||h.sawMarkup;nt=null;const N=G?[]:A.toolCalls;if(!N.length){if(R&&!O.trim()&&!G){if(tt<2){tt++,2===tt&&(ot=!0),nt=1===tt?"你刚才的输出把工具调用写成了文本标记,工具并未被执行,该内容也不会展示给用户。请重新处理:若需调用工具,必须使用规范的函数调用(tool_calls)通道发起,绝不要以任何文本形式书写工具调用标记;若无需工具,请直接用中文输出面向用户的最终答复。":"请停止调用任何工具。基于目前已获得的全部信息,直接用中文输出面向用户的完整最终答复,不要输出任何工具调用或协议标记。",W(`已拦截模型的文本化工具调用标记(工具未被执行),纠正重试中(第 ${tt} 次)`);continue}W("模型持续输出无效的工具调用标记,已全部拦截;本轮无有效答复(可换个说法重试或更换模型)")}f&&!K&&F("\n"),O&&j.push({role:"assistant",content:O}),K&&K.onTurnEnd?.(O);const t=D?.();if(t){j.push({role:"user",content:t});continue}if(st){const t=w("Stop",{cwd:I,result:O},rt);for(const o of t.outputs)W("钩子·Stop:"+P(o.stdout,160))}return{usage:{prompt:L,completion:Q},lastPromptTokens:z}}f&&!K&&F("\n"),K&&O&&K.onAssistantText?.(O),j.push({role:"assistant",content:O,tool_calls:N});const J=N.map((t,o)=>{const e=t.function?.name||"";let n={},r=null;const s=t.function?.arguments||"";if(s)try{n=JSON.parse(s)}catch(t){r=`工具参数不是合法 JSON(${t.message}):${String(s).slice(0,200)}…请修正参数后重新调用。`}const l=Y.get(e),i=S.isActive()&&("write_file"===e||"edit_file"===e);return{tc:t,idx:o,name:e,args:n,argsError:r,extra:l,readOnly:l?!!l.readOnly:a.has(e),deferToIde:i}}),X=t=>{for(let o=t;o<J.length;o++)j.push({role:"tool",tool_call_id:J[o].tc.id||`call_${o}`,content:"(用户中断,该工具未执行)"});return K?K.onInfo?.("已中断"):F(_.gray("\n[已中断]\n")),{aborted:!0,usage:{prompt:L,completion:Q},lastPromptTokens:z}},at=async t=>{if(t.argsError)return t.argsError;let o;try{o=t.extra?await t.extra.run(t.args):await l(t.name,t.args,I,$)}catch(t){o=`工具执行失败:${t.message}`}return capToolResult(String(o))},lt=t=>{K?K.onToolStart?.(t.name,t.args):F(_.cyan(" ⏺ ")+i(t.name,t.args)+"\n")},it=(t,o)=>{T(o)&&et++,K?K.onToolResult?.(t.name,String(o)):F(_.gray(" "+P(o))+"\n"),j.push({role:"tool",tool_call_id:t.tc.id,content:String(o)})},ct=(t,o)=>{K?K.onSkip?.(t.name,t.args):F(_.gray(` ⊘ ${i(t.name,t.args)} — 已跳过\n`)),j.push({role:"tool",tool_call_id:t.tc.id,content:o||"未授权,未执行。"})},ut=t=>"edit_file"===t||"write_file"===t,mt=o=>{try{return t.readFileSync(o,"utf8")}catch{return""}},gt=(t,e)=>{const n=o.resolve(I,String(t.args.path)),r=u(e,mt(n),{path:String(t.args.path)});r&&(K?K.onFileDiff?.(r):F(r+"\n"))};let pt=0;for(;pt<J.length;){if(U?.aborted)return X(pt);const e=J[pt];if(e.argsError){lt(e),it(e,e.argsError),pt++;continue}if(e.readOnly){let t=pt;for(;t<J.length&&J[t].readOnly&&!J[t].argsError;)t++;const o=J.slice(pt,t),e=await x(o,8,async t=>{const o=await M.request(t.name,t.args,I,!0);return o.allow?(lt(t),{item:t,result:await at(t)}):{item:t,denied:o.reason||"未授权,未执行。"}});for(const t of e)null!=t.denied?ct(t.item,t.denied):it(t.item,t.result);if(pt=t,U?.aborted)return X(pt);continue}const n=await M.request(e.name,e.args,I,e.readOnly||e.deferToIde);if(U?.aborted)return X(pt);if(!n.allow){ct(e,n.reason),pt++;continue}if(st){const t=w("PreToolUse",{cwd:I,toolName:e.name,args:e.args},rt);for(const o of t.outputs)W("钩子·PreToolUse:"+P(o.stdout,120));if(t.block){ct(e,"PreToolUse 钩子阻断:"+t.reason),pt++;continue}t.modifiedArgs&&(e.args={...e.args,...t.modifiedArgs},W("钩子·PreToolUse 已修改工具参数"))}lt(e);const r=ut(e.name)&&e.args?.path?mt(o.resolve(I,String(e.args.path))):null;if(q&&null!=r&&e.args?.path){const n=t.existsSync(o.resolve(I,String(e.args.path)));q.push(r.length>2097152?{path:String(e.args.path),tooLarge:!0}:{path:String(e.args.path),before:r,existed:n})}let s=await at(e);if(st){const t=w("PostToolUse",{cwd:I,toolName:e.name,args:e.args,result:String(s)},rt);if(t.outputs.length){for(const o of t.outputs)W("钩子·PostToolUse:"+P(o.stdout,120));s=String(s)+"\n\n[PostToolUse 钩子输出]\n"+t.outputs.map(t=>t.stdout).join("\n")}}it(e,s),null!=r&&gt(e,r),pt++}et>=3&&!ot&&!nt&&(nt="你刚才连续多次工具调用失败。请停下来重新审视:是不是文件路径/内容/前提假设错了?先用 read_file / grep / list_files 核实现状,再换一种更可靠的方法,绝不要重复同样会失败的调用。",et=0,W("检测到连续工具失败,已提示智能体换一种更可靠的方法"));const ft=D?.();ft&&j.push({role:"user",content:ft})}return K?K.onInfo?.("已达最大工具轮数,停止"):F(_.yellow("\n[已达最大工具轮数,停止]\n")),{usage:{prompt:L,completion:Q},lastPromptTokens:z}}
package/src/commands.js CHANGED
@@ -1 +1 @@
1
- import n from"node:fs";import t from"node:path";import e from"node:os";import{execSync as o}from"node:child_process";import{saveConfig as i,DEFAULT_MODEL as s,CONFIG_PATH as r}from"./config.js";import{listModels as a,fetchUsage as c,streamChat as m}from"./api.js";import{listSessions as d,relTime as l,turnCount as p,saveSession as f,createSession as g}from"./session.js";import{compactNow as u,buildModelMessages as h}from"./compaction.js";import{computeInsights as w,formatInsights as y}from"./insights.js";import{setColorMode as k,setAccent as v,getAccent as $,ACCENTS as b}from"./colors.js";import{detectVerifyCommand as S}from"./autofix.js";import{loadAllowRules as j,describeRules as _,parseRule as T,addProjectRule as x,removeProjectRule as M,GLOBAL_SETTINGS_PATH as C,projectSettingsPath as I}from"./settings.js";import{contextReport as L}from"./context-report.js";import{renderDiff as N}from"./diffview.js";import{loadHooks as P,formatHooks as A}from"./hooks.js";import{copyToClipboard as D}from"./clipboard.js";import{toMarkdown as U,recapConversation as E}from"./transcript.js";import{runDoctor as R}from"./doctor.js";import{TOOL_SPECS as F}from"./tools.js";import{makeTodoTool as O}from"./todo.js";import{makeMemoryTool as K}from"./memory.js";const B=/^(agent|workflow|datahuman)-/,H="\n\n# 附加可访问目录(可用 read_file/list_files/grep 以绝对路径访问;写入仍限主目录)\n";function J(n){const t=n.messages&&n.messages[0];if(!t||"system"!==t.role||"string"!=typeof t.content)return;const e=t.content.indexOf(H);let o=e>=0?t.content.slice(0,e):t.content;const i=n.extraDirs||[];i.length&&(o+=H+i.map(n=>"- "+n).join("\n")+"\n"),t.content=o}export const INIT_PROMPT="请为当前项目生成或完善 HUICAI.md(给终端智能体看的项目说明):先用 list_files/glob/read_file 快速调研项目(技术栈、目录结构、构建/测试/运行命令、代码约定),再用 write_file 创建或更新 HUICAI.md(若已存在则保留人工撰写的内容,增量完善)。内容包含:项目简介、关键目录、常用命令(构建/测试/运行)、代码风格与注意事项。控制在 100 行以内,用中文。";const Y=[{name:"help",desc:"显示可用命令(含自定义命令)",run(n){const t=Y.map(n=>[`/${n.name}${n.args?" "+n.args:""}`,n.desc]);for(const e of loadCustomCommands(n.cwd))t.push([`/${e.name}`,`${e.desc}(自定义)`]);t.push(["! <命令>","直接执行 shell 命令并把输出加入上下文"]),t.push(["@<文件路径>","在消息中引用文件内容(自动展开给模型)"]);const e=Math.min(24,Math.max(...t.map(([n])=>n.length))+2);n.print(t.map(([n,t])=>" "+n.padEnd(e)+t).join("\n"))}},{name:"model",args:"[名称|auto]",desc:"查看或切换模型(auto=按账号自动选可用模型,与 VSCode 一致)",async run(n,t){if(t){if("auto"===t){try{const t=(await a(n.config.baseUrl,n.config.apiKey)).map(n=>n.id).filter(n=>!/^(agent|workflow|datahuman)-/.test(n));n.config.model=(t.includes(s)?s:t[0])||n.config.model}catch{}return i({model:"auto"}),void n.print("Auto 已选:"+n.config.model)}try{const e=(await a(n.config.baseUrl,n.config.apiKey)).map(n=>n.id);if(!e.includes(t))return void n.print(`当前账号无权使用模型「${t}」。\n 可用模型:${e.slice(0,12).join(", ")}`+(e.length>12?` …(共 ${e.length} 个,/models 查看全部)`:"")+"\n 或用 /model auto 自动选择可用模型。")}catch(t){n.print(`(未能校验模型可用性:${t.message})仍按输入切换;若无权限,平台会在调用时拒绝。`)}n.config.model=t,i({model:t}),n.print("已切换模型:"+t)}else n.print("当前模型:"+(n.config.model||"(未设置)"))}},{name:"models",desc:"列出平台可用模型",async run(n){try{const t=await a(n.config.baseUrl,n.config.apiKey);n.print(t.map(n=>" "+n.id).join("\n")||"(无)")}catch(t){n.print("拉取失败:"+t.message)}}},{name:"usage",desc:"查看 Token 用量窗口(5小时/7天,按会员等级)",async run(n){const t=n=>"number"==typeof n&&Number.isFinite(n)?n:0,e=(n,e)=>(n=t(n),(e=t(e))<0?`${n.toLocaleString()} / 无限`:0===e?"该档未开放":`${n.toLocaleString()} / ${e.toLocaleString()} (${Math.min(100,Math.round(n/e*100))}%)`);try{const t=await c(n.config.baseUrl,n.config.apiKey);if(!t||"number"!=typeof t.used5h&&"number"!=typeof t.used7d)return void n.print("平台未返回用量数据(请确认平台已支持 /v1/usage)。");n.print(`Token 用量窗口\n 近 5 小时:${e(t.used5h,t.limit5h)}\n 近 7 天 :${e(t.used7d,t.limit7d)}`+(!t.allowed&&t.message?"\n "+t.message:""))}catch(t){n.print("拉取用量失败:"+t.message)}}},{name:"login",desc:"重新登录(浏览器授权;无浏览器请退出后 huicai login --paste)",async run(n){try{const{deviceLogin:t}=await import("./device.js");n.print("正在打开浏览器授权…");const e=await t(n.config.baseUrl);n.config.apiKey=e,i({apiKey:e}),n.print("登录成功。")}catch(t){n.print("登录未完成:"+t.message+"(可退出后用 huicai login --paste 粘贴令牌)")}}},{name:"logout",desc:"退出登录(清除本机保存的令牌)",run(n){n.config.apiKey="",i({apiKey:""}),n.print("已退出登录。后续对话需重新 /login 或 huicai login。")}},{name:"clear",desc:"开启新会话(旧会话仍可 --resume 恢复)",run:()=>({action:"clear"})},{name:"compact",desc:"立即压缩上下文(摘要独立存储,不影响页面与会话原文)",async run(n){n.session.compaction||(n.session.compaction={});const t=await u(n.session.messages,n.config,{state:n.session.compaction});n.print(t>0?`已压缩:${t} 条较早消息已摘要(原对话完整保留,仅发给模型的上下文变小)`:"暂无可压缩的中段历史(会话较短或摘要失败)")}},{name:"memory",args:"[clear]",desc:"查看本项目长期记忆(clear 清空)",async run(n,t){const{loadMemory:e,clearMemory:o,memoryPath:i}=await import("./memory.js");if("clear"===t)return void n.print(o(n.cwd)?"已清空本项目长期记忆。":"本项目暂无长期记忆。");const s=e(n.cwd);n.print(s?s+`\n(文件:${i(n.cwd)} · /memory clear 清空)`:"本项目暂无长期记忆(模型会经 memory_write 自动记录值得跨会话记住的信息;配置 memory:false 可关闭)。")}},{name:"cost",desc:"查看本次运行累计 token 用量(及预算进度)",run(n){const t=n.usage.prompt+n.usage.completion,e=Number(n.config.budgetTokens)||0;n.print(`token 用量:输入 ${n.usage.prompt} / 输出 ${n.usage.completion}`+(n.session.lastPromptTokens?` · 当前上下文 ~${n.session.lastPromptTokens} tok`:"")+(e>0?`\n预算:${t.toLocaleString()} / ${e.toLocaleString()} tokens(${Math.round(t/e*100)}%)`+(t>=e?" ⚠ 已超预算":t>=.8*e?" ⚠ 接近预算":""):""))}},{name:"status",desc:"显示运行状态(版本/模型/目录/会话/MCP/权限规则)",run(n){const t=n.session,e=[...new Set((n.extraTools||[]).map(n=>n.spec?.function?.name||"").filter(n=>n.startsWith("mcp__")).map(n=>n.split("__")[1]))];let i="";try{i=o("git rev-parse --abbrev-ref HEAD",{cwd:n.cwd,stdio:["ignore","pipe","ignore"]}).toString().trim()}catch{}const s=j(n.cwd),r=[`版本 huicai ${n.version||""}`,`模型 ${n.config.model}`,`平台 ${n.config.baseUrl}`,`目录 ${n.cwd}${i?`(git:${i})`:""}`,`会话 ${t.id} · ${p(t.messages)} 轮${t.lastPromptTokens?` · 上下文 ~${t.lastPromptTokens} tok`:""}`,`token 输入 ${n.usage.prompt} / 输出 ${n.usage.completion}`,"审批 "+(n.permissions?.isYolo?.()?"⚠ yolo 自动放行":"写/执行需授权")+` · 允许规则 ${s.length} 条${n.config.planMode?" · 📋 计划模式(只读)":""}`+`${n.config.sandbox?" · 🧪 沙箱":""}${n.config.goal?" · 🎯 目标已设":""}`,`MCP ${e.length?e.join(", "):"(未配置)"}`];n.print(r.join("\n"))}},{name:"sessions",desc:"列出当前目录的历史会话",run(n){const t=d(n.cwd);if(!t.length)return void n.print("该目录暂无历史会话。");const e=t.slice(0,15).map(t=>{const e=t.id===n.session.id?" ← 当前":"";return` ${l(t.updatedAt).padStart(8)} ${t.title} ${t.turns}轮 · ${t.id}${e}`});e.push("(退出后 huicai --resume 可选择恢复)"),n.print(e.join("\n"))}},{name:"yolo",desc:"切换「自动放行写/执行」模式",run(n){const t=!n.permissions.isYolo();n.permissions.setYolo(t),n.print("自动放行(yolo):"+(t?"开(谨慎)":"关"))}},{name:"cwd",desc:"显示当前工作目录",run(n){n.print(n.cwd)}},{name:"markdown",desc:"切换助手回复的 Markdown 渲染(TUI 富文本;关闭则显示原始文本,便于复制)",run(n){const t=!1===n.config.markdown;n.config.markdown=t,i({markdown:t}),n.print("Markdown 渲染:"+(t?"开(下一条回复起生效)":"关(显示原始 Markdown 文本)"))}},{name:"init",desc:"让智能体调研项目并生成/完善 HUICAI.md",run:()=>({prompt:INIT_PROMPT})},{name:"skills",desc:"列出本地技能(Agent Skills;pull/push 用终端命令 huicai skills)",async run(n){const{loadLocalSkills:t}=await import("./skills.js"),e=t(n.cwd);e.length?n.print(e.map(n=>` ${n.name}${n.version?" v"+n.version:""}(${"project"===n.scope?"项目":"全局"}${n.files.length?` · ${n.files.length} 附件`:""})${n.description?" "+n.description.slice(0,60):""}`).join("\n")+"\n(模型会按描述经 load_skill 按需载入;本会话启动时已注入目录)"):n.print("暂无本地技能(.huicai/skills 或 ~/.huicai/skills)。可用 huicai skills pull 从平台拉取。")}},{name:"mcp",desc:"列出已连接的 MCP 工具",run(n){const t=(n.extraTools||[]).filter(n=>(n.spec?.function?.name||"").startsWith("mcp__"));t.length?n.print(t.map(n=>` ${n.spec.function.name}${n.readOnly?"(只读)":""}`).join("\n")):n.print("未连接 MCP 服务。配置 ~/.huicai/mcp.json(通用 mcpServers 格式)后重启生效。")}},{name:"context",desc:"可视化当前上下文占用(各类别 token 估算)",run(n){const t=B.test(String(n.config.model||""));let e=[];if(!t)try{const t=O({state:{list:[]},emit(){}}).spec,o=!1!==n.config.memoryEnabled?K({cwd:n.cwd,emit(){}}).spec:null;e=[...F,t,...o?[o]:[],...(n.extraTools||[]).map(n=>n.spec).filter(Boolean)]}catch{e=F}n.print(L({messages:n.session.messages,compaction:n.session.compaction||{},specs:e,lastPromptTokens:n.session.lastPromptTokens||0,autoCompactTokens:Number(n.config.autoCompactTokens)||0,platform:t}))}},{name:"permissions",args:"[allow|deny <规则> | remove <规则>]",desc:"查看/增删工具权限规则(allow/deny)",run(n,t){const e=String(t||"").trim().split(/\s+/).filter(Boolean),o=(e.shift()||"").toLowerCase(),i=e.join(" ").trim();if(!o){const t=_(n.cwd),e=n=>n.length?n.map(n=>" "+n).join("\n"):" (无)";return void n.print("权限规则(deny 优先于 allow/yolo,对只读工具也生效)\n 全局 ~/.huicai/settings.json\n 允许:\n"+e(t.global.allow)+"\n 拒绝:\n"+e(t.global.deny)+"\n 项目 .huicai/settings.json\n 允许:\n"+e(t.project.allow)+"\n 拒绝:\n"+e(t.project.deny)+"\n 用法:/permissions allow <规则> · /permissions deny <规则> · /permissions remove <规则>\n 规则语法:`工具名` 或 `工具名(模式*)`,如 run_bash(git *)、edit_file、read_file(**/.env*)")}if("allow"!==o&&"deny"!==o){if("remove"===o||"rm"===o){if(!i)return void n.print("用法:/permissions remove <规则原文>(仅移除项目设置里的规则)");const t=M(n.cwd,i);return n.permissions?.reloadRules?.(),void n.print(t.removed?`已移除规则:${i}`:`未在项目设置中找到该规则:${i}`)}n.print("未知子命令。用法:/permissions [allow|deny|remove] <规则>")}else{if(!i||!T(i))return void n.print("规则无效。示例:run_bash(git *)、edit_file、fetch_url(https://a.com*)");try{const t=x(n.cwd,o,i);n.permissions?.reloadRules?.(),n.print(`已添加${"deny"===o?"拒绝":"允许"}规则:${i} → ${t}`)}catch(t){n.print("写入失败:"+t.message)}}}},{name:"diff",desc:"查看未提交改动(git)与本会话智能体改过的文件",run(n){n.print(N(n.cwd,n.session.messages))}},{name:"pr",args:"[commit|address]",desc:"自动提交并开 PR(生成规范 commit→建分支→push→gh 开 PR);address=按 review 评论自动改",async run(n,t){const e=String(t||"").trim().toLowerCase();if("address"===e||"comments"===e)return{prompt:"请处理当前分支/PR 的 review 评论:用 `gh pr view --comments`(无 PR 则先 `gh pr status`)读取评论,逐条理解并修改代码解决,然后 git add/commit(规范信息)+ git push;完成后逐条说明如何处理的。若未装 gh 或无 PR,先告知。"};const{runPrFlow:o}=await import("./gitflow.js");try{const t=await o({cwd:n.cwd,config:n.config,emit:t=>n.print(t),push:"commit"!==e,openPr:"commit"!==e});if(!t.ok)return void n.print("✗ "+t.error);n.print(t.pr?"✅ 已开 PR:"+t.pr:"✅ 完成(分支 "+t.branch+")"+(t.note?":"+t.note:""))}catch(t){n.print("PR 流程出错:"+t.message)}}},{name:"plan",args:"[on|off|approve]",desc:"计划模式:只读调研出方案→/plan approve 批准并分步执行",run(n,t){const e=String(t||"").trim().toLowerCase();if("approve"===e||"go"===e)return n.config.planMode=!1,n.permissions?.setPlanMode?.(!1),n.print("计划已批准,开始分步执行。"),{prompt:"计划已获批准。现在请按你刚才制定的计划(todo 列表)逐步执行:每完成一步用 todo_write 更新状态并简要自检;遇到与计划不符及时调整并说明;全部完成后总结改了什么、以及如何验证(可用 /verify 让我自动跑验证)。"};const o="on"===e||"off"!==e&&!n.config.planMode;n.config.planMode=o,n.permissions?.setPlanMode?.(o),n.print(o?"计划模式:开。只做只读调研并用 todo_write 产出分步方案;写/执行会被拦截。\n 方案满意后用 /plan approve 批准并自动分步执行;或 /plan off 手动执行。":"计划模式:关。恢复正常执行(写/执行仍按权限审批)。")}},{name:"auto",args:"<目标>",desc:"自动规划并自动执行:只读调研出分步计划,确认「是否继续?」后自动逐步执行",run(n,t){const e=String(t||"").trim();if(e)return{action:"autorun",goal:e};n.print("用法:/auto <目标>\n 先自动规划(只读调研 + todo 分步计划),随后询问「是否继续?1、是 2、是,并允许在当前对话中后续相同操作不再询问 3、否」,确认后自动逐步执行并自检。")}},{name:"rewind",args:"[N [chat|code|both]]",desc:"检查点回滚:无参列出;/rewind N 回退对话,加 code/both 一并回滚文件",run(n,t){const e=n.checkpoints;if(!e||!e.list.length)return void n.print("暂无检查点(每个用户回合前自动记录,发生对话后可用)。");const o=String(t||"").trim().split(/\s+/).filter(Boolean);if(!o.length){const t=["检查点(回滚到某个回合之前)"];for(const n of e.list)t.push(` #${n.id} ${l(n.time)} ${n.label}`+(n.journal.length?` · 改动 ${n.journal.length} 处`:""));return t.push(" 用法:/rewind N(仅回退对话,安全)· /rewind N code(回滚文件)· /rewind N both"),t.push(" 提示:code/both 会覆盖当前文件,建议先 /diff 查看。"),void n.print(t.join("\n"))}const i=Number(o[0]);if(!Number.isFinite(i))return void n.print("用法:/rewind N [chat|code|both]");const s=["chat","code","both"].includes(o[1])?o[1]:"chat",r=e.restore(i,s,{session:n.session,cwd:n.cwd});if(!r.ok)return void n.print(r.error);const a=[];r.conversation&&a.push("对话已回退到该回合之前"),r.filesRestored.length&&a.push(`恢复文件 ${r.filesRestored.length} 个`),r.filesDeleted.length&&a.push(`删除新建文件 ${r.filesDeleted.length} 个`),r.filesSkipped.length&&a.push(`跳过(过大) ${r.filesSkipped.length} 个`),r.filesFailed.length&&a.push(`失败 ${r.filesFailed.length} 个`);try{f(n.session)}catch{}return n.print("已回滚到 #"+i+":"+(a.join(";")||"无变化")+(r.filesFailed.length?"\n 失败:"+r.filesFailed.join(";"):"")),r.conversation?{action:"refresh"}:void 0}},{name:"hooks",desc:"查看已配置的工具事件钩子(Pre/PostToolUse/UserPromptSubmit/Stop)",run(n){n.print(A(P(n.cwd)))}},{name:"copy",args:"[N]",desc:"复制助手回复到剪贴板(默认最后一条;N=倒数第 N 条)",async run(n,t){const e=n.session.messages.filter(n=>"assistant"===n.role&&"string"==typeof n.content&&n.content.trim());if(!e.length)return void n.print("还没有可复制的助手回复。");const o=Math.max(1,parseInt(t,10)||1),i=e[e.length-o];if(!i)return void n.print(`只有 ${e.length} 条助手回复。`);const s=await D(i.content);n.print(s.ok?`已复制${o>1?`倒数第 ${o} 条`:"最后一条"}回复到剪贴板(${i.content.length} 字)`:`复制失败(未找到 ${s.tool})。以下为该回复内容:\n\n${i.content}`)}},{name:"export",args:"[文件路径]",desc:"导出当前对话为 Markdown 文件",run(e,o){const i=U(e.session),s=String(o||"").trim()||`huicai-对话-${(new Date).toISOString().slice(0,19).replace(/[:T]/g,"-")}.md`,r=t.isAbsolute(s)?s:t.join(e.cwd,s);try{n.writeFileSync(r,i,"utf8"),e.print("已导出对话到 "+r)}catch(n){e.print("导出失败:"+n.message)}}},{name:"rename",args:"<新标题>",desc:"重命名当前会话",run(n,t){const e=String(t||"").trim();if(e){n.session.title=e.slice(0,80);try{f(n.session)}catch{}n.print("会话已重命名为:"+n.session.title)}else n.print("当前标题:"+(n.session.title||"(未命名)")+"\n 用法:/rename <新标题>")}},{name:"recap",desc:"用一句话总结当前对话进展(直连原始模型,不写入对话)",async run(n){if(n.session.messages.some(n=>"system"!==n.role))try{n.print("小结:"+await E(n.session.messages,n.config))}catch(t){n.print("生成小结失败:"+t.message)}else n.print("对话还没有内容。")}},{name:"distill",desc:"从当前对话提炼项目知识,写入长期记忆(供后续会话自动加载)",async run(n){if(!1===n.config.memoryEnabled)return void n.print("长期记忆已关闭(config.memory=false)。");if(!n.session.messages.some(n=>"system"!==n.role))return void n.print("对话还没有内容。");const{distillMemory:t}=await import("./transcript.js"),{loadMemory:e,appendMemory:o}=await import("./memory.js");try{const i=await t(n.session.messages,n.config,e(n.cwd));if(!i.length)return void n.print("未提炼出值得长期记住的新知识。");let s=0;for(const t of i)o(n.cwd,t)&&s++;n.print(`已写入 ${s} 条项目知识到长期记忆:\n`+i.map(n=>" · "+n).join("\n"))}catch(t){n.print("提炼失败:"+t.message)}}},{name:"doctor",desc:"本机环境体检(登录/网络/模型/git/工作目录/钩子)",async run(n){try{n.print(await R(n))}catch(t){n.print("体检失败:"+t.message)}}},{name:"config",desc:"查看配置文件路径与当前生效配置(密钥脱敏)",run(n){const t=n.config,e=Number(t.autoCompactTokens);var o;n.print(["配置"," 文件 "+r," 平台 "+t.baseUrl," 密钥 "+(o=t.apiKey,o?o.slice(0,4)+"…"+o.slice(-4)+`(${o.length}位)`:"(未设置)")," 模型 "+t.model," 推理强度 "+(t.reasoningEffort||"off")," 自动压缩 "+(e<0?"关闭":e>0?e.toLocaleString()+" tokens":"默认(80k)")," 记忆 "+(!1===t.memoryEnabled?"关":"开")," Markdown "+(!1===t.markdown?"关":"开")," 全局设置 "+C," 项目设置 "+I(n.cwd)," (多数项有对应斜杠命令直接改:/model /effort /autocompact /permissions …)"].join("\n"))}},{name:"effort",args:"[minimal|low|medium|high|off]",desc:"设置推理强度(reasoning_effort 透传;模型不支持则忽略)",run(n,t){const e=String(t||"").trim().toLowerCase();e?["minimal","low","medium","high","off"].includes(e)?(n.config.reasoningEffort="off"===e?"":e,i({reasoningEffort:n.config.reasoningEffort}),n.print("推理强度:"+(n.config.reasoningEffort||"off")+"(下一条消息起生效;模型不支持则被忽略)")):n.print("取值:minimal | low | medium | high | off"):n.print("当前推理强度:"+(n.config.reasoningEffort||"off")+"\n 用法:/effort minimal|low|medium|high|off")}},{name:"autocompact",args:"[N|off|status]",desc:"自动压缩上下文的触发阈值(token 数;off 关闭)",run(n,t){const e=String(t||"").trim().toLowerCase(),o=Number(n.config.autoCompactTokens)||0;if(!e||"status"===e)return void n.print("自动压缩:"+(o<0?"已关闭":o>0?`阈值 ${o.toLocaleString()} tokens`:"默认阈值 80,000 tokens")+"\n /autocompact <N> 设定 · /autocompact off 关闭 · /autocompact status 查看");if("off"===e)return n.config.autoCompactTokens=-1,i({autoCompactTokens:-1}),void n.print("已关闭自动压缩(仍可 /compact 手动压缩)。");const s=parseInt(e,10);!Number.isFinite(s)||s<1e3?n.print("请给 ≥1000 的 token 阈值,或 off / status。"):(n.config.autoCompactTokens=s,i({autoCompactTokens:s}),n.print("自动压缩阈值设为 "+s.toLocaleString()+" tokens(字符阈值同步联动)。"))}},{name:"review",desc:"审查当前未提交改动(找 bug 与可简化处)",run:()=>({prompt:"请审查当前工作区的未提交改动:先用 run_bash 跑 `git diff`(及 `git status`)查看改了什么,聚焦正确性 bug、边界与错误处理、可复用/可简化处、安全隐患。若本地有 code-review 技能可 load_skill 参照其方法。逐条给出:问题、定位(文件:行)、修改建议,按严重度排序;不确定的地方说明。"})},{name:"security-review",desc:"对当前改动做安全审查",run:()=>({prompt:"请对当前工作区改动做安全审查:注入(SQL/命令/路径穿越)、越权、密钥/凭据泄漏、SSRF、不安全依赖、反序列化、XSS 等。先 run_bash 跑 `git diff` 查看改动。若本地有 security-audit 技能可 load_skill 参照。给出风险点、可利用场景、影响与修复建议,按严重度排序。"})},{name:"scan",args:"[fix]",desc:"安全/质量扫描(自动选可用的 npm audit/semgrep/trivy/gitleaks 等);fix=让智能体逐条修",async run(n,t){const{detectScanners:e,runScanners:o}=await import("./scan.js"),i=e(n.cwd);if(!i.length)return void n.print("未检测到可用扫描器。可安装 semgrep / trivy / gitleaks,或项目含 package.json(npm audit)等。");if("fix"===String(t||"").trim().toLowerCase())return{prompt:"请对本项目做安全/质量扫描并修复:依次运行并理解这些扫描器的输出——"+i.map(n=>"`"+n.command+"`("+n.why+")").join("、")+";对发现的问题逐个修复(升级/更换依赖、修代码、移除硬编码密钥等),修完重跑对应扫描确认清零;最后汇总修了什么。"};n.print("运行扫描器:"+i.map(n=>n.name).join(", ")+"(可能较慢)…");const s=await o(n.cwd,i);for(const t of s)n.print(`\n── ${t.name}(${0===t.code?"✅ 无高危":"⚠ 有发现"})──`),n.print(t.output.split("\n").slice(0,20).join("\n"));n.print("\n(/scan fix 让智能体逐条修复)")}},{name:"cover",desc:"覆盖率驱动测试生成:跑覆盖率→定位未覆盖的改动→补测试→复测",run:()=>({prompt:"请做覆盖率驱动的测试补全:\n1) 跑测试覆盖率(按项目选:npm test -- --coverage / jest --coverage / pytest --cov / go test -cover / mvn test 等);\n2) 重点看**最近改动**且覆盖率低或未覆盖的函数与分支;\n3) 为这些薄弱处补写有意义的测试(覆盖正常/边界/错误路径),遵循项目既有测试风格与框架;\n4) 重跑测试与覆盖率,确认通过且覆盖率提升;最后汇报补了哪些测试、覆盖率变化。"})},{name:"verify",args:"[验证命令]",desc:"修到通过:跑测试/构建,失败自动定位修复并重跑,直到通过或达上限",run(n,t){let e=String(t||"").trim();if(!e){if(e=S(n.cwd)||"",!e)return void n.print("未能自动探测验证命令。用法:/verify <命令>(如 /verify npm test)");n.print("自动探测到验证命令:"+e)}return{action:"autofix",command:e,maxIters:Math.min(10,Math.max(1,Number(n.config.verifyMaxIters)||5))}}},{name:"simplify",desc:"审查改动的可复用/简化/抽象层级并直接清理(只清理不找 bug)",run:()=>({prompt:"请只做质量清理(不找 bug):审查最近改动的复用(是否重复造轮子、可否复用现有工具/函数)、简化(是否有更直接的写法)、效率、以及抽象层级是否合适;把确定的改进直接用 edit_file 应用,保持与项目既有风格一致,改后跑测试/构建自检。若本地有 refactor-safely 技能可 load_skill 参照。最后说明改了什么、为什么。"})},{name:"insights",desc:"本机会话使用洞察(数量/轮数/活跃度/按项目·模型分布)",run(n){n.print(y(w(d(null),{cwd:n.cwd})))}},{name:"goal",args:"[目标 | clear]",desc:"设定目标(每轮注入,模型停止前自检是否达成;clear 清除)",run(n,t){const e=String(t||"").trim();if(e){if("clear"===e)return n.config.goal="",void n.print("已清除目标。");n.config.goal=e,n.print("已设定目标:"+e+"(每轮注入,模型将在停止前自检是否达成)")}else n.print(n.config.goal?"当前目标:"+n.config.goal+"\n /goal <新目标> 修改 · /goal clear 清除":"未设定目标。用法:/goal <目标>(模型会在停止前自检是否达成)")}},{name:"btw",args:"<问题>",desc:"问一个旁支问题(带当前上下文,但不写入对话、不调用工具)",async run(n,t){const e=String(t||"").trim();if(!e)return void n.print("用法:/btw <问题>(快速旁支问答,不影响主对话)");const o=B.test(String(n.config.model||""))?n.config.compactModel||s:n.config.model;try{const t=h(n.session.messages,n.session.compaction||{}),i=await m({baseUrl:n.config.baseUrl,apiKey:n.config.apiKey,model:o,messages:[...t,{role:"user",content:"(这是一个旁支问题,请结合上文直接简答,不要调用工具、不要展开无关内容)\n"+e}]});n.print("↪ "+(i.content||"(无回复)").trim())}catch(t){n.print("旁支问答失败:"+t.message)}}},{name:"ide",desc:"查看 IDE 集成状态(编辑器上下文与内联审阅)",run(n){const t=!!process.env.HUICAI_IDE_PORT,e=process.env.HUICAI_IDE_FILE,o=process.env.HUICAI_IDE_SELECTION,i=process.env.HUICAI_IDE_DIAGNOSTICS,s=["IDE 集成"];s.push(" 内联审阅 "+(t?"已连接(write/edit 交编辑器做 diff 审阅)":"未连接(CLI 直接写盘)")),s.push(" 客户端 "+(n.config.client||"cli")),s.push(" 当前文件 "+(e||"(无)")),s.push(" 选区 "+(o?o.length+" 字":"(无)")),s.push(" 诊断 "+(i?"有(来自 Problems 面板)":"(无)")),t||s.push(" (在 VS Code/IDEA 的 huicai 面板或其启动的终端里运行可启用内联审阅与编辑器上下文)"),n.print(s.join("\n"))}},{name:"branch",args:"[标题]",desc:"把当前对话另存为一个可 --resume 的分支副本(当前会话继续)",run(n,t){const e=n.session;if(!e.messages.some(n=>"system"!==n.role))return void n.print("当前对话为空,无需分支。");const o=g({cwd:n.cwd,model:e.model,messages:e.messages.map(n=>({...n}))});try{o.compaction=JSON.parse(JSON.stringify(e.compaction||{}))}catch{o.compaction={}}o.title=(String(t||"").trim()||e.title||"对话").slice(0,70)+"(分支)",o.lastPromptTokens=e.lastPromptTokens||0;try{f(o),f(e)}catch{}n.print(`已保存分支副本「${o.title}」(${o.id})。\n 当前会话继续;分支可用 huicai --resume 恢复到此刻状态。`)}},{name:"theme",args:"[default|mono]",desc:"输出主题:default 彩色 / mono 无色",run(n,t){const e=String(t||"").trim().toLowerCase();e?["default","mono"].includes(e)?(n.config.theme=e,k(e),i({theme:e}),n.print("主题:"+e+("mono"===e?"(已关闭颜色)":"(彩色)"))):n.print("取值:default | mono"):n.print("当前主题:"+(n.config.theme||"default")+"\n /theme default | mono(终端配色由终端掌控;mono=关闭颜色,便于复制/管道)")}},{name:"color",args:"["+b.join("|")+"]",desc:"设置强调色(提示符/高亮)",run(n,t){const e=String(t||"").trim().toLowerCase();e?v(e)?(n.config.accentColor=e,i({accentColor:e}),n.print("强调色:"+e)):n.print("可选:"+b.join(" / ")):n.print("当前强调色:"+$()+"\n 可选:"+b.join(" / "))}},{name:"tui",args:"[auto|rich|plain]",desc:"交互渲染器偏好(下次启动生效)",run(n,t){const e=String(t||"").trim().toLowerCase();e?["auto","rich","plain"].includes(e)?(n.config.tui=e,i({tui:e}),n.print("渲染器偏好:"+e+"(下次启动生效)")):n.print("取值:auto | rich | plain"):n.print("当前渲染器偏好:"+(n.config.tui||"auto")+"\n auto(TTY 下富 TUI)| rich | plain(下次启动生效;本次可加 --tui/--plain 覆盖)")}},{name:"advisor",args:"[模型id|off]",desc:"设置顾问模型(更强模型):智能体遇难题可 ask_advisor 咨询第二意见",async run(n,t){const e=String(t||"").trim();if(!e)return void n.print(n.config.advisorModel?"当前顾问模型:"+n.config.advisorModel+"(智能体可用 ask_advisor 咨询)":"未设置顾问模型。用法:/advisor <模型id>(设一个更强的模型;/models 看可用)· /advisor off 关闭");const o="off"===e?"":e;if(n.config.advisorModel=o,i({advisorModel:o}),Array.isArray(n.extraTools)){const t=n.extraTools.findIndex(n=>"ask_advisor"===n.spec?.function?.name);if(t>=0&&n.extraTools.splice(t,1),o){const{makeAdvisorTool:t}=await import("./advisor.js"),e=t({config:n.config});e&&n.extraTools.push(e)}}n.print(o?"顾问模型已设为 "+o+"(ask_advisor 工具已可用)":"已关闭顾问模型。")}},{name:"automodel",args:"[强模型id|off]",desc:"按任务自动选模型:复杂回合自动升配到该强模型,其余用当前模型",run(n,t){const e=String(t||"").trim();if(!e)return void n.print(n.config.autoModelStrong?"自动升配:开(复杂回合→"+n.config.autoModelStrong+",其余→"+n.config.model+")":"未开启。用法:/automodel <强模型id>(复杂回合自动用它;简单回合仍用当前便宜模型)· /models 看可用");const o="off"===e.toLowerCase()?"":e;n.config.autoModelStrong=o,i({autoModelStrong:o}),n.print(o?"按任务自动选模型:开(复杂回合→"+o+",其余→"+n.config.model+")":"已关闭自动升配。")}},{name:"embed",args:"[嵌入模型id|off]",desc:"search_code 向量语义重排:设一个嵌入模型走 /v1/embeddings 重排(空/off=纯 BM25)",run(n,t){const e=String(t||"").trim();if(!e)return void n.print(n.config.embeddingModel?"search_code 向量重排:开(嵌入模型 "+n.config.embeddingModel+")":"未启用向量重排(纯 BM25,离线可用)。用法:/embed <嵌入模型id>(需平台有该嵌入模型)· /embed off");const o="off"===e.toLowerCase()?"":e;n.config.embeddingModel=o,i({embeddingModel:o}),n.print(o?"search_code 向量语义重排:开(嵌入模型 "+o+";下次检索生效,BM25 召回后按向量相似度重排)":"已关闭向量重排(回到纯 BM25)。")}},{name:"budget",args:"[token数|off|status]",desc:"设置本项目 token 预算(软提醒;/cost 看进度)",run(n,t){const e=String(t||"").trim().toLowerCase(),o=(n.usage?.prompt||0)+(n.usage?.completion||0);if(!e||"status"===e){const t=Number(n.config.budgetTokens)||0;return void n.print(t>0?`token 预算:${o.toLocaleString()} / ${t.toLocaleString()}(${Math.round(o/t*100)}%)`:`未设预算(本次已用 ${o.toLocaleString()} tokens)。/budget <token数> 设定`)}if("off"===e)return n.config.budgetTokens=0,i({budgetTokens:0}),void n.print("已取消预算。");const s=parseInt(e,10);!Number.isFinite(s)||s<1e3?n.print("请给 ≥1000 的 token 数,或 off / status。"):(n.config.budgetTokens=s,i({budgetTokens:s}),n.print("token 预算设为 "+s.toLocaleString()+"(软提醒;/cost 显示进度)。"))}},{name:"debug",args:"[on|off]",desc:"调试输出(每轮打印模型/耗时/token)",run(n,t){const e=String(t||"").trim().toLowerCase(),o="on"===e||"off"!==e&&!n.config.debug;n.config.debug=o,i({debug:o}),n.print("调试输出:"+(o?"开(下一条消息起每轮打印 [debug] 行)":"关"))}},{name:"sandbox",args:"[on|off]",desc:"沙箱模式:禁 shell 与联网(仍可读写本地文件)",run(n,t){const e=String(t||"").trim().toLowerCase(),o="on"===e||"off"!==e&&!n.config.sandbox;n.config.sandbox=o,n.permissions?.setSandbox?.(o),n.print(o?"沙箱模式:开。run_bash 与 fetch_url 将被拒绝;本地文件读写照常。":"沙箱模式:关(写/执行仍按权限审批)。")}},{name:"reload-skills",desc:"从磁盘重新加载技能(新增/改名技能生效;改正文已实时生效)",async run(n){const{makeSkillTools:t,loadLocalSkills:e}=await import("./skills.js");if(Array.isArray(n.extraTools)){for(let t=n.extraTools.length-1;t>=0;t--){const e=n.extraTools[t]?.spec?.function?.name;"load_skill"!==e&&"read_skill_file"!==e||n.extraTools.splice(t,1)}n.extraTools.push(...t(n.cwd))}const o=e(n.cwd);n.print(o.length?`已重载 ${o.length} 个技能:${o.map(n=>n.name).join(", ")}\n (load_skill 现可载入其中任意一个;系统提示词内的技能目录将在下次新会话刷新)`:"未发现本地技能(.huicai/skills 或 ~/.huicai/skills)。")}},{name:"add-dir",args:"[目录 | list | remove <目录>]",desc:"添加可读/搜索的附加目录(写入仍限主目录)",run(e,o){const i=e.session;Array.isArray(i.extraDirs)||(i.extraDirs=[]);const s=String(o||"").trim().split(/\s+/).filter(Boolean),r=(s[0]||"").toLowerCase();if(!o||"list"===r)return void e.print(i.extraDirs.length?"附加目录:\n"+i.extraDirs.map(n=>" "+n).join("\n")+"\n /add-dir <目录> 添加 · /add-dir remove <目录> 移除":"暂无附加目录。用法:/add-dir <目录>(可读/搜索,写仍限主目录)");if("remove"===r||"rm"===r){const n=t.resolve(e.cwd,s.slice(1).join(" ")),o=i.extraDirs.length;i.extraDirs=i.extraDirs.filter(t=>t!==n),J(i);try{f(i)}catch{}return void e.print(i.extraDirs.length<o?"已移除:"+n:"未找到该附加目录:"+n)}const a=t.resolve(e.cwd,String(o).trim());let c=!1;try{c=n.statSync(a).isDirectory()}catch{}if(c)if(a!==e.cwd){i.extraDirs.includes(a)||i.extraDirs.push(a),J(i);try{f(i)}catch{}e.print("已添加可访问目录:"+a+"\n (模型可用 read_file/list_files/grep 以绝对路径访问;写入仍限主目录)")}else e.print("这已是主工作目录。");else e.print("目录不存在:"+a)}},{name:"agents",desc:"列出可用子智能体人格(.huicai/agents/*.md;dispatch_agent 可指定)",async run(n){const{loadAgents:t}=await import("./subagent.js"),e=t(n.cwd);n.print(e.length?"子智能体人格(dispatch_agent 的 agent 参数可指定):\n"+e.map(n=>` ${n.name}${n.description?" — "+n.description.replace(/\s+/g," ").slice(0,60):""}`).join("\n"):"未定义子智能体人格。\n 在 .huicai/agents/<名字>.md 写 frontmatter(name/description)+正文(人格提示词)即可;\n dispatch_agent 默认用通用只读子智能体(无需定义也能用)。")}},{name:"orchestrate",args:"<目标>",desc:"把一个目标自动分解为并行子任务、多智能体协作完成",run(n,t){const e=String(t||"").trim();if(e)return{prompt:`目标:${e}\n\n请作为编排者按以下方式完成:\n1) 先把目标分解为若干**独立、可并行**的子任务(研究/检索/分析/定位);\n2) 用 dispatch_agents 一次性并行委派这些子任务(每个自足,可指定合适的子智能体人格);\n3) 汇总各子智能体的结论,形成整体方案或结果;\n4) 若需落地改动,再据此逐步执行并自检(可用 /verify 让我自动跑验证)。\n优先用 dispatch_agents 并行,减少串行等待。`};n.print("用法:/orchestrate <较大的目标>(我会分解为并行子任务,用 dispatch_agents 并行推进后综合)")}},{name:"batch",args:"<大改动目标>",desc:"把一个大改动拆成独立子任务,在隔离 git worktree 中并行推进",run(n,t){const e=String(t||"").trim();if(e)return{prompt:`目标(需拆分并行落地):${e}\n\n请这样做:\n1) 先把目标拆成若干**相互独立、可并行**的改动子任务(尽量互不冲突的文件/模块);\n2) 对每个子任务,用 \`git worktree add -b huicai-batch/<名> <路径>\` 建隔离工作区,在其中完成该子任务的改动并 commit(不影响主工作区);\n3) 全部完成后,汇总每个 worktree 的分支与改动;需要时可对每个分支执行 \`huicai pr\` 开 PR。\n(并行调研可用 dispatch_agents;改动务必隔离在各自 worktree,避免互相踩踏。完成后可 \`git worktree remove\` 清理。)`};n.print("用法:/batch <一个可拆分的大改动目标>(拆成互不冲突的子任务,各在隔离 worktree 中落地)")}},{name:"fork",args:"<后台任务提示词>",desc:"派生后台智能体(继承当前对话副本,--yolo 自动放行,输出写日志)",async run(n,t){const e=String(t||"").trim();if(!e)return void n.print("用法:/fork <让后台智能体做什么>(以当前对话副本为上下文,后台运行,--yolo 自动放行)");const o=g({cwd:n.cwd,model:n.session.model,messages:n.session.messages.map(n=>({...n}))});try{o.compaction=JSON.parse(JSON.stringify(n.session.compaction||{}))}catch{o.compaction={}}o.title="(fork) "+(n.session.title||"");try{f(o)}catch{}const{forkAgent:i}=await import("./forks.js");let s;try{s=i({prompt:e,cwd:n.cwd,sessionId:o.id})}catch(t){return void n.print("无法派生后台任务:"+t.message)}n.print(`已派生后台智能体 pid ${s.pid}(--yolo 自动放行)\n 上下文:当前对话副本 ${o.id}\n 日志:${s.log}\n /tasks 查看 · /tasks kill ${s.pid} 终止`)}},{name:"tasks",args:"[kill <pid>]",desc:"查看/终止本会话派生的后台任务(/fork)",async run(n,t){const{listForks:e,killFork:o}=await import("./forks.js"),i=String(t||"").trim().split(/\s+/).filter(Boolean);if("kill"===i[0]){const t=Number(i[1]);if(!Number.isFinite(t))return void n.print("用法:/tasks kill <pid>");const e=o(t);return void n.print(e.ok?"已终止 pid "+t:"终止失败:"+(e.reason||""))}const s=e();s.length?n.print("后台任务:\n"+s.map(n=>` pid ${n.pid} · ${n.alive?"运行中":"已结束"} · ${l(n.started)} · ${n.prompt}\n 日志 ${n.log}`).join("\n")+"\n /tasks kill <pid> 终止"):n.print("本会话暂无后台任务(/fork <提示词> 派生)。")}},{name:"schedule",args:'[list | add "<cron>" "<提示词>" | remove <id>]',desc:"管理定时任务(生成 crontab 行交系统 cron 运行)",async run(n,t){const e=await import("./schedule.js"),o=String(t||"").trim(),i="huicai";if(!o||"list"===o){const t=e.listTasks();return t.length?void n.print("定时任务:\n"+t.map(n=>` #${n.id} ${n.cron} ${n.prompt.replace(/\s+/g," ").slice(0,40)}\n crontab: ${e.crontabLine(n,i)}`).join("\n")+"\n /schedule remove <id> 删除"):void n.print('暂无定时任务。\n 添加:/schedule add "*/30 * * * *" "跑测试并把结果写入 report.md"\n (随后把打印的 crontab 行加入 crontab -e)')}if(/^add\b/.test(o)){const t=o.match(/"([^"]*)"/g);if(!t||t.length<2)return void n.print('用法:/schedule add "<cron>" "<提示词>"(cron 与提示词都要加引号)');const s=t[0].slice(1,-1).trim(),r=t[1].slice(1,-1).trim();if(5!==s.split(/\s+/).length)return void n.print('cron 需 5 个字段,如 "*/30 * * * *"(分 时 日 月 周)');const{id:a}=e.addTask({cron:s,prompt:r,cwd:n.cwd});return void n.print("已添加定时任务 #"+a+"\n 加入 crontab(crontab -e):\n "+e.crontabLine({id:a,cron:s,prompt:r,cwd:n.cwd},i))}if(/^(remove|rm)\b/.test(o)){const t=o.split(/\s+/)[1];return void n.print(e.removeTask(t)?"已删除定时任务 "+t:"未找到定时任务 "+t)}n.print('用法:/schedule list · /schedule add "<cron>" "<提示词>" · /schedule remove <id>')}},{name:"cd",args:"<目录>",desc:"切换工作目录(在新目录开启新会话;IDE 中请用「打开文件夹」)",run(e,o){const i=String(o||"").trim();if(!i)return void e.print("当前工作目录:"+e.cwd+"\n 用法:/cd <目录>(将在新目录开启新会话,旧会话已保存)");const s=t.resolve(e.cwd,i);let r=!1;try{r=n.statSync(s).isDirectory()}catch{}if(r){if(s!==e.cwd)return{action:"cd",cwd:s};e.print("已在该目录。")}else e.print("目录不存在:"+s)}},{name:"loop",args:"<秒> <提示词> | off",desc:"每隔 N 秒自动重跑一个提示词(off 停止)",run(n,t){const e=String(t||"").trim();if(!e)return void n.print("用法:/loop <秒> <提示词>(如 /loop 300 检查构建是否通过)· /loop off 停止");if("off"===e)return{action:"loop",stop:!0};const o=/^(\d+)\s+([\s\S]+)$/.exec(e);if(o)return{action:"loop",seconds:Math.max(10,parseInt(o[1],10)),prompt:o[2].trim()};n.print("用法:/loop <秒> <提示词> · /loop off")}},{name:"statusline",args:"<shell命令> | off",desc:"自定义 TUI 底栏(运行该命令,展示其 stdout)",run(n,t){const e=String(t||"").trim();if(!e)return void n.print("当前状态栏命令:"+(n.config.statusLine||"(默认)")+"\n /statusline <shell命令>(stdout 显示在 TUI 底栏)· /statusline off 恢复默认");const o="off"===e?"":e;n.config.statusLine=o,i({statusLine:o}),n.print(o?"状态栏命令已设置(TUI 底栏生效):"+o:"已恢复默认状态栏。")}},{name:"background",desc:"保存当前会话并给出在别处继续/后台运行的方式",run(n){try{f(n.session)}catch{}n.print(`会话已保存(${n.session.id})。\n · 在别处继续:huicai resume ${n.session.id}\n · 后台跑一个任务:/fork <提示词>(派生后台智能体,日志可查)\n · 定时运行:/schedule add "<cron>" "<提示词>"`)}},{name:"exit",desc:"退出",run:()=>({action:"exit"})}];export function listCommands(n){return[...Y.map(n=>({name:n.name,args:n.args||"",desc:n.desc})),...loadCustomCommands(n).map(n=>({name:n.name,args:"",desc:n.desc}))]}export function loadCustomCommands(o){const i=[o?t.join(o,".huicai","commands"):null,t.join(e.homedir(),".huicai","commands")].filter(Boolean),s=new Map;for(const e of i){let o;try{o=n.readdirSync(e).filter(n=>n.endsWith(".md"))}catch{continue}for(const i of o){const o=i.replace(/\.md$/,"").trim();if(o&&!s.has(o)&&!Y.some(n=>n.name===o))try{const r=n.readFileSync(t.join(e,i),"utf8"),{desc:a,body:c}=G(r);s.set(o,{name:o,desc:a||"自定义命令",template:c})}catch{}}}return[...s.values()]}function G(n){const t=/^---\n([\s\S]*?)\n---\n?/.exec(n);if(!t)return{desc:"",body:n.trim()};const e=t[1],o=/^description:\s*(.+)$/m.exec(e);return{desc:o?o[1].trim():"",body:n.slice(t[0].length).trim()}}export async function runSlash(n,t){const e=n.slice(1).split(/\s+/),o=(e[0]||"").trim(),i=e.slice(1).join(" ").trim();if("quit"===o)return{handled:!0,action:"exit"};const s=Y.find(n=>n.name===o);if(s){return{handled:!0,...await s.run(t,i)||{}}}const r=loadCustomCommands(t.cwd).find(n=>n.name===o);if(r){let n=r.template;return n.includes("$ARGUMENTS")?n=n.replaceAll("$ARGUMENTS",i):i&&(n=n+"\n\n"+i),{handled:!0,prompt:n}}return t.print("未知命令。/help 查看可用命令。"),{handled:!0}}
1
+ import n from"node:fs";import e from"node:path";import t from"node:os";import{execSync as o}from"node:child_process";import{saveConfig as i,DEFAULT_MODEL as s,CONFIG_PATH as r}from"./config.js";import{listModels as a,fetchUsage as c,streamChat as d}from"./api.js";import{listSessions as m,relTime as l,turnCount as p,saveSession as f,createSession as u}from"./session.js";import{compactNow as g,buildModelMessages as h}from"./compaction.js";import{computeInsights as w,formatInsights as y}from"./insights.js";import{setColorMode as $,setAccent as k,getAccent as v,ACCENTS as b}from"./colors.js";import{detectVerifyCommand as S}from"./autofix.js";import{loadAllowRules as j,describeRules as x,parseRule as T,addProjectRule as _,removeProjectRule as M,GLOBAL_SETTINGS_PATH as C,projectSettingsPath as I}from"./settings.js";import{contextReport as L}from"./context-report.js";import{renderDiff as N}from"./diffview.js";import{loadHooks as P,formatHooks as A}from"./hooks.js";import{copyToClipboard as E}from"./clipboard.js";import{toMarkdown as D,recapConversation as U}from"./transcript.js";import{runDoctor as q}from"./doctor.js";import{TOOL_SPECS as F}from"./tools.js";import{makeTodoTool as R}from"./todo.js";import{makeMemoryTool as O}from"./memory.js";const K=/^(agent|workflow|datahuman|squad)-/,B="\n\n# 附加可访问目录(可用 read_file/list_files/grep 以绝对路径访问;写入仍限主目录)\n";function H(n){const e=n.messages&&n.messages[0];if(!e||"system"!==e.role||"string"!=typeof e.content)return;const t=e.content.indexOf(B);let o=t>=0?e.content.slice(0,t):e.content;const i=n.extraDirs||[];i.length&&(o+=B+i.map(n=>"- "+n).join("\n")+"\n"),e.content=o}export const INIT_PROMPT="请为当前项目生成或完善 HUICAI.md(给终端智能体看的项目说明):先用 list_files/glob/read_file 快速调研项目(技术栈、目录结构、构建/测试/运行命令、代码约定),再用 write_file 创建或更新 HUICAI.md(若已存在则保留人工撰写的内容,增量完善)。内容包含:项目简介、关键目录、常用命令(构建/测试/运行)、代码风格与注意事项。控制在 100 行以内,用中文。";const J=[{name:"help",desc:"显示可用命令(含自定义命令)",run(n){const e=J.map(n=>[`/${n.name}${n.args?" "+n.args:""}`,n.desc]);for(const t of loadCustomCommands(n.cwd))e.push([`/${t.name}`,`${t.desc}(自定义)`]);e.push(["! <命令>","直接执行 shell 命令并把输出加入上下文"]),e.push(["@<文件路径>","在消息中引用文件内容(自动展开给模型)"]);const t=Math.min(24,Math.max(...e.map(([n])=>n.length))+2);n.print(e.map(([n,e])=>" "+n.padEnd(t)+e).join("\n"))}},{name:"model",args:"[名称|auto]",desc:"查看或切换模型(auto=按账号自动选可用模型,与 VSCode 一致)",async run(n,e){if(e){if("auto"===e){try{const e=(await a(n.config.baseUrl,n.config.apiKey)).map(n=>n.id).filter(n=>!/^(agent|workflow|datahuman|squad)-/.test(n));n.config.model=(e.includes(s)?s:e[0])||n.config.model}catch{}return i({model:"auto"}),void n.print("Auto 已选:"+n.config.model)}try{const t=(await a(n.config.baseUrl,n.config.apiKey)).map(n=>n.id);if(!t.includes(e))return void n.print(`当前账号无权使用模型「${e}」。\n 可用模型:${t.slice(0,12).join(", ")}`+(t.length>12?` …(共 ${t.length} 个,/models 查看全部)`:"")+"\n 或用 /model auto 自动选择可用模型。")}catch(e){n.print(`(未能校验模型可用性:${e.message})仍按输入切换;若无权限,平台会在调用时拒绝。`)}n.config.model=e,i({model:e}),n.print("已切换模型:"+e)}else n.print("当前模型:"+(n.config.model||"(未设置)"))}},{name:"models",desc:"列出平台可用模型",async run(n){try{const e=await a(n.config.baseUrl,n.config.apiKey);n.print(e.map(n=>" "+n.id).join("\n")||"(无)")}catch(e){n.print("拉取失败:"+e.message)}}},{name:"usage",desc:"查看 Token 用量窗口(5小时/7天,按会员等级)",async run(n){const e=n=>"number"==typeof n&&Number.isFinite(n)?n:0,t=(n,t)=>(n=e(n),(t=e(t))<0?`${n.toLocaleString()} / 无限`:0===t?"该档未开放":`${n.toLocaleString()} / ${t.toLocaleString()} (${Math.min(100,Math.round(n/t*100))}%)`);try{const e=await c(n.config.baseUrl,n.config.apiKey);if(!e||"number"!=typeof e.used5h&&"number"!=typeof e.used7d)return void n.print("平台未返回用量数据(请确认平台已支持 /v1/usage)。");n.print(`Token 用量窗口\n 近 5 小时:${t(e.used5h,e.limit5h)}\n 近 7 天 :${t(e.used7d,e.limit7d)}`+(!e.allowed&&e.message?"\n "+e.message:""))}catch(e){n.print("拉取用量失败:"+e.message)}}},{name:"login",desc:"重新登录(浏览器授权;无浏览器请退出后 huicai login --paste)",async run(n){try{const{deviceLogin:e}=await import("./device.js");n.print("正在打开浏览器授权…");const t=await e(n.config.baseUrl);n.config.apiKey=t,i({apiKey:t}),n.print("登录成功。")}catch(e){n.print("登录未完成:"+e.message+"(可退出后用 huicai login --paste 粘贴令牌)")}}},{name:"logout",desc:"退出登录(清除本机保存的令牌)",run(n){n.config.apiKey="",i({apiKey:""}),n.print("已退出登录。后续对话需重新 /login 或 huicai login。")}},{name:"clear",desc:"开启新会话(旧会话仍可 --resume 恢复)",run:()=>({action:"clear"})},{name:"compact",desc:"立即压缩上下文(摘要独立存储,不影响页面与会话原文)",async run(n){n.session.compaction||(n.session.compaction={});const e=await g(n.session.messages,n.config,{state:n.session.compaction});n.print(e>0?`已压缩:${e} 条较早消息已摘要(原对话完整保留,仅发给模型的上下文变小)`:"暂无可压缩的中段历史(会话较短或摘要失败)")}},{name:"memory",args:"[clear]",desc:"查看本项目长期记忆(clear 清空)",async run(n,e){const{loadMemory:t,clearMemory:o,memoryPath:i}=await import("./memory.js");if("clear"===e)return void n.print(o(n.cwd)?"已清空本项目长期记忆。":"本项目暂无长期记忆。");const s=t(n.cwd);n.print(s?s+`\n(文件:${i(n.cwd)} · /memory clear 清空)`:"本项目暂无长期记忆(模型会经 memory_write 自动记录值得跨会话记住的信息;配置 memory:false 可关闭)。")}},{name:"cost",desc:"查看本次运行累计 token 用量(及预算进度)",run(n){const e=n.usage.prompt+n.usage.completion,t=Number(n.config.budgetTokens)||0;n.print(`token 用量:输入 ${n.usage.prompt} / 输出 ${n.usage.completion}`+(n.session.lastPromptTokens?` · 当前上下文 ~${n.session.lastPromptTokens} tok`:"")+(t>0?`\n预算:${e.toLocaleString()} / ${t.toLocaleString()} tokens(${Math.round(e/t*100)}%)`+(e>=t?" ⚠ 已超预算":e>=.8*t?" ⚠ 接近预算":""):""))}},{name:"status",desc:"显示运行状态(版本/模型/目录/会话/MCP/权限规则)",run(n){const e=n.session,t=[...new Set((n.extraTools||[]).map(n=>n.spec?.function?.name||"").filter(n=>n.startsWith("mcp__")).map(n=>n.split("__")[1]))];let i="";try{i=o("git rev-parse --abbrev-ref HEAD",{cwd:n.cwd,stdio:["ignore","pipe","ignore"]}).toString().trim()}catch{}const s=j(n.cwd),r=[`版本 huicai ${n.version||""}`,`模型 ${n.config.model}`,`平台 ${n.config.baseUrl}`,`目录 ${n.cwd}${i?`(git:${i})`:""}`,`会话 ${e.id} · ${p(e.messages)} 轮${e.lastPromptTokens?` · 上下文 ~${e.lastPromptTokens} tok`:""}`,`token 输入 ${n.usage.prompt} / 输出 ${n.usage.completion}`,"审批 "+(n.permissions?.isYolo?.()?"⚠ yolo 自动放行":"写/执行需授权")+` · 允许规则 ${s.length} 条${n.config.planMode?" · 📋 计划模式(只读)":""}`+`${n.config.sandbox?" · 🧪 沙箱":""}${n.config.goal?" · 🎯 目标已设":""}`,`MCP ${t.length?t.join(", "):"(未配置)"}`];n.print(r.join("\n"))}},{name:"sessions",desc:"列出当前目录的历史会话",run(n){const e=m(n.cwd);if(!e.length)return void n.print("该目录暂无历史会话。");const t=e.slice(0,15).map(e=>{const t=e.id===n.session.id?" ← 当前":"";return` ${l(e.updatedAt).padStart(8)} ${e.title} ${e.turns}轮 · ${e.id}${t}`});t.push("(退出后 huicai --resume 可选择恢复)"),n.print(t.join("\n"))}},{name:"yolo",desc:"切换「自动放行写/执行」模式",run(n){const e=!n.permissions.isYolo();n.permissions.setYolo(e),n.print("自动放行(yolo):"+(e?"开(谨慎)":"关"))}},{name:"cwd",desc:"显示当前工作目录",run(n){n.print(n.cwd)}},{name:"markdown",desc:"切换助手回复的 Markdown 渲染(TUI 富文本;关闭则显示原始文本,便于复制)",run(n){const e=!1===n.config.markdown;n.config.markdown=e,i({markdown:e}),n.print("Markdown 渲染:"+(e?"开(下一条回复起生效)":"关(显示原始 Markdown 文本)"))}},{name:"init",desc:"让智能体调研项目并生成/完善 HUICAI.md",run:()=>({prompt:INIT_PROMPT})},{name:"skills",desc:"列出本地技能(Agent Skills;pull/push 用终端命令 huicai skills)",async run(n){const{loadLocalSkills:e}=await import("./skills.js"),t=e(n.cwd);t.length?n.print(t.map(n=>` ${n.name}${n.version?" v"+n.version:""}(${"project"===n.scope?"项目":"全局"}${n.files.length?` · ${n.files.length} 附件`:""})${n.description?" "+n.description.slice(0,60):""}`).join("\n")+"\n(模型会按描述经 load_skill 按需载入;本会话启动时已注入目录)"):n.print("暂无本地技能(.huicai/skills 或 ~/.huicai/skills)。可用 huicai skills pull 从平台拉取。")}},{name:"mcp",desc:"列出已连接的 MCP 工具",run(n){const e=(n.extraTools||[]).filter(n=>(n.spec?.function?.name||"").startsWith("mcp__"));e.length?n.print(e.map(n=>` ${n.spec.function.name}${n.readOnly?"(只读)":""}`).join("\n")):n.print("未连接 MCP 服务。配置 ~/.huicai/mcp.json(通用 mcpServers 格式)后重启生效。")}},{name:"context",desc:"可视化当前上下文占用(各类别 token 估算)",run(n){const e=K.test(String(n.config.model||""));let t=[];if(!e)try{const e=R({state:{list:[]},emit(){}}).spec,o=!1!==n.config.memoryEnabled?O({cwd:n.cwd,emit(){}}).spec:null;t=[...F,e,...o?[o]:[],...(n.extraTools||[]).map(n=>n.spec).filter(Boolean)]}catch{t=F}n.print(L({messages:n.session.messages,compaction:n.session.compaction||{},specs:t,lastPromptTokens:n.session.lastPromptTokens||0,autoCompactTokens:Number(n.config.autoCompactTokens)||0,platform:e}))}},{name:"permissions",args:"[allow|deny <规则> | remove <规则>]",desc:"查看/增删工具权限规则(allow/deny)",run(n,e){const t=String(e||"").trim().split(/\s+/).filter(Boolean),o=(t.shift()||"").toLowerCase(),i=t.join(" ").trim();if(!o){const e=x(n.cwd),t=n=>n.length?n.map(n=>" "+n).join("\n"):" (无)";return void n.print("权限规则(deny 优先于 allow/yolo,对只读工具也生效)\n 全局 ~/.huicai/settings.json\n 允许:\n"+t(e.global.allow)+"\n 拒绝:\n"+t(e.global.deny)+"\n 项目 .huicai/settings.json\n 允许:\n"+t(e.project.allow)+"\n 拒绝:\n"+t(e.project.deny)+"\n 用法:/permissions allow <规则> · /permissions deny <规则> · /permissions remove <规则>\n 规则语法:`工具名` 或 `工具名(模式*)`,如 run_bash(git *)、edit_file、read_file(**/.env*)")}if("allow"!==o&&"deny"!==o){if("remove"===o||"rm"===o){if(!i)return void n.print("用法:/permissions remove <规则原文>(仅移除项目设置里的规则)");const e=M(n.cwd,i);return n.permissions?.reloadRules?.(),void n.print(e.removed?`已移除规则:${i}`:`未在项目设置中找到该规则:${i}`)}n.print("未知子命令。用法:/permissions [allow|deny|remove] <规则>")}else{if(!i||!T(i))return void n.print("规则无效。示例:run_bash(git *)、edit_file、fetch_url(https://a.com*)");try{const e=_(n.cwd,o,i);n.permissions?.reloadRules?.(),n.print(`已添加${"deny"===o?"拒绝":"允许"}规则:${i} → ${e}`)}catch(e){n.print("写入失败:"+e.message)}}}},{name:"diff",desc:"查看未提交改动(git)与本会话智能体改过的文件",run(n){n.print(N(n.cwd,n.session.messages))}},{name:"pr",args:"[commit|address]",desc:"自动提交并开 PR(生成规范 commit→建分支→push→gh 开 PR);address=按 review 评论自动改",async run(n,e){const t=String(e||"").trim().toLowerCase();if("address"===t||"comments"===t)return{prompt:"请处理当前分支/PR 的 review 评论:用 `gh pr view --comments`(无 PR 则先 `gh pr status`)读取评论,逐条理解并修改代码解决,然后 git add/commit(规范信息)+ git push;完成后逐条说明如何处理的。若未装 gh 或无 PR,先告知。"};const{runPrFlow:o}=await import("./gitflow.js");try{const e=await o({cwd:n.cwd,config:n.config,emit:e=>n.print(e),push:"commit"!==t,openPr:"commit"!==t});if(!e.ok)return void n.print("✗ "+e.error);n.print(e.pr?"✅ 已开 PR:"+e.pr:"✅ 完成(分支 "+e.branch+")"+(e.note?":"+e.note:""))}catch(e){n.print("PR 流程出错:"+e.message)}}},{name:"plan",args:"[on|off|approve]",desc:"计划模式:只读调研出方案→/plan approve 批准并分步执行",run(n,e){const t=String(e||"").trim().toLowerCase();if("approve"===t||"go"===t)return n.config.planMode=!1,n.permissions?.setPlanMode?.(!1),n.print("计划已批准,开始分步执行。"),{prompt:"计划已获批准。现在请按你刚才制定的计划(todo 列表)逐步执行:每完成一步用 todo_write 更新状态并简要自检;遇到与计划不符及时调整并说明;全部完成后总结改了什么、以及如何验证(可用 /verify 让我自动跑验证)。"};const o="on"===t||"off"!==t&&!n.config.planMode;n.config.planMode=o,n.permissions?.setPlanMode?.(o),n.print(o?"计划模式:开。只做只读调研并用 todo_write 产出分步方案;写/执行会被拦截。\n 方案满意后用 /plan approve 批准并自动分步执行;或 /plan off 手动执行。":"计划模式:关。恢复正常执行(写/执行仍按权限审批)。")}},{name:"auto",args:"<目标>",desc:"自动规划并自动执行:只读调研出分步计划,确认「是否继续?」后自动逐步执行",run(n,e){const t=String(e||"").trim();if(t)return{action:"autorun",goal:t};n.print("用法:/auto <目标>\n 先自动规划(只读调研 + todo 分步计划),随后询问「是否继续?1、是 2、是,并允许在当前对话中后续相同操作不再询问 3、否」,确认后自动逐步执行并自检。")}},{name:"rewind",args:"[N [chat|code|both]]",desc:"检查点回滚:无参列出;/rewind N 回退对话,加 code/both 一并回滚文件",run(n,e){const t=n.checkpoints;if(!t||!t.list.length)return void n.print("暂无检查点(每个用户回合前自动记录,发生对话后可用)。");const o=String(e||"").trim().split(/\s+/).filter(Boolean);if(!o.length){const e=["检查点(回滚到某个回合之前)"];for(const n of t.list)e.push(` #${n.id} ${l(n.time)} ${n.label}`+(n.journal.length?` · 改动 ${n.journal.length} 处`:""));return e.push(" 用法:/rewind N(仅回退对话,安全)· /rewind N code(回滚文件)· /rewind N both"),e.push(" 提示:code/both 会覆盖当前文件,建议先 /diff 查看。"),void n.print(e.join("\n"))}const i=Number(o[0]);if(!Number.isFinite(i))return void n.print("用法:/rewind N [chat|code|both]");const s=["chat","code","both"].includes(o[1])?o[1]:"chat",r=t.restore(i,s,{session:n.session,cwd:n.cwd});if(!r.ok)return void n.print(r.error);const a=[];r.conversation&&a.push("对话已回退到该回合之前"),r.filesRestored.length&&a.push(`恢复文件 ${r.filesRestored.length} 个`),r.filesDeleted.length&&a.push(`删除新建文件 ${r.filesDeleted.length} 个`),r.filesSkipped.length&&a.push(`跳过(过大) ${r.filesSkipped.length} 个`),r.filesFailed.length&&a.push(`失败 ${r.filesFailed.length} 个`);try{f(n.session)}catch{}return n.print("已回滚到 #"+i+":"+(a.join(";")||"无变化")+(r.filesFailed.length?"\n 失败:"+r.filesFailed.join(";"):"")),r.conversation?{action:"refresh"}:void 0}},{name:"hooks",desc:"查看已配置的工具事件钩子(Pre/PostToolUse/UserPromptSubmit/Stop)",run(n){n.print(A(P(n.cwd)))}},{name:"copy",args:"[N]",desc:"复制助手回复到剪贴板(默认最后一条;N=倒数第 N 条)",async run(n,e){const t=n.session.messages.filter(n=>"assistant"===n.role&&"string"==typeof n.content&&n.content.trim());if(!t.length)return void n.print("还没有可复制的助手回复。");const o=Math.max(1,parseInt(e,10)||1),i=t[t.length-o];if(!i)return void n.print(`只有 ${t.length} 条助手回复。`);const s=await E(i.content);n.print(s.ok?`已复制${o>1?`倒数第 ${o} 条`:"最后一条"}回复到剪贴板(${i.content.length} 字)`:`复制失败(未找到 ${s.tool})。以下为该回复内容:\n\n${i.content}`)}},{name:"export",args:"[文件路径]",desc:"导出当前对话为 Markdown 文件",run(t,o){const i=D(t.session),s=String(o||"").trim()||`huicai-对话-${(new Date).toISOString().slice(0,19).replace(/[:T]/g,"-")}.md`,r=e.isAbsolute(s)?s:e.join(t.cwd,s);try{n.writeFileSync(r,i,"utf8"),t.print("已导出对话到 "+r)}catch(n){t.print("导出失败:"+n.message)}}},{name:"rename",args:"<新标题>",desc:"重命名当前会话",run(n,e){const t=String(e||"").trim();if(t){n.session.title=t.slice(0,80);try{f(n.session)}catch{}n.print("会话已重命名为:"+n.session.title)}else n.print("当前标题:"+(n.session.title||"(未命名)")+"\n 用法:/rename <新标题>")}},{name:"recap",desc:"用一句话总结当前对话进展(直连原始模型,不写入对话)",async run(n){if(n.session.messages.some(n=>"system"!==n.role))try{n.print("小结:"+await U(n.session.messages,n.config))}catch(e){n.print("生成小结失败:"+e.message)}else n.print("对话还没有内容。")}},{name:"distill",desc:"从当前对话提炼项目知识,写入长期记忆(供后续会话自动加载)",async run(n){if(!1===n.config.memoryEnabled)return void n.print("长期记忆已关闭(config.memory=false)。");if(!n.session.messages.some(n=>"system"!==n.role))return void n.print("对话还没有内容。");const{distillMemory:e}=await import("./transcript.js"),{loadMemory:t,appendMemory:o}=await import("./memory.js");try{const i=await e(n.session.messages,n.config,t(n.cwd));if(!i.length)return void n.print("未提炼出值得长期记住的新知识。");let s=0;for(const e of i)o(n.cwd,e)&&s++;n.print(`已写入 ${s} 条项目知识到长期记忆:\n`+i.map(n=>" · "+n).join("\n"))}catch(e){n.print("提炼失败:"+e.message)}}},{name:"doctor",desc:"本机环境体检(登录/网络/模型/git/工作目录/钩子)",async run(n){try{n.print(await q(n))}catch(e){n.print("体检失败:"+e.message)}}},{name:"config",desc:"查看配置文件路径与当前生效配置(密钥脱敏)",run(n){const e=n.config,t=Number(e.autoCompactTokens);var o;n.print(["配置"," 文件 "+r," 平台 "+e.baseUrl," 密钥 "+(o=e.apiKey,o?o.slice(0,4)+"…"+o.slice(-4)+`(${o.length}位)`:"(未设置)")," 模型 "+e.model," 推理强度 "+(e.reasoningEffort||"off")," 自动压缩 "+(t<0?"关闭":t>0?t.toLocaleString()+" tokens":"默认(80k)")," 记忆 "+(!1===e.memoryEnabled?"关":"开")," Markdown "+(!1===e.markdown?"关":"开")," 全局设置 "+C," 项目设置 "+I(n.cwd)," (多数项有对应斜杠命令直接改:/model /effort /autocompact /permissions …)"].join("\n"))}},{name:"effort",args:"[minimal|low|medium|high|off]",desc:"设置推理强度(reasoning_effort 透传;模型不支持则忽略)",run(n,e){const t=String(e||"").trim().toLowerCase();t?["minimal","low","medium","high","off"].includes(t)?(n.config.reasoningEffort="off"===t?"":t,i({reasoningEffort:n.config.reasoningEffort}),n.print("推理强度:"+(n.config.reasoningEffort||"off")+"(下一条消息起生效;模型不支持则被忽略)")):n.print("取值:minimal | low | medium | high | off"):n.print("当前推理强度:"+(n.config.reasoningEffort||"off")+"\n 用法:/effort minimal|low|medium|high|off")}},{name:"autocompact",args:"[N|off|status]",desc:"自动压缩上下文的触发阈值(token 数;off 关闭)",run(n,e){const t=String(e||"").trim().toLowerCase(),o=Number(n.config.autoCompactTokens)||0;if(!t||"status"===t)return void n.print("自动压缩:"+(o<0?"已关闭":o>0?`阈值 ${o.toLocaleString()} tokens`:"默认阈值 80,000 tokens")+"\n /autocompact <N> 设定 · /autocompact off 关闭 · /autocompact status 查看");if("off"===t)return n.config.autoCompactTokens=-1,i({autoCompactTokens:-1}),void n.print("已关闭自动压缩(仍可 /compact 手动压缩)。");const s=parseInt(t,10);!Number.isFinite(s)||s<1e3?n.print("请给 ≥1000 的 token 阈值,或 off / status。"):(n.config.autoCompactTokens=s,i({autoCompactTokens:s}),n.print("自动压缩阈值设为 "+s.toLocaleString()+" tokens(字符阈值同步联动)。"))}},{name:"review",desc:"审查当前未提交改动(找 bug 与可简化处)",run:()=>({prompt:"请审查当前工作区的未提交改动:先用 run_bash 跑 `git diff`(及 `git status`)查看改了什么,聚焦正确性 bug、边界与错误处理、可复用/可简化处、安全隐患。若本地有 code-review 技能可 load_skill 参照其方法。逐条给出:问题、定位(文件:行)、修改建议,按严重度排序;不确定的地方说明。"})},{name:"security-review",desc:"对当前改动做安全审查",run:()=>({prompt:"请对当前工作区改动做安全审查:注入(SQL/命令/路径穿越)、越权、密钥/凭据泄漏、SSRF、不安全依赖、反序列化、XSS 等。先 run_bash 跑 `git diff` 查看改动。若本地有 security-audit 技能可 load_skill 参照。给出风险点、可利用场景、影响与修复建议,按严重度排序。"})},{name:"scan",args:"[fix]",desc:"安全/质量扫描(自动选可用的 npm audit/semgrep/trivy/gitleaks 等);fix=让智能体逐条修",async run(n,e){const{detectScanners:t,runScanners:o}=await import("./scan.js"),i=t(n.cwd);if(!i.length)return void n.print("未检测到可用扫描器。可安装 semgrep / trivy / gitleaks,或项目含 package.json(npm audit)等。");if("fix"===String(e||"").trim().toLowerCase())return{prompt:"请对本项目做安全/质量扫描并修复:依次运行并理解这些扫描器的输出——"+i.map(n=>"`"+n.command+"`("+n.why+")").join("、")+";对发现的问题逐个修复(升级/更换依赖、修代码、移除硬编码密钥等),修完重跑对应扫描确认清零;最后汇总修了什么。"};n.print("运行扫描器:"+i.map(n=>n.name).join(", ")+"(可能较慢)…");const s=await o(n.cwd,i);for(const e of s)n.print(`\n── ${e.name}(${0===e.code?"✅ 无高危":"⚠ 有发现"})──`),n.print(e.output.split("\n").slice(0,20).join("\n"));n.print("\n(/scan fix 让智能体逐条修复)")}},{name:"cover",desc:"覆盖率驱动测试生成:跑覆盖率→定位未覆盖的改动→补测试→复测",run:()=>({prompt:"请做覆盖率驱动的测试补全:\n1) 跑测试覆盖率(按项目选:npm test -- --coverage / jest --coverage / pytest --cov / go test -cover / mvn test 等);\n2) 重点看**最近改动**且覆盖率低或未覆盖的函数与分支;\n3) 为这些薄弱处补写有意义的测试(覆盖正常/边界/错误路径),遵循项目既有测试风格与框架;\n4) 重跑测试与覆盖率,确认通过且覆盖率提升;最后汇报补了哪些测试、覆盖率变化。"})},{name:"verify",args:"[验证命令]",desc:"修到通过:跑测试/构建,失败自动定位修复并重跑,直到通过或达上限",run(n,e){let t=String(e||"").trim();if(!t){if(t=S(n.cwd)||"",!t)return void n.print("未能自动探测验证命令。用法:/verify <命令>(如 /verify npm test)");n.print("自动探测到验证命令:"+t)}return{action:"autofix",command:t,maxIters:Math.min(10,Math.max(1,Number(n.config.verifyMaxIters)||5))}}},{name:"simplify",desc:"审查改动的可复用/简化/抽象层级并直接清理(只清理不找 bug)",run:()=>({prompt:"请只做质量清理(不找 bug):审查最近改动的复用(是否重复造轮子、可否复用现有工具/函数)、简化(是否有更直接的写法)、效率、以及抽象层级是否合适;把确定的改进直接用 edit_file 应用,保持与项目既有风格一致,改后跑测试/构建自检。若本地有 refactor-safely 技能可 load_skill 参照。最后说明改了什么、为什么。"})},{name:"insights",desc:"本机会话使用洞察(数量/轮数/活跃度/按项目·模型分布)",run(n){n.print(y(w(m(null),{cwd:n.cwd})))}},{name:"goal",args:"[目标 | clear]",desc:"设定目标(每轮注入,模型停止前自检是否达成;clear 清除)",run(n,e){const t=String(e||"").trim();if(t){if("clear"===t)return n.config.goal="",void n.print("已清除目标。");n.config.goal=t,n.print("已设定目标:"+t+"(每轮注入,模型将在停止前自检是否达成)")}else n.print(n.config.goal?"当前目标:"+n.config.goal+"\n /goal <新目标> 修改 · /goal clear 清除":"未设定目标。用法:/goal <目标>(模型会在停止前自检是否达成)")}},{name:"btw",args:"<问题>",desc:"问一个旁支问题(带当前上下文,但不写入对话、不调用工具)",async run(n,e){const t=String(e||"").trim();if(!t)return void n.print("用法:/btw <问题>(快速旁支问答,不影响主对话)");const o=K.test(String(n.config.model||""))?n.config.compactModel||s:n.config.model;try{const e=h(n.session.messages,n.session.compaction||{}),i=await d({baseUrl:n.config.baseUrl,apiKey:n.config.apiKey,model:o,messages:[...e,{role:"user",content:"(这是一个旁支问题,请结合上文直接简答,不要调用工具、不要展开无关内容)\n"+t}]});n.print("↪ "+(i.content||"(无回复)").trim())}catch(e){n.print("旁支问答失败:"+e.message)}}},{name:"ide",desc:"查看 IDE 集成状态(编辑器上下文与内联审阅)",run(n){const e=!!process.env.HUICAI_IDE_PORT,t=process.env.HUICAI_IDE_FILE,o=process.env.HUICAI_IDE_SELECTION,i=process.env.HUICAI_IDE_DIAGNOSTICS,s=["IDE 集成"];s.push(" 内联审阅 "+(e?"已连接(write/edit 交编辑器做 diff 审阅)":"未连接(CLI 直接写盘)")),s.push(" 客户端 "+(n.config.client||"cli")),s.push(" 当前文件 "+(t||"(无)")),s.push(" 选区 "+(o?o.length+" 字":"(无)")),s.push(" 诊断 "+(i?"有(来自 Problems 面板)":"(无)")),e||s.push(" (在 VS Code/IDEA 的 huicai 面板或其启动的终端里运行可启用内联审阅与编辑器上下文)"),n.print(s.join("\n"))}},{name:"branch",args:"[标题]",desc:"把当前对话另存为一个可 --resume 的分支副本(当前会话继续)",run(n,e){const t=n.session;if(!t.messages.some(n=>"system"!==n.role))return void n.print("当前对话为空,无需分支。");const o=u({cwd:n.cwd,model:t.model,messages:t.messages.map(n=>({...n}))});try{o.compaction=JSON.parse(JSON.stringify(t.compaction||{}))}catch{o.compaction={}}o.title=(String(e||"").trim()||t.title||"对话").slice(0,70)+"(分支)",o.lastPromptTokens=t.lastPromptTokens||0;try{f(o),f(t)}catch{}n.print(`已保存分支副本「${o.title}」(${o.id})。\n 当前会话继续;分支可用 huicai --resume 恢复到此刻状态。`)}},{name:"theme",args:"[default|mono]",desc:"输出主题:default 彩色 / mono 无色",run(n,e){const t=String(e||"").trim().toLowerCase();t?["default","mono"].includes(t)?(n.config.theme=t,$(t),i({theme:t}),n.print("主题:"+t+("mono"===t?"(已关闭颜色)":"(彩色)"))):n.print("取值:default | mono"):n.print("当前主题:"+(n.config.theme||"default")+"\n /theme default | mono(终端配色由终端掌控;mono=关闭颜色,便于复制/管道)")}},{name:"color",args:"["+b.join("|")+"]",desc:"设置强调色(提示符/高亮)",run(n,e){const t=String(e||"").trim().toLowerCase();t?k(t)?(n.config.accentColor=t,i({accentColor:t}),n.print("强调色:"+t)):n.print("可选:"+b.join(" / ")):n.print("当前强调色:"+v()+"\n 可选:"+b.join(" / "))}},{name:"tui",args:"[auto|rich|plain]",desc:"交互渲染器偏好(下次启动生效)",run(n,e){const t=String(e||"").trim().toLowerCase();t?["auto","rich","plain"].includes(t)?(n.config.tui=t,i({tui:t}),n.print("渲染器偏好:"+t+"(下次启动生效)")):n.print("取值:auto | rich | plain"):n.print("当前渲染器偏好:"+(n.config.tui||"auto")+"\n auto(TTY 下富 TUI)| rich | plain(下次启动生效;本次可加 --tui/--plain 覆盖)")}},{name:"advisor",args:"[模型id|off]",desc:"设置顾问模型(更强模型):智能体遇难题可 ask_advisor 咨询第二意见",async run(n,e){const t=String(e||"").trim();if(!t)return void n.print(n.config.advisorModel?"当前顾问模型:"+n.config.advisorModel+"(智能体可用 ask_advisor 咨询)":"未设置顾问模型。用法:/advisor <模型id>(设一个更强的模型;/models 看可用)· /advisor off 关闭");const o="off"===t?"":t;if(n.config.advisorModel=o,i({advisorModel:o}),Array.isArray(n.extraTools)){const e=n.extraTools.findIndex(n=>"ask_advisor"===n.spec?.function?.name);if(e>=0&&n.extraTools.splice(e,1),o){const{makeAdvisorTool:e}=await import("./advisor.js"),t=e({config:n.config});t&&n.extraTools.push(t)}}n.print(o?"顾问模型已设为 "+o+"(ask_advisor 工具已可用)":"已关闭顾问模型。")}},{name:"automodel",args:"[强模型id|off]",desc:"按任务自动选模型:复杂回合自动升配到该强模型,其余用当前模型",run(n,e){const t=String(e||"").trim();if(!t)return void n.print(n.config.autoModelStrong?"自动升配:开(复杂回合→"+n.config.autoModelStrong+",其余→"+n.config.model+")":"未开启。用法:/automodel <强模型id>(复杂回合自动用它;简单回合仍用当前便宜模型)· /models 看可用");const o="off"===t.toLowerCase()?"":t;n.config.autoModelStrong=o,i({autoModelStrong:o}),n.print(o?"按任务自动选模型:开(复杂回合→"+o+",其余→"+n.config.model+")":"已关闭自动升配。")}},{name:"embed",args:"[嵌入模型id|off]",desc:"search_code 向量语义重排:设一个嵌入模型走 /v1/embeddings 重排(空/off=纯 BM25)",run(n,e){const t=String(e||"").trim();if(!t)return void n.print(n.config.embeddingModel?"search_code 向量重排:开(嵌入模型 "+n.config.embeddingModel+")":"未启用向量重排(纯 BM25,离线可用)。用法:/embed <嵌入模型id>(需平台有该嵌入模型)· /embed off");const o="off"===t.toLowerCase()?"":t;n.config.embeddingModel=o,i({embeddingModel:o}),n.print(o?"search_code 向量语义重排:开(嵌入模型 "+o+";下次检索生效,BM25 召回后按向量相似度重排)":"已关闭向量重排(回到纯 BM25)。")}},{name:"budget",args:"[token数|off|status]",desc:"设置本项目 token 预算(软提醒;/cost 看进度)",run(n,e){const t=String(e||"").trim().toLowerCase(),o=(n.usage?.prompt||0)+(n.usage?.completion||0);if(!t||"status"===t){const e=Number(n.config.budgetTokens)||0;return void n.print(e>0?`token 预算:${o.toLocaleString()} / ${e.toLocaleString()}(${Math.round(o/e*100)}%)`:`未设预算(本次已用 ${o.toLocaleString()} tokens)。/budget <token数> 设定`)}if("off"===t)return n.config.budgetTokens=0,i({budgetTokens:0}),void n.print("已取消预算。");const s=parseInt(t,10);!Number.isFinite(s)||s<1e3?n.print("请给 ≥1000 的 token 数,或 off / status。"):(n.config.budgetTokens=s,i({budgetTokens:s}),n.print("token 预算设为 "+s.toLocaleString()+"(软提醒;/cost 显示进度)。"))}},{name:"debug",args:"[on|off]",desc:"调试输出(每轮打印模型/耗时/token)",run(n,e){const t=String(e||"").trim().toLowerCase(),o="on"===t||"off"!==t&&!n.config.debug;n.config.debug=o,i({debug:o}),n.print("调试输出:"+(o?"开(下一条消息起每轮打印 [debug] 行)":"关"))}},{name:"sandbox",args:"[on|off]",desc:"沙箱模式:禁 shell 与联网(仍可读写本地文件)",run(n,e){const t=String(e||"").trim().toLowerCase(),o="on"===t||"off"!==t&&!n.config.sandbox;n.config.sandbox=o,n.permissions?.setSandbox?.(o),n.print(o?"沙箱模式:开。run_bash 与 fetch_url 将被拒绝;本地文件读写照常。":"沙箱模式:关(写/执行仍按权限审批)。")}},{name:"reload-skills",desc:"从磁盘重新加载技能(新增/改名技能生效;改正文已实时生效)",async run(n){const{makeSkillTools:e,loadLocalSkills:t}=await import("./skills.js");if(Array.isArray(n.extraTools)){for(let e=n.extraTools.length-1;e>=0;e--){const t=n.extraTools[e]?.spec?.function?.name;"load_skill"!==t&&"read_skill_file"!==t||n.extraTools.splice(e,1)}n.extraTools.push(...e(n.cwd))}const o=t(n.cwd);n.print(o.length?`已重载 ${o.length} 个技能:${o.map(n=>n.name).join(", ")}\n (load_skill 现可载入其中任意一个;系统提示词内的技能目录将在下次新会话刷新)`:"未发现本地技能(.huicai/skills 或 ~/.huicai/skills)。")}},{name:"add-dir",args:"[目录 | list | remove <目录>]",desc:"添加可读/搜索的附加目录(写入仍限主目录)",run(t,o){const i=t.session;Array.isArray(i.extraDirs)||(i.extraDirs=[]);const s=String(o||"").trim().split(/\s+/).filter(Boolean),r=(s[0]||"").toLowerCase();if(!o||"list"===r)return void t.print(i.extraDirs.length?"附加目录:\n"+i.extraDirs.map(n=>" "+n).join("\n")+"\n /add-dir <目录> 添加 · /add-dir remove <目录> 移除":"暂无附加目录。用法:/add-dir <目录>(可读/搜索,写仍限主目录)");if("remove"===r||"rm"===r){const n=e.resolve(t.cwd,s.slice(1).join(" ")),o=i.extraDirs.length;i.extraDirs=i.extraDirs.filter(e=>e!==n),H(i);try{f(i)}catch{}return void t.print(i.extraDirs.length<o?"已移除:"+n:"未找到该附加目录:"+n)}const a=e.resolve(t.cwd,String(o).trim());let c=!1;try{c=n.statSync(a).isDirectory()}catch{}if(c)if(a!==t.cwd){i.extraDirs.includes(a)||i.extraDirs.push(a),H(i);try{f(i)}catch{}t.print("已添加可访问目录:"+a+"\n (模型可用 read_file/list_files/grep 以绝对路径访问;写入仍限主目录)")}else t.print("这已是主工作目录。");else t.print("目录不存在:"+a)}},{name:"agents",desc:"列出可用子智能体人格(.huicai/agents/*.md;dispatch_agent 可指定)",async run(n){const{loadAgents:e}=await import("./subagent.js"),t=e(n.cwd);n.print(t.length?"子智能体人格(dispatch_agent 的 agent 参数可指定):\n"+t.map(n=>` ${n.name}${n.description?" — "+n.description.replace(/\s+/g," ").slice(0,60):""}`).join("\n"):"未定义子智能体人格。\n 在 .huicai/agents/<名字>.md 写 frontmatter(name/description)+正文(人格提示词)即可;\n dispatch_agent 默认用通用只读子智能体(无需定义也能用)。")}},{name:"orchestrate",args:"<目标>",desc:"把一个目标自动分解为并行子任务、多智能体协作完成",run(n,e){const t=String(e||"").trim();if(t)return{prompt:`目标:${t}\n\n请作为编排者(只调度不代工)按以下方式完成:\n1) 任务分配预检:先解析目标要什么,把它分解为若干**独立、可并行**的子任务(研究/检索/分析/定位),并确认每个子任务与所派子智能体的能力匹配——超出能力范围的部分如实说明,不硬派;\n2) 用 dispatch_agents 一次性并行委派这些子任务(每个说明必须独立自足——子智能体看不到本对话;可指定合适的子智能体人格);\n3) 信息流经你中转:需要衔接的结论由你提炼后放进下一批子任务的 context,再行派发;\n4) 汇总各子智能体结论形成整体方案或结果,并自查:每部分都有子任务产出支撑、无遗漏、未完成的如实说明;\n5) 若需落地改动,再据此逐步执行并自检(可用 /verify 让我自动跑验证)。\n优先用 dispatch_agents 并行,减少串行等待。`};n.print("用法:/orchestrate <较大的目标>(我会分解为并行子任务,用 dispatch_agents 并行推进后综合)")}},{name:"squads",desc:"列出可用专家团(内置10个+全局/项目自定义;复杂任务也会被自动选团)",async run(n){const{loadSquads:e}=await import("./squad.js"),t=e(n.cwd),o={project:"项目",global:"全局",builtin:"内置"};n.print(t.length?"专家团(/squad <名称> <任务> 交给团队完成;复杂任务对话中也会按「何时使用」自动选团):\n"+t.map(n=>` [${o[n.scope]||"自定义"}] ${n.name} — 主理人${n.leader} · ${n.members.length} 位成员`+(n.when?" · "+n.when.replace(/\s+/g," ").slice(0,46):n.description?" · "+n.description.replace(/\s+/g," ").slice(0,46):"")).join("\n")+"\n(/squad-init <新名> --from <内置团> 以内置团为底稿定制;项目/全局同名可覆盖内置)":"未定义专家团。\n /squad-init <名称> 生成模板(.huicai/squads/<名称>.md):成员表+任务预检+预设工作流;\n 平台上的专家团可直接 --model squad-<分享码> 使用(服务端编排)。")}},{name:"squad",args:"<名称> <任务>",desc:"把任务交给专家团:主理人预检派发、成员协作、汇总交付",async run(n,e){const{loadSquads:t,squadCommandPrompt:o,ensureTeamDir:i}=await import("./squad.js"),s=t(n.cwd),r=String(e||"").trim(),a=r.indexOf(" "),c=a<0?r:r.slice(0,a),d=a<0?"":r.slice(a+1).trim(),m=s.find(n=>n.name===c);if(!r||!m||!d)return void n.print("用法:/squad <名称> <任务>"+(s.length?`\n可用专家团:${s.map(n=>n.name).join("、")}`:"\n还没有专家团定义,先 /squad-init <名称> 生成模板。")+(r&&!m&&c?`\n(未找到「${c}」)`:""));if(!m.members.length)return void n.print(`专家团「${c}」还没有成员,请编辑其定义文件补充「## 成员」。`);const l=i(n.cwd,m.name);return n.print(`已召集专家团「${m.name}」(主理人${m.leader},${m.members.length} 位成员)`+(l?`,团队工作区:${l}`:"")),{prompt:o(m,d,l)}}},{name:"squad-init",args:"[名称] [--from <内置团>]",desc:"生成专家团定义(空白模板,或以内置团为底稿定制)",async run(n,e){const t=await import("node:fs"),o=await import("node:path"),{SQUAD_TEMPLATE:i,loadSquads:s}=await import("./squad.js"),r=String(e||"").trim().split(/\s+/).filter(Boolean),a=r.indexOf("--from"),c=a>=0?r[a+1]:null,d=(a>=0?r.filter((n,e)=>e!==a&&e!==a+1):r)[0];let m,l;if(c){const e=s(n.cwd).find(n=>n.name===c);if(!e||!e.file)return void n.print(`未找到底稿团「${c}」(/squads 查看可用团名)`);l=(d||c).replace(/[^\w.\-一-龥]/g,"-"),m=t.readFileSync(e.file,"utf8").replace(/^(---[\s\S]*?\bname:\s*).*$/m,`$1${l}`)}else l=(d||"my-team").replace(/[^\w.\-一-龥]/g,"-"),m=i.replace("{NAME}",l);const p=o.join(n.cwd,".huicai","squads"),f=o.join(p,`${l}.md`);t.existsSync(f)?n.print(`已存在:${f}(直接编辑即可)`):(t.mkdirSync(p,{recursive:!0}),t.writeFileSync(f,m),n.print(`已生成专家团定义:${f}${c?`(底稿:${c}${l===c?",项目层将覆盖内置同名团":""})`:""}\n 编辑成员表(执行者可指 agent:本地人格 / platform:平台伪模型 / platform:auto:类型 自动绑定 / model:模型名)、成员人设、预检速查表与预设工作流后,/squad `+l+" <任务> 即可开工。"))}},{name:"experts",args:"[refresh|on|off]",desc:"平台能力自动编队:查看/刷新可用的专家团·工作流·智能体·数字人",async run(n,e){const{discoverExperts:t,makeExpertTools:o}=await import("./experts.js"),i=String(e||"").trim().toLowerCase(),s=n=>["consult_expert","consult_experts"].includes(n?.spec?.function?.name);if("off"===i){if(n.config.autoExperts=!1,Array.isArray(n.extraTools))for(let e=n.extraTools.length-1;e>=0;e--)s(n.extraTools[e])&&n.extraTools.splice(e,1);return void n.print('已关闭平台能力自动编队(本会话即时生效)。永久关闭:在 ~/.huicai/config.json 中设 "autoExperts": false')}if("on"===i){if(n.config.autoExperts=!0,Array.isArray(n.extraTools)&&!n.extraTools.some(s)){const e=await o({config:n.config});n.extraTools.push(...e),n.print(e.length?"已开启平台能力自动编队(本会话即时生效)。":"已开启,但当前无可编队的平台专家(需登录且账号下有可用资源,或当前驱动模型为伪模型)。")}else n.print("平台能力自动编队已是开启状态。");return}const r=await t(n.config,{force:"refresh"===i});if(!r.length)return void n.print("当前无可编队的平台专家。"+(n.config.apiKey?"(账号下暂无可用的专家团/工作流/智能体/数字人,或平台不可达)":"请先 huicai login 登录平台。"));const a={squad:"专家团",workflow:"工作流",agent:"智能体",datahuman:"数字人"},c=["平台能力编队(原始模型对话时可被自动咨询"+(!1===n.config.autoExperts?",当前已关闭,/experts on 开启":"")+"):"];for(const n of["squad","workflow","agent","datahuman"]){const e=r.filter(e=>e.type===n);if(e.length){c.push(` ${a[n]}(${e.length})`);for(const n of e)c.push(` ${n.id} — ${n.name}${n.description?":"+n.description.slice(0,50):""}`)}}c.push("refresh"===i?"(已强制刷新)":"(/experts refresh 强制刷新;/experts off 关闭)"),n.print(c.join("\n"))}},{name:"batch",args:"<大改动目标>",desc:"把一个大改动拆成独立子任务,在隔离 git worktree 中并行推进",run(n,e){const t=String(e||"").trim();if(t)return{prompt:`目标(需拆分并行落地):${t}\n\n请这样做:\n1) 先把目标拆成若干**相互独立、可并行**的改动子任务(尽量互不冲突的文件/模块);\n2) 对每个子任务,用 \`git worktree add -b huicai-batch/<名> <路径>\` 建隔离工作区,在其中完成该子任务的改动并 commit(不影响主工作区);\n3) 全部完成后,汇总每个 worktree 的分支与改动;需要时可对每个分支执行 \`huicai pr\` 开 PR。\n(并行调研可用 dispatch_agents;改动务必隔离在各自 worktree,避免互相踩踏。完成后可 \`git worktree remove\` 清理。)`};n.print("用法:/batch <一个可拆分的大改动目标>(拆成互不冲突的子任务,各在隔离 worktree 中落地)")}},{name:"fork",args:"<后台任务提示词>",desc:"派生后台智能体(继承当前对话副本,--yolo 自动放行,输出写日志)",async run(n,e){const t=String(e||"").trim();if(!t)return void n.print("用法:/fork <让后台智能体做什么>(以当前对话副本为上下文,后台运行,--yolo 自动放行)");const o=u({cwd:n.cwd,model:n.session.model,messages:n.session.messages.map(n=>({...n}))});try{o.compaction=JSON.parse(JSON.stringify(n.session.compaction||{}))}catch{o.compaction={}}o.title="(fork) "+(n.session.title||"");try{f(o)}catch{}const{forkAgent:i}=await import("./forks.js");let s;try{s=i({prompt:t,cwd:n.cwd,sessionId:o.id})}catch(e){return void n.print("无法派生后台任务:"+e.message)}n.print(`已派生后台智能体 pid ${s.pid}(--yolo 自动放行)\n 上下文:当前对话副本 ${o.id}\n 日志:${s.log}\n /tasks 查看 · /tasks kill ${s.pid} 终止`)}},{name:"tasks",args:"[kill <pid>]",desc:"查看/终止本会话派生的后台任务(/fork)",async run(n,e){const{listForks:t,killFork:o}=await import("./forks.js"),i=String(e||"").trim().split(/\s+/).filter(Boolean);if("kill"===i[0]){const e=Number(i[1]);if(!Number.isFinite(e))return void n.print("用法:/tasks kill <pid>");const t=o(e);return void n.print(t.ok?"已终止 pid "+e:"终止失败:"+(t.reason||""))}const s=t();s.length?n.print("后台任务:\n"+s.map(n=>` pid ${n.pid} · ${n.alive?"运行中":"已结束"} · ${l(n.started)} · ${n.prompt}\n 日志 ${n.log}`).join("\n")+"\n /tasks kill <pid> 终止"):n.print("本会话暂无后台任务(/fork <提示词> 派生)。")}},{name:"schedule",args:'[list | add "<cron>" "<提示词>" | remove <id>]',desc:"管理定时任务(生成 crontab 行交系统 cron 运行)",async run(n,e){const t=await import("./schedule.js"),o=String(e||"").trim(),i="huicai";if(!o||"list"===o){const e=t.listTasks();return e.length?void n.print("定时任务:\n"+e.map(n=>` #${n.id} ${n.cron} ${n.prompt.replace(/\s+/g," ").slice(0,40)}\n crontab: ${t.crontabLine(n,i)}`).join("\n")+"\n /schedule remove <id> 删除"):void n.print('暂无定时任务。\n 添加:/schedule add "*/30 * * * *" "跑测试并把结果写入 report.md"\n (随后把打印的 crontab 行加入 crontab -e)')}if(/^add\b/.test(o)){const e=o.match(/"([^"]*)"/g);if(!e||e.length<2)return void n.print('用法:/schedule add "<cron>" "<提示词>"(cron 与提示词都要加引号)');const s=e[0].slice(1,-1).trim(),r=e[1].slice(1,-1).trim();if(5!==s.split(/\s+/).length)return void n.print('cron 需 5 个字段,如 "*/30 * * * *"(分 时 日 月 周)');const{id:a}=t.addTask({cron:s,prompt:r,cwd:n.cwd});return void n.print("已添加定时任务 #"+a+"\n 加入 crontab(crontab -e):\n "+t.crontabLine({id:a,cron:s,prompt:r,cwd:n.cwd},i))}if(/^(remove|rm)\b/.test(o)){const e=o.split(/\s+/)[1];return void n.print(t.removeTask(e)?"已删除定时任务 "+e:"未找到定时任务 "+e)}n.print('用法:/schedule list · /schedule add "<cron>" "<提示词>" · /schedule remove <id>')}},{name:"cd",args:"<目录>",desc:"切换工作目录(在新目录开启新会话;IDE 中请用「打开文件夹」)",run(t,o){const i=String(o||"").trim();if(!i)return void t.print("当前工作目录:"+t.cwd+"\n 用法:/cd <目录>(将在新目录开启新会话,旧会话已保存)");const s=e.resolve(t.cwd,i);let r=!1;try{r=n.statSync(s).isDirectory()}catch{}if(r){if(s!==t.cwd)return{action:"cd",cwd:s};t.print("已在该目录。")}else t.print("目录不存在:"+s)}},{name:"loop",args:"<秒> <提示词> | off",desc:"每隔 N 秒自动重跑一个提示词(off 停止)",run(n,e){const t=String(e||"").trim();if(!t)return void n.print("用法:/loop <秒> <提示词>(如 /loop 300 检查构建是否通过)· /loop off 停止");if("off"===t)return{action:"loop",stop:!0};const o=/^(\d+)\s+([\s\S]+)$/.exec(t);if(o)return{action:"loop",seconds:Math.max(10,parseInt(o[1],10)),prompt:o[2].trim()};n.print("用法:/loop <秒> <提示词> · /loop off")}},{name:"statusline",args:"<shell命令> | off",desc:"自定义 TUI 底栏(运行该命令,展示其 stdout)",run(n,e){const t=String(e||"").trim();if(!t)return void n.print("当前状态栏命令:"+(n.config.statusLine||"(默认)")+"\n /statusline <shell命令>(stdout 显示在 TUI 底栏)· /statusline off 恢复默认");const o="off"===t?"":t;n.config.statusLine=o,i({statusLine:o}),n.print(o?"状态栏命令已设置(TUI 底栏生效):"+o:"已恢复默认状态栏。")}},{name:"background",desc:"保存当前会话并给出在别处继续/后台运行的方式",run(n){try{f(n.session)}catch{}n.print(`会话已保存(${n.session.id})。\n · 在别处继续:huicai resume ${n.session.id}\n · 后台跑一个任务:/fork <提示词>(派生后台智能体,日志可查)\n · 定时运行:/schedule add "<cron>" "<提示词>"`)}},{name:"exit",desc:"退出",run:()=>({action:"exit"})}];export function listCommands(n){return[...J.map(n=>({name:n.name,args:n.args||"",desc:n.desc})),...loadCustomCommands(n).map(n=>({name:n.name,args:"",desc:n.desc}))]}export function loadCustomCommands(o){const i=[o?e.join(o,".huicai","commands"):null,e.join(t.homedir(),".huicai","commands")].filter(Boolean),s=new Map;for(const t of i){let o;try{o=n.readdirSync(t).filter(n=>n.endsWith(".md"))}catch{continue}for(const i of o){const o=i.replace(/\.md$/,"").trim();if(o&&!s.has(o)&&!J.some(n=>n.name===o))try{const r=n.readFileSync(e.join(t,i),"utf8"),{desc:a,body:c}=Y(r);s.set(o,{name:o,desc:a||"自定义命令",template:c})}catch{}}}return[...s.values()]}function Y(n){const e=/^---\n([\s\S]*?)\n---\n?/.exec(n);if(!e)return{desc:"",body:n.trim()};const t=e[1],o=/^description:\s*(.+)$/m.exec(t);return{desc:o?o[1].trim():"",body:n.slice(e[0].length).trim()}}export async function runSlash(n,e){const t=n.slice(1).split(/\s+/),o=(t[0]||"").trim(),i=t.slice(1).join(" ").trim();if("quit"===o)return{handled:!0,action:"exit"};const s=J.find(n=>n.name===o);if(s){return{handled:!0,...await s.run(e,i)||{}}}const r=loadCustomCommands(e.cwd).find(n=>n.name===o);if(r){let n=r.template;return n.includes("$ARGUMENTS")?n=n.replaceAll("$ARGUMENTS",i):i&&(n=n+"\n\n"+i),{handled:!0,prompt:n}}return e.print("未知命令。/help 查看可用命令。"),{handled:!0}}
package/src/compaction.js CHANGED
@@ -1 +1 @@
1
- import{streamChat as t}from"./api.js";import{DEFAULT_MODEL as e}from"./config.js";import{stripMarkup as n}from"./markup-filter.js";export function buildModelMessages(t,e){return e&&e.summary&&e.upTo>1&&!(e.upTo>t.length)?[...t.slice(0,1),{role:"user",content:"【上下文压缩】以下是此前多轮对话与工具执行的摘要(原始对话仍完整保留在会话中,仅为节省上下文而摘要):\n"+e.summary},...t.slice(e.upTo)]:t}export async function compactIfNeeded(e,o,s={}){const c=s.state||{},l=s.keepRecent||3;if(!e||e.length<6)return 0;const u=Number(o?.autoCompactTokens);if(!s.force&&Number.isFinite(u)&&u<0)return 0;const i=u>0?u:8e4,a=s.thresholdChars||(u>0?3*u:48e3),m=buildModelMessages(e,c);if(!s.force){const t=Number(s.lastPromptTokens)>i,e=function(t){let e=0;for(const n of t)n.content&&(e+=String(n.content).length),n.tool_calls&&(e+=JSON.stringify(n.tool_calls).length);return e}(m)>a;if(!t&&!e)return 0}const f=Math.max(Number(c.upTo)||1,1),p=[];for(let t=f;t<e.length;t++)"user"===e[t].role&&p.push(t);if(p.length<=l)return 0;const h=p[p.length-l];if(h-f<2)return 0;const g=e.slice(f,h);let d;try{d=await async function(e,o,s){const c=[];s&&s.trim()&&c.push("【先前的摘要(需并入新摘要)】\n"+s.trim());c.push(function(t){const e=[];for(const n of t)if("assistant"===n.role&&n.tool_calls?.length){const t=n.tool_calls.map(t=>`${t.function?.name}(${String(t.function?.arguments||"").slice(0,200)})`).join(" ");e.push(`[助手调用工具] ${t}${n.content?" | "+n.content:""}`)}else"tool"===n.role?e.push(`[工具结果] ${String(n.content||"").slice(0,800)}`):n.content&&e.push(`[${n.role}] ${n.content}`);return e.join("\n")}(e));const l=await t({baseUrl:o.baseUrl,apiKey:o.apiKey,model:r(o),messages:[{role:"system",content:"你是对话/执行记录压缩器。把下面的记录(含可能存在的先前摘要)压缩成一份简洁但信息完整的中文摘要:保留已完成的关键步骤、工具得到的重要事实/数据/结论、涉及的文件路径、尚未解决的问题与下一步线索。只依据记录、不要臆造、不要寒暄。"},{role:"user",content:c.join("\n\n")}]});return n(l.content).text}(g,o,c.summary)}catch{return 0}return d&&d.trim()?(c.summary=d.trim(),c.upTo=h,h-f):0}export function compactNow(t,e,n={}){return compactIfNeeded(t,e,{...n,force:!0})}const o=/^(agent|workflow|datahuman)-/;function r(t){if(t?.compactModel)return t.compactModel;const n=t?.model||"";return o.test(n)?e:n}
1
+ import{streamChat as t}from"./api.js";import{DEFAULT_MODEL as e}from"./config.js";import{stripMarkup as n}from"./markup-filter.js";export function buildModelMessages(t,e){return e&&e.summary&&e.upTo>1&&!(e.upTo>t.length)?[...t.slice(0,1),{role:"user",content:"【上下文压缩】以下是此前多轮对话与工具执行的摘要(原始对话仍完整保留在会话中,仅为节省上下文而摘要):\n"+e.summary},...t.slice(e.upTo)]:t}export async function compactIfNeeded(e,o,s={}){const c=s.state||{},u=s.keepRecent||3;if(!e||e.length<6)return 0;const l=Number(o?.autoCompactTokens);if(!s.force&&Number.isFinite(l)&&l<0)return 0;const i=l>0?l:8e4,a=s.thresholdChars||(l>0?3*l:48e3),m=buildModelMessages(e,c);if(!s.force){const t=Number(s.lastPromptTokens)>i,e=function(t){let e=0;for(const n of t)n.content&&(e+=String(n.content).length),n.tool_calls&&(e+=JSON.stringify(n.tool_calls).length);return e}(m)>a;if(!t&&!e)return 0}const f=Math.max(Number(c.upTo)||1,1),p=[];for(let t=f;t<e.length;t++)"user"===e[t].role&&p.push(t);if(p.length<=u)return 0;const h=p[p.length-u];if(h-f<2)return 0;const g=e.slice(f,h);let d;try{d=await async function(e,o,s){const c=[];s&&s.trim()&&c.push("【先前的摘要(需并入新摘要)】\n"+s.trim());c.push(function(t){const e=[];for(const n of t)if("assistant"===n.role&&n.tool_calls?.length){const t=n.tool_calls.map(t=>`${t.function?.name}(${String(t.function?.arguments||"").slice(0,200)})`).join(" ");e.push(`[助手调用工具] ${t}${n.content?" | "+n.content:""}`)}else"tool"===n.role?e.push(`[工具结果] ${String(n.content||"").slice(0,800)}`):n.content&&e.push(`[${n.role}] ${n.content}`);return e.join("\n")}(e));const u=await t({baseUrl:o.baseUrl,apiKey:o.apiKey,model:r(o),messages:[{role:"system",content:"你是对话/执行记录压缩器。把下面的记录(含可能存在的先前摘要)压缩成一份简洁但信息完整的中文摘要:保留已完成的关键步骤、工具得到的重要事实/数据/结论、涉及的文件路径、尚未解决的问题与下一步线索。只依据记录、不要臆造、不要寒暄。"},{role:"user",content:c.join("\n\n")}]});return n(u.content).text}(g,o,c.summary)}catch{return 0}return d&&d.trim()?(c.summary=d.trim(),c.upTo=h,h-f):0}export function compactNow(t,e,n={}){return compactIfNeeded(t,e,{...n,force:!0})}const o=/^(agent|workflow|datahuman|squad)-/;function r(t){if(t?.compactModel)return t.compactModel;const n=t?.model||"";return o.test(n)?e:n}
package/src/consult.js CHANGED
@@ -1 +1 @@
1
- import{streamChat as t}from"./api.js";const r=/^(agent|workflow|datahuman)-/;export function makeConsultTool({config:e}){const n=e&&e.consult;if(!n||!r.test(String(n)))return null;const o=function(t){return t.startsWith("workflow-")?"工作流":t.startsWith("datahuman-")?"业务数字人":"智能体"}(String(n));return{readOnly:!0,spec:{type:"function",function:{name:"consult_platform",description:`咨询平台${o}「${e.consultLabel||n}」:由平台侧编排作答(带其服务端知识库 RAG / 工具 / 联网检索)。遇到它擅长领域的问题优先调用它获取权威答复,再用本机工具把结论落地到项目。可多次调用。`,parameters:{type:"object",properties:{query:{type:"string",description:"自然语言的咨询 / 查询 / 运行输入"}},required:["query"]}}},run:async r=>{const o=String(r&&r.query||"").trim();if(!o)return"需要提供 query(要咨询平台的自然语言问题 / 输入)";try{return(await t({baseUrl:e.baseUrl,apiKey:e.apiKey,model:n,messages:[{role:"user",content:o}],client:e.client,sessionId:e.sessionId},{})).content||"(平台无返回)"}catch(t){if(t&&"AbortError"===t.name)throw t;return`平台调用失败:${t.message}`}}}}
1
+ import{streamChat as t}from"./api.js";const r=/^(agent|workflow|datahuman|squad)-/;export function makeConsultTool({config:e}){const n=e&&e.consult;if(!n||!r.test(String(n)))return null;const s=function(t){return t.startsWith("workflow-")?"工作流":t.startsWith("datahuman-")?"业务数字人":t.startsWith("squad-")?"专家团":"智能体"}(String(n));return{readOnly:!0,spec:{type:"function",function:{name:"consult_platform",description:`咨询平台${s}「${e.consultLabel||n}」:由平台侧编排作答(带其服务端知识库 RAG / 工具 / 联网检索)。遇到它擅长领域的问题优先调用它获取权威答复,再用本机工具把结论落地到项目。可多次调用。`,parameters:{type:"object",properties:{query:{type:"string",description:"自然语言的咨询 / 查询 / 运行输入"}},required:["query"]}}},run:async r=>{const s=String(r&&r.query||"").trim();if(!s)return"需要提供 query(要咨询平台的自然语言问题 / 输入)";try{return(await t({baseUrl:e.baseUrl,apiKey:e.apiKey,model:n,messages:[{role:"user",content:s}],client:e.client,sessionId:e.sessionId},{})).content||"(平台无返回)"}catch(t){if(t&&"AbortError"===t.name)throw t;return`平台调用失败:${t.message}`}}}}
package/src/experts.js ADDED
@@ -0,0 +1 @@
1
+ import e from"node:fs";import t from"node:path";import n from"node:os";import r from"node:crypto";import{listModels as i,streamChat as a}from"./api.js";const s=6e5,o=/^(agent|workflow|datahuman|squad)-/,c={squad:0,workflow:1,agent:2,datahuman:3},p={squad:"专家团",workflow:"工作流",agent:"智能体",datahuman:"数字人"};function u(e){if(e.hc_type&&p[e.hc_type])return e.hc_type;const t=/^(agent|workflow|datahuman|squad)-/.exec(String(e.id||""));return t?t[1]:"agent"}export function expertsCachePath(e){const i=r.createHash("sha1").update(String(e.baseUrl||"")+"|"+String(e.apiKey||"").slice(-6)).digest("hex").slice(0,12);return t.join(n.homedir(),".huicai","cache",`experts-${i}.json`)}function l(t){try{const n=JSON.parse(e.readFileSync(t,"utf8"));if(Array.isArray(n?.experts))return{fetchedAt:n.fetchedAt||0,experts:n.experts}}catch{}return null}function y(n,r){try{e.mkdirSync(t.dirname(n),{recursive:!0}),e.writeFileSync(n,JSON.stringify({fetchedAt:Date.now(),experts:r}))}catch{}}async function d(e){return(await i(e.baseUrl,e.apiKey)).filter(e=>o.test(String(e.id||""))).map(e=>({id:e.id,name:e.display_name||e.id,description:String(e.description||"").replace(/\s+/g," ").trim(),type:u(e)})).sort((e,t)=>(c[e.type]??9)-(c[t.type]??9)||String(e.name).localeCompare(t.name,"zh"))}const m=new Set;export async function discoverExperts(e,{force:t=!1}={}){if(!e||!e.baseUrl||!e.apiKey)return[];const n=expertsCachePath(e),r=t?null:l(n);if(r)return Date.now()-r.fetchedAt>6e5&&function(e,t){m.has(t)||(m.add(t),d(e).then(e=>y(t,e)).catch(()=>{}).finally(()=>m.delete(t)))}(e,n),r.experts;try{const r=await(i=d(e),a=t?15e3:3500,new Promise((e,t)=>{const n=setTimeout(()=>t(new Error(`发现超时(${a}ms)`)),a);i.then(t=>{clearTimeout(n),e(t)},e=>{clearTimeout(n),t(e)})}));return y(n,r),r}catch{const e=l(n);return e?e.experts:[]}var i,a}export async function makeExpertTools({config:e}){if(!e||!e.baseUrl||!e.apiKey)return[];if(!1===e.autoExperts)return[];if(o.test(String(e.model||"")))return[];const t=await discoverExperts(e);if(!t.length)return[];const n=new Map(t.map(e=>[e.id,e])),r=function(e){const t=e.slice(0,24),n=t.map(e=>`${e.id} — [${p[e.type]||e.type}]${e.name}${e.description?":"+e.description.slice(0,80):""}`);return e.length>t.length&&n.push(`…另有 ${e.length-t.length} 个(/experts 查看全部)`),n.join("\n")}(t),i={fails:0,openUntil:0},c=()=>Date.now()<i.openUntil?"平台专家暂时不可用(连续失败已熔断,稍后自动恢复)。请先用本地工具与自身知识完成任务,稍后可重试咨询。":null,u=async(r,o,c)=>{const u=String(r||"").trim(),l=n.get(u)||t.find(e=>e.name===u);if(!l)return`未找到平台专家「${u}」,请从目录中选择(/experts 可查看全部)`;if(!String(o||"").trim())return"缺少 query(要咨询/交办的自然语言内容)";try{const t=await async function(e,t,n,r){const i=r&&String(r).trim()?`${n}\n\n【背景/上游材料】\n${r}`:String(n);return(await a({baseUrl:e.baseUrl,apiKey:e.apiKey,model:t,messages:[{role:"user",content:i}],client:e.client,sessionId:e.sessionId},{},AbortSignal.timeout(s))).content||"(平台无返回)"}(e,l.id,o,c);return i.fails=0,`平台${p[l.type]||""}「${l.name}」的答复:\n${t}`}catch(e){const t=e&&"AbortError"===e.name?`超过 ${Math.round(10)} 分钟未完成,已中止`:e.message;return i.fails++,i.fails>=3&&(i.openUntil=Date.now()+3e4),`咨询「${l.name}」失败:${t}${i.openUntil>Date.now()?"(平台已熔断,请先用本地能力完成)":""}`}};return[{readOnly:!0,spec:{type:"function",function:{name:"consult_expert",description:"咨询/交办给一个平台专家(当前账号可用的专家团/工作流/智能体/数字人,服务端编排:知识库RAG/工具/多智能体协作/NL→SQL)。遇到专家擅长领域的问题优先交给它产出权威结论,再用本机工具把结论落地到项目。可多次调用(多个独立咨询会自动并行)。\n可用专家目录:\n"+r,parameters:{type:"object",properties:{expert:{type:"string",description:"专家 id(目录第一列)",...t.length<=24?{enum:t.map(e=>e.id)}:{}},query:{type:"string",description:"自然语言的咨询/任务输入(专家看不到本对话,需自足)"},context:{type:"string",description:"可选:完成该咨询所需的背景/上游材料"}},required:["expert","query"]}}},run:async e=>c()||u(e?.expert,e?.query,e?.context)},{readOnly:!0,spec:{type:"function",function:{name:"consult_experts",description:"一次性把多个**独立**的咨询/子任务并行交给多个平台专家(同一专家也可多份),聚合各自答复。适合跨领域任务:如「专家团出方案 + 数字人查数据 + 智能体审内容」同时推进,显著减少串行等待。",parameters:{type:"object",properties:{tasks:{type:"array",description:"并行咨询数组(建议 2-6 个,上限 8)",items:{type:"object",properties:{expert:{type:"string",description:"专家 id(见 consult_expert 目录)"},query:{type:"string",description:"该专家的自足任务/咨询"},context:{type:"string",description:"可选背景"}},required:["expert","query"]}}},required:["tasks"]}}},run:async e=>{const t=c();if(t)return t;const n=Array.isArray(e?.tasks)?e.tasks.slice(0,8):[];if(!n.length)return"未提供 tasks(需要一个咨询数组)";return(await async function(e,t,n){const r=new Array(e.length);let i=0;return await Promise.all(Array.from({length:Math.max(1,Math.min(t,e.length))},async()=>{for(;i<e.length;){const t=i++;r[t]=await n(e[t],t)}})),r}(n,4,async e=>({expert:e?.expert,out:await u(e?.expert,e?.query,e?.context)}))).map((e,t)=>`【咨询 ${t+1} · ${e.expert}】\n${e.out}`).join("\n\n---\n\n")}}]}
package/src/gitflow.js CHANGED
@@ -1 +1 @@
1
- import t from"node:fs";import e from"node:os";import r from"node:path";import{execSync as o}from"node:child_process";import{streamChat as n}from"./api.js";import{DEFAULT_MODEL as i}from"./config.js";const c=/^(agent|workflow|datahuman)-/,a=new Set(["main","master","develop","dev"]);function s(t,e){return o("git "+e,{cwd:t,encoding:"utf8",stdio:["ignore","pipe","pipe"]})}function m(t,e){try{return s(t,e)}catch{return""}}export function gitInfo(t){const e={isRepo:!1,branch:"",dirty:!1,hasRemote:!1,hasGh:!1};try{e.isRepo="true"===s(t,"rev-parse --is-inside-work-tree").trim()}catch{return e}e.branch=m(t,"rev-parse --abbrev-ref HEAD").trim(),e.dirty=m(t,"status --porcelain").trim().length>0,e.hasRemote=m(t,"remote").trim().length>0;try{o("gh --version",{stdio:"ignore"}),e.hasGh=!0}catch{}return e}export async function generateCommitMessage(t,e){let r=m(t,"diff --staged");if(r.trim()||(r=m(t,"diff")),r=r.slice(0,12e3),!r.trim())return"chore: update";const o=c.test(String(e.model||""))?e.compactModel||i:e.model;try{const t=await n({baseUrl:e.baseUrl,apiKey:e.apiKey,model:o,messages:[{role:"system",content:"你是提交信息生成器。根据 git diff 生成一条规范的 Conventional Commits 提交信息:第一行 `<type>(<scope>): <简述>`(简述≤50字,type 用 feat/fix/refactor/docs/test/chore/perf/style 之一);改动较大可空行后加 2-4 条正文要点。只输出提交信息本身,不要解释、不要代码块围栏。"},{role:"user",content:r}]});return(t.content||"").trim().replace(/^```[a-z]*\n?|\n?```$/g,"").trim()||"chore: update"}catch{return"chore: update"}}export async function runPrFlow({cwd:n,config:i,emit:c=()=>{},push:p=!0,openPr:h=!0}){const d=gitInfo(n);if(!d.isRepo)return{ok:!1,error:"当前目录不是 git 仓库"};const u=a.has(d.branch);d.dirty&&(s(n,"add -A"),c("已暂存全部改动"));const f=m(n,"diff --staged --stat").trim();let g="";f&&(c("正在生成提交信息…"),g=await generateCommitMessage(n,i),c("提交信息:"+g.split("\n")[0]));let l=d.branch;u&&(l="huicai/"+function(t){return String(t).split("\n")[0].replace(/^[a-z]+(\([^)]*\))?!?:\s*/i,"").replace(/[^a-zA-Z0-9一-鿿]+/g,"-").replace(/^-+|-+$/g,"").toLowerCase().slice(0,32)||"change"}(g||"change")+"-"+Date.now().toString(36).slice(-4),s(n,`checkout -b ${l}`),c("已从 "+d.branch+" 创建分支 "+l));let y=!1;if(f){const o=r.join(e.tmpdir(),"huicai-commit-"+Date.now()+".txt");t.writeFileSync(o,g);try{s(n,`commit -F "${o}"`),y=!0,c("已提交")}finally{try{t.unlinkSync(o)}catch{}}}else c("无新暂存改动,跳过提交");if(!p)return{ok:!0,branch:l,committed:y};if(!d.hasRemote)return{ok:!0,branch:l,committed:y,note:"无 remote,未推送"};try{s(n,`push -u origin ${l}`),c("已推送 origin/"+l)}catch(t){return{ok:!1,error:"push 失败:"+String(t.message).split("\n")[0],branch:l}}if(!h)return{ok:!0,branch:l,committed:y,pushed:!0};if(!d.hasGh)return{ok:!0,branch:l,committed:y,pushed:!0,note:"未装 gh CLI,PR 请手动开"};try{const t=o("gh pr create --fill",{cwd:n,encoding:"utf8",stdio:["ignore","pipe","pipe"]}).trim();return c("已开 PR:"+t),{ok:!0,branch:l,committed:y,pr:t}}catch(t){return{ok:!0,branch:l,committed:y,pushed:!0,note:"gh pr create 失败(可手动开):"+String(t.message).split("\n")[0]}}}
1
+ import t from"node:fs";import e from"node:os";import r from"node:path";import{execSync as o}from"node:child_process";import{streamChat as n}from"./api.js";import{DEFAULT_MODEL as i}from"./config.js";const c=/^(agent|workflow|datahuman|squad)-/,a=new Set(["main","master","develop","dev"]);function s(t,e){return o("git "+e,{cwd:t,encoding:"utf8",stdio:["ignore","pipe","pipe"]})}function m(t,e){try{return s(t,e)}catch{return""}}export function gitInfo(t){const e={isRepo:!1,branch:"",dirty:!1,hasRemote:!1,hasGh:!1};try{e.isRepo="true"===s(t,"rev-parse --is-inside-work-tree").trim()}catch{return e}e.branch=m(t,"rev-parse --abbrev-ref HEAD").trim(),e.dirty=m(t,"status --porcelain").trim().length>0,e.hasRemote=m(t,"remote").trim().length>0;try{o("gh --version",{stdio:"ignore"}),e.hasGh=!0}catch{}return e}export async function generateCommitMessage(t,e){let r=m(t,"diff --staged");if(r.trim()||(r=m(t,"diff")),r=r.slice(0,12e3),!r.trim())return"chore: update";const o=c.test(String(e.model||""))?e.compactModel||i:e.model;try{const t=await n({baseUrl:e.baseUrl,apiKey:e.apiKey,model:o,messages:[{role:"system",content:"你是提交信息生成器。根据 git diff 生成一条规范的 Conventional Commits 提交信息:第一行 `<type>(<scope>): <简述>`(简述≤50字,type 用 feat/fix/refactor/docs/test/chore/perf/style 之一);改动较大可空行后加 2-4 条正文要点。只输出提交信息本身,不要解释、不要代码块围栏。"},{role:"user",content:r}]});return(t.content||"").trim().replace(/^```[a-z]*\n?|\n?```$/g,"").trim()||"chore: update"}catch{return"chore: update"}}export async function runPrFlow({cwd:n,config:i,emit:c=()=>{},push:p=!0,openPr:h=!0}){const d=gitInfo(n);if(!d.isRepo)return{ok:!1,error:"当前目录不是 git 仓库"};const u=a.has(d.branch);d.dirty&&(s(n,"add -A"),c("已暂存全部改动"));const f=m(n,"diff --staged --stat").trim();let g="";f&&(c("正在生成提交信息…"),g=await generateCommitMessage(n,i),c("提交信息:"+g.split("\n")[0]));let l=d.branch;u&&(l="huicai/"+function(t){return String(t).split("\n")[0].replace(/^[a-z]+(\([^)]*\))?!?:\s*/i,"").replace(/[^a-zA-Z0-9一-鿿]+/g,"-").replace(/^-+|-+$/g,"").toLowerCase().slice(0,32)||"change"}(g||"change")+"-"+Date.now().toString(36).slice(-4),s(n,`checkout -b ${l}`),c("已从 "+d.branch+" 创建分支 "+l));let y=!1;if(f){const o=r.join(e.tmpdir(),"huicai-commit-"+Date.now()+".txt");t.writeFileSync(o,g);try{s(n,`commit -F "${o}"`),y=!0,c("已提交")}finally{try{t.unlinkSync(o)}catch{}}}else c("无新暂存改动,跳过提交");if(!p)return{ok:!0,branch:l,committed:y};if(!d.hasRemote)return{ok:!0,branch:l,committed:y,note:"无 remote,未推送"};try{s(n,`push -u origin ${l}`),c("已推送 origin/"+l)}catch(t){return{ok:!1,error:"push 失败:"+String(t.message).split("\n")[0],branch:l}}if(!h)return{ok:!0,branch:l,committed:y,pushed:!0};if(!d.hasGh)return{ok:!0,branch:l,committed:y,pushed:!0,note:"未装 gh CLI,PR 请手动开"};try{const t=o("gh pr create --fill",{cwd:n,encoding:"utf8",stdio:["ignore","pipe","pipe"]}).trim();return c("已开 PR:"+t),{ok:!0,branch:l,committed:y,pr:t}}catch(t){return{ok:!0,branch:l,committed:y,pushed:!0,note:"gh pr create 失败(可手动开):"+String(t.message).split("\n")[0]}}}
package/src/render.js CHANGED
@@ -1 +1 @@
1
- import{c as t}from"./colors.js";export function toolLabel(t,e={}){switch(t){case"read_file":return`read_file ${e.path??""}`;case"list_files":return`list_files ${e.path??"."}`;case"glob":return`glob ${e.pattern??""}`;case"grep":return`grep ${JSON.stringify(e.pattern??"")}${e.path?" in "+e.path:""}`;case"search_code":return`search_code ${JSON.stringify(e.query??"")}`;case"write_file":return`write_file ${e.path??""}`;case"edit_file":return`edit_file ${e.path??""}`;case"run_bash":return`run_bash: ${String(e.command??"").split("\n")[0]}`;case"fetch_url":return`fetch_url ${e.url??""}`;case"todo_write":return"todo_write(更新任务清单)";case"dispatch_agent":{const t=String(e.objective??"").split("\n")[0];return`dispatch_agent: ${t.length>60?t.slice(0,60)+"…":t}`}case"dispatch_agents":return`dispatch_agents(并行 ${Array.isArray(e.tasks)?e.tasks.length:"?"} 个子任务)`;case"install_packages":return`install_packages ${Array.isArray(e.packages)&&e.packages.length?e.packages.join(" "):"(全部依赖)"}${e.global?" -g":""}${e.dev?" -D":""}`;case"setup_environment":return"setup_environment"+(Array.isArray(e.commands)&&e.commands.length?`(${e.commands.length} 条配置命令)`:"(自动配置运行环境)");case"install_plugin":return`install_plugin ${e.target??""} ${e.id??""}`;case"open_browser":return`open_browser ${e.url??""}`;case"open_path":return`open_path ${e.path??""}`;case"open_app":return`open_app ${e.name??""}${e.target?" "+e.target:""}`;default:return`${t}(${Object.keys(e).join(", ")})`}}export function renderEditDiff(e,n){const r=[];for(const n of String(e??"").split("\n"))r.push(t.red("- "+n));for(const e of String(n??"").split("\n"))r.push(t.green("+ "+e));return r.join("\n")}export function diffLines(t,e){const n=t=>null==t||""===t?[]:String(t).split("\n"),r=n(t),s=n(e),a=r.length,i=s.length;if(a*i>4e6)return[...r.map(t=>({type:"-",text:t})),...s.map(t=>({type:"+",text:t}))];const o=Array.from({length:a+1},()=>new Uint32Array(i+1));for(let t=a-1;t>=0;t--)for(let e=i-1;e>=0;e--)o[t][e]=r[t]===s[e]?o[t+1][e+1]+1:Math.max(o[t+1][e],o[t][e+1]);const p=[];let l=0,c=0;for(;l<a&&c<i;)r[l]===s[c]?(p.push({type:" ",text:r[l]}),l++,c++):o[l+1][c]>=o[l][c+1]?(p.push({type:"-",text:r[l]}),l++):(p.push({type:"+",text:s[c]}),c++);for(;l<a;)p.push({type:"-",text:r[l++]});for(;c<i;)p.push({type:"+",text:s[c++]});return p}export function renderFileDiff(e,n,r={}){const{path:s="",context:a=3,maxLines:i=80}=r,o=diffLines(e,n);let p=0,l=0;for(const t of o)"+"===t.type?p++:"-"===t.type&&l++;if(0===p&&0===l)return"";const c=[];let u=0,h=0;for(const t of o)" "===t.type?(u++,h++,c.push({type:" ",text:t.text,no:u})):"-"===t.type?(u++,c.push({type:"-",text:t.text,no:u})):(h++,c.push({type:"+",text:t.text,no:h}));const g=new Array(c.length).fill(!1);c.forEach((t,e)=>{if(" "!==t.type)for(let t=Math.max(0,e-a);t<=Math.min(c.length-1,e+a);t++)g[t]=!0});const f=String(Math.max(u,h)).length,y=t=>String(null==t?"":t).padStart(f),_=[(s?t.bold(s)+" ":"")+t.green("+"+p)+" "+t.red("−"+l)];let d=!1,$=0,x=!1;for(let e=0;e<c.length;e++){if(!g[e]){d||(_.push(t.gray(" "+" ".repeat(f)+" ⋯")),d=!0);continue}if(d=!1,$>=i){x=!0;break}const n=c[e];"+"===n.type?_.push(t.green(" "+y(n.no)+" + ")+t.green(n.text)):"-"===n.type?_.push(t.red(" "+y(n.no)+" - ")+t.red(n.text)):_.push(t.gray(" "+y(n.no)+" "+n.text)),$++}return x&&_.push(t.gray(` …(差异较大,仅显示前 ${i} 行;共 +${p} −${l})`)),_.join("\n")}export function indent(t,e=" "){return String(t).split("\n").map(t=>e+t).join("\n")}export function summarize(e,n=6){const r=String(e).split("\n");return r.length<=n?e:r.slice(0,n).join("\n")+t.gray(`\n …(共 ${r.length} 行)`)}
1
+ import{c as t}from"./colors.js";export function toolLabel(t,e={}){switch(t){case"read_file":return`read_file ${e.path??""}`;case"list_files":return`list_files ${e.path??"."}`;case"glob":return`glob ${e.pattern??""}`;case"grep":return`grep ${JSON.stringify(e.pattern??"")}${e.path?" in "+e.path:""}`;case"search_code":return`search_code ${JSON.stringify(e.query??"")}`;case"write_file":return`write_file ${e.path??""}`;case"edit_file":return`edit_file ${e.path??""}`;case"run_bash":return`run_bash: ${String(e.command??"").split("\n")[0]}`;case"fetch_url":return`fetch_url ${e.url??""}`;case"todo_write":return"todo_write(更新任务清单)";case"dispatch_agent":{const t=String(e.objective??"").split("\n")[0];return`dispatch_agent: ${t.length>60?t.slice(0,60)+"…":t}`}case"dispatch_agents":return`dispatch_agents(并行 ${Array.isArray(e.tasks)?e.tasks.length:"?"} 个子任务)`;case"dispatch_member":{const t=String(e.task??"").split("\n")[0];return`dispatch_member ${e.squad??""}·${e.member??""}: ${t.length>48?t.slice(0,48)+"…":t}`}case"consult_expert":{const t=String(e.query??"").split("\n")[0];return`consult_expert ${e.expert??""}: ${t.length>48?t.slice(0,48)+"…":t}`}case"consult_experts":return`consult_experts(并行咨询 ${Array.isArray(e.tasks)?e.tasks.length:"?"} 个平台专家)`;case"install_packages":return`install_packages ${Array.isArray(e.packages)&&e.packages.length?e.packages.join(" "):"(全部依赖)"}${e.global?" -g":""}${e.dev?" -D":""}`;case"setup_environment":return"setup_environment"+(Array.isArray(e.commands)&&e.commands.length?`(${e.commands.length} 条配置命令)`:"(自动配置运行环境)");case"install_plugin":return`install_plugin ${e.target??""} ${e.id??""}`;case"open_browser":return`open_browser ${e.url??""}`;case"open_path":return`open_path ${e.path??""}`;case"open_app":return`open_app ${e.name??""}${e.target?" "+e.target:""}`;default:return`${t}(${Object.keys(e).join(", ")})`}}export function renderEditDiff(e,r){const n=[];for(const r of String(e??"").split("\n"))n.push(t.red("- "+r));for(const e of String(r??"").split("\n"))n.push(t.green("+ "+e));return n.join("\n")}export function diffLines(t,e){const r=t=>null==t||""===t?[]:String(t).split("\n"),n=r(t),s=r(e),a=n.length,i=s.length;if(a*i>4e6)return[...n.map(t=>({type:"-",text:t})),...s.map(t=>({type:"+",text:t}))];const p=Array.from({length:a+1},()=>new Uint32Array(i+1));for(let t=a-1;t>=0;t--)for(let e=i-1;e>=0;e--)p[t][e]=n[t]===s[e]?p[t+1][e+1]+1:Math.max(p[t+1][e],p[t][e+1]);const o=[];let l=0,c=0;for(;l<a&&c<i;)n[l]===s[c]?(o.push({type:" ",text:n[l]}),l++,c++):p[l+1][c]>=p[l][c+1]?(o.push({type:"-",text:n[l]}),l++):(o.push({type:"+",text:s[c]}),c++);for(;l<a;)o.push({type:"-",text:n[l++]});for(;c<i;)o.push({type:"+",text:s[c++]});return o}export function renderFileDiff(e,r,n={}){const{path:s="",context:a=3,maxLines:i=80}=n,p=diffLines(e,r);let o=0,l=0;for(const t of p)"+"===t.type?o++:"-"===t.type&&l++;if(0===o&&0===l)return"";const c=[];let u=0,h=0;for(const t of p)" "===t.type?(u++,h++,c.push({type:" ",text:t.text,no:u})):"-"===t.type?(u++,c.push({type:"-",text:t.text,no:u})):(h++,c.push({type:"+",text:t.text,no:h}));const g=new Array(c.length).fill(!1);c.forEach((t,e)=>{if(" "!==t.type)for(let t=Math.max(0,e-a);t<=Math.min(c.length-1,e+a);t++)g[t]=!0});const f=String(Math.max(u,h)).length,_=t=>String(null==t?"":t).padStart(f),y=[(s?t.bold(s)+" ":"")+t.green("+"+o)+" "+t.red("−"+l)];let d=!1,$=0,x=!1;for(let e=0;e<c.length;e++){if(!g[e]){d||(y.push(t.gray(" "+" ".repeat(f)+" ⋯")),d=!0);continue}if(d=!1,$>=i){x=!0;break}const r=c[e];"+"===r.type?y.push(t.green(" "+_(r.no)+" + ")+t.green(r.text)):"-"===r.type?y.push(t.red(" "+_(r.no)+" - ")+t.red(r.text)):y.push(t.gray(" "+_(r.no)+" "+r.text)),$++}return x&&y.push(t.gray(` …(差异较大,仅显示前 ${i} 行;共 +${o} −${l})`)),y.join("\n")}export function indent(t,e=" "){return String(t).split("\n").map(t=>e+t).join("\n")}export function summarize(e,r=6){const n=String(e).split("\n");return n.length<=r?e:n.slice(0,r).join("\n")+t.gray(`\n …(共 ${n.length} 行)`)}
package/src/repl.js CHANGED
@@ -1 +1 @@
1
- import s from"node:readline";import{c as o}from"./colors.js";import{createPermissions as t}from"./permissions.js";import{runTurn as e}from"./agent.js";import{createSession as n,saveSession as r,turnCount as i}from"./session.js";import{createCheckpoints as a}from"./checkpoints.js";import{buildSystemPrompt as c}from"./context.js";import{runVerifyLoop as l}from"./autofix.js";import{runAutoFlow as m}from"./autorun.js";import{loadMcpTools as p,closeMcp as u}from"./mcp.js";import{makeSubagentTool as d,makeDispatchAgentsTool as g}from"./subagent.js";import{makeConsultTool as w}from"./consult.js";import{makeAdvisorTool as f}from"./advisor.js";import{makeRecallTool as y,readMemoryRaw as h}from"./memory.js";import{detectScanners as x,makeScanTool as T}from"./scan.js";import{runSlash as j}from"./commands.js";import{expandAtMentions as k,buildUserContent as b,runBangCommand as I,loadHistory as v,appendHistory as S}from"./input.js";import{makeSkillTools as P}from"./skills.js";import{renderWelcome as E}from"./banner.js";export async function buildExtraTools(s,t){let e={tools:[],clients:[]};try{e=await p()}catch(s){process.stdout.write(o.yellow("MCP 初始化失败(忽略):"+s.message)+"\n")}const n=[...P(t),...e.tools],r=d({config:s,cwd:t,extraTools:e.tools,depth:0});r&&n.push(r);const i=g({config:s,cwd:t,extraTools:e.tools,depth:0});i&&n.push(i);const a=w({config:s});a&&n.push(a);const c=f({config:s});return c&&n.push(c),!1!==s.memoryEnabled&&h(t)&&n.push(y({cwd:t})),x(t).length&&n.push(T({cwd:t})),{extraTools:n,clients:e.clients}}export async function startRepl({config:i,cwd:p,yolo:d,initialPrompt:g,session:w,version:f}){const y=s.createInterface({input:process.stdin,output:process.stdout,history:v(p).reverse(),historySize:200}),h=s=>new Promise(o=>y.question(s,o));let x=t({yolo:d,ask:h,cwd:p});const T={prompt:0,completion:0},P={list:[]},_=a();let O=null,W=null;const q={config:i,cwd:p,permissions:x,usage:T,session:w,version:f};y.on("SIGINT",()=>{O?O.abort():process.stdout.write("\n"+o.gray("输入 /exit 退出。")+"\n")}),function(s,o,t,e){process.stdout.write(E({model:s.model,cwd:o,version:e,platform:/^(agent|workflow|datahuman)-/.test(String(s.model||"")),yolo:t.isYolo(),consult:s.consult?s.consultLabel||s.consult:null})+"\n")}(i,p,x,f),$(w);let{extraTools:C,clients:J}=await buildExtraTools(i,p);const N=async s=>{const{text:t,mentions:n,images:a}=await k(s,p,i);n.length&&process.stdout.write(o.gray(" · 已展开引用:"+n.join("、")+(a.length?`(含 ${a.length} 张图片)`:""))+"\n");const c=_.snapshot({session:q.session,label:s});q.session.messages.push({role:"user",content:b(t,a)}),O=new AbortController,q.session.compaction||(q.session.compaction={}),i.sessionId=q.session.id;const l=await e({config:i,messages:q.session.messages,permissions:x,cwd:p,signal:O.signal,extraTools:C,todoState:P,compaction:q.session.compaction,fileJournal:c});return l?.usage&&(T.prompt+=l.usage.prompt,T.completion+=l.usage.completion),l?.lastPromptTokens&&(q.session.lastPromptTokens=l.lastPromptTokens),O=null,r(q.session),l},Y=()=>({...q,session:q.session,extraTools:C,checkpoints:_,print:s=>process.stdout.write(String(s).endsWith("\n")?String(s):String(s)+"\n")});for(g&&(process.stdout.write(o.cyan("\nhuicai> ")+g+"\n"),await N(g));;){let s;try{s=(await h(o.cyan("\nhuicai> "))).trim()}catch{break}if(""!==s){if(S(p,s),"/exit"===s||"/quit"===s)break;if(s.startsWith("!")&&s.length>1){const t=s.slice(1).trim(),{output:e,record:n}=await I(t,p);process.stdout.write(o.gray(e.split("\n").map(s=>" "+s).join("\n"))+"\n"),q.session.messages.push({role:"user",content:n}),r(q.session);continue}if(s.startsWith("/")){const e=await j(s,Y());if("exit"===e.action)break;"clear"===e.action&&(q.session=n({cwd:p,model:i.model,messages:[q.session.messages[0]]}),P.list=[],_.list.length=0,process.stdout.write(o.gray("已开启新会话(旧会话已保存,可用 huicai --resume 恢复)。\n"))),"refresh"===e.action&&$(q.session),"cd"===e.action&&(p=e.cwd,q.cwd=p,x=t({yolo:x.isYolo(),ask:h,cwd:p}),q.permissions=x,u(J),({extraTools:C,clients:J}=await buildExtraTools(i,p)),q.session=n({cwd:p,model:i.model,messages:[{role:"system",content:c(p,{memory:!1!==i.memoryEnabled})}]}),P.list=[],_.list.length=0,i.sessionId=q.session.id,process.stdout.write(o.gray("已切换到 "+p+"(新会话,旧会话已保存)。\n"))),"autofix"===e.action&&await l({command:e.command,maxIters:e.maxIters,cwd:p,runTurnOnce:s=>N(s),emit:s=>process.stdout.write(o.gray(s)+"\n")}),"autorun"===e.action&&await m({goal:e.goal,config:i,permissions:x,cwd:p,runTurnOnce:s=>N(s),emit:s=>process.stdout.write(o.gray(" · "+s)+"\n")}),"loop"===e.action&&(W&&(clearInterval(W),W=null),e.stop?process.stdout.write(o.gray("已停止循环。\n")):(process.stdout.write(o.gray(`已开始循环:每 ${e.seconds}s 运行「${e.prompt}」(/loop off 停止)。\n`)),W=setInterval(()=>{O||N(e.prompt)},1e3*e.seconds))),e.prompt&&await N(e.prompt);continue}await N(s)}}W&&clearInterval(W),y.close(),u(J),process.stdout.write(o.gray("\n再见。\n"))}function $(s){const t=i(s.messages);if(0===t)return;process.stdout.write(o.gray(` 已恢复会话「${s.title||""}」(${t} 轮)`)+"\n");const e=[...s.messages].reverse().find(s=>"assistant"===s.role&&s.content);if(e){const s=String(e.content).trim(),t=s.length>400?s.slice(0,400)+"…":s;process.stdout.write(o.gray(" 上次回复:")+t.replace(/\n/g,"\n ")+"\n")}}export async function runOnce({config:s,cwd:o,yolo:n,prompt:a,session:c,persist:l,outputFormat:m,maxTurns:p}){a||(process.stderr.write('用法: huicai -p "你的指令"\n'),process.exit(1));const d="json"===m,g=t({yolo:n,ask:null,cwd:o}),{extraTools:w,clients:f}=await buildExtraTools(s,o),{text:y,images:h}=await k(a,o,s);c.messages.push({role:"user",content:b(y,h)}),c.compaction||(c.compaction={}),s.sessionId=c.id;const x=await e({config:s,messages:c.messages,permissions:g,cwd:o,extraTools:w,maxIters:p,compaction:c.compaction,io:d?{write:()=>{}}:void 0});if(x?.lastPromptTokens&&(c.lastPromptTokens=x.lastPromptTokens),l&&r(c),u(f),d){const o=[...c.messages].reverse().find(s=>"assistant"===s.role&&s.content);process.stdout.write(JSON.stringify({type:"result",is_error:!!x?.error,...x?.error?{error:x.error}:{},result:o?String(o.content):"",usage:{input_tokens:x?.usage?.prompt??0,output_tokens:x?.usage?.completion??0},session_id:c.id,model:s.model,num_turns:i(c.messages)})+"\n")}x?.error&&process.exit(1)}
1
+ import s from"node:readline";import{c as o}from"./colors.js";import{createPermissions as t}from"./permissions.js";import{runTurn as e}from"./agent.js";import{createSession as n,saveSession as r,turnCount as i}from"./session.js";import{createCheckpoints as a}from"./checkpoints.js";import{buildSystemPrompt as c}from"./context.js";import{runVerifyLoop as l}from"./autofix.js";import{runAutoFlow as m}from"./autorun.js";import{loadMcpTools as p,closeMcp as u}from"./mcp.js";import{makeSubagentTool as d,makeDispatchAgentsTool as g}from"./subagent.js";import{makeDispatchMemberTool as w}from"./squad.js";import{makeConsultTool as f}from"./consult.js";import{makeExpertTools as h}from"./experts.js";import{makeAdvisorTool as y}from"./advisor.js";import{makeRecallTool as x,readMemoryRaw as T}from"./memory.js";import{detectScanners as j,makeScanTool as k}from"./scan.js";import{runSlash as b}from"./commands.js";import{expandAtMentions as I,buildUserContent as v,runBangCommand as S,loadHistory as P,appendHistory as E}from"./input.js";import{makeSkillTools as $}from"./skills.js";import{renderWelcome as _}from"./banner.js";export async function buildExtraTools(s,t){let e={tools:[],clients:[]};try{e=await p()}catch(s){process.stdout.write(o.yellow("MCP 初始化失败(忽略):"+s.message)+"\n")}const n=[...$(t),...e.tools],r=d({config:s,cwd:t,extraTools:e.tools,depth:0});r&&n.push(r);const i=g({config:s,cwd:t,extraTools:e.tools,depth:0});i&&n.push(i);const a=w({config:s,cwd:t,extraTools:e.tools});a&&n.push(a);const c=f({config:s});c&&n.push(c);try{n.push(...await h({config:s}))}catch{}const l=y({config:s});return l&&n.push(l),!1!==s.memoryEnabled&&T(t)&&n.push(x({cwd:t})),j(t).length&&n.push(k({cwd:t})),{extraTools:n,clients:e.clients}}export async function startRepl({config:i,cwd:p,yolo:d,initialPrompt:g,session:w,version:f}){const h=s.createInterface({input:process.stdin,output:process.stdout,history:P(p).reverse(),historySize:200}),y=s=>new Promise(o=>h.question(s,o));let x=t({yolo:d,ask:y,cwd:p});const T={prompt:0,completion:0},j={list:[]},k=a();let $=null,O=null;const W={config:i,cwd:p,permissions:x,usage:T,session:w,version:f};h.on("SIGINT",()=>{$?$.abort():process.stdout.write("\n"+o.gray("输入 /exit 退出。")+"\n")}),function(s,o,t,e){process.stdout.write(_({model:s.model,cwd:o,version:e,platform:/^(agent|workflow|datahuman|squad)-/.test(String(s.model||"")),yolo:t.isYolo(),consult:s.consult?s.consultLabel||s.consult:null})+"\n")}(i,p,x,f),q(w);let{extraTools:C,clients:J}=await buildExtraTools(i,p);const N=async s=>{const{text:t,mentions:n,images:a}=await I(s,p,i);n.length&&process.stdout.write(o.gray(" · 已展开引用:"+n.join("、")+(a.length?`(含 ${a.length} 张图片)`:""))+"\n");const c=k.snapshot({session:W.session,label:s});W.session.messages.push({role:"user",content:v(t,a)}),$=new AbortController,W.session.compaction||(W.session.compaction={}),i.sessionId=W.session.id;const l=await e({config:i,messages:W.session.messages,permissions:x,cwd:p,signal:$.signal,extraTools:C,todoState:j,compaction:W.session.compaction,fileJournal:c});return l?.usage&&(T.prompt+=l.usage.prompt,T.completion+=l.usage.completion),l?.lastPromptTokens&&(W.session.lastPromptTokens=l.lastPromptTokens),$=null,r(W.session),l},Y=()=>({...W,session:W.session,extraTools:C,checkpoints:k,print:s=>process.stdout.write(String(s).endsWith("\n")?String(s):String(s)+"\n")});for(g&&(process.stdout.write(o.cyan("\nhuicai> ")+g+"\n"),await N(g));;){let s;try{s=(await y(o.cyan("\nhuicai> "))).trim()}catch{break}if(""!==s){if(E(p,s),"/exit"===s||"/quit"===s)break;if(s.startsWith("!")&&s.length>1){const t=s.slice(1).trim(),{output:e,record:n}=await S(t,p);process.stdout.write(o.gray(e.split("\n").map(s=>" "+s).join("\n"))+"\n"),W.session.messages.push({role:"user",content:n}),r(W.session);continue}if(s.startsWith("/")){const e=await b(s,Y());if("exit"===e.action)break;"clear"===e.action&&(W.session=n({cwd:p,model:i.model,messages:[W.session.messages[0]]}),j.list=[],k.list.length=0,process.stdout.write(o.gray("已开启新会话(旧会话已保存,可用 huicai --resume 恢复)。\n"))),"refresh"===e.action&&q(W.session),"cd"===e.action&&(p=e.cwd,W.cwd=p,x=t({yolo:x.isYolo(),ask:y,cwd:p}),W.permissions=x,u(J),({extraTools:C,clients:J}=await buildExtraTools(i,p)),W.session=n({cwd:p,model:i.model,messages:[{role:"system",content:c(p,{memory:!1!==i.memoryEnabled})}]}),j.list=[],k.list.length=0,i.sessionId=W.session.id,process.stdout.write(o.gray("已切换到 "+p+"(新会话,旧会话已保存)。\n"))),"autofix"===e.action&&await l({command:e.command,maxIters:e.maxIters,cwd:p,runTurnOnce:s=>N(s),emit:s=>process.stdout.write(o.gray(s)+"\n")}),"autorun"===e.action&&await m({goal:e.goal,config:i,permissions:x,cwd:p,runTurnOnce:s=>N(s),emit:s=>process.stdout.write(o.gray(" · "+s)+"\n")}),"loop"===e.action&&(O&&(clearInterval(O),O=null),e.stop?process.stdout.write(o.gray("已停止循环。\n")):(process.stdout.write(o.gray(`已开始循环:每 ${e.seconds}s 运行「${e.prompt}」(/loop off 停止)。\n`)),O=setInterval(()=>{$||N(e.prompt)},1e3*e.seconds))),e.prompt&&await N(e.prompt);continue}await N(s)}}O&&clearInterval(O),h.close(),u(J),process.stdout.write(o.gray("\n再见。\n"))}function q(s){const t=i(s.messages);if(0===t)return;process.stdout.write(o.gray(` 已恢复会话「${s.title||""}」(${t} 轮)`)+"\n");const e=[...s.messages].reverse().find(s=>"assistant"===s.role&&s.content);if(e){const s=String(e.content).trim(),t=s.length>400?s.slice(0,400)+"…":s;process.stdout.write(o.gray(" 上次回复:")+t.replace(/\n/g,"\n ")+"\n")}}export async function runOnce({config:s,cwd:o,yolo:n,prompt:a,session:c,persist:l,outputFormat:m,maxTurns:p}){a||(process.stderr.write('用法: huicai -p "你的指令"\n'),process.exit(1));const d="json"===m,g=t({yolo:n,ask:null,cwd:o}),{extraTools:w,clients:f}=await buildExtraTools(s,o),{text:h,images:y}=await I(a,o,s);c.messages.push({role:"user",content:v(h,y)}),c.compaction||(c.compaction={}),s.sessionId=c.id;const x=await e({config:s,messages:c.messages,permissions:g,cwd:o,extraTools:w,maxIters:p,compaction:c.compaction,io:d?{write:()=>{}}:void 0});if(x?.lastPromptTokens&&(c.lastPromptTokens=x.lastPromptTokens),l&&r(c),u(f),d){const o=[...c.messages].reverse().find(s=>"assistant"===s.role&&s.content);process.stdout.write(JSON.stringify({type:"result",is_error:!!x?.error,...x?.error?{error:x.error}:{},result:o?String(o.content):"",usage:{input_tokens:x?.usage?.prompt??0,output_tokens:x?.usage?.completion??0},session_id:c.id,model:s.model,num_turns:i(c.messages)})+"\n")}x?.error&&process.exit(1)}
package/src/squad.js ADDED
@@ -0,0 +1 @@
1
+ import e from"node:fs";import t from"node:path";import n from"node:os";import{fileURLToPath as r}from"node:url";import{runTurn as s}from"./agent.js";import{createPermissions as o}from"./permissions.js";import{loadAgents as i}from"./subagent.js";import{streamChat as a}from"./api.js";import{discoverExperts as c}from"./experts.js";export const BUNDLED_SQUADS_DIR=t.join(t.dirname(r(import.meta.url)),"..","squads");export function squadDirs(e){const r=[];return e&&r.push(t.join(e,".huicai","squads")),r.push(t.join(n.homedir(),".huicai","squads")),r}export function parseSquadDoc(e,t=""){const{attrs:n,body:r}=function(e){const t={attrs:{},body:String(e||"").trim()},n=/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(t.body);if(!n)return t;t.body=t.body.slice(n[0].length).trim();for(const e of n[1].split(/\r?\n/)){const n=/^([A-Za-z][\w-]*)\s*:\s*(.*)$/.exec(e.trim());n&&(t.attrs[n[1]]=n[2].trim().replace(/^["']|["']$/g,""))}return t}(e),s={name:n.name||t,description:n.description||"",leader:n.leader||"主理人",mode:["dispatch","review","ensemble"].includes(n.mode)?n.mode:"dispatch",when:n.when||"",mission:"",members:[],precheck:[],workflows:[]},o=new Map,i=r.split(/^##\s+/m);s.mission=(i[0]||"").trim();for(const e of i.slice(1)){const t=e.indexOf("\n"),n=(t<0?e:e.slice(0,t)).trim(),r=t<0?"":e.slice(t+1);if(/^成员人设/.test(n))for(const e of r.split(/^###\s+/m).slice(1)){const t=e.indexOf("\n"),n=(t<0?e:e.slice(0,t)).trim(),r=t<0?"":e.slice(t+1).trim();n&&r&&o.set(n,r)}else if(/^成员/.test(n))for(const e of r.split(/\r?\n/)){const t=/^\s*[-*]\s+(.+)$/.exec(e);if(!t)continue;const n=t[1].split("|").map(e=>e.trim());n[0]&&s.members.push({id:n[0],callName:n[1]||n[0],duty:n[2]||"",executor:n[3]||""})}else if(/^预检/.test(n))for(const e of r.split(/\r?\n/)){const t=/^\s*[-*]\s+(.+)$/.exec(e);t&&s.precheck.push(t[1].trim())}else if(/^工作流/.test(n))for(const e of r.split(/^###\s+/m).slice(1)){const t=e.indexOf("\n"),n=(t<0?e:e.slice(0,t)).trim(),r=t<0?"":e.slice(t+1),o=/^(.*?)(?:(触发[::](.*?)))?$/.exec(n)||[],i={name:(o[1]||n).trim(),trigger:(o[2]||"").trim(),stages:[],deliverable:""};for(const e of r.split(/\r?\n/)){const t=/^\s*\d+[.、)]\s*(.+)$/.exec(e);if(t){i.stages.push(t[1].trim());continue}const n=/^\s*交付[::]\s*(.+)$/.exec(e);n&&(i.deliverable=n[1].trim())}i.name&&s.workflows.push(i)}}for(const[e,t]of o){const n=s.members.find(t=>t.id===e);n&&(n.persona=t)}return s}export function loadSquads(n){const r=new Map,s=[...squadDirs(n).map((e,t)=>({dir:e,scope:n&&0===t?"project":"global"})),{dir:BUNDLED_SQUADS_DIR,scope:"builtin"}];for(const{dir:n,scope:o}of s){let s;try{s=e.readdirSync(n).filter(e=>e.endsWith(".md"))}catch{continue}for(const i of s){const s=i.replace(/\.md$/,"").trim();if(s)try{const a=t.join(n,i),c=parseSquadDoc(e.readFileSync(a,"utf8"),s);c.scope=o,c.file=a,r.has(c.name)||r.set(c.name,c)}catch{}}}return[...r.values()]}export function ensureTeamDir(n,r){const s=(new Date).toISOString().slice(0,16).replace(/[-:]/g,"").replace("T","-"),o=t.join(n,".huicai","teams",`${r}-${s}`);try{e.mkdirSync(o,{recursive:!0})}catch{return null}return o}export function buildLeaderPrompt(e,t){const n=[];n.push("**Role Override**:以下专家团主理人定义优先于此前任何身份设定,是你在本任务中唯一、权威的角色。"),n.push(""),n.push(`# 专家团「${e.name}」· 主理人${e.leader}`),n.push(`你是专家团「${e.name}」的主理人${e.leader},负责把用户任务拆解、派发给团队成员并汇总交付。`),e.mission&&n.push("",e.mission),n.push("","## 关键规则:只调度,不代工","- 成员职责范围内的专业产出必须经 dispatch_member 由对应成员完成后采信,你不代写;","- 简单澄清/寒暄/对既有产出的说明可直接回答,无须派发。"),n.push("","## 团队架构(调度依据)","| 成员ID | 称呼 | 职责 | 执行者 |","|---|---|---|---|");for(const t of e.members)n.push(`| ${t.id} | ${t.callName} | ${t.duty} | ${t.executor||"(当前模型)"} |`);if(n.push("","## 任务分配预检(每次派发前必做)","成员不会说「这个我不会」——超能力范围的任务结果无保证。所以:","1. 解析用户意图(研究/分析/生成/编辑/写作/审校…);","2. 对照职责与速查表做能力匹配;","3. 匹配 → 派发;不匹配 → 告知用户团队能力限制与替代建议,禁止硬派;","4. 意图模糊 → 先向用户确认。"),e.precheck.length){n.push("### 速查表(常见误派提醒)");for(const t of e.precheck)n.push(`- ${t}`)}if(e.workflows.length){n.push("","## 预设工作流(识别到触发条件时按阶段执行,不自由发挥)");for(const t of e.workflows)n.push(`### ${t.name}${t.trigger?`(触发:${t.trigger})`:""}`),t.stages.forEach((e,t)=>n.push(`${t+1}. ${e}`)),t.deliverable&&n.push(`交付:${t.deliverable}`)}return n.push("","## 团队协作规范","1. 成员在隔离上下文执行,看不到本对话与其他成员产出——任务说明必须独立自足;","2. 跨成员信息流经你中转:把上一阶段产出的必要部分放进下一位成员的 context;","3. 互相独立的任务同一轮并行派发(一次输出多个 dispatch_member 调用),减少串行等待;","4. 成员结论为准:如实汇编;产出不符时修正任务说明重派,而非替他改写。"),"review"===e.mode?n.push("### 生成-审查模式(本团约束)","每个关键产出完成后,必须把「产出全文+原始要求」派给职责匹配的审查成员评审,按意见让原成员修订至通过;未经审查的产出不得作为最终交付。"):"ensemble"===e.mode&&n.push("### 多候选聚合模式(本团约束)","对开放性核心任务,把同一任务并行派给多位匹配成员(或不同侧重多份),对比候选择优或综合,并向用户说明取舍。"),n.push("","## 异常处理(失败三级升级)","- 一级:修正任务说明(更自足、更具体、补 context)后重派原成员;同一任务累计失败满 2 次系统会终止该任务线;","- 二级:换人或换执行方式——改派职责相近的其他成员;平台资源不可用时由本机成员降级承接(系统会自动降级并注明);","- 三级:降级交付——完成可完成的部分,如实说明缺口与原因,并给出用户可选的替代方案。"),t&&n.push("","## 团队工作区",`本次协作的产物统一放在 ${t}(你可用文件工具写入;成员只回传文本结论,由你负责归档与整合)。`),n.push("","## 交付检查清单(最终答复前逐项自查)","- 用户目标的每一部分是否都有对应成员产出支撑?","- 是否汇总了全部关键产出、无散落遗漏?","- 是否标注了各部分由哪位成员产出?","- 未完成的部分是否如实说明?"),n.join("\n")}const m=/^(agent|workflow|datahuman|squad)-/;async function p({squad:e,member:t,ref:n,task:r,context:s,expectation:o,config:i}){const c=[`【专家团「${e.name}」成员任务】${r}`,s?`\n【上游产出/背景】\n${s}`:"",o?`\n【交付要求】\n${o}`:""].join("");return(await a({baseUrl:i.baseUrl,apiKey:i.apiKey,model:n,messages:[{role:"user",content:c}],client:i.client,sessionId:i.sessionId},{})).content||"(平台无返回)"}async function l({squad:e,member:t,task:n,context:r,expectation:a,config:l,cwd:u,readOnlyExtras:d}){const f=String(t.executor||"").trim();let h="";if(f.startsWith("platform:auto:")){const s=await async function(e,t){const n=/^platform:auto:(agent|workflow|datahuman|squad)(?::(.+))?$/.exec(e);if(!n)return null;const[,r,s]=n;try{const e=await c(t),n=(s||"").trim().toLowerCase();return e.find(e=>e.type===r&&(!n||(String(e.name)+" "+String(e.description)).toLowerCase().includes(n)))||null}catch{return null}}(f,l);if(s){const o=await p({squad:e,member:t,ref:s.id,task:n,context:r,expectation:a,config:l});return`(由平台资源「${s.name}」执行)\n${o}`}h=`(未发现可用的平台${f.split(":")[2]||""}资源,已由本机成员降级执行)\n`}else if(f.startsWith("platform:")){const s=f.slice(9).trim();return m.test(s)?p({squad:e,member:t,ref:s,task:n,context:r,expectation:a,config:l}):`成员「${t.callName}」的执行者 ${s} 不是有效的平台伪模型引用`}const $=["**Role Override**:以下角色定义在本次任务中拥有最高优先级。",""];t.persona&&$.push(t.persona,""),$.push(`你是专家团「${e.name}」的成员「${t.callName}」${t.duty?`(职责:${t.duty})`:""},正在执行主理人下发的一项独立任务。`,"- 只完成本任务,直接输出可被主理人直接采用的成果本身,不要寒暄、不要复述任务;","- 无法反问,信息不全时按合理假设执行并注明假设。"),r&&$.push("","## 主理人提供的上游产出/背景",String(r)),a&&$.push("","## 交付要求",String(a));let g=$.join("\n"),w=l;if(f.startsWith("agent:")){const e=f.slice(6).trim(),t=i(u).find(t=>t.name===e);t&&(g=t.prompt.trim()+"\n\n"+g)}else if(f.startsWith("model:")){const e=f.slice(6).trim();e&&(w={...l,model:e})}g+="\n工作目录:"+u;const x=[{role:"system",content:g},{role:"user",content:n}],b=await s({config:w,messages:x,permissions:o({yolo:!1,ask:null}),cwd:u,extraTools:d,depth:1,maxIters:50,io:{write:()=>{}}});if(b?.error)return`成员执行失败:${b.error}`;const y=[...x].reverse().find(e=>"assistant"===e.role&&e.content);return y?h+y.content:"(成员未产出结论)"}export function makeDispatchMemberTool({config:e,cwd:t,extraTools:n=[]}){const r=loadSquads(t);if(!r.length)return null;const s=n.filter(e=>e.readOnly),o=new Map;return{readOnly:!0,spec:{type:"function",function:{name:"dispatch_member",description:"把一项独立、自足的任务派发给某个专家团的一位成员:成员在隔离上下文中执行并只回传成果。复杂/跨角色任务优先按团开工:对照下方各团「何时使用」自主选团,按其预设工作流分阶段派发(互相独立的任务同一轮多次调用并行);成员产出由你汇总并用本机工具落地,落地后验证。派发前先做任务分配预检(对照成员职责做能力匹配)。\n可用专家团:\n"+r.map(e=>`- ${e.name}:${e.when||e.description||""}(成员 ${e.members.map(e=>e.id).join("/")})`).join("\n"),parameters:{type:"object",properties:{squad:{type:"string",enum:r.map(e=>e.name),description:"专家团名称"},member:{type:"string",description:"成员ID(推荐)或称呼,须来自该团的团队架构表"},task:{type:"string",description:"下发的任务说明——必须独立自足(成员看不到本对话与其他成员产出)"},context:{type:"string",description:"可选:上游产出/背景(跨成员信息流经主理人中转,把必要部分放这里)"},expectation:{type:"string",description:"可选:交付要求/验收标准"}},required:["squad","member","task"]}}},run:async n=>{const i=r.find(e=>e.name===String(n?.squad||"").trim());if(!i)return`未找到专家团「${n?.squad}」(可用:${r.map(e=>e.name).join("、")})`;const a=function(e,t){const n=String(t||"").trim();return e.members.find(e=>e.id===n)||e.members.find(e=>e.callName===n)||null}(i,n?.member);if(!a)return`专家团「${i.name}」中没有成员「${n?.member}」(可用:${i.members.map(e=>`${e.id}(${e.callName})`).join("、")})`;const c=String(n?.task||"").trim();if(!c)return"缺少 task(任务说明)";const m=`${i.name}|${a.id}|${c.slice(0,40)}`,p=o.get(m)||0;if(p>=2)return`该任务线已按异常协议终止(成员「${a.callName}」同一任务累计失败满 2 次)。请向用户说明原因并给出替代方案。`;try{const r=await l({squad:i,member:a,task:c,context:n?.context,expectation:n?.expectation,config:e,cwd:t,readOnlyExtras:s});return/^(成员执行失败|平台调用失败)/.test(r)?(o.set(m,p+1),`${r}(累计失败 ${p+1}/2,可修正任务说明后重试或改派他人)`):`成员「${a.callName}」对任务【${c.replace(/\s+/g," ").slice(0,60)}】的产出:\n${r}`}catch(e){if(e&&"AbortError"===e.name)throw e;return o.set(m,p+1),`成员「${a.callName}」执行失败:${e.message}(累计失败 ${p+1}/2)`}}}}export function squadCommandPrompt(e,t,n){return buildLeaderPrompt(e,n)+"\n\n---\n\n用户任务:"+t+`\n\n请以主理人${e.leader}的身份完成:先做任务分配预检(含预设工作流匹配),然后经 dispatch_member`+`(squad="${e.name}")派发执行(独立任务同轮并行),跨成员信息流经你中转,最后按交付检查清单汇总交付。`}export const SQUAD_TEMPLATE="---\nname: {NAME}\ndescription: 内容创作专家团(示例:改成你的团队)\nleader: 司远\nmode: dispatch\nwhen: 需要成套内容创作/深度文章且涉及调研-写作-审校多角色协作时\n---\n\n擅长把用户的创作需求拆解为具体执行方案,并调度合适的成员高效交付。\n\n## 成员\n- researcher | 调研员 | 资料收集、事实核查、竞品调研 | agent:researcher\n- writer | 笔澜 | 公众号长文、营销文案、脚本 |\n- reviewer | 审校 | 审查产出、给出修订意见 |\n- fetcher | 源泉 | (示例)平台数字人自动绑定查数 | platform:auto:datahuman\n\n## 成员人设\n### writer\n角色:资深中文内容主笔。\n工作方法:先定读者与核心论点,再列结构,后成文;每段只讲一件事;用事实与例子代替空话。\n交付标准:结构清晰可扫读、观点有依据、结尾给行动建议;附 3 条备选标题。\n\n## 预检\n- 写长文/文案 → writer,不要派给 researcher\n- 资料/数据收集 → researcher,产出交给 writer 前先经你提炼要点\n\n## 工作流\n### 深度长文(触发:用户要深度文章/长文)\n1. researcher:收集资料与事实\n2. writer:按资料成稿\n3. reviewer:审查并给修订意见(mode=review 时必经)\n交付:终稿 Markdown + 三条标题备选\n";
package/src/transcript.js CHANGED
@@ -1 +1 @@
1
- import{streamChat as t}from"./api.js";import{DEFAULT_MODEL as e}from"./config.js";const n=/^(agent|workflow|datahuman)-/;export function textOf(t){return"string"==typeof t?t:Array.isArray(t)?t.map(t=>"text"===t?.type?t.text:"image_url"===t?.type?"[图片]":"").join("\n"):""}export function toMarkdown(t){const e=t||{},n=["# HuiCai 对话记录","",`- 会话:${e.id||""}`,`- 模型:${e.model||""}`,`- 导出时间:${(new Date).toLocaleString()}`,"","---",""];for(const t of e.messages||[])if("system"!==t.role)if("user"===t.role)n.push("### 🧑 用户","",textOf(t.content).trim()||"(空)","");else if("assistant"===t.role){const e=textOf(t.content).trim();e&&n.push("### 🤖 助手","",e,"");for(const e of t.tool_calls||[]){const t=String(e.function?.arguments||"").replace(/\s+/g," ").slice(0,200);n.push(`> 🔧 \`${e.function?.name}(${t})\``,"")}}else if("tool"===t.role){const e=String(t.content||"").slice(0,1500);n.push("> 工具结果:","","```",e,"```","")}return n.join("\n")}export async function distillMemory(o,r,s=""){const i=n.test(String(r.model||""))?r.compactModel||e:r.model,l=(o||[]).filter(t=>"system"!==t.role).map(t=>`[${t.role}] ${textOf(t.content).replace(/\s+/g," ").slice(0,600)}`).join("\n").slice(0,16e3);if(!l.trim())return[];const c=(s?"【已有记忆(勿重复)】\n"+s+"\n\n":"")+"【本次记录】\n"+l,a=((await t({baseUrl:r.baseUrl,apiKey:r.apiKey,model:i,messages:[{role:"system",content:"你是项目知识提炼器。从下面这段对话/执行记录中,提炼出**值得跨会话长期记住**的稳定事实/约定/结论/踩坑(如构建与测试命令、架构决定、代码约定、易错点、外部依赖要点)。要求:每条一行,以「- 」开头,简洁具体;只输出真正长期有用的,不要一次性细节、不要大段代码;若与「已有记忆」重复则跳过;最多 5 条;没有则输出「(无)」。"},{role:"user",content:c}]})).content||"").trim();return!a||/^[((]无[))]$/.test(a)?[]:a.split("\n").map(t=>t.replace(/^[-*]\s*/,"").trim()).filter(t=>t&&!/^[((]无[))]$/.test(t)).slice(0,5)}export async function recapConversation(o,r){const s=n.test(String(r.model||""))?r.compactModel||e:r.model,i=(o||[]).filter(t=>"system"!==t.role).map(t=>`[${t.role}] ${textOf(t.content).replace(/\s+/g," ").slice(0,500)}`).join("\n").slice(0,12e3);if(!i.trim())return"(对话尚无内容)";return((await t({baseUrl:r.baseUrl,apiKey:r.apiKey,model:s,messages:[{role:"system",content:"用一句话(不超过40个汉字)概括这段对话到目前为止在做什么、进展如何。只输出这一句话,不要引号、不要多余说明。"},{role:"user",content:i}]})).content||"").trim().split("\n")[0]||"(无法生成小结)"}
1
+ import{streamChat as t}from"./api.js";import{DEFAULT_MODEL as e}from"./config.js";const n=/^(agent|workflow|datahuman|squad)-/;export function textOf(t){return"string"==typeof t?t:Array.isArray(t)?t.map(t=>"text"===t?.type?t.text:"image_url"===t?.type?"[图片]":"").join("\n"):""}export function toMarkdown(t){const e=t||{},n=["# HuiCai 对话记录","",`- 会话:${e.id||""}`,`- 模型:${e.model||""}`,`- 导出时间:${(new Date).toLocaleString()}`,"","---",""];for(const t of e.messages||[])if("system"!==t.role)if("user"===t.role)n.push("### 🧑 用户","",textOf(t.content).trim()||"(空)","");else if("assistant"===t.role){const e=textOf(t.content).trim();e&&n.push("### 🤖 助手","",e,"");for(const e of t.tool_calls||[]){const t=String(e.function?.arguments||"").replace(/\s+/g," ").slice(0,200);n.push(`> 🔧 \`${e.function?.name}(${t})\``,"")}}else if("tool"===t.role){const e=String(t.content||"").slice(0,1500);n.push("> 工具结果:","","```",e,"```","")}return n.join("\n")}export async function distillMemory(o,s,r=""){const i=n.test(String(s.model||""))?s.compactModel||e:s.model,l=(o||[]).filter(t=>"system"!==t.role).map(t=>`[${t.role}] ${textOf(t.content).replace(/\s+/g," ").slice(0,600)}`).join("\n").slice(0,16e3);if(!l.trim())return[];const c=(r?"【已有记忆(勿重复)】\n"+r+"\n\n":"")+"【本次记录】\n"+l,a=((await t({baseUrl:s.baseUrl,apiKey:s.apiKey,model:i,messages:[{role:"system",content:"你是项目知识提炼器。从下面这段对话/执行记录中,提炼出**值得跨会话长期记住**的稳定事实/约定/结论/踩坑(如构建与测试命令、架构决定、代码约定、易错点、外部依赖要点)。要求:每条一行,以「- 」开头,简洁具体;只输出真正长期有用的,不要一次性细节、不要大段代码;若与「已有记忆」重复则跳过;最多 5 条;没有则输出「(无)」。"},{role:"user",content:c}]})).content||"").trim();return!a||/^[((]无[))]$/.test(a)?[]:a.split("\n").map(t=>t.replace(/^[-*]\s*/,"").trim()).filter(t=>t&&!/^[((]无[))]$/.test(t)).slice(0,5)}export async function recapConversation(o,s){const r=n.test(String(s.model||""))?s.compactModel||e:s.model,i=(o||[]).filter(t=>"system"!==t.role).map(t=>`[${t.role}] ${textOf(t.content).replace(/\s+/g," ").slice(0,500)}`).join("\n").slice(0,12e3);if(!i.trim())return"(对话尚无内容)";return((await t({baseUrl:s.baseUrl,apiKey:s.apiKey,model:r,messages:[{role:"system",content:"用一句话(不超过40个汉字)概括这段对话到目前为止在做什么、进展如何。只输出这一句话,不要引号、不要多余说明。"},{role:"user",content:i}]})).content||"").trim().split("\n")[0]||"(无法生成小结)"}