@bolloon/bolloon-agent 0.1.11 → 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 (42) 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/utils/auto-update.js +15 -1
  16. package/dist/web/components/p2p/index.js +226 -264
  17. package/dist/web/index.html +12 -0
  18. package/package.json +3 -1
  19. package/scripts/build-web.ts +1 -1
  20. package/scripts/postinstall.js +1 -1
  21. package/src/agents/p2p-chat-tools.ts +383 -0
  22. package/src/agents/p2p-document-tools.ts +151 -1
  23. package/src/agents/workflow-pivot-loop.ts +13 -12
  24. package/src/bollharness-integration/channel-judgment-engine.ts +1 -1
  25. package/src/cli-entry.ts +1 -1
  26. package/src/documents/reader.ts +5 -0
  27. package/src/documents/store.ts +1 -1
  28. package/src/llm/pi-ai.ts +6 -5
  29. package/src/network/iroh-discovery.ts +2 -1
  30. package/src/network/iroh-transport.ts +15 -2
  31. package/src/network/p2p.ts +9 -8
  32. package/src/network/storage/adapters/json-adapter.ts +17 -2
  33. package/src/network/storage/index.ts +19 -3
  34. package/src/social/channels/channel-heartbeat-agent.ts +1 -1
  35. package/src/utils/auto-update.ts +17 -1
  36. package/src/web/server.ts +149 -0
  37. package/tsconfig.electron.json +1 -1
  38. package/tsconfig.json +1 -1
  39. package/dist/web/components/p2p/P2PModal.js +0 -188
  40. package/dist/web/components/p2p/p2p-modal.js +0 -657
  41. package/dist/web/components/p2p/p2p-tools.js +0 -248
  42. package/dist/web/server.js +0 -1890
@@ -205,11 +205,11 @@ export class DocumentStore {
205
205
  async readDocument(docId: string): Promise<{ content: string; metadata: ReceivedDocument } | null> {
206
206
  const docDir = path.join(this.baseDir, docId);
207
207
  const manifestPath = path.join(docDir, 'manifest.json');
208
- const filePath = path.join(docDir);
209
208
 
210
209
  try {
211
210
  const manifestData = await fs.readFile(manifestPath, 'utf-8');
212
211
  const manifest = JSON.parse(manifestData);
212
+ const filePath = path.join(docDir, manifest.fileName);
213
213
  const fileContent = await fs.readFile(filePath, 'utf-8');
214
214
 
215
215
  return {
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}`);
@@ -86,14 +86,20 @@ function getGlobalBolloonDir(): string | null {
86
86
  * 优先使用全局安装的版本(更准确反映实际运行的版本)
87
87
  */
88
88
  function getInstalledVersion(packageName: string): string | null {
89
+ // 调试:打印 cwd
90
+ console.error(`[DEBUG] getInstalledVersion called, cwd=${process.cwd()}, package=${packageName}`);
91
+
89
92
  // 对于 @bolloon/bolloon-agent,始终优先从全局安装位置读取版本
90
93
  // 这样可以准确检测实际安装的版本,而不受 cwd 影响
91
94
  if (packageName === '@bolloon/bolloon-agent') {
92
95
  const globalDir = getGlobalBolloonDir();
96
+ console.error(`[DEBUG] globalDir=${globalDir}`);
93
97
  if (globalDir) {
94
98
  const pkgPath = path.join(globalDir, 'package.json');
99
+ console.error(`[DEBUG] pkgPath=${pkgPath}, exists=${fs.existsSync(pkgPath)}`);
95
100
  try {
96
101
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
102
+ console.error(`[DEBUG] pkg.version=${pkg.version}`);
97
103
  return pkg.version || null;
98
104
  } catch (e) {
99
105
  // 忽略
@@ -102,9 +108,11 @@ function getInstalledVersion(packageName: string): string | null {
102
108
 
103
109
  // 回退到本地 package.json
104
110
  const localPkgPath = path.join(process.cwd(), 'package.json');
111
+ console.error(`[DEBUG] localPkgPath=${localPkgPath}, exists=${fs.existsSync(localPkgPath)}`);
105
112
  if (fs.existsSync(localPkgPath)) {
106
113
  try {
107
114
  const pkg = JSON.parse(fs.readFileSync(localPkgPath, 'utf-8'));
115
+ console.error(`[DEBUG] local pkg.version=${pkg.version}`);
108
116
  return pkg.version || null;
109
117
  } catch (e) {
110
118
  // 忽略
@@ -114,9 +122,11 @@ function getInstalledVersion(packageName: string): string | null {
114
122
 
115
123
  // 检查本地 node_modules
116
124
  const packageJsonPath = findPackageJson(packageName);
125
+ console.error(`[DEBUG] findPackageJson=${packageJsonPath}`);
117
126
  if (packageJsonPath) {
118
127
  try {
119
128
  const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
129
+ console.error(`[DEBUG] node_modules pkg.version=${pkg.version}`);
120
130
  return pkg.version || null;
121
131
  } catch (e) {
122
132
  // 忽略错误
@@ -188,10 +198,16 @@ async function checkBolloonUpdates(): Promise<PackageInfo | null> {
188
198
 
189
199
  for (const pkg of packagesToCheck) {
190
200
  const installed = getInstalledVersion(pkg);
201
+ console.error(`[DEBUG] getInstalledVersion(${pkg}) = ${installed}`);
191
202
  if (!installed) continue;
192
203
 
193
- currentVersion = installed;
204
+ // 只记录 @bolloon/bolloon-agent 的版本作为当前版本
205
+ if (pkg === '@bolloon/bolloon-agent') {
206
+ currentVersion = installed;
207
+ }
208
+
194
209
  const latest = await getLatestVersion(pkg);
210
+ console.error(`[DEBUG] getLatestVersion(${pkg}) = ${latest}`);
195
211
 
196
212
  if (latest && compareVersions(installed, latest) < 0) {
197
213
  hasUpdate = true;
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
  }