@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.
- 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/utils/auto-update.js +15 -1
- package/dist/web/components/p2p/index.js +226 -264
- package/dist/web/index.html +12 -0
- package/package.json +3 -1
- package/scripts/build-web.ts +1 -1
- package/scripts/postinstall.js +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/cli-entry.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/utils/auto-update.ts +17 -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/documents/store.ts
CHANGED
|
@@ -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
|
-
},
|
|
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/utils/auto-update.ts
CHANGED
|
@@ -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
|
-
|
|
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+)/);
|
package/tsconfig.electron.json
CHANGED
package/tsconfig.json
CHANGED