@bolloon/bolloon-agent 0.3.39 → 0.3.42
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.js +9 -0
- package/dist/bootstrap/bootstrap.js +7 -2
- package/dist/cli/ink-app.js +4 -2
- package/dist/cli/mention-data.js +5 -4
- package/dist/index.js +382 -24
- package/dist/migration/external-agent-migrator.js +153 -48
- package/package.json +1 -1
package/dist/agents/pi-sdk.js
CHANGED
|
@@ -167,6 +167,15 @@ export class PiAgentSession {
|
|
|
167
167
|
}
|
|
168
168
|
})();
|
|
169
169
|
}
|
|
170
|
+
/** 2026-08-08: 公开可用工具列表 (name + description + 参数名) 供 /tools 显示 */
|
|
171
|
+
getToolList() {
|
|
172
|
+
const out = [];
|
|
173
|
+
for (const tool of this.allowedTools()) {
|
|
174
|
+
const paramNames = tool.parameters ? Object.keys(tool.parameters) : [];
|
|
175
|
+
out.push({ name: tool.name, description: tool.description || '', parameters: paramNames });
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
170
179
|
/**
|
|
171
180
|
* Judgment 注入门临时结果: 在 prompt / promptStream / promptWithPivotLoop 入口算一次, 拼到本轮 systemPrompt 末尾
|
|
172
181
|
* 每次调用都会重置 (避免上一轮遗留)
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { runAdaptiveScan, logEvolution } from '../pi-ecosystem-judgment/adaptive-scan.js';
|
|
12
12
|
import { collectBolloonContext } from './context-collector.js';
|
|
13
|
-
import { migrateAllExternalAgents, formatMigrationNotices } from '../migration/external-agent-migrator.js';
|
|
13
|
+
import { migrateAllExternalAgents, defaultDeps, formatMigrationNotices } from '../migration/external-agent-migrator.js';
|
|
14
14
|
/**
|
|
15
15
|
* 入口: web server / CLI 启动时调一次
|
|
16
16
|
*/
|
|
@@ -20,7 +20,12 @@ export async function bootstrapBolloon(opts = {}) {
|
|
|
20
20
|
// 0. 外部智能体 (openclaw/hermes) 数据迁移 — 隐式处理, 静默跑, 结果通告用户
|
|
21
21
|
let externalAgentMigrations = [];
|
|
22
22
|
try {
|
|
23
|
-
|
|
23
|
+
const depsM = defaultDeps();
|
|
24
|
+
if (opts.home)
|
|
25
|
+
depsM.home = opts.home;
|
|
26
|
+
if (opts.localAppData)
|
|
27
|
+
depsM.localAppData = opts.localAppData;
|
|
28
|
+
externalAgentMigrations = await migrateAllExternalAgents(depsM);
|
|
24
29
|
for (const line of formatMigrationNotices(externalAgentMigrations)) {
|
|
25
30
|
console.log(`[bootstrap] ${line}`);
|
|
26
31
|
}
|
package/dist/cli/ink-app.js
CHANGED
|
@@ -30,14 +30,16 @@ const Messages = ({ msgs }) => (_jsx(Box, { flexDirection: "column", flexGrow: 1
|
|
|
30
30
|
}) }));
|
|
31
31
|
const MentionPopup = ({ title, items, sel, width, loading }) => {
|
|
32
32
|
const MAX_ROWS = 8;
|
|
33
|
-
|
|
33
|
+
// 2026-08-08: 滑动窗口 — 选中项始终可见 (原实现 fix 屏幕顶部, sel 超窗口时无高亮行)
|
|
34
|
+
const offset = Math.max(0, Math.min(sel - Math.floor(MAX_ROWS / 2), Math.max(0, items.length - MAX_ROWS)));
|
|
35
|
+
const shown = items.slice(offset, offset + MAX_ROWS);
|
|
34
36
|
const innerW = Math.max(width - 2, 10);
|
|
35
37
|
return (_jsxs(Box, { flexDirection: "column", width: width, children: [_jsx(Text, { color: "cyan", bold: true, children: `╭─ ${title} ${'─'.repeat(Math.max(2, innerW - dispWidth(title) - 4))}╮` }), loading && items.length === 0 ? (_jsx(Text, { color: "dim", children: "\u2502 \u626B\u63CF\u4E2D..." })) : !loading && items.length === 0 ? (_jsx(Text, { color: "dim", children: "\u2502 \u65E0\u5339\u914D" })) : (shown.map((it, i) => {
|
|
36
38
|
const active = i === sel;
|
|
37
39
|
const label = it.kind === 'file' ? it.label : `${it.kind === 'skill' ? '⚡' : it.kind === 'plugin' ? '🔌' : ''}${it.label}`;
|
|
38
40
|
const hint = it.hint ? `${it.hint}` : it.kind === 'file' ? '文件' : '';
|
|
39
41
|
return (_jsxs(Box, { width: innerW, children: [_jsx(Text, { color: active ? 'black' : undefined, backgroundColor: active ? 'cyan' : undefined, children: `${active ? '❯ ' : ' '}${label}` }), _jsx(Text, { color: active ? 'black' : 'dim', backgroundColor: active ? 'cyan' : undefined, dimColor: !active, children: ` ${hint}` })] }, `${it.kind}:${it.label}`));
|
|
40
|
-
})), items.length > MAX_ROWS && (_jsxs(Text, { color: "dim", children: ["\u2502 \u8FD8\u6709 ", items.length -
|
|
42
|
+
})), items.length > MAX_ROWS && (_jsxs(Text, { color: "dim", children: ["\u2502 ", offset + 1, "-", offset + shown.length, "/", items.length, " \u00B7 \u8FD8\u6709 ", items.length - (offset + shown.length), " \u9879..."] })), _jsx(Text, { color: "cyan", children: `╰${'─'.repeat(innerW)}╯` })] }));
|
|
41
43
|
};
|
|
42
44
|
const InkApp = ({ onPrompt, initialStatus, getStatusUpdate, terminalW, terminalH }) => {
|
|
43
45
|
const [input, setInput] = useState('');
|
package/dist/cli/mention-data.js
CHANGED
|
@@ -23,7 +23,7 @@ const CLI_COMMANDS = [
|
|
|
23
23
|
{ kind: 'command', label: 'add_friend', hint: '添加好友 <64位hex公钥>', insert: 'add_friend' },
|
|
24
24
|
// 2026-08-06: 系统命令组
|
|
25
25
|
{ kind: 'command', label: 'model', hint: '模型供应商选择器 (↑↓ 选择)', insert: 'model' },
|
|
26
|
-
{ kind: 'command', label: 'login', hint: '
|
|
26
|
+
{ kind: 'command', label: 'login', hint: '登录 GitHub + Google 账号 (骨架)', insert: 'login' },
|
|
27
27
|
{ kind: 'command', label: 'logout', hint: '查看当前供应商', insert: 'logout' },
|
|
28
28
|
{ kind: 'command', label: 'now', hint: '当前状态总览', insert: 'now' },
|
|
29
29
|
{ kind: 'command', label: 'session', hint: '当前会话信息', insert: 'session' },
|
|
@@ -44,14 +44,15 @@ const CLI_COMMANDS = [
|
|
|
44
44
|
{ kind: 'command', label: 'insight', hint: '洞察 (08-Insights)', insert: 'insight' },
|
|
45
45
|
{ kind: 'command', label: 'wiki', hint: 'wiki 状态', insert: 'wiki' },
|
|
46
46
|
{ kind: 'command', label: 'dream', hint: '随机灵感', insert: 'dream' },
|
|
47
|
+
{ kind: 'command', label: 'new agent', hint: '创建新智能体 channel', insert: 'new agent' },
|
|
48
|
+
{ kind: 'command', label: 'new session', hint: '创建新会话', insert: 'new session' },
|
|
49
|
+
{ kind: 'command', label: 'plan', hint: '循环计划 (创建/查看)', insert: 'plan' },
|
|
50
|
+
{ kind: 'command', label: 'todo', hint: '勾选步骤 (循环过程)', insert: 'todo' },
|
|
47
51
|
];
|
|
48
52
|
/** Web 端斜杠命令 (server /message 路由 → LLM 工具) */
|
|
49
53
|
const WEB_COMMANDS = [
|
|
50
|
-
{ kind: 'command', label: 'plan', hint: '创建计划', insert: 'plan' },
|
|
51
|
-
{ kind: 'command', label: 'todo', hint: '勾选步骤', insert: 'todo' },
|
|
52
54
|
{ kind: 'command', label: 'review', hint: '审查计划', insert: 'review' },
|
|
53
55
|
{ kind: 'command', label: 'task', hint: '创建任务', insert: 'task' },
|
|
54
|
-
{ kind: 'command', label: 'goal', hint: '暂停目标', insert: 'goal' },
|
|
55
56
|
{ kind: 'command', label: 'skill', hint: '沉淀技能', insert: 'skill' },
|
|
56
57
|
{ kind: 'command', label: 'add-friend', hint: '添加好友 (智能体工具)', insert: 'add-friend' },
|
|
57
58
|
];
|
package/dist/index.js
CHANGED
|
@@ -586,6 +586,98 @@ async function processInput(input, comm) {
|
|
|
586
586
|
}
|
|
587
587
|
return;
|
|
588
588
|
}
|
|
589
|
+
// /new agent <名字> — 创建新智能体 channel (2026-08-08)
|
|
590
|
+
if (trimmed.toLowerCase().startsWith('/new agent')) {
|
|
591
|
+
const q = trimmed.slice('/new agent'.length).trim();
|
|
592
|
+
if (!q) {
|
|
593
|
+
appendLine(`${C_DIM}用法: /new agent <名字> — 新建一个智能体 channel 并切换过去${RESET}`);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
try {
|
|
597
|
+
const [name, ...rest] = q.split(/\s+/);
|
|
598
|
+
const personaHint = rest.join(' ').trim();
|
|
599
|
+
const { getIdentityStore } = await import('./agents/agent-identity-store.js');
|
|
600
|
+
const store = getIdentityStore();
|
|
601
|
+
await store.load();
|
|
602
|
+
const { readFile, writeFile, mkdir } = await import('fs/promises');
|
|
603
|
+
const { join } = await import('path');
|
|
604
|
+
const home = process.env.HOME || '/tmp';
|
|
605
|
+
const channelsPath = join(home, '.bolloon', 'sessions', 'channels.json');
|
|
606
|
+
let channels = [];
|
|
607
|
+
try {
|
|
608
|
+
const parsed = JSON.parse(await readFile(channelsPath, 'utf-8'));
|
|
609
|
+
channels = Array.isArray(parsed) ? parsed : parsed?.channels || [];
|
|
610
|
+
}
|
|
611
|
+
catch { /* 首次无文件 */ }
|
|
612
|
+
const dupName = channels.find((c) => c.name === name.trim());
|
|
613
|
+
if (dupName) {
|
|
614
|
+
appendLine(`${C_ERROR}同名智能体已存在: '${dupName.name}' (id=${dupName.id})${RESET}`);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
const id = `ch_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
|
|
618
|
+
const ch = {
|
|
619
|
+
id,
|
|
620
|
+
name: name.trim(),
|
|
621
|
+
agentId: `agent-${name.trim().toLowerCase().replace(/\s+/g, '-')}`,
|
|
622
|
+
createdAt: new Date().toISOString(),
|
|
623
|
+
updatedAt: new Date().toISOString(),
|
|
624
|
+
currentSessionId: 'default',
|
|
625
|
+
};
|
|
626
|
+
if (personaHint)
|
|
627
|
+
ch.persona = { name: name.trim(), description: personaHint };
|
|
628
|
+
channels.push(ch);
|
|
629
|
+
await mkdir(join(home, '.bolloon', 'sessions'), { recursive: true });
|
|
630
|
+
await writeFile(channelsPath, JSON.stringify(channels, null, 2), 'utf-8');
|
|
631
|
+
await store.setActive(id);
|
|
632
|
+
cliAgentName = name.trim();
|
|
633
|
+
cliActiveChannelId = id;
|
|
634
|
+
inkSetStatus(getStatus());
|
|
635
|
+
appendLine(`${C_OK}✓ 已创建智能体 channel: ${name.trim()}${RESET} (${C_DIM}${id}${RESET})${personaHint ? `\n ${C_DIM}persona: ${personaHint}${RESET}` : ''}`);
|
|
636
|
+
}
|
|
637
|
+
catch (e) {
|
|
638
|
+
appendLine(`${C_ERROR}/new agent 失败: ${String(e.message || e).slice(0, 200)}${RESET}`);
|
|
639
|
+
}
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
// /new session — 当前 channel 开新会话 (2026-08-08)
|
|
643
|
+
if (trimmed.toLowerCase() === '/new session') {
|
|
644
|
+
try {
|
|
645
|
+
const { readFile, writeFile } = await import('fs/promises');
|
|
646
|
+
const { join } = await import('path');
|
|
647
|
+
const home = process.env.HOME || '/tmp';
|
|
648
|
+
const channelsPath = join(home, '.bolloon', 'sessions', 'channels.json');
|
|
649
|
+
const newSessionId = `sess_${Date.now()}`;
|
|
650
|
+
let saved = false;
|
|
651
|
+
try {
|
|
652
|
+
const parsed = JSON.parse(await readFile(channelsPath, 'utf-8'));
|
|
653
|
+
const channels = Array.isArray(parsed) ? parsed : parsed?.channels || [];
|
|
654
|
+
for (const c of channels) {
|
|
655
|
+
if (cliActiveChannelId && c.id === cliActiveChannelId) {
|
|
656
|
+
c.currentSessionId = newSessionId;
|
|
657
|
+
saved = true;
|
|
658
|
+
}
|
|
659
|
+
else if (!cliActiveChannelId && c.id === channels[0]?.id) {
|
|
660
|
+
c.currentSessionId = newSessionId;
|
|
661
|
+
saved = true;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
await writeFile(channelsPath, JSON.stringify(Array.isArray(parsed) ? channels : { ...parsed, channels }, null, 2), 'utf-8');
|
|
665
|
+
}
|
|
666
|
+
catch { /* 无 channels.json → 仅提示 */ }
|
|
667
|
+
// 重置 agent 消息历史 (新会话空窗口)
|
|
668
|
+
try {
|
|
669
|
+
const a = await getAgent();
|
|
670
|
+
if (a && a.messageHistory)
|
|
671
|
+
a.messageHistory = [];
|
|
672
|
+
}
|
|
673
|
+
catch { /* 非致命 */ }
|
|
674
|
+
appendLine(`${C_OK}✓ 已新建会话${RESET} session=${C_DIM}${newSessionId}${RESET}${saved ? ` (channel: ${cliActiveChannelId || 'default'})` : ''}`);
|
|
675
|
+
}
|
|
676
|
+
catch (e) {
|
|
677
|
+
appendLine(`${C_ERROR}/new session 失败: ${String(e.message || e).slice(0, 200)}${RESET}`);
|
|
678
|
+
}
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
589
681
|
// /queue — 切换队列模式
|
|
590
682
|
if (trimmed.toLowerCase() === '/queue') {
|
|
591
683
|
queueMode = !queueMode;
|
|
@@ -615,8 +707,8 @@ async function processInput(input, comm) {
|
|
|
615
707
|
}
|
|
616
708
|
// ==================== 2026-08-06: 系统命令组 (/model /now /ipfs /memory ...) ====================
|
|
617
709
|
const cmd = trimmed.toLowerCase();
|
|
618
|
-
// /model
|
|
619
|
-
if (cmd === '/model'
|
|
710
|
+
// /model — 模型供应商选择器 (ink 交互渲染, 复用 MentionPopup)
|
|
711
|
+
if (cmd === '/model') {
|
|
620
712
|
try {
|
|
621
713
|
const { llmConfigStore, PROVIDER_INFO } = await import('./llm/config-store.js');
|
|
622
714
|
await llmConfigStore.initialize();
|
|
@@ -644,6 +736,52 @@ async function processInput(input, comm) {
|
|
|
644
736
|
}
|
|
645
737
|
return;
|
|
646
738
|
}
|
|
739
|
+
// /login — GitHub / Google 账号登录骨架 (2026-08-08, 无真实 OAuth, 先做选择 + 记录)
|
|
740
|
+
if (cmd === '/login') {
|
|
741
|
+
try {
|
|
742
|
+
const { readFile, writeFile, mkdir } = await import('fs/promises');
|
|
743
|
+
const { join } = await import('path');
|
|
744
|
+
const home = process.env.HOME || '/tmp';
|
|
745
|
+
const accPath = join(home, '.bolloon', 'accounts.json');
|
|
746
|
+
let accs = [];
|
|
747
|
+
try {
|
|
748
|
+
const parsed = JSON.parse(await readFile(accPath, 'utf-8'));
|
|
749
|
+
accs = Array.isArray(parsed) ? parsed : [];
|
|
750
|
+
}
|
|
751
|
+
catch { /* 无 */ }
|
|
752
|
+
const gh = accs.filter((a) => a.provider === 'github');
|
|
753
|
+
const gg = accs.filter((a) => a.provider === 'google');
|
|
754
|
+
const items = [
|
|
755
|
+
{ kind: 'command', label: 'GitHub', hint: gh.length ? `已登录 ${gh.length} 个账号` : '未登录', insert: 'GitHub' },
|
|
756
|
+
{ kind: 'command', label: 'Google', hint: gg.length ? `已登录 ${gg.length} 个账号` : '未登录', insert: 'Google' },
|
|
757
|
+
];
|
|
758
|
+
globalThis.__inkOpenPicker?.(items, '登录账号 (骨架) · 选择服务 · Esc 取消', async (it) => {
|
|
759
|
+
const provider = it.label.toLowerCase();
|
|
760
|
+
try {
|
|
761
|
+
const existing = accs.find((a) => a.provider === provider);
|
|
762
|
+
if (existing) {
|
|
763
|
+
appendLine(`${C_OK}✓ ${it.label}: 已登录${RESET} 账号=${existing.username || existing.email || '?'} (${existing.loggedAt || ''})`);
|
|
764
|
+
appendLine(` ${C_DIM}token: ${existing.token ? '已保存' : '无'} (未做真实 OAuth, 仅骨架)${RESET}`);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
// 骨架: 记录一个占位账号 (真实 OAuth 后续接入, 在此扩展)
|
|
768
|
+
const entry = { provider, username: `user-${provider}`, email: '', token: '', loggedAt: new Date().toISOString(), skeleton: true };
|
|
769
|
+
accs.push(entry);
|
|
770
|
+
await mkdir(join(home, '.bolloon'), { recursive: true });
|
|
771
|
+
await writeFile(accPath, JSON.stringify(accs, null, 2), 'utf-8');
|
|
772
|
+
appendLine(`${C_OK}✓ ${it.label} 登录骨架已记录 (未做真实 OAuth)${RESET}`);
|
|
773
|
+
appendLine(` ${C_DIM}后续接入: 这里会打开浏览器授权并交换 token${RESET}`);
|
|
774
|
+
}
|
|
775
|
+
catch (e) {
|
|
776
|
+
appendLine(`${C_ERROR}✗ /login 失败: ${String(e.message || e).slice(0, 150)}${RESET}`);
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
catch (e) {
|
|
781
|
+
appendLine(`${C_ERROR}/login 失败: ${String(e.message || e).slice(0, 150)}${RESET}`);
|
|
782
|
+
}
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
647
785
|
// /logout — 显示当前供应商 (减法: 登出 = 查看当前, 切换走 /model)
|
|
648
786
|
if (cmd === '/logout') {
|
|
649
787
|
try {
|
|
@@ -673,17 +811,22 @@ async function processInput(input, comm) {
|
|
|
673
811
|
catch { /* 静默 */ }
|
|
674
812
|
return;
|
|
675
813
|
}
|
|
676
|
-
// /tools — 可用工具列表
|
|
814
|
+
// /tools — 可用工具列表 (2026-08-08: 读 getToolList, 显示名 + 参数)
|
|
677
815
|
if (cmd === '/tools') {
|
|
678
816
|
try {
|
|
679
817
|
const a = await getAgent();
|
|
680
|
-
const
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
818
|
+
const list = a.getToolList?.() ?? [];
|
|
819
|
+
appendLine(`${C_ACCENT}可用工具 (${list.length}):${RESET}`);
|
|
820
|
+
if (list.length === 0) {
|
|
821
|
+
appendLine(` ${C_DIM}无 (agent 未初始化工具列表)${RESET}`);
|
|
822
|
+
}
|
|
823
|
+
for (const t of list.slice(0, 40)) {
|
|
824
|
+
const params = Array.isArray(t.parameters) && t.parameters.length > 0 ? `(${t.parameters.join(',')})` : '';
|
|
825
|
+
const desc = t.description ? ` ${C_DIM}${String(t.description).split('\n')[0].slice(0, 40)}${RESET}` : '';
|
|
826
|
+
appendLine(` ${C_DIM}·${RESET} ${t.name}${params}${desc}`);
|
|
827
|
+
}
|
|
828
|
+
if (list.length > 40)
|
|
829
|
+
appendLine(` ${C_DIM}... 共 ${list.length} 个${RESET}`);
|
|
687
830
|
}
|
|
688
831
|
catch { /* 静默 */ }
|
|
689
832
|
return;
|
|
@@ -754,14 +897,14 @@ async function processInput(input, comm) {
|
|
|
754
897
|
catch { /* 静默 */ }
|
|
755
898
|
return;
|
|
756
899
|
}
|
|
757
|
-
// /goal —
|
|
900
|
+
// /goal — 进行中的目标/计划; /goal <文本> 设定新目标并触发循环 (2026-08-08)
|
|
758
901
|
if (cmd === '/goal') {
|
|
759
902
|
try {
|
|
760
|
-
const { listActivePlans
|
|
903
|
+
const { listActivePlans } = await import('./agents/plan-store.js');
|
|
761
904
|
const plans = await listActivePlans();
|
|
762
905
|
appendLine(`${C_ACCENT}目标 (${plans.length} 个进行中):${RESET}`);
|
|
763
906
|
if (plans.length === 0) {
|
|
764
|
-
appendLine(` ${C_DIM}无进行中计划 — 可用 /plan 创建${RESET}`);
|
|
907
|
+
appendLine(` ${C_DIM}无进行中计划 — 可用 /goal <目标> 设定 或 /plan 创建${RESET}`);
|
|
765
908
|
}
|
|
766
909
|
for (const p of plans.slice(0, 5)) {
|
|
767
910
|
appendLine(` ${C_ACCENT}●${RESET} ${p.goal || p.planId} ${C_DIM}[${p.status || 'active'}]${RESET}`);
|
|
@@ -774,6 +917,102 @@ async function processInput(input, comm) {
|
|
|
774
917
|
catch { /* 静默 */ }
|
|
775
918
|
return;
|
|
776
919
|
}
|
|
920
|
+
if (cmd.startsWith('/goal ')) {
|
|
921
|
+
const q = trimmed.slice('/goal '.length).trim();
|
|
922
|
+
try {
|
|
923
|
+
const { createPlan } = await import('./agents/plan-store.js');
|
|
924
|
+
const r = await createPlan({ goal: q, steps: [q], createdBy: 'user', originChannel: cliActiveChannelId || 'cli' });
|
|
925
|
+
if (!r.ok || !r.plan) {
|
|
926
|
+
appendLine(`${C_ERROR}/goal 设定失败: ${r.error || '未知'}${RESET}`);
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
appendLine(`${C_OK}✓ 目标已设定: ${C_ACCENT}${q}${RESET} (${C_DIM}plan ${r.plan.planId}${RESET})`);
|
|
930
|
+
// 触发自我改进循环 (沙箱分支, 输出供用户审)
|
|
931
|
+
const { runSelfImproveLoop } = await import('./agents/pi-sdk-session-factory.js');
|
|
932
|
+
const loop = await runSelfImproveLoop(q).catch(() => ({ success: false, error: '未启动' }));
|
|
933
|
+
if (loop.success)
|
|
934
|
+
appendLine(` ${C_DIM}${loop.output}${RESET}`);
|
|
935
|
+
else
|
|
936
|
+
appendLine(` ${C_WARN}⚠ 循环未启动: ${loop.error}${RESET}`);
|
|
937
|
+
}
|
|
938
|
+
catch (e) {
|
|
939
|
+
appendLine(`${C_ERROR}/goal 失败: ${String(e.message || e).slice(0, 200)}${RESET}`);
|
|
940
|
+
}
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
// /plan — 循环过程工具: 创建/查看计划 (2026-08-08)
|
|
944
|
+
// /plan <目标> :: <步骤1> | <步骤2> ... 创建
|
|
945
|
+
// /plan 查看进行中
|
|
946
|
+
if (cmd.startsWith('/plan ')) {
|
|
947
|
+
const q = trimmed.slice('/plan '.length).trim();
|
|
948
|
+
const [goalText, ...rest] = q.split('::');
|
|
949
|
+
const stepsFlat = rest.length > 0 ? rest[0] : '';
|
|
950
|
+
const steps = stepsFlat ? stepsFlat.split(/\s*[||]\s*/).map(s => s.trim()).filter(Boolean) : [goalText].filter(Boolean);
|
|
951
|
+
try {
|
|
952
|
+
const { createPlan } = await import('./agents/plan-store.js');
|
|
953
|
+
const r = await createPlan({ goal: goalText || q, steps: steps.length ? steps : [goalText], createdBy: 'user', originChannel: cliActiveChannelId || 'cli' });
|
|
954
|
+
if (!r.ok || !r.plan) {
|
|
955
|
+
appendLine(`${C_ERROR}/plan 创建失败: ${r.error || '未知'}${RESET}`);
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
appendLine(`${C_OK}✓ 计划已创建: ${C_ACCENT}${r.plan.goal}${RESET} (${C_DIM}${r.plan.planId} · ${r.plan.steps.length} 步${RESET})`);
|
|
959
|
+
for (const s of r.plan.steps)
|
|
960
|
+
appendLine(` ${C_DIM}· ${s.description}${RESET}`);
|
|
961
|
+
}
|
|
962
|
+
catch (e) {
|
|
963
|
+
appendLine(`${C_ERROR}/plan 失败: ${String(e.message || e).slice(0, 200)}${RESET}`);
|
|
964
|
+
}
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
// /todo — 循环过程工具: 查看/勾选步骤 (2026-08-08)
|
|
968
|
+
if (cmd === '/todo') {
|
|
969
|
+
try {
|
|
970
|
+
const { listActivePlans } = await import('./agents/plan-store.js');
|
|
971
|
+
const plans = await listActivePlans();
|
|
972
|
+
if (plans.length === 0) {
|
|
973
|
+
appendLine(`${C_DIM}无进行中计划 — 可用 /plan <目标> 创建${RESET}`);
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
for (const p of plans.slice(0, 3)) {
|
|
977
|
+
appendLine(`${C_ACCENT}● ${p.goal}${RESET} ${C_DIM}[${p.status || 'active'}]${RESET}`);
|
|
978
|
+
const steps = Array.isArray(p.steps) ? p.steps : [];
|
|
979
|
+
for (let i = 0; i < steps.length; i++) {
|
|
980
|
+
const s = steps[i];
|
|
981
|
+
const done = s.status === 'done' || s.done;
|
|
982
|
+
appendLine(` ${done ? '✓' : '○'} ${i + 1}. ${s.description || ''}${done ? '' : ` ${C_DIM}/todo ${p.planId} ${i + 1} 勾选${RESET}`}`);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
catch { /* 静默 */ }
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
if (/^\/todo\s+\S+\s+\d+/.test(trimmed.toLowerCase())) {
|
|
990
|
+
const parts = trimmed.split(/\s+/);
|
|
991
|
+
const planId = parts[1];
|
|
992
|
+
const idx = parseInt(parts.slice(2).join(' ').trim(), 10) - 1 || 0;
|
|
993
|
+
try {
|
|
994
|
+
const { loadPlan, updatePlan } = await import('./agents/plan-store.js');
|
|
995
|
+
const plan = await loadPlan(planId);
|
|
996
|
+
if (!plan) {
|
|
997
|
+
appendLine(`${C_ERROR}/todo 失败: plan '${planId}' 不存在${RESET}`);
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
const step = plan.steps[idx];
|
|
1001
|
+
if (!step) {
|
|
1002
|
+
appendLine(`${C_ERROR}/todo 失败: 无第 ${idx + 1} 步${RESET}`);
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
await updatePlan(planId, { stepId: step.id, status: 'done' });
|
|
1006
|
+
const done = plan.steps.filter(s => s.status === 'done' || s.done).length + 1;
|
|
1007
|
+
const total = plan.steps.length;
|
|
1008
|
+
const allDone = done >= total;
|
|
1009
|
+
appendLine(`${C_OK}✓ 勾选 ${idx + 1}. ${step.description}${RESET} (${done}/${total})${allDone ? `\n ${C_ACCENT}🎯 循环达到完成标准, 可以 review 结束: /review ${planId}${RESET}` : ''}`);
|
|
1010
|
+
}
|
|
1011
|
+
catch (e) {
|
|
1012
|
+
appendLine(`${C_ERROR}/todo 失败: ${String(e.message || e).slice(0, 150)}${RESET}`);
|
|
1013
|
+
}
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
777
1016
|
// /skill — 技能候选 (skill-writer 落盘)
|
|
778
1017
|
if (cmd === '/skill') {
|
|
779
1018
|
try {
|
|
@@ -899,14 +1138,15 @@ async function processInput(input, comm) {
|
|
|
899
1138
|
catch { /* 静默 */ }
|
|
900
1139
|
return;
|
|
901
1140
|
}
|
|
902
|
-
// /email —
|
|
1141
|
+
// /email — 邮件配置管理; /email <host:port:user:from> 设置 / /email clear 清除 (2026-08-08)
|
|
903
1142
|
if (cmd === '/email') {
|
|
904
1143
|
try {
|
|
905
|
-
const { readFile } = await import('fs/promises');
|
|
1144
|
+
const { readFile, writeFile, mkdir } = await import('fs/promises');
|
|
906
1145
|
const { join } = await import('path');
|
|
1146
|
+
const p = join(process.env.HOME || '/tmp', '.bolloon', 'smtp.json');
|
|
907
1147
|
let cfg = null;
|
|
908
1148
|
try {
|
|
909
|
-
cfg = JSON.parse(await readFile(
|
|
1149
|
+
cfg = JSON.parse(await readFile(p, 'utf-8'));
|
|
910
1150
|
}
|
|
911
1151
|
catch { /* 无 */ }
|
|
912
1152
|
appendLine(`${C_ACCENT}邮件 (SMTP):${RESET}`);
|
|
@@ -917,11 +1157,59 @@ async function processInput(input, comm) {
|
|
|
917
1157
|
appendLine(` ${C_DIM}host:${RESET} ${cfg.host || 'smtp.qq.com'}`);
|
|
918
1158
|
appendLine(` ${C_DIM}发件人:${RESET} ${cfg.from || cfg.user || '—'}`);
|
|
919
1159
|
}
|
|
1160
|
+
appendLine(` ${C_DIM}用法: /email <host:port:user:from> 设置 · /email clear 清除 · /email pass <授权码> 设密码${RESET}`);
|
|
920
1161
|
}
|
|
921
1162
|
catch { /* 静默 */ }
|
|
922
1163
|
return;
|
|
923
1164
|
}
|
|
924
|
-
|
|
1165
|
+
if (cmd === '/email clear') {
|
|
1166
|
+
try {
|
|
1167
|
+
const { writeFile, mkdir } = await import('fs/promises');
|
|
1168
|
+
const { join } = await import('path');
|
|
1169
|
+
const p = join(process.env.HOME || '/tmp', '.bolloon', 'smtp.json');
|
|
1170
|
+
await mkdir(join(process.env.HOME || '/tmp', '.bolloon'), { recursive: true });
|
|
1171
|
+
await writeFile(p, '{}', 'utf-8');
|
|
1172
|
+
appendLine(`${C_OK}✓ smtp 配置已清除${RESET}`);
|
|
1173
|
+
}
|
|
1174
|
+
catch {
|
|
1175
|
+
appendLine(`${C_ERROR}/email clear 失败${RESET}`);
|
|
1176
|
+
}
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
if (cmd.startsWith('/email ')) {
|
|
1180
|
+
const q = trimmed.slice('/email '.length).trim();
|
|
1181
|
+
try {
|
|
1182
|
+
const { writeFile, mkdir } = await import('fs/promises');
|
|
1183
|
+
const { join } = await import('path');
|
|
1184
|
+
const p = join(process.env.HOME || '/tmp', '.bolloon', 'smtp.json');
|
|
1185
|
+
await mkdir(join(process.env.HOME || '/tmp', '.bolloon'), { recursive: true });
|
|
1186
|
+
let cfg = {};
|
|
1187
|
+
try {
|
|
1188
|
+
cfg = JSON.parse(await import('fs/promises').then(m => m.readFile(p, 'utf-8')));
|
|
1189
|
+
}
|
|
1190
|
+
catch { /* 无 */ }
|
|
1191
|
+
if (cmd.startsWith('/email pass')) {
|
|
1192
|
+
cfg.pass = q;
|
|
1193
|
+
await writeFile(p, JSON.stringify(cfg, null, 2), 'utf-8');
|
|
1194
|
+
appendLine(`${C_OK}✓ SMTP 授权码已保存${RESET}`);
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
const parts = q.split(':');
|
|
1198
|
+
if (parts.length >= 3) {
|
|
1199
|
+
cfg = { host: parts[0], port: parseInt(parts[1], 10) || 465, user: parts[2], from: parts[3] || parts[2], pass: cfg.pass };
|
|
1200
|
+
await writeFile(p, JSON.stringify(cfg, null, 2), 'utf-8');
|
|
1201
|
+
appendLine(`${C_OK}✓ SMTP 已设置: ${cfg.host}:${cfg.port} (发件人 ${cfg.from})${RESET}`);
|
|
1202
|
+
}
|
|
1203
|
+
else {
|
|
1204
|
+
appendLine(`${C_DIM}格式不对: /email <host:port:user:from> 或 /email clear 或 /email pass <授权码>${RESET}`);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
catch (e) {
|
|
1208
|
+
appendLine(`${C_ERROR}/email 设置失败: ${String(e.message || e).slice(0, 150)}${RESET}`);
|
|
1209
|
+
}
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
// /loop — 当前循环状态; /loop <目标> <完成标准> 设目标+标准并启动循环 (2026-08-08)
|
|
925
1213
|
if (cmd === '/loop') {
|
|
926
1214
|
try {
|
|
927
1215
|
const a = await getAgent();
|
|
@@ -937,10 +1225,42 @@ async function processInput(input, comm) {
|
|
|
937
1225
|
appendLine(`${C_ACCENT}Loop 状态:${RESET}`);
|
|
938
1226
|
appendLine(` ${C_DIM}消息:${RESET} ${h.length} 条 (窗口 15, ${Math.max(0, h.length - 15)} 条早期压缩)`);
|
|
939
1227
|
appendLine(` ${C_DIM}token:${RESET} ${(tokens / 1000).toFixed(1)}k / 1M (${((tokens / 1_000_000) * 100).toFixed(2)}%)`);
|
|
1228
|
+
appendLine(` ${C_DIM}用法: /loop <目标> (| <完成标准>) — 设目标并循环, 达到标准自动结束${RESET}`);
|
|
940
1229
|
}
|
|
941
1230
|
catch { /* 静默 */ }
|
|
942
1231
|
return;
|
|
943
1232
|
}
|
|
1233
|
+
if (cmd.startsWith('/loop ')) {
|
|
1234
|
+
const q = trimmed.slice('/loop '.length).trim();
|
|
1235
|
+
const [goalText, criteriaText] = q.split(/\s*[||]\s*/).map(s => s.trim());
|
|
1236
|
+
try {
|
|
1237
|
+
const { createPlan } = await import('./agents/plan-store.js');
|
|
1238
|
+
const criterion = criteriaText ? `达到标准: ${criteriaText}` : '';
|
|
1239
|
+
const r = await createPlan({
|
|
1240
|
+
goal: goalText || q,
|
|
1241
|
+
steps: [goalText || q, criterion].filter(Boolean),
|
|
1242
|
+
createdBy: 'user',
|
|
1243
|
+
originChannel: cliActiveChannelId || 'cli',
|
|
1244
|
+
});
|
|
1245
|
+
if (!r.ok || !r.plan) {
|
|
1246
|
+
appendLine(`${C_ERROR}/loop 启动失败: ${r.error || '未知'}${RESET}`);
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
appendLine(`${C_OK}✓ 循环已启动: ${C_ACCENT}${goalText}${RESET}${criteriaText ? `\n ${C_DIM}完成标准: ${criteriaText}${RESET}` : ''} (${C_DIM}plan ${r.plan.planId}${RESET})`);
|
|
1250
|
+
appendLine(` ${C_DIM}当标准达成时用 /todo 勾选最后一步 / 或 review 后自动结束循环${RESET}`);
|
|
1251
|
+
// 启动自我改进循环
|
|
1252
|
+
const { runSelfImproveLoop } = await import('./agents/pi-sdk-session-factory.js');
|
|
1253
|
+
const loop = await runSelfImproveLoop(goalText).catch(() => ({ success: false, error: '未启动' }));
|
|
1254
|
+
if (loop.success)
|
|
1255
|
+
appendLine(` ${C_DIM}${loop.output}${RESET}`);
|
|
1256
|
+
else
|
|
1257
|
+
appendLine(` ${C_WARN}⚠ 循环未启动: ${loop.error}${RESET}`);
|
|
1258
|
+
}
|
|
1259
|
+
catch (e) {
|
|
1260
|
+
appendLine(`${C_ERROR}/loop 失败: ${String(e.message || e).slice(0, 200)}${RESET}`);
|
|
1261
|
+
}
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
944
1264
|
// /judgement — 判断力列表
|
|
945
1265
|
if (cmd === '/judgement' || cmd === '/judgments') {
|
|
946
1266
|
try {
|
|
@@ -992,7 +1312,7 @@ async function processInput(input, comm) {
|
|
|
992
1312
|
catch { /* 静默 */ }
|
|
993
1313
|
return;
|
|
994
1314
|
}
|
|
995
|
-
// /dream —
|
|
1315
|
+
// /dream — 随机灵感; /dream <主题> 把用户主题落盘到梦想文档并触发循环 (2026-08-08)
|
|
996
1316
|
if (cmd === '/dream') {
|
|
997
1317
|
try {
|
|
998
1318
|
const { readContextAssets, readAssetBody } = await import('./bootstrap/context-os.js');
|
|
@@ -1014,25 +1334,63 @@ async function processInput(input, comm) {
|
|
|
1014
1334
|
const pick = pool[Math.floor(Math.random() * pool.length)];
|
|
1015
1335
|
appendLine(`${C_DIM}🌙 ${pick}${RESET}`);
|
|
1016
1336
|
}
|
|
1337
|
+
appendLine(` ${C_DIM}用法: /dream <主题> — 把主题写入梦想文档并启动循环${RESET}`);
|
|
1017
1338
|
}
|
|
1018
1339
|
catch { /* 静默 */ }
|
|
1019
1340
|
return;
|
|
1020
1341
|
}
|
|
1342
|
+
if (cmd.startsWith('/dream ')) {
|
|
1343
|
+
const topic = trimmed.slice('/dream '.length).trim();
|
|
1344
|
+
try {
|
|
1345
|
+
const { writeFile, mkdir } = await import('fs/promises');
|
|
1346
|
+
const { join } = await import('path');
|
|
1347
|
+
const home = process.env.HOME || '/tmp';
|
|
1348
|
+
const dreamDir = join(home, '.bolloon', 'dreams');
|
|
1349
|
+
await mkdir(dreamDir, { recursive: true });
|
|
1350
|
+
// 梦想文档路径: 用户名 + 主题 → 文件名 (用户信息集成进路径)
|
|
1351
|
+
const userTag = (cliAgentName || 'user').toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
|
1352
|
+
const safeTopic = topic.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '-').slice(0, 40);
|
|
1353
|
+
const dreamPath = join(dreamDir, `${new Date().toISOString().slice(0, 10)}-${userTag}-${safeTopic}.md`);
|
|
1354
|
+
const doc = `# 🌙 Dream: ${topic}\n\ndate: ${new Date().toISOString()}\nuser: ${cliAgentName || 'user'}\nchannel: ${cliActiveChannelId || 'cli'}\n\n> 自动生成于 /dream, 触发循环去探索这个主题。\n`;
|
|
1355
|
+
await writeFile(dreamPath, doc, 'utf-8');
|
|
1356
|
+
appendLine(`${C_OK}✓ 梦想文档已写入: ${C_DIM}${dreamPath}${RESET}`);
|
|
1357
|
+
// 触发循环
|
|
1358
|
+
const { createPlan } = await import('./agents/plan-store.js');
|
|
1359
|
+
const r = await createPlan({ goal: `探索主题: ${topic}`, steps: [`研读 ${topic}`, '产出洞察'], createdBy: 'user', originChannel: cliActiveChannelId || 'cli' });
|
|
1360
|
+
if (r.ok)
|
|
1361
|
+
appendLine(` ${C_DIM}循环已关联 plan ${r.plan?.planId}${RESET}`);
|
|
1362
|
+
const { runSelfImproveLoop } = await import('./agents/pi-sdk-session-factory.js');
|
|
1363
|
+
const loop = await runSelfImproveLoop(`探索主题: ${topic}`).catch(() => ({ success: false, error: '未启动' }));
|
|
1364
|
+
if (loop.success)
|
|
1365
|
+
appendLine(` ${C_DIM}${loop.output}${RESET}`);
|
|
1366
|
+
else
|
|
1367
|
+
appendLine(` ${C_WARN}⚠ 循环未启动: ${loop.error}${RESET}`);
|
|
1368
|
+
}
|
|
1369
|
+
catch (e) {
|
|
1370
|
+
appendLine(`${C_ERROR}/dream 失败: ${String(e.message || e).slice(0, 200)}${RESET}`);
|
|
1371
|
+
}
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1021
1374
|
if (trimmed.toLowerCase() === '/help' || trimmed === 'help') {
|
|
1022
1375
|
appendLine(`${C_DIM}命令:${RESET}`);
|
|
1023
1376
|
appendLine(` ${C_ACCENT}!<cmd>${RESET} 执行终端命令 ${C_DIM}如 !ls -la${RESET}`);
|
|
1024
1377
|
appendLine(` ${C_ACCENT}/queue${RESET} 切换队列模式 ${C_DIM}输入排队, 当前结束后自动执行${RESET}`);
|
|
1025
1378
|
appendLine(` ${C_ACCENT}/dequeue${RESET} 出队一条`);
|
|
1026
1379
|
appendLine(` ${C_ACCENT}/channel [名字|id|序号]${RESET} 切换当前智能体 ${C_DIM}无参列出所有; 支持名字/ID/序号三种解析${RESET}`);
|
|
1027
|
-
appendLine(` ${C_ACCENT}/model${RESET}
|
|
1380
|
+
appendLine(` ${C_ACCENT}/model${RESET} 模型供应商选择器 ${C_DIM}↑↓ 选择 · Enter 确认 · Esc 取消${RESET}`);
|
|
1381
|
+
appendLine(` ${C_ACCENT}/login${RESET} 登录 GitHub/Google 账号 (骨架) ${C_DIM}暂无真实 OAuth${RESET}`);
|
|
1028
1382
|
appendLine(` ${C_ACCENT}/logout${RESET} 查看当前供应商`);
|
|
1383
|
+
appendLine(` ${C_ACCENT}/new agent${RESET} 创建新智能体 channel ${C_DIM}/new agent <名字>${RESET}`);
|
|
1384
|
+
appendLine(` ${C_ACCENT}/new session${RESET} 开新会话 ${C_DIM}清空当前 channel 消息窗口${RESET}`);
|
|
1029
1385
|
appendLine(` ${C_ACCENT}/now${RESET} 当前状态总览 ${C_DIM}智能体/运行时间/上下文 tokens/消息数${RESET}`);
|
|
1030
1386
|
appendLine(` ${C_ACCENT}/session${RESET} 当前会话信息 ${C_DIM}channel/agent/消息窗口${RESET}`);
|
|
1031
|
-
appendLine(` ${C_ACCENT}/loop${RESET}
|
|
1387
|
+
appendLine(` ${C_ACCENT}/loop${RESET} 循环状态/启动 ${C_DIM}/loop <目标> (| <完成标准>)${RESET}`);
|
|
1032
1388
|
appendLine(` ${C_ACCENT}/memory${RESET} 记忆摘要 ${C_DIM}memory-compressor 落盘摘要${RESET}`);
|
|
1033
1389
|
appendLine(` ${C_ACCENT}/resume${RESET} 恢复上下文 ${C_DIM}最近记忆 + 进行中计划${RESET}`);
|
|
1034
|
-
appendLine(` ${C_ACCENT}/goal${RESET}
|
|
1035
|
-
appendLine(` ${C_ACCENT}/
|
|
1390
|
+
appendLine(` ${C_ACCENT}/goal${RESET} 查看/设定目标 ${C_DIM}/goal 查看 · /goal <目标> 设定+循环${RESET}`);
|
|
1391
|
+
appendLine(` ${C_ACCENT}/plan${RESET} 创建计划 ${C_DIM}/plan <目标> :: <步骤1>|<步骤2>${RESET}`);
|
|
1392
|
+
appendLine(` ${C_ACCENT}/todo${RESET} 查看/勾选循环步骤 ${C_DIM}/todo <planId> <序号>${RESET}`);
|
|
1393
|
+
appendLine(` ${C_ACCENT}/tools${RESET} 可用工具列表 (名/参数/简介)`);
|
|
1036
1394
|
appendLine(` ${C_ACCENT}/skill${RESET} 技能候选 ${C_DIM}skill-writer 沉淀候选${RESET}`);
|
|
1037
1395
|
appendLine(` ${C_ACCENT}/mcp${RESET} MCP 服务器列表`);
|
|
1038
1396
|
appendLine(` ${C_ACCENT}/agent${RESET} 当前智能体身份`);
|
|
@@ -1040,11 +1398,11 @@ async function processInput(input, comm) {
|
|
|
1040
1398
|
appendLine(` ${C_ACCENT}/ipfs${RESET} Kubo 状态 ${C_DIM}节点/peers/pins${RESET}`);
|
|
1041
1399
|
appendLine(` ${C_ACCENT}/ipns${RESET} IPNS keys + resolve`);
|
|
1042
1400
|
appendLine(` ${C_ACCENT}/wallet${RESET} 钱包状态`);
|
|
1043
|
-
appendLine(` ${C_ACCENT}/email${RESET}
|
|
1401
|
+
appendLine(` ${C_ACCENT}/email${RESET} 邮件配置管理 ${C_DIM}/email 查看 · <host:port:user:from> 设置 · clear 清除${RESET}`);
|
|
1044
1402
|
appendLine(` ${C_ACCENT}/judgement${RESET} 判断力列表`);
|
|
1045
1403
|
appendLine(` ${C_ACCENT}/insight${RESET} Context OS 洞察 (08-Insights)`);
|
|
1046
1404
|
appendLine(` ${C_ACCENT}/wiki${RESET} wiki 状态`);
|
|
1047
|
-
appendLine(` ${C_ACCENT}/dream${RESET}
|
|
1405
|
+
appendLine(` ${C_ACCENT}/dream${RESET} 随机灵感 ${C_DIM}/dream <主题> 写入梦想文档并触发循环${RESET}`);
|
|
1048
1406
|
appendLine(` ${C_ACCENT}@名字${RESET} @ 命中智能体 ${C_DIM}弹出窗选择后发送给智能体${RESET}`);
|
|
1049
1407
|
appendLine(` ${C_ACCENT}/名字${RESET} / 命中命令/技能/插件 ${C_DIM}输入 / 自动弹出${RESET}`);
|
|
1050
1408
|
appendLine(` ${C_ACCENT}#路径${RESET} # 命中文件 ${C_DIM}输入 # 自动弹出文件列表${RESET}`);
|
|
@@ -9,16 +9,12 @@
|
|
|
9
9
|
* 这样 Bolloon 能直接加载同一套性格 / 记忆 / 技能, 无缝兼容.
|
|
10
10
|
* - 隐式处理: 启动时静默跑, 失败不影响主流程; 完成后通告给用户 (见 report).
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
* - workspace/SOUL.md
|
|
14
|
-
*
|
|
15
|
-
* -
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* - workspace/MEMORY.md → persona/<agent>/wiki.md
|
|
19
|
-
* - workspace/skills/<name>/SKILL.md → ~/.bolloon/skills/<name>/ (整目录复制)
|
|
20
|
-
* - workspace/memory/*.md → ~/.bolloon/memory/<agent>/sessions/<n>.summary.md
|
|
21
|
-
* - workspace/*.md (其它) → ~/.bolloon/context-os/04-Projects/<agent>-docs/
|
|
12
|
+
* 异构布局 (2026-08-08 v0.3.40):
|
|
13
|
+
* - OpenClaw 平铺在 ~/.openclaw/workspace/ (SOUL/IDENTITY/USER/AGENTS/TOOLS/MEMORY.md +
|
|
14
|
+
* skills/<name>/ + memory/*.md)
|
|
15
|
+
* - Hermes 根在 %LOCALAPPDATA%\hermes (Windows, 兜底 ~/.hermes), persona 分布在
|
|
16
|
+
* SOUL.md(根) + memories/{USER,MEMORY}.md, skills 是 skills/<分类>/<技能>/SKILL.md
|
|
17
|
+
* 两级嵌套 (235 个). 迁移时展平并以 <分类>-<技能> 命名避免重名冲突.
|
|
22
18
|
*
|
|
23
19
|
* 幂等: 每个源落一份 manifest (~/.bolloon/migration/<source>.json),
|
|
24
20
|
* 记录已迁移的文件 hash; 未变化则跳过, 已存在则覆盖源文档 (文档允许演进),
|
|
@@ -49,8 +45,13 @@ function realExists(p) {
|
|
|
49
45
|
return fs.access(p).then(() => true).catch(() => false);
|
|
50
46
|
}
|
|
51
47
|
export function defaultDeps() {
|
|
48
|
+
const home = os.homedir();
|
|
52
49
|
return {
|
|
53
|
-
home
|
|
50
|
+
home,
|
|
51
|
+
platform: os.platform(),
|
|
52
|
+
localAppData: (typeof process !== 'undefined' && process.env && (process.env.LOCALAPPDATA || process.env.ProgramData))
|
|
53
|
+
? (process.env.LOCALAPPDATA || process.env.ProgramData)
|
|
54
|
+
: path.join(home, 'AppData', 'Local'),
|
|
54
55
|
readFile: realReadFile,
|
|
55
56
|
readdir: realReaddir,
|
|
56
57
|
stat: realStat,
|
|
@@ -63,13 +64,47 @@ export function defaultDeps() {
|
|
|
63
64
|
// ============================================================
|
|
64
65
|
// 纯函数: 目录布局 + hash
|
|
65
66
|
// ============================================================
|
|
66
|
-
/**
|
|
67
|
+
/** 各源默认根目录: openclaw 在 home/.openclaw; hermes 兜底 home/.hermes (真实见 candidates) */
|
|
67
68
|
export function sourceRootPath(source, home) {
|
|
68
69
|
return source === 'openclaw'
|
|
69
70
|
? path.join(home, '.openclaw')
|
|
70
71
|
: path.join(home, '.hermes');
|
|
71
72
|
}
|
|
72
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* 各源全部候选根路径 (按优先级, 遍历时取第一个存在者).
|
|
75
|
+
*
|
|
76
|
+
* 覆盖三大平台的实际安装位置:
|
|
77
|
+
* OpenClaw: 主目录 ~/.openclaw (三平台一致), 兜底 ~/.config/openclaw
|
|
78
|
+
* Hermes :
|
|
79
|
+
* - win32 %LOCALAPPDATA%\hermes → ~/.hermes
|
|
80
|
+
* - darwin ~/Library/Application Support/hermes → ~/.hermes
|
|
81
|
+
* - linux ~/.local/share/hermes → ~/.config/hermes → ~/.hermes
|
|
82
|
+
*/
|
|
83
|
+
export function sourceRootCandidates(source, deps) {
|
|
84
|
+
const home = deps.home;
|
|
85
|
+
const platform = deps.platform || 'linux';
|
|
86
|
+
if (source === 'openclaw') {
|
|
87
|
+
return [
|
|
88
|
+
path.join(home, '.openclaw'),
|
|
89
|
+
path.join(home, '.config', 'openclaw'),
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
const candidates = [];
|
|
93
|
+
if (platform === 'win32') {
|
|
94
|
+
if (deps.localAppData)
|
|
95
|
+
candidates.push(path.join(deps.localAppData, 'hermes'));
|
|
96
|
+
}
|
|
97
|
+
else if (platform === 'darwin') {
|
|
98
|
+
candidates.push(path.join(home, 'Library', 'Application Support', 'hermes'));
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
candidates.push(path.join(home, '.local', 'share', 'hermes'));
|
|
102
|
+
candidates.push(path.join(home, '.config', 'hermes'));
|
|
103
|
+
}
|
|
104
|
+
candidates.push(path.join(home, '.hermes'));
|
|
105
|
+
return candidates;
|
|
106
|
+
}
|
|
107
|
+
/** workspace 路径 (openclaw 用 workspace/, hermes 平铺在根) */
|
|
73
108
|
export function workspacePath(source, sourceRoot) {
|
|
74
109
|
return source === 'openclaw'
|
|
75
110
|
? path.join(sourceRoot, 'workspace')
|
|
@@ -79,6 +114,36 @@ export function workspacePath(source, sourceRoot) {
|
|
|
79
114
|
export function sha1(content) {
|
|
80
115
|
return crypto.createHash('sha1').update(content).digest('hex');
|
|
81
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* 内容级脱敏: 抹掉明敏凭据, 防止迁移产物把真实的 Bearer token / API key /
|
|
119
|
+
* MT5 会话标识 / 长随机串 带进 Bolloon 落盘. 迁移只搬运"知识", 不搬运"秘密".
|
|
120
|
+
*
|
|
121
|
+
* 处理模式:
|
|
122
|
+
* - Authorization / Bearer <token>
|
|
123
|
+
* - token: / api_key: / access_token: / secret: 等 key 声明后的随机串
|
|
124
|
+
* - 形如 sk-... / ghp_... / AKIA... 的 platform token
|
|
125
|
+
* - 一行冒号后跟 20+ 位 base64/hex 随机串 ("MT5 data: D0E8...", "token: Ab...")
|
|
126
|
+
*
|
|
127
|
+
* 保留中文句子与结构说明, 只替换被判定为凭据的 token 片段.
|
|
128
|
+
*/
|
|
129
|
+
export function redactSecrets(text) {
|
|
130
|
+
const REDACTED = '***REDACTED***';
|
|
131
|
+
let out = text;
|
|
132
|
+
// 0. Bearer <JWT/base64> / Authorization: Bearer ... — 最优先, 连同包头一起抹
|
|
133
|
+
out = out.replace(/\bBearer\s+[A-Za-z0-9_./\-=+]{12,}/gi, REDACTED);
|
|
134
|
+
out = out.replace(/\bAuthorization\s*[:=]\s*(?:Bearer\s+)?[A-Za-z0-9_./\-=+]{12,}/gi, REDACTED);
|
|
135
|
+
// 1. explicit key=value 声明 (token/apiKey/access_token/key/secret/password)
|
|
136
|
+
out = out.replace(/\b(token|api[_-]?key|access[_-]?token|secret|password)\b\s*[:=]\s*["']?[A-Za-z0-9_./+\-]{8,}["']?/gi, (_m, k) => `${k}: ${REDACTED}`);
|
|
137
|
+
// 2. platform 前缀 token (sk- / sk-proj- / sk-ant- / ghp_ / AKIA / xoxb-)
|
|
138
|
+
out = out.replace(/\b(sk-[A-Za-z0-9_]{8,}|sk-proj-[A-Za-z0-9_-]{8,}|sk-ant-[A-Za-z0-9_-]{8,}|ghp_[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|xox[bp]-[A-Za-z0-9-]{8,})\b/g, REDACTED);
|
|
139
|
+
// 3. "标签: <20+位随机串>" 结构 — 冒号后跟长的 alnum token (MT5 data: D0E8...).
|
|
140
|
+
// 保守: 只匹配不含 `/`(避开 URL/路径) 且不含域名点 的纯 token 骨架.
|
|
141
|
+
out = out.replace(/([A-Za-z][A-Za-z0-9 _-]{0,20})\s*[::]\s*([A-Za-z0-9_+\-]{20,})\b(?![A-Za-z0-9])/g, (_m, name) => `${name}: ${REDACTED}`);
|
|
142
|
+
// 4. 宽松: 独立长串 24+ (base64) — 前后为空白/标点/行首行尾, 避开 URL 与中文段.
|
|
143
|
+
// 保守: 不含 `/` 与 `.`, 防误伤 URL/域名/路径.
|
|
144
|
+
out = out.replace(/(^|[\s(([::\])])[A-Za-z0-9_+=]{22,}(?=[\s))\].,,。;;::§]|$)/gm, (_m, prefix) => `${prefix}${REDACTED}`);
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
82
147
|
/** bolloon 目标根 (默认 ~/.bolloon, 可注入 override) */
|
|
83
148
|
function bolloonRoot(home) {
|
|
84
149
|
return path.join(home, '.bolloon');
|
|
@@ -86,15 +151,16 @@ function bolloonRoot(home) {
|
|
|
86
151
|
// ============================================================
|
|
87
152
|
// 单文件迁移 helper
|
|
88
153
|
// ============================================================
|
|
89
|
-
async function copyIfNeeded(deps, from, to, manifest) {
|
|
154
|
+
async function copyIfNeeded(deps, from, to, manifest, redact = false) {
|
|
90
155
|
const content = await deps.readFile(from);
|
|
91
156
|
if (content === undefined)
|
|
92
157
|
return false;
|
|
93
|
-
const
|
|
158
|
+
const contentOut = redact ? redactSecrets(content) : content;
|
|
159
|
+
const hash = sha1(contentOut);
|
|
94
160
|
if (manifest.get(to) === hash)
|
|
95
161
|
return false; // 未变化, 跳过
|
|
96
162
|
await deps.mkdir(path.dirname(to));
|
|
97
|
-
await deps.writeFile(to,
|
|
163
|
+
await deps.writeFile(to, contentOut);
|
|
98
164
|
manifest.set(to, hash);
|
|
99
165
|
return true;
|
|
100
166
|
}
|
|
@@ -122,14 +188,28 @@ async function copyDirIfNeeded(deps, srcDir, destDir, manifest) {
|
|
|
122
188
|
// ============================================================
|
|
123
189
|
// 主迁移
|
|
124
190
|
// ============================================================
|
|
125
|
-
/**
|
|
191
|
+
/** 探测某源是否安装: 返回选中的根目录 or null. */
|
|
126
192
|
export async function detectSource(deps, source) {
|
|
127
|
-
const root
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
193
|
+
for (const root of sourceRootCandidates(source, deps)) {
|
|
194
|
+
const st = await deps.stat(root);
|
|
195
|
+
if (st?.isDirectory)
|
|
196
|
+
return root;
|
|
197
|
+
}
|
|
131
198
|
return null;
|
|
132
199
|
}
|
|
200
|
+
const OPENCLAW_PERSONA = [
|
|
201
|
+
{ src: 'SOUL.md', toName: 'soul.md' },
|
|
202
|
+
{ src: 'IDENTITY.md', toName: 'identity.md' },
|
|
203
|
+
{ src: 'USER.md', toName: 'user.md' },
|
|
204
|
+
{ src: 'AGENTS.md', toName: 'agent.md' },
|
|
205
|
+
{ src: 'TOOLS.md', toName: 'project.md' },
|
|
206
|
+
{ src: 'MEMORY.md', toName: 'wiki.md' },
|
|
207
|
+
];
|
|
208
|
+
const HERMES_PERSONA = [
|
|
209
|
+
{ src: 'SOUL.md', toName: 'soul.md' },
|
|
210
|
+
{ src: 'memories/USER.md', toName: 'user.md' },
|
|
211
|
+
{ src: 'memories/MEMORY.md', toName: 'wiki.md' },
|
|
212
|
+
];
|
|
133
213
|
/**
|
|
134
214
|
* 迁移单个源的全部数据到 Bolloon.
|
|
135
215
|
* 返回 report; 源不存在 → migrated=false 且不抛错 (静默).
|
|
@@ -154,6 +234,7 @@ export async function migrateExternalAgent(source, deps = defaultDeps()) {
|
|
|
154
234
|
report.migrated = false;
|
|
155
235
|
return report; // 未安装, 静默
|
|
156
236
|
}
|
|
237
|
+
report.sourceRoot = root;
|
|
157
238
|
const ws = workspacePath(source, root);
|
|
158
239
|
const wsStat = await deps.stat(ws);
|
|
159
240
|
if (!wsStat?.isDirectory) {
|
|
@@ -185,43 +266,67 @@ export async function migrateExternalAgent(source, deps = defaultDeps()) {
|
|
|
185
266
|
catch { /* 损坏忽略, 从头迁 */ }
|
|
186
267
|
}
|
|
187
268
|
await deps.mkdir(path.join(bRoot, 'migration'));
|
|
188
|
-
// 1. persona
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
['USER.md', 'user.md'],
|
|
193
|
-
['AGENTS.md', 'agent.md'],
|
|
194
|
-
['TOOLS.md', 'project.md'],
|
|
195
|
-
['MEMORY.md', 'wiki.md'],
|
|
196
|
-
];
|
|
197
|
-
for (const [fromName, toName] of personaMap) {
|
|
198
|
-
const from = path.join(ws, fromName);
|
|
269
|
+
// 1. persona (per-source spec) — 含敏感 token, 内容级脱敏后写入
|
|
270
|
+
const personaSpec = source === 'openclaw' ? OPENCLAW_PERSONA : HERMES_PERSONA;
|
|
271
|
+
for (const { src, toName } of personaSpec) {
|
|
272
|
+
const from = path.join(ws, src);
|
|
199
273
|
const to = path.join(personaDir, toName);
|
|
200
|
-
if (await copyIfNeeded(deps, from, to, manifest)) {
|
|
274
|
+
if (await copyIfNeeded(deps, from, to, manifest, true)) {
|
|
201
275
|
report.persona.push(toName);
|
|
202
276
|
report.entries.push({ from, to, kind: 'persona' });
|
|
203
277
|
}
|
|
204
278
|
}
|
|
205
|
-
// 2. skills
|
|
279
|
+
// 2. skills → ~/.bolloon/skills/<name>/
|
|
206
280
|
const skillsSrc = path.join(ws, 'skills');
|
|
207
281
|
const skillDirs = await deps.readdir(skillsSrc);
|
|
208
282
|
if (skillDirs) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
283
|
+
// OpenClaw: skills/<name>/SKILL.md 一层, 直接落盘 <name>.
|
|
284
|
+
// Hermes: skills/<分类>/<技能>/SKILL.md 两层, 逐 <技能> 递归找 SKILL.md,
|
|
285
|
+
// 落盘 <分类>-<技能> 展平, 避免跨分类重名.
|
|
286
|
+
for (const entryName of skillDirs) {
|
|
287
|
+
if (entryName.startsWith('.'))
|
|
213
288
|
continue;
|
|
214
|
-
|
|
289
|
+
const entry = path.join(skillsSrc, entryName);
|
|
290
|
+
const st = await deps.stat(entry);
|
|
291
|
+
if (!st?.isDirectory)
|
|
215
292
|
continue;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
293
|
+
if (source === 'hermes') {
|
|
294
|
+
// 分类下的每个技能目录 → 目标 <分类>-<技能>
|
|
295
|
+
const cat = entryName;
|
|
296
|
+
const subDirs = await deps.readdir(entry);
|
|
297
|
+
if (!subDirs)
|
|
298
|
+
continue;
|
|
299
|
+
for (const skillName of subDirs) {
|
|
300
|
+
if (skillName.startsWith('.'))
|
|
301
|
+
continue;
|
|
302
|
+
const skillDir = path.join(entry, skillName);
|
|
303
|
+
const sst = await deps.stat(skillDir);
|
|
304
|
+
if (!sst?.isDirectory)
|
|
305
|
+
continue;
|
|
306
|
+
// 该分类下菊 不一定有 SKILL.md → 跳过
|
|
307
|
+
const hasSkill = await deps.exists(path.join(skillDir, 'SKILL.md'));
|
|
308
|
+
if (!hasSkill)
|
|
309
|
+
continue;
|
|
310
|
+
const targetName = `${cat}-${skillName}`;
|
|
311
|
+
const destDir = path.join(skillsRoot, targetName);
|
|
312
|
+
const copied = await copyDirIfNeeded(deps, skillDir, destDir, manifest);
|
|
313
|
+
if (copied.length > 0) {
|
|
314
|
+
report.skillsCopied.push(targetName);
|
|
315
|
+
report.entries.push({ from: skillDir, to: destDir, kind: 'skill' });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
const destDir = path.join(skillsRoot, entryName);
|
|
321
|
+
const copied = await copyDirIfNeeded(deps, entry, destDir, manifest);
|
|
322
|
+
if (copied.length > 0) {
|
|
323
|
+
report.skillsCopied.push(entryName);
|
|
324
|
+
report.entries.push({ from: entry, to: destDir, kind: 'skill' });
|
|
325
|
+
}
|
|
221
326
|
}
|
|
222
327
|
}
|
|
223
328
|
}
|
|
224
|
-
// 3. memory: workspace/memory/*.md → memory
|
|
329
|
+
// 3. memory: openclaw workspace/memory/*.md → sessions/; hermes 无独立 memory 目录
|
|
225
330
|
const memSrc = path.join(ws, 'memory');
|
|
226
331
|
const memFiles = await deps.readdir(memSrc);
|
|
227
332
|
if (memFiles) {
|
|
@@ -231,7 +336,7 @@ export async function migrateExternalAgent(source, deps = defaultDeps()) {
|
|
|
231
336
|
continue;
|
|
232
337
|
const from = path.join(memSrc, f);
|
|
233
338
|
const to = path.join(memoryRoot, `${idx + 1}-${f}`);
|
|
234
|
-
if (await copyIfNeeded(deps, from, to, manifest)) {
|
|
339
|
+
if (await copyIfNeeded(deps, from, to, manifest, true)) {
|
|
235
340
|
report.memoryCopied.push(f);
|
|
236
341
|
report.entries.push({ from, to, kind: 'memory' });
|
|
237
342
|
}
|
|
@@ -241,11 +346,11 @@ export async function migrateExternalAgent(source, deps = defaultDeps()) {
|
|
|
241
346
|
// 4. docs: workspace 根其他 .md → context-os/04-Projects/<source>-docs/
|
|
242
347
|
const wsFiles = await deps.readdir(ws);
|
|
243
348
|
if (wsFiles) {
|
|
244
|
-
const excluded =
|
|
349
|
+
const excluded = personaSpec.map((p) => p.toName);
|
|
245
350
|
for (const f of wsFiles) {
|
|
246
351
|
if (!f.endsWith('.md'))
|
|
247
352
|
continue;
|
|
248
|
-
if (excluded.
|
|
353
|
+
if (excluded.includes(f))
|
|
249
354
|
continue;
|
|
250
355
|
const from = path.join(ws, f);
|
|
251
356
|
const st = await deps.stat(from);
|