@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.
- package/dist/agents/p2p-chat-tools.js +321 -0
- package/dist/agents/p2p-document-tools.js +121 -1
- package/dist/agents/workflow-pivot-loop.js +4 -4
- package/dist/cli-entry.js +1 -1
- package/dist/documents/reader.js +5 -0
- package/dist/documents/store.js +1 -1
- package/dist/llm/pi-ai.js +6 -5
- package/dist/network/iroh-discovery.js +2 -1
- package/dist/network/iroh-transport.js +15 -2
- package/dist/network/p2p.js +9 -8
- package/dist/network/storage/adapters/json-adapter.js +16 -1
- package/dist/network/storage/index.js +2 -1
- package/dist/pi-ecosystem-judgment/index.js +43 -115
- package/dist/social/channels/channel-heartbeat-agent.js +1 -1
- package/dist/web/components/p2p/index.js +226 -264
- package/dist/web/index.html +12 -0
- package/package.json +1 -1
- package/scripts/build-web.ts +1 -1
- package/src/agents/p2p-chat-tools.ts +383 -0
- package/src/agents/p2p-document-tools.ts +151 -1
- package/src/agents/workflow-pivot-loop.ts +13 -12
- package/src/bollharness-integration/channel-judgment-engine.ts +1 -1
- package/src/documents/reader.ts +5 -0
- package/src/documents/store.ts +1 -1
- package/src/llm/pi-ai.ts +6 -5
- package/src/network/iroh-discovery.ts +2 -1
- package/src/network/iroh-transport.ts +15 -2
- package/src/network/p2p.ts +9 -8
- package/src/network/storage/adapters/json-adapter.ts +17 -2
- package/src/network/storage/index.ts +19 -3
- package/src/social/channels/channel-heartbeat-agent.ts +1 -1
- package/src/web/server.ts +149 -0
- package/tsconfig.electron.json +1 -1
- package/tsconfig.json +1 -1
- package/dist/web/components/p2p/P2PModal.js +0 -188
- package/dist/web/components/p2p/p2p-modal.js +0 -657
- package/dist/web/components/p2p/p2p-tools.js +0 -248
- 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
|
-
},
|
|
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
|
|
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
|
),
|
package/src/network/p2p.ts
CHANGED
|
@@ -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
|
|
550
|
-
let payload
|
|
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
|
-
|
|
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
|
-
//
|
|
801
|
-
|
|
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 '
|
|
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
|
-
|
|
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
|
-
|
|
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 }
|
|
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+)/);
|
package/tsconfig.electron.json
CHANGED
package/tsconfig.json
CHANGED
|
@@ -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
|
-
}
|