@bolloon/bolloon-agent 0.1.12 → 0.1.13

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 (38) hide show
  1. package/dist/agents/p2p-chat-tools.js +321 -0
  2. package/dist/agents/p2p-document-tools.js +121 -1
  3. package/dist/agents/workflow-pivot-loop.js +4 -4
  4. package/dist/cli-entry.js +1 -1
  5. package/dist/documents/reader.js +5 -0
  6. package/dist/documents/store.js +1 -1
  7. package/dist/llm/pi-ai.js +6 -5
  8. package/dist/network/iroh-discovery.js +2 -1
  9. package/dist/network/iroh-transport.js +15 -2
  10. package/dist/network/p2p.js +9 -8
  11. package/dist/network/storage/adapters/json-adapter.js +16 -1
  12. package/dist/network/storage/index.js +2 -1
  13. package/dist/pi-ecosystem-judgment/index.js +43 -115
  14. package/dist/social/channels/channel-heartbeat-agent.js +1 -1
  15. package/dist/web/components/p2p/index.js +226 -264
  16. package/dist/web/index.html +12 -0
  17. package/package.json +1 -1
  18. package/scripts/build-web.ts +1 -1
  19. package/src/agents/p2p-chat-tools.ts +383 -0
  20. package/src/agents/p2p-document-tools.ts +151 -1
  21. package/src/agents/workflow-pivot-loop.ts +13 -12
  22. package/src/bollharness-integration/channel-judgment-engine.ts +1 -1
  23. package/src/documents/reader.ts +5 -0
  24. package/src/documents/store.ts +1 -1
  25. package/src/llm/pi-ai.ts +6 -5
  26. package/src/network/iroh-discovery.ts +2 -1
  27. package/src/network/iroh-transport.ts +15 -2
  28. package/src/network/p2p.ts +9 -8
  29. package/src/network/storage/adapters/json-adapter.ts +17 -2
  30. package/src/network/storage/index.ts +19 -3
  31. package/src/social/channels/channel-heartbeat-agent.ts +1 -1
  32. package/src/web/server.ts +149 -0
  33. package/tsconfig.electron.json +1 -1
  34. package/tsconfig.json +1 -1
  35. package/dist/web/components/p2p/P2PModal.js +0 -188
  36. package/dist/web/components/p2p/p2p-modal.js +0 -657
  37. package/dist/web/components/p2p/p2p-tools.js +0 -248
  38. package/dist/web/server.js +0 -1890
package/src/llm/pi-ai.ts CHANGED
@@ -143,13 +143,14 @@ export class PiAIModel {
143
143
  return this.config.baseUrl;
144
144
  }
145
145
 
146
+ // 允许通过 OPENAI_BASE_URL 等环境变量覆盖默认 base URL
146
147
  const baseUrls: Record<ModelProvider, string> = {
147
- openai: 'https://api.openai.com/v1',
148
+ openai: process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1',
148
149
  anthropic: 'https://api.anthropic.com/v1',
149
150
  ollama: process.env.OLLAMA_BASE_URL || 'http://localhost:11434',
150
- openrouter: 'https://openrouter.ai/api/v1',
151
+ openrouter: process.env.OPENROUTER_BASE_URL || 'https://openrouter.ai/api/v1',
151
152
  gemini: 'https://generativelanguage.googleapis.com/v1beta',
152
- minimax: 'https://api.minimaxi.com/v1',
153
+ minimax: process.env.MINIMAX_BASE_URL || 'https://api.minimaxi.com/v1',
153
154
  local: 'http://localhost:11434'
154
155
  };
155
156
 
@@ -158,12 +159,12 @@ export class PiAIModel {
158
159
 
159
160
  private mapModel(): string {
160
161
  const modelMap: Record<ModelProvider, string> = {
161
- openai: this.config.model || 'gpt-4',
162
+ openai: this.config.model || process.env.OPENAI_MODEL || 'gpt-4',
162
163
  anthropic: this.config.model || 'claude-3-5-sonnet-20241022',
163
164
  ollama: this.config.model || 'llama3.2',
164
165
  openrouter: this.config.model || 'anthropic/claude-3.5-sonnet',
165
166
  gemini: this.config.model || 'gemini-2.0-flash',
166
- minimax: this.config.model || 'MiniMax-M2.7',
167
+ minimax: this.config.model || process.env.MINIMAX_MODEL || 'MiniMax-M2.7',
167
168
  local: this.config.model || 'llama3.2'
168
169
  };
169
170
  return modelMap[this.provider];
@@ -89,9 +89,10 @@ export class IrohDiscoveryService {
89
89
  }
90
90
 
91
91
  private startDiscoveryLoop(): void {
92
+ const interval = this.config.discoveryIntervalMs ?? 30000;
92
93
  this.discoveryTimer = setInterval(async () => {
93
94
  await this.discoverPeers();
94
- }, this.discoveryIntervalMs!);
95
+ }, interval);
95
96
 
96
97
  setTimeout(() => this.discoverPeers(), 2000);
97
98
  }
@@ -56,6 +56,7 @@ interface MessageStore {
56
56
  dequeueOfflineMessage(id: string): Promise<void>;
57
57
  incrementOfflineRetry(id: string): Promise<void>;
58
58
  getPendingOfflineCount(): Promise<number>;
59
+ getAllOfflineTargets(): Promise<string[]>;
59
60
  savePendingResponse(req: Omit<PendingResponse, 'id'>): Promise<PendingResponse>;
60
61
  getPendingResponse(requestId: string): Promise<PendingResponse | null>;
61
62
  removePendingResponse(requestId: string): Promise<void>;
@@ -172,6 +173,9 @@ export class IrohTransport {
172
173
  for (const queue of offlineQueues.values()) count += queue.length;
173
174
  return count;
174
175
  },
176
+ async getAllOfflineTargets() {
177
+ return Array.from(offlineQueues.keys());
178
+ },
175
179
  async savePendingResponse(req) {
176
180
  const id = crypto.randomUUID();
177
181
  const pending = { ...req, id };
@@ -196,9 +200,14 @@ export class IrohTransport {
196
200
  if (!this.messageStore) return;
197
201
 
198
202
  this.offlineDeliveryInterval = setInterval(async () => {
203
+ // 遍历所有有离线消息的目标节点(不仅是"已连接"的)
204
+ // 这样目标节点一旦在线(accept 连接)就能拿到离线消息
205
+ const allTargets = await this.messageStore!.getAllOfflineTargets();
199
206
  const connectedPeers = this.getConnectedPeers();
207
+ // 合并:已连接 + 有离线消息但未连接(也会去尝试 connect)
208
+ const targets = new Set<string>([...connectedPeers, ...allTargets]);
200
209
 
201
- for (const peerId of connectedPeers) {
210
+ for (const peerId of targets) {
202
211
  const offlineMsgs = await this.messageStore!.getOfflineMessages(peerId);
203
212
 
204
213
  for (const msg of offlineMsgs) {
@@ -216,6 +225,8 @@ export class IrohTransport {
216
225
  if (success) {
217
226
  await this.messageStore!.dequeueOfflineMessage(msg.id);
218
227
  console.log(`[IrohTransport] Delivered offline message to ${peerId.substring(0, 12)}...`);
228
+ } else {
229
+ await this.messageStore!.incrementOfflineRetry(msg.id);
219
230
  }
220
231
  } catch {
221
232
  await this.messageStore!.incrementOfflineRetry(msg.id);
@@ -461,8 +472,10 @@ export class IrohTransport {
461
472
  await send.finish();
462
473
 
463
474
  // 等待响应,带超时
475
+ // 注意: server sendResponse 后会关闭连接,导致 readToEnd 以 "connection lost" 错误 reject
476
+ // 这里我们把 readToEnd 的错误吞掉(视为流结束),只有超时才视为失败
464
477
  const response = await Promise.race([
465
- recv.readToEnd(64 * 1024),
478
+ recv.readToEnd(64 * 1024).catch(() => new Uint8Array(0)),
466
479
  new Promise<null>((_, rejectTimeout) =>
467
480
  setTimeout(() => rejectTimeout(new Error('timeout')), timeout)
468
481
  ),
@@ -546,8 +546,8 @@ export class P2PNetwork {
546
546
  const colonIdx = messageStr.indexOf(':');
547
547
  const didMarker = 'DID:';
548
548
  let did: string | undefined;
549
- let type: string;
550
- let payload: string;
549
+ let type = 'message';
550
+ let payload = '';
551
551
  let requestId: string | undefined = undefined;
552
552
 
553
553
  if (messageStr.startsWith(didMarker)) {
@@ -770,7 +770,11 @@ export class P2PNetwork {
770
770
  * Register a handler for responses (used by the receiving side)
771
771
  */
772
772
  onResponse(type: string, handler: (payload: string, from: string, did?: string, requestId?: string) => void): void {
773
- this.messageHandlers.set(type, handler);
773
+ // Store as pendingResponseHandlers-shaped wrapper. Extra args (did, requestId) are not
774
+ // available in pendingResponseHandlers signature, so ignore them when invoked.
775
+ this.pendingResponseHandlers.set(type, (responseData: string, from: string) => {
776
+ handler(responseData, from, undefined, undefined);
777
+ });
774
778
  }
775
779
 
776
780
  /**
@@ -797,11 +801,8 @@ export class P2PNetwork {
797
801
  private handleRequest(type: string, payload: string, requestId: string, fromPeerId: string, did?: string): void {
798
802
  const handler = this.messageHandlers.get(type);
799
803
  if (handler) {
800
- // Create a wrapper that sends the response
801
- const originalHandler = handler;
802
- handler = (msg: Uint8Array, from: string, didParam?: string) => {
803
- originalHandler(msg, from, didParam);
804
- };
804
+ // Forward raw payload; callers register with onMessage() and adapt as needed.
805
+ handler(new TextEncoder().encode(payload), fromPeerId, did);
805
806
  }
806
807
 
807
808
  // Check if there's a response handler registered
@@ -16,7 +16,7 @@ import type {
16
16
  MessageQueryOptions,
17
17
  StorageConfig,
18
18
  MessageStatus,
19
- } from './types.js';
19
+ } from '../types';
20
20
 
21
21
  const DEFAULT_CONFIG: Required<StorageConfig> = {
22
22
  baseDir: '',
@@ -69,7 +69,7 @@ export class JsonMessageStore implements MessageStore {
69
69
  const filePath = this.getMessageFilePath(new Date(msg.timestamp));
70
70
 
71
71
  await this.withLock(filePath, async () => {
72
- const messages = await this.readJsonFile<StoredMessage[]>(filePath) || [];
72
+ let messages = await this.readJsonFile<StoredMessage[]>(filePath) || [];
73
73
  messages.push(stored);
74
74
 
75
75
  // 如果文件过大,拆分
@@ -238,6 +238,21 @@ export class JsonMessageStore implements MessageStore {
238
238
  return count;
239
239
  }
240
240
 
241
+ async getAllOfflineTargets(): Promise<string[]> {
242
+ // 重新从磁盘加载最新状态(避免内存 vs 磁盘不一致)
243
+ const baseDir = path.join(this.config.baseDir, 'offline');
244
+ let files: string[] = [];
245
+ try {
246
+ files = await fs.readdir(baseDir);
247
+ } catch {
248
+ return Array.from(this.offlineMessages.keys());
249
+ }
250
+ return files
251
+ .filter((f) => f.endsWith('.json'))
252
+ .map((f) => f.replace(/\.json$/, ''))
253
+ .filter((id) => id.length > 0);
254
+ }
255
+
241
256
  // ============================================================================
242
257
  // 待响应请求
243
258
  // ============================================================================
@@ -3,7 +3,7 @@
3
3
  * 导出消息存储工厂函数和类型
4
4
  */
5
5
 
6
- export type {
6
+ import type {
7
7
  MessageStore,
8
8
  StoredMessage,
9
9
  OfflineMessage,
@@ -18,11 +18,27 @@ export type {
18
18
  IrohMessage,
19
19
  IrohMessageHandler,
20
20
  } from './types.js';
21
+ import { DEFAULT_STORAGE_CONFIG } from './types.js';
22
+
23
+ export type {
24
+ MessageStore,
25
+ StoredMessage,
26
+ OfflineMessage,
27
+ PendingResponse,
28
+ LocalPendingRequest,
29
+ MessageQueryOptions,
30
+ StorageConfig,
31
+ MessageDirection,
32
+ MessageStatus,
33
+ TransportType,
34
+ IrohPeer,
35
+ IrohMessage,
36
+ IrohMessageHandler,
37
+ };
21
38
 
22
- export { DEFAULT_STORAGE_CONFIG } from './types.js';
39
+ export { DEFAULT_STORAGE_CONFIG };
23
40
 
24
41
  import { JsonMessageStore } from './adapters/json-adapter.js';
25
- import type { MessageStore, StorageConfig } from './types.js';
26
42
  import * as path from 'path';
27
43
 
28
44
  // 默认存储配置
@@ -388,7 +388,7 @@ export class ChannelHeartbeatAgent {
388
388
  senderName: peer.name
389
389
  };
390
390
 
391
- const decision = this.judgmentEngine.decide(context);
391
+ const decision = await this.judgmentEngine.decide(context);
392
392
 
393
393
  if (decision.shouldCall) {
394
394
  console.log(`[HeartbeatAgent] Auto-triggering Harness: Gate ${decision.gate} for ${peer.name}`);
package/src/web/server.ts CHANGED
@@ -1263,6 +1263,79 @@ app.get('/channels', async (_req, res) => {
1263
1263
  }
1264
1264
  });
1265
1265
 
1266
+ // 统一 AI 解析入口:CLI / 接收方节点 调这里完成 LLM + judgment + harness
1267
+ // 入参: { text, mimeType, fileName, fromNodeId, source }
1268
+ // 出参: { summary, qualityScore, judgmentId?, gateArtifact? }
1269
+ app.post('/api/ai-parse', async (req, res) => {
1270
+ try {
1271
+ const { text, mimeType, fileName, fromNodeId, source } = req.body || {};
1272
+ if (!text || !fileName) {
1273
+ return res.status(400).json({ error: 'text and fileName required' });
1274
+ }
1275
+
1276
+ const truncated = text.length > 6000 ? text.substring(0, 6000) + '...[截断]' : text;
1277
+ const prompt = `请分析以下 ${mimeType || 'text'} 文档,并给出 (1) 一句话中文摘要 (2) 三个关键要点 (3) 质量评分(0-1)。\n\n文件名: ${fileName}\n\n内容:\n${truncated}`;
1278
+
1279
+ // 1. LLM 解析
1280
+ const llm = getMinimax();
1281
+ const t0 = Date.now();
1282
+ const llmResult = await llm.summarize(prompt);
1283
+ const dt = Date.now() - t0;
1284
+
1285
+ const out: any = {
1286
+ ok: true,
1287
+ summary: llmResult.summary,
1288
+ qualityScore: llmResult.qualityScore,
1289
+ latencyMs: dt,
1290
+ mimeType: mimeType || 'text/plain',
1291
+ fileName,
1292
+ };
1293
+
1294
+ // 2. 蒸馏为 judgment (异步,失败不影响主返回)
1295
+ try {
1296
+ const judgmentMod = await import('../pi-ecosystem-judgment/index.js');
1297
+ await judgmentMod.initializeJudgmentStore();
1298
+ const j = await judgmentMod.createJudgment({
1299
+ type: 'trajectory',
1300
+ content: `AI 解析 ${fileName}: ${llmResult.summary.slice(0, 200)}`,
1301
+ source: 'agent',
1302
+ confidence: Math.min(1, llmResult.qualityScore),
1303
+ context: `ai-parse:${mimeType || 'text'}:${source || 'p2p'}`,
1304
+ evidence: {
1305
+ trajectory: [{
1306
+ timestamp: new Date().toISOString(),
1307
+ action: `parse:${fileName}`,
1308
+ outcome: `score=${llmResult.qualityScore.toFixed(2)}`,
1309
+ approved: true,
1310
+ }],
1311
+ },
1312
+ });
1313
+ out.judgmentId = j.id;
1314
+ } catch (e) {
1315
+ out.judgmentError = (e as Error).message;
1316
+ }
1317
+
1318
+ // 3. 在 harness 落产物 (异步,失败不影响)
1319
+ try {
1320
+ const harnessMod = await import('../bollharness-integration/index.js');
1321
+ const gate = new harnessMod.GateStateMachine();
1322
+ gate.submitArtifact(`ai-parse:${fileName}`, {
1323
+ summary: llmResult.summary,
1324
+ score: llmResult.qualityScore,
1325
+ fromNodeId: fromNodeId || null,
1326
+ parsedAt: Date.now(),
1327
+ });
1328
+ out.gateArtifact = `ai-parse:${fileName}`;
1329
+ } catch (e) {
1330
+ out.gateError = (e as Error).message;
1331
+ }
1332
+
1333
+ res.json(out);
1334
+ } catch (err: any) {
1335
+ res.status(500).json({ error: err.message });
1336
+ }
1337
+ });
1338
+
1266
1339
  // ==================== P2P Network API ====================
1267
1340
 
1268
1341
  // 获取当前身份
@@ -1734,6 +1807,61 @@ app.get('/channels', async (_req, res) => {
1734
1807
  }
1735
1808
  });
1736
1809
 
1810
+ // Chat inbox: 列出所有 peer 的 inbox + outbox
1811
+ app.get('/api/chat/inbox', async (_req, res) => {
1812
+ try {
1813
+ const { getInbox } = await import('../agents/p2p-chat-tools.js');
1814
+ const entries = await getInbox();
1815
+ // 按 status 分组, 时间倒序
1816
+ const grouped = {
1817
+ received: entries.filter((e: any) => e.status === 'received'),
1818
+ drafted: entries.filter((e: any) => e.status === 'drafted'),
1819
+ sent: entries.filter((e: any) => e.status === 'sent'),
1820
+ dismissed: entries.filter((e: any) => e.status === 'dismissed'),
1821
+ };
1822
+ res.json({ total: entries.length, grouped, all: entries });
1823
+ } catch (err: any) {
1824
+ res.status(500).json({ error: err.message });
1825
+ }
1826
+ });
1827
+
1828
+ // 触发 processPendingInbox (手动 wake-up)
1829
+ app.post('/api/chat/process-pending', async (_req, res) => {
1830
+ try {
1831
+ const { processPendingInbox } = await import('../agents/p2p-chat-tools.js');
1832
+ const r = await processPendingInbox();
1833
+ res.json({ ok: true, ...r });
1834
+ } catch (err: any) {
1835
+ res.status(500).json({ error: err.message });
1836
+ }
1837
+ });
1838
+
1839
+ // 主人审阅: 批准 draft
1840
+ app.post('/api/chat/approve', async (req, res) => {
1841
+ try {
1842
+ const { messageId, peerDID, finalText } = req.body || {};
1843
+ if (!messageId || !peerDID) return res.status(400).json({ error: 'messageId and peerDID required' });
1844
+ const { approveAndSend } = await import('../agents/p2p-chat-tools.js');
1845
+ const ok = await approveAndSend(messageId, peerDID, finalText);
1846
+ res.json({ ok, messageId });
1847
+ } catch (err: any) {
1848
+ res.status(500).json({ error: err.message });
1849
+ }
1850
+ });
1851
+
1852
+ // 主人审阅: 丢弃 draft
1853
+ app.post('/api/chat/dismiss', async (req, res) => {
1854
+ try {
1855
+ const { messageId, peerDID } = req.body || {};
1856
+ if (!messageId || !peerDID) return res.status(400).json({ error: 'messageId and peerDID required' });
1857
+ const { dismissDraft } = await import('../agents/p2p-chat-tools.js');
1858
+ const ok = await dismissDraft(messageId, peerDID);
1859
+ res.json({ ok, messageId });
1860
+ } catch (err: any) {
1861
+ res.status(500).json({ error: err.message });
1862
+ }
1863
+ });
1864
+
1737
1865
  // 标记消息已读
1738
1866
  app.post('/api/peer-messages/:messageId/read', async (req, res) => {
1739
1867
  try {
@@ -2095,6 +2223,8 @@ app.get('/channels', async (_req, res) => {
2095
2223
  server.listen(port, () => {
2096
2224
  console.log(`Web 服务器启动完成: http://localhost:${port}`);
2097
2225
  console.log('服务器已监听');
2226
+ // 安装 chat bus -> SSE 桥 (供前端 inbox UI 实时刷新)
2227
+ void installChatBusHook();
2098
2228
  setInterval(() => {
2099
2229
  for (const client of sseClients) {
2100
2230
  client.res.write(': ping\n\n');
@@ -2120,6 +2250,25 @@ function broadcast(data: { type: string; [key: string]: unknown }, channelId?: s
2120
2250
  }
2121
2251
  }
2122
2252
 
2253
+ // ============================================================================
2254
+ // Chat 事件总线 -> SSE 桥 (供前端 inbox UI 用)
2255
+ // ============================================================================
2256
+ let chatBusHookInstalled = false;
2257
+ async function installChatBusHook(): Promise<void> {
2258
+ if (chatBusHookInstalled) return;
2259
+ chatBusHookInstalled = true;
2260
+ try {
2261
+ const { chatEventBus } = await import('../agents/p2p-chat-tools.js');
2262
+ chatEventBus.on('chat', (ev: any) => {
2263
+ // 推送给所有 SSE 客户端 (channelId 留空 = 广播)
2264
+ broadcast({ type: 'chat_event', chatKind: ev.kind, payload: ev }, undefined);
2265
+ });
2266
+ console.log('[chat-bus] SSE bridge installed');
2267
+ } catch (e) {
2268
+ console.warn('[chat-bus] install failed:', (e as Error).message);
2269
+ }
2270
+ }
2271
+
2123
2272
  function getUserName(): string {
2124
2273
  const home = process.env.HOME || process.env.USERPROFILE || '';
2125
2274
  const match = home.match(/\/Users\/(\w+)/);
@@ -7,7 +7,7 @@
7
7
  "allowSyntheticDefaultImports": true,
8
8
  "strict": true,
9
9
  "skipLibCheck": true,
10
- "ignoreDeprecations": "6.0",
10
+ "ignoreDeprecations": "5.0",
11
11
  "outDir": "dist",
12
12
  "rootDir": "src",
13
13
  "declaration": false,
package/tsconfig.json CHANGED
@@ -14,5 +14,5 @@
14
14
  "jsxImportSource": "react"
15
15
  },
16
16
  "include": ["src/**/*"],
17
- "exclude": ["node_modules", "src/bollharness/node_modules"]
17
+ "exclude": ["node_modules", "src/bollharness/node_modules", "src/test", "src/**/__tests__", "src/constraint-runtime"]
18
18
  }
@@ -1,188 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState, useEffect } from 'react';
3
- import { p2pManager } from './p2p-manager.js';
4
- export function P2PModal({ visible, onClose }) {
5
- const [activeTab, setActiveTab] = useState('identity');
6
- const [initialized, setInitialized] = useState(false);
7
- const [status, setStatus] = useState('idle');
8
- const [statusText, setStatusText] = useState('未初始化');
9
- const [identity, setIdentity] = useState(null);
10
- const [connectInput, setConnectInput] = useState('');
11
- const [progress, setProgress] = useState(null);
12
- const [progressVisible, setProgressVisible] = useState(false);
13
- const [connectResult, setConnectResult] = useState(null);
14
- const [history, setHistory] = useState([]);
15
- const [messages, setMessages] = useState([]);
16
- const [unreadCount, setUnreadCount] = useState(0);
17
- const [peers, setPeers] = useState([]);
18
- const [persistentConnections, setPersistentConnections] = useState([]);
19
- const [toast, setToast] = useState(null);
20
- useEffect(() => {
21
- if (visible && !initialized) {
22
- console.log('[P2P Modal] visible=true, initialized=false, calling initP2P');
23
- initP2P();
24
- }
25
- else if (visible && initialized && !identity) {
26
- // 已初始化但没有identity,可能是首次未触发
27
- console.log('[P2P Modal] visible=true, initialized=true, but no identity, calling initP2P again');
28
- initP2P();
29
- }
30
- }, [visible, initialized]);
31
- useEffect(() => {
32
- if (visible) {
33
- loadActiveTab();
34
- }
35
- }, [activeTab, visible]);
36
- async function initP2P() {
37
- setStatus('connecting');
38
- setStatusText('初始化中...');
39
- try {
40
- const identity = await p2pManager.init();
41
- setIdentity(identity);
42
- setStatus('online');
43
- setStatusText('已连接');
44
- setInitialized(true);
45
- }
46
- catch (e) {
47
- setStatus('error');
48
- setStatusText('初始化失败');
49
- }
50
- }
51
- async function loadActiveTab() {
52
- switch (activeTab) {
53
- case 'history':
54
- await loadHistory();
55
- break;
56
- case 'messages':
57
- await loadMessages();
58
- break;
59
- case 'connect':
60
- loadPeers();
61
- loadPersistentConnections();
62
- break;
63
- }
64
- }
65
- async function loadHistory() {
66
- try {
67
- const hist = await p2pManager.getHistory();
68
- setHistory(hist);
69
- }
70
- catch (e) {
71
- console.error('[P2P Modal] 加载历史失败:', e);
72
- }
73
- }
74
- async function loadMessages() {
75
- try {
76
- const msgs = await p2pManager.getMessages();
77
- setMessages(msgs);
78
- const unread = p2pManager.getUnreadCount();
79
- setUnreadCount(unread);
80
- }
81
- catch (e) {
82
- console.error('[P2P Modal] 加载消息失败:', e);
83
- }
84
- }
85
- function loadPeers() {
86
- const connectedPeers = p2pManager.getConnectedPeers();
87
- setPeers(connectedPeers);
88
- }
89
- async function loadPersistentConnections() {
90
- try {
91
- const connections = await p2pManager.getPersistentConnections();
92
- setPersistentConnections(connections);
93
- }
94
- catch (e) {
95
- console.error('[P2P Modal] 加载持久连接失败:', e);
96
- }
97
- }
98
- async function handleToggleConnection(connection) {
99
- const newStatus = connection.status === 'connected' ? 'disconnected' : 'connected';
100
- const enable = newStatus === 'connected';
101
- try {
102
- const success = await p2pManager.toggleConnection(connection, enable);
103
- if (success) {
104
- await loadPersistentConnections();
105
- loadPeers();
106
- showToast(enable ? '正在连接...' : '已断开');
107
- }
108
- else {
109
- showToast('操作失败');
110
- }
111
- }
112
- catch (e) {
113
- showToast('操作失败');
114
- }
115
- }
116
- async function handleOpenChannel(channelId) {
117
- showToast(`打开通道: ${channelId}`);
118
- onClose();
119
- }
120
- async function handleConnect() {
121
- if (!connectInput.trim())
122
- return;
123
- setProgressVisible(true);
124
- setProgress({ stage: 'init', percent: 0, message: '验证输入格式...' });
125
- setConnectResult(null);
126
- try {
127
- const result = await p2pManager.connectAndCreateChannel(connectInput, (p) => setProgress(p));
128
- if (result.success) {
129
- setConnectResult({ type: 'success', text: `已连接到 ${result.name || '节点'}` });
130
- setConnectInput('');
131
- await loadHistory();
132
- loadPeers();
133
- loadPersistentConnections();
134
- }
135
- else {
136
- setConnectResult({ type: 'error', text: result.error || '连接失败' });
137
- }
138
- }
139
- catch (e) {
140
- setConnectResult({ type: 'error', text: e.message });
141
- }
142
- finally {
143
- setTimeout(() => {
144
- setProgressVisible(false);
145
- setProgress(null);
146
- }, 2000);
147
- }
148
- }
149
- async function handleHistoryAction(action, item) {
150
- if (action === 'connect') {
151
- setConnectInput(item.cid);
152
- setActiveTab('connect');
153
- }
154
- else if (action === 'pin') {
155
- await p2pManager.updateHistory(item.id, { isPinned: !item.isPinned });
156
- await loadHistory();
157
- }
158
- else if (action === 'delete') {
159
- await p2pManager.deleteHistory(item.id);
160
- await loadHistory();
161
- }
162
- }
163
- async function handleMarkAllRead() {
164
- await p2pManager.messages.markAllRead();
165
- await loadMessages();
166
- }
167
- function copyToClipboard(text) {
168
- navigator.clipboard.writeText(text);
169
- showToast('已复制');
170
- }
171
- function showToast(msg) {
172
- setToast(msg);
173
- setTimeout(() => setToast(null), 2000);
174
- }
175
- function handleCopyLink() {
176
- if (identity) {
177
- const link = `bolloon://connect?did=${encodeURIComponent(identity.did)}&cid=${encodeURIComponent(identity.cid)}`;
178
- navigator.clipboard.writeText(link);
179
- showToast('链接已复制');
180
- }
181
- }
182
- function handleExportFile() {
183
- p2pManager.identity.exportIdentityFile();
184
- }
185
- if (!visible)
186
- return null;
187
- return (_jsxs("div", { className: "p2p-modal-overlay", onClick: (e) => e.target === e.currentTarget && onClose(), children: [_jsxs("div", { className: "p2p-modal", children: [_jsxs("div", { className: "modal-header", children: [_jsx("h2", { children: "P2P \u7F51\u7EDC" }), _jsx("button", { className: "modal-close", onClick: onClose, children: "\u00D7" })] }), _jsxs("div", { className: "tabs", children: [_jsx("button", { className: `tab ${activeTab === 'identity' ? 'active' : ''}`, onClick: () => setActiveTab('identity'), children: "\u6211\u7684\u8EAB\u4EFD" }), _jsx("button", { className: `tab ${activeTab === 'connect' ? 'active' : ''}`, onClick: () => setActiveTab('connect'), children: "\u8FDE\u63A5" }), _jsx("button", { className: `tab ${activeTab === 'history' ? 'active' : ''}`, onClick: () => setActiveTab('history'), children: "\u5386\u53F2\u8BB0\u5F55" }), _jsxs("button", { className: `tab ${activeTab === 'messages' ? 'active' : ''}`, onClick: () => setActiveTab('messages'), children: ["\u6D88\u606F ", unreadCount > 0 && _jsx("span", { className: "unread-badge", children: unreadCount })] })] }), activeTab === 'identity' && (_jsxs("div", { className: "tab-content active", children: [_jsxs("div", { className: "identity-card", children: [_jsxs("div", { className: "status-row", children: [_jsx("span", { className: `status-indicator ${status}` }), _jsx("span", { children: statusText })] }), _jsxs("div", { className: "info-row", children: [_jsx("span", { className: "info-label", children: "DID:" }), _jsx("code", { className: "info-value", children: identity?.did || '-' }), identity?.did && _jsx("button", { className: "copy-btn", onClick: () => copyToClipboard(identity.did), children: "\uD83D\uDCCB" })] }), _jsxs("div", { className: "info-row", children: [_jsx("span", { className: "info-label", children: "CID:" }), _jsx("code", { className: "info-value", children: identity?.cid || '-' }), identity?.cid && _jsx("button", { className: "copy-btn", onClick: () => copyToClipboard(identity.cid), children: "\uD83D\uDCCB" })] }), _jsxs("div", { className: "info-row", children: [_jsx("span", { className: "info-label", children: "Node ID:" }), _jsx("code", { className: "info-value", children: identity?.irohNodeId || '-' }), identity?.irohNodeId && _jsx("button", { className: "copy-btn", onClick: () => copyToClipboard(identity.irohNodeId), children: "\uD83D\uDCCB" })] })] }), !initialized && _jsx("button", { className: "btn-primary", onClick: initP2P, children: "\u521D\u59CB\u5316 P2P" }), identity && (_jsxs("div", { className: "share-panel", children: [_jsx("h4", { children: "\u5206\u4EAB\u7ED9\u597D\u53CB" }), _jsxs("div", { className: "share-actions", children: [_jsx("button", { className: "btn-secondary", onClick: handleCopyLink, children: "\uD83D\uDCCB \u590D\u5236\u94FE\u63A5" }), _jsx("button", { className: "btn-secondary", onClick: handleExportFile, children: "\uD83D\uDCC1 \u5BFC\u51FA\u6587\u4EF6" })] })] }))] })), activeTab === 'connect' && (_jsxs("div", { className: "tab-content active", children: [_jsxs("div", { className: "connect-form", children: [_jsx("input", { type: "text", placeholder: "\u7C98\u8D34 CID \u6216\u94FE\u63A5...", value: connectInput, onChange: (e) => setConnectInput(e.target.value), onKeyPress: (e) => e.key === 'Enter' && handleConnect() }), _jsx("button", { className: "btn-secondary", onClick: handleConnect, children: "\u8FDE\u63A5 \u25B6" })] }), progressVisible && progress && (_jsxs("div", { className: "progress show", children: [_jsx("div", { className: "progress-bar", children: _jsx("div", { className: "progress-fill", style: { width: `${progress.percent}%` } }) }), _jsx("span", { className: "progress-text", children: progress.message })] })), connectResult && (_jsx("div", { className: `connect-result ${connectResult.type} show`, children: connectResult.text })), _jsxs("div", { className: "persistent-peers-section", children: [_jsxs("h4", { children: ["\u6301\u4E45\u8FDE\u63A5 (", persistentConnections.length, ")"] }), persistentConnections.length === 0 ? (_jsx("div", { className: "empty-hint", children: "\u6682\u65E0\u6301\u4E45\u8FDE\u63A5" })) : (persistentConnections.map((conn) => (_jsxs("div", { className: `persistent-peer-item ${conn.status}`, children: [_jsx("div", { className: "peer-status-indicator", children: _jsx("span", { className: `dot ${conn.status === 'connected' ? 'online' : 'offline'}` }) }), _jsxs("div", { className: "peer-info", children: [_jsxs("div", { className: "peer-name", children: [conn.peerName || 'Unknown', conn.isAutoConnect && _jsx("span", { className: "auto-badge", children: "\u81EA\u52A8" })] }), _jsxs("div", { className: "peer-meta", children: [_jsxs("span", { children: ["DID: ", conn.peerDid?.substring(0, 16), "..."] }), _jsxs("span", { children: ["\u72B6\u6001: ", conn.status === 'connected' ? '已连接' : '未连接'] })] })] }), _jsxs("div", { className: "peer-actions", children: [_jsx("button", { className: `btn-sm ${conn.status === 'connected' ? 'btn-danger' : 'btn-primary'}`, onClick: () => handleToggleConnection(conn), children: conn.status === 'connected' ? '断开' : '连接' }), conn.channelId && (_jsx("button", { className: "btn-sm btn-secondary", onClick: () => handleOpenChannel(conn.channelId), children: "\u5BF9\u8BDD" }))] })] }, conn.id))))] }), _jsxs("div", { className: "peers-section", children: [_jsxs("h4", { children: ["\u5F53\u524D\u8FDE\u63A5 (", peers.length, ")"] }), _jsxs("div", { id: "p2p-peers-list", children: [peers.length === 0 && _jsx("div", { className: "empty-hint", children: "\u6682\u65E0\u8FDE\u63A5" }), peers.map((peer, i) => (_jsxs("div", { className: "peer-item", children: [_jsx("div", { className: "peer-status", children: _jsx("span", { className: "dot online" }) }), _jsxs("div", { className: "peer-info", children: [_jsx("div", { className: "peer-name", children: peer.info?.name || 'Unknown' }), _jsxs("div", { className: "peer-meta", children: [(peer.nodeId || '').substring(0, 16), "..."] })] })] }, i)))] })] })] })), activeTab === 'history' && (_jsxs("div", { className: "tab-content active", children: [_jsx("div", { className: "toolbar", children: _jsx("button", { className: "btn-secondary btn-sm", onClick: loadHistory, children: "\uD83D\uDD04 \u5237\u65B0" }) }), _jsxs("div", { id: "p2p-history-list", children: [history.length === 0 && _jsx("div", { className: "empty-hint", children: "\u6682\u65E0\u8FDE\u63A5\u5386\u53F2" }), history.map((item) => (_jsxs("div", { className: `history-item ${item.isPinned ? 'pinned' : ''}`, children: [_jsx("div", { className: "history-item-icon", children: "\uD83D\uDCAC" }), _jsxs("div", { className: "history-item-info", children: [_jsxs("div", { className: "history-item-name", children: [item.name || 'Unknown', item.isPinned && _jsx("span", { className: "pin-icon", children: "\uD83D\uDCCC" })] }), _jsxs("div", { className: "history-item-meta", children: [_jsxs("span", { children: ["\u4E0A\u6B21: ", new Date(item.lastConnectedAt).toLocaleString()] }), _jsxs("span", { children: ["\u6D88\u606F: ", item.totalMessages || 0] })] })] }), _jsxs("div", { className: "history-item-actions", children: [_jsx("button", { className: "btn-sm btn-secondary", onClick: () => handleHistoryAction('connect', item), children: "\u8FDE\u63A5" }), _jsx("button", { className: "btn-sm btn-secondary", onClick: () => handleHistoryAction('pin', item), children: item.isPinned ? '取消置顶' : '置顶' }), _jsx("button", { className: "btn-sm btn-secondary", onClick: () => handleHistoryAction('delete', item), children: "\u5220\u9664" })] })] }, item.id)))] })] })), activeTab === 'messages' && (_jsxs("div", { className: "tab-content active", children: [_jsx("div", { className: "toolbar", children: _jsx("button", { className: "btn-secondary btn-sm", onClick: handleMarkAllRead, children: "\u5168\u90E8\u5DF2\u8BFB" }) }), _jsxs("div", { id: "p2p-messages-list", children: [messages.length === 0 && _jsx("div", { className: "empty-hint", children: "\u6682\u65E0\u6D88\u606F" }), messages.slice(-20).map((msg) => (_jsxs("div", { className: `message-item ${!msg.isRead ? 'unread' : ''}`, children: [_jsxs("div", { className: "message-header", children: [_jsx("span", { className: "message-sender", children: msg.fromName || msg.fromDid }), _jsx("span", { className: "message-time", children: new Date(msg.timestamp).toLocaleString() })] }), _jsx("div", { className: "message-content", children: msg.content.substring(0, 200) })] }, msg.id)))] })] }))] }), toast && _jsx("div", { className: "toast", children: toast })] }));
188
- }