@huicai/cli 2026.711.24616 → 2026.712.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +5 -5
- package/README.md +35 -3
- package/bin/huicai.js +35 -0
- package/bin/preuninstall.cjs +20 -0
- package/package.json +3 -2
- package/src/agent.js +1 -1
- package/src/automation.js +1 -0
- package/src/autorun.js +1 -0
- package/src/commands.js +1 -1
- package/src/context.js +1 -1
- package/src/permissions.js +1 -1
- package/src/render.js +1 -1
- package/src/repl.js +1 -1
- package/src/settings.js +1 -1
- package/src/tools.js +1 -1
- package/src/tui/app.js +1 -1
- package/src/uninstall.js +1 -0
package/LICENSE
CHANGED
|
@@ -4,25 +4,25 @@ HuiCai AI — 专有软件许可协议 / Proprietary Software License
|
|
|
4
4
|
Copyright © 2026 HuiCai. All rights reserved.
|
|
5
5
|
|
|
6
6
|
本软件(包括 HuiCai AI 命令行工具、VS Code 扩展、JetBrains 插件及其捆绑的
|
|
7
|
-
核心代码、资源与文档,下称"本软件"
|
|
7
|
+
核心代码、资源与文档,下称"本软件")为HuiCai及其许可方的专有财产,受著作权法
|
|
8
8
|
及国际条约保护。本软件按许可授权使用,而非出售。
|
|
9
9
|
|
|
10
|
-
1.
|
|
10
|
+
1. 授权。在您遵守本协议的前提下,HuiCai授予您一项有限的、非独占的、不可转让、
|
|
11
11
|
不可再许可的权利,以目标代码形式安装并使用本软件,用于您的内部业务用途。
|
|
12
12
|
|
|
13
|
-
2.
|
|
13
|
+
2. 限制。除适用法律明确允许或经HuiCai事先书面同意外,您不得:
|
|
14
14
|
(a) 复制、分发、出售、出租、出借、再许可或以其他方式向第三方提供本软件;
|
|
15
15
|
(b) 修改、翻译、改编本软件,或基于本软件创作衍生作品;
|
|
16
16
|
(c) 反向工程、反编译、反汇编本软件,或以其他方式尝试获取其源代码或内部结构
|
|
17
17
|
(适用法律明确不可排除者除外);
|
|
18
18
|
(d) 去除或更改本软件中的任何专有声明、标识或标记。
|
|
19
19
|
|
|
20
|
-
3.
|
|
20
|
+
3. 所有权。本软件的一切权利、所有权及知识产权均归HuiCai及其许可方所有。本协议
|
|
21
21
|
未明确授予的权利均予保留。
|
|
22
22
|
|
|
23
23
|
4. 无担保。本软件按"现状"提供,不附带任何明示或默示担保。
|
|
24
24
|
|
|
25
|
-
5.
|
|
25
|
+
5. 责任限制。在法律允许的最大范围内,HuiCai不对因使用或无法使用本软件而产生的
|
|
26
26
|
任何间接、附带、特殊或后果性损害承担责任。
|
|
27
27
|
|
|
28
28
|
6. 终止。若您违反本协议,本授权自动终止;届时您须停止使用并销毁本软件的全部
|
package/README.md
CHANGED
|
@@ -47,6 +47,9 @@ huicai -c # 继续当前目录最近一次会话
|
|
|
47
47
|
huicai -r # 从历史会话中选择恢复
|
|
48
48
|
huicai sessions # 列出历史会话
|
|
49
49
|
huicai models # 列出可用模型(含 agent-/workflow-/datahuman- 伪模型)
|
|
50
|
+
huicai kb-sync <知识库ID> [ref] # 配合 git diff 增量同步知识库,保鲜 RAG(--dry 试运行)
|
|
51
|
+
huicai uninstall [--yes] # 清理本机所有 CLI 数据(见下方「卸载 / 清理数据」)
|
|
52
|
+
huicai help # 查看全部子命令(skills / pr / daemon / watch / usage …)
|
|
50
53
|
```
|
|
51
54
|
|
|
52
55
|
### 交互增强
|
|
@@ -92,9 +95,16 @@ huicai -c -p "继续修" --yolo # 非交互续接(CI)
|
|
|
92
95
|
但**工具依旧在你本机执行**(读写文件/shell)。`<id>` 用智能体的分享令牌或 `agent-<token>`(见 `huicai models`)。
|
|
93
96
|
未显式 `-m` 时自动采用该智能体配置的模型。适合把平台上调好的「代码审查员」「重构助手」等直接搬到终端里干活。
|
|
94
97
|
|
|
95
|
-
交互中的斜杠命令(REPL 与 TUI
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
交互中的斜杠命令(REPL 与 TUI 共用一套注册表,60+ 个):**输入 `/` 即弹出候选命令**——富 TUI 里每页 10 个、`↑↓` 选择、`PgUp/PgDn` 翻页、`Tab` 补全、`⏎` 执行;`/help` 查看全部。按类速览:
|
|
99
|
+
|
|
100
|
+
- 会话/上下文:`/clear /compact /context /cost /status /sessions /rename /export /copy /recap /rewind`
|
|
101
|
+
- 模型/配置:`/model`(切换前**校验当前账号是否有权使用该模型**,无权则拒绝并列可用模型)`/models /effort /automodel /autocompact /theme /config`
|
|
102
|
+
- 开发工作流:`/plan /auto`(**自动规划并自动执行**:只读调研出分步计划 → 询问「是否继续?1、是 2、是,并允许在当前对话中后续相同操作不再询问 3、否」→ 确认后自动逐步执行并自检)`/diff /pr /review /security-review /scan /cover /verify /distill /orchestrate`
|
|
103
|
+
- 工具/权限/技能:`/mcp /permissions /hooks /skills /reload-skills /agents /sandbox /yolo`
|
|
104
|
+
- 目录/会话流转:`/cd`(**富 TUI 现已支持**:切换后在新目录开新会话并重建规则/工具/MCP)`/add-dir /cwd /branch /fork /loop /schedule`
|
|
105
|
+
- 记忆/项目:`/memory /init /goal /insights` + 自定义命令。
|
|
106
|
+
|
|
107
|
+
`/init` 让智能体实际调研项目后生成/完善 `HUICAI.md`(非静态模板);`/status` 显示版本/模型/会话/MCP/权限规则。
|
|
98
108
|
|
|
99
109
|
## 内置本机工具
|
|
100
110
|
|
|
@@ -106,8 +116,16 @@ huicai -c -p "继续修" --yolo # 非交互续接(CI)
|
|
|
106
116
|
| `fetch_url` | 抓取网页/接口(HTML 自动转纯文本),查在线文档 | 需授权 |
|
|
107
117
|
| `todo_write` | 维护任务清单(TUI 实时渲染进度) | 自动放行 |
|
|
108
118
|
| `dispatch_agent` | 委派子任务给隔离上下文的只读子智能体 | 自动放行(只读) |
|
|
119
|
+
| `install_packages` | 安装 Node.js 依赖包(npm/pnpm/yarn 按锁文件自动识别;省略包名=装全部依赖) | **是否继续?三选项确认** |
|
|
120
|
+
| `setup_environment` | 一键配置运行环境:检测技术栈(Node/Python/Go/Rust/Maven)→ 检查工具链 → 装依赖;或执行显式配置命令 | **是否继续?三选项确认** |
|
|
121
|
+
| `install_plugin` | 安装 VS Code 扩展 / JetBrains 插件 / npm 全局 CLI | **是否继续?三选项确认** |
|
|
122
|
+
| `open_browser` / `open_path` / `open_app` | 打开默认浏览器 / 文件管理器(访达·资源管理器)/ 启动任意本机应用 | **是否继续?三选项确认** |
|
|
109
123
|
| `mcp__<服务>__<工具>` | 本地 MCP 服务提供的工具 | 按 readOnlyHint(写型需授权) |
|
|
110
124
|
|
|
125
|
+
**自动化操作的三选项确认**(三端一致):执行前询问「是否继续?」——`1、是`(仅此一次)·
|
|
126
|
+
`2、是,并允许在当前对话中后续相同操作不再询问`(按工具名记忆,仅本会话)· `3、否`(可直接输入原因,会回灌给模型调整方案)。
|
|
127
|
+
沙箱模式(`/sandbox`)下这些操作与 shell/联网一样被禁止;全部以 argv 直接 spawn(不经 shell)+ 参数白名单校验,杜绝命令注入。
|
|
128
|
+
|
|
111
129
|
**执行策略**:同一轮里连续的只读工具调用**并行执行**(结果仍按序回灌);单条工具输出超长自动**截断保头尾**,
|
|
112
130
|
防止撑爆上下文;中断(Esc/Ctrl+C)时为未执行的调用合成占位结果,保证消息协议不被破坏、会话可无缝续聊。
|
|
113
131
|
|
|
@@ -204,6 +222,20 @@ TTY 下默认启用 ink + React 全屏界面:已定稿对话记录 + 流式正
|
|
|
204
222
|
- 令牌文件 0600;日志不打印令牌。
|
|
205
223
|
- ⚠ `--yolo` 会跳过所有确认,仅在你信任的仓库使用。
|
|
206
224
|
|
|
225
|
+
## 卸载 / 清理数据
|
|
226
|
+
|
|
227
|
+
CLI 的全部本机数据都在单一目录 `~/.huicai`(配置与登录令牌、会话历史、长期记忆、技能、自定义命令、计划任务、日志)。
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
huicai uninstall # 列出各项数据与体积 → 交互确认 (y/N) → 彻底清空 ~/.huicai
|
|
231
|
+
huicai uninstall --yes # 跳过确认(脚本 / CI;非交互环境必须加 --yes)
|
|
232
|
+
npm uninstall -g @huicai/cli # 卸载命令本体;其 preuninstall 钩子会自动清理 ~/.huicai(卸载不留痕)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
- `huicai uninstall` 先列出将删除的数据(含登录令牌),确认后删除,**不可撤销**;如需保留请先备份 `~/.huicai`。
|
|
236
|
+
- `npm uninstall` 触发 `preuninstall` 钩子自动清空 `~/.huicai`——卸载后不残留配置与令牌。
|
|
237
|
+
- 只想退出登录、保留其它数据:用 `huicai logout`。
|
|
238
|
+
|
|
207
239
|
## 架构(一图)
|
|
208
240
|
|
|
209
241
|
```
|
package/bin/huicai.js
CHANGED
|
@@ -135,6 +135,7 @@ ${c.bold('huicai')} — 终端智能体,以 HuiCai_AI 平台为模型后端,
|
|
|
135
135
|
huicai kb-sync <id> [ref] 配合 git diff 增量同步知识库,保鲜 RAG(--dry 试运行)
|
|
136
136
|
huicai skills pull [name] 平台技能 → 本地(默认 ~/.huicai/skills;--project 放入项目)
|
|
137
137
|
huicai skills push <name> 本地技能 → 平台(默认同名跳过;--overwrite 覆盖)
|
|
138
|
+
huicai uninstall [--yes] 清理本机所有 CLI 数据(~/.huicai:配置/令牌/会话/记忆/技能…)
|
|
138
139
|
huicai help 显示本帮助
|
|
139
140
|
|
|
140
141
|
选项:
|
|
@@ -439,6 +440,40 @@ async function main() {
|
|
|
439
440
|
process.stdout.write(c.green('已退出登录(已清除本机保存的令牌)。') + '\n');
|
|
440
441
|
return;
|
|
441
442
|
}
|
|
443
|
+
if (sub === 'uninstall') {
|
|
444
|
+
// 清理本机所有 HuiCai CLI 数据(~/.huicai)。加 --yes/-y 跳过确认。
|
|
445
|
+
const { describeData, purgeData, fmtSize } = await import('../src/uninstall.js');
|
|
446
|
+
const info = describeData();
|
|
447
|
+
if (!info.exists || info.entries.length === 0) {
|
|
448
|
+
process.stdout.write(c.gray('没有可清理的本机数据(' + info.dir + ' 不存在或为空)。') + '\n');
|
|
449
|
+
process.stdout.write(c.gray('卸载命令本体:npm uninstall -g @huicai/cli') + '\n');
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
process.stdout.write('将永久删除以下 HuiCai CLI 本机数据(' + info.dir + ',共 ' + fmtSize(info.total) + '):\n');
|
|
453
|
+
for (const e of info.entries) {
|
|
454
|
+
process.stdout.write(' - ' + e.name + (e.kind === 'dir' ? '/' : '') + c.gray(' ' + fmtSize(e.size)) + '\n');
|
|
455
|
+
}
|
|
456
|
+
process.stdout.write(c.gray(' 含登录令牌、会话历史、长期记忆、技能与自定义命令等,删除不可撤销。') + '\n');
|
|
457
|
+
const yes = positional.includes('--yes') || positional.includes('-y') || flags.yolo;
|
|
458
|
+
if (!yes) {
|
|
459
|
+
if (!process.stdin.isTTY) {
|
|
460
|
+
process.stderr.write(c.red('非交互环境:请加 --yes 确认删除。') + '\n');
|
|
461
|
+
process.exit(1);
|
|
462
|
+
}
|
|
463
|
+
const ans = await new Promise((resolve) => {
|
|
464
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
465
|
+
rl.question('确认永久删除以上全部数据?(y/N) ', (a) => { rl.close(); resolve(a); });
|
|
466
|
+
});
|
|
467
|
+
if (!/^y(es)?$/i.test(String(ans).trim())) {
|
|
468
|
+
process.stdout.write(c.gray('已取消,未删除任何数据。') + '\n');
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
const r = purgeData();
|
|
473
|
+
process.stdout.write(c.green('✅ 已清理所有 HuiCai CLI 本机数据:' + r.dir) + '\n');
|
|
474
|
+
process.stdout.write(c.gray('卸载命令本体:npm uninstall -g @huicai/cli(其 preuninstall 钩子也会自动清理本目录)') + '\n');
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
442
477
|
|
|
443
478
|
const config = loadConfig();
|
|
444
479
|
if (flags.base) config.baseUrl = flags.base.replace(/\/+$/, '');
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// npm 卸载钩子(package.json 的 preuninstall):`npm uninstall [-g] @huicai/cli` 时触发,
|
|
3
|
+
// 清理 ~/.huicai 全部本机数据(配置/令牌/会话/历史/记忆/技能/计划等),做到卸载后不留痕。
|
|
4
|
+
// 需保留数据者:卸载前先备份 ~/.huicai,或改用可控的 `huicai uninstall`(带确认)。
|
|
5
|
+
// 说明:用 CommonJS(.cjs) + 零依赖,置于已打包的 bin/ 下,普通 npm pack 与发布用 pack:release 都含它。
|
|
6
|
+
'use strict';
|
|
7
|
+
const fs = require('node:fs');
|
|
8
|
+
const os = require('node:os');
|
|
9
|
+
const path = require('node:path');
|
|
10
|
+
|
|
11
|
+
const dir = path.join(os.homedir(), '.huicai');
|
|
12
|
+
try {
|
|
13
|
+
if (fs.existsSync(dir)) {
|
|
14
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
15
|
+
process.stdout.write('[huicai] 已清理本机数据目录:' + dir + '(卸载不留痕)\n');
|
|
16
|
+
}
|
|
17
|
+
} catch (e) {
|
|
18
|
+
// 卸载钩子失败不应阻断 npm 卸载流程
|
|
19
|
+
process.stdout.write('[huicai] 清理 ' + dir + ' 失败:' + e.message + '(可手动删除该目录)\n');
|
|
20
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@huicai/cli",
|
|
3
|
-
"version": "2026.
|
|
4
|
-
"versionDisplay": "2026.07.
|
|
3
|
+
"version": "2026.712.0",
|
|
4
|
+
"versionDisplay": "2026.07.12",
|
|
5
5
|
"description": "在终端运行的智能体 CLI:以 HuiCai_AI 平台的 OpenAI 兼容 /v1 为模型后端,在用户本机执行文件/命令工具。",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"test": "node --test --test-concurrency=1 test/*.test.mjs",
|
|
24
24
|
"version:next": "node scripts/next-version.cjs",
|
|
25
25
|
"pack:release": "node scripts/pack-release.cjs",
|
|
26
|
+
"preuninstall": "node bin/preuninstall.cjs",
|
|
26
27
|
"prepublishOnly": "node --check bin/huicai.js"
|
|
27
28
|
},
|
|
28
29
|
"keywords": [
|
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
|
|
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&>(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}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import t from"node:fs";import e from"node:path";import{spawn as n}from"node:child_process";import{runBash as o}from"./executors.js";const r=3e4,a=3e5;export const AUTOMATION_TOOLS=new Set(["install_packages","setup_environment","install_plugin","open_browser","open_path","open_app","auto_execute"]);export function automationActionLabel(t,e={}){switch(t){case"install_packages":return`安装 Node.js 包:${Array.isArray(e.packages)&&e.packages.length?e.packages.join(" "):"package.json 全部依赖"}${e.global?"(全局)":""}${e.dev?"(devDependencies)":""}`;case"setup_environment":return Array.isArray(e.commands)&&e.commands.length?`自动配置运行环境:执行 ${e.commands.length} 条配置命令`:"自动配置运行环境:检测项目技术栈并安装依赖";case"install_plugin":return`安装${"jetbrains"===e.target?"JetBrains 插件":"npm"===e.target?"npm 全局工具":"VS Code 扩展"}:${e.id??""}`;case"open_browser":return`打开浏览器访问:${e.url??""}`;case"open_path":return`在文件管理器中打开:${e.path??""}`;case"open_app":return`启动应用:${e.name??""}${e.target?"("+e.target+")":""}`;case"auto_execute":{const t=String(e.goal||"").replace(/\s+/g," ");return"自动执行已规划的任务"+(t?":"+(t.length>60?t.slice(0,60)+"…":t):"")}default:return t}}export function automationDetail(t,e={}){return"setup_environment"===t&&Array.isArray(e.commands)&&e.commands.length?e.commands.slice(0,12).map(t=>"$ "+String(t)).join("\n"):"open_browser"===t?String(e.url??""):"open_path"===t?String(e.path??""):"open_app"===t?String(e.target??""):""}export function which(n,{pathStr:o=process.env.PATH||"",platform:r=process.platform}={}){const a="win32"===r?";":":",s="win32"===r?[".cmd",".exe",".bat",""]:[""];for(const r of o.split(a))if(r)for(const o of s){const a=e.join(r,n+o);try{if(t.accessSync(a,t.constants.X_OK),t.statSync(a).isFile())return a}catch{}}return null}export function runArgv(t,e,{cwd:o,timeoutMs:a=3e5,winShell:s=!1}={}){const i="win32"===process.platform;return new Promise(c=>{let l,p="",u=!1;try{l=n(t,e,i?{cwd:o,shell:!!s}:{cwd:o,detached:!0})}catch(t){return void c({code:-1,killed:!1,output:`命令启动失败:${t.message}`})}const d=setTimeout(()=>{u=!0,(()=>{try{!i&&l.pid?process.kill(-l.pid,"SIGKILL"):l.kill("SIGKILL")}catch{try{l.kill("SIGKILL")}catch{}}})()},a),m=t=>{p.length<r&&(p+=t.toString())};l.stdout?.on("data",m),l.stderr?.on("data",m),l.on("error",t=>{clearTimeout(d),c({code:-1,killed:!1,output:`命令启动失败:${t.message}`})}),l.on("close",t=>{clearTimeout(d),c({code:t,killed:u,output:p.length>r?p.slice(0,r)+"\n…(输出已截断)":p})})})}function s(t,e){return new Promise(o=>{let r,a="";try{r=n(t,e,{stdio:["ignore","pipe","pipe"]})}catch(t){return void o({code:-1,output:`命令启动失败:${t.message}`})}const s=setTimeout(()=>{try{r.unref()}catch{}o({code:0,output:a,lingering:!0})},8e3);r.stdout.on("data",t=>a+=t),r.stderr.on("data",t=>a+=t),r.on("error",t=>{clearTimeout(s),o({code:-1,output:`命令启动失败:${t.message}`})}),r.on("close",t=>{clearTimeout(s),o({code:t,output:a})})})}function i(t){if(!t||"string"!=typeof t)throw new Error("工作目录未设置:请在项目目录中运行(VS Code / IDEA 中需先「打开文件夹」)")}function c(t,e=1200){const n=String(t||"").trim();return n.length>e?"…"+n.slice(-e):n}export function detectManager(n){try{if(t.existsSync(e.join(n,"pnpm-lock.yaml")))return"pnpm";if(t.existsSync(e.join(n,"yarn.lock")))return"yarn"}catch{}return"npm"}export function validPackageToken(t){return!("string"!=typeof t||!t.trim()||/\s/.test(t)||t.startsWith("-"))&&/^(@[a-z0-9~][\w.-]*\/)?[a-z0-9~][\w.-]*(@[\w.^~=+*:-]+)?$/i.test(t)}export function buildInstallPlan(t,e={},n=process.platform){const o=["npm","pnpm","yarn"].includes(String(e.manager||"").toLowerCase())?String(e.manager).toLowerCase():detectManager(t),r=(Array.isArray(e.packages)?e.packages:[]).map(t=>String(t).trim()).filter(Boolean);for(const t of r)if(!validPackageToken(t))throw new Error(`包名不合法(仅支持 名字[@版本],不接受选项/空白/特殊字符):${t}`);const a=!!e.dev,s=!!e.global;if(s&&!r.length)throw new Error("全局安装(global=true)需要明确的 packages 列表");let i;i=r.length?"yarn"===o?s?["global","add",...r]:["add",...a?["-D"]:[],...r]:"pnpm"===o?["add",...s?["-g"]:[],...a?["-D"]:[],...r]:["install",...s?["-g"]:[],...a?["--save-dev"]:[],...r]:["install"];return{manager:o,bin:o,argv:i,winShell:"win32"===n,display:`${o} ${i.join(" ")}`}}export async function installPackages(t,e){i(t);const n=buildInstallPlan(t,e||{});if(!which(n.bin))return`未找到 ${n.bin}:请先安装 Node.js(https://nodejs.org,macOS 可 brew install node)`+("npm"!==n.manager?',或改用 manager:"npm"':"");const o=Number.isFinite(e?.timeout)?Math.min(e.timeout,6e5):a,r=await runArgv(n.bin,n.argv,{cwd:t,timeoutMs:o,winShell:n.winShell}),s=r.killed?"【超时被终止】":`退出码 ${r.code}`,l=`$ ${n.display}\n${s}`;return 0===r.code?`${l}\n已安装完成。${c(r.output,800)?"\n"+c(r.output,800):""}`:`${l}\n${c(r.output)||"(无输出)"}`}export function buildSetupSteps(n,o={},r={}){const a=r.platform||process.platform,s=r.pathStr,i=Array.isArray(o.commands)?o.commands.map(t=>String(t).trim()).filter(Boolean).slice(0,12):null;if(i&&i.length)return{mode:"commands",commands:i,detected:[]};const c=o=>{try{return t.existsSync(e.join(n,o))}catch{return!1}},l=[];c("package.json")&&l.push("node"),(c("requirements.txt")||c("pyproject.toml"))&&l.push("python"),c("go.mod")&&l.push("go"),c("Cargo.toml")&&l.push("rust"),c("pom.xml")&&l.push("maven"),(c("build.gradle")||c("build.gradle.kts"))&&l.push("gradle");const p=Array.isArray(o.stacks)&&o.stacks.length?l.filter(t=>o.stacks.includes(t)):l,u=[],d=[],m=(t,e)=>which(t,{pathStr:s,platform:a})?null:e;for(const t of p)if("node"===t){const e=detectManager(n),o=m(e,`未找到 ${e}:请先安装 Node.js(https://nodejs.org,macOS 可 brew install node)`);u.push(o?{stack:t,label:"安装 Node.js 依赖",missing:o}:{stack:t,label:`安装 Node.js 依赖(${e} install)`,bin:e,argv:["install"],winShell:!0})}else if("python"===t){const o="win32"===a?"python":"python3",r=m(o,`未找到 ${o}:macOS 可 brew install python,或从 python.org 安装`);if(r){u.push({stack:t,label:"配置 Python 环境",missing:r});continue}c(".venv")?d.push(".venv 已存在,跳过创建"):u.push({stack:t,label:"创建 Python 虚拟环境 .venv",bin:o,argv:["-m","venv",".venv"]});const s="win32"===a?".venv\\Scripts\\pip.exe":e.join(n,".venv/bin/pip");u.push(c("requirements.txt")?{stack:t,label:"安装 Python 依赖(requirements.txt)",bin:s,argv:["install","-r","requirements.txt"]}:{stack:t,label:"安装 Python 依赖(pyproject.toml)",bin:s,argv:["install","-e","."]})}else if("go"===t){const e=m("go","未找到 go:可从 https://go.dev/dl 安装(macOS 可 brew install go)");u.push(e?{stack:t,label:"下载 Go 依赖",missing:e}:{stack:t,label:"下载 Go 依赖(go mod download)",bin:"go",argv:["mod","download"]})}else if("rust"===t){const e=m("cargo","未找到 cargo:可用 rustup 安装(https://rustup.rs)");u.push(e?{stack:t,label:"拉取 Rust 依赖",missing:e}:{stack:t,label:"拉取 Rust 依赖(cargo fetch)",bin:"cargo",argv:["fetch"]})}else if("maven"===t){const e=m("mvn","未找到 mvn:macOS 可 brew install maven");u.push(e?{stack:t,label:"解析 Maven 依赖",missing:e}:{stack:t,label:"解析 Maven 依赖(mvn dependency:resolve)",bin:"mvn",argv:["-q","-DskipTests","dependency:resolve"],winShell:!0})}else"gradle"===t&&d.push("检测到 Gradle 项目:构建脚本可执行任意代码,出于安全未自动执行;可手动运行 ./gradlew build,或让我用 run_bash 执行");return{mode:"auto",detected:l,want:p,steps:u,notes:d}}export async function setupEnvironment(t,e){i(t);const n=e||{},r=buildSetupSteps(t,n),a=Number.isFinite(n.timeout)?Math.min(n.timeout,6e5):6e5;if("commands"===r.mode){const e=["环境配置(按给定命令逐条执行):"];for(const n of r.commands){const r=await o(t,{command:n,timeout:a});if(e.push(`$ ${n}\n${r}`),!/^退出码 0\b/.test(String(r))){e.push("(上一步未成功,中止后续配置命令)");break}}return e.join("\n\n")}if(!r.detected.length)return"未检测到已知技术栈(package.json / requirements.txt / pyproject.toml / go.mod / Cargo.toml / pom.xml / build.gradle)。可改用 commands 参数显式指定要执行的配置命令。";const s=[`检测到技术栈:${r.detected.join("、")}`+(r.want.length!==r.detected.length?`(本次仅处理:${r.want.join("、")||"无"})`:"")];for(const e of r.steps){if(e.missing){s.push(`✘ ${e.label}:${e.missing}`);continue}const n=await runArgv(e.bin,e.argv,{cwd:t,timeoutMs:a,winShell:e.winShell});if(0!==n.code){s.push(`✘ ${e.label} → ${n.killed?"超时被终止":"退出码 "+n.code}${c(n.output)?"\n"+c(n.output):""}`),s.push("(该步失败,停止后续步骤;可修正后重试,或用 commands 显式指定命令)");break}s.push(`✔ ${e.label} → 退出码 0`)}for(const t of r.notes||[])s.push("· "+t);return"环境配置报告:\n"+s.join("\n")}const l=["code","code-insiders","cursor","windsurf"],p=["idea","idea64","webstorm","pycharm","goland","rider","clion"];export function buildPluginPlan(t={},e={}){const n=e.platform||process.platform,o=e.pathStr,r=String(t.id||"").trim();if(!r||r.startsWith("-")||!/^[@\w./:-]+$/.test(r))throw new Error(`插件 id 不合法:${r||"(空)"}`);const a=String(t.target||"").trim().toLowerCase();if("vscode"===a){for(const t of l)if(which(t,{pathStr:o,platform:n}))return{kind:"vscode",bin:t,argv:["--install-extension",r],winShell:!0,note:`经 ${t} CLI 安装`};throw new Error("未找到 VS Code 命令行工具(code)。请在 VS Code 命令面板执行「Shell Command: Install 'code' command in PATH」后重试,或到扩展市场手动安装 "+r)}if("jetbrains"===a){for(const t of p)if(which(t,{pathStr:o,platform:n}))return{kind:"jetbrains",bin:t,argv:["installPlugins",r],note:"安装后需重启 IDE 生效"};throw new Error("未找到 JetBrains IDE 命令行启动器(idea 等)。可在 IDE 的 Tools → Create Command-line Launcher 生成,或到 Settings → Plugins 里搜索安装 "+r)}if("npm"===a){if(!which("npm",{pathStr:o,platform:n}))throw new Error("未找到 npm:请先安装 Node.js(https://nodejs.org)");return{kind:"npm",bin:"npm",argv:["install","-g",r],winShell:!0,note:"全局 CLI 工具"}}throw new Error("target 需为 vscode | jetbrains | npm")}export async function installPlugin(t,e){const n=buildPluginPlan(e||{}),o="jetbrains"===n.kind?18e4:a,r=await runArgv(n.bin,n.argv,{cwd:t||void 0,timeoutMs:o,winShell:n.winShell}),s=`${n.bin} ${n.argv.join(" ")}`;return 0===r.code?`$ ${s}\n退出码 0\n已安装 ${e?.id}${n.note?"("+n.note+")":""}${c(r.output,600)?"\n"+c(r.output,600):""}`:`$ ${s}\n${r.killed?"【超时被终止】":"退出码 "+r.code}\n${c(r.output)||"(无输出)"}`}export async function openBrowser(t,e){const n=String(e?.url||"").trim();if(!/^https?:\/\/\S+$/i.test(n))throw new Error("仅支持 http/https 网址");const o=process.platform,r="darwin"===o?await s("open",[n]):"win32"===o?await s("rundll32",["url.dll,FileProtocolHandler",n]):await s("xdg-open",[n]);return 0!==r.code?`打开浏览器失败(退出码 ${r.code}):${c(r.output)||"未知原因"}`:`已在系统默认浏览器打开:${n}`}export async function openPath(n,o){const r=String(o?.path||"").trim();if(!r)throw new Error("缺少路径 path");const a=e.resolve(n||".",r);if(!t.existsSync(a))throw new Error("路径不存在:"+a);const i=t.statSync(a).isDirectory(),l=null!=o?.reveal?!!o.reveal:!i,p=process.platform;let u;return"darwin"===p?u=await s("open",l&&!i?["-R",a]:[a]):"win32"===p?(u=await s("explorer.exe",l&&!i?["/select,"+a]:[a]),u={code:0,output:u.output}):u=await s("xdg-open",[l&&!i?e.dirname(a):a]),0!==u.code?`打开文件管理器失败(退出码 ${u.code}):${c(u.output)||"未知原因"}`:`已在文件管理器中${l&&!i?"定位":"打开"}:${a}`}export async function openApp(t,e){const o=String(e?.name||"").trim(),r=null!=e?.target?String(e.target).trim():"";if(!o)throw new Error("缺少应用名 name");if(o.length>120||/[\0-\x1f"'`$\\;|&<>{}[\]*?%]/.test(o))throw new Error("应用名含不允许的字符:"+o);if(r&&/[\0-\x1f"'`$\\;|&<>]/.test(r))throw new Error("target 含不允许的字符:"+r);const a=process.platform;if("darwin"===a){const t=await s("open",r?["-a",o,r]:["-a",o]);return 0!==t.code?`启动应用失败(退出码 ${t.code}):${c(t.output)||"未找到应用 "+o}`:`已启动应用:${o}${r?"(打开 "+r+")":""}`}if("win32"===a)return await new Promise(t=>{const e=`start "" "${o}"${r?` "${r}"`:""}`,a=n(process.env.COMSPEC||"cmd.exe",["/d","/s","/c",e],{windowsVerbatimArguments:!0});a.on("error",e=>t(`启动应用失败:${e.message}`)),a.on("close",e=>t(0===e?`已启动应用:${o}`:`启动应用失败(退出码 ${e})`))});if(!/^[\w./+-]+$/.test(o))throw new Error("Linux 下应用名需为可执行文件名(字母数字._/+-):"+o);return await new Promise(t=>{const e=n(o,r?[r]:[],{detached:!0,stdio:"ignore"});e.once("error",e=>t(`启动应用失败:${e.message}`)),e.once("spawn",()=>{e.unref(),t(`已启动应用:${o}${r?"(打开 "+r+")":""}`)})})}const u=(t,e)=>({type:"object",properties:t,required:e||[]}),d=t=>({type:"string",description:t}),m=t=>({type:"boolean",description:t}),g=t=>({type:"integer",description:t});export const AUTOMATION_TOOL_SPECS=[{type:"function",function:{name:"install_packages",description:"安装 Node.js 依赖包(npm/pnpm/yarn 按锁文件自动识别;执行前会征询用户同意)。省略 packages 时安装 package.json 的全部依赖。仅支持包名[@版本],不接受额外命令行选项。",parameters:u({packages:{type:"array",items:{type:"string"},description:'要安装的包(如 ["lodash","@types/node@^20"]);省略=安装全部依赖'},dev:m("可选:装入 devDependencies(默认否)"),global:m("可选:全局安装(需明确 packages;默认否)"),manager:d("可选:npm | pnpm | yarn(默认按锁文件自动识别)"),timeout:g("可选:超时毫秒(默认 300000,上限 600000)")},[])}},{type:"function",function:{name:"setup_environment",description:"自动配置项目运行环境(执行前会征询用户同意):检测技术栈(Node/Python/Go/Rust/Maven)→ 检查工具链是否安装 → 安装依赖(npm install、创建 .venv 并 pip install、go mod download 等)。也可用 commands 显式指定要执行的配置命令(逐条执行,失败即停)。",parameters:u({stacks:{type:"array",items:{type:"string"},description:"可选:只处理这些技术栈(node|python|go|rust|maven)"},commands:{type:"array",items:{type:"string"},description:"可选:显式配置命令列表(优先于自动检测,最多 12 条)"},timeout:g("可选:单步超时毫秒(默认 600000)")},[])}},{type:"function",function:{name:"install_plugin",description:"安装编辑器/IDE 插件(执行前会征询用户同意):VS Code 扩展(code --install-extension)、JetBrains 插件(idea installPlugins,装后需重启 IDE)、或 npm 全局 CLI 工具(npm install -g)。",parameters:u({target:d("vscode | jetbrains | npm"),id:d("插件标识:VS Code 扩展 id(如 dbaeumer.vscode-eslint)/ JetBrains 插件 id / npm 包名")},["target","id"])}},{type:"function",function:{name:"open_browser",description:"用系统默认浏览器打开一个 http/https 网址(执行前会征询用户同意)。适合把文档、本地预览页、报告直接呈现给用户。",parameters:u({url:d("完整网址(http/https)")},["url"])}},{type:"function",function:{name:"open_path",description:"在系统文件管理器(访达/资源管理器)中打开目录或定位文件(执行前会征询用户同意)。",parameters:u({path:d("目录或文件路径(相对项目根或绝对路径)"),reveal:m("可选:定位(选中)该文件而非打开;文件默认定位、目录默认打开")},["path"])}},{type:"function",function:{name:"open_app",description:'启动本机任意应用程序(执行前会征询用户同意),可附带要打开的文件或网址。macOS 用应用名(如 "Safari"、"Visual Studio Code"),Windows 用可启动名,Linux 用可执行文件名。',parameters:u({name:d("应用名/可执行名"),target:d("可选:让该应用打开的文件路径或 URL")},["name"])}}];export const AUTOMATION_EXECUTORS={install_packages:installPackages,setup_environment:setupEnvironment,install_plugin:installPlugin,open_browser:openBrowser,open_path:openPath,open_app:openApp};
|
package/src/autorun.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function planPrompt(e){return`目标:${e}\n\n请先自动规划(不要执行任何改动):\n1) 用只读工具(read_file/glob/grep/search_code/list_files)充分调研现状;\n2) 把达成目标的完整步骤用 todo_write 列成分步计划(涉及哪些文件、每步改动要点、如何验证、风险点);\n3) 用一段话概述方案要点,然后停下——等待用户确认后才会开始执行。`}export function execPrompt(){return"计划已获用户确认。现在开始自动逐步执行你制定的计划:每完成一步用 todo_write 更新状态并简要自检;遇到与计划不符的情况及时调整并说明;改动后尽量运行测试/构建自检,失败要定位修复。全部完成后总结改了什么、如何验证。"}export async function runAutoFlow({goal:e,config:o,permissions:n,cwd:t,runTurnOnce:a,emit:r=()=>{}}){const l=n.isPlanMode?!!n.isPlanMode():!!o.planMode;r("自动规划中:先只读调研并产出分步计划…"),o.planMode=!0,n.setPlanMode?.(!0);try{await a(planPrompt(e))}finally{o.planMode=l,n.setPlanMode?.(l)}const p=await n.request("auto_execute",{goal:e},t,!1);return p.allow?(o.planMode=!1,n.setPlanMode?.(!1),r("已确认,开始自动执行计划…"),await a(execPrompt()),{approved:!0}):(r("已取消自动执行:计划已生成但未执行"+(p.reason?`(${p.reason})`:"")+"。可补充要求后重新 /auto,或 /plan approve 按现有计划执行。"),{approved:!1})}
|
package/src/commands.js
CHANGED
|
@@ -1 +1 @@
|
|
|
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 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 E,recapConversation as U}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 B}from"./memory.js";const K=/^(agent|workflow|datahuman)-/,H="\n\n# 附加可访问目录(可用 read_file/list_files/grep 以绝对路径访问;写入仍限主目录)\n";function J(n){const e=n.messages&&n.messages[0];if(!e||"system"!==e.role||"string"!=typeof e.content)return;const t=e.content.indexOf(H);let o=t>=0?e.content.slice(0,t):e.content;const i=n.extraDirs||[];i.length&&(o+=H+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 Y=[{name:"help",desc:"显示可用命令(含自定义命令)",run(n){const e=Y.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)-/.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)}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 u(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=d(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=O({state:{list:[]},emit(){}}).spec,o=!1!==n.config.memoryEnabled?B({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=_(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=x(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:"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 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(t,o){const i=E(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 R(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(d(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 m({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=g({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,k(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?v(t)?(n.config.accentColor=t,i({accentColor:t}),n.print("强调色:"+t)):n.print("可选:"+b.join(" / ")):n.print("当前强调色:"+$()+"\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),J(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),J(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) 汇总各子智能体的结论,形成整体方案或结果;\n4) 若需落地改动,再据此逐步执行并自检(可用 /verify 让我自动跑验证)。\n优先用 dispatch_agents 并行,减少串行等待。`};n.print("用法:/orchestrate <较大的目标>(我会分解为并行子任务,用 dispatch_agents 并行推进后综合)")}},{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=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: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[...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?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)&&!Y.some(n=>n.name===o))try{const r=n.readFileSync(e.join(t,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 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=Y.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}}
|
|
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}}
|
package/src/context.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import n from"node:fs";import e from"node:path";import t from"node:os";import{execSync as r}from"node:child_process";import{loadMemory as i}from"./memory.js";import{skillCatalog as o}from"./skills.js";import{buildRepoMap as s}from"./codeindex.js";const c=["- 先了解现状再动手:并行使用 read_file/glob/grep/list_files 收集信息(一轮可同时调多个只读工具),不要臆测文件内容。","- read_file 输出的「行号→」前缀仅供定位,不是文件内容;写 edit_file 的 old_string/new_string 时绝不能包含行号前缀。","- 多步骤任务先用 todo_write 列出计划,并随进度更新状态(同一时刻至多一项 in_progress);单步小事不必用。","- 改代码优先用 edit_file 做最小精确改动;新建文件才用 write_file。遵循项目既有风格与命名。","- 用 run_bash 执行构建/测试/git 等;改动后尽量运行测试或构建自检,失败要定位修复而不是忽略。","- 需要在线资料(文档/API 说明)可用 fetch_url;带明确目的使用,不要漫游。","- 破坏性或不可逆操作(删除、强推、重置等)前先说明意图;写/执行类工具会请用户授权,被拒绝时据反馈调整方案。","- 不要把密钥、令牌等敏感值回显到输出或写进代码。","- 简洁直接,用中文回答;完成后用一两句话总结改了什么、如何验证的。"];export function buildSystemPrompt(l,
|
|
1
|
+
import n from"node:fs";import e from"node:path";import t from"node:os";import{execSync as r}from"node:child_process";import{loadMemory as i}from"./memory.js";import{skillCatalog as o}from"./skills.js";import{buildRepoMap as s}from"./codeindex.js";const c=["- 先了解现状再动手:并行使用 read_file/glob/grep/list_files 收集信息(一轮可同时调多个只读工具),不要臆测文件内容。","- read_file 输出的「行号→」前缀仅供定位,不是文件内容;写 edit_file 的 old_string/new_string 时绝不能包含行号前缀。","- 多步骤任务先用 todo_write 列出计划,并随进度更新状态(同一时刻至多一项 in_progress);单步小事不必用。","- 改代码优先用 edit_file 做最小精确改动;新建文件才用 write_file。遵循项目既有风格与命名。","- 用 run_bash 执行构建/测试/git 等;改动后尽量运行测试或构建自检,失败要定位修复而不是忽略。","- 需要在线资料(文档/API 说明)可用 fetch_url;带明确目的使用,不要漫游。","- 环境与应用自动化:install_packages 装 Node.js 包、setup_environment 一键配置运行环境、install_plugin 装 VS Code/JetBrains 插件、open_browser/open_path/open_app 打开浏览器/文件管理器/任意应用。这些操作会先征询用户「是否继续」;被拒绝时不要重试,改为说明手动做法。","- 破坏性或不可逆操作(删除、强推、重置等)前先说明意图;写/执行类工具会请用户授权,被拒绝时据反馈调整方案。","- 不要把密钥、令牌等敏感值回显到输出或写进代码。","- 简洁直接,用中文回答;完成后用一两句话总结改了什么、如何验证的。"];export function buildSystemPrompt(l,a={}){const m=function(n){const e=e=>r(e,{cwd:n,stdio:["ignore","pipe","ignore"]}).toString().trim();try{const n=e("git rev-parse --abbrev-ref HEAD");let t="";try{t=e("git status --short")}catch{}return{branch:n,status:t}}catch{return null}}(l),p=function(t){for(const r of["HUICAI.md","AGENTS.md","CLAUDE.md"])try{const i=n.readFileSync(e.join(t,r),"utf8");if(i.trim())return{name:r,content:i.slice(0,8e3)}}catch{}return null}(l),u=function(){try{const r=n.readFileSync(e.join(t.homedir(),".huicai","HUICAI.md"),"utf8");return r.trim()?r.slice(0,4e3):null}catch{return null}}();let f="";if(a.persona&&a.persona.trim()?(f+="# 你的角色设定\n"+a.persona.trim()+"\n\n",f+="# 运行方式\n",f+="你现在运行在用户终端里,可通过工具在用户本机的项目里读写文件、搜索、执行 shell 命令来真正完成任务。请在保持上述角色设定的前提下遵循以下准则:\n"):(f+="你是 huicai,一个运行在用户终端里的专业自治编码与自动化智能体。\n",f+="你能通过工具在用户本机的项目里读写文件、搜索、执行 shell 命令,从而真正完成任务,而不仅是给建议。\n\n",f+="# 工作准则\n"),f+=c.join("\n")+"\n\n",f+="# 运行环境\n",f+=`- 工作目录: ${l}\n`,f+=`- 操作系统: ${t.platform()} ${t.release()} (${t.arch()})\n`,f+=`- 日期: ${(new Date).toISOString().slice(0,10)}\n`,m&&(f+=`- Git 分支: ${m.branch}\n`,m.status)){f+=`- 未提交改动:\n${m.status.split("\n").slice(0,30).map(n=>" "+n).join("\n")}\n`}const d=function(e){try{return n.readdirSync(e,{withFileTypes:!0}).filter(n=>".git"!==n.name&&"node_modules"!==n.name).slice(0,60).map(n=>n.isDirectory()?n.name+"/":n.name).join(" ")}catch{return""}}(l);d&&(f+=`- 顶层条目: ${d}\n`);const _=function(){const n=process.env.HUICAI_IDE_FILE,e=process.env.HUICAI_IDE_SELECTION,t=process.env.HUICAI_IDE_DIAGNOSTICS;if(!n&&!e&&!t)return"";let r="";n&&(r+=`- 用户当前正在编辑的文件: ${n}\n`);if(e){r+="- 用户当前选中的代码片段:\n```\n"+(e.length>4e3?e.slice(0,4e3)+"\n…(选区已截断)":e)+"\n```\n"}if(t){r+="- 编辑器诊断(错误/警告,来自 Problems 面板):\n```\n"+(t.length>4e3?t.slice(0,4e3)+"\n…(诊断已截断)":t)+"\n```\n"}return r}();if(_&&(f+="\n# 编辑器上下文(来自 IDE 集成)\n"+_),u&&(f+=`\n# 全局用户说明(~/.huicai/HUICAI.md)\n${u}\n`),p&&(f+=`\n# 项目说明(${p.name})\n${p.content}\n`),!1!==a.repoMap)try{const n=s(l);n&&(f+=`\n# 项目地图(关键文件与主要符号;用 search_code 按概念检索相关实现)\n${n}\n`)}catch{}const h=o(l);if(h&&(f+=`\n# 可用技能(本地 Agent Skills)\n${h}\n`),!1!==a.memory){const n=i(l);n&&(f+=`\n# 长期记忆(此前会话经 memory_write 记录,跨会话生效)\n${n}\n`,f+="(发现新的、值得跨会话记住的稳定事实/偏好/结论时,用 memory_write 记录。)\n")}return a.append&&String(a.append).trim()&&(f+="\n# 附加指示\n"+String(a.append).trim()+"\n"),f}
|
package/src/permissions.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{c as o}from"./colors.js";import{toolLabel as t,renderEditDiff as r,indent as e}from"./render.js";import{loadAllowRules as s,loadDenyRules as n,matchAllowRules as l,suggestRule as a,addProjectAllowRule as i,riskWarning as
|
|
1
|
+
import{c as o}from"./colors.js";import{toolLabel as t,renderEditDiff as r,indent as e}from"./render.js";import{loadAllowRules as s,loadDenyRules as n,matchAllowRules as l,suggestRule as a,addProjectAllowRule as i,riskWarning as c}from"./settings.js";import{AUTOMATION_TOOLS as u,automationActionLabel as d,automationDetail as w}from"./automation.js";export{u as AUTOMATION_TOOLS,d as automationActionLabel,w as automationDetail};export function sandboxBlocked(o){return"run_bash"===o||"fetch_url"===o||u.has(o)}export const PLAN_DENY_REASON="当前处于计划模式:只做只读调研,不执行任何改动。请先产出完整方案交用户确认;用户 /plan 关闭计划模式后再执行改动。";export const SANDBOX_DENY_REASON="当前处于沙箱模式:禁止执行 shell 命令(run_bash)、联网(fetch_url)与环境/应用自动化操作。可继续读写本地文件;如需执行/联网,请用户 /sandbox off 关闭沙箱。";export function createPermissions({yolo:f=!1,ask:p=null,cwd:y=null}={}){const _=new Set;let m=!!f,g=!1,h=!1,b=y?s(y):[],N=y?n(y):[];return{request:async function(n,f,S,$=!1){if(N.length&&l(N,n,f))return{allow:!1,reason:"该操作被拒绝规则阻止(/permissions 查看或移除)。"};if(g&&!$)return{allow:!1,reason:PLAN_DENY_REASON};if(h&&sandboxBlocked(n))return{allow:!1,reason:SANDBOX_DENY_REASON};if($)return{allow:!0};if(m||_.has(n))return{allow:!0};if(b.length&&l(b,n,f))return{allow:!0};if(!p)return{allow:!1,reason:"当前为非交互模式,未授权写/执行操作(加 --yolo 可自动放行)。"};if(u.has(n)){!function(t,r){process.stdout.write("\n"+o.bold(o.yellow("● 是否继续?"))+d(t,r)+"\n");const s=w(t,r);s&&process.stdout.write(e(o.gray(s)," ")+"\n");process.stdout.write(" "+o.bold("1、是")+" · "+o.bold("2、是,并允许在当前对话中后续相同操作不再询问")+" · "+o.bold("3、否")+"\n")}(n,f);const t=(await p(o.yellow("请选择")+" [1/2/3]: ")||"").trim();return"1"===t||/^(y|yes|是)$/i.test(t)?{allow:!0}:"2"===t||"a"===t?(_.add(n),{allow:!0}):""===t||"3"===t||/^(n|no|否)$/i.test(t)?{allow:!1,reason:"用户选择了「否」,该操作未执行。"}:{allow:!1,reason:`用户拒绝并说明:${t}`}}!function(s,n){process.stdout.write("\n"+o.bold(o.yellow("● 需要授权:"))+t(s,n)+"\n");const l=c(s,n);l&&process.stdout.write(" "+o.bold(o.red("⚠ "+l))+"\n");if("run_bash"===s)process.stdout.write(o.gray(" $ ")+String(n.command??"")+"\n");else if("write_file"===s){const t=String(n.content??"").split("\n").slice(0,12).join("\n");process.stdout.write(o.gray(` 将写入 ${n.path}:`)+"\n"+e(t)+"\n")}else"edit_file"===s?process.stdout.write(o.gray(` 编辑 ${n.path}:`)+"\n"+e(r(n.old_string,n.new_string))+"\n"):"fetch_url"===s&&process.stdout.write(o.gray(" GET ")+String(n.url??"")+"\n")}(n,f);const x=(await p(`${o.yellow("允许执行?")} [y]是 · [a]本会话该工具 · [A]永久允许(写入项目设置) · [N]否(可附原因): `)||"").trim();if("a"===x)return _.add(n),{allow:!0};if("A"===x){if(_.add(n),y){const t=a(n,f);try{const r=i(y,t);b=s(y),process.stdout.write(o.gray(` 已写入规则 ${t} → ${r}`)+"\n")}catch(t){process.stdout.write(o.yellow(` 规则写入失败(本会话仍生效):${t.message}`)+"\n")}}return{allow:!0}}const A=x.toLowerCase();return"y"===A||"yes"===A?{allow:!0}:""===x||"n"===A||"no"===A?{allow:!1,reason:"用户拒绝了该操作。"}:{allow:!1,reason:`用户拒绝并说明:${x}`}},setYolo(o){m=!!o},isYolo:()=>m,setPlanMode(o){g=!!o},isPlanMode:()=>g,setSandbox(o){h=!!o},isSandbox:()=>h,reloadRules(){b=y?s(y):[],N=y?n(y):[]}}}
|
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:"?"} 个子任务)`;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),
|
|
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} 行)`)}
|
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{loadMcpTools as
|
|
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)}
|
package/src/settings.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import r from"node:fs";import t from"node:path";import{CONFIG_DIR as n}from"./config.js";export const GLOBAL_SETTINGS_PATH=t.join(n,"settings.json");export function projectSettingsPath(r){return t.join(r,".huicai","settings.json")}function e(t){try{return JSON.parse(r.readFileSync(t,"utf8"))}catch{return null}}function s(r,t){const n=r?.permissions?.[t];return Array.isArray(n)?n.filter(r=>"string"==typeof r&&r.trim()):[]}function o(r){return s(r,"allow")}function i(r){return s(r,"deny")}export function loadAllowRules(r){const t=[...o(e(GLOBAL_SETTINGS_PATH))];return r&&t.push(...o(e(projectSettingsPath(r)))),t}export function loadDenyRules(r){const t=[...i(e(GLOBAL_SETTINGS_PATH))];return r&&t.push(...i(e(projectSettingsPath(r)))),t}export function loadRules(r){return{allow:loadAllowRules(r),deny:loadDenyRules(r)}}export function describeRules(r){const t=e(GLOBAL_SETTINGS_PATH),n=r?e(projectSettingsPath(r)):null;return{global:{allow:o(t),deny:i(t)},project:{allow:o(n),deny:i(n)}}}export function loadRawSettings(r){return{global:e(GLOBAL_SETTINGS_PATH)||{},project:r&&e(projectSettingsPath(r))||{}}}export function parseRule(r){const t=/^\s*([A-Za-z0-9_-]+)\s*(?:\((.*)\))?\s*$/.exec(String(r));return t?{name:t[1],pattern:null!=t[2]&&""!==t[2]?t[2]:null}:null}function u(r,t={}){return"run_bash"===r?String(t.command??""):"fetch_url"===r?String(t.url??""):null!=t.path?String(t.path):""}function l(r){const t=String(r).replace(/[.*+?^${}()|[\]\\]/g,r=>"*"===r?"\0":"\\"+r);return new RegExp("^"+t.replaceAll("\0","[\\s\\S]*")+"$")}export function matchAllowRules(r,t,n){for(const e of r||[]){const r=parseRule(e);if(r&&r.name===t){if(!r.pattern)return!0;try{if(l(r.pattern).test(u(t,n)))return!0}catch{}}}return!1}export function suggestRule(r,t={}){if("run_bash"===r){const r=String(t.command??"").trim().split(/\s+/)[0];return r?`run_bash(${r} *)`:"run_bash"}if("fetch_url"===r)try{return`fetch_url(${new URL(String(t.url)).origin}*)`}catch{return"fetch_url"}return r}export function addProjectRule(n,s,o){const i="deny"===s?"deny":"allow",u=projectSettingsPath(n),l=e(u)||{};return l.permissions&&"object"==typeof l.permissions||(l.permissions={}),Array.isArray(l.permissions[i])||(l.permissions[i]=[]),l.permissions[i].includes(o)||l.permissions[i].push(o),r.mkdirSync(t.dirname(u),{recursive:!0}),r.writeFileSync(u,JSON.stringify(l,null,2)+"\n"),u}export function removeProjectRule(t,n){const s=projectSettingsPath(t),o=e(s);if(!o?.permissions)return{removed:!1,path:s};let i=!1;for(const r of["allow","deny"])Array.isArray(o.permissions[r])&&o.permissions[r].includes(n)&&(o.permissions[r]=o.permissions[r].filter(r=>r!==n),i=!0);return i&&r.writeFileSync(s,JSON.stringify(o,null,2)+"\n"),{removed:i,path:s}}export function addProjectAllowRule(r,t){return addProjectRule(r,"allow",t)}export function riskWarning(r,t={}){if("run_bash"!==r)return null;const n=String(t.command??""),e=[[/\brm\s+(-\w*[rf]\w*\s+)+/i,"递归/强制删除"],[/\bsudo\b/,"提权执行"],[/\bgit\s+push\b.*(--force|-f\b)/,"强制推送"],[/\bgit\s+reset\s+--hard\b/,"硬重置(丢弃改动)"],[/\bgit\s+clean\b.*-\w*f/,"清除未跟踪文件"],[/\bchmod\s+(-R\s+)?777\b/,"开放全部权限"],[/\bdd\s+if=/,"磁盘级写入"],[/\bmkfs\b|\bdiskutil\s+erase/i,"格式化磁盘"],[/>\s*\/dev\/(sd|disk|nvme)/,"覆写设备"],[/\bDROP\s+(TABLE|DATABASE)\b/i,"删除数据表/库"],[/\bkubectl\s+delete\b|\bterraform\s+destroy\b/,"销毁云资源"]];for(const[r,t]of e)if(r.test(n))return`高风险命令(${t}),请仔细确认`;return null}
|
|
1
|
+
import r from"node:fs";import t from"node:path";import{CONFIG_DIR as n}from"./config.js";export const GLOBAL_SETTINGS_PATH=t.join(n,"settings.json");export function projectSettingsPath(r){return t.join(r,".huicai","settings.json")}function e(t){try{return JSON.parse(r.readFileSync(t,"utf8"))}catch{return null}}function s(r,t){const n=r?.permissions?.[t];return Array.isArray(n)?n.filter(r=>"string"==typeof r&&r.trim()):[]}function o(r){return s(r,"allow")}function i(r){return s(r,"deny")}export function loadAllowRules(r){const t=[...o(e(GLOBAL_SETTINGS_PATH))];return r&&t.push(...o(e(projectSettingsPath(r)))),t}export function loadDenyRules(r){const t=[...i(e(GLOBAL_SETTINGS_PATH))];return r&&t.push(...i(e(projectSettingsPath(r)))),t}export function loadRules(r){return{allow:loadAllowRules(r),deny:loadDenyRules(r)}}export function describeRules(r){const t=e(GLOBAL_SETTINGS_PATH),n=r?e(projectSettingsPath(r)):null;return{global:{allow:o(t),deny:i(t)},project:{allow:o(n),deny:i(n)}}}export function loadRawSettings(r){return{global:e(GLOBAL_SETTINGS_PATH)||{},project:r&&e(projectSettingsPath(r))||{}}}export function parseRule(r){const t=/^\s*([A-Za-z0-9_-]+)\s*(?:\((.*)\))?\s*$/.exec(String(r));return t?{name:t[1],pattern:null!=t[2]&&""!==t[2]?t[2]:null}:null}function u(r,t={}){return"run_bash"===r?String(t.command??""):"fetch_url"===r||"open_browser"===r?String(t.url??""):"open_app"===r?String(t.name??""):"install_plugin"===r?String(t.id??""):"install_packages"===r?Array.isArray(t.packages)?t.packages.join(" "):"":null!=t.path?String(t.path):""}function l(r){const t=String(r).replace(/[.*+?^${}()|[\]\\]/g,r=>"*"===r?"\0":"\\"+r);return new RegExp("^"+t.replaceAll("\0","[\\s\\S]*")+"$")}export function matchAllowRules(r,t,n){for(const e of r||[]){const r=parseRule(e);if(r&&r.name===t){if(!r.pattern)return!0;try{if(l(r.pattern).test(u(t,n)))return!0}catch{}}}return!1}export function suggestRule(r,t={}){if("run_bash"===r){const r=String(t.command??"").trim().split(/\s+/)[0];return r?`run_bash(${r} *)`:"run_bash"}if("fetch_url"===r)try{return`fetch_url(${new URL(String(t.url)).origin}*)`}catch{return"fetch_url"}return r}export function addProjectRule(n,s,o){const i="deny"===s?"deny":"allow",u=projectSettingsPath(n),l=e(u)||{};return l.permissions&&"object"==typeof l.permissions||(l.permissions={}),Array.isArray(l.permissions[i])||(l.permissions[i]=[]),l.permissions[i].includes(o)||l.permissions[i].push(o),r.mkdirSync(t.dirname(u),{recursive:!0}),r.writeFileSync(u,JSON.stringify(l,null,2)+"\n"),u}export function removeProjectRule(t,n){const s=projectSettingsPath(t),o=e(s);if(!o?.permissions)return{removed:!1,path:s};let i=!1;for(const r of["allow","deny"])Array.isArray(o.permissions[r])&&o.permissions[r].includes(n)&&(o.permissions[r]=o.permissions[r].filter(r=>r!==n),i=!0);return i&&r.writeFileSync(s,JSON.stringify(o,null,2)+"\n"),{removed:i,path:s}}export function addProjectAllowRule(r,t){return addProjectRule(r,"allow",t)}export function riskWarning(r,t={}){if("run_bash"!==r)return null;const n=String(t.command??""),e=[[/\brm\s+(-\w*[rf]\w*\s+)+/i,"递归/强制删除"],[/\bsudo\b/,"提权执行"],[/\bgit\s+push\b.*(--force|-f\b)/,"强制推送"],[/\bgit\s+reset\s+--hard\b/,"硬重置(丢弃改动)"],[/\bgit\s+clean\b.*-\w*f/,"清除未跟踪文件"],[/\bchmod\s+(-R\s+)?777\b/,"开放全部权限"],[/\bdd\s+if=/,"磁盘级写入"],[/\bmkfs\b|\bdiskutil\s+erase/i,"格式化磁盘"],[/>\s*\/dev\/(sd|disk|nvme)/,"覆写设备"],[/\bDROP\s+(TABLE|DATABASE)\b/i,"删除数据表/库"],[/\bkubectl\s+delete\b|\bterraform\s+destroy\b/,"销毁云资源"]];for(const[r,t]of e)if(r.test(n))return`高风险命令(${t}),请仔细确认`;return null}
|
package/src/tools.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import*as e from"./executors.js";import{searchCode as t}from"./codeindex.js";const
|
|
1
|
+
import*as e from"./executors.js";import{searchCode as t}from"./codeindex.js";import{AUTOMATION_TOOL_SPECS as r,AUTOMATION_EXECUTORS as i}from"./automation.js";const n=(e,t)=>({type:"object",properties:e,required:t||[]}),o=e=>({type:"string",description:e}),a=e=>({type:"integer",description:e}),s=e=>({type:"boolean",description:e});export const TOOL_SPECS=[{type:"function",function:{name:"read_file",description:"读取文本文件内容,每行带「行号→」前缀(行号仅供定位,不属于文件内容,编辑时不要包含它)。默认最多 2000 行,可用 offset(起始行,1基)/limit(行数) 分段读取大文件。",parameters:n({path:o("相对项目根的文件路径"),offset:a("可选:起始行(1基)"),limit:a("可选:读取行数")},["path"])}},{type:"function",function:{name:"list_files",description:"列出目录下的文件树(跳过 .git/node_modules 等)。可选 path 指定子目录(默认项目根)。",parameters:n({path:o("可选:子目录路径,默认当前项目根")},[])}},{type:"function",function:{name:"glob",description:'按 glob 模式查找文件(支持 ** 与 *),如 "**/*.ts"、"src/**/*.java"。',parameters:n({pattern:o("glob 模式")},["pattern"])}},{type:"function",function:{name:"search_code",description:"按「概念/功能」检索最相关的代码块(BM25 相关性排序,比 grep 更懂「哪段代码在做这件事」)。适合「登录逻辑在哪」「哪里处理支付回调」这类找相关实现;要找精确字符串仍用 grep。返回 文件:行范围 + 代码块。",parameters:n({query:o("要找的功能/概念(自然语言或关键词组合,多写几个相关词更准)"),k:a("可选:返回块数,默认 8,上限 20")},["query"])}},{type:"function",function:{name:"grep",description:"在文件内容中按正则搜索,返回 匹配文件:行号: 该行。可选 path 限定目录、glob 限定文件名模式、ignore_case 忽略大小写、files_only 只列出命中文件。",parameters:n({pattern:o("正则表达式"),path:o("可选:搜索目录,默认项目根"),glob:o("可选:文件名 glob 过滤"),ignore_case:s("可选:忽略大小写(默认否)"),files_only:s("可选:只返回命中文件列表,不含匹配行(默认否)"),max_results:a("可选:最多返回条数(默认 200,上限 500)")},["pattern"])}},{type:"function",function:{name:"write_file",description:"把文本写入文件(覆盖式,自动建父目录)。仅限当前项目目录内。仅用于新建文件或全量重写;小改动优先 edit_file。",parameters:n({path:o("相对项目根的文件路径"),content:o("完整文件内容")},["path","content"])}},{type:"function",function:{name:"edit_file",description:"精确替换文件中的片段:old_string 必须是文件原文(不含 read_file 的行号前缀)且在文件中唯一(除非 replace_all=true)。仅限项目目录内。",parameters:n({path:o("相对项目根的文件路径"),old_string:o("被替换的原文(需在文件中唯一,除非 replace_all;不要包含行号前缀)"),new_string:o("替换为的新内容"),replace_all:s("是否替换全部匹配(默认否)")},["path","old_string","new_string"])}},{type:"function",function:{name:"run_bash",description:"在项目目录下执行一条 shell 命令并返回输出(stdout+stderr+退出码)。用于构建、测试、git、脚手架等。默认超时 120s(可用 timeout 调整)。避免用它读文件/搜索(用 read_file/grep 更高效)。",parameters:n({command:o("要执行的 shell 命令"),timeout:a("可选:超时毫秒")},["command"])}},{type:"function",function:{name:"fetch_url",description:"抓取一个 http/https 网页或接口(GET),HTML 自动转为纯文本。用于查在线文档、API 说明、Issue 等。结果可能很长,尽量带着明确目的使用。",parameters:n({url:o("完整 URL(http/https)"),timeout:a("可选:超时毫秒,默认 30000")},["url"])}},...r];export const READONLY_TOOLS=new Set(["read_file","list_files","glob","grep","search_code"]);const p={read_file:e.readFile,list_files:e.listFiles,glob:e.glob,grep:e.grep,search_code:t,write_file:e.writeFile,edit_file:e.editFile,run_bash:e.runBash,fetch_url:e.fetchUrl,...i};export async function runTool(e,t,r,i){const n=p[e];if(!n)return`未知工具:${e}`;try{return await n(r,t||{},i)}catch(e){return`工具执行失败:${e.message}`}}
|
package/src/tui/app.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import r,{useState as e,useRef as t,useReducer as n,useEffect as o,useMemo as s}from"react";import{execSync as c}from"node:child_process";import{render as l,Box as u,Text as i,Static as a,useInput as m,useApp as p}from"ink";import{runTurn as f}from"../agent.js";import{fetchUsage as g}from"../api.js";import{toolLabel as d}from"../render.js";import{renderMarkdown as h,looksLikeMarkdown as x}from"../markdown.js";import{renderWelcome as y}from"../banner.js";import{createSession as w,saveSession as b}from"../session.js";import{createCheckpoints as v}from"../checkpoints.js";import{buildExtraTools as k}from"../repl.js";import{closeMcp as T}from"../mcp.js";import{runSlash as $,listCommands as j}from"../commands.js";import{usageStatus as S,fmtReset as A}from"../usage.js";import{expandAtMentions as I,buildUserContent as P,runBangCommand as E,loadHistory as M,appendHistory as L}from"../input.js";import{loadAllowRules as _,loadDenyRules as C,matchAllowRules as D,suggestRule as R,addProjectAllowRule as U,riskWarning as W}from"../settings.js";import{PLAN_DENY_REASON as F,SANDBOX_DENY_REASON as K}from"../permissions.js";import{runVerifyLoop as Y}from"../autofix.js";const q=r.createElement;function B(r){return(r=Number(r)||0)>=1e3?(r/1e3).toFixed(1)+"k":String(r)}const G=["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"];export function App({config:r,cwd:l,session:y,extraTools:k,yolo:T,initialPrompt:N,version:O,onExit:Q}){const X=p(),z=t([]),H=t(0),V=t(""),Z=t(y),rr=t(null),er=t(null),tr=t(new Set),nr=t(_(l)),or=t(C(l)),sr=t(!1),cr=t(!1),lr=t(v()),ur=t(!!T),ir=t(!1),ar=t("idle"),mr=t([]),pr=t({list:[]}),fr=t(M(l)),gr=t(null),dr=t(""),[,hr]=n(r=>r+1,0),[xr,yr]=n(r=>r+1,0),[wr,br]=e(0),[vr,kr]=e("idle"),[Tr,$r]=e({text:"",cursor:0}),[jr,Sr]=e(null),[Ar,Ir]=e({prompt:0,completion:0}),[Pr,Er]=e(0),[Mr,Lr]=e([]),[_r,Cr]=e(0),[Dr,Rr]=e(!1),[Ur,Wr]=e(""),Fr=t(null),[Kr,Yr]=e(0),[qr,Br]=e(null),Gr=t(null),[Jr,Nr]=e(0),[Or,Qr]=e(!1),Xr=s(()=>j(l),[l]),zr=(r,e)=>{z.current.push({id:H.current++,kind:r,text:e}),yr()},Hr=()=>{if(V.current){const e=V.current,t=!1!==r.markdown&&x(e);zr("assistant",t?h(e):e),V.current=""}},Vr=r=>{ar.current=r?"busy":"idle",kr(ar.current)};o(()=>{if("busy"!==vr)return;const r=setInterval(()=>Yr(r=>(r+1)%G.length),80);return()=>clearInterval(r)},[vr]);const Zr=async()=>{if(r.apiKey)try{const e=await g(r.baseUrl,r.apiKey);Gr.current=e,Br(e)}catch{}};o(()=>{Zr();const r=setInterval(()=>{Zr()},18e4);return()=>clearInterval(r)},[]),o(()=>{if(!r.statusLine)return void Wr("");const e=()=>{try{const e=c(r.statusLine,{cwd:l,timeout:3e3,stdio:["ignore","pipe","ignore"]}).toString().split("\n")[0].slice(0,80);Wr(e)}catch{Wr("")}};e();const t=setInterval(e,5e3);return()=>clearInterval(t)},[r.statusLine]),o(()=>()=>{Fr.current&&clearInterval(Fr.current)},[]);const re=t(""),ee={onDelta:r=>{re.current&&(re.current=""),V.current+=r,hr()},onReasoning:r=>{re.current=(re.current+r).slice(-400),hr()},onToolStart:(r,e)=>{re.current="",Hr(),zr("tool",d(r,e))},onToolResult:(r,e)=>zr("toolresult",function(r,e=200){const t=String(r).split("\n")[0];return t.length>e?t.slice(0,e)+"…":t}(e)),onFileDiff:r=>zr("diff",r),onSkip:(r,e)=>zr("skip",d(r,e)+" — 已跳过"),onInfo:r=>{Hr(),zr("info",r)},onTodo:r=>Lr(r),onTurnEnd:()=>{re.current="",Hr()}},te={request:(r,e,t,n)=>or.current.length&&D(or.current,r,e)?Promise.resolve({allow:!1,reason:"该操作被拒绝规则阻止(/permissions 查看或移除)。"}):sr.current&&!n?Promise.resolve({allow:!1,reason:F}):!cr.current||"run_bash"!==r&&"fetch_url"!==r?n||ur.current||tr.current.has(r)||D(nr.current,r,e)?Promise.resolve({allow:!0}):new Promise(t=>{er.current={name:r,args:e,resolve:t},Sr({label:d(r,e),name:r,args:e,risk:W(r,e)})}):Promise.resolve({allow:!1,reason:K}),isYolo:()=>ur.current,setYolo:r=>{ur.current=!!r},setPlanMode:r=>{sr.current=!!r,Rr(!!r)},isPlanMode:()=>sr.current,setSandbox:r=>{cr.current=!!r},isSandbox:()=>cr.current,reloadRules:()=>{nr.current=_(l),or.current=C(l)}},ne=()=>{const r=mr.current.shift();return Cr(mr.current.length),null==r?null:r},oe=async e=>{const t=S(Gr.current);if(t&&"exhausted"===t.level)return void zr("info",`✗ Token 用量窗口(${t.label})已用尽,暂无法继续。${A(t.resetAtEpochMs)}`);const{text:n,images:o}=await I(e,l,r),s=lr.current.snapshot({session:Z.current,label:e});Z.current.messages.push({role:"user",content:P(n,o)}),Vr(!0),rr.current=new AbortController,Z.current.compaction||(Z.current.compaction={});const c=await f({config:r,messages:Z.current.messages,permissions:te,cwd:l,extraTools:k,events:ee,signal:rr.current.signal,todoState:pr.current,compaction:Z.current.compaction,pullQueued:ne,fileJournal:s});return rr.current=null,Hr(),c?.usage&&Ir(r=>({prompt:r.prompt+c.usage.prompt,completion:r.completion+c.usage.completion})),c?.lastPromptTokens&&(Er(c.lastPromptTokens),Z.current.lastPromptTokens=c.lastPromptTokens),b(Z.current),(c?.aborted||c?.error)&&mr.current.length&&(mr.current=[],Cr(0),zr("info","已清空排队消息")),Vr(!1),Zr(),c},se=()=>{Q?.(),X.exit()},ce=async r=>{if("busy"===ar.current)return void zr("info","正在处理上一条,! 命令请稍候再试");zr("user","! "+r);const{output:e,record:t}=await E(r,l);zr("toolresult",function(r,e=12){const t=String(r).split("\n");return t.length<=e?r:t.slice(0,e).join("\n")+`\n…(共 ${t.length} 行)`}(e)),Z.current.messages.push({role:"user",content:t}),b(Z.current)},le=async e=>{const t=e.trim();if($r({text:"",cursor:0}),gr.current=null,t){if(L(l,t),fr.current[fr.current.length-1]!==t&&fr.current.push(t),t.startsWith("/"))return(async e=>{const t={version:O,config:r,cwd:l,usage:Ar,permissions:te,extraTools:k,session:Z.current,checkpoints:lr.current,print:r=>zr("info",String(r))},n=await $(e,t);if("exit"===n.action)return se();"clear"===n.action&&(Z.current=w({cwd:l,model:r.model,messages:[Z.current.messages[0]]}),pr.current.list=[],lr.current.list.length=0,Lr([]),Er(0),z.current=[],V.current="",br(r=>r+1),zr("info","已开启新会话(旧会话仍可 --resume 恢复)")),"refresh"===n.action&&(pr.current.list=[],Lr([]),Er(0)),"autofix"===n.action&&await Y({command:n.command,maxIters:n.maxIters,cwd:l,runTurnOnce:r=>oe(r),emit:r=>zr("info",r)}),"cd"===n.action&&zr("info","富 TUI 暂不支持 /cd 切换目录;请用 huicai --plain(普通模式)使用,或退出后到目标目录重开。"),"loop"===n.action&&(Fr.current&&(clearInterval(Fr.current),Fr.current=null),n.stop?zr("info","已停止循环。"):(zr("info",`已开始循环:每 ${n.seconds}s 运行「${n.prompt}」(/loop off 停止)`),Fr.current=setInterval(()=>{"busy"!==ar.current&&(zr("user",n.prompt),oe(n.prompt))},1e3*n.seconds))),n.prompt&&("busy"===ar.current?(mr.current.push(n.prompt),Cr(mr.current.length)):(zr("user",n.prompt),await oe(n.prompt)))})(t);if(t.startsWith("!")&&t.length>1)return ce(t.slice(1).trim());if("busy"===ar.current)return zr("user",t),mr.current.push(t),void Cr(mr.current.length);zr("user",t),await oe(t)}};o(()=>{!ir.current&&N&&(ir.current=!0,le(N))},[]);const ue=/^\/(\S*)$/.exec(Tr.text),ie=ue?ue[1]:null,ae=null==ie?[]:Xr.filter(r=>r.name.toLowerCase().startsWith(ie.toLowerCase())).slice(0,8),me=ae.length>0&&!Or;o(()=>{Nr(0),Qr(!1)},[ie]),m((r,e)=>{if(er.current){const t=er.current,n=r=>{er.current=null,Sr(null),t.resolve(r)};if("y"===r)return n({allow:!0});if("a"===r)return tr.current.add(t.name),n({allow:!0});if("A"===r){tr.current.add(t.name);const r=R(t.name,t.args);try{U(l,r),nr.current=_(l),zr("info",`已写入允许规则:${r} → .huicai/settings.json`)}catch(r){zr("info",`规则写入失败(本会话仍生效):${r.message}`)}return n({allow:!0})}return"n"===r||e.escape||e.return?n({allow:!1,reason:"用户拒绝了该操作。"}):void 0}if(me){const r=()=>ae[Math.min(Jr,ae.length-1)];if(e.upArrow)return Nr(r=>(r-1+ae.length)%ae.length);if(e.downArrow)return Nr(r=>(r+1)%ae.length);if(e.tab){const e=r().name;return $r({text:"/"+e+" ",cursor:e.length+2})}if(e.return)return void le("/"+r().name);if(e.escape)return Qr(!0)}if(e.escape)"busy"===ar.current&&rr.current&&(rr.current.abort(),zr("info","已请求中断(Esc)"));else if(e.ctrl&&"c"===r)"busy"===ar.current&&rr.current?(rr.current.abort(),zr("info","已请求中断")):se();else if(e.return)le(Tr.text);else{if(e.upArrow)return(()=>{const r=fr.current;if(!r.length)return;null==gr.current?(dr.current=Tr.text,gr.current=r.length-1):gr.current>0&&gr.current--;const e=r[gr.current]??"";$r({text:e,cursor:e.length})})();if(e.downArrow)return(()=>{if(null==gr.current)return;const r=fr.current;if(gr.current<r.length-1){gr.current++;const e=r[gr.current];$r({text:e,cursor:e.length})}else gr.current=null,$r({text:dr.current,cursor:dr.current.length})})();if(e.leftArrow)return $r(r=>({...r,cursor:Math.max(0,r.cursor-1)}));if(e.rightArrow)return $r(r=>({...r,cursor:Math.min(r.text.length,r.cursor+1)}));if(e.ctrl&&"a"===r)return $r(r=>({...r,cursor:0}));if(e.ctrl&&"e"===r)return $r(r=>({...r,cursor:r.text.length}));if(e.ctrl&&"u"===r)return $r(r=>({text:r.text.slice(r.cursor),cursor:0}));if(e.ctrl&&"k"===r)return $r(r=>({text:r.text.slice(0,r.cursor),cursor:r.cursor}));if(e.ctrl&&"w"===r)return $r(r=>{const e=r.text.slice(0,r.cursor).replace(/\S+\s*$/,"");return{text:e+r.text.slice(r.cursor),cursor:e.length}});if(e.backspace||e.delete)return gr.current=null,$r(r=>r.cursor>0?{text:r.text.slice(0,r.cursor-1)+r.text.slice(r.cursor),cursor:r.cursor-1}:r);if(r&&!e.ctrl&&!e.meta){gr.current=null;const e=r.replace(/\r\n?/g,"\n");$r(r=>({text:r.text.slice(0,r.cursor)+e+r.text.slice(r.cursor),cursor:r.cursor+e.length}))}}});const pe=s(()=>z.current.slice(),[xr]),fe=l.split("/").filter(Boolean).pop()||l,ge=Tr.text.slice(0,Tr.cursor),de=Tr.text[Tr.cursor]??" ",he=Tr.text.slice(Tr.cursor+1),xe=Math.max(8,process.stdout.columns||80),ye=S(qr),we=ye?"exhausted"===ye.level?`⛔ ${ye.label}用量已用尽${A(ye.resetAtEpochMs)?" · "+A(ye.resetAtEpochMs):""}`:`⚠ ${ye.label}用量 ${Math.round(100*ye.pct)}%(${B(ye.used)}/${B(ye.limit)})`:"";return q(u,{flexDirection:"column"},q(a,{key:wr,items:pe},r=>q(J,{key:r.id,item:r})),re.current?q(u,null,q(i,{color:"gray",italic:!0},"✦ 思考 "+re.current.replace(/\n/g," ").slice(-160))):null,V.current?q(u,{marginTop:0},q(i,null,V.current)):null,Mr.length?q(u,{flexDirection:"column",paddingLeft:2,marginTop:1},...Mr.map((r,e)=>q(i,{key:e,color:"completed"===r.status?"green":"in_progress"===r.status?"cyan":"gray",dimColor:"completed"===r.status,bold:"in_progress"===r.status},("completed"===r.status?"☑ ":"in_progress"===r.status?"◐ ":"☐ ")+r.content))):null,"busy"!==vr||V.current?null:q(u,null,q(i,{color:"cyan"},G[Kr]+" 思考中…"),q(i,{color:"gray"},"(Esc 中断 · 输入回车可排队补充)")),jr?q(u,{flexDirection:"column",borderStyle:"round",borderColor:"yellow",paddingX:1,marginTop:1},q(i,{color:"yellow",bold:!0},"● 需要授权:"+jr.label),jr.risk?q(i,{color:"red",bold:!0},"⚠ "+jr.risk):null,"run_bash"===jr.name?q(i,{color:"gray"},"$ "+(jr.args.command||"")):null,"fetch_url"===jr.name?q(i,{color:"gray"},"GET "+(jr.args.url||"")):null,q(i,null,"[y] 允许一次 · [a] 本会话 · [A] 永久(写入项目设置) · [n/Esc] 拒绝")):null,me?q(u,{flexDirection:"column",marginTop:1},q(i,{color:"gray"}," 斜杠命令 · ↑↓ 选择 · Tab 补全 · ⏎ 执行 · Esc 关闭"),...ae.map((r,e)=>q(u,{key:r.name},q(i,{color:"cyan",bold:e===Jr},(e===Jr?" ❯ ":" ")+"/"+r.name+(r.args?" "+r.args:"")),q(i,{color:"gray"}," "+String(r.desc||"").replace(/\s+/g," ").slice(0,46))))):null,q(u,{marginTop:me?0:1},q(i,{color:"blue"},"─".repeat(xe))),q(u,null,q(i,{color:"busy"===vr?"yellow":"cyan"},"busy"===vr?"⧗ ":"› "),q(i,null,ge),q(i,{inverse:!0},de),q(i,null,he)),q(i,{color:"blue"},"─".repeat(xe)),q(u,{justifyContent:"space-between"},q(i,{color:"gray"},(Ur?Ur+" · ":"")+`${fe} · ${r.model}${ur.current?" · ⚠ yolo":""}${Dr?" · 📋 计划":""}`+(_r?` · 排队 ${_r}`:"")+" · /help"),we?q(i,{color:"exhausted"===ye.level?"red":"yellow",bold:!0},we):q(i,null,"")))}function J({item:r}){const{kind:e,text:t}=r;if("user"===e)return q(u,{marginTop:1},q(i,{color:"cyan",bold:!0},"› "),q(i,{bold:!0},t));if("assistant"===e)return q(u,null,q(i,null,t));if("diff"===e)return q(u,{marginLeft:2},q(i,null,t));if("tool"===e)return q(i,{color:"cyan"}," ⏺ "+t);if("toolresult"===e)return q(i,{color:"gray"}," "+t);if("skip"===e)return q(i,{color:"gray"}," ⊘ "+t);if("info"===e){const r=/✗|失败|错误|error/i.test(t);return q(i,{color:r?"red":"yellow"},(r?" ✗ ":" · ")+t.replace(/^✗\s*/,""))}return q(i,null,t)}export async function runInk({config:r,cwd:e,session:t,yolo:n,initialPrompt:o,version:s}){process.stdout.write(y({model:r.model,cwd:e,version:s,platform:/^(agent|workflow|datahuman)-/.test(String(r.model||"")),yolo:n,consult:r.consult?r.consultLabel||r.consult:null,ink:!0})+"\n");const{extraTools:c,clients:u}=await k(r,e),{waitUntilExit:i}=l(q(App,{config:r,cwd:e,session:t,extraTools:c,yolo:n,initialPrompt:o,version:s,onExit:()=>T(u)}));await i(),T(u)}
|
|
1
|
+
import r,{useState as t,useRef as e,useReducer as n,useEffect as o,useMemo as s}from"react";import{execSync as c}from"node:child_process";import{render as l,Box as u,Text as i,Static as a,useInput as m,useApp as p}from"ink";import{runTurn as f}from"../agent.js";import{fetchUsage as g}from"../api.js";import{toolLabel as d}from"../render.js";import{renderMarkdown as h,looksLikeMarkdown as w}from"../markdown.js";import{renderWelcome as x}from"../banner.js";import{createSession as y,saveSession as b}from"../session.js";import{createCheckpoints as v}from"../checkpoints.js";import{buildExtraTools as k}from"../repl.js";import{buildSystemPrompt as T}from"../context.js";import{closeMcp as $}from"../mcp.js";import{runSlash as j,listCommands as E}from"../commands.js";import{usageStatus as P,fmtReset as S}from"../usage.js";import{expandAtMentions as M,buildUserContent as A,runBangCommand as I,loadHistory as D,appendHistory as L}from"../input.js";import{loadAllowRules as C,loadDenyRules as _,matchAllowRules as U,suggestRule as R,addProjectAllowRule as W,riskWarning as F}from"../settings.js";import{PLAN_DENY_REASON as K,SANDBOX_DENY_REASON as O,sandboxBlocked as Y,AUTOMATION_TOOLS as q,automationActionLabel as B,automationDetail as G}from"../permissions.js";import{runVerifyLoop as J}from"../autofix.js";import{runAutoFlow as N}from"../autorun.js";const Q=r.createElement,X=10;function z(r,t=12){const e=String(r).split("\n");return e.length<=t?r:e.slice(0,t).join("\n")+`\n…(共 ${e.length} 行)`}function H(r){return(r=Number(r)||0)>=1e3?(r/1e3).toFixed(1)+"k":String(r)}const V=["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"];export function App({config:r,cwd:l,session:x,extraTools:rr,mcp:tr,yolo:er,initialPrompt:nr,version:or,onExit:sr}){const cr=p(),[lr,ur]=t(l),ir=e(rr),ar=e([]),mr=e(0),pr=e(""),fr=e(x),gr=e(null),dr=e(null),hr=e(new Set),wr=e(C(lr)),xr=e(_(lr)),yr=e(!1),br=e(!1),vr=e(v()),kr=e(!!er),Tr=e(!1),$r=e("idle"),jr=e([]),Er=e({list:[]}),Pr=e(D(lr)),Sr=e(null),Mr=e(""),[,Ar]=n(r=>r+1,0),[Ir,Dr]=n(r=>r+1,0),[Lr,Cr]=t(0),[_r,Ur]=t("idle"),[Rr,Wr]=t({text:"",cursor:0}),[Fr,Kr]=t(null),[Or,Yr]=t({prompt:0,completion:0}),[qr,Br]=t(0),[Gr,Jr]=t([]),[Nr,Qr]=t(0),[Xr,zr]=t(!1),[Hr,Vr]=t(""),Zr=e(null),[rt,tt]=t(0),[et,nt]=t(null),ot=e(null),[st,ct]=t(0),[lt,ut]=t(!1),it=s(()=>E(lr),[lr]),at=(r,t)=>{ar.current.push({id:mr.current++,kind:r,text:t}),Dr()},mt=()=>{if(pr.current){const t=pr.current,e=!1!==r.markdown&&w(t);at("assistant",e?h(t):t),pr.current=""}},pt=r=>{$r.current=r?"busy":"idle",Ur($r.current)};o(()=>{if("busy"!==_r)return;const r=setInterval(()=>tt(r=>(r+1)%V.length),80);return()=>clearInterval(r)},[_r]);const ft=async()=>{if(r.apiKey)try{const t=await g(r.baseUrl,r.apiKey);ot.current=t,nt(t)}catch{}};o(()=>{ft();const r=setInterval(()=>{ft()},18e4);return()=>clearInterval(r)},[]),o(()=>{if(!r.statusLine)return void Vr("");const t=()=>{try{const t=c(r.statusLine,{cwd:lr,timeout:3e3,stdio:["ignore","pipe","ignore"]}).toString().split("\n")[0].slice(0,80);Vr(t)}catch{Vr("")}};t();const e=setInterval(t,5e3);return()=>clearInterval(e)},[r.statusLine]),o(()=>()=>{Zr.current&&clearInterval(Zr.current)},[]);const gt=e(""),dt={onDelta:r=>{gt.current&&(gt.current=""),pr.current+=r,Ar()},onReasoning:r=>{gt.current=(gt.current+r).slice(-400),Ar()},onToolStart:(r,t)=>{gt.current="",mt(),at("tool",d(r,t))},onToolResult:(r,t)=>at("toolresult",function(r,t=200){const e=String(r).split("\n")[0];return e.length>t?e.slice(0,t)+"…":e}(t)),onFileDiff:r=>at("diff",r),onSkip:(r,t)=>at("skip",d(r,t)+" — 已跳过"),onInfo:r=>{mt(),at("info",r)},onTodo:r=>Jr(r),onTurnEnd:()=>{gt.current="",mt()}},ht={request:(r,t,e,n)=>{if(xr.current.length&&U(xr.current,r,t))return Promise.resolve({allow:!1,reason:"该操作被拒绝规则阻止(/permissions 查看或移除)。"});if(yr.current&&!n)return Promise.resolve({allow:!1,reason:K});if(br.current&&Y(r))return Promise.resolve({allow:!1,reason:O});if(n||kr.current||hr.current.has(r)||U(wr.current,r,t))return Promise.resolve({allow:!0});const o=q.has(r);return new Promise(e=>{dr.current={name:r,args:t,resolve:e,consent:o},Kr({label:o?B(r,t):d(r,t),detail:o?G(r,t):"",name:r,args:t,consent:o,risk:F(r,t)})})},isYolo:()=>kr.current,setYolo:r=>{kr.current=!!r},setPlanMode:r=>{yr.current=!!r,zr(!!r)},isPlanMode:()=>yr.current,setSandbox:r=>{br.current=!!r},isSandbox:()=>br.current,reloadRules:()=>{wr.current=C(lr),xr.current=_(lr)}},wt=()=>{const r=jr.current.shift();return Qr(jr.current.length),null==r?null:r},xt=async t=>{const e=P(ot.current);if(e&&"exhausted"===e.level)return void at("info",`✗ Token 用量窗口(${e.label})已用尽,暂无法继续。${S(e.resetAtEpochMs)}`);const{text:n,images:o}=await M(t,lr,r),s=vr.current.snapshot({session:fr.current,label:t});fr.current.messages.push({role:"user",content:A(n,o)}),pt(!0),gr.current=new AbortController,fr.current.compaction||(fr.current.compaction={});const c=await f({config:r,messages:fr.current.messages,permissions:ht,cwd:lr,extraTools:ir.current,events:dt,signal:gr.current.signal,todoState:Er.current,compaction:fr.current.compaction,pullQueued:wt,fileJournal:s});return gr.current=null,mt(),c?.usage&&Yr(r=>({prompt:r.prompt+c.usage.prompt,completion:r.completion+c.usage.completion})),c?.lastPromptTokens&&(Br(c.lastPromptTokens),fr.current.lastPromptTokens=c.lastPromptTokens),b(fr.current),(c?.aborted||c?.error)&&jr.current.length&&(jr.current=[],Qr(0),at("info","已清空排队消息")),pt(!1),ft(),c},yt=()=>{sr?.(),cr.exit()},bt=async t=>{const e=t.trim();if(Wr({text:"",cursor:0}),Sr.current=null,e){if(L(lr,e),Pr.current[Pr.current.length-1]!==e&&Pr.current.push(e),e.startsWith("/"))return(async t=>{const e={version:or,config:r,cwd:lr,usage:Or,permissions:ht,extraTools:ir.current,session:fr.current,checkpoints:vr.current,print:r=>at("info",String(r))},n=await j(t,e);if("exit"===n.action)return yt();if("clear"===n.action&&(fr.current=y({cwd:lr,model:r.model,messages:[fr.current.messages[0]]}),Er.current.list=[],vr.current.list.length=0,Jr([]),Br(0),ar.current=[],pr.current="",Cr(r=>r+1),at("info","已开启新会话(旧会话仍可 --resume 恢复)")),"refresh"===n.action&&(Er.current.list=[],Jr([]),Br(0)),"autofix"===n.action&&await J({command:n.command,maxIters:n.maxIters,cwd:lr,runTurnOnce:r=>xt(r),emit:r=>at("info",r)}),"autorun"===n.action&&await N({goal:n.goal,config:r,permissions:ht,cwd:lr,runTurnOnce:r=>xt(r),emit:r=>at("info",r)}),"cd"===n.action){wr.current=C(n.cwd),xr.current=_(n.cwd),$(tr?.clients);const t=await k(r,n.cwd);ir.current=t.extraTools,tr&&(tr.clients=t.clients),fr.current=y({cwd:n.cwd,model:r.model,messages:[{role:"system",content:T(n.cwd,{memory:!1!==r.memoryEnabled})}]}),r.sessionId=fr.current.id,Er.current.list=[],vr.current.list.length=0,Jr([]),Br(0),ar.current=[],pr.current="",Cr(r=>r+1),ur(n.cwd),at("info","已切换到 "+n.cwd+"(新会话,旧会话已保存)")}"loop"===n.action&&(Zr.current&&(clearInterval(Zr.current),Zr.current=null),n.stop?at("info","已停止循环。"):(at("info",`已开始循环:每 ${n.seconds}s 运行「${n.prompt}」(/loop off 停止)`),Zr.current=setInterval(()=>{"busy"!==$r.current&&(at("user",n.prompt),xt(n.prompt))},1e3*n.seconds))),n.prompt&&("busy"===$r.current?(jr.current.push(n.prompt),Qr(jr.current.length)):(at("user",n.prompt),await xt(n.prompt)))})(e);if(e.startsWith("!")&&e.length>1)return(async r=>{if("busy"===$r.current)return void at("info","正在处理上一条,! 命令请稍候再试");at("user","! "+r);const{output:t,record:e}=await I(r,lr);at("toolresult",z(t)),fr.current.messages.push({role:"user",content:e}),b(fr.current)})(e.slice(1).trim());if("busy"===$r.current)return at("user",e),jr.current.push(e),void Qr(jr.current.length);at("user",e),await xt(e)}};o(()=>{!Tr.current&&nr&&(Tr.current=!0,bt(nr))},[]);const vt=/^\/(\S*)$/.exec(Rr.text),kt=vt?vt[1]:null,Tt=null==kt?[]:it.filter(r=>r.name.toLowerCase().startsWith(kt.toLowerCase())),$t=Tt.length>0&&!lt,jt=Tt.length<=X?0:Math.max(0,Math.min(st-Math.floor(5),Tt.length-X)),Et=Tt.slice(jt,jt+X);o(()=>{ct(0),ut(!1)},[kt]),m((r,t)=>{if(dr.current){const e=dr.current,n=r=>{dr.current=null,Kr(null),e.resolve(r)};if("y"===r||"1"===r)return n({allow:!0});if("a"===r||"2"===r)return hr.current.add(e.name),n({allow:!0});if("3"===r)return n({allow:!1,reason:"用户选择了「否」,该操作未执行。"});if("A"===r){hr.current.add(e.name);const r=R(e.name,e.args);try{W(lr,r),wr.current=C(lr),at("info",`已写入允许规则:${r} → .huicai/settings.json`)}catch(r){at("info",`规则写入失败(本会话仍生效):${r.message}`)}return n({allow:!0})}return"n"===r||t.escape||t.return?n({allow:!1,reason:"用户拒绝了该操作。"}):void 0}if($t){const r=()=>Tt[Math.min(st,Tt.length-1)];if(t.upArrow)return ct(r=>(r-1+Tt.length)%Tt.length);if(t.downArrow)return ct(r=>(r+1)%Tt.length);if(t.pageUp)return ct(r=>Math.max(0,r-X));if(t.pageDown)return ct(r=>Math.min(Tt.length-1,r+X));if(t.tab){const t=r().name;return Wr({text:"/"+t+" ",cursor:t.length+2})}if(t.return)return void bt("/"+r().name);if(t.escape)return ut(!0)}if(t.escape)"busy"===$r.current&&gr.current&&(gr.current.abort(),at("info","已请求中断(Esc)"));else if(t.ctrl&&"c"===r)"busy"===$r.current&&gr.current?(gr.current.abort(),at("info","已请求中断")):yt();else if(t.return)bt(Rr.text);else{if(t.upArrow)return(()=>{const r=Pr.current;if(!r.length)return;null==Sr.current?(Mr.current=Rr.text,Sr.current=r.length-1):Sr.current>0&&Sr.current--;const t=r[Sr.current]??"";Wr({text:t,cursor:t.length})})();if(t.downArrow)return(()=>{if(null==Sr.current)return;const r=Pr.current;if(Sr.current<r.length-1){Sr.current++;const t=r[Sr.current];Wr({text:t,cursor:t.length})}else Sr.current=null,Wr({text:Mr.current,cursor:Mr.current.length})})();if(t.leftArrow)return Wr(r=>({...r,cursor:Math.max(0,r.cursor-1)}));if(t.rightArrow)return Wr(r=>({...r,cursor:Math.min(r.text.length,r.cursor+1)}));if(t.ctrl&&"a"===r)return Wr(r=>({...r,cursor:0}));if(t.ctrl&&"e"===r)return Wr(r=>({...r,cursor:r.text.length}));if(t.ctrl&&"u"===r)return Wr(r=>({text:r.text.slice(r.cursor),cursor:0}));if(t.ctrl&&"k"===r)return Wr(r=>({text:r.text.slice(0,r.cursor),cursor:r.cursor}));if(t.ctrl&&"w"===r)return Wr(r=>{const t=r.text.slice(0,r.cursor).replace(/\S+\s*$/,"");return{text:t+r.text.slice(r.cursor),cursor:t.length}});if(t.backspace||t.delete)return Sr.current=null,Wr(r=>r.cursor>0?{text:r.text.slice(0,r.cursor-1)+r.text.slice(r.cursor),cursor:r.cursor-1}:r);if(r&&!t.ctrl&&!t.meta){Sr.current=null;const t=r.replace(/\r\n?/g,"\n");Wr(r=>({text:r.text.slice(0,r.cursor)+t+r.text.slice(r.cursor),cursor:r.cursor+t.length}))}}});const Pt=s(()=>ar.current.slice(),[Ir]),St=lr.split("/").filter(Boolean).pop()||lr,Mt=Rr.text.slice(0,Rr.cursor),At=Rr.text[Rr.cursor]??" ",It=Rr.text.slice(Rr.cursor+1),Dt=Math.max(8,process.stdout.columns||80),Lt=P(et),Ct=Lt?"exhausted"===Lt.level?`⛔ ${Lt.label}用量已用尽${S(Lt.resetAtEpochMs)?" · "+S(Lt.resetAtEpochMs):""}`:`⚠ ${Lt.label}用量 ${Math.round(100*Lt.pct)}%(${H(Lt.used)}/${H(Lt.limit)})`:"";return Q(u,{flexDirection:"column"},Q(a,{key:Lr,items:Pt},r=>Q(Z,{key:r.id,item:r})),gt.current?Q(u,null,Q(i,{color:"gray",italic:!0},"✦ 思考 "+gt.current.replace(/\n/g," ").slice(-160))):null,pr.current?Q(u,{marginTop:0},Q(i,null,pr.current)):null,Gr.length?Q(u,{flexDirection:"column",paddingLeft:2,marginTop:1},...Gr.map((r,t)=>Q(i,{key:t,color:"completed"===r.status?"green":"in_progress"===r.status?"cyan":"gray",dimColor:"completed"===r.status,bold:"in_progress"===r.status},("completed"===r.status?"☑ ":"in_progress"===r.status?"◐ ":"☐ ")+r.content))):null,"busy"!==_r||pr.current?null:Q(u,null,Q(i,{color:"cyan"},V[rt]+" 思考中…"),Q(i,{color:"gray"},"(Esc 中断 · 输入回车可排队补充)")),Fr?Q(u,{flexDirection:"column",borderStyle:"round",borderColor:"yellow",paddingX:1,marginTop:1},Q(i,{color:"yellow",bold:!0},Fr.consent?"● 是否继续?"+Fr.label:"● 需要授权:"+Fr.label),Fr.risk?Q(i,{color:"red",bold:!0},"⚠ "+Fr.risk):null,Fr.consent&&Fr.detail?Q(i,{color:"gray"},z(Fr.detail,6)):null,"run_bash"===Fr.name?Q(i,{color:"gray"},"$ "+(Fr.args.command||"")):null,"fetch_url"===Fr.name?Q(i,{color:"gray"},"GET "+(Fr.args.url||"")):null,Q(i,null,Fr.consent?"[1] 是 · [2] 是,并允许在当前对话中后续相同操作不再询问 · [3/Esc] 否":"[y] 允许一次 · [a] 本会话 · [A] 永久(写入项目设置) · [n/Esc] 拒绝")):null,$t?Q(u,{flexDirection:"column",marginTop:1},Q(i,{color:"gray"},Tt.length>X?` 斜杠命令 ${jt+1}-${jt+Et.length}/${Tt.length} · ↑↓ 选择 · PgUp/PgDn 翻页 · Tab 补全 · ⏎ 执行 · Esc 关闭`:" 斜杠命令 · ↑↓ 选择 · Tab 补全 · ⏎ 执行 · Esc 关闭"),...Et.map((r,t)=>{const e=jt+t;return Q(u,{key:r.name},Q(i,{color:"cyan",bold:e===st},(e===st?" ❯ ":" ")+"/"+r.name+(r.args?" "+r.args:"")),Q(i,{color:"gray"}," "+String(r.desc||"").replace(/\s+/g," ").slice(0,46)))})):null,Q(u,{marginTop:$t?0:1},Q(i,{color:"blue"},"─".repeat(Dt))),Q(u,null,Q(i,{color:"busy"===_r?"yellow":"cyan"},"busy"===_r?"⧗ ":"› "),Q(i,null,Mt),Q(i,{inverse:!0},At),Q(i,null,It)),Q(i,{color:"blue"},"─".repeat(Dt)),Q(u,{justifyContent:"space-between"},Q(i,{color:"gray"},(Hr?Hr+" · ":"")+`${St} · ${r.model}${kr.current?" · ⚠ yolo":""}${Xr?" · 📋 计划":""}`+(Nr?` · 排队 ${Nr}`:"")+" · /help"),Ct?Q(i,{color:"exhausted"===Lt.level?"red":"yellow",bold:!0},Ct):Q(i,null,"")))}function Z({item:r}){const{kind:t,text:e}=r;if("user"===t)return Q(u,{marginTop:1},Q(i,{color:"cyan",bold:!0},"› "),Q(i,{bold:!0},e));if("assistant"===t)return Q(u,null,Q(i,null,e));if("diff"===t)return Q(u,{marginLeft:2},Q(i,null,e));if("tool"===t)return Q(i,{color:"cyan"}," ⏺ "+e);if("toolresult"===t)return Q(i,{color:"gray"}," "+e);if("skip"===t)return Q(i,{color:"gray"}," ⊘ "+e);if("info"===t){const r=/✗|失败|错误|error/i.test(e);return Q(i,{color:r?"red":"yellow"},(r?" ✗ ":" · ")+e.replace(/^✗\s*/,""))}return Q(i,null,e)}export async function runInk({config:r,cwd:t,session:e,yolo:n,initialPrompt:o,version:s}){process.stdout.write(x({model:r.model,cwd:t,version:s,platform:/^(agent|workflow|datahuman)-/.test(String(r.model||"")),yolo:n,consult:r.consult?r.consultLabel||r.consult:null,ink:!0})+"\n");const{extraTools:c,clients:u}=await k(r,t),i={clients:u},{waitUntilExit:a}=l(Q(App,{config:r,cwd:t,session:e,extraTools:c,mcp:i,yolo:n,initialPrompt:o,version:s,onExit:null}));await a(),$(i.clients)}
|
package/src/uninstall.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import t from"node:fs";import e from"node:path";import{CONFIG_DIR as r}from"./config.js";function i(r){let n=0,o=[];try{o=t.readdirSync(r,{withFileTypes:!0})}catch{return 0}for(const s of o){const o=e.join(r,s.name);try{n+=s.isDirectory()?i(o):t.statSync(o).size}catch{}}return n}export function describeData(){const n=r;if(!t.existsSync(n))return{exists:!1,dir:n,entries:[],total:0};const o=[];let s=0;for(const r of t.readdirSync(n,{withFileTypes:!0})){const c=e.join(n,r.name);let a=0;try{a=r.isDirectory()?i(c):t.statSync(c).size}catch{}s+=a,o.push({name:r.name,kind:r.isDirectory()?"dir":"file",size:a})}return o.sort((t,e)=>t.name.localeCompare(e.name)),{exists:!0,dir:n,entries:o,total:s}}export function purgeData(){const e=r;return t.existsSync(e)?(t.rmSync(e,{recursive:!0,force:!0}),{removed:!0,dir:e}):{removed:!1,dir:e}}export function fmtSize(t){return t<1024?t+"B":t<1048576?(t/1024).toFixed(1)+"KB":(t/1024/1024).toFixed(1)+"MB"}
|