@bolloon/bolloon-agent 0.4.4 → 0.4.6
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/dist/agents/pi-sdk-tools.js +62 -45
- package/dist/agents/pi-sdk.js +13 -0
- package/dist/agents/skill-writer.js +15 -3
- package/dist/agents/workflow-pivot-loop.js +11 -1
- package/dist/bootstrap/memory-compressor.js +1 -1
- package/dist/cli/ink-app.js +18 -0
- package/dist/index.js +110 -26
- package/dist/orbitdb/agent-tools.js +139 -0
- package/dist/orbitdb/kanban-store.js +259 -0
- package/dist/orbitdb/task-store.js +112 -0
- package/dist/pi-ecosystem-judgment/human-value-store.js +1 -1
- package/dist/pi-ecosystem-mcp/index.js +146 -5
- package/dist/security/tool-gate.js +2 -0
- package/dist/web/edge-auth-client.js +120 -0
- package/dist/web/server-storage.js +29 -0
- package/dist/web/server.js +38 -55
- package/package.json +1 -1
|
@@ -34,6 +34,43 @@ export const SIDE_EFFECT_TOOLS = new Set([
|
|
|
34
34
|
'write_file', 'edit_file', 'shell_exec', 'git_commit', 'git_push', 'git_branch',
|
|
35
35
|
'create_task', 'update_task',
|
|
36
36
|
]);
|
|
37
|
+
export async function runTerminalCommand(raw, opts = {}) {
|
|
38
|
+
const { checkTerminalCommand } = await import('./shell-guard.js');
|
|
39
|
+
const timeoutMs = opts.timeoutMs ?? 30000;
|
|
40
|
+
const list = (opts.commands && opts.commands.length > 0) ? opts.commands : [raw];
|
|
41
|
+
for (const c of list) {
|
|
42
|
+
const guard = checkTerminalCommand(String(c || '').trim());
|
|
43
|
+
if (!guard.allowed) {
|
|
44
|
+
return { success: false, error: `[terminal-guard] ${guard.reason}`, deniedByGuard: true };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const { exec } = await import('child_process');
|
|
48
|
+
const runOne = (cmdStr) => new Promise((resolve) => {
|
|
49
|
+
exec(cmdStr, {
|
|
50
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
51
|
+
timeout: timeoutMs,
|
|
52
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
53
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
54
|
+
}, (err, stdout, stderr) => {
|
|
55
|
+
const output = ((stdout || '') + (stderr ? `\n[stderr]\n${stderr}` : '')).trim().slice(0, 8000);
|
|
56
|
+
if (err) {
|
|
57
|
+
resolve({ success: false, cmd: cmdStr.slice(0, 120), error: `exit ${err.code ?? '?'}: ${String(err.message || '').slice(0, 200)}`, output: output || undefined });
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
resolve({ success: true, cmd: cmdStr.slice(0, 120), output: output || '(无输出)' });
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
if (list.length === 1)
|
|
65
|
+
return runOne(list[0]);
|
|
66
|
+
const results = await Promise.all(list.map((c) => runOne(c)));
|
|
67
|
+
const failed = results.filter((r) => !r.success);
|
|
68
|
+
const allOutput = results.map((r) => `${r.cmd}\n${r.output || r.error || ''}`).join('\n\n---\n\n');
|
|
69
|
+
if (failed.length > 0) {
|
|
70
|
+
return { success: false, error: `${failed.length}/${list.length} 条命令失败: ${failed[0].error}`, output: allOutput, partial: results };
|
|
71
|
+
}
|
|
72
|
+
return { success: true, output: allOutput, count: list.length, parallel: true };
|
|
73
|
+
}
|
|
37
74
|
export function registerBuiltinTools(ctx) {
|
|
38
75
|
ctx.tools.set('read_document', {
|
|
39
76
|
name: 'read_document',
|
|
@@ -621,17 +658,22 @@ export function registerBuiltinTools(ctx) {
|
|
|
621
658
|
for (const tool of p2pDocumentTools) {
|
|
622
659
|
ctx.tools.set(tool.name, tool);
|
|
623
660
|
}
|
|
624
|
-
// shell_exec
|
|
661
|
+
// 2026-08-12 (Task2): shell_exec / terminal 统一走模块级 runTerminalCommand (宽松护栏 + 多命令并行).
|
|
662
|
+
// shell_exec — 2026-08-12 (Task2): 与 terminal 统一走宽松护栏 (denylist-only).
|
|
663
|
+
// 兼容旧格式 (command + args 数组), 内部转成完整命令字符串交给 runTerminal,
|
|
664
|
+
// 不再用窄白名单 → 模型不会因 "command 不在白名单" 报错.
|
|
625
665
|
ctx.tools.set('shell_exec', {
|
|
626
666
|
name: 'shell_exec',
|
|
627
|
-
description: '
|
|
628
|
-
parameters: { command: '可执行文件 (
|
|
667
|
+
description: '执行 shell 命令 (兼容模式, 参数数组或命令字符串均可). 护栏只挡高危破坏操作 (sudo/格式化/rm -rf 根目录/写 ~/.bolloon 数据). 推荐直接用 terminal 传完整命令字符串.',
|
|
668
|
+
parameters: { command: '可执行文件 或 完整命令 (必填)', args: '参数数组, 逗号分隔 (可选)', timeoutMs: '超时毫秒, 默认 30000' },
|
|
629
669
|
execute: async (args) => {
|
|
630
670
|
const cmd = String(args.command || '').trim();
|
|
631
671
|
if (!cmd)
|
|
632
672
|
return { success: false, error: 'command 必填' };
|
|
633
|
-
|
|
673
|
+
// 兼容参数数组 → 拼成命令字符串
|
|
674
|
+
let full = cmd;
|
|
634
675
|
const rawArgs = args.args;
|
|
676
|
+
let argList = [];
|
|
635
677
|
if (Array.isArray(rawArgs)) {
|
|
636
678
|
argList = rawArgs.map((s) => String(s).trim()).filter(Boolean);
|
|
637
679
|
}
|
|
@@ -640,61 +682,36 @@ export function registerBuiltinTools(ctx) {
|
|
|
640
682
|
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
|
641
683
|
try {
|
|
642
684
|
const parsed = JSON.parse(trimmed);
|
|
643
|
-
if (Array.isArray(parsed))
|
|
685
|
+
if (Array.isArray(parsed))
|
|
644
686
|
argList = parsed.map((s) => String(s).trim()).filter(Boolean);
|
|
645
|
-
}
|
|
646
687
|
}
|
|
647
|
-
catch { /*
|
|
688
|
+
catch { /* 非 JSON 数组 */ }
|
|
648
689
|
}
|
|
649
|
-
if (argList.length === 0)
|
|
690
|
+
if (argList.length === 0)
|
|
650
691
|
argList = trimmed.split(',').map(s => s.trim()).filter(Boolean);
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
const timeoutMs = Number(args.timeoutMs) || 30000;
|
|
654
|
-
const result = await shellExec(cmd, argList, { timeoutMs });
|
|
655
|
-
if (result.deniedByGuard) {
|
|
656
|
-
return { success: false, error: result.error };
|
|
657
692
|
}
|
|
658
|
-
|
|
659
|
-
|
|
693
|
+
// 若 command 只是可执行文件名且带 args → 拼成 "cmd arg1 arg2"; 否则原样
|
|
694
|
+
if (argList.length > 0 && !/\s/.test(cmd) && !cmd.includes('&&') && !cmd.includes(';') && !cmd.includes('|')) {
|
|
695
|
+
full = `${cmd} ${argList.map(a => (/[\s"&|<>^()%!`]/.test(a) ? `"${a.replace(/"/g, '""')}"` : a)).join(' ')}`;
|
|
660
696
|
}
|
|
661
|
-
|
|
697
|
+
const timeoutMs = Number(args.timeoutMs) || 30000;
|
|
698
|
+
return await runTerminalCommand(full, { timeoutMs, cwd: ctx.cwd });
|
|
662
699
|
}
|
|
663
700
|
});
|
|
664
701
|
// 2026-08-10: terminal — 灵活终端写命令 (用户要求: bolloon 自己写命令进 terminal, 少围栏).
|
|
665
|
-
//
|
|
666
|
-
//
|
|
702
|
+
// 2026-08-12 (Task2): 支持 commands 数组并行执行; 与 shell_exec 统一走 runTerminalCommand.
|
|
703
|
+
// 与 shell_exec 的区别: 直接接受完整 shell 命令字符串, 更适合模型自主写命令.
|
|
667
704
|
ctx.tools.set('terminal', {
|
|
668
705
|
name: 'terminal',
|
|
669
|
-
description: '执行完整 shell 命令 (支持管道/重定向/写文件/跑脚本). 护栏只挡高危破坏操作 (sudo/格式化/rm -rf 根目录/写 ~/.bolloon 数据), 其余灵活放行. 适合: 写 HTML 文件、跑 python/node 脚本、查系统状态、装依赖.',
|
|
670
|
-
parameters: { command: '完整 shell 命令 (必填, 如: echo "<html>" > /tmp/site/index.html && ls /tmp/site)', timeoutMs: '超时毫秒, 默认 30000' },
|
|
706
|
+
description: '执行完整 shell 命令 (支持管道/重定向/写文件/跑脚本). 护栏只挡高危破坏操作 (sudo/格式化/rm -rf 根目录/写 ~/.bolloon 数据), 其余灵活放行. 适合: 写 HTML 文件、跑 python/node 脚本、查系统状态、装依赖. 多条命令用 commands 数组并行执行.',
|
|
707
|
+
parameters: { command: '完整 shell 命令 (必填, 如: echo "<html>" > /tmp/site/index.html && ls /tmp/site)', commands: '可选: 多条命令数组 (并行执行), 每条独立字符串', timeoutMs: '超时毫秒, 默认 30000' },
|
|
671
708
|
execute: async (args) => {
|
|
672
709
|
const raw = String(args.command || '').trim();
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
const guard = checkTerminalCommand(raw);
|
|
677
|
-
if (!guard.allowed) {
|
|
678
|
-
return { success: false, error: `[terminal-guard] ${guard.reason}`, deniedByGuard: true };
|
|
679
|
-
}
|
|
680
|
-
const { exec } = await import('child_process');
|
|
710
|
+
const commands = Array.isArray(args.commands) ? args.commands.map((c) => String(c || '').trim()).filter(Boolean) : [];
|
|
711
|
+
if (!raw && commands.length === 0)
|
|
712
|
+
return { success: false, error: 'command 或 commands 必填' };
|
|
681
713
|
const timeoutMs = Number(args.timeoutMs) || 30000;
|
|
682
|
-
return
|
|
683
|
-
exec(raw, {
|
|
684
|
-
cwd: process.cwd(),
|
|
685
|
-
timeout: timeoutMs,
|
|
686
|
-
maxBuffer: 8 * 1024 * 1024,
|
|
687
|
-
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
688
|
-
}, (err, stdout, stderr) => {
|
|
689
|
-
const output = ((stdout || '') + (stderr ? `\n[stderr]\n${stderr}` : '')).trim().slice(0, 8000);
|
|
690
|
-
if (err) {
|
|
691
|
-
resolve({ success: false, error: `exit ${err.code ?? '?'}: ${String(err.message || '').slice(0, 200)}`, output: output || undefined });
|
|
692
|
-
}
|
|
693
|
-
else {
|
|
694
|
-
resolve({ success: true, output: output || '(无输出)' });
|
|
695
|
-
}
|
|
696
|
-
});
|
|
697
|
-
});
|
|
714
|
+
return await runTerminalCommand(raw, { timeoutMs, cwd: ctx.cwd, commands: commands.length > 0 ? commands : undefined });
|
|
698
715
|
}
|
|
699
716
|
});
|
|
700
717
|
// self_improve
|
package/dist/agents/pi-sdk.js
CHANGED
|
@@ -201,6 +201,15 @@ export class PiAgentSession {
|
|
|
201
201
|
currentSignal = null;
|
|
202
202
|
/** Bootstrap SessionStart 拼的 system prompt 片段 (用完即清) */
|
|
203
203
|
bootstrapAddition = '';
|
|
204
|
+
/** 2026-08-12 (Task3 认知卸载): 工具选择与认知卸载指南 — 注入 system prompt, 降低"模型不触发 write/edit/read"率. */
|
|
205
|
+
static TOOL_SELECTION_GUIDE = `【工具选择与认知卸载指南 (严格遵循)】
|
|
206
|
+
- 写/改文件: 必须用 write_file / edit_file (不要用 terminal 拼字符串写文件).
|
|
207
|
+
- 读文件: 用 read_file / read_directory / list_files.
|
|
208
|
+
- 跑命令/查状态/装依赖/跑脚本: 用 terminal.
|
|
209
|
+
- 执行 git 操作: 用 git_* 专用工具 (git_diff/git_commit/git_push/git_branch/git_log).
|
|
210
|
+
- 查看/调用技能: 用 list_skills / use_skill.
|
|
211
|
+
- 认知卸载: 当任务对你过大或反复失败时, 不要死磕 — 用 delegate_to_engine 把编码/复杂任务委派给外部引擎 (codex/claude-code/opencode), 或把目标拆小分步完成.
|
|
212
|
+
- 每步只选一个最合适的工具, 先读后写, 写完验证 (跑 tsc/vitest 或读回文件).`;
|
|
204
213
|
/** 当前 prompt 开始时间 (供 Stop hook 计算 durationMs) */
|
|
205
214
|
promptStartTime = 0;
|
|
206
215
|
/** 当前 channel id (由 getAgentForChannel / prompt 4 参注入, 供 hook / log 使用) */
|
|
@@ -1055,6 +1064,8 @@ ${this.currentIntentHint}
|
|
|
1055
1064
|
|
|
1056
1065
|
${this.getToolDefinitions()}
|
|
1057
1066
|
|
|
1067
|
+
${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
1068
|
+
|
|
1058
1069
|
工作模式:
|
|
1059
1070
|
1. 理解用户自然语言请求
|
|
1060
1071
|
2. 分析需要哪些工具来完成
|
|
@@ -1295,6 +1306,8 @@ ${loopProgressSection}
|
|
|
1295
1306
|
|
|
1296
1307
|
${toolDefs}
|
|
1297
1308
|
|
|
1309
|
+
${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
1310
|
+
|
|
1298
1311
|
工作模式:
|
|
1299
1312
|
1. 理解用户自然语言请求
|
|
1300
1313
|
2. 分析需要哪些工具来完成
|
|
@@ -266,9 +266,21 @@ export async function writeRunEndSkillCandidates(steps, source, minOk = 2) {
|
|
|
266
266
|
return { wrote: false, reason: `成功工具不足 (${okSteps.length} < ${minOk})` };
|
|
267
267
|
}
|
|
268
268
|
const toolNames = okSteps.map((s) => s.name).slice(0, 5).join(', ');
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
269
|
+
// 2026-08-12 (Task5): 让 run-end 候选 body 更可复用 — 结构化呈现调用链 + 每步作用 + 适用场景.
|
|
270
|
+
const stepLines = okSteps.map((s, i) => {
|
|
271
|
+
const out = s.output ? String(s.output).replace(/\s+/g, ' ').slice(0, 140) : '';
|
|
272
|
+
return `${i + 1}. **${s.name}**${out ? ` — ${out}` : ''}`;
|
|
273
|
+
}).join('\n');
|
|
274
|
+
const body = `## 适用场景\n用户请求需要连续执行工具序列: ${toolNames}.\n\n` +
|
|
275
|
+
`## 调用链\n${stepLines}\n\n` +
|
|
276
|
+
`## 流程要点\n${okSteps.map((s, i) => {
|
|
277
|
+
// 2026-08-12 (Task5 优化): 用真实输出提炼每步作用, 避免千篇一律的"调用 X 获取下一步输入".
|
|
278
|
+
const out = s.output ? String(s.output).replace(/\s+/g, ' ').trim() : '';
|
|
279
|
+
const clue = out && out.length > 30 ? out.slice(0, 60) + '…' : (out || '完成');
|
|
280
|
+
return `- 第 ${i + 1} 步 ${s.name}: ${clue}`;
|
|
281
|
+
}).join('\n')}\n\n` +
|
|
282
|
+
`## 如何验证\n- 涉及改代码: 跑 tsc + vitest 确认无错; 涉及写文件: read_file 读回校验.\n` +
|
|
283
|
+
`- 沉淀为正式 skill 前请人工确认流程可复用, 再 list_skill_candidates / promote_skill 转正\n`;
|
|
272
284
|
// 2026-08-08: 稳定签名 + 固定文件名 → 同一套工具反复跑时合并更新到同一个候选 (runs++)
|
|
273
285
|
const signature = toolSignature(okSteps);
|
|
274
286
|
const candName = `auto-${signature}`;
|
|
@@ -193,6 +193,16 @@ export class WorkflowPivotLoop {
|
|
|
193
193
|
* 传原生 tools + tool_choice auto → LLM 返回结构化 tool_calls.
|
|
194
194
|
*/
|
|
195
195
|
buildOpenAITools() {
|
|
196
|
+
// 2026-08-12 (Task3 认知卸载): 核心编码工具加强 description, 提升模型触发率.
|
|
197
|
+
const USAGE_HINT = {
|
|
198
|
+
write_file: '写/创建文件用此工具 (改代码首选). ',
|
|
199
|
+
edit_file: '修改文件指定内容用此工具 (改代码首选). ',
|
|
200
|
+
read_file: '读取文件内容用此工具. ',
|
|
201
|
+
read_directory: '列目录用此工具. ',
|
|
202
|
+
list_files: '列出文件用此工具. ',
|
|
203
|
+
terminal: '跑 shell 命令/脚本/装依赖/查状态用此工具 (不要用它写文件). ',
|
|
204
|
+
delegate_to_engine: '任务过大或复杂时委派给外部编码引擎 (认知卸载). ',
|
|
205
|
+
};
|
|
196
206
|
const out = [];
|
|
197
207
|
for (const [name, tool] of this.tools) {
|
|
198
208
|
const params = tool.parameters || {};
|
|
@@ -207,7 +217,7 @@ export class WorkflowPivotLoop {
|
|
|
207
217
|
type: 'function',
|
|
208
218
|
function: {
|
|
209
219
|
name,
|
|
210
|
-
description: tool.description || name,
|
|
220
|
+
description: (USAGE_HINT[name] || '') + (tool.description || name),
|
|
211
221
|
parameters: { type: 'object', properties, required },
|
|
212
222
|
},
|
|
213
223
|
});
|
|
@@ -54,7 +54,7 @@ export function getSessionCursorPath(agentId, channelId, sessionId, home) {
|
|
|
54
54
|
/**
|
|
55
55
|
* Session 缓存文件路径 (跟 server.ts:1806 sessionKey 规则一致)
|
|
56
56
|
*/
|
|
57
|
-
function getSessionCacheFile(channelId, sessionId, home) {
|
|
57
|
+
export function getSessionCacheFile(channelId, sessionId, home) {
|
|
58
58
|
const root = path.join(home || os.homedir(), '.bolloon', 'sessions', 'cache');
|
|
59
59
|
const safeChannel = sanitizeKey(channelId);
|
|
60
60
|
const safeSession = sanitizeKey(sessionId).replace(/:/g, '__');
|
package/dist/cli/ink-app.js
CHANGED
|
@@ -293,11 +293,23 @@ const InkApp = ({ onPrompt, initialStatus, getStatusUpdate, terminalW, terminalH
|
|
|
293
293
|
globalThis.__inkSetTransient = (v) => {
|
|
294
294
|
setTransient(v === undefined ? null : v);
|
|
295
295
|
};
|
|
296
|
+
// 2026-08-12 (Task4): 原地替换最后一条消息 (命令加载态 → 完成态用).
|
|
297
|
+
// 不命中则追加 (兼容旧逻辑).
|
|
298
|
+
globalThis.__inkReplaceLast = (line) => {
|
|
299
|
+
setMsgs(prev => {
|
|
300
|
+
if (prev.length === 0)
|
|
301
|
+
return [...prev, line];
|
|
302
|
+
const next = prev.slice();
|
|
303
|
+
next[next.length - 1] = line;
|
|
304
|
+
return next;
|
|
305
|
+
});
|
|
306
|
+
};
|
|
296
307
|
return () => {
|
|
297
308
|
delete globalThis.__inkAppend;
|
|
298
309
|
delete globalThis.__inkSetStatus;
|
|
299
310
|
delete globalThis.__inkSetThinking;
|
|
300
311
|
delete globalThis.__inkSetTransient;
|
|
312
|
+
delete globalThis.__inkReplaceLast;
|
|
301
313
|
};
|
|
302
314
|
}, []);
|
|
303
315
|
const onSubmit = useCallback((value) => {
|
|
@@ -574,6 +586,12 @@ export function inkAppendLine(line) {
|
|
|
574
586
|
if (fn)
|
|
575
587
|
fn(line);
|
|
576
588
|
}
|
|
589
|
+
/** 2026-08-12 (Task4): 原地替换最后一条消息 (命令加载态 → 完成态). 无消息时追加. */
|
|
590
|
+
export function inkReplaceLastLine(line) {
|
|
591
|
+
const fn = globalThis.__inkReplaceLast;
|
|
592
|
+
if (fn)
|
|
593
|
+
fn(line);
|
|
594
|
+
}
|
|
577
595
|
export function inkSetStatus(s) {
|
|
578
596
|
const fn = globalThis.__inkSetStatus;
|
|
579
597
|
if (fn)
|
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import { getGlobalSharedContext } from './social/global-shared-context.js';
|
|
|
16
16
|
import { createBollharnessIntegration } from './bollharness-integration/index.js';
|
|
17
17
|
import * as readline from 'readline';
|
|
18
18
|
import { printBanner, renderDialog, renderUserMessage, renderAgentMessage, renderMessageBox, renderToolCallListItem, termWidth } from './cli/loading-tui.js';
|
|
19
|
-
import { startInk, stopInk, inkAppendLine as appendLine, inkSetStatus, inkSetThinking, inkSetTransient } from './cli/ink-app.js';
|
|
19
|
+
import { startInk, stopInk, inkAppendLine as appendLine, inkReplaceLastLine as replaceLastLine, inkSetStatus, inkSetThinking, inkSetTransient } from './cli/ink-app.js';
|
|
20
20
|
// 启动自动检查更新:后台、节流、检测到新版本自动安装(可被 --no-update / BOLLOON_SKIP_UPDATE 关闭)
|
|
21
21
|
import { createRequire } from 'module';
|
|
22
22
|
const _require = createRequire(import.meta.url);
|
|
@@ -423,6 +423,9 @@ let cliStartTime = 0;
|
|
|
423
423
|
let cliModelName = '…';
|
|
424
424
|
let cliAgentName = '…';
|
|
425
425
|
let cliActiveChannelId = null;
|
|
426
|
+
// 2026-08-12: 当前 active channel 的 agentId (如 agent-alice). memory 落盘按 agentId 存,
|
|
427
|
+
// /memory /resume /did 读路径必须用 agentId 而非 display name (cliAgentName), 否则路径不一致读不到.
|
|
428
|
+
let cliAgentId = null;
|
|
426
429
|
// 2026-08-10: CLI 自动整理心跳 (与社交心跳并列, 独立于 server) — 退出时 stop
|
|
427
430
|
let cliOrganizeHeartbeat = null;
|
|
428
431
|
function fmtDuration(ms) {
|
|
@@ -435,6 +438,13 @@ function fmtDuration(ms) {
|
|
|
435
438
|
const h = Math.floor(m / 60);
|
|
436
439
|
return `${h}h ${m % 60}m`;
|
|
437
440
|
}
|
|
441
|
+
/** 2026-08-12: 返回当前 agentId (memory 路径用). 优先 cliAgentId, fallback cliAgentName. */
|
|
442
|
+
function getCliAgentId() {
|
|
443
|
+
if (cliAgentId)
|
|
444
|
+
return cliAgentId;
|
|
445
|
+
// 无显式 agentId 时退到 cliAgentName (harness 默认 'agent', 其余用名字)
|
|
446
|
+
return cliAgentName === 'bolloon' ? 'agent' : (cliAgentName || 'agent');
|
|
447
|
+
}
|
|
438
448
|
/** 2026-08-06: 从 ContextManager 读上下文用量 (CLI 状态栏数据源, 失败退化 0/1M) */
|
|
439
449
|
// 2026-08-07: 不能用 require 加载 ESM 模块 (ERR_REQUIRE_ESM) → startCLI 里 await import 一次缓存引用,
|
|
440
450
|
// 同步函数 getCliCtxUsage/getStatus 复用 — 之前裸 require 与 _require 都抛错被 catch → 状态栏恒 0/1M
|
|
@@ -563,6 +573,9 @@ async function startCLI(comm) {
|
|
|
563
573
|
if (active) {
|
|
564
574
|
cliAgentName = active.name;
|
|
565
575
|
cliActiveChannelId = active.channelId ?? null;
|
|
576
|
+
// 2026-08-12: 同步 agentId (memory 路径一致) — 从 rawChannels 按 channelId 取 agentId
|
|
577
|
+
const raw = active.channelId ? store.rawChannels.find((c) => c.id === active.channelId) : undefined;
|
|
578
|
+
cliAgentId = raw?.agentId || null;
|
|
566
579
|
}
|
|
567
580
|
}
|
|
568
581
|
catch {
|
|
@@ -692,23 +705,36 @@ async function processInput(input, comm) {
|
|
|
692
705
|
const runEndOkSteps = [];
|
|
693
706
|
// each iteration
|
|
694
707
|
let lastToolEvent = null;
|
|
695
|
-
// !command —
|
|
708
|
+
// !command — 直接执行终端命令. 2026-08-12 (Task4): 支持多命令 (&& / ;) 逐段顺序执行 + 加载显示.
|
|
696
709
|
if (trimmed.startsWith('!')) {
|
|
697
710
|
const cmd = trimmed.slice(1).trim();
|
|
698
711
|
if (!cmd) {
|
|
699
|
-
appendLine(`${C_DIM}!<命令>
|
|
712
|
+
appendLine(`${C_DIM}!<命令> 执行终端命令 (支持 && 串联多命令), 如 !ls -la${RESET}`);
|
|
700
713
|
return;
|
|
701
714
|
}
|
|
702
|
-
|
|
715
|
+
const { execSync } = await import('child_process');
|
|
716
|
+
// 拆成多段: && 逻辑与 (前失败则停) / ; 无条件顺序. 保留每段顺序执行, 显示加载态.
|
|
717
|
+
const segments = cmd.split(/\s*;\s*/).filter(Boolean);
|
|
703
718
|
try {
|
|
704
|
-
const
|
|
705
|
-
|
|
706
|
-
|
|
719
|
+
for (const seg of segments) {
|
|
720
|
+
const segCmds = seg.split(/\s*&&\s*/).filter(Boolean);
|
|
721
|
+
for (const c of segCmds) {
|
|
722
|
+
appendLine(`${C_DIM}── $ ${c}${RESET}`);
|
|
723
|
+
try {
|
|
724
|
+
const out = execSync(c, { timeout: 30000, encoding: 'utf-8', cwd: process.cwd() });
|
|
725
|
+
appendLine(`${C_DIM}${out || '(无输出)'}${RESET}`);
|
|
726
|
+
}
|
|
727
|
+
catch (e) {
|
|
728
|
+
appendLine(`${C_ERROR}${e.stderr || e.message}${RESET}`);
|
|
729
|
+
// && 逻辑与: 某段失败则中止后续 && 段
|
|
730
|
+
break;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
707
734
|
}
|
|
708
|
-
|
|
709
|
-
appendLine(`${
|
|
735
|
+
finally {
|
|
736
|
+
appendLine(`${C_DIM}──${RESET}`);
|
|
710
737
|
}
|
|
711
|
-
appendLine(`${C_DIM}──${RESET}`);
|
|
712
738
|
return;
|
|
713
739
|
}
|
|
714
740
|
// /channel — 切换当前智能体 (agent channel), 参数 name/id/number 自动解析
|
|
@@ -743,6 +769,7 @@ async function processInput(input, comm) {
|
|
|
743
769
|
await store.setActive(r.channel.id);
|
|
744
770
|
cliAgentName = r.identity.name;
|
|
745
771
|
cliActiveChannelId = r.channel.id;
|
|
772
|
+
cliAgentId = r.channel.agentId || null; // 2026-08-12: memory 路径一致
|
|
746
773
|
// 2026-08-09: 切 channel 必须重建 agent session — 否则身份/记忆停留在旧 channel (bug 修复)
|
|
747
774
|
invalidateAgent();
|
|
748
775
|
// 立即重建 (提前建好, 避免下次输入才卡顿; 失败不阻塞切换)
|
|
@@ -809,6 +836,7 @@ async function processInput(input, comm) {
|
|
|
809
836
|
await store.setActive(id);
|
|
810
837
|
cliAgentName = name.trim();
|
|
811
838
|
cliActiveChannelId = id;
|
|
839
|
+
cliAgentId = agentId; // 2026-08-12: memory 路径一致
|
|
812
840
|
// 2026-08-09: 新建 agent 后立即重建 session — 否则新 agent 身份不加载 (bug 修复)
|
|
813
841
|
invalidateAgent();
|
|
814
842
|
try {
|
|
@@ -1107,7 +1135,7 @@ async function processInput(input, comm) {
|
|
|
1107
1135
|
const { getMemoryDir } = await import('./bootstrap/memory-compressor.js');
|
|
1108
1136
|
const { readdir, readFile } = await import('fs/promises');
|
|
1109
1137
|
const { join } = await import('path');
|
|
1110
|
-
const dir = getMemoryDir(
|
|
1138
|
+
const dir = getMemoryDir(getCliAgentId());
|
|
1111
1139
|
const files = (await readdir(join(dir, 'sessions')).catch(() => [])).filter((f) => f.endsWith('.summary.md'));
|
|
1112
1140
|
appendLine(`${C_ACCENT}记忆摘要 (${files.length} 个 session):${RESET}`);
|
|
1113
1141
|
for (const f of files.slice(-5)) {
|
|
@@ -1126,10 +1154,10 @@ async function processInput(input, comm) {
|
|
|
1126
1154
|
// /resume — 恢复: 最近记忆摘要 + 进行中计划
|
|
1127
1155
|
if (cmd === '/resume' || cmd.startsWith('/resume ')) {
|
|
1128
1156
|
try {
|
|
1129
|
-
const { getMemoryDir
|
|
1157
|
+
const { getMemoryDir } = await import('./bootstrap/memory-compressor.js');
|
|
1130
1158
|
const { readFile, readdir } = await import('fs/promises');
|
|
1131
1159
|
const { join } = await import('path');
|
|
1132
|
-
const dir = getMemoryDir(
|
|
1160
|
+
const dir = getMemoryDir(getCliAgentId());
|
|
1133
1161
|
const files = (await readdir(join(dir, 'sessions')).catch(() => [])).filter((f) => f.endsWith('.summary.md'));
|
|
1134
1162
|
appendLine(`${C_ACCENT}↻ 恢复上下文:${RESET}`);
|
|
1135
1163
|
if (files.length > 0) {
|
|
@@ -1286,6 +1314,50 @@ async function processInput(input, comm) {
|
|
|
1286
1314
|
catch { /* 静默 */ }
|
|
1287
1315
|
return;
|
|
1288
1316
|
}
|
|
1317
|
+
// /skills [名] — 查看正式技能 (2026-08-12 Task5): 无参列全部, 带名看详情. 运行时开始前的技能 view.
|
|
1318
|
+
if (cmd === '/skills' || cmd.startsWith('/skills ')) {
|
|
1319
|
+
try {
|
|
1320
|
+
const { defaultSkillPaths, loadSkillsDir } = await import('./agents/skill-loader.js');
|
|
1321
|
+
const dirs = defaultSkillPaths();
|
|
1322
|
+
const metas = [];
|
|
1323
|
+
for (const d of dirs) {
|
|
1324
|
+
const m = await loadSkillsDir(d);
|
|
1325
|
+
for (const s of m)
|
|
1326
|
+
if (s.status === 'active' && !metas.some(x => x.name === s.name))
|
|
1327
|
+
metas.push(s);
|
|
1328
|
+
}
|
|
1329
|
+
const q = cmd.startsWith('/skills ') ? cmd.slice('/skills '.length).trim().toLowerCase() : '';
|
|
1330
|
+
if (!q) {
|
|
1331
|
+
appendLine(`${C_ACCENT}技能 (${metas.length}):${RESET}`);
|
|
1332
|
+
if (metas.length === 0)
|
|
1333
|
+
appendLine(` ${C_DIM}暂无正式技能 — run-end 经验可沉淀为 skill${RESET}`);
|
|
1334
|
+
for (const s of metas.slice(0, 20)) {
|
|
1335
|
+
const desc = (s.description || '').slice(0, 60);
|
|
1336
|
+
appendLine(` ${C_DIM}·${RESET} ${C_ACCENT}${s.name}${RESET}${desc ? ` ${C_DIM}${desc}${RESET}` : ''}`);
|
|
1337
|
+
}
|
|
1338
|
+
appendLine(`${C_DIM}用法: /skills <名> 查看详情${RESET}`);
|
|
1339
|
+
}
|
|
1340
|
+
else {
|
|
1341
|
+
const hit = metas.find(s => s.name.toLowerCase() === q || s.name.toLowerCase().includes(q));
|
|
1342
|
+
if (!hit) {
|
|
1343
|
+
appendLine(`${C_WARN}未找到技能: '${q}'${RESET}`);
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
appendLine(`${C_ACCENT}═ ${hit.name} ═${RESET}`);
|
|
1347
|
+
if (hit.description)
|
|
1348
|
+
appendLine(` ${C_DIM}描述:${RESET} ${hit.description}`);
|
|
1349
|
+
if (hit.triggers && hit.triggers.length > 0)
|
|
1350
|
+
appendLine(` ${C_DIM}触发:${RESET} ${hit.triggers.join(', ')}`);
|
|
1351
|
+
const body = (hit.body || '').trim().slice(0, 1200);
|
|
1352
|
+
if (body)
|
|
1353
|
+
appendLine(` ${C_DIM}---${RESET}\n${body}`);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
catch (e) {
|
|
1357
|
+
appendLine(`${C_ERROR}/skills 失败: ${String(e?.message || e).slice(0, 120)}${RESET}`);
|
|
1358
|
+
}
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1289
1361
|
// /mcp — MCP 插件/工具列表
|
|
1290
1362
|
if (cmd === '/mcp') {
|
|
1291
1363
|
try {
|
|
@@ -1324,7 +1396,7 @@ async function processInput(input, comm) {
|
|
|
1324
1396
|
if (cmd === '/did') {
|
|
1325
1397
|
try {
|
|
1326
1398
|
const { loadOrCreateAgentIdentity } = await import('./agents/agent-identity.js');
|
|
1327
|
-
const identity = loadOrCreateAgentIdentity(
|
|
1399
|
+
const identity = loadOrCreateAgentIdentity(getCliAgentId());
|
|
1328
1400
|
appendLine(`${C_ACCENT}DID 身份:${RESET}`);
|
|
1329
1401
|
appendLine(` ${C_DIM}did:${RESET} ${identity.did}`);
|
|
1330
1402
|
appendLine(` ${C_DIM}publicKey:${RESET} ${identity.publicKey?.slice(0, 32) || '—'}...`);
|
|
@@ -1649,6 +1721,7 @@ async function processInput(input, comm) {
|
|
|
1649
1721
|
appendLine(` ${C_ACCENT}/todo${RESET} 查看/勾选循环步骤 ${C_DIM}/todo <planId> <序号>${RESET}`);
|
|
1650
1722
|
appendLine(` ${C_ACCENT}/tools${RESET} 可用工具列表 (名/参数/简介)`);
|
|
1651
1723
|
appendLine(` ${C_ACCENT}/skill${RESET} 技能候选 ${C_DIM}skill-writer 沉淀候选${RESET}`);
|
|
1724
|
+
appendLine(` ${C_ACCENT}/skills${RESET} 查看正式技能 ${C_DIM}/skills <名> 看详情 (运行时开始前 view)${RESET}`);
|
|
1652
1725
|
appendLine(` ${C_ACCENT}/mcp${RESET} MCP 服务器列表`);
|
|
1653
1726
|
appendLine(` ${C_ACCENT}/agent${RESET} 当前智能体身份`);
|
|
1654
1727
|
appendLine(` ${C_ACCENT}/did${RESET} DID 身份`);
|
|
@@ -1753,15 +1826,8 @@ async function processInput(input, comm) {
|
|
|
1753
1826
|
// 真正思考内容在 status 的 Reflection/💡 事件 → 下方框渲染
|
|
1754
1827
|
}
|
|
1755
1828
|
else if (e.phase && !e.type) {
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
const phLabel = {
|
|
1759
|
-
intent_classified: '意图识别',
|
|
1760
|
-
tool_selected: '工具选择',
|
|
1761
|
-
reflection: '反思',
|
|
1762
|
-
planning: '规划',
|
|
1763
|
-
};
|
|
1764
|
-
appendLine(`${C_WARN}◈ ${phLabel[ph] || ph}${detail}${RESET}`);
|
|
1829
|
+
// 2026-08-12 (Task4): phase (意图识别/工具选择/规划) 是模型内部规划过程,
|
|
1830
|
+
// 用户偏好"中间过程不显示" → 静默丢弃, 不污染终端. (仅 Reflection 框保留, 走 status 分支)
|
|
1765
1831
|
}
|
|
1766
1832
|
else if (e.type === 'status' && e.content) {
|
|
1767
1833
|
const content = String(e.content);
|
|
@@ -1771,13 +1837,24 @@ async function processInput(input, comm) {
|
|
|
1771
1837
|
if (body.trim())
|
|
1772
1838
|
appendLine(renderMessageBox({ title: '💡 反思', body, color: C_WARN }));
|
|
1773
1839
|
}
|
|
1774
|
-
else if (!content.includes('🔄 循环') && !content.includes('📋 参数')
|
|
1840
|
+
else if (!content.includes('🔄 循环') && !content.includes('📋 参数')
|
|
1841
|
+
&& !content.includes('🔍 任务复杂度') && !content.includes('⚙️ 动态配置')
|
|
1842
|
+
&& !content.includes('⏹️ pivot loop')
|
|
1843
|
+
// 2026-08-12 (Task4): 循环过渡噪音 — "工具执行完成继续循环"/"继续总结" 是内部推进过程,
|
|
1844
|
+
// 不是给用户看的内容, 一律静默丢弃 (用户抱怨"每次显示触发下一轮循环"的真凶).
|
|
1845
|
+
&& !content.includes('继续循环') && !content.includes('继续总结')) {
|
|
1775
1846
|
appendLine(`${C_DIM}${content}${RESET}`);
|
|
1776
1847
|
}
|
|
1777
1848
|
}
|
|
1778
1849
|
else if (e.type === 'step_start') {
|
|
1779
1850
|
tuiToolCounter++;
|
|
1780
|
-
|
|
1851
|
+
const toolName = e.tool || '?';
|
|
1852
|
+
tuiToolCalls.push({ tool: toolName, args: e.args, _t: Date.now() });
|
|
1853
|
+
// 2026-08-12 (Task4): 命令/工具调用显示加载态 — 追加一行 "运行中", done 时原地替换.
|
|
1854
|
+
// system/loop 这类内部步骤不显示, 只显示真实工具/命令调用.
|
|
1855
|
+
if (toolName !== 'system' && toolName !== 'loop' && toolName !== '?') {
|
|
1856
|
+
appendLine(` ${C_DIM}🔧 ${toolName} 运行中...${RESET}`);
|
|
1857
|
+
}
|
|
1781
1858
|
}
|
|
1782
1859
|
else if (e.type === 'step_done' || e.type === 'step_error') {
|
|
1783
1860
|
const p = tuiToolCalls.shift();
|
|
@@ -1795,7 +1872,14 @@ async function processInput(input, comm) {
|
|
|
1795
1872
|
runEndOkSteps.push({ status: 'ok', name: t, output: e.output });
|
|
1796
1873
|
}
|
|
1797
1874
|
}
|
|
1798
|
-
|
|
1875
|
+
// 2026-08-12 (Task4): done/error 原地替换 step_start 的加载行 (若 tool 非内部).
|
|
1876
|
+
const doneTool = e.tool ?? p?.tool;
|
|
1877
|
+
if (doneTool !== 'system' && doneTool !== 'loop' && doneTool !== '?') {
|
|
1878
|
+
replaceLastLine(renderToolCallListItem(doneItem, tuiToolCalls.length + 1, tuiToolCounter));
|
|
1879
|
+
}
|
|
1880
|
+
else {
|
|
1881
|
+
appendLine(renderToolCallListItem(doneItem, tuiToolCalls.length + 1, tuiToolCounter));
|
|
1882
|
+
}
|
|
1799
1883
|
}
|
|
1800
1884
|
}
|
|
1801
1885
|
});
|