@bolloon/bolloon-agent 0.4.12 → 0.4.14

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.
@@ -1771,6 +1771,275 @@
1771
1771
  notice.remove();
1772
1772
  }, 15e3);
1773
1773
  }
1774
+ var gwGroups = [];
1775
+ var gwNetworks = [];
1776
+ var gwActiveChat = null;
1777
+ var gwMsgCache = /* @__PURE__ */ new Map();
1778
+ function gwToast(text) {
1779
+ try {
1780
+ window.showToast?.(text);
1781
+ } catch {
1782
+ }
1783
+ console.log("[gateway]", text);
1784
+ }
1785
+ function gwEsc(s) {
1786
+ try {
1787
+ return escapeHtml2(String(s ?? ""));
1788
+ } catch {
1789
+ return String(s ?? "");
1790
+ }
1791
+ }
1792
+ function gwModal(html) {
1793
+ const overlay = document.createElement("div");
1794
+ overlay.className = "gw-modal-overlay";
1795
+ overlay.innerHTML = `<div class="gw-modal">${html}<div class="gw-modal-close" title="\u5173\u95ED">\u2715</div></div>`;
1796
+ document.body.appendChild(overlay);
1797
+ const close = () => overlay.remove();
1798
+ overlay.querySelector(".gw-modal-close")?.addEventListener("click", close);
1799
+ overlay.addEventListener("click", (e) => {
1800
+ if (e.target === overlay) close();
1801
+ });
1802
+ return { root: overlay, close };
1803
+ }
1804
+ async function loadGatewaySidebar() {
1805
+ try {
1806
+ const [g, n] = await Promise.all([
1807
+ fetch("/api/gateway/groups").then((r) => r.json()).catch(() => ({ groups: [] })),
1808
+ fetch("/api/gateway/status").then((r) => r.json()).catch(() => ({ networks: [] }))
1809
+ ]);
1810
+ gwGroups = Array.isArray(g.groups) ? g.groups : [];
1811
+ gwNetworks = Array.isArray(n.networks) ? n.networks : [];
1812
+ renderGatewaySidebar();
1813
+ } catch {
1814
+ }
1815
+ }
1816
+ function renderGatewaySidebar() {
1817
+ const list = document.getElementById("gateway-list");
1818
+ if (!list) return;
1819
+ list.innerHTML = "";
1820
+ for (const grp of gwGroups) {
1821
+ const li = document.createElement("li");
1822
+ li.className = "gateway-item";
1823
+ li.innerHTML = `<span class="gateway-item-icon">\u{1F465}</span>
1824
+ <span class="gateway-item-main">
1825
+ <span class="gateway-item-name">${gwEsc(grp.name || "\u7FA4\u7EC4")}</span>
1826
+ <span class="gateway-item-sub">${grp.memberCount ?? 0} \u6210\u5458 \xB7 ${grp.messageCount ?? 0} \u6761\u6D88\u606F</span>
1827
+ </span>`;
1828
+ li.title = "\u6253\u5F00\u7FA4\u804A";
1829
+ li.onclick = () => openGroupChat(grp.id, grp.name || grp.id);
1830
+ list.appendChild(li);
1831
+ }
1832
+ for (const net of gwNetworks) {
1833
+ const li = document.createElement("li");
1834
+ li.className = "gateway-item gateway-item-net";
1835
+ li.innerHTML = `<span class="gateway-item-icon">\u{1F310}</span>
1836
+ <span class="gateway-item-main">
1837
+ <span class="gateway-item-name">${gwEsc(net.name || String(net.link || "").slice(0, 24) || "\u7F51\u7EDC")}</span>
1838
+ <span class="gateway-item-sub">${net.serviceCount ?? 0} \u4E2A\u670D\u52A1</span>
1839
+ </span>`;
1840
+ li.title = `\u670D\u52A1\u7F51\u7EDC: ${net.link || ""}`;
1841
+ list.appendChild(li);
1842
+ }
1843
+ if (!gwGroups.length && !gwNetworks.length) {
1844
+ list.innerHTML = '<li class="sidebar-empty-hint">(\u6682\u65E0\u7F51\u7EDC, \u70B9 + \u52A0\u5165)</li>';
1845
+ }
1846
+ }
1847
+ function openGatewayJoinModal() {
1848
+ const { root, close } = gwModal(`
1849
+ <div class="gw-join">
1850
+ <h3>\u52A0\u5165 Agent \u7F51\u7EDC / \u7FA4\u7EC4</h3>
1851
+ <p class="gw-hint">\u7C98\u8D34\u9080\u8BF7\u94FE\u63A5 (orbitdb://...), \u81EA\u52A8\u8BC6\u522B\u670D\u52A1\u7F51\u7EDC\u6216\u7FA4\u7EC4.</p>
1852
+ <input class="gw-join-input" placeholder="orbitdb:///orbitdb/...?type=group&name=..." />
1853
+ <div class="gw-join-actions">
1854
+ <button class="gw-join-go">\u52A0\u5165</button>
1855
+ <button class="gw-join-my">\u{1F517} \u6211\u7684\u7F51\u7EDC\u94FE\u63A5</button>
1856
+ </div>
1857
+ <div class="gw-join-result"></div>
1858
+ </div>`);
1859
+ const input2 = root.querySelector(".gw-join-input");
1860
+ const go = root.querySelector(".gw-join-go");
1861
+ const my = root.querySelector(".gw-join-my");
1862
+ const result = root.querySelector(".gw-join-result");
1863
+ go.onclick = async () => {
1864
+ const link = String(input2.value || "").trim();
1865
+ if (!link) {
1866
+ result.textContent = "\u8BF7\u8F93\u5165\u94FE\u63A5";
1867
+ return;
1868
+ }
1869
+ if (link.includes("type=group")) {
1870
+ const r = await fetch("/api/gateway/groups/join", {
1871
+ method: "POST",
1872
+ headers: { "Content-Type": "application/json" },
1873
+ body: JSON.stringify({ link })
1874
+ }).then((x) => x.json()).catch(() => ({ ok: false, error: "\u8BF7\u6C42\u5931\u8D25" }));
1875
+ if (r.ok) {
1876
+ result.textContent = r.already ? "\u5DF2\u5728\u8BE5\u7FA4\u7EC4\u4E2D \u2713" : `\u2705 \u5DF2\u52A0\u5165\u7FA4\u7EC4\u300C${r.group?.name || ""}\u300D`;
1877
+ close();
1878
+ loadGatewaySidebar();
1879
+ } else result.textContent = `\u274C ${r.error || "\u52A0\u5165\u5931\u8D25"}`;
1880
+ } else {
1881
+ const r = await fetch("/api/gateway/join", {
1882
+ method: "POST",
1883
+ headers: { "Content-Type": "application/json" },
1884
+ body: JSON.stringify({ link })
1885
+ }).then((x) => x.json()).catch(() => ({ ok: false, error: "\u8BF7\u6C42\u5931\u8D25" }));
1886
+ if (r.ok) {
1887
+ result.textContent = r.already ? "\u5DF2\u5728\u8BE5\u7F51\u7EDC\u4E2D \u2713" : `\u2705 \u5DF2\u52A0\u5165\u7F51\u7EDC (${r.total} \u670D\u52A1, \u65B0\u589E ${r.joined})`;
1888
+ close();
1889
+ loadGatewaySidebar();
1890
+ } else result.textContent = `\u274C ${r.error || "\u52A0\u5165\u5931\u8D25"}`;
1891
+ }
1892
+ };
1893
+ my.onclick = async () => {
1894
+ const r = await fetch("/api/gateway/link").then((x) => x.json()).catch(() => null);
1895
+ if (r?.link) {
1896
+ try {
1897
+ await navigator.clipboard.writeText(r.link);
1898
+ } catch {
1899
+ }
1900
+ result.innerHTML = `\u6211\u7684\u7F51\u7EDC\u9080\u8BF7\u94FE\u63A5\u5DF2\u590D\u5236:<br><code>${gwEsc(r.link)}</code>`;
1901
+ } else result.textContent = "\u274C \u751F\u6210\u94FE\u63A5\u5931\u8D25 (OrbitDB \u672A\u5C31\u7EEA)";
1902
+ };
1903
+ }
1904
+ function openGatewayNewGroupModal() {
1905
+ const { root, close } = gwModal(`
1906
+ <div class="gw-join">
1907
+ <h3>\u521B\u5EFA\u7FA4\u7EC4</h3>
1908
+ <p class="gw-hint">\u7FA4\u7EC4 = \u5FAE\u4FE1\u5F0F\u7FA4\u804A, \u5206\u4EAB\u94FE\u63A5\u9080\u8BF7\u5176\u4ED6 Agent / Bolloon \u52A0\u5165.</p>
1909
+ <input class="gw-join-input" placeholder="\u7FA4\u7EC4\u540D (\u5982: \u7814\u7A76\u534F\u4F5C\u7EC4)" />
1910
+ <div class="gw-join-actions"><button class="gw-join-go">\u521B\u5EFA</button></div>
1911
+ <div class="gw-join-result"></div>
1912
+ </div>`);
1913
+ const input2 = root.querySelector(".gw-join-input");
1914
+ const go = root.querySelector(".gw-join-go");
1915
+ const result = root.querySelector(".gw-join-result");
1916
+ go.onclick = async () => {
1917
+ const name = String(input2.value || "").trim();
1918
+ if (!name) {
1919
+ result.textContent = "\u8BF7\u8F93\u5165\u7FA4\u7EC4\u540D";
1920
+ return;
1921
+ }
1922
+ const r = await fetch("/api/gateway/groups", {
1923
+ method: "POST",
1924
+ headers: { "Content-Type": "application/json" },
1925
+ body: JSON.stringify({ name, from: "web-user" })
1926
+ }).then((x) => x.json()).catch(() => ({ ok: false, error: "\u8BF7\u6C42\u5931\u8D25" }));
1927
+ if (r.ok) {
1928
+ result.innerHTML = `\u2705 \u7FA4\u7EC4\u5DF2\u521B\u5EFA.<br><b>\u9080\u8BF7\u94FE\u63A5:</b><br><code>${gwEsc(r.group?.link)}</code><br>\u590D\u5236\u5206\u4EAB\u7ED9\u5176\u4ED6 Bolloon/Agent.`;
1929
+ loadGatewaySidebar();
1930
+ } else result.textContent = `\u274C ${r.error || "\u521B\u5EFA\u5931\u8D25"}`;
1931
+ };
1932
+ }
1933
+ function openGroupChat(id, name) {
1934
+ gwActiveChat = { id, name };
1935
+ const { root, close } = gwModal(`
1936
+ <div class="gw-chat">
1937
+ <div class="gw-chat-header">
1938
+ <b>\u{1F465} ${gwEsc(name)}</b>
1939
+ <span class="gw-chat-actions">
1940
+ <button class="gw-chat-invite" title="\u590D\u5236\u7FA4\u9080\u8BF7\u94FE\u63A5">\u{1F517} \u9080\u8BF7</button>
1941
+ </span>
1942
+ </div>
1943
+ <div class="gw-chat-msgs" id="gw-msgs-${id}"><div class="gw-msg-empty">\u52A0\u8F7D\u4E2D...</div></div>
1944
+ <div class="gw-chat-input-row">
1945
+ <input class="gw-chat-input" id="gw-input-${id}" placeholder="\u53D1\u6D88\u606F\u7ED9\u7FA4\u6210\u5458... (Enter \u53D1\u9001)" />
1946
+ <button class="gw-chat-send">\u53D1\u9001</button>
1947
+ </div>
1948
+ </div>`);
1949
+ root.querySelector(".gw-chat-invite")?.addEventListener("click", async () => {
1950
+ const r = await fetch(`/api/gateway/groups/${id}/link`).then((x) => x.json()).catch(() => null);
1951
+ if (r?.link) {
1952
+ try {
1953
+ await navigator.clipboard.writeText(r.link);
1954
+ } catch {
1955
+ }
1956
+ gwToast("\u7FA4\u9080\u8BF7\u94FE\u63A5\u5DF2\u590D\u5236, \u5206\u4EAB\u7ED9\u5176\u4ED6 Agent");
1957
+ } else gwToast("\u83B7\u53D6\u94FE\u63A5\u5931\u8D25");
1958
+ });
1959
+ const sendBtn2 = root.querySelector(".gw-chat-send");
1960
+ const input2 = root.querySelector(".gw-chat-input");
1961
+ sendBtn2.onclick = () => {
1962
+ void sendGroupMessage(id);
1963
+ };
1964
+ input2.onkeydown = (e) => {
1965
+ if (e.key === "Enter") {
1966
+ e.preventDefault();
1967
+ void sendGroupMessage(id);
1968
+ }
1969
+ };
1970
+ void loadGroupMessages(id);
1971
+ }
1972
+ async function loadGroupMessages(id) {
1973
+ const box = document.getElementById(`gw-msgs-${id}`);
1974
+ if (!box) return;
1975
+ const r = await fetch(`/api/gateway/groups/${id}/messages?limit=50`).then((x) => x.json()).catch(() => ({ messages: [] }));
1976
+ gwMsgCache.set(id, Array.isArray(r.messages) ? r.messages : []);
1977
+ renderGroupMessages(id);
1978
+ }
1979
+ function renderGroupMessages(id) {
1980
+ const box = document.getElementById(`gw-msgs-${id}`);
1981
+ if (!box) return;
1982
+ const msgs = gwMsgCache.get(id) || [];
1983
+ const myName = (document.getElementById("user-name")?.textContent || "web-user").trim();
1984
+ box.innerHTML = msgs.map((m) => {
1985
+ const mine = m.from === myName || m.from === "web-user" || m.from === "me";
1986
+ const t = new Date(m.ts).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
1987
+ return `<div class="gw-msg ${mine ? "gw-msg-mine" : ""}">
1988
+ <div class="gw-msg-meta"><b>${gwEsc(String(m.from).slice(0, 24))}</b> \xB7 ${t}</div>
1989
+ <div class="gw-msg-text">${gwEsc(String(m.text))}</div>
1990
+ </div>`;
1991
+ }).join("") || '<div class="gw-msg-empty">\u7FA4\u8FD8\u6CA1\u6709\u6D88\u606F, \u8BF4\u70B9\u4EC0\u4E48\u5427 \u{1F44B}</div>';
1992
+ box.scrollTop = box.scrollHeight;
1993
+ }
1994
+ async function sendGroupMessage(id) {
1995
+ const input2 = document.getElementById(`gw-input-${id}`);
1996
+ const text = input2?.value?.trim();
1997
+ if (!text) return;
1998
+ const myName = (document.getElementById("user-name")?.textContent || "web-user").trim();
1999
+ const r = await fetch(`/api/gateway/groups/${id}/message`, {
2000
+ method: "POST",
2001
+ headers: { "Content-Type": "application/json" },
2002
+ body: JSON.stringify({ text, from: myName })
2003
+ }).then((x) => x.json()).catch(() => ({ ok: false, error: "\u8BF7\u6C42\u5931\u8D25" }));
2004
+ if (r.ok) {
2005
+ if (input2) input2.value = "";
2006
+ void loadGroupMessages(id);
2007
+ } else gwToast(r.error || "\u53D1\u9001\u5931\u8D25");
2008
+ }
2009
+ function handleGroupMessage(msg) {
2010
+ const { groupId, message, groupName } = msg || {};
2011
+ if (!groupId || !message) return;
2012
+ const cur = gwActiveChat;
2013
+ if (cur && cur.id === groupId) {
2014
+ const cache = gwMsgCache.get(groupId) || [];
2015
+ cache.push(message);
2016
+ gwMsgCache.set(groupId, cache.slice(-100));
2017
+ renderGroupMessages(groupId);
2018
+ } else {
2019
+ gwToast(`\u{1F4E2} ${groupName || "\u7FA4\u7EC4"} \u65B0\u6D88\u606F: ${String(message.text || "").slice(0, 40)}`);
2020
+ }
2021
+ void loadGatewaySidebar();
2022
+ }
2023
+ document.addEventListener("DOMContentLoaded", () => {
2024
+ const addBtn = document.getElementById("gateway-add-btn");
2025
+ if (addBtn) addBtn.onclick = openGatewayJoinModal;
2026
+ const ngBtn = document.getElementById("gateway-new-group-btn");
2027
+ if (ngBtn) ngBtn.onclick = openGatewayNewGroupModal;
2028
+ const gHeader = document.getElementById("gateway-header");
2029
+ if (gHeader) gHeader.onclick = () => {
2030
+ const t = document.getElementById("gateway-toggle");
2031
+ const list = document.getElementById("gateway-list");
2032
+ if (t && list) {
2033
+ const open = t.textContent === "\u25BC";
2034
+ t.textContent = open ? "\u25B6" : "\u25BC";
2035
+ list.style.display = open ? "none" : "";
2036
+ }
2037
+ };
2038
+ void loadGatewaySidebar();
2039
+ setInterval(() => {
2040
+ void loadGatewaySidebar();
2041
+ }, 3e4);
2042
+ });
1774
2043
  var v3GlobalEventSource = null;
1775
2044
  function startV3GlobalSSE() {
1776
2045
  if (v3GlobalEventSource) return;
@@ -1779,6 +2048,10 @@
1779
2048
  v3GlobalEventSource.onmessage = (e) => {
1780
2049
  try {
1781
2050
  const msg = JSON.parse(e.data);
2051
+ if (msg.type === "group-message") {
2052
+ handleGroupMessage(msg);
2053
+ return;
2054
+ }
1782
2055
  if (msg.type === "remote-chat-sent") {
1783
2056
  console.log("[v3] \u6536\u5230 remote-chat-sent:", msg.channelId, "|", String(msg.text || "").slice(0, 30));
1784
2057
  try {
@@ -90,6 +90,23 @@
90
90
  </ul>
91
91
  </div>
92
92
 
93
+ <!-- 2026-08-14: Agent 网络 (Gateway) — 群组群聊 + 服务网络 -->
94
+ <div class="sidebar-section" id="gateway-section">
95
+ <div class="section-header is-compact is-clickable" id="gateway-header">
96
+ <span class="section-header-row">
97
+ <span id="gateway-toggle">▼</span>
98
+ <span class="section-title">Agent 网络</span>
99
+ </span>
100
+ <span class="section-header-actions">
101
+ <button id="gateway-new-group-btn" class="sidebar-mini-btn" title="创建群组 (微信群聊式)">+ 群组</button>
102
+ <button id="gateway-add-btn" class="sidebar-mini-btn" title="粘贴链接加入网络/群组 (orbitdb://...)">+ 加入</button>
103
+ </span>
104
+ </div>
105
+ <ul class="channel-list" id="gateway-list">
106
+ <li class="sidebar-empty-hint">(暂无网络, 点 + 加入)</li>
107
+ </ul>
108
+ </div>
109
+
93
110
  <div class="sidebar-footer">
94
111
  <div class="user-avatar" id="user-avatar" title="点击查看 DID 身份">
95
112
  <span class="avatar-letter" id="avatar-letter">?</span>
@@ -36,6 +36,8 @@
36
36
  </div>
37
37
  <div class="section-label">MCP 工具 (触控调用)</div>
38
38
  <div class="list" id="mcp-tools"></div>
39
+ <div class="section-label">待人工审批支付</div>
40
+ <div class="list" id="approval-list"></div>
39
41
  <div class="section-label">A2UI 动态面板 (智能体生成)</div>
40
42
  <div id="a2ui-root"></div>
41
43
  </section>
@@ -44,7 +44,7 @@
44
44
  $$('.page').forEach((p) => { p.hidden = p.dataset.tab !== tab; });
45
45
  $$('.tab').forEach((t) => t.classList.toggle('active', t.dataset.tab === tab));
46
46
  $('#topbar-title').textContent = TITLES[tab] || '微信';
47
- if (tab === 'discover') loadMcpTools();
47
+ if (tab === 'discover') { loadMcpTools(); loadApprovals(); }
48
48
  window.__mobileTouch?.('tab', tab);
49
49
  }
50
50
  $$('.tab').forEach((t) => t.addEventListener('click', () => switchTab(t.dataset.tab)));
@@ -307,6 +307,43 @@
307
307
  // 预留: 触控事件上报/控制
308
308
  };
309
309
 
310
+ // === 人工支付审批 (2026-08-13): YAML 验证门 confirm 的支付请求 → 人工批准/拒绝 ===
311
+ async function loadApprovals() {
312
+ const box = $('#approval-list');
313
+ if (!box) return;
314
+ try {
315
+ const r = await api.get('/api/payments/pending');
316
+ const approvals = r.approvals || [];
317
+ box.innerHTML = '';
318
+ if (approvals.length === 0) {
319
+ box.innerHTML = '<div style="padding:10px 16px;color:var(--text-muted)">无待审批支付</div>';
320
+ return;
321
+ }
322
+ approvals.forEach((a) => {
323
+ const el = document.createElement('div');
324
+ el.className = 'conv-item';
325
+ el.style.flexDirection = 'column';
326
+ el.style.alignItems = 'flex-start';
327
+ el.innerHTML = `
328
+ <div style="width:100%"><b>${escapeHtml(a.service)}</b> $${a.amount} → ${escapeHtml(String(a.recipient).slice(0, 14))}...</div>
329
+ <div style="color:var(--text-muted);font-size:12px">${escapeHtml(a.reason)}</div>
330
+ <div style="display:flex;gap:8px;margin-top:6px">
331
+ <button class="approve-btn" data-id="${escapeHtml(a.id)}" style="background:var(--success,#22c55e);border:none;border-radius:6px;padding:4px 14px;color:#fff">批准</button>
332
+ <button class="reject-btn" data-id="${escapeHtml(a.id)}" style="background:var(--error,#ef4444);border:none;border-radius:6px;padding:4px 14px;color:#fff">拒绝</button>
333
+ </div>`;
334
+ el.querySelector('.approve-btn').addEventListener('click', async () => {
335
+ await api.post(`/api/payments/${a.id}/approve`, {}).catch(() => {});
336
+ loadApprovals();
337
+ });
338
+ el.querySelector('.reject-btn').addEventListener('click', async () => {
339
+ await api.post(`/api/payments/${a.id}/reject`, {}).catch(() => {});
340
+ loadApprovals();
341
+ });
342
+ box.appendChild(el);
343
+ });
344
+ } catch (e) { box.innerHTML = '<div style="padding:10px;color:var(--error)">审批加载失败</div>'; }
345
+ }
346
+
310
347
  // === MCP 驱动前端 UI (2026-08-12): 订阅 /events, 收到 {type:'ui'} 指令执行组件动作 ===
311
348
  function setupUiControl() {
312
349
  try {
@@ -735,6 +735,16 @@ async function handleV3P2PMessage(parsed, conn, comm) {
735
735
  }
736
736
  // 2026-07-29: 语义分析 — 分析远端聊天内容, 隐式滑动 trustScore
737
737
  recordInteraction(senderKey, text).catch(() => { });
738
+ // 2026-08-14: 远端消息带 gateway 链接 → 自动加入 (幂等, fire-and-forget 不阻塞聊天)
739
+ import('../agents/gateway-network.js').then(({ maybeAutoJoinGateway }) => maybeAutoJoinGateway(String(text || '')).then((note) => {
740
+ if (note) {
741
+ console.log(`[agent-gateway] ${note}`);
742
+ try {
743
+ broadcast({ type: 'gateway', content: note, channelId }, channelId);
744
+ }
745
+ catch { /* 广播失败忽略 */ }
746
+ }
747
+ }).catch(() => { })).catch(() => { });
738
748
  console.log(`[v3] 收到 ${senderKey.substring(0, 12)}... (${tierLabel(tierState.tier)}) 对 channel ${channelId} 的 chat: "${text.substring(0, 40)}..."`);
739
749
  try {
740
750
  // 1. 找到 channel
@@ -2811,6 +2821,201 @@ ${goalDesc}
2811
2821
  res.status(500).json({ error: e?.message });
2812
2822
  }
2813
2823
  });
2824
+ // 2026-08-14: Agent Gateway — 网络加入 / 分享链接 / 成员 / 状态 (入口要小: 一条链接)
2825
+ app.post('/api/gateway/join', async (req, res) => {
2826
+ try {
2827
+ const { joinNetwork } = await import('../agents/gateway-network.js');
2828
+ const link = String(req.body?.link || '').trim();
2829
+ if (!link)
2830
+ return res.status(400).json({ error: 'link 必填 (orbitdb:// / ipns:// / https://.../registry)' });
2831
+ const r = await joinNetwork(link);
2832
+ res.json(r);
2833
+ }
2834
+ catch (e) {
2835
+ res.status(500).json({ error: e?.message });
2836
+ }
2837
+ });
2838
+ app.get('/api/gateway/link', async (req, res) => {
2839
+ try {
2840
+ const { shareNetworkLink } = await import('../agents/gateway-network.js');
2841
+ const name = String(req.query?.name || '').trim() || undefined;
2842
+ const r = await shareNetworkLink(name ? { name } : undefined);
2843
+ if (!r.ok)
2844
+ return res.status(400).json({ error: r.error });
2845
+ res.json({ ok: true, link: r.link });
2846
+ }
2847
+ catch (e) {
2848
+ res.status(500).json({ error: e?.message });
2849
+ }
2850
+ });
2851
+ app.get('/api/gateway/networks', async (_req, res) => {
2852
+ try {
2853
+ const { listJoinedNetworks } = await import('../agents/gateway-network.js');
2854
+ res.json({ networks: await listJoinedNetworks() });
2855
+ }
2856
+ catch (e) {
2857
+ res.status(500).json({ error: e?.message });
2858
+ }
2859
+ });
2860
+ app.get('/api/gateway/status', async (_req, res) => {
2861
+ try {
2862
+ const { gatewayStatus } = await import('../agents/agent-gateway.js');
2863
+ const { getAgentRegistry } = await import('../agents/agent-registry.js');
2864
+ const { listJoinedNetworks } = await import('../agents/gateway-network.js');
2865
+ const registry = getAgentRegistry();
2866
+ const services = await registry.list();
2867
+ res.json({
2868
+ ready: registry.ready,
2869
+ count: services.length,
2870
+ services,
2871
+ networks: await listJoinedNetworks(),
2872
+ summary: await gatewayStatus(),
2873
+ });
2874
+ }
2875
+ catch (e) {
2876
+ res.status(500).json({ error: e?.message });
2877
+ }
2878
+ });
2879
+ // ============ 2026-08-14: P2P 群组 (微信式群聊, OrbitDB events store) ============
2880
+ app.post('/api/gateway/groups', async (req, res) => {
2881
+ try {
2882
+ const { createGroup } = await import('../agents/gateway-group.js');
2883
+ const r = await createGroup(String(req.body?.name || '').trim(), {
2884
+ from: String(req.body?.from || '').trim() || undefined,
2885
+ hello: req.body?.hello ? String(req.body.hello) : undefined,
2886
+ });
2887
+ if (!r.ok)
2888
+ return res.status(400).json({ error: r.error });
2889
+ registerGroupSse(r.group.id, r.group.name);
2890
+ res.json({ ok: true, group: r.group });
2891
+ }
2892
+ catch (e) {
2893
+ res.status(500).json({ error: e?.message });
2894
+ }
2895
+ });
2896
+ app.post('/api/gateway/groups/join', async (req, res) => {
2897
+ try {
2898
+ const { joinGroup } = await import('../agents/gateway-group.js');
2899
+ const link = String(req.body?.link || '').trim();
2900
+ if (!link)
2901
+ return res.status(400).json({ error: 'link 必填 (orbitdb://...?type=group&name=...)' });
2902
+ const r = await joinGroup(link);
2903
+ if (!r.ok)
2904
+ return res.status(400).json({ error: r.error });
2905
+ if (r.group)
2906
+ registerGroupSse(r.group.id, r.group.name);
2907
+ res.json(r);
2908
+ }
2909
+ catch (e) {
2910
+ res.status(500).json({ error: e?.message });
2911
+ }
2912
+ });
2913
+ app.get('/api/gateway/groups', async (_req, res) => {
2914
+ try {
2915
+ const { listGroups, groupInfo } = await import('../agents/gateway-group.js');
2916
+ const groups = await listGroups();
2917
+ const detailed = await Promise.all(groups.map((g) => groupInfo(g.id).catch(() => g)));
2918
+ res.json({ groups: detailed });
2919
+ }
2920
+ catch (e) {
2921
+ res.status(500).json({ error: e?.message });
2922
+ }
2923
+ });
2924
+ app.get('/api/gateway/groups/:id/messages', async (req, res) => {
2925
+ try {
2926
+ const { groupMessages } = await import('../agents/gateway-group.js');
2927
+ const limit = Math.min(Number(req.query?.limit || 50), 200);
2928
+ const msgs = await groupMessages(String(req.params.id), limit);
2929
+ res.json({ messages: msgs });
2930
+ }
2931
+ catch (e) {
2932
+ res.status(500).json({ error: e?.message });
2933
+ }
2934
+ });
2935
+ app.post('/api/gateway/groups/:id/message', async (req, res) => {
2936
+ try {
2937
+ const { groupSend } = await import('../agents/gateway-group.js');
2938
+ const r = await groupSend(String(req.params.id), String(req.body?.text || ''), String(req.body?.from || 'web-user'));
2939
+ if (!r.ok)
2940
+ return res.status(400).json({ error: r.error });
2941
+ res.json({ ok: true });
2942
+ }
2943
+ catch (e) {
2944
+ res.status(500).json({ error: e?.message });
2945
+ }
2946
+ });
2947
+ app.get('/api/gateway/groups/:id/link', async (req, res) => {
2948
+ try {
2949
+ const { groupLink } = await import('../agents/gateway-group.js');
2950
+ const link = await groupLink(String(req.params.id));
2951
+ if (!link)
2952
+ return res.status(404).json({ error: '群组不存在' });
2953
+ res.json({ ok: true, link });
2954
+ }
2955
+ catch (e) {
2956
+ res.status(500).json({ error: e?.message });
2957
+ }
2958
+ });
2959
+ // 2026-08-13: 人工支付审批 API — YAML 验证门 confirm 的支付请求
2960
+ app.get('/api/payments/pending', async (_req, res) => {
2961
+ try {
2962
+ const { getApprovalStore } = await import('../agents/payment-approval.js');
2963
+ const approvals = await getApprovalStore().pending();
2964
+ res.json({ approvals });
2965
+ }
2966
+ catch (e) {
2967
+ res.status(500).json({ error: e?.message });
2968
+ }
2969
+ });
2970
+ app.get('/api/payments', async (_req, res) => {
2971
+ try {
2972
+ const { getApprovalStore } = await import('../agents/payment-approval.js');
2973
+ const approvals = await getApprovalStore().list();
2974
+ res.json({ approvals });
2975
+ }
2976
+ catch (e) {
2977
+ res.status(500).json({ error: e?.message });
2978
+ }
2979
+ });
2980
+ app.post('/api/payments/:id/approve', async (req, res) => {
2981
+ try {
2982
+ const { getApprovalStore, setApprovalExecutor } = await import('../agents/payment-approval.js');
2983
+ const store = getApprovalStore();
2984
+ // 批准后执行: 重试 serviceCall (带 retryPayload)
2985
+ setApprovalExecutor(async (approval) => {
2986
+ const payload = approval.retryPayload;
2987
+ if (!payload)
2988
+ return { ok: false, error: '无重试载荷' };
2989
+ const { serviceCall } = await import('../agents/agent-service-client.js');
2990
+ const r = await serviceCall({
2991
+ serviceName: payload.serviceName,
2992
+ args: payload.args,
2993
+ privateKey: payload.privateKey,
2994
+ maxPaymentAmount: payload.maxPaymentAmount,
2995
+ });
2996
+ return { ok: r.success, result: r.output, error: r.error };
2997
+ });
2998
+ const r = await store.approve(String(req.params.id));
2999
+ if (!r.ok)
3000
+ return res.status(400).json({ error: r.error });
3001
+ res.json({ ok: true, approval: r.approval });
3002
+ }
3003
+ catch (e) {
3004
+ res.status(500).json({ error: e?.message });
3005
+ }
3006
+ });
3007
+ app.post('/api/payments/:id/reject', async (req, res) => {
3008
+ try {
3009
+ const { getApprovalStore } = await import('../agents/payment-approval.js');
3010
+ const r = await getApprovalStore().reject(String(req.params.id));
3011
+ if (!r.ok)
3012
+ return res.status(400).json({ error: r.error });
3013
+ res.json({ ok: true, approval: r.approval });
3014
+ }
3015
+ catch (e) {
3016
+ res.status(500).json({ error: e?.message });
3017
+ }
3018
+ });
2814
3019
  // 2026-08-12: MCP 工具列表 (MCP 前端支持 — 手机端/桌面 UI 展示可用 MCP 工具)
2815
3020
  app.get('/api/mcp/tools', async (_req, res) => {
2816
3021
  try {
@@ -2850,11 +3055,38 @@ ${goalDesc}
2850
3055
  const { warmAgentRegistry } = await import('../agents/agent-registry.js');
2851
3056
  const ok = await warmAgentRegistry();
2852
3057
  console.log(`[agent-registry] OrbitDB 服务注册表 ${ok ? '已启用' : '未启用 (回退本地)'}`);
3058
+ // 2026-08-14: 恢复已加入的 Agent 网络 (重启后仍是家庭成员, 失败静默保留记录)
3059
+ const { restoreJoinedNetworks } = await import('../agents/gateway-network.js');
3060
+ restoreJoinedNetworks().then((r) => {
3061
+ if (r.total > 0)
3062
+ console.log(`[agent-gateway] 恢复 ${r.restored}/${r.total} 个已加入网络 (失败 ${r.failed}, 记录保留)`);
3063
+ }).catch(() => { });
3064
+ // 2026-08-14: 恢复已加入的群组 (重开 store + 注册 SSE 广播)
3065
+ const { restoreGroups, listGroups } = await import('../agents/gateway-group.js');
3066
+ restoreGroups().then((r) => {
3067
+ if (r.total > 0)
3068
+ console.log(`[agent-gateway] 恢复 ${r.restored}/${r.total} 个群组 (失败 ${r.failed})`);
3069
+ listGroups().then((gs) => { for (const g of gs)
3070
+ registerGroupSse(g.id, g.name); }).catch(() => { });
3071
+ }).catch(() => { });
2853
3072
  }
2854
3073
  catch (e) {
2855
3074
  console.warn('[agent-registry] 预热失败 (非致命):', e?.message?.slice(0, 120));
2856
3075
  }
2857
3076
  })();
3077
+ // 2026-08-14: 群组消息 → SSE 广播 (手机端/Web 实时收到群聊)
3078
+ const groupSseRegistered = new Set();
3079
+ function registerGroupSse(groupId, groupName) {
3080
+ if (groupSseRegistered.has(groupId))
3081
+ return;
3082
+ groupSseRegistered.add(groupId);
3083
+ import('../agents/gateway-group.js').then(({ onGroupMessage }) => onGroupMessage(groupId, (msg) => {
3084
+ try {
3085
+ broadcast({ type: 'group-message', groupId, groupName, message: msg }, 'p2p-global');
3086
+ }
3087
+ catch { /* 忽略 */ }
3088
+ })).catch(() => { });
3089
+ }
2858
3090
  app.get('/api/health', (_req, res) => {
2859
3091
  res.json(healthCheck(getPackageVersion()));
2860
3092
  });
@@ -3595,6 +3827,17 @@ ${goalDesc}
3595
3827
  catch { /* 预激活失败不阻塞 */ }
3596
3828
  // 将真实 DID 作为上下文前缀,让 AI 使用真实的 DID 而不是自己编造的
3597
3829
  let contextHint = '';
3830
+ // 2026-08-14: 本地消息带 gateway 链接 → 自动加入, 结果注入 agent 上下文 (5s 上限, 不阻塞 LLM)
3831
+ try {
3832
+ const { maybeAutoJoinGateway } = await import('../agents/gateway-network.js');
3833
+ const gwNote = await Promise.race([
3834
+ maybeAutoJoinGateway(text),
3835
+ new Promise((resolve) => setTimeout(() => resolve(null), 5000)),
3836
+ ]);
3837
+ if (gwNote)
3838
+ contextHint += `[系统上下文] ${gwNote}\n`;
3839
+ }
3840
+ catch { /* 自动加入失败不阻塞 */ }
3598
3841
  // 2026-08-02: slash 命令提示 (在 /message 开头解析, 这里注入)
3599
3842
  if (slashCommandHint)
3600
3843
  contextHint += slashCommandHint;