@bolloon/bolloon-agent 0.3.34 → 0.3.36

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/index.js CHANGED
@@ -354,7 +354,6 @@ function rpcErr(code, msg) {
354
354
  // CLI with persistent bottom prompt
355
355
  // 2026-07-28: 改用 readline.createInterface + replReadline 循环
356
356
  let isRunning = false;
357
- let cliContextPct = 0;
358
357
  let queueMode = false;
359
358
  const pendingQueue = [];
360
359
  let cliStartTime = 0;
@@ -371,20 +370,53 @@ function fmtDuration(ms) {
371
370
  const h = Math.floor(m / 60);
372
371
  return `${h}h ${m % 60}m`;
373
372
  }
374
- /** 状态栏: 模型 当前智能体 ( channel) ⏱ 时间 │ 上下文进度条 (模块级, CLI/Web 共用) */
373
+ /** 2026-08-06: ContextManager 读上下文用量 (CLI 状态栏数据源, 失败退化 0/1M) */
374
+ function getCliCtxUsage() {
375
+ try {
376
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
377
+ const cm = require('./bootstrap/context-manager.js').getContextManager();
378
+ const u = cm.getUsage();
379
+ return {
380
+ // 保留浮点 (0-100), 由 buildContextBar 格式化 — round 会让 <0.5% 全变 0, 状态栏像死代码
381
+ pct: Math.min(100, u.pct * 100),
382
+ usedTokens: u.usedTokens,
383
+ maxTokens: u.maxTokens,
384
+ stage: u.stage,
385
+ };
386
+ }
387
+ catch {
388
+ return { pct: 0, usedTokens: 0, maxTokens: 1_000_000, stage: 'normal' };
389
+ }
390
+ }
391
+ /** 上下文进度条: 320k/1M │ [██████░░░░] 32% (bolloon 色系: #c4d640 主色) */
392
+ function buildContextBar(usage) {
393
+ const barLen = 10;
394
+ const filled = Math.min(barLen, Math.max(0, Math.round((usage.pct / 100) * barLen)));
395
+ const barColor = usage.stage === 'warning' || usage.stage === 'compressing' ? C_WARN : C_ACCENT;
396
+ const bar = `${C_DIM}[${RESET}${barColor}${'█'.repeat(filled)}${RESET}${C_DIM}${'░'.repeat(barLen - filled)}${RESET}${C_DIM}]${RESET}`;
397
+ const fmtK = (n) => (n >= 1_000_000 ? (n % 1_000_000 === 0 ? `${n / 1_000_000}M` : `${(n / 1_000_000).toFixed(1)}M`) : n >= 1000 ? `${Math.round(n / 1000)}k` : String(n));
398
+ const usageTxt = `${C_TEXT}${fmtK(usage.usedTokens)}/${fmtK(usage.maxTokens)}${RESET}`;
399
+ // 百分比: >=10% 整数, >=1% 一位小数, <1% 两位小数 (1M 窗口下小 token 数也可见变化)
400
+ const pctTxt = usage.pct >= 10 ? `${Math.round(usage.pct)}%` : usage.pct >= 1 ? `${usage.pct.toFixed(1)}%` : `${usage.pct.toFixed(2)}%`;
401
+ let suffix = '';
402
+ if (usage.stage === 'warning')
403
+ suffix = ` ${C_WARN}⚠ 即将压缩${RESET}`;
404
+ else if (usage.stage === 'compressing')
405
+ suffix = ` ${C_WARN}🗜️ 压缩中...${RESET}`;
406
+ else if (usage.stage === 'compressed')
407
+ suffix = ` ${C_OK}✓ 已压缩${RESET}`;
408
+ return `${usageTxt} ${C_DIM}│${RESET} ${bar} ${barColor}${pctTxt}${RESET}${suffix}`;
409
+ }
410
+ /** 状态栏: 模型 │ 当前智能体 (含 channel) │ ⏱ 时间 │ 320k/1M │ [██████░░░░] 32% (bolloon 色系) */
375
411
  function getStatus() {
376
- const barLen = 12;
377
- const filled = Math.round((cliContextPct / 100) * barLen);
378
- const bar = '█'.repeat(filled) + '░'.repeat(barLen - filled);
412
+ const usage = getCliCtxUsage();
379
413
  const agentPart = cliActiveChannelId ? `${cliAgentName} ${C_DIM}(ch:${cliActiveChannelId.slice(0, 10)})${RESET}` : cliAgentName;
380
- return `${C_ACCENT}${cliModelName}\x1b[0m\x1b[90m │\x1b[0m ${agentPart} \x1b[90m│\x1b[0m ⏱ ${fmtDuration(Date.now() - cliStartTime)}\x1b[90m │\x1b[0m ${bar} ${cliContextPct}%`;
414
+ return `${C_ACCENT}${cliModelName}${RESET}${C_DIM} │${RESET} ${agentPart} ${C_DIM}│${RESET} ⏱ ${C_TEXT}${fmtDuration(Date.now() - cliStartTime)}${RESET}${C_DIM} │${RESET} ${buildContextBar(usage)}`;
381
415
  }
382
416
  function statusBarLine() {
383
417
  const dur = cliStartTime ? fmtDuration(Date.now() - cliStartTime) : '0s';
384
- const barLen = 12;
385
- const filled = Math.round((cliContextPct / 100) * barLen);
386
- const bar = C_OK + '█'.repeat(filled) + C_DIM + '░'.repeat(barLen - filled) + RESET;
387
- return `${C_ACCENT}${cliModelName}${RESET}${C_DIM} │${RESET} ${cliAgentName} ${C_DIM}│${RESET} ⏱ ${C_ACCENT}${dur}${RESET} ${C_DIM}│${RESET} ${bar} ${C_DIM}${cliContextPct}%${RESET}`;
418
+ const usage = getCliCtxUsage();
419
+ return `${C_ACCENT}${cliModelName}${RESET}${C_DIM} │${RESET} ${cliAgentName} ${C_DIM}│${RESET} ${C_ACCENT}${dur}${RESET} ${C_DIM}│${RESET} ${buildContextBar(usage)}`;
388
420
  }
389
421
  async function startCLI(comm) {
390
422
  isRunning = true;
@@ -436,7 +468,8 @@ async function startCLI(comm) {
436
468
  /* 无 channels/active 记录时保持默认 */
437
469
  }
438
470
  // 进入 Ink TUI 输入循环
439
- const initialStatus = `${C_ACCENT}${cliModelName}\x1b[0m\x1b[90m │\x1b[0m ${cliAgentName} \x1b[90m│\x1b[0m ⏱ 0s`;
471
+ // 2026-08-06: 初始状态栏也带上下文显示 (0/1M [░░░░░░░░░░] 0%)
472
+ const initialStatus = `${C_ACCENT}${cliModelName}${RESET}${C_DIM} │${RESET} ${cliAgentName} ${C_DIM}│${RESET} ⏱ 0s${C_DIM} │${RESET} ${buildContextBar(getCliCtxUsage())}`;
440
473
  startInk((text) => { processInput(text, comm); }, initialStatus, getStatus);
441
474
  // Wait on a promise that resolves on Ctrl+C / 双击 Esc
442
475
  // (ink-app 的 requestExit 调 __inkRequestExit → resolve, 清理后 process.exit)
@@ -548,12 +581,438 @@ async function processInput(input, comm) {
548
581
  appendLine(`${C_WARN}[队列 ${pendingQueue.length}]${RESET} 已入队, 执行完当前后自动运行`);
549
582
  return;
550
583
  }
584
+ // ==================== 2026-08-06: 系统命令组 (/model /now /ipfs /memory ...) ====================
585
+ const cmd = trimmed.toLowerCase();
586
+ // /model /login — 模型供应商选择器 (ink 交互渲染, 复用 MentionPopup)
587
+ if (cmd === '/model' || cmd === '/login') {
588
+ try {
589
+ const { llmConfigStore, PROVIDER_INFO } = await import('./llm/config-store.js');
590
+ await llmConfigStore.initialize();
591
+ const config = await llmConfigStore.getConfig();
592
+ const items = Object.entries(config.providers).map(([name, p]) => ({
593
+ kind: 'command',
594
+ label: name,
595
+ hint: `${String(PROVIDER_INFO[name]?.name || '').padEnd(14)} ${p.apiKey ? '🔑' : p.requiresApiKey ? '⚠ 无key' : ''} ${p.model || ''}`,
596
+ insert: name,
597
+ }));
598
+ globalThis.__inkOpenPicker?.(items, '选择模型供应商 (↑↓ 选择 · Enter 确认 · Esc 取消)', async (it) => {
599
+ try {
600
+ await llmConfigStore.setActiveProvider(it.label);
601
+ const active = await llmConfigStore.getActiveProvider();
602
+ appendLine(`${C_OK}✓ 已切换到 ${it.label} (${String(PROVIDER_INFO[it.label]?.name || '')})${RESET}`);
603
+ appendLine(`${C_DIM} 当前模型: ${config.providers[it.label]?.model || '默认'}${RESET}`);
604
+ }
605
+ catch (e) {
606
+ appendLine(`${C_ERROR}✗ 切换失败: ${String(e.message || e).slice(0, 150)}${RESET}`);
607
+ }
608
+ });
609
+ }
610
+ catch (e) {
611
+ appendLine(`${C_ERROR}/model 失败: ${String(e.message || e).slice(0, 150)}${RESET}`);
612
+ }
613
+ return;
614
+ }
615
+ // /logout — 显示当前供应商 (减法: 登出 = 查看当前, 切换走 /model)
616
+ if (cmd === '/logout') {
617
+ try {
618
+ const { llmConfigStore, PROVIDER_INFO } = await import('./llm/config-store.js');
619
+ await llmConfigStore.initialize();
620
+ const active = await llmConfigStore.getActiveProvider();
621
+ const cfg = await llmConfigStore.getActiveProviderConfig();
622
+ appendLine(`${C_DIM}当前供应商:${RESET} ${C_ACCENT}${active}${RESET} (${String(PROVIDER_INFO[active]?.name || '')})`);
623
+ appendLine(`${C_DIM} 模型: ${cfg?.model || '默认'}${RESET}`);
624
+ appendLine(`${C_DIM} 切换: /model 打开选择器${RESET}`);
625
+ }
626
+ catch { /* 静默 */ }
627
+ return;
628
+ }
629
+ // /now — 当前状态总览
630
+ if (cmd === '/now') {
631
+ try {
632
+ const cm = require('./bootstrap/context-manager.js').getContextManager();
633
+ const usage = cm.getUsage();
634
+ appendLine(`${C_ACCENT}● 当前状态${RESET}`);
635
+ appendLine(` ${C_DIM}智能体:${RESET} ${cliAgentName} ${cliActiveChannelId ? `(${C_DIM}ch:${cliActiveChannelId.slice(0, 12)}${RESET})` : ''}`);
636
+ appendLine(` ${C_DIM}运行:${RESET} ${fmtDuration(Date.now() - cliStartTime)}`);
637
+ appendLine(` ${C_DIM}上下文:${RESET} ${(usage.usedTokens / 1000).toFixed(0)}k / ${(usage.maxTokens / 1000).toFixed(0)}k tokens (${Math.round(usage.pct * 100)}%)${usage.stage === 'warning' ? ` ${C_WARN}⚠ 即将压缩${RESET}` : ''}`);
638
+ const a = await getAgent();
639
+ appendLine(` ${C_DIM}消息:${RESET} ${a.messageHistory?.length ?? 0} 条`);
640
+ }
641
+ catch { /* 静默 */ }
642
+ return;
643
+ }
644
+ // /tools — 可用工具列表
645
+ if (cmd === '/tools') {
646
+ try {
647
+ const a = await getAgent();
648
+ const defs = (a.getToolDefinitions?.() ?? []);
649
+ const names = Array.isArray(defs) ? defs.map((d) => d.name || d.function?.name).filter(Boolean) : Object.keys(defs || {});
650
+ appendLine(`${C_ACCENT}可用工具 (${names.length}):${RESET}`);
651
+ for (const n of names.slice(0, 40))
652
+ appendLine(` ${C_DIM}·${RESET} ${n}`);
653
+ if (names.length > 40)
654
+ appendLine(` ${C_DIM}... 共 ${names.length} 个${RESET}`);
655
+ }
656
+ catch { /* 静默 */ }
657
+ return;
658
+ }
659
+ // /session — 当前会话信息
660
+ if (cmd === '/session') {
661
+ try {
662
+ const a = await getAgent();
663
+ const h = a.messageHistory ?? [];
664
+ appendLine(`${C_ACCENT}会话:${RESET}`);
665
+ appendLine(` ${C_DIM}channel:${RESET} ${a.currentChannelId || '—'}`);
666
+ appendLine(` ${C_DIM}agent:${RESET} ${a.currentAgentId || '—'}`);
667
+ appendLine(` ${C_DIM}消息:${RESET} ${h.length} 条 (${h.length > 15 ? `${h.length - 15} 条已压缩` : '窗口内'})`);
668
+ }
669
+ catch { /* 静默 */ }
670
+ return;
671
+ }
672
+ // /memory — 记忆摘要 (memory-compressor 落盘文件)
673
+ if (cmd === '/memory') {
674
+ try {
675
+ const { getMemoryDir } = await import('./bootstrap/memory-compressor.js');
676
+ const { readdir, readFile } = await import('fs/promises');
677
+ const { join } = await import('path');
678
+ const dir = getMemoryDir(cliAgentName === 'bolloon' ? 'agent' : cliAgentName);
679
+ const files = (await readdir(join(dir, 'sessions')).catch(() => [])).filter((f) => f.endsWith('.summary.md'));
680
+ appendLine(`${C_ACCENT}记忆摘要 (${files.length} 个 session):${RESET}`);
681
+ for (const f of files.slice(-5)) {
682
+ try {
683
+ const raw = await readFile(join(dir, 'sessions', f), 'utf-8');
684
+ const tail = raw.trim().split('\n').slice(-6).join(' ').slice(0, 180);
685
+ appendLine(` ${C_DIM}·${RESET} ${f.replace('.summary.md', '').slice(-30)}`);
686
+ appendLine(` ${C_DIM}${tail}${RESET}`);
687
+ }
688
+ catch { /* 跳过 */ }
689
+ }
690
+ }
691
+ catch { /* 静默 */ }
692
+ return;
693
+ }
694
+ // /resume — 恢复: 最近记忆摘要 + 进行中计划
695
+ if (cmd === '/resume' || cmd.startsWith('/resume ')) {
696
+ try {
697
+ const { getMemoryDir, getSessionSummaryPath } = await import('./bootstrap/memory-compressor.js');
698
+ const { readFile, readdir } = await import('fs/promises');
699
+ const { join } = await import('path');
700
+ const dir = getMemoryDir(cliAgentName === 'bolloon' ? 'agent' : cliAgentName);
701
+ const files = (await readdir(join(dir, 'sessions')).catch(() => [])).filter((f) => f.endsWith('.summary.md'));
702
+ appendLine(`${C_ACCENT}↻ 恢复上下文:${RESET}`);
703
+ if (files.length > 0) {
704
+ const f = files[files.length - 1];
705
+ const raw = await readFile(join(dir, 'sessions', f), 'utf-8');
706
+ const block = raw.trim().split('\n').slice(-12).join('\n').slice(-1200);
707
+ appendLine(` ${C_DIM}最近记忆 (${f.slice(0, 24)}...):${RESET}`);
708
+ for (const line of block.split('\n').slice(-8))
709
+ appendLine(` ${C_DIM}${line.slice(0, 100)}${RESET}`);
710
+ }
711
+ else {
712
+ appendLine(` ${C_DIM}暂无记忆摘要${RESET}`);
713
+ }
714
+ const { listActivePlans } = await import('./agents/plan-store.js');
715
+ const plans = await listActivePlans();
716
+ if (plans.length > 0) {
717
+ appendLine(` ${C_DIM}进行中计划 (${plans.length}):${RESET}`);
718
+ for (const p of plans.slice(0, 3))
719
+ appendLine(` ${C_ACCENT}·${RESET} ${p.goal || p.planId} ${C_DIM}${p.status || ''}${RESET}`);
720
+ }
721
+ }
722
+ catch { /* 静默 */ }
723
+ return;
724
+ }
725
+ // /goal — 进行中的目标/计划
726
+ if (cmd === '/goal') {
727
+ try {
728
+ const { listActivePlans, planToContext } = await import('./agents/plan-store.js');
729
+ const plans = await listActivePlans();
730
+ appendLine(`${C_ACCENT}目标 (${plans.length} 个进行中):${RESET}`);
731
+ if (plans.length === 0) {
732
+ appendLine(` ${C_DIM}无进行中计划 — 可用 /plan 创建${RESET}`);
733
+ }
734
+ for (const p of plans.slice(0, 5)) {
735
+ appendLine(` ${C_ACCENT}●${RESET} ${p.goal || p.planId} ${C_DIM}[${p.status || 'active'}]${RESET}`);
736
+ const steps = Array.isArray(p.steps) ? p.steps : [];
737
+ const done = steps.filter((s) => s.done || s.status === 'done').length;
738
+ if (steps.length > 0)
739
+ appendLine(` ${C_DIM}${done}/${steps.length} 步完成${RESET}`);
740
+ }
741
+ }
742
+ catch { /* 静默 */ }
743
+ return;
744
+ }
745
+ // /skill — 技能候选 (skill-writer 落盘)
746
+ if (cmd === '/skill') {
747
+ try {
748
+ const { listSkillCandidates } = await import('./agents/skill-writer.js');
749
+ const cands = await listSkillCandidates();
750
+ appendLine(`${C_ACCENT}技能候选 (${cands.length}):${RESET}`);
751
+ if (cands.length === 0) {
752
+ appendLine(` ${C_DIM}无候选 — 连续成功工具调用 ≥2 自动生成${RESET}`);
753
+ }
754
+ for (const c of cands.slice(0, 8)) {
755
+ appendLine(` ${C_DIM}·${RESET} ${c.name || '?'} ${C_DIM}(${c.source || ''})${RESET}`);
756
+ }
757
+ }
758
+ catch { /* 静默 */ }
759
+ return;
760
+ }
761
+ // /mcp — MCP 插件/工具列表
762
+ if (cmd === '/mcp') {
763
+ try {
764
+ const { readFile } = await import('fs/promises');
765
+ const { join } = await import('path');
766
+ let servers = {};
767
+ try {
768
+ servers = JSON.parse(await readFile(join(process.env.HOME || '/tmp', '.mcp.json'), 'utf-8')).mcpServers || {};
769
+ }
770
+ catch { /* 无 */ }
771
+ appendLine(`${C_ACCENT}MCP 服务器 (${Object.keys(servers).length}):${RESET}`);
772
+ if (Object.keys(servers).length === 0) {
773
+ appendLine(` ${C_DIM}无 (~/.mcp.json 未配置)${RESET}`);
774
+ }
775
+ for (const [name, s] of Object.entries(servers)) {
776
+ const cmdStr = s?.command || '';
777
+ appendLine(` ${C_DIM}·${RESET} ${name} ${C_DIM}(${String(cmdStr).slice(0, 40)})${RESET}`);
778
+ }
779
+ }
780
+ catch { /* 静默 */ }
781
+ return;
782
+ }
783
+ // /agent — 当前智能体身份
784
+ if (cmd === '/agent') {
785
+ try {
786
+ const a = await getAgent();
787
+ appendLine(`${C_ACCENT}智能体:${RESET}`);
788
+ appendLine(` ${C_DIM}名称:${RESET} ${cliAgentName}`);
789
+ appendLine(` ${C_DIM}agentId:${RESET} ${a.currentAgentId || '—'}`);
790
+ appendLine(` ${C_DIM}channel:${RESET} ${cliActiveChannelId || '—'}`);
791
+ }
792
+ catch { /* 静默 */ }
793
+ return;
794
+ }
795
+ // /did — DID 身份
796
+ if (cmd === '/did') {
797
+ try {
798
+ const { loadOrCreateAgentIdentity } = await import('./agents/agent-identity.js');
799
+ const identity = loadOrCreateAgentIdentity(cliAgentName === 'bolloon' ? 'default-agent' : cliAgentName);
800
+ appendLine(`${C_ACCENT}DID 身份:${RESET}`);
801
+ appendLine(` ${C_DIM}did:${RESET} ${identity.did}`);
802
+ appendLine(` ${C_DIM}publicKey:${RESET} ${identity.publicKey?.slice(0, 32) || '—'}...`);
803
+ appendLine(` ${C_DIM}发布:${RESET} 可用 publish_did 工具发布到 IPFS+IPNS`);
804
+ }
805
+ catch (e) {
806
+ appendLine(`${C_ERROR}/did 失败: ${String(e.message || e).slice(0, 120)}${RESET}`);
807
+ }
808
+ return;
809
+ }
810
+ // /ipfs — Kubo 状态
811
+ if (cmd === '/ipfs') {
812
+ try {
813
+ const { kuboApi } = await import('./agents/pi-sdk-tools.js');
814
+ const id = await kuboApi('/api/v0/id');
815
+ const peers = await kuboApi('/api/v0/swarm/peers');
816
+ const pins = await kuboApi('/api/v0/pin/ls?type=recursive');
817
+ appendLine(`${C_ACCENT}IPFS (Kubo):${RESET}`);
818
+ appendLine(` ${C_DIM}节点:${RESET} ${String(id.ID || '').slice(0, 24)}...`);
819
+ appendLine(` ${C_DIM}版本:${RESET} ${id.AgentVersion || ''}`);
820
+ appendLine(` ${C_DIM}peers:${RESET} ${peers?.Peers?.length ?? 0}`);
821
+ appendLine(` ${C_DIM}pins:${RESET} ${pins?.Keys ? Object.keys(pins.Keys).length : 0}`);
822
+ }
823
+ catch (e) {
824
+ appendLine(`${C_ERROR}/ipfs 失败: ${String(e.message || e).slice(0, 120)}${RESET}`);
825
+ }
826
+ return;
827
+ }
828
+ // /ipns — IPNS 状态 (keys + self 解析)
829
+ if (cmd === '/ipns') {
830
+ try {
831
+ const { kuboApi } = await import('./agents/pi-sdk-tools.js');
832
+ const keys = await kuboApi('/api/v0/key/list');
833
+ const keyList = keys?.Keys || [];
834
+ appendLine(`${C_ACCENT}IPNS keys (${keyList.length}):${RESET}`);
835
+ for (const k of keyList.slice(0, 10)) {
836
+ appendLine(` ${C_DIM}·${RESET} ${k.Name} ${C_DIM}${String(k.Id).slice(0, 20)}...${RESET}`);
837
+ }
838
+ try {
839
+ const r = await kuboApi('/api/v0/name/resolve?arg=ui-deploy&recursive=true&nocache=true', undefined, 15000);
840
+ appendLine(` ${C_DIM}ui-deploy →${RESET} ${r.Path || ''}`);
841
+ }
842
+ catch { /* 无 ui-deploy */ }
843
+ }
844
+ catch (e) {
845
+ appendLine(`${C_ERROR}/ipns 失败: ${String(e.message || e).slice(0, 120)}${RESET}`);
846
+ }
847
+ return;
848
+ }
849
+ // /wallet — 钱包状态
850
+ if (cmd === '/wallet') {
851
+ try {
852
+ const { readFile } = await import('fs/promises');
853
+ const { join } = await import('path');
854
+ let wallets = [];
855
+ try {
856
+ wallets = JSON.parse(await readFile(join(process.env.HOME || '/tmp', '.bolloon', 'wallets.json'), 'utf-8'));
857
+ }
858
+ catch { /* 无 */ }
859
+ appendLine(`${C_ACCENT}钱包 (${Array.isArray(wallets) ? wallets.length : 0}):${RESET}`);
860
+ if (!Array.isArray(wallets) || wallets.length === 0) {
861
+ appendLine(` ${C_DIM}无 — 可用 wallet_create 工具创建 EVM 钱包${RESET}`);
862
+ }
863
+ for (const w of (Array.isArray(wallets) ? wallets : []).slice(0, 5)) {
864
+ appendLine(` ${C_DIM}·${RESET} ${w.name || w.address?.slice(0, 12) || '?'} ${C_DIM}${String(w.address || '').slice(0, 16)}...${RESET}`);
865
+ }
866
+ }
867
+ catch { /* 静默 */ }
868
+ return;
869
+ }
870
+ // /email — 邮件配置状态
871
+ if (cmd === '/email') {
872
+ try {
873
+ const { readFile } = await import('fs/promises');
874
+ const { join } = await import('path');
875
+ let cfg = null;
876
+ try {
877
+ cfg = JSON.parse(await readFile(join(process.env.HOME || '/tmp', '.bolloon', 'smtp.json'), 'utf-8'));
878
+ }
879
+ catch { /* 无 */ }
880
+ appendLine(`${C_ACCENT}邮件 (SMTP):${RESET}`);
881
+ if (!cfg) {
882
+ appendLine(` ${C_DIM}未配置 smtp.json — 发件人: 天墟星剑 <2844169590@qq.com>${RESET}`);
883
+ }
884
+ else {
885
+ appendLine(` ${C_DIM}host:${RESET} ${cfg.host || 'smtp.qq.com'}`);
886
+ appendLine(` ${C_DIM}发件人:${RESET} ${cfg.from || cfg.user || '—'}`);
887
+ }
888
+ }
889
+ catch { /* 静默 */ }
890
+ return;
891
+ }
892
+ // /loop — 当前循环状态 (消息数 + token 估算)
893
+ if (cmd === '/loop') {
894
+ try {
895
+ const a = await getAgent();
896
+ const h = a.messageHistory ?? [];
897
+ let tokens = 0;
898
+ try {
899
+ const { estimateTokens } = require('./context-compaction/index.js');
900
+ tokens = estimateTokens(h);
901
+ }
902
+ catch {
903
+ tokens = Math.round(JSON.stringify(h).length / 4);
904
+ }
905
+ appendLine(`${C_ACCENT}Loop 状态:${RESET}`);
906
+ appendLine(` ${C_DIM}消息:${RESET} ${h.length} 条 (窗口 15, ${Math.max(0, h.length - 15)} 条早期压缩)`);
907
+ appendLine(` ${C_DIM}token:${RESET} ${(tokens / 1000).toFixed(1)}k / 1M (${((tokens / 1_000_000) * 100).toFixed(2)}%)`);
908
+ }
909
+ catch { /* 静默 */ }
910
+ return;
911
+ }
912
+ // /judgement — 判断力列表
913
+ if (cmd === '/judgement' || cmd === '/judgments') {
914
+ try {
915
+ const { loadAllJudgments } = await import('./pi-ecosystem-judgment/human-value-store.js');
916
+ const all = await loadAllJudgments().catch(() => []);
917
+ appendLine(`${C_ACCENT}判断力 (${all.length} 条):${RESET}`);
918
+ for (const j of all.slice(0, 8)) {
919
+ appendLine(` ${C_DIM}·${RESET} ${String(j.decision || '').slice(0, 70)}`);
920
+ }
921
+ }
922
+ catch { /* 静默 */ }
923
+ return;
924
+ }
925
+ // /insight — Context OS 08-Insights 资产
926
+ if (cmd === '/insight') {
927
+ try {
928
+ const { readContextAssets, readAssetBody } = await import('./bootstrap/context-os.js');
929
+ const listings = await readContextAssets('08-Insights');
930
+ const files = listings[0]?.files || [];
931
+ appendLine(`${C_ACCENT}洞察 (${files.length} 篇):${RESET}`);
932
+ if (files.length === 0) {
933
+ appendLine(` ${C_DIM}无 — 价值点路由自动沉淀 insight 到 08-Insights${RESET}`);
934
+ }
935
+ for (const f of files.slice(0, 6)) {
936
+ const body = await readAssetBody('08-Insights', f.file).catch(() => null);
937
+ const firstLine = (body?.body || '').split('\n').filter(l => l.trim() && !l.startsWith('---')).slice(0, 2).join(' ').slice(0, 90);
938
+ appendLine(` ${C_ACCENT}·${RESET} ${f.title} ${C_DIM}${firstLine ? '— ' + firstLine : ''}${RESET}`);
939
+ }
940
+ }
941
+ catch { /* 静默 */ }
942
+ return;
943
+ }
944
+ // /wiki — wiki 状态
945
+ if (cmd === '/wiki') {
946
+ try {
947
+ const { readFile } = await import('fs/promises');
948
+ const { join } = await import('path');
949
+ const root = process.cwd();
950
+ const statusPath = join(root, 'docs', 'wiki', 'current-status.md');
951
+ const raw = await readFile(statusPath, 'utf-8');
952
+ const title = raw.match(/^title:\s*(.+)$/m)?.[1] || 'current-status';
953
+ const confirmed = raw.match(/^last_confirmed:\s*(.+)$/m)?.[1] || '?';
954
+ const supported = (raw.match(/\|\|+/g) || []).length;
955
+ appendLine(`${C_ACCENT}Wiki:${RESET} ${title}`);
956
+ appendLine(` ${C_DIM}last_confirmed:${RESET} ${confirmed}`);
957
+ appendLine(` ${C_DIM}已支持条目:${RESET} ${supported}`);
958
+ appendLine(` ${C_DIM}位置:${RESET} docs/wiki/ (wiki-first 范式)`);
959
+ }
960
+ catch { /* 静默 */ }
961
+ return;
962
+ }
963
+ // /dream — 随机灵感 (从 Insights + Knowledge 资产随机取一条)
964
+ if (cmd === '/dream') {
965
+ try {
966
+ const { readContextAssets, readAssetBody } = await import('./bootstrap/context-os.js');
967
+ const layers = ['08-Insights', '07-Knowledge', '12-Analysis'];
968
+ const pool = [];
969
+ for (const layer of layers) {
970
+ const listings = await readContextAssets(layer);
971
+ for (const f of (listings[0]?.files || []).slice(0, 5)) {
972
+ const body = await readAssetBody(layer, f.file).catch(() => null);
973
+ const lines = (body?.body || '').split('\n').filter(l => l.trim() && !l.startsWith('---') && !l.startsWith('#') && !l.startsWith('>') && !l.startsWith('未来'));
974
+ if (lines[0])
975
+ pool.push(lines[0].trim().slice(0, 100));
976
+ }
977
+ }
978
+ if (pool.length === 0) {
979
+ appendLine(`${C_DIM}🌙 梦境空空 — 多对话让记忆沉淀出洞察后, /dream 就有素材了${RESET}`);
980
+ }
981
+ else {
982
+ const pick = pool[Math.floor(Math.random() * pool.length)];
983
+ appendLine(`${C_DIM}🌙 ${pick}${RESET}`);
984
+ }
985
+ }
986
+ catch { /* 静默 */ }
987
+ return;
988
+ }
551
989
  if (trimmed.toLowerCase() === '/help' || trimmed === 'help') {
552
990
  appendLine(`${C_DIM}命令:${RESET}`);
553
991
  appendLine(` ${C_ACCENT}!<cmd>${RESET} 执行终端命令 ${C_DIM}如 !ls -la${RESET}`);
554
992
  appendLine(` ${C_ACCENT}/queue${RESET} 切换队列模式 ${C_DIM}输入排队, 当前结束后自动执行${RESET}`);
555
993
  appendLine(` ${C_ACCENT}/dequeue${RESET} 出队一条`);
556
994
  appendLine(` ${C_ACCENT}/channel [名字|id|序号]${RESET} 切换当前智能体 ${C_DIM}无参列出所有; 支持名字/ID/序号三种解析${RESET}`);
995
+ appendLine(` ${C_ACCENT}/model${RESET} / ${C_ACCENT}/login${RESET} 模型供应商选择器 ${C_DIM}↑↓ 选择 · Enter 确认 · Esc 取消${RESET}`);
996
+ appendLine(` ${C_ACCENT}/logout${RESET} 查看当前供应商`);
997
+ appendLine(` ${C_ACCENT}/now${RESET} 当前状态总览 ${C_DIM}智能体/运行时间/上下文 tokens/消息数${RESET}`);
998
+ appendLine(` ${C_ACCENT}/session${RESET} 当前会话信息 ${C_DIM}channel/agent/消息窗口${RESET}`);
999
+ appendLine(` ${C_ACCENT}/loop${RESET} 循环状态 ${C_DIM}消息数 + token 估算${RESET}`);
1000
+ appendLine(` ${C_ACCENT}/memory${RESET} 记忆摘要 ${C_DIM}memory-compressor 落盘摘要${RESET}`);
1001
+ appendLine(` ${C_ACCENT}/resume${RESET} 恢复上下文 ${C_DIM}最近记忆 + 进行中计划${RESET}`);
1002
+ appendLine(` ${C_ACCENT}/goal${RESET} 进行中目标 ${C_DIM}plan-store active plans${RESET}`);
1003
+ appendLine(` ${C_ACCENT}/tools${RESET} 可用工具列表`);
1004
+ appendLine(` ${C_ACCENT}/skill${RESET} 技能候选 ${C_DIM}skill-writer 沉淀候选${RESET}`);
1005
+ appendLine(` ${C_ACCENT}/mcp${RESET} MCP 服务器列表`);
1006
+ appendLine(` ${C_ACCENT}/agent${RESET} 当前智能体身份`);
1007
+ appendLine(` ${C_ACCENT}/did${RESET} DID 身份`);
1008
+ appendLine(` ${C_ACCENT}/ipfs${RESET} Kubo 状态 ${C_DIM}节点/peers/pins${RESET}`);
1009
+ appendLine(` ${C_ACCENT}/ipns${RESET} IPNS keys + resolve`);
1010
+ appendLine(` ${C_ACCENT}/wallet${RESET} 钱包状态`);
1011
+ appendLine(` ${C_ACCENT}/email${RESET} 邮件配置`);
1012
+ appendLine(` ${C_ACCENT}/judgement${RESET} 判断力列表`);
1013
+ appendLine(` ${C_ACCENT}/insight${RESET} Context OS 洞察 (08-Insights)`);
1014
+ appendLine(` ${C_ACCENT}/wiki${RESET} wiki 状态`);
1015
+ appendLine(` ${C_ACCENT}/dream${RESET} 随机灵感`);
557
1016
  appendLine(` ${C_ACCENT}@名字${RESET} @ 命中智能体 ${C_DIM}弹出窗选择后发送给智能体${RESET}`);
558
1017
  appendLine(` ${C_ACCENT}/名字${RESET} / 命中命令/技能/插件 ${C_DIM}输入 / 自动弹出${RESET}`);
559
1018
  appendLine(` ${C_ACCENT}#路径${RESET} # 命中文件 ${C_DIM}输入 # 自动弹出文件列表${RESET}`);
@@ -678,14 +1137,31 @@ async function processInput(input, comm) {
678
1137
  catch { /* 非致命, 静默 */ }
679
1138
  });
680
1139
  }
681
- // 更新状态栏: 上下文进度
1140
+ // 更新状态栏: 上下文进度 (2026-08-06: 每轮按当前 messageHistory 重算并写回 ContextManager,
1141
+ // 保证状态栏按需更新 — 不依赖 pi-sdk loop 内部上报, 1s 定时器读到的一定是最新值)
682
1142
  try {
683
- const msgLen = JSON.stringify(a.messageHistory ?? []).length;
684
- cliContextPct = Math.min(100, Math.round((msgLen / 240_000) * 100));
685
- const barLen = 12;
686
- const filled = Math.round((cliContextPct / 100) * barLen);
687
- const bar = '█'.repeat(filled) + '░'.repeat(barLen - filled);
688
- const statusText = `${C_ACCENT}${cliModelName}\x1b[0m\x1b[90m │\x1b[0m ${cliAgentName} \x1b[90m│\x1b[0m ⏱ ${fmtDuration(Date.now() - cliStartTime)} \x1b[90m│\x1b[0m ${msgLen.toLocaleString()}B/240K │ ${bar} ${cliContextPct}%`;
1143
+ const { getContextManager } = await import('./bootstrap/context-manager.js');
1144
+ const cm = getContextManager();
1145
+ // context-compaction 的估算器 (与 pi-sdk estimateHistoryTokens 同源: 4 字符 ≈ 1 token)
1146
+ const history = a.messageHistory ?? [];
1147
+ let usedTokens = 0;
1148
+ try {
1149
+ const { estimateTokens } = require('./context-compaction/index.js');
1150
+ usedTokens = estimateTokens(history);
1151
+ }
1152
+ catch {
1153
+ usedTokens = Math.max(0, Math.round(JSON.stringify(history).length / 4));
1154
+ }
1155
+ // 写回数据源 — 状态栏/Web/任何订阅方都拿到新鲜值
1156
+ const usage = cm.updateUsage(usedTokens);
1157
+ const usageView = {
1158
+ // 保留浮点 (0-100), buildContextBar 内部格式化
1159
+ pct: Math.min(100, (usage.usedTokens / Math.max(1, usage.maxTokens)) * 100),
1160
+ usedTokens: usage.usedTokens,
1161
+ maxTokens: usage.maxTokens,
1162
+ stage: usage.stage,
1163
+ };
1164
+ const statusText = `${C_ACCENT}${cliModelName}${RESET}${C_DIM} │${RESET} ${cliAgentName} ${C_DIM}│${RESET} ⏱ ${fmtDuration(Date.now() - cliStartTime)}${C_DIM} │${RESET} ${buildContextBar(usageView)}`;
689
1165
  inkSetStatus(statusText);
690
1166
  }
691
1167
  catch { /* 降级容忍 */ }
@@ -1672,6 +1672,15 @@
1672
1672
  async function loadChannels() {
1673
1673
  try {
1674
1674
  const res = await fetch("/channels");
1675
+ const ct = res.headers.get("content-type") || "";
1676
+ if (!ct.includes("application/json")) {
1677
+ const text = await res.text().catch(() => "");
1678
+ if (!text.trim().startsWith("[") && !text.trim().startsWith("{")) {
1679
+ console.warn("[\u52A0\u8F7D\u9891\u9053] \u68C0\u6D4B\u5230 IPFS \u9759\u6001\u6A21\u5F0F (\u65E0\u540E\u7AEF server), \u529F\u80FD\u53D7\u9650");
1680
+ showStaticModeNotice();
1681
+ return;
1682
+ }
1683
+ }
1675
1684
  channels = await res.json();
1676
1685
  console.log("[\u52A0\u8F7D\u9891\u9053] \u4ECE\u670D\u52A1\u5668\u83B7\u53D6\u5230", channels.length, "\u4E2A\u9891\u9053");
1677
1686
  channels.forEach((ch, i) => {
@@ -1694,6 +1703,17 @@
1694
1703
  console.error("[\u52A0\u8F7D\u9891\u9053] \u5931\u8D25:", err);
1695
1704
  }
1696
1705
  }
1706
+ function showStaticModeNotice() {
1707
+ if (document.getElementById("ipfs-static-notice")) return;
1708
+ const notice = document.createElement("div");
1709
+ notice.id = "ipfs-static-notice";
1710
+ notice.style.cssText = "position:fixed;bottom:60px;left:50%;transform:translateX(-50%);z-index:9999;background:#1a1a18;border:1px solid #c4d640;color:#d8d8c8;padding:10px 16px;border-radius:8px;font-size:12px;box-shadow:0 4px 20px rgba(0,0,0,.5);max-width:560px;text-align:center;";
1711
+ notice.innerHTML = `\u{1F4E1} <b style="color:#c4d640">IPFS \u9759\u6001\u6A21\u5F0F</b> \u2014 \u6B64\u9875\u9762\u901A\u8FC7 IPNS \u4ECE\u53BB\u4E2D\u5FC3\u5316\u7F51\u7EDC\u52A0\u8F7D.<br>\u5B8C\u6574\u529F\u80FD (\u5BF9\u8BDD/\u5DE5\u5177/\u5224\u65AD\u529B) \u9700\u8FDE\u63A5\u672C\u5730 Bolloon server: <code style="color:#c4d640">bolloon --web</code>`;
1712
+ document.body.appendChild(notice);
1713
+ setTimeout(() => {
1714
+ notice.remove();
1715
+ }, 15e3);
1716
+ }
1697
1717
  var v3GlobalEventSource = null;
1698
1718
  function startV3GlobalSSE() {
1699
1719
  if (v3GlobalEventSource) return;
@@ -1937,6 +1957,19 @@ ${msg.text || ""}`, "ai", false, log);
1937
1957
  console.log(`[v3-friend] \u2705 ack \u6536\u5230: ${name} \u5DF2\u6536\u5230\u597D\u53CB\u7533\u8BF7`);
1938
1958
  showSimpleToast(`\u{1F4EC} ${name} \u5DF2\u6536\u5230\u4F60\u7684\u597D\u53CB\u7533\u8BF7, \u7B49\u5BF9\u65B9\u63A5\u53D7`);
1939
1959
  }
1960
+ } else if (msg.type === "context_event") {
1961
+ try {
1962
+ const evt = msg.evt || {};
1963
+ if (evt.type === "context.warning") {
1964
+ showSimpleToast(`\u26A0\uFE0F \u4E0A\u4E0B\u6587\u4F7F\u7528\u7387 ${Math.round((evt.usage?.pct || 0) * 100)}%, \u5373\u5C06\u81EA\u52A8\u538B\u7F29`);
1965
+ } else if (evt.type === "context.compress.start") {
1966
+ showSimpleToast(`\u{1F5DC}\uFE0F \u4E0A\u4E0B\u6587\u538B\u7F29\u5F00\u59CB (${(evt.beforeTokens || 0).toLocaleString()} tokens)`);
1967
+ } else if (evt.type === "context.compress.complete") {
1968
+ const s = evt.snapshot || {};
1969
+ showSimpleToast(`\u2713 \u4E0A\u4E0B\u6587\u5DF2\u538B\u7F29: ${((s.beforeTokens || 0) / 1e3).toFixed(0)}k \u2192 ${((s.afterTokens || 0) / 1e3).toFixed(0)}k tokens`);
1970
+ }
1971
+ } catch (ctxErr) {
1972
+ }
1940
1973
  }
1941
1974
  } catch (err) {
1942
1975
  console.error("[v3] \u5168\u5C40 SSE \u89E3\u6790\u5931\u8D25:", err);
@@ -6,20 +6,20 @@
6
6
  <title>Bolloon Agent</title>
7
7
 
8
8
  <!-- Favicon -->
9
- <link rel="icon" type="image/x-icon" href="/icons/favicon.ico">
10
- <link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32x32.png">
11
- <link rel="icon" type="image/png" sizes="16x16" href="/icons/favicon-16x16.png">
9
+ <link rel="icon" type="image/x-icon" href="./icons/favicon.ico">
10
+ <link rel="icon" type="image/png" sizes="32x32" href="./icons/favicon-32x32.png">
11
+ <link rel="icon" type="image/png" sizes="16x16" href="./icons/favicon-16x16.png">
12
12
 
13
13
  <!-- Apple Touch Icon -->
14
- <link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
14
+ <link rel="apple-touch-icon" href="./icons/apple-touch-icon.png">
15
15
 
16
16
  <!-- PWA Manifest -->
17
- <link rel="manifest" href="/manifest.json">
17
+ <link rel="manifest" href="./manifest.json">
18
18
 
19
19
  <!-- 2026-06-11: 移除 Google Fonts + jsdelivr 外部 CDN 阻塞 — 在大陆/跨公网 timeout 拖慢首屏/返回主页
20
20
  字体: style.css 只用字面量 'JetBrains Mono', 系统有就用, 没有自动 fall back monospace
21
21
  marked/qrcode: 改 async 不阻塞 (下载失败时本地降级到 escape 文本, 不影响主聊天) -->
22
- <link rel="stylesheet" href="/style.css">
22
+ <link rel="stylesheet" href="./style.css">
23
23
  <script async src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
24
24
  <script async src="https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js"></script>
25
25
  </head>
@@ -414,11 +414,11 @@
414
414
  </main>
415
415
  </div>
416
416
 
417
- <script type="module" src="/components/wallet-viem.mjs"></script>
418
- <script type="module" src="/components/p2p/index.js"></script>
419
- <script type="module" src="/ui/step-timeline.js"></script>
420
- <script type="module" src="/ui/message-renderer.js"></script>
417
+ <script type="module" src="./components/wallet-viem.mjs"></script>
418
+ <script type="module" src="./components/p2p/index.js"></script>
419
+ <script type="module" src="./ui/step-timeline.js"></script>
420
+ <script type="module" src="./ui/message-renderer.js"></script>
421
421
  <!-- 2026-07-06: client.js 含 import 语句 (safeChannelName 兜底), 必须 type="module" -->
422
- <script type="module" src="/client.js"></script>
422
+ <script type="module" src="./client.js"></script>
423
423
  </body>
424
424
  </html>