@bolloon/bolloon-agent 0.4.14 → 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 (213) 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/constraint-layer.js +19 -19
  6. package/dist/agents/judgment-protocol.js +14 -14
  7. package/dist/agents/pi-sdk-types.js +12 -12
  8. package/dist/agents/pi-sdk.js +101 -101
  9. package/dist/agents/skill-organizer.js +19 -19
  10. package/dist/agents/workflow-pivot-loop.js +2 -2
  11. package/dist/agents/x402/x402Pay.js +4 -0
  12. package/dist/bollharness/src/scripts/deploy-guard.js +8 -8
  13. package/dist/bollharness-integration/context-router-judgment.js +4 -4
  14. package/dist/bollharness-integration/context-router.js +292 -292
  15. package/dist/bollharness-integration/gate-state-machine.js +13 -13
  16. package/dist/bollharness-integration/llm-judgment-engine.js +134 -134
  17. package/dist/bollharness-integration/skill-adapter.js +21 -21
  18. package/dist/bootstrap/chat-archiver.js +14 -14
  19. package/dist/bootstrap/context-os.js +24 -24
  20. package/dist/bootstrap/memory-compressor.js +13 -13
  21. package/dist/cli-entry.js +36 -36
  22. package/dist/constraint-runtime/reference_data/archive_surface_snapshot.json +63 -0
  23. package/dist/constraint-runtime/reference_data/commands_snapshot.json +1037 -0
  24. package/dist/constraint-runtime/reference_data/subsystems/assistant.json +8 -0
  25. package/dist/constraint-runtime/reference_data/subsystems/bootstrap.json +8 -0
  26. package/dist/constraint-runtime/reference_data/subsystems/bridge.json +32 -0
  27. package/dist/constraint-runtime/reference_data/subsystems/buddy.json +13 -0
  28. package/dist/constraint-runtime/reference_data/subsystems/cli.json +26 -0
  29. package/dist/constraint-runtime/reference_data/subsystems/components.json +32 -0
  30. package/dist/constraint-runtime/reference_data/subsystems/constants.json +28 -0
  31. package/dist/constraint-runtime/reference_data/subsystems/coordinator.json +8 -0
  32. package/dist/constraint-runtime/reference_data/subsystems/entrypoints.json +15 -0
  33. package/dist/constraint-runtime/reference_data/subsystems/hooks.json +32 -0
  34. package/dist/constraint-runtime/reference_data/subsystems/keybindings.json +21 -0
  35. package/dist/constraint-runtime/reference_data/subsystems/memdir.json +15 -0
  36. package/dist/constraint-runtime/reference_data/subsystems/migrations.json +18 -0
  37. package/dist/constraint-runtime/reference_data/subsystems/moreright.json +8 -0
  38. package/dist/constraint-runtime/reference_data/subsystems/native_ts.json +11 -0
  39. package/dist/constraint-runtime/reference_data/subsystems/outputStyles.json +8 -0
  40. package/dist/constraint-runtime/reference_data/subsystems/plugins.json +9 -0
  41. package/dist/constraint-runtime/reference_data/subsystems/remote.json +11 -0
  42. package/dist/constraint-runtime/reference_data/subsystems/schemas.json +8 -0
  43. package/dist/constraint-runtime/reference_data/subsystems/screens.json +10 -0
  44. package/dist/constraint-runtime/reference_data/subsystems/server.json +10 -0
  45. package/dist/constraint-runtime/reference_data/subsystems/services.json +32 -0
  46. package/dist/constraint-runtime/reference_data/subsystems/skills.json +27 -0
  47. package/dist/constraint-runtime/reference_data/subsystems/state.json +13 -0
  48. package/dist/constraint-runtime/reference_data/subsystems/types.json +18 -0
  49. package/dist/constraint-runtime/reference_data/subsystems/upstreamproxy.json +9 -0
  50. package/dist/constraint-runtime/reference_data/subsystems/utils.json +32 -0
  51. package/dist/constraint-runtime/reference_data/subsystems/vim.json +12 -0
  52. package/dist/constraint-runtime/reference_data/subsystems/voice.json +8 -0
  53. package/dist/constraint-runtime/reference_data/tools_snapshot.json +922 -0
  54. package/dist/context-compaction/auto-compact.js +7 -7
  55. package/dist/electron/config.js.map +1 -0
  56. package/dist/electron/dialogs.js.map +1 -0
  57. package/dist/electron/first-run.js +54 -54
  58. package/dist/electron/first-run.js.map +1 -0
  59. package/dist/electron/ipc.js.map +1 -0
  60. package/dist/electron/logger.js.map +1 -0
  61. package/dist/electron/main.js.map +1 -0
  62. package/dist/electron/menu.js.map +1 -0
  63. package/dist/electron/paths.js.map +1 -0
  64. package/dist/electron/server.js.map +1 -0
  65. package/dist/electron/tray.js.map +1 -0
  66. package/dist/electron/window.js.map +1 -0
  67. package/dist/electron-build/electron/first-run.js +54 -54
  68. package/dist/electron-preload.js.map +1 -1
  69. package/dist/electron.js.map +1 -1
  70. package/dist/index.js +112 -112
  71. package/dist/llm/llm-judgment-client.js +102 -102
  72. package/dist/llm/pi-ai.js +48 -48
  73. package/dist/llm/system-prompt/layers/channel/human-async.md +41 -41
  74. package/dist/llm/system-prompt/layers/channel/local.md +14 -14
  75. package/dist/llm/system-prompt/layers/channel/p2p-agent.md +18 -18
  76. package/dist/llm/system-prompt/layers/channel/p2p-peer-sync.md +51 -51
  77. package/dist/llm/system-prompt/layers/channel/p2p-proactive.md +43 -43
  78. package/dist/llm/system-prompt/layers/channel/p2p-visitor.md +19 -19
  79. package/dist/llm/system-prompt/layers/channel/session-handoff.md +61 -61
  80. package/dist/llm/system-prompt/layers/core/artifacts_storage.md +89 -89
  81. package/dist/llm/system-prompt/layers/core/evenhandedness.md +21 -21
  82. package/dist/llm/system-prompt/layers/core/external-engagement.md +73 -73
  83. package/dist/llm/system-prompt/layers/core/hibs_reminders.md +15 -15
  84. package/dist/llm/system-prompt/layers/core/identity.md +48 -48
  85. package/dist/llm/system-prompt/layers/core/knowledge.md +17 -17
  86. package/dist/llm/system-prompt/layers/core/memory_system.md +12 -12
  87. package/dist/llm/system-prompt/layers/core/network_filesystem.md +28 -28
  88. package/dist/llm/system-prompt/layers/core/refusal.md +37 -37
  89. package/dist/llm/system-prompt/layers/core/tone.md +31 -31
  90. package/dist/llm/system-prompt/layers/core/tools.thin.md +13 -13
  91. package/dist/llm/system-prompt/layers/core/wellbeing.md +41 -41
  92. package/dist/llm/system-prompt/layers/role/architect.md +20 -20
  93. package/dist/llm/system-prompt/layers/role/expert.md +19 -19
  94. package/dist/llm/system-prompt/layers/role/implementer.md +15 -15
  95. package/dist/llm/system-prompt/layers/role/security.md +15 -15
  96. package/dist/llm/system-prompt/layers/tool/artifacts.md +72 -72
  97. package/dist/llm/system-prompt/layers/tool/bash.md +25 -25
  98. package/dist/llm/system-prompt/layers/tool/goal_handoff.md +77 -77
  99. package/dist/llm/system-prompt/layers/tool/hibs_api.md +171 -171
  100. package/dist/llm/system-prompt/layers/tool/image_search.md +70 -70
  101. package/dist/llm/system-prompt/layers/tool/manifest.md +89 -89
  102. package/dist/llm/system-prompt/layers/tool/mcp_apps.md +53 -53
  103. package/dist/llm/system-prompt/layers/tool/p2p_request.md +61 -61
  104. package/dist/llm/system-prompt/layers/tool/web_search.md +83 -83
  105. package/dist/llm/tool-manifest/ask_user_input.js +1 -1
  106. package/dist/llm/tool-manifest/bash.js +3 -3
  107. package/dist/llm/tool-manifest/create_file.js +4 -4
  108. package/dist/llm/tool-manifest/fetch_sports_data.js +3 -3
  109. package/dist/llm/tool-manifest/image_search.js +2 -2
  110. package/dist/llm/tool-manifest/index.js +59 -0
  111. package/dist/llm/tool-manifest/mcp.js +2 -2
  112. package/dist/llm/tool-manifest/message_compose.js +3 -3
  113. package/dist/llm/tool-manifest/places.js +2 -2
  114. package/dist/llm/tool-manifest/present_files.js +1 -1
  115. package/dist/llm/tool-manifest/recipe.js +2 -2
  116. package/dist/llm/tool-manifest/str_replace.js +5 -5
  117. package/dist/llm/tool-manifest/view.js +3 -3
  118. package/dist/llm/tool-manifest/weather.js +4 -4
  119. package/dist/llm/tool-manifest/web.js +4 -4
  120. package/dist/network/p2p.js +78 -4
  121. package/dist/pi-ecosystem-judgment/causal-judge.js +51 -51
  122. package/dist/pi-ecosystem-judgment/distill-prompt.js +42 -42
  123. package/dist/pi-ecosystem-judgment/distillation.js +14 -14
  124. package/dist/pi-ecosystem-judgment/index.js +1 -1
  125. package/dist/pi-ecosystem-judgment/monitor-gate.js +28 -28
  126. package/dist/pi-ecosystem-judgment/value-injection.js +102 -102
  127. package/dist/security/context-router-tool.js +15 -15
  128. package/dist/utils/auto-update.js.map +1 -0
  129. package/dist/web/a2ui-client.js +1594 -223
  130. package/dist/web/api-config.html +779 -779
  131. package/dist/web/client-hearth.js +10 -10
  132. package/dist/web/client.js.map +7 -0
  133. package/dist/web/components/wallet-viem.mjs +118 -118
  134. package/dist/web/index.html +502 -502
  135. package/dist/web/manifest.json +20 -20
  136. package/dist/web/mobile-agent.js +397 -0
  137. package/dist/web/mobile-core.js +86235 -0
  138. package/dist/web/mobile-data.js +346 -0
  139. package/dist/web/mobile-http-api.js +71 -0
  140. package/dist/web/mobile-p2p.js +250 -0
  141. package/dist/web/mobile-payments.js +80 -0
  142. package/dist/web/mobile-wallet.js +226 -0
  143. package/dist/web/mobile.css +483 -6
  144. package/dist/web/mobile.html +99 -50
  145. package/dist/web/mobile.js +1040 -187
  146. package/dist/web/server.js +13 -13
  147. package/dist/web/style.css +4946 -4946
  148. package/package.json +201 -198
  149. package/scripts/build-cli.js +215 -215
  150. package/scripts/build-web.ts +162 -149
  151. package/scripts/postinstall.js +152 -152
  152. package/bin/bolloon.js +0 -157
  153. package/dist/agents/python-exec.js +0 -115
  154. package/dist/pi-ecosystem-colony/index.js +0 -365
  155. package/dist/social/ant-colony/AdaptiveHeartbeat.js +0 -101
  156. package/dist/social/ant-colony/PheromoneEngine.js +0 -227
  157. package/dist/social/ant-colony/index.js +0 -6
  158. package/dist/social/ant-colony/types.js +0 -24
  159. package/dist/storage/trajectory.js +0 -101
  160. package/dist/test/ai-judgment-test.js +0 -80
  161. package/dist/test/bollharness-integration.test.js +0 -318
  162. package/dist/test/channel-agent-multi-dialogue.js +0 -205
  163. package/dist/test/channel-heartbeat-agent-test.js +0 -201
  164. package/dist/test/constraint-layer.test.js +0 -164
  165. package/dist/test/diap-identity-test.js +0 -172
  166. package/dist/test/diap-quick-test.js +0 -62
  167. package/dist/test/global-shared-context.test.js +0 -315
  168. package/dist/test/harness-judgment-injection.test.js +0 -246
  169. package/dist/test/harness-workflow-integrator-test.js +0 -228
  170. package/dist/test/human-value-store.test.js +0 -243
  171. package/dist/test/hybrid-integration-test.js +0 -118
  172. package/dist/test/hybrid-messenger-verify.js +0 -55
  173. package/dist/test/iroh-bistream-debug.js +0 -38
  174. package/dist/test/iroh-communication.test.js +0 -66
  175. package/dist/test/iroh-debug-test.js +0 -57
  176. package/dist/test/iroh-diap-test.js +0 -71
  177. package/dist/test/iroh-direct-connect.js +0 -55
  178. package/dist/test/iroh-e2e-fixed.js +0 -89
  179. package/dist/test/iroh-e2e-same-process.js +0 -63
  180. package/dist/test/iroh-e2e.js +0 -66
  181. package/dist/test/iroh-final-e2e.js +0 -72
  182. package/dist/test/iroh-relay-test.js +0 -37
  183. package/dist/test/iroh-simple-test.js +0 -41
  184. package/dist/test/iroh-transport-verify.js +0 -54
  185. package/dist/test/iroh-transport.test.js +0 -37
  186. package/dist/test/iroh-two-nodes.js +0 -70
  187. package/dist/test/iroh-verify.js +0 -44
  188. package/dist/test/judgment-decision.test.js +0 -219
  189. package/dist/test/llm-judgment-integration.test.js +0 -220
  190. package/dist/test/p2p-agent-complex-dialogue.js +0 -385
  191. package/dist/test/p2p-agent-dialogue.js +0 -341
  192. package/dist/test/p2p-agent-full-bidirectional.js +0 -510
  193. package/dist/test/p2p-agent-harness-flow.js +0 -437
  194. package/dist/test/p2p-agent-harness-single.js +0 -143
  195. package/dist/test/p2p-ai-dialogue-test.js +0 -318
  196. package/dist/test/p2p-cid-connect-test.js +0 -195
  197. package/dist/test/p2p-connect-receiver.js +0 -69
  198. package/dist/test/p2p-doc-transfer.js +0 -110
  199. package/dist/test/p2p-identity-page-test.js +0 -77
  200. package/dist/test/p2p-iroh-test.js +0 -171
  201. package/dist/test/p2p-minimal-test.js +0 -241
  202. package/dist/test/p2p-node-1.js +0 -148
  203. package/dist/test/p2p-node-2.js +0 -148
  204. package/dist/test/p2p-server.js +0 -281
  205. package/dist/test/p2p-two-nodes-test.js +0 -438
  206. package/dist/test/pi-sdk.test.js +0 -44
  207. package/dist/test/set-persona.js +0 -40
  208. package/dist/test/simple.test.js +0 -9
  209. package/dist/test/storage-integration.test.js +0 -150
  210. package/dist/test/subagent-manager.test.js +0 -276
  211. package/dist/test/test-gate-flow.test.js +0 -81
  212. package/dist/test/workflow-engine.test.js +0 -87
  213. package/dist/test/workflow-pivot-loop.test.js +0 -246
@@ -0,0 +1,346 @@
1
+ /**
2
+ * mobile-data.ts — 手机端数据同步层 (2026-08-15)
3
+ *
4
+ * 独立子系统 #1: 手机是"独立存储副本", 不是桌面的缓存.
5
+ * - IndexedDB 是手机的本地方言数据 (channels / sessions / messages)
6
+ * - 与远端 (桌面/其他节点) 通过 P2P 协议双向同步
7
+ * - 协议类型 (复用桌面消息协议格式 DID:<did>|type:payload):
8
+ * data.sync : 请求全量 (channels + 所有 session) → 响应 data.snapshot
9
+ * data.snapshot : 全量快照 (channels + sessions)
10
+ * data.channels : channels 列表 (增量更新)
11
+ * data.session : 单个 session (增量更新, 合并去重)
12
+ * data.pull : 拉取指定 channelId 的 session → 响应 data.session
13
+ * data.llm-config : 请求 LLM 配置 (桌面 llm-config.json 同步) → 响应 data.llm-config.reply
14
+ * data.llm-config.reply: 收到远端 LLM 配置 → 保存 + 通知 agent 层注入
15
+ * - 与 agent 功能无关: 本层只做数据一致, 不跑 LLM. LLM 配置同步是"数据"不是"智能".
16
+ *
17
+ * 离线: 读写本地 IndexedDB, 静默.
18
+ * 在线: 通过 mobile-p2p 与 peer 同步, 冲突按 ts 最新合并.
19
+ */
20
+ // ============ IndexedDB 存储 (独立于 agent 层) ============
21
+ const DB_NAME = 'bolloon-mobile-data';
22
+ const DB_VERSION = 1;
23
+ let _db = null;
24
+ function openDb() {
25
+ if (_db)
26
+ return Promise.resolve(_db);
27
+ return new Promise((resolve, reject) => {
28
+ const req = indexedDB.open(DB_NAME, DB_VERSION);
29
+ req.onupgradeneeded = () => {
30
+ const db = req.result;
31
+ if (!db.objectStoreNames.contains('kv')) {
32
+ db.createObjectStore('kv');
33
+ }
34
+ };
35
+ req.onsuccess = () => { _db = req.result; resolve(_db); };
36
+ req.onerror = () => reject(req.error);
37
+ });
38
+ }
39
+ /** 测试用: 关闭并清空当前库 (避免 deleteDatabase 阻塞) */
40
+ export async function resetDataDb() {
41
+ if (_db) {
42
+ _db.close();
43
+ _db = null;
44
+ }
45
+ await new Promise((resolve) => {
46
+ try {
47
+ const req = indexedDB.deleteDatabase(DB_NAME);
48
+ req.onsuccess = () => resolve();
49
+ req.onerror = () => resolve();
50
+ req.onblocked = () => resolve();
51
+ }
52
+ catch {
53
+ resolve();
54
+ }
55
+ });
56
+ }
57
+ async function idbGet(key) {
58
+ const db = await openDb();
59
+ return new Promise((resolve) => {
60
+ const tx = db.transaction('kv', 'readonly');
61
+ const req = tx.objectStore('kv').get(key);
62
+ req.onsuccess = () => resolve(req.result ?? null);
63
+ req.onerror = () => resolve(null);
64
+ });
65
+ }
66
+ async function idbSet(key, val) {
67
+ const db = await openDb();
68
+ return new Promise((resolve, reject) => {
69
+ const tx = db.transaction('kv', 'readwrite');
70
+ tx.objectStore('kv').put(val, key);
71
+ tx.oncomplete = () => resolve();
72
+ tx.onerror = () => reject(tx.error);
73
+ });
74
+ }
75
+ /** 手机端默认 LLM 配置 (未同步时用, 对应 Kotlin AgentLlmConfig 默认 deepseek) */
76
+ export function defaultLlmConfig() {
77
+ return {
78
+ activeProvider: 'deepseek',
79
+ providers: {
80
+ deepseek: { enabled: true, apiKey: '', baseUrl: 'https://api.deepseek.com/v1', model: 'deepseek-chat', temperature: 0.7, maxTokens: 4096, requiresApiKey: true },
81
+ },
82
+ };
83
+ }
84
+ /** 从 LlmConfigSnapshot 提取对 agent 层最有用的单 provider 配置 (activeProvider 优先) */
85
+ export function activeProviderConfig(cfg) {
86
+ if (!cfg)
87
+ return {};
88
+ const name = cfg.activeProvider || 'deepseek';
89
+ const p = cfg.providers?.[name] || cfg.providers?.deepseek;
90
+ return { baseUrl: p?.baseUrl, apiKey: p?.apiKey, model: p?.model, maxTokens: p?.maxTokens };
91
+ }
92
+ // ============ 本地存储 API (手机独立副本) ============
93
+ /** 读取 channels (本地) */
94
+ export async function getChannels() {
95
+ return (await idbGet('channels')) || [];
96
+ }
97
+ /** 保存 channels (本地) */
98
+ export async function saveChannels(channels) {
99
+ await idbSet('channels', channels);
100
+ }
101
+ /** 读取单个 session (本地), 不存在返回空 */
102
+ export async function getSession(channelId) {
103
+ const s = await idbGet('session:' + channelId);
104
+ if (s)
105
+ return s;
106
+ return { channelId, messages: [], updatedAt: 0 };
107
+ }
108
+ /** 保存单个 session (本地, 按 ts 去重合并) */
109
+ export async function saveSession(session) {
110
+ const prev = await getSession(session.channelId);
111
+ const merged = [];
112
+ const seen = new Set();
113
+ for (const m of [...(prev.messages || []), ...(session.messages || [])]) {
114
+ const k = m.role + ':' + m.content + ':' + (m.ts || 0);
115
+ if (!seen.has(k)) {
116
+ seen.add(k);
117
+ merged.push(m);
118
+ }
119
+ }
120
+ merged.sort((a, b) => (a.ts || 0) - (b.ts || 0));
121
+ await idbSet('session:' + session.channelId, {
122
+ channelId: session.channelId,
123
+ messages: merged,
124
+ updatedAt: Math.max(prev.updatedAt || 0, session.updatedAt || 0, Date.now()),
125
+ });
126
+ }
127
+ /** 追加消息到 session (本地) */
128
+ export async function appendMessage(channelId, msg) {
129
+ const s = await getSession(channelId);
130
+ s.messages.push(msg);
131
+ await saveSession(s);
132
+ }
133
+ /** 全量快照 (本地当前状态, 用于响应 data.sync / data.pull) */
134
+ export async function snapshot() {
135
+ return {
136
+ channels: await getChannels(),
137
+ sessions: await listSessions(),
138
+ syncedAt: Date.now(),
139
+ };
140
+ }
141
+ // ============ LLM 配置存储 (桌面同步 / 手机默认) ============
142
+ /** 读取本地 LLM 配置, 无则返回默认 (手机端) */
143
+ export async function getLlmConfig() {
144
+ const c = await idbGet('llm-config');
145
+ return c || defaultLlmConfig();
146
+ }
147
+ /** 保存本地 LLM 配置 */
148
+ export async function saveLlmConfig(cfg) {
149
+ const merged = { ...defaultLlmConfig(), ...cfg, updatedAt: cfg.updatedAt || Date.now() };
150
+ await idbSet('llm-config', merged);
151
+ }
152
+ /** 是否有过桌面同步的 LLM 配置 (未同步时 mobile-agent 用手机默认) */
153
+ export async function hasSyncedLlmConfig() {
154
+ const c = await idbGet('llm-config');
155
+ return !!(c && (c.updatedAt || c.providers));
156
+ }
157
+ const llmConfigListeners = new Set();
158
+ /** 注册 LLM 配置变更回调 */
159
+ export function onLlmConfig(fn) {
160
+ llmConfigListeners.add(fn);
161
+ }
162
+ function notifyLlmConfig(cfg) {
163
+ for (const fn of llmConfigListeners) {
164
+ try {
165
+ fn(cfg);
166
+ }
167
+ catch { /* 忽略 */ }
168
+ }
169
+ }
170
+ /** 向 peer 请求 LLM 配置 (data.llm-config → data.llm-config.reply) */
171
+ export async function requestLlmConfigFromPeer(peerId) {
172
+ const send = getSend();
173
+ if (!send)
174
+ return false;
175
+ return await send('data.llm-config', JSON.stringify({}), peerId);
176
+ }
177
+ /** 列出所有 session (遍历 kv) */
178
+ export async function listSessions() {
179
+ const db = await openDb();
180
+ return new Promise((resolve) => {
181
+ const tx = db.transaction('kv', 'readonly');
182
+ const req = tx.objectStore('kv').openCursor();
183
+ const out = [];
184
+ req.onsuccess = () => {
185
+ const c = req.result;
186
+ if (c) {
187
+ if (typeof c.key === 'string' && c.key.startsWith('session:')) {
188
+ out.push(c.value);
189
+ }
190
+ c.continue();
191
+ }
192
+ else {
193
+ resolve(out);
194
+ }
195
+ };
196
+ req.onerror = () => resolve(out);
197
+ });
198
+ }
199
+ // ============ 合并远端数据 (增量同步核心) ============
200
+ /** 合并远端快照: channels 以 ts 新者优先; sessions 逐条合并去重 */
201
+ export async function mergeSnapshot(remote) {
202
+ const localCh = await getChannels();
203
+ const chMap = new Map();
204
+ for (const c of localCh)
205
+ chMap.set(c.id, c);
206
+ for (const c of remote.channels || []) {
207
+ const prev = chMap.get(c.id);
208
+ if (!prev || (c.ts || 0) >= (prev.ts || 0))
209
+ chMap.set(c.id, c);
210
+ }
211
+ const mergedChannels = chMap.size - localCh.length;
212
+ await saveChannels(Array.from(chMap.values()));
213
+ let mergedSessions = 0;
214
+ for (const s of remote.sessions || []) {
215
+ const prev = await getSession(s.channelId);
216
+ if ((s.updatedAt || 0) >= (prev.updatedAt || 0)) {
217
+ await saveSession(s);
218
+ mergedSessions++;
219
+ }
220
+ }
221
+ return { mergedChannels, mergedSessions };
222
+ }
223
+ /** 合并单个远端 session */
224
+ export async function mergeRemoteSession(s) {
225
+ const prev = await getSession(s.channelId);
226
+ if ((s.updatedAt || 0) >= (prev.updatedAt || 0)) {
227
+ await saveSession(s);
228
+ return true;
229
+ }
230
+ return false;
231
+ }
232
+ let _send = null;
233
+ /** 由 mobile-core 注入发送函数 (对 mobile-p2p 的封装) */
234
+ export function setDataTransport(fn) {
235
+ _send = fn;
236
+ }
237
+ function getSend() {
238
+ return _send;
239
+ }
240
+ let _syncStatus = { lastSyncAt: 0, mergedChannels: 0, mergedSessions: 0, mode: 'offline' };
241
+ export function syncStatus() {
242
+ return { ..._syncStatus };
243
+ }
244
+ /** 向 peer 请求全量同步 (data.sync → data.snapshot), 成功则合并 */
245
+ export async function syncFromPeer(peerId, timeoutMs = 8000) {
246
+ const send = getSend();
247
+ if (!send)
248
+ return { ..._syncStatus, mode: 'offline' };
249
+ const ok = await send('data.sync', JSON.stringify({ since: _syncStatus.lastSyncAt }), peerId);
250
+ if (!ok)
251
+ return { ..._syncStatus, mode: 'offline' };
252
+ // 响应 data.snapshot 由 mobile-core 的 onMessage 分发到 mergeRemoteSnapshot
253
+ return { ..._syncStatus, mode: 'online', lastSyncAt: Date.now() };
254
+ }
255
+ /** 处理入站 data.* 消息 (由 mobile-core 的 P2P 消息路由调用) */
256
+ export async function handleIncomingDataMessage(type, payload, fromPeer) {
257
+ const send = getSend();
258
+ switch (type) {
259
+ case 'data.sync': {
260
+ // 远端请求全量 → 回 data.snapshot
261
+ if (send) {
262
+ const snap = await snapshot();
263
+ await send('data.snapshot', JSON.stringify(snap), fromPeer);
264
+ }
265
+ break;
266
+ }
267
+ case 'data.snapshot': {
268
+ // 远端快照 → 合并本地
269
+ try {
270
+ const snap = JSON.parse(payload);
271
+ const r = await mergeSnapshot(snap);
272
+ _syncStatus = { lastSyncAt: Date.now(), mergedChannels: r.mergedChannels, mergedSessions: r.mergedSessions, mode: 'online' };
273
+ }
274
+ catch { /* 解析失败忽略 */ }
275
+ break;
276
+ }
277
+ case 'data.channels': {
278
+ try {
279
+ const chs = JSON.parse(payload);
280
+ await saveChannels(chs);
281
+ _syncStatus = { ..._syncStatus, mergedChannels: chs.length, mode: 'online' };
282
+ }
283
+ catch { /* 忽略 */ }
284
+ break;
285
+ }
286
+ case 'data.session': {
287
+ try {
288
+ const s = JSON.parse(payload);
289
+ const merged = await mergeRemoteSession(s);
290
+ _syncStatus = { ..._syncStatus, mergedSessions: merged ? 1 : 0, mode: 'online' };
291
+ }
292
+ catch { /* 忽略 */ }
293
+ break;
294
+ }
295
+ case 'data.pull': {
296
+ // 远端拉取指定 session → 回 data.session
297
+ if (send) {
298
+ try {
299
+ const { channelId } = JSON.parse(payload);
300
+ const s = await getSession(channelId);
301
+ await send('data.session', JSON.stringify(s), fromPeer);
302
+ }
303
+ catch { /* 忽略 */ }
304
+ }
305
+ break;
306
+ }
307
+ case 'data.llm-config': {
308
+ // 远端请求本机 LLM 配置 → 回 data.llm-config.reply
309
+ if (send) {
310
+ try {
311
+ const cfg = await getLlmConfig();
312
+ await send('data.llm-config.reply', JSON.stringify(cfg), fromPeer);
313
+ }
314
+ catch { /* 忽略 */ }
315
+ }
316
+ break;
317
+ }
318
+ case 'data.llm-config.reply': {
319
+ // 收到远端 LLM 配置 → 保存 + 通知 agent 层注入 bridge
320
+ try {
321
+ const cfg = JSON.parse(payload);
322
+ if (cfg && (cfg.providers || cfg.activeProvider)) {
323
+ await saveLlmConfig(cfg);
324
+ notifyLlmConfig(await getLlmConfig());
325
+ }
326
+ }
327
+ catch { /* 解析失败忽略 */ }
328
+ break;
329
+ }
330
+ default:
331
+ break;
332
+ }
333
+ }
334
+ /** 广播本地最新状态给所有已连接 peer (主动推送) */
335
+ export async function pushLocal(toPeer) {
336
+ const send = getSend();
337
+ if (!send)
338
+ return;
339
+ const chs = await getChannels();
340
+ await send('data.channels', JSON.stringify(chs), toPeer);
341
+ const sessions = await listSessions();
342
+ for (const s of sessions) {
343
+ await send('data.session', JSON.stringify(s), toPeer);
344
+ }
345
+ }
346
+ export default { getChannels, saveChannels, getSession, saveSession, appendMessage, snapshot, mergeSnapshot, syncFromPeer, pushLocal, syncStatus, handleIncomingDataMessage, setDataTransport, getLlmConfig, saveLlmConfig, hasSyncedLlmConfig, onLlmConfig, requestLlmConfigFromPeer, defaultLlmConfig, activeProviderConfig };
@@ -0,0 +1,71 @@
1
+ /**
2
+ * mobile-http-api.ts — 手机端本地 HTTP API (2026-08-15)
3
+ *
4
+ * 手机是自治节点: 外部控制端 (桌面/真机验证脚本/局域网工具) 经 HTTP 触发
5
+ * 手机端独立 AgentLoop 执行, 不需要经过电脑同意.
6
+ *
7
+ * 端点:
8
+ * GET /health 存活检查
9
+ * GET /api/phone/status Agent 状态 (did/mode/llm/capabilities)
10
+ * POST /api/phone/agent/run { goal, requestId? } → 手机自治执行 → { ok, result, mode }
11
+ * POST /api/phone/agent/cancel { reason? } → 取消当前任务
12
+ *
13
+ * 运行环境:
14
+ * - Capacitor WebView (浏览器): 无 node:http, 提供 handleHttpRequest (fetch 风格) 供原生 HTTP server 调用
15
+ * - Node (测试/真机验证脚本): startLocalHttpServer() 起真实 localhost server
16
+ */
17
+ import core from './mobile-core.js';
18
+ /** 处理一个 HTTP 请求 (返回 fetch 风格 Response 或对象) — 供 WebView 原生 HTTP server / Node server 共用 */
19
+ export async function handleHttpRequest(method, path, bodyStr) {
20
+ if (method === 'GET' && (path === '/' || path === '/health')) {
21
+ return { status: 200, body: { ok: true, service: 'bolloon-mobile', time: Date.now() } };
22
+ }
23
+ if (method === 'GET' && path === '/api/phone/status') {
24
+ const st = await core.phone.status();
25
+ return { status: 200, body: st };
26
+ }
27
+ if (method === 'POST' && path === '/api/phone/agent/run') {
28
+ const req = JSON.parse(bodyStr || '{}');
29
+ const result = await core.phone.run(String(req.goal || ''));
30
+ return { status: 200, body: { requestId: req.requestId || '', ...result } };
31
+ }
32
+ if (method === 'POST' && path === '/api/phone/agent/cancel') {
33
+ const req = JSON.parse(bodyStr || '{}');
34
+ const r = await core.phone.cancel(String(req.reason || '本地取消'));
35
+ return { status: 200, body: r };
36
+ }
37
+ return { status: 404, body: { error: `no route: ${method} ${path}` } };
38
+ }
39
+ /** Node 环境: 起真实 localhost HTTP server (真机验证/测试用). 浏览器环境自动降级为 null */
40
+ export async function startLocalHttpServer(port = 7788) {
41
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
42
+ const hasNode = typeof process !== 'undefined' && !!(process.versions?.node);
43
+ if (!hasNode) {
44
+ console.log('[mobile-http-api] 非 Node 环境, 不启动本地 HTTP server (走 handleHttpRequest 供原生层)');
45
+ return null;
46
+ }
47
+ const http = await import('node:http');
48
+ const server = http.createServer(async (req, res) => {
49
+ const url = new URL(req.url || '/', 'http://localhost');
50
+ let body = '';
51
+ req.on('data', (c) => { body += c; });
52
+ req.on('end', async () => {
53
+ try {
54
+ const r = await handleHttpRequest(req.method || 'GET', url.pathname, body);
55
+ res.writeHead(r.status, { 'Content-Type': 'application/json; charset=utf-8' });
56
+ res.end(JSON.stringify(r.body));
57
+ }
58
+ catch (e) {
59
+ res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
60
+ res.end(JSON.stringify({ error: String(e?.message || e).slice(0, 100) }));
61
+ }
62
+ });
63
+ });
64
+ await new Promise((resolve) => server.listen(port, '127.0.0.1', () => resolve()));
65
+ console.log(`[mobile-http-api] 本地 HTTP API 已启动: http://127.0.0.1:${port}`);
66
+ return {
67
+ close() { server.close(); },
68
+ port,
69
+ };
70
+ }
71
+ export default { handleHttpRequest, startLocalHttpServer };
@@ -0,0 +1,250 @@
1
+ /**
2
+ * mobile-p2p.ts — 手机端浏览器 P2P 节点 (2026-08-15, Phase 2)
3
+ *
4
+ * 目标: 让手机端 (Capacitor WebView) 真正独立 P2P 入网, 不依赖桌面.
5
+ *
6
+ * 与桌面 p2p.ts (node libp2p + tcp) 兼容:
7
+ * - 浏览器只能用 WebSockets 传输 (@libp2p/websockets) — 桌面已加 /ws listen
8
+ * - 同一协议栈: noise + yamux + gossipsub + kad-dht
9
+ * - 同一消息协议: '/agent/message' 流, 格式 "DID:<did>|type:payload"
10
+ *
11
+ * 连接方式: 手机 ws 连桌面节点 wss 地址 (joinNetwork 时传入), 或经 relay.
12
+ * 离线时静默降级为单机模式 (mobile-core 内置回复).
13
+ */
14
+ import { createLibp2p } from 'libp2p';
15
+ import { webSockets } from '@libp2p/websockets';
16
+ import { noise } from '@chainsafe/libp2p-noise';
17
+ import { yamux } from '@chainsafe/libp2p-yamux';
18
+ import { identify } from '@libp2p/identify';
19
+ import { kadDHT } from '@libp2p/kad-dht';
20
+ import { gossipsub } from '@libp2p/gossipsub';
21
+ import { circuitRelayTransport } from '@libp2p/circuit-relay-v2';
22
+ import { ping } from '@libp2p/ping';
23
+ import { multiaddr as createMultiaddr } from '@multiformats/multiaddr';
24
+ let node = null;
25
+ let state = { connected: false, peerCount: 0, peerIds: [] };
26
+ const msgHandlers = [];
27
+ /** 持久化好友地址 (localStorage), 供 addMobilePeer 写入 + startMobileP2P 自动重连 */
28
+ let peerAddrs = [];
29
+ function loadPeerAddrs() {
30
+ try {
31
+ peerAddrs = JSON.parse(localStorage.getItem('bolloon_mobile_peers') || '[]');
32
+ }
33
+ catch {
34
+ peerAddrs = [];
35
+ }
36
+ }
37
+ function savePeerAddrs() {
38
+ try {
39
+ localStorage.setItem('bolloon_mobile_peers', JSON.stringify(peerAddrs));
40
+ }
41
+ catch { }
42
+ }
43
+ loadPeerAddrs();
44
+ /** 创建浏览器 libp2p 节点 (websockets) */
45
+ export async function startMobileP2P(cfg = {}) {
46
+ if (node)
47
+ return state;
48
+ try {
49
+ node = await createLibp2p({
50
+ transports: [webSockets(), circuitRelayTransport()],
51
+ connectionEncrypters: [noise()],
52
+ streamMuxers: [yamux()],
53
+ services: {
54
+ identify: identify(),
55
+ ping: ping(),
56
+ dht: kadDHT({ clientMode: true }),
57
+ pubsub: gossipsub({ emitSelf: false }),
58
+ },
59
+ });
60
+ // 处理入站 '/agent/message' (libp2p 3.x handler 签名: (stream, connection))
61
+ node.handle('/agent/message', async (stream, connection) => {
62
+ const chunks = [];
63
+ for await (const chunk of stream) {
64
+ chunks.push(chunk instanceof Uint8Array ? chunk : chunk.subarray());
65
+ }
66
+ const data = new Uint8Array(chunks.reduce((acc, c) => acc + c.length, 0));
67
+ let off = 0;
68
+ for (const c of chunks) {
69
+ data.set(c, off);
70
+ off += c.length;
71
+ }
72
+ const text = new TextDecoder().decode(data);
73
+ const fromPeer = connection.remotePeer.toString();
74
+ console.log(`[mobile-p2p] 入站消息 ${text.slice(0, 60)} from ${fromPeer.slice(0, 10)}`);
75
+ for (const h of msgHandlers) {
76
+ try {
77
+ h(text, fromPeer);
78
+ }
79
+ catch { }
80
+ }
81
+ });
82
+ node.addEventListener('peer:connect', () => refreshState());
83
+ node.addEventListener('peer:disconnect', () => refreshState());
84
+ await node.start();
85
+ state = {
86
+ connected: true,
87
+ peerCount: 0,
88
+ peerIds: [],
89
+ nodeId: node.peerId.toString(),
90
+ };
91
+ // 连接种子节点 (桌面 wss / relay)
92
+ for (const addr of cfg.seedAddrs || []) {
93
+ try {
94
+ await node.dial(createMultiaddr(addr));
95
+ console.log(`[mobile-p2p] connected to ${addr}`);
96
+ }
97
+ catch (e) {
98
+ console.warn(`[mobile-p2p] failed to dial ${addr}:`, String(e).slice(0, 100));
99
+ }
100
+ }
101
+ // 连接已保存的好友地址 (上次 addMobilePeer 记录)
102
+ for (const addr of peerAddrs) {
103
+ try {
104
+ await node.dial(createMultiaddr(addr));
105
+ console.log(`[mobile-p2p] connected to saved peer ${addr}`);
106
+ }
107
+ catch (e) {
108
+ console.warn(`[mobile-p2p] failed to dial saved ${addr}:`, String(e).slice(0, 100));
109
+ }
110
+ }
111
+ refreshState();
112
+ return state;
113
+ }
114
+ catch (e) {
115
+ console.warn('[mobile-p2p] start failed (单机模式):', String(e).slice(0, 150));
116
+ return { connected: false, peerCount: 0, peerIds: [] };
117
+ }
118
+ }
119
+ function refreshState() {
120
+ if (!node)
121
+ return;
122
+ try {
123
+ const peers = node.getPeers() || [];
124
+ state = {
125
+ connected: node.isStarted(),
126
+ peerCount: peers.length,
127
+ peerIds: peers.map((p) => p.toString()),
128
+ nodeId: node.peerId.toString(),
129
+ };
130
+ }
131
+ catch {
132
+ /* 保持上次 */
133
+ }
134
+ }
135
+ /** 订阅入站消息 (与 mobile-core 集成) */
136
+ export function onMobileP2PMessage(fn) {
137
+ msgHandlers.push(fn);
138
+ }
139
+ /** 向指定 peer 发送 /agent/message (peerIdOrAddr: peerId 或 multiaddr 字符串) */
140
+ export async function sendMobileP2PMessage(peerIdOrAddr, type, payload, ownDid) {
141
+ if (!node)
142
+ return false;
143
+ const build = () => {
144
+ const full = ownDid
145
+ ? `DID:${ownDid}|${type}:${payload}`
146
+ : `${type}:${payload}`;
147
+ return new TextEncoder().encode(full);
148
+ };
149
+ // 0. '*' = 广播给所有已连接 peer
150
+ if (peerIdOrAddr === '*') {
151
+ const conns = node.getConnections() || [];
152
+ let sentAny = false;
153
+ for (const c of conns) {
154
+ try {
155
+ const stream = await node.dialProtocol(c.remotePeer, '/agent/message');
156
+ stream.send(build());
157
+ await stream.close();
158
+ sentAny = true;
159
+ }
160
+ catch { /* 单个失败跳过 */ }
161
+ }
162
+ return sentAny;
163
+ }
164
+ // 1. 若已连接, 直接复用连接 (remotePeer 是 PeerId 对象, 最稳)
165
+ const conns = node.getConnections() || [];
166
+ for (const c of conns) {
167
+ const peerStr = c.remotePeer.toString();
168
+ if (peerIdOrAddr.includes(peerStr) || peerStr.includes(peerIdOrAddr)) {
169
+ try {
170
+ const stream = await node.dialProtocol(c.remotePeer, '/agent/message');
171
+ stream.send(build());
172
+ await stream.close();
173
+ return true;
174
+ }
175
+ catch (e) {
176
+ console.warn('[mobile-p2p] dial on existing conn failed:', String(e).slice(0, 100));
177
+ }
178
+ }
179
+ }
180
+ // 2. 未连接 → 尝试 dial multiaddr 字符串
181
+ try {
182
+ const target = /^\/ip/.test(peerIdOrAddr) || /^\/dns/.test(peerIdOrAddr)
183
+ ? createMultiaddr(peerIdOrAddr)
184
+ : createMultiaddr(`/p2p/${peerIdOrAddr}`);
185
+ const stream = await node.dialProtocol(target, '/agent/message');
186
+ stream.send(build());
187
+ await stream.close();
188
+ return true;
189
+ }
190
+ catch (err) {
191
+ console.warn('[mobile-p2p] send failed:', String(err).slice(0, 120));
192
+ return false;
193
+ }
194
+ }
195
+ /** 当前节点状态 */
196
+ export function getMobileP2PState() {
197
+ return { ...state };
198
+ }
199
+ /** 诊断: 当前活跃连接数 (getConnections) */
200
+ export function getMobileP2PConnections() {
201
+ if (!node)
202
+ return [];
203
+ try {
204
+ return (node.getConnections() || []).map((c) => ({
205
+ peer: c.remotePeer.toString(),
206
+ addr: c.remoteAddr?.toString?.() || '',
207
+ }));
208
+ }
209
+ catch {
210
+ return [];
211
+ }
212
+ }
213
+ /** 已保存的好友地址 */
214
+ export function listMobilePeerAddrs() {
215
+ return [...peerAddrs];
216
+ }
217
+ /**
218
+ * 添加 P2P 好友 (按 multiaddr 地址 dial 连接)。
219
+ * 校验 → 记录到 localStorage (startMobileP2P 会自动重连) → 若节点已启动则立即 dial。
220
+ */
221
+ export async function addMobilePeer(addr) {
222
+ const normalized = (addr || '').trim();
223
+ if (!normalized)
224
+ return { ok: false, error: '地址不能为空' };
225
+ try {
226
+ createMultiaddr(normalized);
227
+ }
228
+ catch (e) {
229
+ return { ok: false, error: '非法 multiaddr: ' + String(e?.message || e) };
230
+ }
231
+ if (!peerAddrs.includes(normalized)) {
232
+ peerAddrs.push(normalized);
233
+ savePeerAddrs();
234
+ }
235
+ if (!node || !node.isStarted()) {
236
+ return { ok: true, connected: false, error: '已记录好友地址, P2P 启动后自动连接' };
237
+ }
238
+ try {
239
+ await node.dial(createMultiaddr(normalized));
240
+ refreshState();
241
+ return { ok: true, connected: true };
242
+ }
243
+ catch (e) {
244
+ return { ok: false, error: '连接失败: ' + String(e?.message || e).slice(0, 120) };
245
+ }
246
+ }
247
+ // 暴露同步状态读取 (mobile-core.network.status 用)
248
+ if (typeof globalThis !== 'undefined') {
249
+ globalThis.__mobileP2PStateSync = () => ({ ...state });
250
+ }