@bolloon/bolloon-agent 0.4.24 → 0.4.26

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.
Files changed (49) hide show
  1. package/dist/agents/execution-supervisor.js +446 -0
  2. package/dist/agents/external-events.js +162 -0
  3. package/dist/agents/goal-criteria.js +124 -0
  4. package/dist/agents/goal-store.js +526 -0
  5. package/dist/agents/pi-harness.js +263 -0
  6. package/dist/agents/pi-sdk.js +607 -126
  7. package/dist/agents/run-store.js +772 -0
  8. package/dist/agents/runner-resolver.js +225 -0
  9. package/dist/agents/skill-readiness.js +133 -0
  10. package/dist/agents/skill-supervisor-link.js +70 -0
  11. package/dist/agents/skills-manager.js +717 -0
  12. package/dist/agents/supervisor-host.js +249 -0
  13. package/dist/cli/setup-wizard.js +96 -127
  14. package/dist/cron/tick-lock.js +1 -1
  15. package/dist/electron/first-run.js +33 -2
  16. package/dist/electron-build/electron/first-run.js +35 -2
  17. package/dist/electron-build/electron/first-run.js.map +1 -1
  18. package/dist/index.js +549 -26
  19. package/dist/ios/agent-delegate-server.js +58 -12
  20. package/dist/ios/icons/icon-1024x1024.png +0 -0
  21. package/dist/ios/icons/icon-1024x1024.webp +0 -0
  22. package/dist/ios/icons/icon-216x216.png +0 -0
  23. package/dist/ios/icons/icon-216x216.webp +0 -0
  24. package/dist/ios/index.html +21 -1
  25. package/dist/ios/manifest.json +1 -1
  26. package/dist/ios/mobile-agent.js +195 -1
  27. package/dist/ios/mobile-core.js +24876 -24723
  28. package/dist/ios/mobile.css +15 -0
  29. package/dist/ios/mobile.html +21 -1
  30. package/dist/ios/mobile.js +143 -0
  31. package/dist/ios/server.js +51 -4
  32. package/dist/llm/config-store.js +35 -4
  33. package/dist/network/agent-network.js +10 -0
  34. package/dist/network/goal-event-bridge.js +57 -0
  35. package/dist/setup/onboard.js +549 -0
  36. package/dist/setup/setup-store.js +592 -0
  37. package/dist/web/icons/icon-1024x1024.png +0 -0
  38. package/dist/web/icons/icon-1024x1024.webp +0 -0
  39. package/dist/web/icons/icon-216x216.png +0 -0
  40. package/dist/web/icons/icon-216x216.webp +0 -0
  41. package/dist/web/manifest.json +1 -1
  42. package/dist/web/mobile-agent.js +2 -2
  43. package/dist/web/mobile-core.js +24884 -24726
  44. package/dist/web/mobile-privacy.js +185 -0
  45. package/dist/web/mobile.css +15 -0
  46. package/dist/web/mobile.html +21 -1
  47. package/dist/web/mobile.js +179 -6
  48. package/dist/web/server.js +633 -0
  49. package/package.json +2 -2
@@ -688,3 +688,18 @@ body {
688
688
  color: var(--bg);
689
689
  font-weight: 600;
690
690
  }
691
+
692
+ /* —— 隐私政策 (2026-09-16) —— */
693
+ .privacy-link {
694
+ display: block; margin: 6px 0 2px; text-align: center;
695
+ color: var(--accent); font-size: 13px; text-decoration: none;
696
+ }
697
+ .privacy-link:active { opacity: .7; }
698
+ .policy-body {
699
+ flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch;
700
+ padding: 14px 16px calc(40px + env(safe-area-inset-bottom));
701
+ }
702
+ .policy-body h3 { font-size: 14px; font-weight: 600; color: var(--text); margin: 18px 0 6px; }
703
+ .policy-body p { font-size: 13px; line-height: 1.75; color: var(--text-secondary); margin: 0 0 10px; }
704
+ .policy-body a { color: var(--accent); word-break: break-all; }
705
+ .policy-meta { font-size: 12px; color: var(--text-muted); }
@@ -43,7 +43,7 @@
43
43
  </header>
44
44
 
45
45
  <!-- 主页面: 双频滑动卡片 -->
46
- <main class="page-container" id="page-main" data-tab="main">
46
+ <main class="page-container" id="page-main" data-tab="main" hidden>
47
47
  <!-- 水平滑动卡片轨道 -->
48
48
  <div class="card-carousel" id="card-carousel">
49
49
  <div class="card-track" id="card-track">
@@ -139,6 +139,26 @@
139
139
  <button class="create-session-btn" id="btn-create-session" hidden>+ 创建新会话</button>
140
140
 
141
141
  <!-- 创建智能体: 底部滑入加载 sheet -->
142
+ <!-- 2026-09-16: 首启隐私同意门 — 上架要求: 用户同意前不读取本机数据 / 不连网 / 不申请任何权限 -->
143
+ <div class="sheet" id="privacy-gate" hidden>
144
+ <div class="sheet-inner">
145
+ <div class="sheet-title" id="privacy-gate-title">隐私政策与权限说明</div>
146
+ <div class="sheet-text" id="privacy-gate-body"></div>
147
+ <a class="privacy-link" href="#" id="privacy-gate-link">阅读完整隐私政策 ›</a>
148
+ <button class="sheet-choice" id="privacy-agree">同意并继续</button>
149
+ <button class="sheet-choice sheet-cancel" id="privacy-decline">不同意</button>
150
+ </div>
151
+ </div>
152
+
153
+ <!-- 2026-09-16: 应用内隐私政策 (全屏, 离线可用; 权威版 = bolloon.cn/privacy.html) -->
154
+ <div class="chat-page" id="policy-page" hidden>
155
+ <div class="chat-topbar">
156
+ <button class="icon-btn" id="policy-back">←</button>
157
+ <div style="flex:1;font-weight:600">隐私政策</div>
158
+ </div>
159
+ <div class="policy-body" id="policy-body"></div>
160
+ </div>
161
+
142
162
  <div class="sheet" id="create-sheet" hidden>
143
163
  <div class="sheet-inner">
144
164
  <div class="spinner"></div>
@@ -1717,6 +1717,9 @@
1717
1717
  <div class="conv-item" id="settings-helia"><span class="list-icon">${ICONS.globe}</span><span>本机 IPFS 节点</span><span class="list-arrow">›</span></div>
1718
1718
  <div class="conv-item" id="settings-selfcard"><span class="list-icon">${ICONS.chip}</span><span id="selfcard-text">显示本机卡片: 开</span></div>
1719
1719
  <div class="conv-item" id="settings-did"><span class="list-icon">${ICONS.idcard}</span><span>DID</span></div>
1720
+ <div class="conv-item" id="settings-privacy"><span class="list-icon">${ICONS.chip}</span><span>隐私政策与个人信息</span><span class="list-arrow">›</span></div>
1721
+ <div class="conv-item" id="settings-wipe"><span class="list-icon">${ICONS.trash}</span><span style="flex:1;min-width:0"><span style="display:block">清除本机数据(注销)</span><span class="conv-preview" style="display:block">删除本机身份、智能体、会话与钱包</span></span><span class="list-arrow">›</span></div>
1722
+ <div class="conv-item" id="settings-filing"><span class="list-icon">${ICONS.idcard}</span><span id="filing-text">APP 备案号:备案办理中</span></div>
1720
1723
  </div>`;
1721
1724
  document.body.appendChild(page);
1722
1725
  applyTheme(resolveThemePref(), false);
@@ -1757,6 +1760,20 @@
1757
1760
  });
1758
1761
  }
1759
1762
  $('#settings-desktop').addEventListener('click', openDesktopSync);
1763
+ // 隐私政策 (双入口之一: 设置页; 另一个是首启同意门)
1764
+ const privacyRow = $('#settings-privacy');
1765
+ if (privacyRow) privacyRow.addEventListener('click', openPrivacyPolicy);
1766
+ // 注销: 清除本机数据
1767
+ const wipeRow = $('#settings-wipe');
1768
+ if (wipeRow) wipeRow.addEventListener('click', () => { void wipeLocalDataFlow(); });
1769
+ // 备案号展示 (未备案时如实显示"备案办理中", 不伪造编号)
1770
+ const filingRow = $('#settings-filing');
1771
+ if (filingRow) {
1772
+ const ft = $('#filing-text');
1773
+ const ptxt = privacyCore() && privacyCore().filingText ? privacyCore().filingText() : '';
1774
+ if (ft && ptxt) ft.textContent = ptxt;
1775
+ filingRow.addEventListener('click', () => { const pp = privacyCore(); alert((pp && pp.filingText ? pp.filingText() : 'APP 备案号:备案办理中') + '\n\n备案信息以工信部备案系统公示为准。'); });
1776
+ }
1760
1777
  // 本机卡片显示开关 (移除后从这里恢复)
1761
1778
  const drawSelfCardToggle = () => {
1762
1779
  const on = (() => { try { return localStorage.getItem(SELF_CARD_HIDDEN_KEY) !== '1'; } catch { return true; } })();
@@ -2842,7 +2859,133 @@
2842
2859
  });
2843
2860
  }
2844
2861
 
2862
+ // ============ 隐私合规 (2026-09-16): 同意门 / 政策页 / 注销 ============
2863
+ // 上架要求: 首次启动必须先展示隐私政策; 用户同意前不得读取本机数据、不得连网、不得申请任何权限。
2864
+ // 所以 init() 只做一件事 —— 决定"先弹门"还是"进应用"; 真正的初始化在 initApp()。
2865
+
2866
+ function privacyCore() {
2867
+ return (window.BolloonCore && window.BolloonCore.privacy) || null;
2868
+ }
2869
+
2870
+ function privacyNeedsConsent() {
2871
+ const p = privacyCore();
2872
+ if (!p || typeof p.needsConsent !== 'function') return true; // 内核未就绪 → 按"需要同意"处理
2873
+ try { return !!p.needsConsent(); } catch (e) { return true; }
2874
+ }
2875
+
2876
+ function escHtml(v) {
2877
+ return String(v == null ? '' : v)
2878
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
2879
+ .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
2880
+ }
2881
+
2882
+ /** 政策摘要 HTML (要素来自内核常量, 单测锁了必填项) */
2883
+ function privacySummaryHtml() {
2884
+ const p = privacyCore();
2885
+ const items = (p && p.summary) || [];
2886
+ return items.map((it) => `<h3>${escHtml(it.title)}</h3><p>${escHtml(it.body)}</p>`).join('');
2887
+ }
2888
+
2889
+ function showPrivacyGate() {
2890
+ const p = privacyCore();
2891
+ const t = (p && p.consentText) || {};
2892
+ const gate = $('#privacy-gate');
2893
+ if (!gate) return;
2894
+ const titleEl = $('#privacy-gate-title'), bodyEl = $('#privacy-gate-body');
2895
+ if (titleEl && t.title) titleEl.textContent = t.title;
2896
+ if (bodyEl) bodyEl.textContent = t.intro || '使用前请先阅读并同意隐私政策。';
2897
+ const linkEl = $('#privacy-gate-link');
2898
+ if (linkEl && t.policyLink) linkEl.textContent = t.policyLink + ' ›';
2899
+ const agreeEl = $('#privacy-agree'), declineEl = $('#privacy-decline');
2900
+ if (agreeEl && t.agree) agreeEl.textContent = t.agree;
2901
+ if (declineEl && t.decline) declineEl.textContent = t.decline;
2902
+ if (linkEl) linkEl.onclick = (e) => { e.preventDefault(); openPrivacyPolicy(); };
2903
+ if (agreeEl) agreeEl.onclick = () => {
2904
+ const ok = p && typeof p.grant === 'function' ? p.grant() : false;
2905
+ if (!ok) { /* 本机写不进去(隐私模式/满盘): 不假装同意, 下次启动会再问 */ }
2906
+ hideSheet('#privacy-gate');
2907
+ initApp();
2908
+ };
2909
+ if (declineEl) declineEl.onclick = () => showPrivacyDeclined();
2910
+ gate.hidden = false;
2911
+ }
2912
+
2913
+ /** 不同意 → 停在说明页: 不初始化功能、不收集任何东西, 但也不强制退出应用 */
2914
+ function showPrivacyDeclined() {
2915
+ const p = privacyCore();
2916
+ const t = (p && p.consentText) || {};
2917
+ const gate = $('#privacy-gate');
2918
+ if (!gate) return;
2919
+ const titleEl = $('#privacy-gate-title'), bodyEl = $('#privacy-gate-body');
2920
+ if (titleEl) titleEl.textContent = t.declinedTitle || '未同意';
2921
+ if (bodyEl) bodyEl.textContent = t.declinedBody || '未同意隐私政策时应用不会收集任何信息。';
2922
+ const linkEl = $('#privacy-gate-link');
2923
+ if (linkEl) { linkEl.textContent = (t.declinedReread || '重新阅读') + ' ›'; linkEl.onclick = (e) => { e.preventDefault(); showPrivacyGate(); }; }
2924
+ const agreeEl = $('#privacy-agree'), declineEl = $('#privacy-decline');
2925
+ if (agreeEl) { agreeEl.textContent = t.agree || '同意并继续'; agreeEl.onclick = () => { const ok = p && p.grant ? p.grant() : false; if (!ok) {} hideSheet('#privacy-gate'); initApp(); }; }
2926
+ if (declineEl) {
2927
+ declineEl.textContent = t.declinedExit || '退出应用';
2928
+ declineEl.onclick = () => {
2929
+ const cap = window.Capacitor;
2930
+ if (cap && cap.Plugins && cap.Plugins.App && cap.Plugins.App.exitApp) { try { cap.Plugins.App.exitApp(); return; } catch (e) {} }
2931
+ alert('请手动退出应用(返回键 / 上滑关闭)。未同意隐私政策期间应用不会收集任何信息。');
2932
+ };
2933
+ }
2934
+ gate.hidden = false;
2935
+ }
2936
+
2937
+ /** 应用内完整政策 (全屏, 离线可用)。权威版永远是 bolloon.cn/privacy.html (商店表单填的就是它) */
2938
+ function openPrivacyPolicy() {
2939
+ const p = privacyCore();
2940
+ const page = $('#policy-page');
2941
+ const bodyEl = $('#policy-body');
2942
+ if (!page || !bodyEl) return;
2943
+ const t = (p && p.consentText) || {};
2944
+ const url = (p && p.policyUrl) || 'https://bolloon.cn/privacy.html';
2945
+ const contact = (p && p.contact) || '';
2946
+ const filing = p && typeof p.filingText === 'function' ? p.filingText() : '';
2947
+ bodyEl.innerHTML = `
2948
+ <p class="policy-meta">${escHtml(t.intro || '')}</p>
2949
+ ${privacySummaryHtml()}
2950
+ <h3>完整政策与备案信息</h3>
2951
+ <p>在线完整版:<a href="${escHtml(url)}" target="_blank" rel="noopener">${escHtml(url)}</a></p>
2952
+ <p>${escHtml(filing)}</p>
2953
+ <p class="policy-meta">联系方式:${escHtml(contact)}(承诺 7 个工作日内答复)</p>
2954
+ <p class="policy-meta">本页内容与在线版本一致;如两者不一致,以在线版本为准。</p>`;
2955
+ const back = $('#policy-back');
2956
+ if (back) back.onclick = () => { page.hidden = true; };
2957
+ page.hidden = false;
2958
+ }
2959
+
2960
+ /** 注销: 清除本机全部数据 (身份 DID / 智能体 / 会话消息 / 支付 / 钱包) */
2961
+ async function wipeLocalDataFlow() {
2962
+ if (!confirm('确定清除本机数据(注销)?\n\n将删除本机上的:身份标识(DID)、智能体、会话与消息、钱包账本。\n已写入公开区块链的交易记录无法删除。\n\n此操作不可撤销。')) return;
2963
+ const p = privacyCore();
2964
+ if (!p || typeof p.wipe !== 'function') { alert('清除失败:本机内核未就绪,请重启应用后重试。'); return; }
2965
+ showToast('正在清除本机数据…');
2966
+ try {
2967
+ const r = await p.wipe();
2968
+ const failed = (r && r.failed) || [];
2969
+ const deleted = (r && r.deletedDatabases) || [];
2970
+ const cleared = (r && r.clearedStorageKeys) || [];
2971
+ if (failed.length) {
2972
+ alert(`部分数据未能清除(可重试):\n${failed.join('\n')}\n\n已清除 ${deleted.length} 个数据库 / ${cleared.length} 个本机键。`);
2973
+ } else {
2974
+ alert(`${p.wipeNotice || '已清除本机数据。'}\n\n(本次删除 ${deleted.length} 个数据库、${cleared.length} 个本机键)`);
2975
+ }
2976
+ location.reload(); // 回到首启状态 (同意记录保留, 不再重复弹门)
2977
+ } catch (e) {
2978
+ alert('清除失败:' + ((e && e.message) || e));
2979
+ }
2980
+ }
2981
+
2845
2982
  function init() {
2983
+ // 首启/政策版本变更 → 先弹同意门, 不做任何初始化 (不读本机数据 / 不连网 / 不申请权限)
2984
+ if (privacyNeedsConsent()) { showPrivacyGate(); return; }
2985
+ initApp();
2986
+ }
2987
+
2988
+ function initApp() {
2846
2989
  bindMenu();
2847
2990
  applyTheme(resolveThemePref(), false);
2848
2991
  switchTab('main');
@@ -3020,14 +3020,61 @@ ${goalDesc}
3020
3020
  // 2026-09-15: agent-delegate (manifest 协议 + agent_delegate) **启动即挂载**
3021
3021
  // 文档 bolloon-gateway-join.md 第 3 节要求 POST /api/agent/register 可用;
3022
3022
  // 之前只在 iroh 懒初始化 (/api/iroh/info 首次访问) 的收尾挂上 → 没触发时恒 404。
3023
+ //
3024
+ // 2026-09-15 (二修): 同时注入**真执行器** —— 被委派的一方不再是
3025
+ // `resultCid: mock-<ts>` 的假签收: 真的跑本地 agent session 出结果,
3026
+ // 并把结果存进 OrbitDB 得到真实内容寻址 CID; 无活跃 channel / 无 LLM 时如实报错。
3027
+ const delegateExecutor = async (req) => {
3028
+ try {
3029
+ const channels = await loadChannels();
3030
+ const active = (() => {
3031
+ try {
3032
+ const raw = fsSync.existsSync(path.join(process.env.HOME || '/tmp', '.bolloon', 'active-channel.json'))
3033
+ ? fsSync.readFileSync(path.join(process.env.HOME || '/tmp', '.bolloon', 'active-channel.json'), 'utf-8') : '';
3034
+ return raw ? JSON.parse(raw) : null;
3035
+ }
3036
+ catch {
3037
+ return null;
3038
+ }
3039
+ })();
3040
+ const ch = channels.find((c) => c.id === active?.channelId) || channels.find((c) => c.id === active?.id) || channels[0];
3041
+ if (!ch)
3042
+ return { ok: false, summary: 'no local channel to execute with', error: 'no-channel' };
3043
+ const agent = await getAgentForChannel(ch.id, ch.publicKey, ch.name, ch.didDocument);
3044
+ const task = [
3045
+ `【被委派任务 · 能力 ${req.capability}】`,
3046
+ req.instruction,
3047
+ req.docPath ? `\n资料路径: ${req.docPath}` : '',
3048
+ req.docContent ? `\n资料内容:\n${req.docContent}` : '',
3049
+ `\n(来自 agent ${req.fromAgentId || 'unknown'}),请直接给出可交付结果。`,
3050
+ ].join('');
3051
+ const out = await agent.prompt(task);
3052
+ const text = String(out || '').trim();
3053
+ if (!text || text.startsWith('❌') || text.startsWith('[AI 服务调用失败]')) {
3054
+ return { ok: false, summary: text.slice(0, 800) || '(空结果)', error: 'execution-failed' };
3055
+ }
3056
+ // 真结果 → 内容寻址存储 (CID), 失败不编造 CID, 只回摘要
3057
+ let cid;
3058
+ try {
3059
+ const { getCIDDatabase } = await import('../orbitdb/cid-database.js');
3060
+ const rec = await getCIDDatabase().save({ agentId: req.targetAgentId, type: 'context', content: text, metadata: { capability: req.capability, fromAgentId: req.fromAgentId, kind: 'delegate-result' } });
3061
+ cid = rec?.id;
3062
+ }
3063
+ catch { /* 存不上就不给 CID */ }
3064
+ return { ok: true, summary: text.slice(0, 4000), resultCid: cid };
3065
+ }
3066
+ catch (e) {
3067
+ return { ok: false, summary: `delegate executor failed: ${String(e?.message || e).slice(0, 300)}`, error: 'executor-error' };
3068
+ }
3069
+ };
3023
3070
  let agentDelegateMounted = false;
3024
3071
  try {
3025
3072
  const delegateTransport = createIrohDelegateTransport({ verbose: true });
3026
3073
  // 注意: createAgentDelegateApp 内部声明的是绝对路径 (/api/agent/...),
3027
3074
  // 所以挂载时**不能**再带 '/api/agent' 前缀 (否则变成 /api/agent/api/agent/... 恒 404)
3028
- app.use(createAgentDelegateApp(delegateTransport));
3075
+ app.use(createAgentDelegateApp(delegateTransport, { execute: delegateExecutor }));
3029
3076
  agentDelegateMounted = true;
3030
- console.log('[agent-delegate] 已挂载到 /api/agent (启动即用: local-manifest / register / pick / delegate)');
3077
+ console.log('[agent-delegate] 已挂载到 /api/agent (启动即用: local-manifest / register / pick / delegate, 真执行器已注入)');
3031
3078
  }
3032
3079
  catch (e) {
3033
3080
  console.warn('[agent-delegate] 挂载失败 (非致命):', e?.message);
@@ -6588,10 +6635,10 @@ ${goalDesc}
6588
6635
  if (!agentDelegateMounted) {
6589
6636
  try {
6590
6637
  const delegateTransport = createIrohDelegateTransport({ verbose: true });
6591
- const delegateApp = createAgentDelegateApp(delegateTransport);
6638
+ const delegateApp = createAgentDelegateApp(delegateTransport, { execute: delegateExecutor });
6592
6639
  app.use(delegateApp);
6593
6640
  agentDelegateMounted = true;
6594
- console.log('[iroh API] agent-delegate app 已挂载到 /api/agent (补挂)');
6641
+ console.log('[iroh API] agent-delegate app 已挂载到 /api/agent (补挂, 真执行器已注入)');
6595
6642
  }
6596
6643
  catch (e) {
6597
6644
  console.error('[iroh API] 挂载 agent-delegate app 失败:', e);
@@ -4,11 +4,19 @@
4
4
  */
5
5
  import * as fs from 'fs/promises';
6
6
  import * as path from 'path';
7
- const CONFIG_DIR = path.join(process.env.HOME || '/tmp', '.bolloon');
7
+ import * as os from 'os';
8
+ import * as fsSync from 'fs';
9
+ /** 2026-09-16 (M1): 不再在模块加载时固定 HOME —— 独立宿主/测试注入/长驻进程都走同一个解析 */
10
+ function configDir() {
11
+ const env = process.env.BOLLOON_HOME?.trim();
12
+ if (env)
13
+ return env;
14
+ return path.join(process.env.HOME || os.homedir() || '/tmp', '.bolloon');
15
+ }
8
16
  // 2026-08-07: llm-config.json → bolloon-config.json (统一配置文件名, Bolloon 自己有写权限)
9
17
  // initialize() 会做一次迁移: 旧文件存在且新文件不存在 → 复制旧内容, 之后统一读写新文件
10
- const CONFIG_PATH = path.join(CONFIG_DIR, 'bolloon-config.json');
11
- const LEGACY_CONFIG_PATH = path.join(CONFIG_DIR, 'llm-config.json');
18
+ const CONFIG_PATH = path.join(configDir(), 'bolloon-config.json');
19
+ const LEGACY_CONFIG_PATH = path.join(configDir(), 'llm-config.json');
12
20
  export const DEFAULT_PROVIDER_CONFIGS = {
13
21
  openai: {
14
22
  enabled: false,
@@ -231,6 +239,10 @@ function getDefaultConfig() {
231
239
  class LLMConfigStore {
232
240
  config = null;
233
241
  initialized = false;
242
+ /** 2026-09-16: 记住加载时用的配置目录 —— 目录变了必须丢弃缓存, 否则会把 A 目录的配置写到 B 目录 */
243
+ loadedDir = null;
244
+ /** 2026-09-16: 记住加载时的文件签名 —— 外部改了配置 (repair/agent 工具/用户手改) 必须重新读, 不能一直用旧值 */
245
+ loadedSig = null;
234
246
  // v0.2.15: single-flight lock around read-modify-write of `~/.bolloon/llm-config.json`.
235
247
  // Prevents concurrent save() calls from clobbering each other when the user
236
248
  // configures two providers back-to-back (e.g. saving gemini, then anthropic, in
@@ -244,10 +256,19 @@ class LLMConfigStore {
244
256
  return next;
245
257
  }
246
258
  async initialize() {
259
+ // 目录变了 (换 HOME / 独立宿主 / 测试注入): 内存缓存作废, 重新读盘
260
+ const dir = configDir();
261
+ const sig = this.fileSignature();
262
+ if (this.initialized && (this.loadedDir !== dir || this.loadedSig !== sig)) {
263
+ this.initialized = false;
264
+ this.config = null;
265
+ }
247
266
  if (this.initialized)
248
267
  return;
268
+ this.loadedDir = dir;
269
+ this.loadedSig = sig;
249
270
  try {
250
- await fs.mkdir(CONFIG_DIR, { recursive: true });
271
+ await fs.mkdir(configDir(), { recursive: true });
251
272
  // 2026-08-07: 迁移 — 旧 llm-config.json 存在且新 bolloon-config.json 不存在时复制旧内容
252
273
  try {
253
274
  await fs.access(CONFIG_PATH);
@@ -289,6 +310,16 @@ class LLMConfigStore {
289
310
  // 2026-08-07: mode 0o600 — 仅当前用户可读写 (含 API key 的敏感配置); Bolloon 自身进程可写
290
311
  await fs.writeFile(CONFIG_PATH, JSON.stringify(this.config, null, 2), { mode: 0o600 });
291
312
  }
313
+ /** 配置文件签名 (mtime+size); 不存在时返回 'missing' */
314
+ fileSignature() {
315
+ try {
316
+ const st = fsSync.statSync(configDir() + '/bolloon-config.json');
317
+ return `${st.mtimeMs}:${st.size}`;
318
+ }
319
+ catch {
320
+ return 'missing';
321
+ }
322
+ }
292
323
  async getConfig() {
293
324
  await this.initialize();
294
325
  return { ...this.config };
@@ -619,6 +619,16 @@ export class AgentMessaging {
619
619
  console.warn(`[Messaging] Signature verification failed for ${signedMsg.from.substring(0, 20)}`);
620
620
  return false;
621
621
  }
622
+ // 2026-09-16 (2-C.4): 目标关联的真实外部事件 —— 签名已通过校验, 这里再按
623
+ // 来源 / correlation / 过期 / eventId 去重做一次严格匹配, 只唤醒"正在等这个事件"的 Goal。
624
+ // **不在这里启动 agent**: 只写事实 + 唤醒, 由 Supervisor 下一轮继续。
625
+ try {
626
+ const { tryDeliverGoalEvent } = await import('./goal-event-bridge.js');
627
+ await tryDeliverGoalEvent(signedMsg, fromPeerId);
628
+ }
629
+ catch (e) {
630
+ console.warn(`[Messaging] goal event bridge 失败 (不影响普通消息):`, e?.message);
631
+ }
622
632
  const handler = this.messageHandlers.get(signedMsg.type);
623
633
  if (handler) {
624
634
  handler(data, fromPeerId, signedMsg.from);
@@ -0,0 +1,57 @@
1
+ /**
2
+ * goal-event-bridge.ts — 把"真实入站的签名消息"翻译成 Goal 外部事件 (批次 2-C.4)
3
+ *
4
+ * 只有签名已通过校验的消息才进到这里 (来源 = 对端 DID)。这里只做解析 + 投递,
5
+ * 启动执行由 Supervisor 决定 —— 事件处理器不碰 agent。
6
+ *
7
+ * 识别的两种真实来源:
8
+ * · delegate 回包: type 含 `delegate` 或 payload 里有 delegateId/resultCid → source='delegate'
9
+ * · P2P 协作回复: 其它带 goalId/requestId/continuationId 的消息 → source='p2p'
10
+ */
11
+ import { deliverExternalEvent } from '../agents/external-events.js';
12
+ function parsePayload(raw) {
13
+ if (!raw)
14
+ return null;
15
+ if (typeof raw === 'object')
16
+ return raw;
17
+ try {
18
+ return JSON.parse(String(raw));
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
24
+ /** 从 payload 里提取目标关联字段 (支持 goalEvent 包裹或平铺两种写法) */
25
+ export function extractGoalEvent(signed, fromDid) {
26
+ const body = parsePayload(signed.payload);
27
+ if (!body || typeof body !== 'object')
28
+ return null;
29
+ const env = (body.goalEvent && typeof body.goalEvent === 'object') ? body.goalEvent : body;
30
+ const goalId = env.goalId || env.goal_id;
31
+ const requestId = env.requestId || env.request_id || env.correlationId;
32
+ const continuationId = env.continuationId || env.continuation_id;
33
+ const eventId = env.eventId || env.event_id;
34
+ if (!goalId && !requestId && !continuationId)
35
+ return null; // 与 Goal 无关 → 普通消息
36
+ if (!eventId)
37
+ return null; // 没有 eventId 无法去重 → 不当事件处理
38
+ const isDelegate = /delegate/i.test(String(signed.type)) || !!(env.delegateId || env.resultCid);
39
+ return {
40
+ source: isDelegate ? 'delegate' : 'p2p',
41
+ eventId: String(eventId),
42
+ requestId: requestId ? String(requestId) : undefined,
43
+ continuationId: continuationId ? String(continuationId) : undefined,
44
+ goalId: goalId ? String(goalId) : undefined,
45
+ fromDid: fromDid || signed.from,
46
+ eventName: env.event || env.eventName || env.kind,
47
+ payload: env.result ?? env.payload ?? body,
48
+ };
49
+ }
50
+ /** 投递 (未匹配/不合法 → 返回 matched:false, 调用方继续走普通消息处理) */
51
+ export async function tryDeliverGoalEvent(signed, fromDid) {
52
+ const event = extractGoalEvent(signed, fromDid);
53
+ if (!event)
54
+ return { matched: false };
55
+ const res = await deliverExternalEvent(event);
56
+ return { matched: res.ok, reason: res.reason, goalId: res.goalId };
57
+ }