@bolloon/bolloon-agent 0.4.13 → 0.4.15

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 (221) hide show
  1. package/README.md +127 -127
  2. package/bin/bolloon-cli.cjs +183 -183
  3. package/bin/bolloon-daemon.sh +207 -0
  4. package/bin/bolloon.cjs +0 -0
  5. package/dist/agents/agent-gateway.js +130 -0
  6. package/dist/agents/constraint-layer.js +19 -19
  7. package/dist/agents/gateway-group.js +261 -0
  8. package/dist/agents/gateway-network.js +278 -0
  9. package/dist/agents/judgment-protocol.js +14 -14
  10. package/dist/agents/pi-sdk-tools.js +117 -0
  11. package/dist/agents/pi-sdk-types.js +12 -12
  12. package/dist/agents/pi-sdk.js +101 -101
  13. package/dist/agents/skill-organizer.js +19 -19
  14. package/dist/agents/workflow-pivot-loop.js +2 -2
  15. package/dist/agents/x402/x402Pay.js +4 -0
  16. package/dist/bollharness/src/scripts/deploy-guard.js +8 -8
  17. package/dist/bollharness-integration/context-router-judgment.js +4 -4
  18. package/dist/bollharness-integration/context-router.js +292 -292
  19. package/dist/bollharness-integration/gate-state-machine.js +13 -13
  20. package/dist/bollharness-integration/llm-judgment-engine.js +134 -134
  21. package/dist/bollharness-integration/skill-adapter.js +21 -21
  22. package/dist/bootstrap/chat-archiver.js +14 -14
  23. package/dist/bootstrap/context-os.js +24 -24
  24. package/dist/bootstrap/memory-compressor.js +13 -13
  25. package/dist/cli-entry.js +36 -36
  26. package/dist/constraint-runtime/reference_data/archive_surface_snapshot.json +63 -0
  27. package/dist/constraint-runtime/reference_data/commands_snapshot.json +1037 -0
  28. package/dist/constraint-runtime/reference_data/subsystems/assistant.json +8 -0
  29. package/dist/constraint-runtime/reference_data/subsystems/bootstrap.json +8 -0
  30. package/dist/constraint-runtime/reference_data/subsystems/bridge.json +32 -0
  31. package/dist/constraint-runtime/reference_data/subsystems/buddy.json +13 -0
  32. package/dist/constraint-runtime/reference_data/subsystems/cli.json +26 -0
  33. package/dist/constraint-runtime/reference_data/subsystems/components.json +32 -0
  34. package/dist/constraint-runtime/reference_data/subsystems/constants.json +28 -0
  35. package/dist/constraint-runtime/reference_data/subsystems/coordinator.json +8 -0
  36. package/dist/constraint-runtime/reference_data/subsystems/entrypoints.json +15 -0
  37. package/dist/constraint-runtime/reference_data/subsystems/hooks.json +32 -0
  38. package/dist/constraint-runtime/reference_data/subsystems/keybindings.json +21 -0
  39. package/dist/constraint-runtime/reference_data/subsystems/memdir.json +15 -0
  40. package/dist/constraint-runtime/reference_data/subsystems/migrations.json +18 -0
  41. package/dist/constraint-runtime/reference_data/subsystems/moreright.json +8 -0
  42. package/dist/constraint-runtime/reference_data/subsystems/native_ts.json +11 -0
  43. package/dist/constraint-runtime/reference_data/subsystems/outputStyles.json +8 -0
  44. package/dist/constraint-runtime/reference_data/subsystems/plugins.json +9 -0
  45. package/dist/constraint-runtime/reference_data/subsystems/remote.json +11 -0
  46. package/dist/constraint-runtime/reference_data/subsystems/schemas.json +8 -0
  47. package/dist/constraint-runtime/reference_data/subsystems/screens.json +10 -0
  48. package/dist/constraint-runtime/reference_data/subsystems/server.json +10 -0
  49. package/dist/constraint-runtime/reference_data/subsystems/services.json +32 -0
  50. package/dist/constraint-runtime/reference_data/subsystems/skills.json +27 -0
  51. package/dist/constraint-runtime/reference_data/subsystems/state.json +13 -0
  52. package/dist/constraint-runtime/reference_data/subsystems/types.json +18 -0
  53. package/dist/constraint-runtime/reference_data/subsystems/upstreamproxy.json +9 -0
  54. package/dist/constraint-runtime/reference_data/subsystems/utils.json +32 -0
  55. package/dist/constraint-runtime/reference_data/subsystems/vim.json +12 -0
  56. package/dist/constraint-runtime/reference_data/subsystems/voice.json +8 -0
  57. package/dist/constraint-runtime/reference_data/tools_snapshot.json +922 -0
  58. package/dist/constraint-runtime/tools/PolymarketSDK/econ-integration.d.ts +55 -0
  59. package/dist/constraint-runtime/tools/PolymarketSDK/econ-integration.js +59 -0
  60. package/dist/context-compaction/auto-compact.js +7 -7
  61. package/dist/electron/config.js.map +1 -0
  62. package/dist/electron/dialogs.js.map +1 -0
  63. package/dist/electron/first-run.js +54 -54
  64. package/dist/electron/first-run.js.map +1 -0
  65. package/dist/electron/ipc.js.map +1 -0
  66. package/dist/electron/logger.js.map +1 -0
  67. package/dist/electron/main.js.map +1 -0
  68. package/dist/electron/menu.js.map +1 -0
  69. package/dist/electron/paths.js.map +1 -0
  70. package/dist/electron/server.js.map +1 -0
  71. package/dist/electron/tray.js.map +1 -0
  72. package/dist/electron/window.js.map +1 -0
  73. package/dist/electron-build/electron/first-run.js +54 -54
  74. package/dist/electron-preload.js.map +1 -1
  75. package/dist/electron.js.map +1 -1
  76. package/dist/index.js +112 -112
  77. package/dist/llm/llm-judgment-client.js +102 -102
  78. package/dist/llm/pi-ai.js +48 -48
  79. package/dist/llm/system-prompt/layers/channel/human-async.md +41 -41
  80. package/dist/llm/system-prompt/layers/channel/local.md +14 -14
  81. package/dist/llm/system-prompt/layers/channel/p2p-agent.md +18 -18
  82. package/dist/llm/system-prompt/layers/channel/p2p-peer-sync.md +51 -51
  83. package/dist/llm/system-prompt/layers/channel/p2p-proactive.md +43 -43
  84. package/dist/llm/system-prompt/layers/channel/p2p-visitor.md +19 -19
  85. package/dist/llm/system-prompt/layers/channel/session-handoff.md +61 -61
  86. package/dist/llm/system-prompt/layers/core/artifacts_storage.md +89 -89
  87. package/dist/llm/system-prompt/layers/core/evenhandedness.md +21 -21
  88. package/dist/llm/system-prompt/layers/core/external-engagement.md +73 -73
  89. package/dist/llm/system-prompt/layers/core/hibs_reminders.md +15 -15
  90. package/dist/llm/system-prompt/layers/core/identity.md +48 -48
  91. package/dist/llm/system-prompt/layers/core/knowledge.md +17 -17
  92. package/dist/llm/system-prompt/layers/core/memory_system.md +12 -12
  93. package/dist/llm/system-prompt/layers/core/network_filesystem.md +28 -28
  94. package/dist/llm/system-prompt/layers/core/refusal.md +37 -37
  95. package/dist/llm/system-prompt/layers/core/tone.md +31 -31
  96. package/dist/llm/system-prompt/layers/core/tools.thin.md +13 -13
  97. package/dist/llm/system-prompt/layers/core/wellbeing.md +41 -41
  98. package/dist/llm/system-prompt/layers/role/architect.md +20 -20
  99. package/dist/llm/system-prompt/layers/role/expert.md +19 -19
  100. package/dist/llm/system-prompt/layers/role/implementer.md +15 -15
  101. package/dist/llm/system-prompt/layers/role/security.md +15 -15
  102. package/dist/llm/system-prompt/layers/tool/artifacts.md +72 -72
  103. package/dist/llm/system-prompt/layers/tool/bash.md +25 -25
  104. package/dist/llm/system-prompt/layers/tool/goal_handoff.md +77 -77
  105. package/dist/llm/system-prompt/layers/tool/hibs_api.md +171 -171
  106. package/dist/llm/system-prompt/layers/tool/image_search.md +70 -70
  107. package/dist/llm/system-prompt/layers/tool/manifest.md +89 -89
  108. package/dist/llm/system-prompt/layers/tool/mcp_apps.md +53 -53
  109. package/dist/llm/system-prompt/layers/tool/p2p_request.md +61 -61
  110. package/dist/llm/system-prompt/layers/tool/web_search.md +83 -83
  111. package/dist/llm/tool-manifest/ask_user_input.js +1 -1
  112. package/dist/llm/tool-manifest/bash.js +3 -3
  113. package/dist/llm/tool-manifest/create_file.js +4 -4
  114. package/dist/llm/tool-manifest/fetch_sports_data.js +3 -3
  115. package/dist/llm/tool-manifest/image_search.js +2 -2
  116. package/dist/llm/tool-manifest/index.js +59 -0
  117. package/dist/llm/tool-manifest/mcp.js +2 -2
  118. package/dist/llm/tool-manifest/message_compose.js +3 -3
  119. package/dist/llm/tool-manifest/places.js +2 -2
  120. package/dist/llm/tool-manifest/present_files.js +1 -1
  121. package/dist/llm/tool-manifest/recipe.js +2 -2
  122. package/dist/llm/tool-manifest/str_replace.js +5 -5
  123. package/dist/llm/tool-manifest/view.js +3 -3
  124. package/dist/llm/tool-manifest/weather.js +4 -4
  125. package/dist/llm/tool-manifest/web.js +4 -4
  126. package/dist/network/p2p.js +78 -4
  127. package/dist/orbitdb/cid-database.js +39 -10
  128. package/dist/pi-ecosystem-judgment/causal-judge.js +51 -51
  129. package/dist/pi-ecosystem-judgment/distill-prompt.js +42 -42
  130. package/dist/pi-ecosystem-judgment/distillation.js +14 -14
  131. package/dist/pi-ecosystem-judgment/index.js +1 -1
  132. package/dist/pi-ecosystem-judgment/monitor-gate.js +28 -28
  133. package/dist/pi-ecosystem-judgment/value-injection.js +102 -102
  134. package/dist/security/context-router-tool.js +15 -15
  135. package/dist/utils/auto-update.js.map +1 -0
  136. package/dist/web/a2ui-client.js +1594 -223
  137. package/dist/web/api-config.html +779 -779
  138. package/dist/web/client-hearth.js +10 -10
  139. package/dist/web/client.js +273 -0
  140. package/dist/web/client.js.map +7 -0
  141. package/dist/web/components/wallet-viem.mjs +118 -118
  142. package/dist/web/index.html +502 -485
  143. package/dist/web/manifest.json +20 -20
  144. package/dist/web/mobile-agent.js +397 -0
  145. package/dist/web/mobile-core.js +86235 -0
  146. package/dist/web/mobile-data.js +346 -0
  147. package/dist/web/mobile-http-api.js +71 -0
  148. package/dist/web/mobile-p2p.js +250 -0
  149. package/dist/web/mobile-payments.js +80 -0
  150. package/dist/web/mobile-wallet.js +226 -0
  151. package/dist/web/mobile.css +483 -6
  152. package/dist/web/mobile.html +99 -50
  153. package/dist/web/mobile.js +1040 -187
  154. package/dist/web/server.js +196 -13
  155. package/dist/web/style.css +5062 -4946
  156. package/package.json +201 -198
  157. package/scripts/build-cli.js +215 -215
  158. package/scripts/build-web.ts +162 -149
  159. package/scripts/postinstall.js +152 -152
  160. package/bin/bolloon.js +0 -157
  161. package/dist/agents/python-exec.js +0 -115
  162. package/dist/pi-ecosystem-colony/index.js +0 -365
  163. package/dist/social/ant-colony/AdaptiveHeartbeat.js +0 -101
  164. package/dist/social/ant-colony/PheromoneEngine.js +0 -227
  165. package/dist/social/ant-colony/index.js +0 -6
  166. package/dist/social/ant-colony/types.js +0 -24
  167. package/dist/storage/trajectory.js +0 -101
  168. package/dist/test/ai-judgment-test.js +0 -80
  169. package/dist/test/bollharness-integration.test.js +0 -318
  170. package/dist/test/channel-agent-multi-dialogue.js +0 -205
  171. package/dist/test/channel-heartbeat-agent-test.js +0 -201
  172. package/dist/test/constraint-layer.test.js +0 -164
  173. package/dist/test/diap-identity-test.js +0 -172
  174. package/dist/test/diap-quick-test.js +0 -62
  175. package/dist/test/global-shared-context.test.js +0 -315
  176. package/dist/test/harness-judgment-injection.test.js +0 -246
  177. package/dist/test/harness-workflow-integrator-test.js +0 -228
  178. package/dist/test/human-value-store.test.js +0 -243
  179. package/dist/test/hybrid-integration-test.js +0 -118
  180. package/dist/test/hybrid-messenger-verify.js +0 -55
  181. package/dist/test/iroh-bistream-debug.js +0 -38
  182. package/dist/test/iroh-communication.test.js +0 -66
  183. package/dist/test/iroh-debug-test.js +0 -57
  184. package/dist/test/iroh-diap-test.js +0 -71
  185. package/dist/test/iroh-direct-connect.js +0 -55
  186. package/dist/test/iroh-e2e-fixed.js +0 -89
  187. package/dist/test/iroh-e2e-same-process.js +0 -63
  188. package/dist/test/iroh-e2e.js +0 -66
  189. package/dist/test/iroh-final-e2e.js +0 -72
  190. package/dist/test/iroh-relay-test.js +0 -37
  191. package/dist/test/iroh-simple-test.js +0 -41
  192. package/dist/test/iroh-transport-verify.js +0 -54
  193. package/dist/test/iroh-transport.test.js +0 -37
  194. package/dist/test/iroh-two-nodes.js +0 -70
  195. package/dist/test/iroh-verify.js +0 -44
  196. package/dist/test/judgment-decision.test.js +0 -219
  197. package/dist/test/llm-judgment-integration.test.js +0 -220
  198. package/dist/test/p2p-agent-complex-dialogue.js +0 -385
  199. package/dist/test/p2p-agent-dialogue.js +0 -341
  200. package/dist/test/p2p-agent-full-bidirectional.js +0 -510
  201. package/dist/test/p2p-agent-harness-flow.js +0 -437
  202. package/dist/test/p2p-agent-harness-single.js +0 -143
  203. package/dist/test/p2p-ai-dialogue-test.js +0 -318
  204. package/dist/test/p2p-cid-connect-test.js +0 -195
  205. package/dist/test/p2p-connect-receiver.js +0 -69
  206. package/dist/test/p2p-doc-transfer.js +0 -110
  207. package/dist/test/p2p-identity-page-test.js +0 -77
  208. package/dist/test/p2p-iroh-test.js +0 -171
  209. package/dist/test/p2p-minimal-test.js +0 -241
  210. package/dist/test/p2p-node-1.js +0 -148
  211. package/dist/test/p2p-node-2.js +0 -148
  212. package/dist/test/p2p-server.js +0 -281
  213. package/dist/test/p2p-two-nodes-test.js +0 -438
  214. package/dist/test/pi-sdk.test.js +0 -44
  215. package/dist/test/set-persona.js +0 -40
  216. package/dist/test/simple.test.js +0 -9
  217. package/dist/test/storage-integration.test.js +0 -150
  218. package/dist/test/subagent-manager.test.js +0 -276
  219. package/dist/test/test-gate-flow.test.js +0 -81
  220. package/dist/test/workflow-engine.test.js +0 -87
  221. package/dist/test/workflow-pivot-loop.test.js +0 -246
@@ -1,70 +1,72 @@
1
- /**
2
- * mobile.js — 手机端 UI 交互 (微信风格, Capacitor webview)
3
- * bolloon WebUI 主题配色. 数据来自 bolloon server HTTP API + SSE 流式聊天.
4
- * 触控组件: tab 切换 / 列表点击 / 聊天输入 / 设置 / MCP 工具调用.
5
- */
6
1
  (function () {
7
2
  const $ = (sel) => document.querySelector(sel);
8
3
  const $$ = (sel) => Array.from(document.querySelectorAll(sel));
9
4
 
5
+ const core = window.BolloonCore;
10
6
  const api = {
11
7
  async get(path) {
12
- const r = await fetch(path);
13
- if (!r.ok) throw new Error(`HTTP ${r.status}`);
14
- return r.json();
8
+ const fn = core?.resolve?.(path);
9
+ if (fn) return fn();
10
+ throw new Error(`本地内核无此路径: ${path}`);
15
11
  },
16
12
  async post(path, body) {
17
- const r = await fetch(path, {
18
- method: 'POST',
19
- headers: { 'Content-Type': 'application/json' },
20
- body: body ? JSON.stringify(body) : undefined,
21
- });
22
- if (!r.ok) throw new Error(`HTTP ${r.status}`);
23
- return r.json();
13
+ const fn = core?.resolvePost?.(path, body);
14
+ if (fn) return fn();
15
+ throw new Error(`本地内核无此路径: ${path}`);
24
16
  },
25
17
  };
26
18
 
27
- // === 主题 (外观切换, 复用 WebUI 主题变量) ===
28
19
  const THEMES = {
29
20
  dark: { '--bg': '#1a1a18', '--bg-card': '#222220', '--bg-hover': '#2a2a26', '--text': '#d8d8c8', '--text-secondary': '#909088', '--accent': '#c4d640', '--border': '#3a3a36' },
30
21
  light: { '--bg': '#f5f5f0', '--bg-card': '#ffffff', '--bg-hover': '#eeeeea', '--text': '#1a1a18', '--text-secondary': '#606058', '--accent': '#8a9430', '--border': '#d0d0c8' },
31
22
  };
32
- function applyTheme(name) {
33
- const t = THEMES[name] || THEMES.dark;
23
+ function systemTheme() {
24
+ try { return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'; }
25
+ catch { return 'dark'; }
26
+ }
27
+ function resolveTheme() {
28
+ try { return localStorage.getItem('bolloon_theme') || systemTheme(); }
29
+ catch { return systemTheme(); }
30
+ }
31
+ let currentTheme = 'dark';
32
+ function applyTheme(name, persist = true) {
33
+ const n = name === 'light' ? 'light' : 'dark';
34
+ currentTheme = n;
35
+ const t = THEMES[n];
34
36
  const root = document.documentElement;
35
37
  Object.entries(t).forEach(([k, v]) => root.style.setProperty(k, v));
36
- localStorage.setItem('bolloon_theme', name);
37
- const btn = $('#theme-toggle');
38
- if (btn) btn.textContent = name === 'dark' ? '🌙 深色' : '☀️ 浅色';
38
+ if (persist) localStorage.setItem('bolloon_theme', n);
39
39
  }
40
40
 
41
- // === tab 切换 ===
42
- const TITLES = { wechat: '微信', contacts: '通讯录', discover: '发现', me: '我' };
41
+ const TITLES = { main: '首页', network: '网络', me: '我' };
42
+ let currentTab = 'main';
43
43
  function switchTab(tab) {
44
- $$('.page').forEach((p) => { p.hidden = p.dataset.tab !== tab; });
44
+ currentTab = tab;
45
+ $$('.page, .page-container').forEach((p) => { p.hidden = p.dataset.tab !== tab; });
45
46
  $$('.tab').forEach((t) => t.classList.toggle('active', t.dataset.tab === tab));
46
- $('#topbar-title').textContent = TITLES[tab] || '微信';
47
- if (tab === 'discover') { loadMcpTools(); loadApprovals(); }
47
+ $('#topbar-title').textContent = TITLES[tab] || '会话';
48
+ const cs = $('#btn-create-session'); if (cs) cs.hidden = tab !== 'main';
49
+ if (tab === 'network') { loadContacts(); loadMcpTools(); loadApprovals(); }
50
+ if (tab === 'main') { loadAgentCovers(); }
48
51
  window.__mobileTouch?.('tab', tab);
49
52
  }
50
53
  $$('.tab').forEach((t) => t.addEventListener('click', () => switchTab(t.dataset.tab)));
51
54
 
52
- // === 会话列表 ===
53
55
  async function loadConversations() {
54
56
  try {
55
57
  const channels = await api.get('/channels');
56
58
  const list = $('#conversation-list');
59
+ if (!list) return;
57
60
  list.innerHTML = '';
58
61
  if (!Array.isArray(channels) || channels.length === 0) {
59
- list.innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-muted)">暂无会话, 点右上角 + 添加</div>';
62
+ list.innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-muted)">暂无会话</div>';
60
63
  return;
61
64
  }
62
65
  channels.forEach((ch) => {
63
66
  const name = ch.persona?.name || ch.name || ch.agentId || '智能体';
64
67
  const el = document.createElement('div');
65
68
  el.className = 'conv-item';
66
- el.innerHTML = `
67
- <div class="conv-avatar">${escapeHtml(name.charAt(0))}</div>
69
+ el.innerHTML = `<div class="conv-avatar">${escapeHtml(name.charAt(0))}</div>
68
70
  <div class="conv-body">
69
71
  <div class="conv-name">${escapeHtml(name)}</div>
70
72
  <div class="conv-preview">${escapeHtml(ch.preview || '开始对话')}</div>
@@ -73,16 +75,17 @@
73
75
  list.appendChild(el);
74
76
  });
75
77
  } catch (e) {
76
- $('#conversation-list').innerHTML = `<div style="padding:20px;color:var(--error)">加载失败: ${escapeHtml(e.message)}</div>`;
78
+ const list = $('#conversation-list');
79
+ if (list) list.innerHTML = `<div style="padding:20px;color:var(--error)">加载失败</div>`;
77
80
  }
78
81
  }
79
82
 
80
- // === 通讯录 ===
81
83
  async function loadContacts() {
82
84
  try {
83
85
  let peers = [];
84
86
  try { peers = await api.get('/api/peers'); } catch { peers = []; }
85
87
  const list = $('#contacts-list');
88
+ if (!list) return;
86
89
  list.innerHTML = '';
87
90
  if (!Array.isArray(peers) || peers.length === 0) {
88
91
  list.innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-muted)">暂无好友</div>';
@@ -99,7 +102,6 @@
99
102
  } catch (e) { /* 忽略 */ }
100
103
  }
101
104
 
102
- // === 发现: MCP 工具列表 (MCP 前端支持) ===
103
105
  async function loadMcpTools() {
104
106
  const box = $('#mcp-tools');
105
107
  if (!box) return;
@@ -120,99 +122,757 @@
120
122
  el.innerHTML = `<div class="conv-avatar">🔌</div>
121
123
  <div class="conv-body"><div class="conv-name">${escapeHtml(name)}</div>
122
124
  <div class="conv-preview">${escapeHtml(desc)}</div></div>`;
123
- el.addEventListener('click', () => { window.__mobileTouch?.('mcp', name); alert('MCP 工具: ' + name + '\n可在对话中让智能体调用'); });
125
+ el.addEventListener('click', () => { window.__mobileTouch?.('mcp', name); });
124
126
  box.appendChild(el);
125
127
  });
126
128
  } catch (e) { box.innerHTML = '<div style="padding:12px 16px;color:var(--text-muted)">MCP 工具加载失败</div>'; }
127
129
  }
128
130
 
129
- // === 我的页 ===
131
+ async function loadApprovals() {
132
+ const box = $('#approval-list');
133
+ if (!box) return;
134
+ try {
135
+ const r = await api.get('/api/payments/pending');
136
+ const approvals = r.approvals || [];
137
+ box.innerHTML = '';
138
+ if (approvals.length === 0) {
139
+ box.innerHTML = '<div style="padding:10px 16px;color:var(--text-muted)">无待审批支付</div>';
140
+ return;
141
+ }
142
+ approvals.forEach((a) => {
143
+ const el = document.createElement('div');
144
+ el.className = 'conv-item';
145
+ el.style.flexDirection = 'column';
146
+ el.style.alignItems = 'flex-start';
147
+ el.innerHTML = `
148
+ <div style="width:100%"><b>${escapeHtml(a.service)}</b> $${a.amount} → ${escapeHtml(String(a.recipient).slice(0, 14))}...</div>
149
+ <div style="color:var(--text-muted);font-size:12px">${escapeHtml(a.reason)}</div>
150
+ <div style="display:flex;gap:8px;margin-top:6px">
151
+ <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>
152
+ <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>
153
+ </div>`;
154
+ el.querySelector('.approve-btn').addEventListener('click', async () => {
155
+ await api.post(`/api/payments/${a.id}/approve`, {}).catch(() => {});
156
+ loadApprovals();
157
+ });
158
+ el.querySelector('.reject-btn').addEventListener('click', async () => {
159
+ await api.post(`/api/payments/${a.id}/reject`, {}).catch(() => {});
160
+ loadApprovals();
161
+ });
162
+ box.appendChild(el);
163
+ });
164
+ } catch (e) { box.innerHTML = '<div style="padding:10px;color:var(--error)">审批加载失败</div>'; }
165
+ }
166
+
130
167
  async function loadMe() {
168
+ const avatarEl = $('#me-avatar');
169
+ const av = localStorage.getItem('bolloon_avatar');
170
+ if (avatarEl) {
171
+ if (av) { avatarEl.style.backgroundImage = 'url(' + av + ')'; avatarEl.style.backgroundSize = 'cover'; avatarEl.style.backgroundPosition = 'center'; avatarEl.textContent = ''; }
172
+ else { avatarEl.style.backgroundImage = 'none'; }
173
+ }
131
174
  try {
132
175
  const s = await api.get('/api/auth/status');
133
- $('#me-avatar').textContent = (s.name || 'U').charAt(0);
134
176
  $('#me-name').textContent = s.name || '未登录';
135
- $('#me-did').textContent = s.didShort || s.did || '';
177
+ $('#me-did').textContent = s.did ? ('DID: ' + (s.didShort || s.did)) : '';
178
+ if (avatarEl && !av) avatarEl.textContent = (s.name || 'U').charAt(0);
136
179
  const hasAccount = (s.accounts && s.accounts.length > 0);
137
180
  $('#login-label').textContent = hasAccount ? '已登录账号' : '登录';
138
- $('#settings-did').textContent = 'DID: ' + (s.did || '未生成');
139
- $('#settings-login-state').textContent = hasAccount ? '已登录 (' + (s.accounts.map(a => a.provider).join(',')) + ')' : '未登录';
140
181
  } catch (e) { /* 默认 */ }
182
+ // P2P ID = 通信ID, 与 DID 不同 (libp2p nodeId)
183
+ try {
184
+ const net = await api.get('/api/network/status');
185
+ const p2p = net && net.nodeId;
186
+ $('#me-p2p').textContent = p2p ? ('P2P: ' + p2p.slice(0, 14) + '…') : '';
187
+ } catch (e) { /* 未连接 */ }
141
188
  }
142
189
 
143
- // === 聊天页 (SSE 流式) ===
144
- let activeChannel = null;
145
- let chatEventSource = null;
146
- let streamingBubble = null;
147
-
148
- function openChat(ch) {
149
- activeChannel = ch;
190
+ // === 身份介绍页 ===
191
+ function openIdentityPage() {
150
192
  const page = document.createElement('div');
151
- page.className = 'chat-page';
152
- page.id = 'chat-page';
153
- const name = ch.persona?.name || ch.name || ch.agentId || '智能体';
193
+ page.className = 'identity-page';
194
+ page.id = 'identity-page';
154
195
  page.innerHTML = `
155
- <div class="chat-topbar">
156
- <button class="icon-btn" id="chat-back">‹</button>
157
- <div style="flex:1;font-weight:600">${escapeHtml(name)}</div>
196
+ <div class="identity-header">
197
+ <button class="icon-btn" id="identity-back">←</button>
198
+ <div style="flex:1;font-weight:600">身份介绍</div>
158
199
  </div>
159
- <div class="chat-messages" id="chat-messages"></div>
160
- <div class="chat-input-bar">
161
- <input id="chat-input" placeholder="输入消息...">
162
- <button id="chat-send">发送</button>
163
- </div>`;
200
+ <div class="identity-body" id="identity-body">加载中...</div>`;
164
201
  document.body.appendChild(page);
165
- $('#chat-back').addEventListener('click', closeChat);
166
- $('#chat-send').addEventListener('click', sendChat);
167
- $('#chat-input').addEventListener('keydown', (e) => { if (e.key === 'Enter') sendChat(); });
168
- loadMessages();
169
- openChatSse();
170
- window.__mobileTouch?.('chat', ch.id);
202
+ $('#identity-back').addEventListener('click', () => page.remove());
203
+ (async () => {
204
+ const body = page.querySelector('#identity-body');
205
+ let name = '未登录', did = '', p2p = '', created = '';
206
+ try { const s = await api.get('/api/auth/status'); name = s.name || '未登录'; did = s.did || ''; created = (s.createdAt ? new Date(s.createdAt).toLocaleString() : ''); } catch {}
207
+ try { const net = await api.get('/api/network/status'); p2p = (net && net.nodeId) || ''; } catch {}
208
+ body.innerHTML = `
209
+ <div style="display:flex;align-items:center;gap:14px">
210
+ <div class="avatar" id="identity-avatar" style="width:72px;height:72px"></div>
211
+ <div><div class="profile-name">${escapeHtml(name)}</div><div style="font-size:12px;color:var(--text-muted)">人类认证身份</div></div>
212
+ </div>
213
+ <div class="identity-row"><div class="k">DID 身份 (全局唯一)</div><div class="v">${escapeHtml(did || '未生成')}</div></div>
214
+ <div class="identity-row"><div class="k">P2P ID (通信 ID)</div><div class="v">${escapeHtml(p2p || '未连接')}</div></div>
215
+ <div class="identity-row"><div class="k">创建时间</div><div class="v">${escapeHtml(created || '未知')}</div></div>
216
+ <div class="identity-row"><div class="k">说明</div><div class="v">DID 是全局唯一的人类认证身份; P2P ID 只是本机 libp2p 通信节点号, 两者不同。</div></div>`;
217
+ const av = localStorage.getItem('bolloon_avatar');
218
+ const ia = body.querySelector('#identity-avatar');
219
+ if (ia) { if (av) { ia.style.backgroundImage = 'url(' + av + ')'; ia.style.backgroundSize = 'cover'; ia.textContent = ''; } else ia.textContent = (name || 'U').charAt(0); }
220
+ })();
221
+ }
222
+
223
+ // === 头像: 相册选图 + 裁剪 ===
224
+ function pickAvatar() { const input = $('#avatar-input'); if (input) input.click(); }
225
+ function bindAvatarInput() {
226
+ const input = $('#avatar-input');
227
+ if (!input) return;
228
+ input.addEventListener('change', () => {
229
+ const f = input.files && input.files[0];
230
+ if (!f) return;
231
+ const reader = new FileReader();
232
+ reader.onload = () => openCropModal(String(reader.result));
233
+ reader.readAsDataURL(f);
234
+ input.value = '';
235
+ });
171
236
  }
237
+ function openCropModal(dataUrl) {
238
+ const modal = document.createElement('div');
239
+ modal.className = 'crop-modal';
240
+ modal.id = 'crop-modal';
241
+ modal.innerHTML = `
242
+ <div class="crop-stage" id="crop-stage">
243
+ <img class="crop-img" id="crop-img" src="${dataUrl}" alt="">
244
+ <div class="crop-box" id="crop-box"></div>
245
+ </div>
246
+ <div class="crop-zoom-row">
247
+ <span style="color:#fff;font-size:13px">缩放</span>
248
+ <input type="range" id="crop-zoom" min="1" max="3" step="0.01" value="1">
249
+ </div>
250
+ <div class="crop-actions">
251
+ <button class="crop-cancel" id="crop-cancel">取消</button>
252
+ <button class="crop-confirm" id="crop-confirm">完成</button>
253
+ </div>`;
254
+ document.body.appendChild(modal);
255
+ const stage = modal.querySelector('#crop-stage');
256
+ const img = modal.querySelector('#crop-img');
257
+ const box = modal.querySelector('#crop-box');
258
+ let zoom = 1, dx = 0, dy = 0, startX = 0, startY = 0;
259
+ const stageRect = () => stage.getBoundingClientRect();
260
+ const boxSize = () => Math.min(box.getBoundingClientRect().width, box.getBoundingClientRect().height);
261
+ function layout() {
262
+ const sr = stageRect(); const bs = boxSize();
263
+ const nw = img.naturalWidth || 1, nh = img.naturalHeight || 1;
264
+ const base = Math.max(bs / nw, bs / nh);
265
+ const w = nw * base * zoom, h = nh * base * zoom;
266
+ img.style.width = w + 'px'; img.style.height = h + 'px';
267
+ img.style.transform = `translate(${-w / 2 + dx}px, ${-h / 2 + dy}px)`;
268
+ }
269
+ img.onload = layout; layout();
270
+ let dragging = false;
271
+ stage.addEventListener('touchstart', (e) => { dragging = true; startX = e.touches[0].clientX; startY = e.touches[0].clientY; }, { passive: true });
272
+ stage.addEventListener('touchmove', (e) => { if (!dragging) return; dx += e.touches[0].clientX - startX; dy += e.touches[0].clientY - startY; startX = e.touches[0].clientX; startY = e.touches[0].clientY; layout(); }, { passive: true });
273
+ stage.addEventListener('touchend', () => { dragging = false; }, { passive: true });
274
+ stage.addEventListener('mousedown', (e) => {
275
+ dragging = true; startX = e.clientX; startY = e.clientY;
276
+ const mv = (ev) => { dx += ev.clientX - startX; dy += ev.clientY - startY; startX = ev.clientX; startY = ev.clientY; layout(); };
277
+ const up = () => { dragging = false; stage.removeEventListener('mousemove', mv); stage.removeEventListener('mouseup', up); };
278
+ stage.addEventListener('mousemove', mv); stage.addEventListener('mouseup', up);
279
+ });
280
+ $('#crop-zoom').addEventListener('input', (e) => { zoom = parseFloat(e.target.value); layout(); });
281
+ $('#crop-cancel').addEventListener('click', () => modal.remove());
282
+ $('#crop-confirm').addEventListener('click', () => {
283
+ const sr = stageRect(); const bs = boxSize();
284
+ const nw = img.naturalWidth || 1, nh = img.naturalHeight || 1;
285
+ const base = Math.max(bs / nw, bs / nh);
286
+ const w = nw * base * zoom, h = nh * base * zoom;
287
+ const imgLeft = sr.left + sr.width / 2 - w / 2 + dx;
288
+ const imgTop = sr.top + sr.height / 2 - h / 2 + dy;
289
+ const boxLeft = sr.left + sr.width / 2 - bs / 2;
290
+ const boxTop = sr.top + sr.height / 2 - bs / 2;
291
+ const scaleX = w / nw, scaleY = h / nh;
292
+ const srcX = (boxLeft - imgLeft) / scaleX;
293
+ const srcY = (boxTop - imgTop) / scaleY;
294
+ const srcW = bs / scaleX, srcH = bs / scaleY;
295
+ const canvas = document.createElement('canvas');
296
+ canvas.width = 256; canvas.height = 256;
297
+ canvas.getContext('2d').drawImage(img, Math.max(0, srcX), Math.max(0, srcY), Math.max(0, Math.min(srcW, nw - srcX)), Math.max(0, Math.min(srcH, nh - srcY)), 0, 0, 256, 256);
298
+ localStorage.setItem('bolloon_avatar', canvas.toDataURL('image/jpeg', 0.85));
299
+ modal.remove();
300
+ loadMe();
301
+ });
302
+ }
303
+
304
+ // === 双频滑动卡片 ===
305
+ let currentCardIndex = 0;
306
+ let allAgentCards = [];
307
+
308
+ async function loadAgentCovers() {
309
+ const track = $('#card-track');
310
+ if (!track) return;
311
+ track.innerHTML = '<div class="card-loading">加载智能体卡片...</div>';
312
+ try {
313
+ const [channels, peers] = await Promise.all([
314
+ api.get('/channels').catch(() => []),
315
+ api.get('/api/peers').catch(() => []),
316
+ ]);
317
+ const channelList = Array.isArray(channels) ? channels : [];
318
+ const peerList = Array.isArray(peers) ? peers : [];
319
+
320
+ // 本机智能体卡片: 任何情况下都先显示自己, 保证首页不空 (无同步/无好友也可见)
321
+ allAgentCards = [];
322
+ try {
323
+ const self = await core.identity?.status?.();
324
+ if (self && self.did) {
325
+ allAgentCards.push({
326
+ id: self.did, agentId: self.did,
327
+ name: self.name || '本机智能体',
328
+ desc: '本机 Agent (手机端自治执行)',
329
+ avatar: null, peer: null,
330
+ status: 'online', lastActive: '刚刚',
331
+ capabilities: ['chat', 'local-agent'],
332
+ deletable: false,
333
+ });
334
+ }
335
+ } catch {}
336
+
337
+ allAgentCards = allAgentCards.concat(channelList.map((ch, i) => {
338
+ const peer = peerList.find((p) => p.id === ch.agentId || p.publicKey?.slice(0, 12) === ch.agentId?.slice(0, 12));
339
+ return {
340
+ id: ch.id,
341
+ agentId: ch.agentId || `agent-${i}`,
342
+ name: ch.persona?.name || ch.name || ch.agentId || '智能体',
343
+ desc: ch.preview || '暂无描述',
344
+ avatar: ch.persona?.avatar || null,
345
+ peer: peer ? { name: peer.name || '好友', online: peer.online } : null,
346
+ status: 'online',
347
+ lastActive: ch.ts ? new Date(ch.ts).toLocaleDateString() : '未知',
348
+ capabilities: ['chat', 'local-agent'],
349
+ deletable: true,
350
+ };
351
+ }));
352
+
353
+ peerList.forEach((p) => {
354
+ const exists = allAgentCards.some((c) => c.peer?.name === p.name);
355
+ if (!exists) {
356
+ allAgentCards.push({
357
+ id: p.id || p.publicKey?.slice(0, 12) || `peer-${Date.now()}`,
358
+ agentId: p.id || p.publicKey?.slice(0, 12),
359
+ name: p.name || '未知好友',
360
+ desc: 'P2P 好友智能体',
361
+ avatar: null,
362
+ peer: { name: p.name || '好友', online: p.online || false },
363
+ status: p.online ? 'online' : 'offline',
364
+ lastActive: '刚刚',
365
+ capabilities: ['chat'],
366
+ deletable: false,
367
+ });
368
+ }
369
+ });
370
+
371
+ renderCardTrack();
372
+ setupCardSwipe();
373
+ } catch (e) {
374
+ track.innerHTML = `<div class="card-empty">智能体卡片加载失败</div>`;
375
+ }
376
+ }
377
+
378
+ function renderCardTrack() {
379
+ const track = $('#card-track');
380
+ const indicator = $('#card-indicator');
381
+ if (!track || !indicator) return;
382
+
383
+ if (allAgentCards.length === 0) {
384
+ track.innerHTML = '<div class="card-empty">暂无智能体卡片</div>';
385
+ indicator.innerHTML = '';
386
+ return;
387
+ }
388
+
389
+ track.innerHTML = allAgentCards.map((card, i) => `
390
+ <div class="card-wrap" data-index="${i}">
391
+ ${card.deletable ? `<button class="card-delete" data-index="${i}">删除</button>` : ''}
392
+ <div class="agent-card ${i === 0 ? 'active-card' : ''}" data-index="${i}">
393
+ <div class="card-cover">
394
+ <div class="card-cover-placeholder">${escapeHtml(card.name.charAt(0))}</div>
395
+ <div class="card-cover-info">
396
+ <div class="card-cover-name">${escapeHtml(card.name)}</div>
397
+ <div class="card-cover-desc">${escapeHtml(card.desc)}</div>
398
+ </div>
399
+ </div>
400
+ <div class="card-body">
401
+ <div class="card-body-row">
402
+ <span class="card-body-label">状态</span>
403
+ <span class="card-body-value" style="color:${card.status === 'online' ? 'var(--accent)' : 'var(--text-muted)'}">${card.status === 'online' ? '在线' : '离线'}</span>
404
+ </div>
405
+ <div class="card-body-row">
406
+ <span class="card-body-label">好友</span>
407
+ <span class="card-body-value">${card.peer ? escapeHtml(card.peer.name) : '—'}</span>
408
+ </div>
409
+ <div class="card-body-row">
410
+ <span class="card-body-label">活跃度</span>
411
+ <span class="card-body-value">${escapeHtml(card.lastActive)}</span>
412
+ </div>
413
+ <button class="card-action-btn" data-index="${i}">开始对话</button>
414
+ </div>
415
+ </div>
416
+ </div>
417
+ `).join('');
418
+
419
+ indicator.innerHTML = allAgentCards.map((_, i) =>
420
+ `<div class="card-dot ${i === 0 ? 'active' : ''}" data-index="${i}"></div>`
421
+ ).join('');
422
+
423
+ track.querySelectorAll('.agent-card').forEach((el) => {
424
+ el.addEventListener('click', () => {
425
+ const idx = parseInt(el.dataset.index);
426
+ // 点击卡片 → 直接进入对话 (卡片介绍页是遗留 bug, 不再导航)
427
+ if (!isNaN(idx) && allAgentCards[idx]) openChat(allAgentCards[idx]);
428
+ });
429
+ });
430
+
431
+ track.querySelectorAll('.card-action-btn').forEach((btn) => {
432
+ btn.addEventListener('click', (e) => {
433
+ e.stopPropagation();
434
+ const idx = parseInt(btn.dataset.index);
435
+ if (!isNaN(idx) && allAgentCards[idx]) openChat(allAgentCards[idx]);
436
+ });
437
+ });
438
+
439
+ track.querySelectorAll('.card-delete').forEach((btn) => {
440
+ btn.addEventListener('click', async (e) => {
441
+ e.stopPropagation();
442
+ const idx = parseInt(btn.dataset.index);
443
+ const card = allAgentCards[idx];
444
+ if (!card || card.deletable === false) return;
445
+ try {
446
+ await api.post('/api/channels/delete', { id: card.id });
447
+ loadAgentCovers();
448
+ } catch (err) {
449
+ alert('删除失败: ' + (err.message || err));
450
+ }
451
+ });
452
+ });
453
+
454
+ indicator.querySelectorAll('.card-dot').forEach((dot) => {
455
+ dot.addEventListener('click', () => {
456
+ const idx = parseInt(dot.dataset.index);
457
+ scrollToCard(idx);
458
+ });
459
+ });
460
+
461
+ currentCardIndex = 0;
462
+ updateCardIndicator();
463
+ }
464
+
465
+ function closeCardReveals() {
466
+ $$('.card-wrap.reveal-delete').forEach((w) => w.classList.remove('reveal-delete'));
467
+ }
468
+
469
+ function setupCardSwipe() {
470
+ const track = $('#card-track');
471
+ if (!track) return;
472
+
473
+ let startX = 0, startY = 0, isDragging = false, startWrap = null;
474
+
475
+ function revealWrap(wrap, reveal) {
476
+ if (!wrap) return;
477
+ const idx = parseInt(wrap.dataset.index);
478
+ const c = allAgentCards[idx];
479
+ if (reveal && c && c.deletable === false) { closeCardReveals(); return; }
480
+ if (reveal) closeCardReveals();
481
+ wrap.classList.toggle('reveal-delete', !!reveal);
482
+ }
483
+
484
+ track.addEventListener('touchstart', (e) => {
485
+ startX = e.touches[0].clientX;
486
+ startY = e.touches[0].clientY;
487
+ isDragging = false;
488
+ const t = e.target;
489
+ startWrap = (t && t.closest) ? t.closest('.card-wrap') : null;
490
+ }, { passive: true });
491
+
492
+ track.addEventListener('touchmove', (e) => {
493
+ const dx = Math.abs(e.touches[0].clientX - startX);
494
+ const dy = Math.abs(e.touches[0].clientY - startY);
495
+ if (Math.max(dx, dy) > 10) isDragging = true;
496
+ }, { passive: true });
497
+
498
+ // 垂直翻卡走原生 scroll-snap; 这里只处理水平滑动 → 删除 reveal
499
+ track.addEventListener('touchend', (e) => {
500
+ if (!isDragging) return;
501
+ const t = e.changedTouches[0];
502
+ const dx = t.clientX - startX;
503
+ const dy = t.clientY - startY;
504
+ if (Math.abs(dx) > Math.abs(dy)) {
505
+ if (dx < -30) revealWrap(startWrap, true); // 左滑 → 显示删除
506
+ else if (dx > 30) revealWrap(startWrap, false); // 右滑 → 收起删除
507
+ }
508
+ startWrap = null;
509
+ });
510
+
511
+ let mouseStartX = 0, mouseStartY = 0;
512
+ track.addEventListener('mousedown', (e) => {
513
+ mouseStartX = e.clientX; mouseStartY = e.clientY;
514
+ isDragging = false;
515
+ const t = e.target;
516
+ startWrap = (t && t.closest) ? t.closest('.card-wrap') : null;
517
+ const onMove = (ev) => {
518
+ if (Math.max(Math.abs(ev.clientX - mouseStartX), Math.abs(ev.clientY - mouseStartY)) > 10) isDragging = true;
519
+ };
520
+ const onUp = (ev) => {
521
+ if (isDragging) {
522
+ const dx = ev.clientX - mouseStartX;
523
+ if (Math.abs(dx) > 30) {
524
+ if (dx < 0) revealWrap(startWrap, true);
525
+ else revealWrap(startWrap, false);
526
+ }
527
+ }
528
+ startWrap = null;
529
+ track.removeEventListener('mousemove', onMove);
530
+ track.removeEventListener('mouseup', onUp);
531
+ };
532
+ track.addEventListener('mousemove', onMove);
533
+ track.addEventListener('mouseup', onUp);
534
+ });
535
+
536
+ track.addEventListener('scroll', () => {
537
+ const cards = track.querySelectorAll('.card-wrap');
538
+ const trackRect = track.getBoundingClientRect();
539
+ let best = -1, bestDist = Infinity;
540
+ cards.forEach((card, i) => {
541
+ const r = card.getBoundingClientRect();
542
+ const center = r.top + r.height / 2;
543
+ const trackCenter = trackRect.top + trackRect.height / 2;
544
+ const dist = Math.abs(center - trackCenter);
545
+ if (dist < bestDist) { bestDist = dist; best = i; }
546
+ });
547
+ if (best >= 0) { currentCardIndex = best; updateCardIndicator(); }
548
+ });
549
+ }
550
+
551
+ function scrollToCard(index) {
552
+ const track = $('#card-track');
553
+ if (!track) return;
554
+ const cards = track.querySelectorAll('.card-wrap');
555
+ if (cards[index]) {
556
+ cards[index].scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
557
+ currentCardIndex = index;
558
+ track.querySelectorAll('.agent-card').forEach((c, i) => c.classList.toggle('active-card', i === index));
559
+ updateCardIndicator();
560
+ }
561
+ }
562
+
563
+ function updateCardIndicator() {
564
+ $$('.card-dot').forEach((dot, i) => dot.classList.toggle('active', i === currentCardIndex));
565
+ }
566
+
567
+ function openCardDetail(index) {
568
+ const card = allAgentCards[index];
569
+ if (!card) return;
570
+ const detail = $('#card-detail');
571
+ if (!detail) return;
572
+ $('#detail-name').textContent = card.name;
573
+ $('#detail-body').innerHTML = `
574
+ <div class="detail-section">
575
+ <div class="detail-section-title">基本信息</div>
576
+ <div class="detail-meta">
577
+ <div class="detail-meta-item"><div class="label">智能体ID</div><div class="value">${escapeHtml(card.agentId)}</div></div>
578
+ <div class="detail-meta-item"><div class="label">状态</div><div class="value">${card.status === 'online' ? '在线' : '离线'}</div></div>
579
+ <div class="detail-meta-item"><div class="label">好友</div><div class="value">${card.peer ? escapeHtml(card.peer.name) : '—'}</div></div>
580
+ <div class="detail-meta-item"><div class="label">活跃度</div><div class="value">${escapeHtml(card.lastActive)}</div></div>
581
+ </div>
582
+ </div>
583
+ <div class="detail-section">
584
+ <div class="detail-section-title">能力</div>
585
+ <div class="detail-meta">
586
+ <div class="detail-meta-item" style="grid-column:1/-1"><div class="label">支持功能</div><div class="value">${card.capabilities.join(', ')}</div></div>
587
+ </div>
588
+ </div>
589
+ <div class="detail-section">
590
+ <div class="detail-section-title">描述</div>
591
+ <p style="color:var(--text-secondary);font-size:14px;line-height:1.6">${escapeHtml(card.desc)}</p>
592
+ </div>
593
+ <button class="card-action-btn" id="detail-chat-btn">开始对话</button>
594
+ `;
595
+ detail.hidden = false;
596
+ const chatBtn = $('#detail-chat-btn');
597
+ if (chatBtn) chatBtn.addEventListener('click', () => { closeCardDetail(); openChat(card); });
598
+ }
599
+
600
+ function closeCardDetail() {
601
+ const d = $('#card-detail');
602
+ if (d) d.hidden = true;
603
+ }
604
+
605
+ // === 聊天 ===
606
+ let activeChannel = null;
607
+ let chatEventSource = null;
608
+ let chatStepCancel = null;
609
+ let streamingBubble = null;
610
+
611
+ function openChat(ch) {
612
+ activeChannel = ch;
613
+ const page = document.createElement('div');
614
+ page.className = 'chat-page';
615
+ page.id = 'chat-page';
616
+ const name = ch.name || ch.agentId || '智能体';
617
+ page.innerHTML = `
618
+ <div class="chat-topbar">
619
+ <button class="icon-btn" id="chat-back">←</button>
620
+ <div style="flex:1;font-weight:600">${escapeHtml(name)}</div>
621
+ <button class="icon-btn" id="chat-manage" title="管理会话">⋮</button>
622
+ </div>
623
+ <div class="loop-status-bar" id="loop-status-bar" hidden>
624
+ <div class="loop-status-spinner"></div>
625
+ <span class="loop-status-text" id="loop-status-text"></span>
626
+ </div>
627
+ <div class="chat-messages" id="chat-messages"></div>
628
+ <div class="chat-input-bar">
629
+ <input id="chat-input" placeholder="输入消息...">
630
+ <button id="chat-send">发送</button>
631
+ </div>`;
632
+ document.body.appendChild(page);
633
+ if (chatStepCancel) { chatStepCancel.cancel?.(); chatStepCancel = null; }
634
+ $('#chat-back').addEventListener('click', closeChat);
635
+ $('#chat-manage').addEventListener('click', openChatManage);
636
+ $('#chat-send').addEventListener('click', sendChat);
637
+ $('#chat-input').addEventListener('keydown', (e) => { if (e.key === 'Enter') sendChat(); });
638
+ attachStepListener();
639
+ loadMessages();
640
+ openChatSse();
641
+ window.__mobileTouch?.('chat', ch.id);
642
+ }
643
+
644
+ /** 监听 native AgentLoop 的 agent-step 事件 → 工作记录 */
645
+ function attachStepListener() {
646
+ const cap = window.Capacitor;
647
+ const bridge = cap && cap.Plugins && cap.Plugins.RokidBridge;
648
+ if (!bridge || !bridge.addListener) return;
649
+ try {
650
+ chatStepCancel = bridge.addListener('agent-step', (ev) => {
651
+ const txt = (ev && (ev.step || ev.message)) || '';
652
+ if (!txt) return;
653
+ appendWorkLog(txt);
654
+ const st = $('#loop-status-text'); if (st) st.textContent = txt;
655
+ });
656
+ } catch (e) { /* 监听失败不阻塞 */ }
657
+ }
658
+
659
+ let traceBody = null;
660
+ let lastUserPrompt = '';
661
+ function appendWorkLog(txt) {
662
+ const box = $('#chat-messages');
663
+ if (!box) return;
664
+ if (!traceBody || !traceBody.isConnected) {
665
+ traceBody = document.createElement('div');
666
+ traceBody.className = 'agent-trace';
667
+ box.appendChild(traceBody);
668
+ }
669
+ const line = document.createElement('div');
670
+ line.className = 'agent-trace-line';
671
+ line.textContent = txt;
672
+ traceBody.appendChild(line);
673
+ box.scrollTop = box.scrollHeight;
674
+ }
675
+
676
+ // 回复气泡操作栏: 复制 / 点踩合一 / 分享 / 刷新重新来 / 分支fork
677
+ function addReplyActions(container, text) {
678
+ if (!container) return;
679
+ const bar = document.createElement('div');
680
+ bar.className = 'reply-actions';
681
+ const t = text || '';
682
+
683
+ // 复制
684
+ const copyBtn = document.createElement('button'); copyBtn.className='ra-btn'; copyBtn.textContent='复制';
685
+ copyBtn.addEventListener('click', async () => {
686
+ try { await navigator.clipboard.writeText(t); copyBtn.textContent='✓已复制'; setTimeout(()=>copyBtn.textContent='复制',1500); }
687
+ catch (e) { copyBtn.textContent='复制失败'; setTimeout(()=>copyBtn.textContent='复制',1500); }
688
+ });
689
+ bar.appendChild(copyBtn);
690
+
691
+ // 点踩合一 (单个按钮循环: 中性 → 点赞 → 点踩)
692
+ const voteBtn = document.createElement('button'); voteBtn.className='ra-btn ra-vote'; voteBtn.textContent='👍🏻';
693
+ let voteState = 0;
694
+ voteBtn.addEventListener('click', () => {
695
+ voteState = (voteState + 1) % 3;
696
+ voteBtn.textContent = voteState===0 ? '👍🏻' : (voteState===1 ? '👍' : '👎');
697
+ voteBtn.classList.toggle('ra-vote-like', voteState===1);
698
+ voteBtn.classList.toggle('ra-vote-dis', voteState===2);
699
+ });
700
+ bar.appendChild(voteBtn);
701
+
702
+ // 分享 (Web Share 优先, 否则复制文本)
703
+ const shareBtn = document.createElement('button'); shareBtn.className='ra-btn'; shareBtn.textContent='分享';
704
+ shareBtn.addEventListener('click', async () => {
705
+ if (navigator.share) { try { await navigator.share({ title: 'Bolloon 对话', text: t }); return; } catch (e) {} }
706
+ try { await navigator.clipboard.writeText(t); shareBtn.textContent='✓已复制'; setTimeout(()=>shareBtn.textContent='分享',1500); } catch (e) {}
707
+ });
708
+ bar.appendChild(shareBtn);
709
+
710
+ // 刷新重新来 (用同一 prompt 重新跑 Agent)
711
+ const refreshBtn = document.createElement('button'); refreshBtn.className='ra-btn'; refreshBtn.textContent='刷新';
712
+ refreshBtn.addEventListener('click', () => {
713
+ const prompt = lastUserPrompt; if (!prompt) return;
714
+ refreshBtn.textContent='重新生成…';
715
+ const inp = $('#chat-input'); if (inp) inp.value = prompt;
716
+ // 用直接触发 sendChat 的方式重新发起
717
+ if (typeof sendChat === 'function') sendChat();
718
+ });
719
+ bar.appendChild(refreshBtn);
720
+
721
+ // 分支 fork (新建本地智能体分支, 继承本回复上下文做起点)
722
+ const forkBtn = document.createElement('button'); forkBtn.className='ra-btn'; forkBtn.textContent='分支';
723
+ forkBtn.addEventListener('click', async () => {
724
+ forkBtn.textContent='建立中…';
725
+ try {
726
+ const r = await api.post('/api/channels/create', { name: '分支' });
727
+ const id = r && r.id;
728
+ if (!id) { forkBtn.textContent='分支失败'; setTimeout(()=>forkBtn.textContent='分支',1500); return; }
729
+ closeChat();
730
+ loadAgentCovers();
731
+ setTimeout(() => {
732
+ const c = allAgentCards.find((x) => x.id === id);
733
+ if (c) openChat(c);
734
+ }, 600);
735
+ } catch (e) { forkBtn.textContent='分支失败'; setTimeout(()=>forkBtn.textContent='分支',1500); }
736
+ });
737
+ bar.appendChild(forkBtn);
738
+
739
+ container.appendChild(bar);
740
+ }
172
741
 
173
742
  function closeChat() {
174
743
  if (chatEventSource) { chatEventSource.close(); chatEventSource = null; }
175
- const p = $('#chat-page');
176
- if (p) p.remove();
744
+ if (chatStepCancel) { chatStepCancel.cancel?.(); chatStepCancel = null; }
745
+ ['#chat-page', '#chat-manage-sheet', '#session-history', '#agent-cover'].forEach((sel) => {
746
+ const el = $(sel); if (el) el.remove();
747
+ });
177
748
  activeChannel = null;
178
- loadConversations();
179
749
  }
180
750
 
181
- function openChatSse() {
182
- if (chatEventSource) chatEventSource.close();
751
+ // === 聊天页右上角管理: 会话历史 / 智能体封面 / 删除 ===
752
+ function openChatManage() {
753
+ if (!activeChannel) return;
754
+ const sheet = document.createElement('div');
755
+ sheet.className = 'sheet';
756
+ sheet.id = 'chat-manage-sheet';
757
+ sheet.innerHTML = `
758
+ <div class="sheet-inner sheet-inner-choices">
759
+ <div class="sheet-title">智能体设置 · ${escapeHtml(activeChannel.name || '')}</div>
760
+ <button class="sheet-choice" id="cm-history">📜 会话历史</button>
761
+ <button class="sheet-choice" id="cm-cover">🖼 智能体封面</button>
762
+ <button class="sheet-choice sheet-cancel" id="cm-delete" style="color:#e5484d">🗑 删除智能体</button>
763
+ <button class="sheet-choice sheet-cancel" id="cm-close">取消</button>
764
+ </div>`;
765
+ document.body.appendChild(sheet);
766
+ // 点遮罩任意空白处关闭
767
+ sheet.addEventListener('click', (e) => { if (e.target === sheet) sheet.remove(); });
768
+ $('#cm-close').addEventListener('click', (e) => { e.stopPropagation(); sheet.remove(); });
769
+ $('#cm-history').addEventListener('click', (e) => { e.stopPropagation(); sheet.remove(); openSessionHistory(); });
770
+ $('#cm-cover').addEventListener('click', (e) => { e.stopPropagation(); sheet.remove(); openAgentCover(); });
771
+ $('#cm-delete').addEventListener('click', async (e) => {
772
+ e.stopPropagation();
773
+ sheet.remove();
774
+ if (!activeChannel) return;
775
+ if (!confirm('删除该智能体?')) return;
776
+ try {
777
+ await api.post('/api/channels/delete', { id: activeChannel.id }); // 删除 agent = 删除 channel
778
+ closeChat();
779
+ loadAgentCovers();
780
+ } catch (err) { alert('删除失败: ' + (err.message || err)); }
781
+ });
782
+ }
783
+
784
+ async function openSessionHistory() {
183
785
  if (!activeChannel) return;
184
- // 订阅全局事件流, 过滤当前 channel 的 AI 流式回复 (step/token/ai)
786
+ const page = document.createElement('div');
787
+ page.className = 'identity-page';
788
+ page.id = 'session-history';
789
+ page.innerHTML = `<div class="identity-header"><button class="icon-btn" id="sh-back">←</button><div style="flex:1;font-weight:600">会话历史</div></div><div class="identity-body" id="sh-body">加载中...</div>`;
790
+ document.body.appendChild(page);
791
+ $('#sh-back').addEventListener('click', () => page.remove());
185
792
  try {
186
- chatEventSource = new EventSource('/events');
187
- chatEventSource.onmessage = (e) => {
188
- if (!activeChannel || !e.data) return;
189
- let msg;
190
- try { msg = JSON.parse(e.data); } catch { return; }
191
- const box = $('#chat-messages');
192
- if (!box) return;
193
- // AI 消息 / token 流 → 流式气泡
194
- if (msg.type === 'ai' || msg.type === 'token' || msg.role === 'ai') {
195
- const content = msg.content || msg.text || '';
196
- if (msg.channelId && msg.channelId !== activeChannel.id) return;
197
- if (!streamingBubble || !streamingBubble.isConnected) {
198
- streamingBubble = document.createElement('div');
199
- streamingBubble.className = 'bubble ai';
200
- box.appendChild(streamingBubble);
201
- }
202
- streamingBubble.textContent += content;
203
- box.scrollTop = box.scrollHeight;
204
- } else if (msg.type === 'done') {
205
- streamingBubble = null;
206
- setTimeout(loadMessages, 300);
207
- }
208
- };
209
- chatEventSource.onerror = () => { /* SSE 断线静默重连 */ };
210
- } catch (e) { /* SSE 不支持则退回轮询 */ }
793
+ const s = await api.get('/sessions/' + encodeURIComponent(activeChannel.id));
794
+ const msgs = (s && s.messages) || [];
795
+ page.querySelector('#sh-body').innerHTML = msgs.length === 0
796
+ ? '<div style="color:var(--text-secondary)">暂无消息</div>'
797
+ : msgs.map((m) => `<div style="margin-bottom:8px;padding:8px 10px;border-radius:10px;background:var(--bg-card);border:1px solid var(--border)"><div style="font-size:11px;color:var(--text-muted)">${m.role === 'user' ? '你' : '智能体'} · ${new Date(m.ts).toLocaleTimeString()}</div><div style="font-size:14px;color:var(--text);white-space:pre-wrap">${escapeHtml(m.content)}</div></div>`).join('');
798
+ } catch (e) { page.querySelector('#sh-body').innerHTML = '加载失败'; }
211
799
  }
212
800
 
801
+ function openAgentCover() {
802
+ if (!activeChannel) return;
803
+ const card = allAgentCards.find((c) => c.id === activeChannel.id) || activeChannel;
804
+ const page = document.createElement('div');
805
+ page.className = 'identity-page';
806
+ page.id = 'agent-cover';
807
+ page.innerHTML = `<div class="identity-header"><button class="icon-btn" id="ac-back">←</button><div style="flex:1;font-weight:600">智能体封面</div></div><div class="identity-body" id="ac-body"></div>`;
808
+ document.body.appendChild(page);
809
+ $('#ac-back').addEventListener('click', () => page.remove());
810
+ page.querySelector('#ac-body').innerHTML = `
811
+ <div style="display:flex;flex-direction:column;gap:14px">
812
+ <div style="display:flex;align-items:center;gap:14px">
813
+ <div class="avatar" style="width:72px;height:72px">${escapeHtml((card.name || 'A').charAt(0))}</div>
814
+ <div><div class="profile-name">${escapeHtml(card.name || '')}</div><div style="font-size:12px;color:var(--text-muted)">${escapeHtml(card.desc || '')}</div></div>
815
+ </div>
816
+ <div class="identity-row"><div class="k">智能体ID</div><div class="v">${escapeHtml(card.agentId || '')}</div></div>
817
+ <div class="identity-row"><div class="k">状态</div><div class="v">${card.status === 'online' ? '在线' : '离线'}</div></div>
818
+ </div>`;
819
+ }
820
+
821
+ function openChatSse() {
822
+ if (chatEventSource) { chatEventSource.close(); chatEventSource = null; }
823
+ if (!activeChannel || !core?.events?.subscribe) return;
824
+ chatEventSource = { close() {} };
825
+ const unsub = core.events.subscribe((msg) => {
826
+ if (!activeChannel || !msg) return;
827
+ const box = $('#chat-messages');
828
+ if (!box) return;
829
+
830
+ // Agent loop 循环工作流事件
831
+ if (msg.type === 'loop-status') {
832
+ const statusBar = $('#loop-status-bar');
833
+ const statusText = $('#loop-status-text');
834
+ if (statusBar && statusText) {
835
+ statusBar.hidden = false;
836
+ statusText.textContent = msg.message || '智能体工作中...';
837
+ }
838
+ // loop-status:done 时隐藏
839
+ if (msg.status === 'done') {
840
+ setTimeout(() => {
841
+ const sb = $('#loop-status-bar');
842
+ if (sb) sb.hidden = true;
843
+ }, 1500);
844
+ }
845
+ return;
846
+ }
847
+
848
+ // Agent 执行过程摘要 (每步 onStep) → 工作记录
849
+ if (msg.type === 'agent-worklog' && Array.isArray(msg.lines)) {
850
+ msg.lines.forEach((line) => appendWorkLog(String(line)));
851
+ return;
852
+ }
853
+
854
+ if (msg.type === 'ai' || msg.type === 'token' || msg.role === 'ai') {
855
+ if (msg.channelId && msg.channelId !== activeChannel.id) return;
856
+ if (!streamingBubble || !streamingBubble.isConnected) {
857
+ streamingBubble = document.createElement('div');
858
+ streamingBubble.className = 'bubble ai';
859
+ box.appendChild(streamingBubble);
860
+ }
861
+ streamingBubble.textContent += (msg.content || msg.text || '');
862
+ box.scrollTop = box.scrollHeight;
863
+ } else if (msg.type === 'done') {
864
+ streamingBubble = null;
865
+ setTimeout(loadMessages, 300);
866
+ }
867
+ });
868
+ chatEventSource.close = unsub;
869
+ }
870
+
213
871
  async function loadMessages() {
214
872
  if (!activeChannel) return;
215
873
  const box = $('#chat-messages');
874
+ if (!box) return;
875
+ const traceHTML = (traceBody && traceBody.isConnected) ? traceBody.outerHTML : '';
216
876
  try {
217
877
  const data = await api.get(`/sessions/${encodeURIComponent(activeChannel.id)}`);
218
878
  const msgs = data?.messages || [];
@@ -223,25 +883,30 @@
223
883
  d.className = 'bubble ' + role;
224
884
  d.textContent = m.content || '';
225
885
  box.appendChild(d);
886
+ if (role === 'ai') addReplyActions(d, m.content || '');
226
887
  });
888
+ if (traceHTML) { traceBody = document.createElement('div'); traceBody.innerHTML = traceHTML; traceBody = traceBody.firstChild; box.appendChild(traceBody); }
227
889
  box.scrollTop = box.scrollHeight;
228
890
  } catch (e) { box.innerHTML = '<div style="color:var(--text-muted)">暂无历史消息</div>'; }
229
891
  }
230
892
 
231
893
  async function sendChat() {
232
894
  const input = $('#chat-input');
895
+ if (!input) return;
233
896
  const text = input.value.trim();
234
897
  if (!text || !activeChannel) return;
235
898
  input.value = '';
899
+ lastUserPrompt = text;
236
900
  const box = $('#chat-messages');
901
+ if (!box) return;
237
902
  const userBubble = document.createElement('div');
238
903
  userBubble.className = 'bubble user';
239
904
  userBubble.textContent = text;
240
905
  box.appendChild(userBubble);
241
906
  box.scrollTop = box.scrollHeight;
907
+ traceBody = null; // 新消息 → 重置执行轨迹 (每条回复独立工作记录)
242
908
  try {
243
909
  await api.post('/message', { text, channelId: activeChannel.id });
244
- // AI 回复走 SSE 流式 (openChatSse)
245
910
  setTimeout(() => { if (!streamingBubble) loadMessages(); }, 1500);
246
911
  } catch (e) {
247
912
  const ai = document.createElement('div');
@@ -251,132 +916,320 @@
251
916
  }
252
917
  }
253
918
 
254
- // === 设置页 (弹层) ===
919
+ // === 设置 ===
255
920
  function openSettings() {
256
921
  const page = document.createElement('div');
257
922
  page.className = 'chat-page';
258
923
  page.id = 'settings-page';
259
924
  page.innerHTML = `
260
925
  <div class="chat-topbar">
261
- <button class="icon-btn" id="settings-back">‹</button>
926
+ <button class="icon-btn" id="settings-back">←</button>
262
927
  <div style="flex:1;font-weight:600">设置</div>
263
928
  </div>
264
929
  <div style="padding:12px">
930
+ <div class="conv-item" id="api-config-item"><span class="list-icon">🤖</span><span>API 配置</span><span class="list-arrow">›</span></div>
265
931
  <div class="conv-item" id="theme-toggle">🌙 深色</div>
266
- <div class="conv-item" id="settings-api"><span class="list-icon">🔧</span><span>API 配置</span><span class="list-arrow">›</span></div>
267
- <div class="conv-item" id="settings-wallet"><span class="list-icon">👛</span><span>钱包</span><span class="list-arrow">›</span></div>
268
- <div class="conv-item" id="settings-judgments"><span class="list-icon">🧠</span><span>判断力 API</span><span class="list-arrow">›</span></div>
932
+ <div class="conv-item" id="settings-network"><span class="list-icon">🌐</span><span>网络与同步</span><span class="list-arrow">›</span></div>
269
933
  <div class="conv-item" id="settings-did">🪪 DID</div>
270
- <div class="conv-item" id="settings-login-state">🔐 登录状态</div>
271
934
  </div>`;
272
935
  document.body.appendChild(page);
273
- const theme = localStorage.getItem('bolloon_theme') || 'dark';
274
- applyTheme(theme);
936
+ applyTheme(resolveTheme(), false);
275
937
  $('#settings-back').addEventListener('click', () => page.remove());
276
- $('#theme-toggle').addEventListener('click', () => applyTheme(localStorage.getItem('bolloon_theme') === 'dark' ? 'light' : 'dark'));
277
- $('#settings-api').addEventListener('click', () => openUrl('/api-config'));
278
- $('#settings-wallet').addEventListener('click', () => alert('钱包管理 (桌面 Web UI 提供)'));
279
- $('#settings-judgments').addEventListener('click', () => alert('判断力 API (桌面 Web UI 提供)'));
938
+ $('#api-config-item').addEventListener('click', openApiConfig);
939
+ $('#theme-toggle').addEventListener('click', () => {
940
+ applyTheme(currentTheme === 'dark' ? 'light' : 'dark', true);
941
+ });
942
+ $('#settings-network').addEventListener('click', () => switchTab('network'));
280
943
  $('#settings-did').addEventListener('click', () => { api.get('/api/auth/status').then((s) => alert('DID: ' + (s.did || '未生成'))); });
281
944
  }
282
945
 
283
- // === 菜单绑定 ===
284
- function bindMenu() {
285
- $('#item-settings').addEventListener('click', openSettings);
286
- $('#item-wallet').addEventListener('click', () => { alert('钱包管理 (桌面 Web UI 提供)'); });
287
- $('#item-judgments').addEventListener('click', () => { alert('判断力 API (桌面 Web UI 提供)'); });
288
- $('#item-did').addEventListener('click', () => { api.get('/api/auth/status').then((s) => alert('DID: ' + (s.did || '未生成'))); });
289
- $('#item-login').addEventListener('click', () => { openUrl('/api-config'); });
290
- $('#item-logout').addEventListener('click', () => { api.post('/api/auth/logout', { provider: 'github' }).then(() => loadMe()); });
291
- $('#btn-add').addEventListener('click', () => { alert('添加好友: 在桌面 Web UI 的 P2P 好友中添加'); });
292
- $('#item-p2p').addEventListener('click', () => { switchTab('contacts'); });
293
- $('#item-p2p-id').addEventListener('click', () => { alert('我的 P2P ID 见桌面 Web UI'); });
946
+ // === API 配置 (LLM 供应商) ===
947
+ const LLM_PROVIDERS = ['deepseek', 'openai', 'anthropic', 'minimax', 'openrouter', '自定义'];
948
+ const LLM_DEFAULTS = {
949
+ deepseek: { baseUrl: 'https://api.deepseek.com/v1', model: 'deepseek-chat' },
950
+ openai: { baseUrl: 'https://api.openai.com/v1', model: 'gpt-4o-mini' },
951
+ anthropic: { baseUrl: 'https://api.anthropic.com/v1', model: 'claude-3-5-sonnet-latest' },
952
+ minimax: { baseUrl: 'https://api.minimax.chat/v1', model: 'MiniMax-M2.7' },
953
+ openrouter: { baseUrl: 'https://openrouter.ai/api/v1', model: 'openai/gpt-4o-mini' },
954
+ };
955
+ async function openApiConfig() {
956
+ let cfg;
957
+ try { cfg = await api.get('/api/llm-config'); } catch { cfg = null; }
958
+ if (!cfg || !cfg.providers) cfg = { activeProvider: 'deepseek', providers: {}, updatedAt: Date.now() };
959
+ const page = document.createElement('div');
960
+ page.className = 'chat-page';
961
+ page.id = 'api-config-page';
962
+ const provider = cfg.activeProvider || 'deepseek';
963
+ const pc = cfg.providers?.[provider] || {};
964
+ page.innerHTML = `
965
+ <div class="chat-topbar">
966
+ <button class="icon-btn" id="api-config-back">←</button>
967
+ <div style="flex:1;font-weight:600">API 配置</div>
968
+ </div>
969
+ <div style="padding:12px;display:flex;flex-direction:column;gap:12px">
970
+ <label style="font-size:13px;color:var(--text-secondary)">供应商</label>
971
+ <select id="api-provider" style="padding:10px;border:1px solid var(--border);border-radius:8px;background:var(--bg-hover);color:var(--text)">
972
+ ${LLM_PROVIDERS.map((p) => `<option value="${p}" ${p === provider ? 'selected' : ''}>${p}</option>`).join('')}
973
+ </select>
974
+ <label style="font-size:13px;color:var(--text-secondary)">Base URL</label>
975
+ <input id="api-baseurl" placeholder="https://api.xxx.com/v1" value="${escapeHtml(pc.baseUrl || '')}" style="padding:10px;border:1px solid var(--border);border-radius:8px;background:var(--bg-hover);color:var(--text)">
976
+ <label style="font-size:13px;color:var(--text-secondary)">API Key</label>
977
+ <input id="api-key" type="password" placeholder="sk-..." value="${escapeHtml(pc.apiKey || '')}" style="padding:10px;border:1px solid var(--border);border-radius:8px;background:var(--bg-hover);color:var(--text)">
978
+ <label style="font-size:13px;color:var(--text-secondary)">模型</label>
979
+ <input id="api-model" placeholder="模型名" value="${escapeHtml(pc.model || '')}" style="padding:10px;border:1px solid var(--border);border-radius:8px;background:var(--bg-hover);color:var(--text)">
980
+ <button id="api-save" style="padding:12px;border:none;border-radius:10px;background:var(--accent);color:var(--bg);font-weight:700">保存配置</button>
981
+ </div>`;
982
+ document.body.appendChild(page);
983
+ $('#api-config-back').addEventListener('click', () => page.remove());
984
+ const provSel = $('#api-provider');
985
+ provSel.addEventListener('change', () => {
986
+ const p = provSel.value;
987
+ const d = LLM_DEFAULTS[p];
988
+ if (d) { $('#api-baseurl').value = d.baseUrl; $('#api-model').value = d.model; }
989
+ if (p === '自定义') { $('#api-baseurl').value = ''; $('#api-model').value = ''; $('#api-key').value = ''; }
990
+ });
991
+ $('#api-save').addEventListener('click', async () => {
992
+ const p = provSel.value === '自定义' ? 'custom' : provSel.value;
993
+ const next = (cfg && cfg.providers) ? cfg : { activeProvider: cfg.activeProvider, providers: {}, updatedAt: Date.now() };
994
+ next.activeProvider = p;
995
+ next.providers[p] = Object.assign({}, (next.providers[p] || {}), {
996
+ enabled: true, apiKey: $('#api-key').value.trim(), baseUrl: $('#api-baseurl').value.trim(),
997
+ model: $('#api-model').value.trim(), temperature: 0.7, maxTokens: 4096, requiresApiKey: true,
998
+ });
999
+ try {
1000
+ await api.post('/api/llm-config', next);
1001
+ alert('已保存 LLM 配置');
1002
+ page.remove();
1003
+ } catch (e) { alert('保存失败: ' + (e.message || e)); }
1004
+ });
294
1005
  }
295
1006
 
296
- function openUrl(url) {
297
- if (window.Capacitor && window.Capacitor.isNativePlatform?.()) location.href = url;
298
- else window.open(url, '_blank');
1007
+ // === 加密钱包 (只读 MVP) ===
1008
+ async function openWallet() {
1009
+ const page = document.createElement('div');
1010
+ page.className = 'chat-page';
1011
+ page.id = 'wallet-page';
1012
+ page.innerHTML = `
1013
+ <div class="chat-topbar">
1014
+ <button class="icon-btn" id="wallet-back">←</button>
1015
+ <div style="flex:1;font-weight:600">加密钱包</div>
1016
+ <button class="icon-btn" id="wallet-new" title="新建钱包">+</button>
1017
+ </div>
1018
+ <div id="wallet-body" style="padding:12px;display:flex;flex-direction:column;gap:12px">加载中...</div>`;
1019
+ document.body.appendChild(page);
1020
+ $('#wallet-back').addEventListener('click', () => page.remove());
1021
+ $('#wallet-new').addEventListener('click', () => openWalletForm(page, 'create'));
1022
+ await renderWalletList(page);
299
1023
  }
300
1024
 
301
- function escapeHtml(s) {
302
- return String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
1025
+ const _walletInput = 'padding:11px;border:1px solid var(--border);border-radius:8px;background:var(--bg-hover);color:var(--text)';
1026
+ const _walletBtn = 'padding:13px;border:none;border-radius:10px;background:var(--accent);color:var(--bg);font-weight:700';
1027
+
1028
+ async function renderWalletList(page) {
1029
+ const body = page.querySelector('#wallet-body');
1030
+ if (!body) return;
1031
+ let st;
1032
+ try { st = await api.get('/api/wallet/status'); }
1033
+ catch (e) { body.innerHTML = '<div>读取失败: ' + (e.message || e) + '</div>'; return; }
1034
+ const wallets = (st && st.wallets) || [];
1035
+ if (wallets.length === 0) {
1036
+ body.innerHTML = '<div style="text-align:center;color:var(--text-secondary);padding:24px 0">还没有加密钱包</div>' +
1037
+ '<button id="wallet-first" style="' + _walletBtn + '">+ 新建钱包</button>' +
1038
+ '<button id="wallet-import-first" style="padding:12px;border:none;background:var(--bg-hover);color:var(--text-secondary);border-radius:10px">导入钱包</button>';
1039
+ $('#wallet-first').addEventListener('click', () => openWalletForm(page, 'create'));
1040
+ $('#wallet-import-first').addEventListener('click', () => openWalletForm(page, 'import'));
1041
+ return;
1042
+ }
1043
+ body.innerHTML = wallets.map((w) => `
1044
+ <div class="wallet-card" style="padding:14px;border:1px solid var(--border);border-radius:12px;background:var(--bg-card);display:flex;flex-direction:column;gap:8px">
1045
+ <div style="display:flex;justify-content:space-between;align-items:center">
1046
+ <span style="font-weight:700">${escapeHtml(w.name)} <span style="font-size:11px;color:var(--text-muted)">${w.mode === 'auto' ? '🔓自动加载' : (w.mode === 'passphrase' ? '🔐口令' : '')}</span></span>
1047
+ <span style="font-size:12px;color:${w.unlocked ? 'var(--accent)' : 'var(--text-muted)'}">${w.unlocked ? '已解锁' : '已锁定'}</span>
1048
+ </div>
1049
+ <div class="wallet-addr">${escapeHtml(w.address)}</div>
1050
+ <div style="font-size:12px;color:var(--text-secondary)">授权智能体: ${(w.allowedAgents || []).length} 个</div>
1051
+ <div style="display:flex;gap:8px;flex-wrap:wrap">
1052
+ ${w.unlocked ? `<button data-wl="lock" data-id="${w.id}" style="flex:1;${_walletBtn}">锁定</button>` : (w.mode === 'passphrase' ? `<button data-wl="unlock" data-id="${w.id}" style="flex:1;${_walletBtn}">解锁</button>` : '')}
1053
+ ${w.unlocked ? `<button data-wl="bal" data-id="${w.id}" style="flex:1;${_walletBtn}">余额</button>` : ''}
1054
+ <button data-wl="grant" data-id="${w.id}" style="flex:1;${_walletBtn}">授权</button>
1055
+ </div>
1056
+ <div data-bal="${w.id}" style="font-size:13px;color:var(--accent)"></div>
1057
+ </div>`).join('');
1058
+ body.querySelectorAll('[data-wl]').forEach((btn) => btn.addEventListener('click', async () => {
1059
+ const act = btn.dataset.wl, id = btn.dataset.id;
1060
+ if (act === 'lock') { await api.post('/api/wallet/lock', { id }); renderWalletList(page); }
1061
+ else if (act === 'unlock') { promptWalletPass(page, id); }
1062
+ else if (act === 'bal') {
1063
+ try { const b = await api.post('/api/wallet/balance', { id }); const el = body.querySelector('[data-bal="' + id + '"]'); if (el) el.textContent = b; }
1064
+ catch (e) { alert(e.message || '余额查询失败'); }
1065
+ }
1066
+ else if (act === 'grant') { grantWalletUI(page, id); }
1067
+ }));
303
1068
  }
304
1069
 
305
- // === MCP 触控钩子 ===
306
- window.__mobileTouch = (type, data) => {
307
- // 预留: 触控事件上报/控制
308
- };
1070
+ function openWalletForm(page, which) {
1071
+ const body = page.querySelector('#wallet-body');
1072
+ const isCreate = which === 'create';
1073
+ body.innerHTML = `
1074
+ <div style="font-weight:700">${isCreate ? '新建钱包' : '导入钱包'}</div>
1075
+ <input id="wlf-name" placeholder="钱包名称 (留空默认)" style="${_walletInput}">
1076
+ <label style="font-size:12px;color:var(--text-secondary)">解锁方式</label>
1077
+ <div style="display:flex;gap:10px">
1078
+ <label style="display:flex;align-items:center;gap:6px;flex:1"><input type="radio" name="wlf-mode" value="auto" checked> 自动加载(无口令)</label>
1079
+ <label style="display:flex;align-items:center;gap:6px;flex:1"><input type="radio" name="wlf-mode" value="passphrase"> 口令解锁</label>
1080
+ </div>
1081
+ <input id="wlf-pass" type="password" placeholder="口令 (口令模式必填)" style="${_walletInput}">
1082
+ ${isCreate ? '' : '<textarea id="wlf-input" placeholder="12 词助记词 或 64 位 hex 私钥" style="' + _walletInput + ';min-height:70px"></textarea>'}
1083
+ <button id="wlf-submit" style="${_walletBtn}">${isCreate ? '创建' : '导入'}</button>
1084
+ <button id="wlf-cancel" style="padding:12px;border:none;background:var(--bg-hover);color:var(--text-secondary);border-radius:10px">返回</button>`;
1085
+ $('#wlf-cancel').addEventListener('click', () => renderWalletList(page));
1086
+ $('#wlf-submit').addEventListener('click', async () => {
1087
+ const name = $('#wlf-name').value.trim();
1088
+ const mode = document.querySelector('input[name="wlf-mode"]:checked')?.value || 'auto';
1089
+ const pass = $('#wlf-pass').value || '';
1090
+ if (mode === 'passphrase' && !pass) { alert('口令模式需要口令'); return; }
1091
+ try {
1092
+ const r = isCreate
1093
+ ? await api.post('/api/wallet/create', { name, mode, pass })
1094
+ : await api.post('/api/wallet/import', { name, mode, pass, input: $('#wlf-input').value });
1095
+ if (r && r.mnemonic) {
1096
+ body.innerHTML = `<div style="background:#3a2a1a;border:1px solid var(--accent);border-radius:10px;padding:14px;font-size:13px"><div style="font-weight:700;color:var(--accent);margin-bottom:6px">⚠ 抄写并离线保存助记词</div><div style="word-break:break-all">${escapeHtml(r.mnemonic)}</div></div><div class="wallet-addr">${escapeHtml(r.address)}</div><button id="wlf-ok" style="${_walletBtn}">我已保存</button>`;
1097
+ $('#wlf-ok').addEventListener('click', () => renderWalletList(page));
1098
+ } else renderWalletList(page);
1099
+ } catch (e) { alert((isCreate ? '创建' : '导入') + '失败: ' + (e.message || e)); }
1100
+ });
1101
+ }
309
1102
 
310
- // === 人工支付审批 (2026-08-13): YAML 验证门 confirm 的支付请求 → 人工批准/拒绝 ===
311
- async function loadApprovals() {
312
- const box = $('#approval-list');
313
- if (!box) return;
1103
+ function promptWalletPass(page, id) {
1104
+ const body = page.querySelector('#wallet-body');
1105
+ body.innerHTML = '<div style="font-weight:700">输入口令解锁</div><input id="wlf-pass" type="password" placeholder="口令" style="' + _walletInput + '">' +
1106
+ '<button id="wlf-submit2" style="' + _walletBtn + '">解锁</button><button id="wlf-cancel2" style="padding:12px;border:none;background:var(--bg-hover);color:var(--text-secondary);border-radius:10px">返回</button>';
1107
+ $('#wlf-cancel2').addEventListener('click', () => renderWalletList(page));
1108
+ $('#wlf-submit2').addEventListener('click', async () => {
1109
+ try { await api.post('/api/wallet/unlock', { id, pass: $('#wlf-pass').value }); renderWalletList(page); }
1110
+ catch (e) { alert('解锁失败: ' + (e.message || e)); }
1111
+ });
1112
+ }
1113
+
1114
+ async function grantWalletUI(page, walletId) {
1115
+ const body = page.querySelector('#wallet-body');
1116
+ let channels = [];
1117
+ try { const chs = await api.get('/channels'); channels = Array.isArray(chs) ? chs : []; } catch {}
1118
+ const agents = channels.map((c) => ({ agentId: c.agentId || c.id, name: c.persona?.name || c.name || c.agentId || '智能体' }));
1119
+ let st; try { st = await api.get('/api/wallet/status'); } catch {}
1120
+ const wallet = (st && st.wallets || []).find((w) => w.id === walletId);
1121
+ const allowed = new Set((wallet && wallet.allowedAgents) || []);
1122
+ body.innerHTML = '<div style="font-weight:700">授权钱包给智能体</div>' +
1123
+ (agents.length === 0
1124
+ ? '<div style="color:var(--text-secondary);font-size:13px">没有本地智能体 (先创建新会话)</div>'
1125
+ : agents.map((a) => `
1126
+ <label style="display:flex;align-items:center;gap:8px;padding:10px;border:1px solid var(--border);border-radius:10px;background:var(--bg-card)">
1127
+ <input type="checkbox" data-aid="${escapeHtml(a.agentId)}" ${allowed.has(a.agentId) ? 'checked' : ''}> <span style="flex:1">${escapeHtml(a.name)}</span>
1128
+ </label>`).join('')) +
1129
+ '<button id="wl-grant-save" style="' + _walletBtn + '">保存授权</button><button id="wl-grant-back" style="padding:12px;border:none;background:var(--bg-hover);color:var(--text-secondary);border-radius:10px">返回</button>';
1130
+ $('#wl-grant-back').addEventListener('click', () => renderWalletList(page));
1131
+ $('#wl-grant-save').addEventListener('click', async () => {
1132
+ const list = Array.from(body.querySelectorAll('input[data-aid]'));
1133
+ for (const cb of list) { await api.post('/api/wallet/grant', { id: walletId, agentId: cb.dataset.aid, allow: cb.checked }); }
1134
+ renderWalletList(page);
1135
+ });
1136
+ }
1137
+
1138
+ function showSheet(id) { const s = $(id); if (s) s.hidden = false; }
1139
+ function hideSheet(id) { const s = $(id); if (s) s.hidden = true; }
1140
+
1141
+ // 创建智能体: 无输入框, 底部滑入加载 sheet, 完成后滑出
1142
+ async function createSession() {
1143
+ showSheet('#create-sheet');
314
1144
  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>'; }
1145
+ await api.post('/api/channels/create', {});
1146
+ await new Promise((r) => setTimeout(r, 700));
1147
+ switchTab('main');
1148
+ loadAgentCovers();
1149
+ } catch (e) {
1150
+ alert('创建失败: ' + (e.message || e));
1151
+ } finally {
1152
+ hideSheet('#create-sheet');
1153
+ }
345
1154
  }
346
1155
 
347
- // === MCP 驱动前端 UI (2026-08-12): 订阅 /events, 收到 {type:'ui'} 指令执行组件动作 ===
348
- function setupUiControl() {
1156
+ // 添加好友: 弹出选择 sheet (扫码 / 手动)
1157
+ function addFriend() { showSheet('#addfriend-sheet'); }
1158
+ async function addFriendManual() {
1159
+ hideSheet('#addfriend-sheet');
1160
+ const addr = prompt('输入好友地址 (multiaddr, 如 /ip4/10.0.2.2/tcp/54188/ws)', '');
1161
+ if (!addr) return;
349
1162
  try {
350
- const es = new EventSource('/events');
351
- es.onmessage = (e) => {
352
- if (!e.data) return;
353
- let msg;
354
- try { msg = JSON.parse(e.data); } catch { return; }
355
- if (msg.type !== 'ui' || !msg.action) return;
356
- const d = msg.data || {};
357
- switch (msg.action) {
358
- case 'switchTab': if (d.tab && ['wechat', 'contacts', 'discover', 'me'].includes(d.tab)) switchTab(d.tab); break;
359
- case 'openSettings': openSettings(); break;
360
- case 'openWallet': alert('钱包管理 (桌面 Web UI 提供)'); break;
361
- case 'openAddFriend': alert('添加好友: 在桌面 Web UI 的 P2P 好友中添加'); break;
362
- case 'showToast': alert(d.message || ''); break;
363
- case 'goBack': { const p = $('#chat-page'); if (p) closeChat(); else openSettings(); break; }
364
- default: break;
365
- }
366
- window.__mobileTouch?.('ui', msg.action);
367
- };
368
- es.onerror = () => { /* SSE 断线静默重连 */ };
369
- } catch (e) { /* 忽略 */ }
1163
+ const r = await api.post('/api/peers/add', { addr });
1164
+ alert(r && r.ok ? (r.connected ? '已连接好友' : '已记录好友地址, 连接中...') : ((r && r.error) || '添加失败'));
1165
+ if (currentTab === 'network') loadContacts();
1166
+ } catch (e) {
1167
+ alert('添加失败: ' + (e.message || e));
1168
+ }
1169
+ }
1170
+ function addFriendScan() {
1171
+ hideSheet('#addfriend-sheet');
1172
+ alert('扫码添加 (真机可用相机扫码)');
1173
+ }
1174
+
1175
+ // === 菜单 ===
1176
+ function bindMenu() {
1177
+ $('#item-settings').addEventListener('click', openSettings);
1178
+ $('#item-wallet').addEventListener('click', () => { openWallet(); });
1179
+ $('#item-judgments').addEventListener('click', () => { alert('判断力 API'); });
1180
+ // 头顶身份栏: 头像可改, 右侧卡片开身份介绍页; DID 不再作为标签按钮
1181
+ $('#me-avatar').addEventListener('click', pickAvatar);
1182
+ $('#profile-info').addEventListener('click', openIdentityPage);
1183
+ bindAvatarInput();
1184
+ $('#item-login').addEventListener('click', async () => {
1185
+ try {
1186
+ const s = await api.get('/api/auth/status');
1187
+ const did = (s && (s.did || s.didShort)) || '未生成';
1188
+ alert('本机 DID: ' + did + '\n\nOrbitDB 身份同步(待接入):手机生成 DID 后经 P2P 推给桌面 publish。');
1189
+ } catch (e) { alert('DID: 获取失败'); }
1190
+ });
1191
+ $('#item-logout').addEventListener('click', () => { api.post('/api/auth/logout', {}).then(() => loadMe()); });
1192
+ $('#btn-add').addEventListener('click', addFriend);
1193
+ const cs = $('#btn-create-session'); if (cs) cs.addEventListener('click', createSession);
1194
+ const csScan = $('#choice-scan'); if (csScan) csScan.addEventListener('click', addFriendScan);
1195
+ const csMan = $('#choice-manual'); if (csMan) csMan.addEventListener('click', addFriendManual);
1196
+ const csCan = $('#choice-cancel'); if (csCan) csCan.addEventListener('click', () => hideSheet('#addfriend-sheet'));
1197
+ $('#item-p2p').addEventListener('click', () => { switchTab('network'); });
1198
+ $('#item-p2p-id').addEventListener('click', async () => {
1199
+ try { const net = await api.get('/api/network/status'); const p2p = net && net.nodeId; alert('P2P ID (通信ID, ≠ DID):\n' + (p2p || '未连接')); }
1200
+ catch (e) { alert('P2P ID: 获取失败'); }
1201
+ });
1202
+ }
1203
+
1204
+ function escapeHtml(s) {
1205
+ return String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
1206
+ }
1207
+
1208
+ window.__mobileTouch = (type, data) => {};
1209
+
1210
+ function setupUiControl() {
1211
+ if (!core?.events?.subscribe) return;
1212
+ core.events.subscribe((msg) => {
1213
+ if (!msg || msg.type !== 'ui' || !msg.action) return;
1214
+ const d = msg.data || {};
1215
+ switch (msg.action) {
1216
+ case 'switchTab': if (d.tab && ['main', 'network', 'me'].includes(d.tab)) switchTab(d.tab); break;
1217
+ case 'openSettings': openSettings(); break;
1218
+ case 'showToast': alert(d.message || ''); break;
1219
+ case 'goBack': closeCardDetail(); closeChat(); break;
1220
+ default: break;
1221
+ }
1222
+ });
370
1223
  }
371
1224
 
372
1225
  function init() {
373
1226
  bindMenu();
374
- applyTheme(localStorage.getItem('bolloon_theme') || 'dark');
375
- switchTab('wechat');
1227
+ applyTheme(resolveTheme(), false);
1228
+ switchTab('main');
376
1229
  setupUiControl();
377
- loadConversations();
378
- loadContacts();
1230
+ loadAgentCovers();
379
1231
  loadMe();
1232
+ if (core?.network?.start) core.network.start().catch(() => {});
380
1233
  }
381
1234
  document.addEventListener('DOMContentLoaded', init);
382
1235
  if (document.readyState !== 'loading') init();