@huicai/cli 0.5.1 → 2026.710.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/bin/huicai.js +47 -2
- package/package.json +6 -4
- package/src/banner.js +83 -0
- package/src/commands.js +75 -9
- package/src/config.js +2 -0
- package/src/consult.js +65 -0
- package/src/input.js +71 -7
- package/src/markdown.js +0 -0
- package/src/repl.js +22 -20
- package/src/tui/app.js +32 -10
package/bin/huicai.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import readline from 'node:readline';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
6
7
|
import { c } from '../src/colors.js';
|
|
7
8
|
import { loadConfig, saveConfig, CONFIG_PATH, DEFAULT_MODEL } from '../src/config.js';
|
|
8
9
|
import { listModels, getAgentPersona, fetchUsage, listPlatformSkills, getPlatformSkill, pushPlatformSkill } from '../src/api.js';
|
|
@@ -12,7 +13,9 @@ import { buildSystemPrompt } from '../src/context.js';
|
|
|
12
13
|
import * as sessions from '../src/session.js';
|
|
13
14
|
import { loadLocalSkills, skillDirs, writeSkillToDir, readSkillDirForPush } from '../src/skills.js';
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
// 版本从 package.json 读取(versionDisplay 年月日展示式,与 VSCode 扩展、IDEA 插件一致);勿手改,用 scripts/next-version.cjs
|
|
17
|
+
const _pkg = createRequire(import.meta.url)('../package.json');
|
|
18
|
+
const VERSION = _pkg.versionDisplay || _pkg.version;
|
|
16
19
|
|
|
17
20
|
function parse(argv) {
|
|
18
21
|
const flags = {};
|
|
@@ -51,6 +54,9 @@ function parse(argv) {
|
|
|
51
54
|
case '--agent':
|
|
52
55
|
flags.agent = argv[++i];
|
|
53
56
|
break;
|
|
57
|
+
case '--consult':
|
|
58
|
+
flags.consult = argv[++i];
|
|
59
|
+
break;
|
|
54
60
|
case '--project':
|
|
55
61
|
flags.project = true;
|
|
56
62
|
break;
|
|
@@ -119,6 +125,7 @@ ${c.bold('huicai')} — 终端智能体,以 HuiCai_AI 平台为模型后端,
|
|
|
119
125
|
huicai sessions [-a] 列出历史会话(-a 跨目录全部)
|
|
120
126
|
huicai login 浏览器授权登录(内置平台地址,无需输入网关)
|
|
121
127
|
huicai login --paste 无浏览器环境:手动粘贴 API 令牌
|
|
128
|
+
huicai logout 退出登录(清除本机保存的令牌)
|
|
122
129
|
huicai models 列出可用模型
|
|
123
130
|
huicai usage 查看 Token 用量窗口(5小时/7天,按会员等级)
|
|
124
131
|
huicai skills 列出本地 + 平台技能(Agent Skills)
|
|
@@ -129,6 +136,7 @@ ${c.bold('huicai')} — 终端智能体,以 HuiCai_AI 平台为模型后端,
|
|
|
129
136
|
选项:
|
|
130
137
|
-m, --model <id> 指定模型(覆盖配置)
|
|
131
138
|
--agent <id> 采用平台智能体人格(分享令牌或 agent-xxx)
|
|
139
|
+
--consult <ref> 混合模式:本机驱动模型 + 平台智能体/工作流/数字人(agent-/workflow-/datahuman-xxx)作咨询工具
|
|
132
140
|
-c, --continue 继续最近一次会话(当前目录)
|
|
133
141
|
-r, --resume 选择并恢复历史会话(当前目录)
|
|
134
142
|
--base <url> 覆盖内置平台地址(自建 / 联调用)
|
|
@@ -145,7 +153,8 @@ ${c.bold('huicai')} — 终端智能体,以 HuiCai_AI 平台为模型后端,
|
|
|
145
153
|
-v, --version 版本
|
|
146
154
|
|
|
147
155
|
交互技巧:
|
|
148
|
-
/help 查看命令 ·
|
|
156
|
+
/help 查看命令 · @文件 引用文件(PDF/Word/Excel 等自动解析为文本、图片作为视觉输入) · !命令 直通 shell(输出进上下文)
|
|
157
|
+
模型 -m auto 或 /model auto:按账号自动选择可用模型(与 VSCode 扩展一致)
|
|
149
158
|
自定义命令:.huicai/commands/<名>.md($ARGUMENTS 为参数占位符)
|
|
150
159
|
权限规则:.huicai/settings.json {"permissions":{"allow":["run_bash(git *)"]}}
|
|
151
160
|
|
|
@@ -217,6 +226,16 @@ function validateAuth(config) {
|
|
|
217
226
|
return null;
|
|
218
227
|
}
|
|
219
228
|
|
|
229
|
+
/** Auto 模型解析:按当前账号选一个可用的「原始渠道模型」(优先内置默认;伪模型不适合本机工具循环)。 */
|
|
230
|
+
async function resolveAutoModel(config) {
|
|
231
|
+
try {
|
|
232
|
+
const models = await listModels(config.baseUrl, config.apiKey);
|
|
233
|
+
const raw = models.map((m) => m.id).filter((id) => !/^(agent|workflow|datahuman)-/.test(id));
|
|
234
|
+
if (raw.length) return raw.includes(DEFAULT_MODEL) ? DEFAULT_MODEL : raw[0];
|
|
235
|
+
} catch { /* 网络失败:回退内置默认 */ }
|
|
236
|
+
return DEFAULT_MODEL;
|
|
237
|
+
}
|
|
238
|
+
|
|
220
239
|
async function cmdModels(config) {
|
|
221
240
|
const problem = validateAuth(config);
|
|
222
241
|
if (problem) {
|
|
@@ -411,6 +430,11 @@ async function main() {
|
|
|
411
430
|
await login(flags);
|
|
412
431
|
return;
|
|
413
432
|
}
|
|
433
|
+
if (sub === 'logout') {
|
|
434
|
+
saveConfig({ apiKey: '' });
|
|
435
|
+
process.stdout.write(c.green('已退出登录(已清除本机保存的令牌)。') + '\n');
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
414
438
|
|
|
415
439
|
const config = loadConfig();
|
|
416
440
|
if (flags.base) config.baseUrl = flags.base.replace(/\/+$/, '');
|
|
@@ -460,6 +484,19 @@ async function main() {
|
|
|
460
484
|
}
|
|
461
485
|
}
|
|
462
486
|
|
|
487
|
+
// --consult:混合模式——把平台伪模型作为 consult_platform 工具(本机原始驱动 + 平台编排咨询)
|
|
488
|
+
if (flags.consult) {
|
|
489
|
+
if (!/^(agent|workflow|datahuman)-/.test(flags.consult)) {
|
|
490
|
+
process.stderr.write(c.red('--consult 需平台伪模型 id:agent-xxx / workflow-xxx / datahuman-xxx(分享码见分享窗口)') + '\n');
|
|
491
|
+
process.exit(1);
|
|
492
|
+
}
|
|
493
|
+
config.consult = flags.consult;
|
|
494
|
+
try {
|
|
495
|
+
const p = await getAgentPersona(config.baseUrl, config.apiKey, flags.consult);
|
|
496
|
+
if (p?.name) config.consultLabel = p.name;
|
|
497
|
+
} catch { /* 忽略:用 ref 作展示名 */ }
|
|
498
|
+
}
|
|
499
|
+
|
|
463
500
|
// 恢复会话:resume <id> 子命令 / -r 选择器 / -c 最近一次
|
|
464
501
|
let resumeId = null;
|
|
465
502
|
let positionalPrompt = positional;
|
|
@@ -486,12 +523,20 @@ async function main() {
|
|
|
486
523
|
config.model = session.model; // 恢复会话默认沿用其模型(-m/--agent 可覆盖)
|
|
487
524
|
}
|
|
488
525
|
|
|
526
|
+
// Auto 模型(与 VSCode 扩展一致):model 为空/auto,或混合模式下 model 仍是伪模型时,解析为可用的原始驱动模型
|
|
527
|
+
if (!config.model || config.model === 'auto'
|
|
528
|
+
|| (config.consult && /^(agent|workflow|datahuman)-/.test(config.model))) {
|
|
529
|
+
config.model = await resolveAutoModel(config);
|
|
530
|
+
}
|
|
489
531
|
if (!config.model) {
|
|
490
532
|
process.stderr.write(
|
|
491
533
|
c.red('未设置模型。用 `huicai -m <模型>`、`huicai login` 设置,或先 `huicai models` 查看可用模型。') + '\n',
|
|
492
534
|
);
|
|
493
535
|
process.exit(1);
|
|
494
536
|
}
|
|
537
|
+
if (config.consult) {
|
|
538
|
+
process.stdout.write(c.gray(`已启用混合模式:本机驱动 ${config.model} + 咨询平台 ${config.consultLabel || config.consult}`) + '\n');
|
|
539
|
+
}
|
|
495
540
|
|
|
496
541
|
// 新会话:用当前 cwd/persona 构建系统提示词(--system-prompt 完全替换、--append-system-prompt 追加)
|
|
497
542
|
if (!session) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@huicai/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2026.710.0",
|
|
4
|
+
"versionDisplay": "2026.07.10",
|
|
4
5
|
"description": "在终端运行的智能体 CLI:以 HuiCai_AI 平台的 OpenAI 兼容 /v1 为模型后端,在用户本机执行文件/命令工具。",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"bin": {
|
|
@@ -19,13 +20,15 @@
|
|
|
19
20
|
"start": "node bin/huicai.js",
|
|
20
21
|
"check": "node --check bin/huicai.js",
|
|
21
22
|
"test": "node --test --test-concurrency=1 test/*.test.mjs",
|
|
23
|
+
"version:next": "node scripts/next-version.cjs",
|
|
22
24
|
"prepublishOnly": "node --check bin/huicai.js"
|
|
23
25
|
},
|
|
24
26
|
"keywords": [
|
|
25
27
|
"cli",
|
|
26
28
|
"agent",
|
|
27
29
|
"ai",
|
|
28
|
-
"terminal",
|
|
30
|
+
"terminal",
|
|
31
|
+
"huicai",
|
|
29
32
|
"openai-compatible",
|
|
30
33
|
"coding-agent"
|
|
31
34
|
],
|
|
@@ -34,8 +37,7 @@
|
|
|
34
37
|
"homepage": "https://github.com/SJHT-SDK/huicai-cli/tree/master#readme",
|
|
35
38
|
"repository": {
|
|
36
39
|
"type": "git",
|
|
37
|
-
"url": "https://github.com/SJHT-SDK/huicai-cli.git
|
|
38
|
-
"directory": "huicai-cli"
|
|
40
|
+
"url": "https://github.com/SJHT-SDK/huicai-cli.git"
|
|
39
41
|
},
|
|
40
42
|
"bugs": {
|
|
41
43
|
"url": "https://github.com/SJHT-SDK/huicai-cli/issues"
|
package/src/banner.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// 启动欢迎横幅:渐变 ASCII 字标 + 上下文信息 + 快捷提示(对标 Claude CLI 进入时的欢迎屏)。
|
|
2
|
+
// 零依赖;真彩(COLORTERM=truecolor)时走 24-bit 青→紫渐变,仅 16 色时退化为青色,NO_COLOR/非 TTY 输出朴素字标。
|
|
3
|
+
|
|
4
|
+
import { c, colorEnabled } from './colors.js';
|
|
5
|
+
|
|
6
|
+
// 5 行块字(每字形 5 列宽,字间 1 空格)。
|
|
7
|
+
const FONT = {
|
|
8
|
+
H: ['█ █', '█ █', '█████', '█ █', '█ █'],
|
|
9
|
+
U: ['█ █', '█ █', '█ █', '█ █', '█████'],
|
|
10
|
+
I: ['█████', ' █ ', ' █ ', ' █ ', '█████'],
|
|
11
|
+
C: ['█████', '█ ', '█ ', '█ ', '█████'],
|
|
12
|
+
A: ['█████', '█ █', '█████', '█ █', '█ █'],
|
|
13
|
+
' ': [' ', ' ', ' ', ' ', ' '],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function wordmark(word) {
|
|
17
|
+
const rows = ['', '', '', '', ''];
|
|
18
|
+
[...word].forEach((ch, idx) => {
|
|
19
|
+
const g = FONT[ch] || FONT[' '];
|
|
20
|
+
for (let r = 0; r < 5; r++) rows[r] += (idx ? ' ' : '') + g[r];
|
|
21
|
+
});
|
|
22
|
+
return rows;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const truecolor = colorEnabled && /truecolor|24bit/i.test(process.env.COLORTERM || '');
|
|
26
|
+
const lerp = (a, b, t) => Math.round(a + (b - a) * t);
|
|
27
|
+
|
|
28
|
+
// 按列做水平渐变;无真彩退化为青色;无色原样返回。
|
|
29
|
+
function gradientRow(row, from, to) {
|
|
30
|
+
if (!colorEnabled) return row;
|
|
31
|
+
const chars = [...row];
|
|
32
|
+
const n = chars.length;
|
|
33
|
+
let out = '';
|
|
34
|
+
for (let i = 0; i < n; i++) {
|
|
35
|
+
const ch = chars[i];
|
|
36
|
+
if (ch === ' ') { out += ' '; continue; }
|
|
37
|
+
if (!truecolor) { out += `\x1b[36m${ch}`; continue; }
|
|
38
|
+
const t = n <= 1 ? 0 : i / (n - 1);
|
|
39
|
+
out += `\x1b[38;2;${lerp(from[0], to[0], t)};${lerp(from[1], to[1], t)};${lerp(from[2], to[2], t)}m${ch}`;
|
|
40
|
+
}
|
|
41
|
+
return out + '\x1b[0m';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const PAD = ' ';
|
|
45
|
+
const FROM = [0, 229, 255]; // 青
|
|
46
|
+
const TO = [168, 85, 247]; // 紫
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 生成欢迎横幅字符串(不含结尾换行)。
|
|
50
|
+
* @param {object} p
|
|
51
|
+
* @param {string} p.model 当前模型 / 平台目标
|
|
52
|
+
* @param {string} p.cwd 工作目录
|
|
53
|
+
* @param {string} [p.version] 版本展示串
|
|
54
|
+
* @param {boolean} [p.platform] 是否平台伪模型(agent-/workflow-/datahuman-)
|
|
55
|
+
* @param {boolean} [p.yolo] 是否已开自动放行
|
|
56
|
+
* @param {string} [p.consult] 混合模式咨询目标展示名
|
|
57
|
+
* @param {boolean} [p.ink] 是否 ink 富终端(附注一行)
|
|
58
|
+
*/
|
|
59
|
+
export function renderWelcome({ model, cwd, version, platform, yolo, consult, ink } = {}) {
|
|
60
|
+
const cols = process.stdout.columns || 60;
|
|
61
|
+
const sep = c.gray('─'.repeat(Math.min(Math.max(cols - 2, 24), 50)));
|
|
62
|
+
const L = [''];
|
|
63
|
+
|
|
64
|
+
if (colorEnabled) {
|
|
65
|
+
for (const row of wordmark('HUICAI')) L.push(PAD + gradientRow(row, FROM, TO));
|
|
66
|
+
} else {
|
|
67
|
+
L.push(PAD + 'H U I C A I');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
L.push('');
|
|
71
|
+
L.push(PAD + c.bold('HuiCai AI') + c.gray(' · 终端智能体') + (version ? c.gray(' v' + version) : '')
|
|
72
|
+
+ (ink ? c.gray(' · ink') : ''));
|
|
73
|
+
L.push(PAD + sep);
|
|
74
|
+
L.push(PAD + c.gray(platform ? '目标 ' : '模型 ') + c.cyan(model || c.gray('(未设置,/model 设置)'))
|
|
75
|
+
+ (platform ? c.gray(' · 平台编排(无本机工具)') : ''));
|
|
76
|
+
L.push(PAD + c.gray('目录 ') + cwd);
|
|
77
|
+
if (consult) L.push(PAD + c.gray('咨询 ') + c.magenta(consult) + c.gray(' · 混合模式(本机工具 + 平台咨询)'));
|
|
78
|
+
if (yolo) L.push(PAD + c.yellow('⚠ 自动放行(yolo) 已开:写文件/执行命令不再逐次确认'));
|
|
79
|
+
L.push('');
|
|
80
|
+
L.push(PAD + c.magenta('✦ ') + c.gray('/help 命令 · @文件 引用 · !命令 直通 shell · Ctrl+C 中断'));
|
|
81
|
+
|
|
82
|
+
return L.join('\n');
|
|
83
|
+
}
|
package/src/commands.js
CHANGED
|
@@ -7,8 +7,8 @@ import fs from 'node:fs';
|
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import os from 'node:os';
|
|
9
9
|
import { execSync } from 'node:child_process';
|
|
10
|
-
import { saveConfig } from './config.js';
|
|
11
|
-
import { listModels } from './api.js';
|
|
10
|
+
import { saveConfig, DEFAULT_MODEL } from './config.js';
|
|
11
|
+
import { listModels, fetchUsage } from './api.js';
|
|
12
12
|
import { listSessions, relTime, turnCount } from './session.js';
|
|
13
13
|
import { compactNow } from './compaction.js';
|
|
14
14
|
import { loadAllowRules } from './settings.js';
|
|
@@ -42,15 +42,25 @@ const BUILTINS = [
|
|
|
42
42
|
},
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
|
-
name: 'model', args: '[
|
|
46
|
-
run(ctx, arg) {
|
|
47
|
-
if (arg) {
|
|
48
|
-
ctx.config.model = arg;
|
|
49
|
-
saveConfig({ model: arg });
|
|
50
|
-
ctx.print('已切换模型:' + arg);
|
|
51
|
-
} else {
|
|
45
|
+
name: 'model', args: '[名称|auto]', desc: '查看或切换模型(auto=按账号自动选可用模型,与 VSCode 一致)',
|
|
46
|
+
async run(ctx, arg) {
|
|
47
|
+
if (!arg) {
|
|
52
48
|
ctx.print('当前模型:' + (ctx.config.model || '(未设置)'));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (arg === 'auto') {
|
|
52
|
+
try {
|
|
53
|
+
const models = await listModels(ctx.config.baseUrl, ctx.config.apiKey);
|
|
54
|
+
const raw = models.map((m) => m.id).filter((id) => !/^(agent|workflow|datahuman)-/.test(id));
|
|
55
|
+
ctx.config.model = (raw.includes(DEFAULT_MODEL) ? DEFAULT_MODEL : raw[0]) || ctx.config.model;
|
|
56
|
+
} catch { /* 拉取失败保持现值 */ }
|
|
57
|
+
saveConfig({ model: 'auto' }); // 保存 auto 偏好(下次启动重新解析),本会话用已解析的具体模型
|
|
58
|
+
ctx.print('Auto 已选:' + ctx.config.model);
|
|
59
|
+
return;
|
|
53
60
|
}
|
|
61
|
+
ctx.config.model = arg;
|
|
62
|
+
saveConfig({ model: arg });
|
|
63
|
+
ctx.print('已切换模型:' + arg);
|
|
54
64
|
},
|
|
55
65
|
},
|
|
56
66
|
{
|
|
@@ -64,6 +74,53 @@ const BUILTINS = [
|
|
|
64
74
|
}
|
|
65
75
|
},
|
|
66
76
|
},
|
|
77
|
+
{
|
|
78
|
+
name: 'usage', desc: '查看 Token 用量窗口(5小时/7天,按会员等级)',
|
|
79
|
+
async run(ctx) {
|
|
80
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
|
|
81
|
+
const fmt = (used, limit) => {
|
|
82
|
+
used = num(used);
|
|
83
|
+
limit = num(limit);
|
|
84
|
+
if (limit < 0) return `${used.toLocaleString()} / 无限`;
|
|
85
|
+
if (limit === 0) return '该档未开放';
|
|
86
|
+
return `${used.toLocaleString()} / ${limit.toLocaleString()} (${Math.min(100, Math.round((used / limit) * 100))}%)`;
|
|
87
|
+
};
|
|
88
|
+
try {
|
|
89
|
+
const u = await fetchUsage(ctx.config.baseUrl, ctx.config.apiKey);
|
|
90
|
+
if (!u || (typeof u.used5h !== 'number' && typeof u.used7d !== 'number')) {
|
|
91
|
+
ctx.print('平台未返回用量数据(请确认平台已支持 /v1/usage)。');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
ctx.print(`Token 用量窗口\n 近 5 小时:${fmt(u.used5h, u.limit5h)}\n 近 7 天 :${fmt(u.used7d, u.limit7d)}`
|
|
95
|
+
+ (!u.allowed && u.message ? '\n ' + u.message : ''));
|
|
96
|
+
} catch (e) {
|
|
97
|
+
ctx.print('拉取用量失败:' + e.message);
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'login', desc: '重新登录(浏览器授权;无浏览器请退出后 huicai login --paste)',
|
|
103
|
+
async run(ctx) {
|
|
104
|
+
try {
|
|
105
|
+
const { deviceLogin } = await import('./device.js');
|
|
106
|
+
ctx.print('正在打开浏览器授权…');
|
|
107
|
+
const key = await deviceLogin(ctx.config.baseUrl);
|
|
108
|
+
ctx.config.apiKey = key;
|
|
109
|
+
saveConfig({ apiKey: key });
|
|
110
|
+
ctx.print('登录成功。');
|
|
111
|
+
} catch (e) {
|
|
112
|
+
ctx.print('登录未完成:' + e.message + '(可退出后用 huicai login --paste 粘贴令牌)');
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: 'logout', desc: '退出登录(清除本机保存的令牌)',
|
|
118
|
+
run(ctx) {
|
|
119
|
+
ctx.config.apiKey = '';
|
|
120
|
+
saveConfig({ apiKey: '' });
|
|
121
|
+
ctx.print('已退出登录。后续对话需重新 /login 或 huicai login。');
|
|
122
|
+
},
|
|
123
|
+
},
|
|
67
124
|
{
|
|
68
125
|
name: 'clear', desc: '开启新会话(旧会话仍可 --resume 恢复)',
|
|
69
126
|
run() {
|
|
@@ -160,6 +217,15 @@ const BUILTINS = [
|
|
|
160
217
|
ctx.print(ctx.cwd);
|
|
161
218
|
},
|
|
162
219
|
},
|
|
220
|
+
{
|
|
221
|
+
name: 'markdown', desc: '切换助手回复的 Markdown 渲染(TUI 富文本;关闭则显示原始文本,便于复制)',
|
|
222
|
+
run(ctx) {
|
|
223
|
+
const next = ctx.config.markdown === false; // 当前关→开,否则→关
|
|
224
|
+
ctx.config.markdown = next;
|
|
225
|
+
saveConfig({ markdown: next });
|
|
226
|
+
ctx.print('Markdown 渲染:' + (next ? '开(下一条回复起生效)' : '关(显示原始 Markdown 文本)'));
|
|
227
|
+
},
|
|
228
|
+
},
|
|
163
229
|
{
|
|
164
230
|
name: 'init', desc: '让智能体调研项目并生成/完善 HUICAI.md',
|
|
165
231
|
run() {
|
package/src/config.js
CHANGED
|
@@ -43,6 +43,8 @@ export function loadConfig() {
|
|
|
43
43
|
memoryEnabled: process.env.HUICAI_MEMORY === '0' ? false : file.memory !== false,
|
|
44
44
|
// 思考过程流式展示:默认开;配置 showReasoning:false 或 HUICAI_REASONING=0 关闭
|
|
45
45
|
showReasoning: process.env.HUICAI_REASONING === '0' ? false : file.showReasoning !== false,
|
|
46
|
+
// 助手回复 Markdown 渲染(TUI 富文本):默认开;配置 markdown:false 或 HUICAI_MARKDOWN=0 关闭(显示原始文本)
|
|
47
|
+
markdown: process.env.HUICAI_MARKDOWN === '0' ? false : file.markdown !== false,
|
|
46
48
|
// 客户端来源标识(对话落库审计用):默认 cli;VSCode 扩展进程内置 HUICAI_CLIENT=vscode
|
|
47
49
|
client: (process.env.HUICAI_CLIENT || 'cli').trim().toLowerCase(),
|
|
48
50
|
};
|
package/src/consult.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// 混合模式(对应 VSCode 扩展的「平台伪模型 + 工作区」):把平台智能体/工作流/数字人作为「咨询工具」
|
|
2
|
+
// 暴露给本机智能体循环。本机用原始驱动模型 + 本地工具(读写文件/执行命令/子智能体…),遇到平台目标擅长的问题
|
|
3
|
+
// 调用 consult_platform——由平台侧编排作答(服务端知识库 RAG / 工具 / 联网),再用本机工具把结论落地到项目。
|
|
4
|
+
|
|
5
|
+
import { streamChat } from './api.js';
|
|
6
|
+
|
|
7
|
+
const PSEUDO = /^(agent|workflow|datahuman)-/;
|
|
8
|
+
|
|
9
|
+
/** 平台伪模型 id → 中文类别名。 */
|
|
10
|
+
function kindOf(ref) {
|
|
11
|
+
if (ref.startsWith('workflow-')) return '工作流';
|
|
12
|
+
if (ref.startsWith('datahuman-')) return '业务数字人';
|
|
13
|
+
return '智能体';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 构造 consult_platform 工具;未配置或 ref 非平台伪模型时返回 null。
|
|
18
|
+
* @param {object} p
|
|
19
|
+
* @param {object} p.config 运行配置(读时取最新;含 baseUrl/apiKey/consult/consultLabel/client/sessionId)
|
|
20
|
+
*/
|
|
21
|
+
export function makeConsultTool({ config }) {
|
|
22
|
+
const ref = config && config.consult;
|
|
23
|
+
if (!ref || !PSEUDO.test(String(ref))) return null;
|
|
24
|
+
const kind = kindOf(String(ref));
|
|
25
|
+
const label = config.consultLabel || ref;
|
|
26
|
+
|
|
27
|
+
const spec = {
|
|
28
|
+
type: 'function',
|
|
29
|
+
function: {
|
|
30
|
+
name: 'consult_platform',
|
|
31
|
+
description:
|
|
32
|
+
`咨询平台${kind}「${label}」:由平台侧编排作答(带其服务端知识库 RAG / 工具 / 联网检索)。`
|
|
33
|
+
+ `遇到它擅长领域的问题优先调用它获取权威答复,再用本机工具把结论落地到项目。可多次调用。`,
|
|
34
|
+
parameters: {
|
|
35
|
+
type: 'object',
|
|
36
|
+
properties: {
|
|
37
|
+
query: { type: 'string', description: '自然语言的咨询 / 查询 / 运行输入' },
|
|
38
|
+
},
|
|
39
|
+
required: ['query'],
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const run = async (args) => {
|
|
45
|
+
const q = String((args && args.query) || '').trim();
|
|
46
|
+
if (!q) return '需要提供 query(要咨询平台的自然语言问题 / 输入)';
|
|
47
|
+
try {
|
|
48
|
+
// 走平台伪模型 → 服务端编排;client/sessionId 使这轮咨询也计入对话审计
|
|
49
|
+
const r = await streamChat({
|
|
50
|
+
baseUrl: config.baseUrl,
|
|
51
|
+
apiKey: config.apiKey,
|
|
52
|
+
model: ref,
|
|
53
|
+
messages: [{ role: 'user', content: q }],
|
|
54
|
+
client: config.client,
|
|
55
|
+
sessionId: config.sessionId,
|
|
56
|
+
}, {});
|
|
57
|
+
return r.content || '(平台无返回)';
|
|
58
|
+
} catch (e) {
|
|
59
|
+
if (e && e.name === 'AbortError') throw e;
|
|
60
|
+
return `平台调用失败:${e.message}`;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return { readOnly: true, spec, run };
|
|
65
|
+
}
|
package/src/input.js
CHANGED
|
@@ -8,24 +8,43 @@ import path from 'node:path';
|
|
|
8
8
|
import os from 'node:os';
|
|
9
9
|
import { CONFIG_DIR } from './config.js';
|
|
10
10
|
import { runBash } from './executors.js';
|
|
11
|
+
import { extractDocument } from './api.js';
|
|
11
12
|
|
|
12
13
|
const MAX_MENTIONS = 5;
|
|
13
14
|
const MENTION_FILE_CHARS = 12_000;
|
|
14
15
|
const HISTORY_PATH = path.join(CONFIG_DIR, 'history.json');
|
|
15
16
|
const HISTORY_CAP = 1000;
|
|
16
17
|
|
|
18
|
+
/** 图片扩展名 → 作为多模态 image_url 附加。 */
|
|
19
|
+
const IMAGE_MIME = {
|
|
20
|
+
png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif',
|
|
21
|
+
webp: 'image/webp', bmp: 'image/bmp', svg: 'image/svg+xml',
|
|
22
|
+
};
|
|
23
|
+
/** 文档扩展名 → 经平台 /v1/files/extract 解析为纯文本(与 VSCode 扩展附件同源)。 */
|
|
24
|
+
const DOC_EXT = new Set(['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'rtf', 'odt', 'ods', 'odp', 'csv']);
|
|
25
|
+
const MAX_IMAGE_BYTES = 8 * 1024 * 1024;
|
|
26
|
+
const MAX_DOC_BYTES = 20 * 1024 * 1024;
|
|
27
|
+
|
|
17
28
|
/**
|
|
18
29
|
* 展开消息中的 @文件/@目录 引用:原文保留,内容以引用块追加在末尾(模型可见全文)。
|
|
19
30
|
* @returns {{text:string, mentions:string[]}}
|
|
20
31
|
*/
|
|
21
|
-
export function expandAtMentions(text, cwd) {
|
|
32
|
+
export async function expandAtMentions(text, cwd, config = null) {
|
|
22
33
|
const re = /(^|[\s("'`(【,,::])@([~A-Za-z0-9_./\\-]+)/g;
|
|
23
34
|
const seen = new Set();
|
|
24
|
-
const
|
|
35
|
+
const candidates = [];
|
|
25
36
|
let m;
|
|
26
|
-
while ((m = re.exec(text)) &&
|
|
37
|
+
while ((m = re.exec(text)) && candidates.length < MAX_MENTIONS) {
|
|
27
38
|
const raw = m[2].replace(/[.,;:。,;]+$/, ''); // 去句尾标点
|
|
28
|
-
if (
|
|
39
|
+
if (raw && !seen.has(raw)) {
|
|
40
|
+
seen.add(raw);
|
|
41
|
+
candidates.push(raw);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const blocks = [];
|
|
45
|
+
const images = []; // 多模态图片附件 {label, dataUri}
|
|
46
|
+
const mentions = [];
|
|
47
|
+
for (const raw of candidates) {
|
|
29
48
|
const abs = raw.startsWith('~') ? path.join(os.homedir(), raw.slice(1)) : path.resolve(cwd, raw);
|
|
30
49
|
let st;
|
|
31
50
|
try {
|
|
@@ -33,7 +52,7 @@ export function expandAtMentions(text, cwd) {
|
|
|
33
52
|
} catch {
|
|
34
53
|
continue; // 不存在的路径按普通文本处理
|
|
35
54
|
}
|
|
36
|
-
|
|
55
|
+
mentions.push(raw);
|
|
37
56
|
if (st.isDirectory()) {
|
|
38
57
|
try {
|
|
39
58
|
const entries = fs.readdirSync(abs).slice(0, 100).join('\n');
|
|
@@ -43,6 +62,42 @@ export function expandAtMentions(text, cwd) {
|
|
|
43
62
|
}
|
|
44
63
|
continue;
|
|
45
64
|
}
|
|
65
|
+
const ext = path.extname(abs).slice(1).toLowerCase();
|
|
66
|
+
// 图片:作为多模态 image_url 附加(需当前模型支持视觉,或走平台伪模型的服务端识图)
|
|
67
|
+
if (IMAGE_MIME[ext]) {
|
|
68
|
+
if (st.size > MAX_IMAGE_BYTES) {
|
|
69
|
+
blocks.push(`【图片 @${raw}】(超过 ${Math.round(MAX_IMAGE_BYTES / 1048576)}MB,未附加)`);
|
|
70
|
+
} else {
|
|
71
|
+
try {
|
|
72
|
+
const b64 = fs.readFileSync(abs).toString('base64');
|
|
73
|
+
images.push({ label: raw, dataUri: `data:${IMAGE_MIME[ext]};base64,${b64}` });
|
|
74
|
+
blocks.push(`【已附加图片 @${raw}】`);
|
|
75
|
+
} catch {
|
|
76
|
+
/* ignore */
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
// 文档(PDF/Word/Excel/PPT/RTF…):经平台 /v1/files/extract 解析为纯文本
|
|
82
|
+
if (DOC_EXT.has(ext)) {
|
|
83
|
+
if (!config || !config.baseUrl || !config.apiKey) {
|
|
84
|
+
blocks.push(`【文档 @${raw}】(需登录后才能解析为文本)`);
|
|
85
|
+
} else if (st.size > MAX_DOC_BYTES) {
|
|
86
|
+
blocks.push(`【文档 @${raw}】(超过 ${Math.round(MAX_DOC_BYTES / 1048576)}MB,未解析)`);
|
|
87
|
+
} else {
|
|
88
|
+
try {
|
|
89
|
+
const b64 = fs.readFileSync(abs).toString('base64');
|
|
90
|
+
const r = await extractDocument(config.baseUrl, config.apiKey,
|
|
91
|
+
{ filename: path.basename(abs), data: b64, maxChars: 60_000 });
|
|
92
|
+
const meta = `${r.pages ? r.pages + '页·' : ''}${r.chars ?? 0}字${r.truncated ? '·已截断' : ''}`;
|
|
93
|
+
blocks.push(`【引用文档 @${raw}(${meta})】\n${r.text || '(空)'}`);
|
|
94
|
+
} catch (e) {
|
|
95
|
+
blocks.push(`【文档 @${raw}】(解析失败:${e.message})`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
// 其余:文本文件展开,二进制跳过
|
|
46
101
|
if (st.size > 512 * 1024) {
|
|
47
102
|
blocks.push(`【引用文件 @${raw}】\n(文件过大:${Math.round(st.size / 1024)}KB,未展开;可让我用 read_file 分段读取)`);
|
|
48
103
|
continue;
|
|
@@ -60,8 +115,8 @@ export function expandAtMentions(text, cwd) {
|
|
|
60
115
|
/* ignore */
|
|
61
116
|
}
|
|
62
117
|
}
|
|
63
|
-
|
|
64
|
-
return { text:
|
|
118
|
+
const outText = blocks.length ? text + '\n\n' + blocks.join('\n\n') : text;
|
|
119
|
+
return { text: outText, mentions, images };
|
|
65
120
|
}
|
|
66
121
|
|
|
67
122
|
function isBinary(buf) {
|
|
@@ -70,6 +125,15 @@ function isBinary(buf) {
|
|
|
70
125
|
return false;
|
|
71
126
|
}
|
|
72
127
|
|
|
128
|
+
/** 组装用户消息 content:有图片附件则用 OpenAI 多模态数组,否则纯文本字符串。 */
|
|
129
|
+
export function buildUserContent(text, images) {
|
|
130
|
+
if (!images || !images.length) return text;
|
|
131
|
+
return [
|
|
132
|
+
{ type: 'text', text },
|
|
133
|
+
...images.map((i) => ({ type: 'image_url', image_url: { url: i.dataUri } })),
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
|
|
73
137
|
/**
|
|
74
138
|
* 执行 !命令 直通:在 cwd 下跑 shell 命令,返回展示文本与写入对话的记录文本。
|
|
75
139
|
* @returns {Promise<{output:string, record:string}>}
|
package/src/markdown.js
ADDED
|
Binary file
|
package/src/repl.js
CHANGED
|
@@ -8,9 +8,11 @@ import { runTurn } from './agent.js';
|
|
|
8
8
|
import { createSession, saveSession, turnCount } from './session.js';
|
|
9
9
|
import { loadMcpTools, closeMcp } from './mcp.js';
|
|
10
10
|
import { makeSubagentTool } from './subagent.js';
|
|
11
|
+
import { makeConsultTool } from './consult.js';
|
|
11
12
|
import { runSlash } from './commands.js';
|
|
12
|
-
import { expandAtMentions, runBangCommand, loadHistory, appendHistory } from './input.js';
|
|
13
|
+
import { expandAtMentions, buildUserContent, runBangCommand, loadHistory, appendHistory } from './input.js';
|
|
13
14
|
import { makeSkillTools } from './skills.js';
|
|
15
|
+
import { renderWelcome } from './banner.js';
|
|
14
16
|
|
|
15
17
|
/** 汇集动态工具:本地技能(load_skill)+ 本地 MCP 工具 + dispatch_agent 子智能体。返回 {extraTools, clients}。 */
|
|
16
18
|
export async function buildExtraTools(config, cwd) {
|
|
@@ -24,22 +26,21 @@ export async function buildExtraTools(config, cwd) {
|
|
|
24
26
|
const extraTools = [...makeSkillTools(cwd), ...mcp.tools];
|
|
25
27
|
const dispatch = makeSubagentTool({ config, cwd, extraTools: mcp.tools, depth: 0 });
|
|
26
28
|
if (dispatch) extraTools.push(dispatch);
|
|
29
|
+
// 混合模式:配置了 --consult 平台伪模型时,暴露 consult_platform 工具(本机驱动 + 平台编排咨询)
|
|
30
|
+
const consult = makeConsultTool({ config });
|
|
31
|
+
if (consult) extraTools.push(consult);
|
|
27
32
|
return { extraTools, clients: mcp.clients };
|
|
28
33
|
}
|
|
29
34
|
|
|
30
|
-
function banner(config, cwd, permissions) {
|
|
31
|
-
process.stdout.write(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
` ${permissions.isYolo() ? '⚠ 自动放行(yolo) 已开 · ' : ''}` +
|
|
40
|
-
'/help 命令 · @文件 引用 · !命令 直通 shell · Ctrl+C 中断本回合',
|
|
41
|
-
) + '\n',
|
|
42
|
-
);
|
|
35
|
+
function banner(config, cwd, permissions, version) {
|
|
36
|
+
process.stdout.write(renderWelcome({
|
|
37
|
+
model: config.model,
|
|
38
|
+
cwd,
|
|
39
|
+
version,
|
|
40
|
+
platform: /^(agent|workflow|datahuman)-/.test(String(config.model || '')),
|
|
41
|
+
yolo: permissions.isYolo(),
|
|
42
|
+
consult: config.consult ? (config.consultLabel || config.consult) : null,
|
|
43
|
+
}) + '\n');
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
export async function startRepl({ config, cwd, yolo, initialPrompt, session, version }) {
|
|
@@ -62,14 +63,15 @@ export async function startRepl({ config, cwd, yolo, initialPrompt, session, ver
|
|
|
62
63
|
else process.stdout.write('\n' + c.gray('输入 /exit 退出。') + '\n');
|
|
63
64
|
});
|
|
64
65
|
|
|
65
|
-
banner(config, cwd, permissions);
|
|
66
|
+
banner(config, cwd, permissions, version);
|
|
66
67
|
echoResumed(session);
|
|
67
68
|
const { extraTools, clients } = await buildExtraTools(config, cwd); // 连接 MCP(若已配置)
|
|
68
69
|
|
|
69
70
|
const doTurn = async (text) => {
|
|
70
|
-
const { text: expanded, mentions } = expandAtMentions(text, cwd);
|
|
71
|
-
if (mentions.length) process.stdout.write(c.gray(' · 已展开引用:' + mentions.join('、')
|
|
72
|
-
|
|
71
|
+
const { text: expanded, mentions, images } = await expandAtMentions(text, cwd, config);
|
|
72
|
+
if (mentions.length) process.stdout.write(c.gray(' · 已展开引用:' + mentions.join('、')
|
|
73
|
+
+ (images.length ? `(含 ${images.length} 张图片)` : '')) + '\n');
|
|
74
|
+
ctx.session.messages.push({ role: 'user', content: buildUserContent(expanded, images) });
|
|
73
75
|
currentAbort = new AbortController();
|
|
74
76
|
if (!ctx.session.compaction) ctx.session.compaction = {};
|
|
75
77
|
config.sessionId = ctx.session.id; // 对话落库审计:当前会话号(/clear 换新会话后随之更新)
|
|
@@ -169,8 +171,8 @@ export async function runOnce({ config, cwd, yolo, prompt, session, persist, out
|
|
|
169
171
|
const json = outputFormat === 'json';
|
|
170
172
|
const permissions = createPermissions({ yolo, ask: null, cwd });
|
|
171
173
|
const { extraTools, clients } = await buildExtraTools(config, cwd);
|
|
172
|
-
const { text: expanded } = expandAtMentions(prompt, cwd);
|
|
173
|
-
session.messages.push({ role: 'user', content: expanded });
|
|
174
|
+
const { text: expanded, images } = await expandAtMentions(prompt, cwd, config);
|
|
175
|
+
session.messages.push({ role: 'user', content: buildUserContent(expanded, images) });
|
|
174
176
|
if (!session.compaction) session.compaction = {};
|
|
175
177
|
config.sessionId = session.id; // 对话落库审计:会话号
|
|
176
178
|
const r = await runTurn({
|
package/src/tui/app.js
CHANGED
|
@@ -10,15 +10,17 @@
|
|
|
10
10
|
//
|
|
11
11
|
// 仅在 TTY 且已安装 ink 时启用;未安装时 bin 回退到零依赖 readline REPL。
|
|
12
12
|
|
|
13
|
-
import React, { useState, useRef, useReducer, useEffect } from 'react';
|
|
13
|
+
import React, { useState, useRef, useReducer, useEffect, useMemo } from 'react';
|
|
14
14
|
import { render, Box, Text, Static, useInput, useApp } from 'ink';
|
|
15
15
|
import { runTurn } from '../agent.js';
|
|
16
16
|
import { toolLabel } from '../render.js';
|
|
17
|
+
import { renderMarkdown, looksLikeMarkdown } from '../markdown.js';
|
|
18
|
+
import { renderWelcome } from '../banner.js';
|
|
17
19
|
import { createSession, saveSession } from '../session.js';
|
|
18
20
|
import { buildExtraTools } from '../repl.js';
|
|
19
21
|
import { closeMcp } from '../mcp.js';
|
|
20
22
|
import { runSlash } from '../commands.js';
|
|
21
|
-
import { expandAtMentions, runBangCommand, loadHistory, appendHistory } from '../input.js';
|
|
23
|
+
import { expandAtMentions, buildUserContent, runBangCommand, loadHistory, appendHistory } from '../input.js';
|
|
22
24
|
import { loadAllowRules, matchAllowRules, suggestRule, addProjectAllowRule, riskWarning } from '../settings.js';
|
|
23
25
|
|
|
24
26
|
const h = React.createElement;
|
|
@@ -60,6 +62,10 @@ function App({ config, cwd, session: initialSession, extraTools, yolo, initialPr
|
|
|
60
62
|
const draftRef = useRef('');
|
|
61
63
|
|
|
62
64
|
const [, force] = useReducer((x) => x + 1, 0);
|
|
65
|
+
// 定稿记录(logRef)变更计数:ink <Static> 靠 items「引用变化」识别新项,就地 push 同一数组它不会刷新
|
|
66
|
+
// (会导致轮末助手回复无后续渲染触发而丢失)。故每次 addLog 自增 logVer,据此派生「全新数组引用」交给 Static。
|
|
67
|
+
const [logVer, bumpLog] = useReducer((x) => x + 1, 0);
|
|
68
|
+
const [staticKey, setStaticKey] = useState(0); // /clear 时自增以重挂 Static,复位其内部已渲染索引
|
|
63
69
|
const [status, setStatus] = useState('idle'); // idle | busy
|
|
64
70
|
const [inp, setInp] = useState({ text: '', cursor: 0 });
|
|
65
71
|
const [pending, setPending] = useState(null);
|
|
@@ -71,11 +77,15 @@ function App({ config, cwd, session: initialSession, extraTools, yolo, initialPr
|
|
|
71
77
|
|
|
72
78
|
const addLog = (kind, text) => {
|
|
73
79
|
logRef.current.push({ id: idRef.current++, kind, text });
|
|
74
|
-
|
|
80
|
+
bumpLog(); // 变更定稿记录 → 触发 Static 用新数组引用刷新(见 logVer 说明)
|
|
75
81
|
};
|
|
76
82
|
const flushLive = () => {
|
|
77
83
|
if (liveRef.current) {
|
|
78
|
-
|
|
84
|
+
// 定稿时把助手正文渲染为 Markdown(ink <Text> 原样保留内嵌 ANSI);流式期间仍显示原文,完成即定稿为富文本。
|
|
85
|
+
// 纯散文(无 Markdown 结构)直接留原文,避免无谓处理与边角改动。
|
|
86
|
+
const raw = liveRef.current;
|
|
87
|
+
const md = config.markdown !== false && looksLikeMarkdown(raw);
|
|
88
|
+
addLog('assistant', md ? renderMarkdown(raw) : raw);
|
|
79
89
|
liveRef.current = '';
|
|
80
90
|
}
|
|
81
91
|
};
|
|
@@ -139,14 +149,15 @@ function App({ config, cwd, session: initialSession, extraTools, yolo, initialPr
|
|
|
139
149
|
};
|
|
140
150
|
|
|
141
151
|
const pullQueued = () => {
|
|
152
|
+
// runTurn 同步回调:排队消息按原文注入(@ 文档/图片的异步展开只在主输入路径做,队列消息保留 @ 路径原文,模型可再 read_file)
|
|
142
153
|
const q = queueRef.current.shift();
|
|
143
154
|
setQueueLen(queueRef.current.length);
|
|
144
|
-
|
|
145
|
-
return expandAtMentions(q, cwd).text;
|
|
155
|
+
return q == null ? null : q;
|
|
146
156
|
};
|
|
147
157
|
|
|
148
158
|
const runUserTurn = async (text) => {
|
|
149
|
-
|
|
159
|
+
const { text: expanded, images } = await expandAtMentions(text, cwd, config);
|
|
160
|
+
sessionRef.current.messages.push({ role: 'user', content: buildUserContent(expanded, images) });
|
|
150
161
|
setBusy(true);
|
|
151
162
|
abortRef.current = new AbortController();
|
|
152
163
|
if (!sessionRef.current.compaction) sessionRef.current.compaction = {};
|
|
@@ -203,6 +214,7 @@ function App({ config, cwd, session: initialSession, extraTools, yolo, initialPr
|
|
|
203
214
|
setCtxTok(0);
|
|
204
215
|
logRef.current = [];
|
|
205
216
|
liveRef.current = '';
|
|
217
|
+
setStaticKey((k) => k + 1); // 重挂 Static:复位其内部已渲染索引,新会话记录从头正常渲染
|
|
206
218
|
addLog('info', '已开启新会话(旧会话仍可 --resume 恢复)');
|
|
207
219
|
}
|
|
208
220
|
if (r.prompt) {
|
|
@@ -360,7 +372,9 @@ function App({ config, cwd, session: initialSession, extraTools, yolo, initialPr
|
|
|
360
372
|
});
|
|
361
373
|
|
|
362
374
|
// ---- 渲染 ----
|
|
363
|
-
|
|
375
|
+
// 每当定稿记录变更(logVer)派生一个全新数组引用,Static 才能识别并渲染新追加项;
|
|
376
|
+
// 流式增量只调 force()(logVer 不变)→ 此处返回同一引用,Static 不重复刷历史记录。
|
|
377
|
+
const logItems = useMemo(() => logRef.current.slice(), [logVer]);
|
|
364
378
|
const cwdName = cwd.split('/').filter(Boolean).pop() || cwd;
|
|
365
379
|
const before = inp.text.slice(0, inp.cursor);
|
|
366
380
|
const at = inp.text[inp.cursor] ?? ' ';
|
|
@@ -370,7 +384,7 @@ function App({ config, cwd, session: initialSession, extraTools, yolo, initialPr
|
|
|
370
384
|
Box,
|
|
371
385
|
{ flexDirection: 'column' },
|
|
372
386
|
// 已定稿记录(Static:只渲染一次,天然滚动)
|
|
373
|
-
h(Static, { items: logItems }, (item) => h(LogLine, { key: item.id, item })),
|
|
387
|
+
h(Static, { key: staticKey, items: logItems }, (item) => h(LogLine, { key: item.id, item })),
|
|
374
388
|
// 思考过程(reasoning,流式、不入定稿记录)
|
|
375
389
|
thinkRef.current
|
|
376
390
|
? h(Box, null, h(Text, { color: 'gray', italic: true }, '✦ 思考 ' + thinkRef.current.replace(/\n/g, ' ').slice(-160)))
|
|
@@ -458,7 +472,15 @@ function LogLine({ item }) {
|
|
|
458
472
|
* 启动 ink TUI。先(在 ink 接管屏幕前)连接 MCP 并打印连接信息,再进入全屏界面;退出后关闭 MCP。
|
|
459
473
|
*/
|
|
460
474
|
export async function runInk({ config, cwd, session, yolo, initialPrompt, version }) {
|
|
461
|
-
process.stdout.write(
|
|
475
|
+
process.stdout.write(renderWelcome({
|
|
476
|
+
model: config.model,
|
|
477
|
+
cwd,
|
|
478
|
+
version,
|
|
479
|
+
platform: /^(agent|workflow|datahuman)-/.test(String(config.model || '')),
|
|
480
|
+
yolo,
|
|
481
|
+
consult: config.consult ? (config.consultLabel || config.consult) : null,
|
|
482
|
+
ink: true,
|
|
483
|
+
}) + '\n');
|
|
462
484
|
const { extraTools, clients } = await buildExtraTools(config, cwd);
|
|
463
485
|
const { waitUntilExit } = render(
|
|
464
486
|
h(App, { config, cwd, session, extraTools, yolo, initialPrompt, version, onExit: () => closeMcp(clients) }),
|