@bolloon/bolloon-agent 0.1.2 → 0.1.4
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/bin/bolloon-cli.cjs +15 -7
- package/dist/agents/pi-sdk.js +12 -4
- package/dist/llm/config-store.js +8 -5
- package/dist/web/components/p2p/P2PModal.js +188 -0
- package/dist/web/components/p2p/index.js +264 -226
- package/dist/web/components/p2p/p2p-modal.js +657 -0
- package/dist/web/components/p2p/p2p-tools.js +248 -0
- package/dist/web/server.js +1890 -0
- package/package.json +1 -1
- package/src/web/server.ts +25 -26
|
@@ -0,0 +1,1890 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { createServer } from 'http';
|
|
3
|
+
import { join, dirname } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import * as fs from 'fs/promises';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import { createHyperswarmCommunicator, createTopic, KeyManager, AgentAuthManager, } from '@diap/sdk';
|
|
8
|
+
import { documentReader } from '../documents/reader.js';
|
|
9
|
+
import { initMinimax, getMinimax } from '../constraints/index.js';
|
|
10
|
+
import { createAgentSession } from '../agents/pi-sdk.js';
|
|
11
|
+
import { llmConfigStore } from '../llm/config-store.js';
|
|
12
|
+
import { irohTransport } from '../network/iroh-transport.js';
|
|
13
|
+
// 前端资源路径:在打包后会通过 CommonJS require 加载,使用 import.meta.url
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = dirname(__filename);
|
|
16
|
+
const webRoot = path.join(__dirname, '..', '..', 'dist', 'web');
|
|
17
|
+
const SHARED_SESSION_PATH = path.join(process.env.HOME || '/tmp', '.bolloon', 'sessions');
|
|
18
|
+
const SESSION_CACHE_PATH = path.join(SHARED_SESSION_PATH, 'cache');
|
|
19
|
+
const CHANNELS_PATH = path.join(SHARED_SESSION_PATH, 'channels.json');
|
|
20
|
+
const THEME_PATH = path.join(SHARED_SESSION_PATH, 'theme.json');
|
|
21
|
+
const IPFS_ENDPOINT = 'http://127.0.0.1:5001';
|
|
22
|
+
let irohNodeInfo = null;
|
|
23
|
+
let irohInitialized = false;
|
|
24
|
+
async function ensureSessionDirs() {
|
|
25
|
+
await fs.mkdir(SESSION_CACHE_PATH, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
async function loadChannels() {
|
|
28
|
+
try {
|
|
29
|
+
const data = await fs.readFile(CHANNELS_PATH, 'utf-8');
|
|
30
|
+
return JSON.parse(data);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function saveChannels(channels) {
|
|
37
|
+
const jsonStr = JSON.stringify(channels, null, 2);
|
|
38
|
+
console.log('[saveChannels] 保存频道数据, 数量:', channels.length);
|
|
39
|
+
console.log('[saveChannels] JSON 长度:', jsonStr.length);
|
|
40
|
+
await fs.writeFile(CHANNELS_PATH, jsonStr);
|
|
41
|
+
// 验证保存的内容
|
|
42
|
+
const verifyData = await fs.readFile(CHANNELS_PATH, 'utf-8');
|
|
43
|
+
const verifyChannels = JSON.parse(verifyData);
|
|
44
|
+
console.log('[saveChannels] 验证 - 保存了', verifyChannels.length, '个频道');
|
|
45
|
+
verifyChannels.forEach((ch, i) => {
|
|
46
|
+
console.log(` [${i}] ${ch.name}: did=${ch.did || '无'}`);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
async function loadSession(channelId, sessionId) {
|
|
50
|
+
// sessionId is optional for backward compatibility; if provided, load specific session
|
|
51
|
+
const key = sessionId ? `${channelId}:${sessionId}` : channelId;
|
|
52
|
+
const sessionPath = path.join(SESSION_CACHE_PATH, `${key}.json`);
|
|
53
|
+
try {
|
|
54
|
+
const data = await fs.readFile(sessionPath, 'utf-8');
|
|
55
|
+
return JSON.parse(data);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async function saveSession(session) {
|
|
62
|
+
const key = session.sessionId ? `${session.channelId}:${session.sessionId}` : session.channelId;
|
|
63
|
+
const sessionPath = path.join(SESSION_CACHE_PATH, `${key}.json`);
|
|
64
|
+
await fs.writeFile(sessionPath, JSON.stringify(session, null, 2));
|
|
65
|
+
}
|
|
66
|
+
async function loadTheme() {
|
|
67
|
+
try {
|
|
68
|
+
const data = await fs.readFile(THEME_PATH, 'utf-8');
|
|
69
|
+
return JSON.parse(data);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return { theme: 'light', agentId: '' };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function saveTheme(theme, agentId) {
|
|
76
|
+
await fs.writeFile(THEME_PATH, JSON.stringify({ theme, agentId }, null, 2));
|
|
77
|
+
}
|
|
78
|
+
// ==================== Task Queue & Workflow System ====================
|
|
79
|
+
const TASK_QUEUE_PATH = path.join(SHARED_SESSION_PATH, 'task-queue.json');
|
|
80
|
+
const WORKFLOW_STATE_PATH = path.join(SHARED_SESSION_PATH, 'workflow-state.json');
|
|
81
|
+
async function loadTaskQueue() {
|
|
82
|
+
try {
|
|
83
|
+
const data = await fs.readFile(TASK_QUEUE_PATH, 'utf-8');
|
|
84
|
+
return JSON.parse(data);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function saveTaskQueue(tasks) {
|
|
91
|
+
await fs.writeFile(TASK_QUEUE_PATH, JSON.stringify(tasks, null, 2));
|
|
92
|
+
}
|
|
93
|
+
async function loadWorkflowState(channelId) {
|
|
94
|
+
try {
|
|
95
|
+
const data = await fs.readFile(WORKFLOW_STATE_PATH, 'utf-8');
|
|
96
|
+
const states = JSON.parse(data);
|
|
97
|
+
return states.find(s => s.channelId === channelId) || null;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function saveWorkflowState(state) {
|
|
104
|
+
try {
|
|
105
|
+
const data = await fs.readFile(WORKFLOW_STATE_PATH, 'utf-8');
|
|
106
|
+
const states = JSON.parse(data);
|
|
107
|
+
const index = states.findIndex(s => s.channelId === state.channelId);
|
|
108
|
+
if (index >= 0) {
|
|
109
|
+
states[index] = state;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
states.push(state);
|
|
113
|
+
}
|
|
114
|
+
await fs.writeFile(WORKFLOW_STATE_PATH, JSON.stringify(states, null, 2));
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
await fs.writeFile(WORKFLOW_STATE_PATH, JSON.stringify([state], null, 2));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
let isExecutingTask = false;
|
|
121
|
+
let executionTaskId = null;
|
|
122
|
+
async function executeTask(task, channelId) {
|
|
123
|
+
if (isExecutingTask)
|
|
124
|
+
return;
|
|
125
|
+
isExecutingTask = true;
|
|
126
|
+
executionTaskId = task.id;
|
|
127
|
+
const agent = await getAgentForChannel(channelId);
|
|
128
|
+
const tasks = await loadTaskQueue();
|
|
129
|
+
const taskIndex = tasks.findIndex(t => t.id === task.id);
|
|
130
|
+
if (taskIndex >= 0) {
|
|
131
|
+
tasks[taskIndex].status = 'running';
|
|
132
|
+
tasks[taskIndex].updatedAt = new Date().toISOString();
|
|
133
|
+
await saveTaskQueue(tasks);
|
|
134
|
+
}
|
|
135
|
+
broadcast({ type: 'task_status', taskId: task.id, status: 'running', progress: 0 }, channelId);
|
|
136
|
+
try {
|
|
137
|
+
let result = '';
|
|
138
|
+
switch (task.type) {
|
|
139
|
+
case 'chat':
|
|
140
|
+
if (task.description) {
|
|
141
|
+
broadcast({ type: 'status', content: `执行任务: ${task.title}` }, channelId);
|
|
142
|
+
result = await agent.prompt(task.description);
|
|
143
|
+
}
|
|
144
|
+
break;
|
|
145
|
+
case 'read':
|
|
146
|
+
if (task.description) {
|
|
147
|
+
broadcast({ type: 'status', content: `读取文档: ${task.description}` }, channelId);
|
|
148
|
+
const content = await documentReader.read(task.description);
|
|
149
|
+
result = `📄 文档读取完成\n\n${content.text.substring(0, 500)}${content.text.length > 500 ? '...' : ''}`;
|
|
150
|
+
}
|
|
151
|
+
break;
|
|
152
|
+
case 'summarize':
|
|
153
|
+
if (task.description) {
|
|
154
|
+
broadcast({ type: 'status', content: `总结文档: ${task.description}` }, channelId);
|
|
155
|
+
const content = await documentReader.read(task.description);
|
|
156
|
+
const llm = getMinimax();
|
|
157
|
+
const summary = await llm.summarize(content.text);
|
|
158
|
+
result = `📝 文档总结:\n\n${summary.summary}`;
|
|
159
|
+
}
|
|
160
|
+
break;
|
|
161
|
+
case 'workflow':
|
|
162
|
+
// 执行多步骤工作流
|
|
163
|
+
if (task.steps && task.steps.length > 0) {
|
|
164
|
+
let loopCount = 0;
|
|
165
|
+
for (let i = 0; i < task.steps.length; i++) {
|
|
166
|
+
// 广播循环开始
|
|
167
|
+
loopCount++;
|
|
168
|
+
broadcast({ type: 'workflow_loop', loopCount, content: `开始步骤 ${i + 1}/${task.steps.length}: ${task.steps[i].name}` }, channelId);
|
|
169
|
+
task.steps[i].status = 'running';
|
|
170
|
+
broadcast({ type: 'task_status', taskId: task.id, status: 'running', currentStep: i, totalSteps: task.steps.length }, channelId);
|
|
171
|
+
broadcast({ type: 'workflow_step', step: `步骤 ${i + 1}`, content: `执行中: ${task.steps[i].name}` }, channelId);
|
|
172
|
+
// 执行步骤 - 模拟流式输出
|
|
173
|
+
for (let j = 0; j < 3; j++) {
|
|
174
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
175
|
+
broadcast({ type: 'workflow_step', step: `步骤 ${i + 1}`, content: `执行中... (${(j + 1) * 33}%)` }, channelId);
|
|
176
|
+
}
|
|
177
|
+
task.steps[i].status = 'completed';
|
|
178
|
+
task.progress = Math.round(((i + 1) / task.steps.length) * 100);
|
|
179
|
+
broadcast({ type: 'workflow_step', step: `步骤 ${i + 1}`, content: `✅ 完成: ${task.steps[i].name}` }, channelId);
|
|
180
|
+
broadcast({ type: 'workflow_loop', loopCount, status: 'completed', content: `步骤 ${i + 1} 完成` }, channelId);
|
|
181
|
+
broadcast({ type: 'task_status', taskId: task.id, progress: task.progress }, channelId);
|
|
182
|
+
}
|
|
183
|
+
result = '✅ 工作流执行完成';
|
|
184
|
+
broadcast({ type: 'workflow_loop', loopCount, status: 'finished', content: result }, channelId);
|
|
185
|
+
}
|
|
186
|
+
break;
|
|
187
|
+
default:
|
|
188
|
+
result = '未知任务类型';
|
|
189
|
+
}
|
|
190
|
+
// 更新任务状态
|
|
191
|
+
const tasks = await loadTaskQueue();
|
|
192
|
+
const idx = tasks.findIndex(t => t.id === task.id);
|
|
193
|
+
if (idx >= 0) {
|
|
194
|
+
tasks[idx].status = 'completed';
|
|
195
|
+
tasks[idx].progress = 100;
|
|
196
|
+
tasks[idx].result = result;
|
|
197
|
+
tasks[idx].updatedAt = new Date().toISOString();
|
|
198
|
+
await saveTaskQueue(tasks);
|
|
199
|
+
}
|
|
200
|
+
broadcast({ type: 'task_status', taskId: task.id, status: 'completed', progress: 100, result }, channelId);
|
|
201
|
+
broadcast({ type: 'ai', content: result }, channelId);
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
const tasks = await loadTaskQueue();
|
|
205
|
+
const idx = tasks.findIndex(t => t.id === task.id);
|
|
206
|
+
if (idx >= 0) {
|
|
207
|
+
tasks[idx].status = 'failed';
|
|
208
|
+
tasks[idx].error = error.message;
|
|
209
|
+
tasks[idx].updatedAt = new Date().toISOString();
|
|
210
|
+
await saveTaskQueue(tasks);
|
|
211
|
+
}
|
|
212
|
+
broadcast({ type: 'task_status', taskId: task.id, status: 'failed', error: error.message }, channelId);
|
|
213
|
+
broadcast({ type: 'error', content: `任务执行失败: ${error.message}` }, channelId);
|
|
214
|
+
}
|
|
215
|
+
isExecutingTask = false;
|
|
216
|
+
executionTaskId = null;
|
|
217
|
+
}
|
|
218
|
+
let sseClients = new Set();
|
|
219
|
+
let channelSessions = new Map(); // key: channelId
|
|
220
|
+
let sessionMessages = new Map(); // key: channelId + sessionId
|
|
221
|
+
async function getAgentForChannel(channelId, channelDid, channelName, channelDidDoc) {
|
|
222
|
+
// 获取当前 channel 的 currentSessionId
|
|
223
|
+
const channels = await loadChannels();
|
|
224
|
+
const channel = channels.find(c => c.id === channelId);
|
|
225
|
+
const currentSessionId = channel?.currentSessionId || 'default';
|
|
226
|
+
const sessionKey = `${channelId}:${currentSessionId}`;
|
|
227
|
+
console.log(`[Agent] 获取频道 ${channelId} 的 session, sessionKey = ${sessionKey}`);
|
|
228
|
+
const existingSession = channelSessions.get(sessionKey);
|
|
229
|
+
// 如果已有 session,检查是否需要更新 identity
|
|
230
|
+
if (existingSession) {
|
|
231
|
+
console.log(`[Agent] 找到现有 session: ${sessionKey}`);
|
|
232
|
+
const currentIdentity = existingSession.getIdentity();
|
|
233
|
+
// 如果当前 identity 没有真实 DID,或者 DID 与频道的 DID 不匹配,需要重建
|
|
234
|
+
let needsUpdate = !currentIdentity.did.startsWith('did:pi:') ||
|
|
235
|
+
(channelDid && !currentIdentity.did.includes(channelId));
|
|
236
|
+
if (!needsUpdate && channelDid && currentIdentity.did !== channelDid) {
|
|
237
|
+
needsUpdate = true;
|
|
238
|
+
}
|
|
239
|
+
if (needsUpdate && channelDid) {
|
|
240
|
+
// 更新现有 session 的 identity
|
|
241
|
+
existingSession.updateIdentity({
|
|
242
|
+
did: channelDid,
|
|
243
|
+
name: channelName || `Channel-${channelId.slice(-6)}`,
|
|
244
|
+
publicKey: '',
|
|
245
|
+
createdAt: Date.now(),
|
|
246
|
+
cid: channelDidDoc?.cid,
|
|
247
|
+
ipnsName: channelDidDoc?.ipnsName
|
|
248
|
+
});
|
|
249
|
+
console.log(`[Agent] 频道 ${channelId} 身份更新: DID = ${channelDid}`);
|
|
250
|
+
}
|
|
251
|
+
return existingSession;
|
|
252
|
+
}
|
|
253
|
+
// 构建频道的身份文档
|
|
254
|
+
const identityDoc = channelDid ? {
|
|
255
|
+
did: channelDid,
|
|
256
|
+
name: channelName || `Channel-${channelId.slice(-6)}`,
|
|
257
|
+
publicKey: '',
|
|
258
|
+
createdAt: Date.now(),
|
|
259
|
+
cid: channelDidDoc?.cid,
|
|
260
|
+
ipnsName: channelDidDoc?.ipnsName
|
|
261
|
+
} : undefined;
|
|
262
|
+
console.log(`[Agent] 创建新 session: ${sessionKey}`);
|
|
263
|
+
const session = await createAgentSession({
|
|
264
|
+
cwd: process.cwd(),
|
|
265
|
+
peerId: `channel-${channelId}:${currentSessionId}`,
|
|
266
|
+
identityDoc
|
|
267
|
+
}, true); // forceNew: true 强制创建新实例
|
|
268
|
+
channelSessions.set(sessionKey, session);
|
|
269
|
+
if (channelDid) {
|
|
270
|
+
console.log(`[Agent] 新建频道 ${channelId} session, DID = ${channelDid}, sessionId = ${currentSessionId}`);
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
console.log(`[Agent] 新建频道 ${channelId} session, 使用默认身份, sessionId = ${currentSessionId}`);
|
|
274
|
+
}
|
|
275
|
+
return session;
|
|
276
|
+
}
|
|
277
|
+
export async function createWebServer(port = 3000) {
|
|
278
|
+
// 防止 P2P DHT 超时等错误导致进程崩溃
|
|
279
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
280
|
+
console.error('[警告] 未处理的 Promise 拒绝:', reason);
|
|
281
|
+
});
|
|
282
|
+
// 重置旧的 agent session,确保使用新的 LLM 配置
|
|
283
|
+
const { resetAgentSession } = await import('../agents/pi-sdk.js');
|
|
284
|
+
resetAgentSession();
|
|
285
|
+
// 初始化 LLM(从配置文件读取 MiniMax 配置)
|
|
286
|
+
initMinimax();
|
|
287
|
+
// ==================== P2P DIAP 身份初始化 ====================
|
|
288
|
+
let p2pIdentity = {
|
|
289
|
+
did: '',
|
|
290
|
+
name: '',
|
|
291
|
+
publicKey: '',
|
|
292
|
+
keypair: null
|
|
293
|
+
};
|
|
294
|
+
let p2pCommunicator = null;
|
|
295
|
+
try {
|
|
296
|
+
console.log('开始生成 P2P 身份...');
|
|
297
|
+
// 生成 DIAP 身份
|
|
298
|
+
const kp = KeyManager.generate();
|
|
299
|
+
console.log('KeyManager.generate() 完成, kp:', !!kp, 'kp.did:', kp?.did);
|
|
300
|
+
console.log('kp.publicKey:', kp?.publicKey);
|
|
301
|
+
const did = kp.did || 'did:unknown:123456';
|
|
302
|
+
console.log(`DID: ${did}`);
|
|
303
|
+
const username = 'web-user';
|
|
304
|
+
const suffix = did?.split(':').pop()?.substring(0, 4) || 'xxxx';
|
|
305
|
+
const name = `blln-${username}-${suffix}`;
|
|
306
|
+
p2pIdentity = {
|
|
307
|
+
did: did || '',
|
|
308
|
+
name,
|
|
309
|
+
publicKey: Buffer.from(kp.publicKey).toString('hex'),
|
|
310
|
+
keypair: kp
|
|
311
|
+
};
|
|
312
|
+
console.log(`P2P 身份已生成: ${p2pIdentity.did}`);
|
|
313
|
+
// 尝试发布 DID 到 IPFS
|
|
314
|
+
try {
|
|
315
|
+
const auth = await AgentAuthManager.newWithRemoteIpfs('http://127.0.0.1:5001', 'http://127.0.0.1:8080');
|
|
316
|
+
await auth.registerAgent({ name, services: [] }, kp, '');
|
|
317
|
+
console.log('P2P DID 已发布到 IPFS');
|
|
318
|
+
}
|
|
319
|
+
catch (e) {
|
|
320
|
+
console.log('P2P DID 本地模式运行');
|
|
321
|
+
}
|
|
322
|
+
// 初始化 P2P 通信器
|
|
323
|
+
try {
|
|
324
|
+
const rawSeed = crypto.getRandomValues(new Uint8Array(32));
|
|
325
|
+
p2pCommunicator = createHyperswarmCommunicator({
|
|
326
|
+
server: true,
|
|
327
|
+
client: true,
|
|
328
|
+
autoConnect: true,
|
|
329
|
+
maxConnections: 50,
|
|
330
|
+
seed: rawSeed
|
|
331
|
+
});
|
|
332
|
+
p2pCommunicator.on('connection', (conn) => {
|
|
333
|
+
console.log(`P2P 连接: ${conn.publicKey.substring(0, 8)}...`);
|
|
334
|
+
});
|
|
335
|
+
p2pCommunicator.on('message', async (msg, conn) => {
|
|
336
|
+
const content = new TextDecoder().decode(msg.content);
|
|
337
|
+
console.log(`P2P 收到消息: ${content.substring(0, 50)}...`);
|
|
338
|
+
// 可以在这里处理接收到的消息
|
|
339
|
+
broadcast({ type: 'p2p_message', from: conn.publicKey.substring(0, 8), content }, undefined);
|
|
340
|
+
});
|
|
341
|
+
await p2pCommunicator.start();
|
|
342
|
+
const topic = createTopic('bolloon-agent-harness');
|
|
343
|
+
await p2pCommunicator.joinTopic(topic);
|
|
344
|
+
console.log(`P2P 网络已就绪`);
|
|
345
|
+
}
|
|
346
|
+
catch (e) {
|
|
347
|
+
console.log(`P2P 网络初始化失败: ${e.message}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
catch (e) {
|
|
351
|
+
console.log(`P2P 身份初始化失败: ${e.message}`);
|
|
352
|
+
}
|
|
353
|
+
const app = express();
|
|
354
|
+
const server = createServer(app);
|
|
355
|
+
await ensureSessionDirs();
|
|
356
|
+
app.use(express.json());
|
|
357
|
+
app.use((req, res, next) => {
|
|
358
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
359
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
360
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
361
|
+
if (req.method === 'OPTIONS') {
|
|
362
|
+
return res.status(200).end();
|
|
363
|
+
}
|
|
364
|
+
next();
|
|
365
|
+
});
|
|
366
|
+
app.use(express.static(webRoot));
|
|
367
|
+
app.get('/', (req, res) => {
|
|
368
|
+
res.sendFile(join(webRoot, 'index.html'));
|
|
369
|
+
});
|
|
370
|
+
app.get('/api-config', (req, res) => {
|
|
371
|
+
res.sendFile(join(webRoot, 'api-config.html'));
|
|
372
|
+
});
|
|
373
|
+
app.get('/events', (req, res) => {
|
|
374
|
+
const channelId = req.query.channelId;
|
|
375
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
376
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
377
|
+
res.setHeader('Connection', 'keep-alive');
|
|
378
|
+
res.flushHeaders();
|
|
379
|
+
const clientInfo = { res, channelId };
|
|
380
|
+
sseClients.add(clientInfo);
|
|
381
|
+
req.on('close', () => {
|
|
382
|
+
sseClients.delete(clientInfo);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
app.post('/message', async (req, res) => {
|
|
386
|
+
const { text, channelId, channelDid } = req.body;
|
|
387
|
+
if (!text) {
|
|
388
|
+
return res.status(400).json({ error: 'No text provided' });
|
|
389
|
+
}
|
|
390
|
+
if (!channelId) {
|
|
391
|
+
return res.status(400).json({ error: 'No channelId provided' });
|
|
392
|
+
}
|
|
393
|
+
// 获取频道信息,包括真实 DID 和完整 DID 文档
|
|
394
|
+
const channels = await loadChannels();
|
|
395
|
+
const channel = channels.find(c => c.id === channelId);
|
|
396
|
+
const currentSessionId = channel?.currentSessionId || 'default';
|
|
397
|
+
const realChannelDid = channelDid || channel?.did || '';
|
|
398
|
+
const realChannelName = channel?.name || '';
|
|
399
|
+
const realChannelDidDoc = channel?.didDocument;
|
|
400
|
+
broadcast({ type: 'user', content: text }, channelId);
|
|
401
|
+
try {
|
|
402
|
+
const agent = await getAgentForChannel(channelId, realChannelDid, realChannelName, realChannelDidDoc);
|
|
403
|
+
let fullResponse = '';
|
|
404
|
+
const streamCallback = (event) => {
|
|
405
|
+
// 同时发送给流式显示和工作流显示
|
|
406
|
+
if (event.type === 'token' || event.type === 'thinking') {
|
|
407
|
+
broadcast({ type: 'stream', streamType: event.type, content: event.content }, channelId);
|
|
408
|
+
// 同时作为 workflow_step 显示(用于动态 loop 循环)
|
|
409
|
+
if (event.content) {
|
|
410
|
+
broadcast({ type: 'workflow_step', step: 'AI 思考', content: event.content.substring(0, 100) }, channelId);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
else if (event.type === 'status' || event.type === 'tool') {
|
|
414
|
+
broadcast({ type: 'status', tool: event.tool, content: event.content }, channelId);
|
|
415
|
+
broadcast({ type: 'workflow_step', step: event.tool || '系统', content: event.content }, channelId);
|
|
416
|
+
console.log(`[SSE 广播] workflow_step: step=${event.tool}, content="${event.content?.substring(0, 80)}..."`);
|
|
417
|
+
}
|
|
418
|
+
else if (event.type === 'error') {
|
|
419
|
+
broadcast({ type: 'error', content: event.content }, channelId);
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
console.log(`[消息处理] 开始处理用户消息, channelId: ${channelId}, sessionId: ${currentSessionId}`);
|
|
423
|
+
// 将真实 DID 作为上下文前缀,让 AI 使用真实的 DID 而不是自己编造的
|
|
424
|
+
const contextHint = realChannelDid ? `[系统上下文] 当前频道名称: ${realChannelName}, 你的真实 DID: ${realChannelDid}\n\n` : '';
|
|
425
|
+
fullResponse = await agent.promptStream(contextHint + text, streamCallback);
|
|
426
|
+
broadcast({ type: 'ai', content: fullResponse }, channelId);
|
|
427
|
+
const existingSession = await loadSession(channelId, currentSessionId);
|
|
428
|
+
const session = existingSession || { channelId, sessionId: currentSessionId, messages: [], lastUpdated: new Date().toISOString() };
|
|
429
|
+
session.sessionId = currentSessionId;
|
|
430
|
+
session.messages.push({ id: crypto.randomUUID(), type: 'user', content: text, timestamp: new Date().toISOString() });
|
|
431
|
+
session.messages.push({ id: crypto.randomUUID(), type: 'ai', content: fullResponse, timestamp: new Date().toISOString() });
|
|
432
|
+
session.lastUpdated = new Date().toISOString();
|
|
433
|
+
await saveSession(session);
|
|
434
|
+
const channels = await loadChannels();
|
|
435
|
+
const channel = channels.find(c => c.id === channelId);
|
|
436
|
+
if (channel && channel.name === '智能体') {
|
|
437
|
+
const renameSuggestion = await agent.suggestRename(session.messages);
|
|
438
|
+
if (renameSuggestion) {
|
|
439
|
+
channel.name = renameSuggestion;
|
|
440
|
+
await saveChannels(channels);
|
|
441
|
+
broadcast({ type: 'renamed', channelId, newName: renameSuggestion }, channelId);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (channel) {
|
|
445
|
+
channel.updatedAt = new Date().toISOString();
|
|
446
|
+
await saveChannels(channels);
|
|
447
|
+
}
|
|
448
|
+
broadcast({ type: 'done' }, channelId);
|
|
449
|
+
res.json({ ok: true });
|
|
450
|
+
}
|
|
451
|
+
catch (err) {
|
|
452
|
+
broadcast({ type: 'error', content: err.message }, channelId);
|
|
453
|
+
broadcast({ type: 'done' }, channelId);
|
|
454
|
+
res.status(500).json({ error: err.message });
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
// 获取频道并确保每个频道都有 DID
|
|
458
|
+
async function getChannelsWithDID() {
|
|
459
|
+
const channels = await loadChannels();
|
|
460
|
+
console.log(`[getChannelsWithDID] 加载了 ${channels.length} 个频道`);
|
|
461
|
+
let changed = false;
|
|
462
|
+
for (const channel of channels) {
|
|
463
|
+
// 检查 DID 是否有效
|
|
464
|
+
const didMissing = channel.did === undefined || channel.did === null || channel.did === 'undefined' || channel.did === 'null' || channel.did === '';
|
|
465
|
+
console.log(`[getChannelsWithDID] 频道 ${channel.name}: did=${JSON.stringify(channel.did)}, 缺失=${didMissing}`);
|
|
466
|
+
if (didMissing) {
|
|
467
|
+
console.log(`[修复频道] ${channel.name} (${channel.id}) 缺少 DID,正在生成...`);
|
|
468
|
+
try {
|
|
469
|
+
const kp = KeyManager.generate();
|
|
470
|
+
const generatedDid = kp.did;
|
|
471
|
+
console.log(`[修复频道] KeyManager.generate() 结果: kp=${!!kp}, did=${generatedDid}`);
|
|
472
|
+
if (generatedDid && typeof generatedDid === 'string' && generatedDid.length > 0) {
|
|
473
|
+
channel.did = generatedDid;
|
|
474
|
+
channel.publicKey = Buffer.from(kp.publicKey).toString('hex');
|
|
475
|
+
console.log(`[修复频道] ${channel.name} 生成了 DID: ${channel.did}`);
|
|
476
|
+
// 发布到 IPFS 并保存完整 DID 文档
|
|
477
|
+
try {
|
|
478
|
+
const auth = await AgentAuthManager.newWithRemoteIpfs('http://127.0.0.1:5001', 'http://127.0.0.1:8080');
|
|
479
|
+
const result = await auth.registerAgent({ name: channel.name, services: [] }, kp, '');
|
|
480
|
+
channel.cid = result.cid || '';
|
|
481
|
+
// 保存完整 DID 文档(用于传递给 session)
|
|
482
|
+
if (result.didDocument) {
|
|
483
|
+
channel.didDocument = result.didDocument;
|
|
484
|
+
}
|
|
485
|
+
console.log(`[修复频道] ${channel.name} CID: ${channel.cid}`);
|
|
486
|
+
}
|
|
487
|
+
catch (ipfsErr) {
|
|
488
|
+
console.log(`[修复频道] ${channel.name} IPFS 失败`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
console.log(`[修复频道] ${channel.name} KeyManager 返回无效 DID`);
|
|
493
|
+
channel.did = `did:web:${channel.id}`;
|
|
494
|
+
channel.publicKey = `pk_${channel.id}`;
|
|
495
|
+
}
|
|
496
|
+
changed = true;
|
|
497
|
+
}
|
|
498
|
+
catch (e) {
|
|
499
|
+
console.log(`[修复频道] ${channel.name} 失败: ${e}`);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if (changed) {
|
|
504
|
+
console.log('[getChannelsWithDID] 保存修改后的频道');
|
|
505
|
+
await saveChannels(channels);
|
|
506
|
+
}
|
|
507
|
+
return channels;
|
|
508
|
+
}
|
|
509
|
+
app.get('/channels', async (_req, res) => {
|
|
510
|
+
try {
|
|
511
|
+
console.log('[API] /channels 被调用');
|
|
512
|
+
const channels = await getChannelsWithDID();
|
|
513
|
+
console.log('[获取频道] 返回', channels.length, '个');
|
|
514
|
+
channels.forEach((ch, i) => {
|
|
515
|
+
console.log(` [${i}] ${ch.name} - did: ${ch.did || '无'} - cid: ${ch.cid || '无'}`);
|
|
516
|
+
});
|
|
517
|
+
res.json(channels);
|
|
518
|
+
}
|
|
519
|
+
catch (err) {
|
|
520
|
+
console.error('[API] /channels 错误:', err);
|
|
521
|
+
res.status(500).json({ error: err.message });
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
app.post('/channels', async (req, res) => {
|
|
525
|
+
try {
|
|
526
|
+
const { name, agentId } = req.body;
|
|
527
|
+
console.log(`[创建频道] 收到请求: name=${name}, agentId=${agentId}`);
|
|
528
|
+
if (!name || !agentId) {
|
|
529
|
+
return res.status(400).json({ error: 'name and agentId required' });
|
|
530
|
+
}
|
|
531
|
+
const channels = await loadChannels();
|
|
532
|
+
const id = `ch_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
|
|
533
|
+
// 先创建频道(不阻塞等待 DID 生成)
|
|
534
|
+
const channel = {
|
|
535
|
+
id,
|
|
536
|
+
name,
|
|
537
|
+
agentId,
|
|
538
|
+
createdAt: new Date().toISOString(),
|
|
539
|
+
updatedAt: new Date().toISOString(),
|
|
540
|
+
currentSessionId: `sess_${Date.now()}`,
|
|
541
|
+
sessions: [{
|
|
542
|
+
id: `sess_${Date.now()}`,
|
|
543
|
+
createdAt: new Date().toISOString(),
|
|
544
|
+
messageCount: 0,
|
|
545
|
+
preview: ''
|
|
546
|
+
}]
|
|
547
|
+
};
|
|
548
|
+
console.log(`[创建频道] 先保存频道 ID: ${id}`);
|
|
549
|
+
channels.push(channel);
|
|
550
|
+
await saveChannels(channels);
|
|
551
|
+
await saveSession({ channelId: id, sessionId: 'default', messages: [], lastUpdated: new Date().toISOString() });
|
|
552
|
+
res.json(channel);
|
|
553
|
+
// 后台生成 DID
|
|
554
|
+
console.log(`[创建频道] 后台生成 DID...`);
|
|
555
|
+
setTimeout(async () => {
|
|
556
|
+
try {
|
|
557
|
+
const kp = KeyManager.generate();
|
|
558
|
+
if (kp.did) {
|
|
559
|
+
const allChannels = await loadChannels();
|
|
560
|
+
const ch = allChannels.find(c => c.id === id);
|
|
561
|
+
if (ch) {
|
|
562
|
+
ch.did = kp.did;
|
|
563
|
+
ch.publicKey = Buffer.from(kp.publicKey).toString('hex');
|
|
564
|
+
console.log(`[创建频道] DID 生成完成: ${ch.did}`);
|
|
565
|
+
// 发布到 IPFS
|
|
566
|
+
try {
|
|
567
|
+
const auth = await AgentAuthManager.newWithRemoteIpfs('http://127.0.0.1:5001', 'http://127.0.0.1:8080');
|
|
568
|
+
const result = await auth.registerAgent({ name, services: [] }, kp, '');
|
|
569
|
+
ch.cid = result.cid || '';
|
|
570
|
+
console.log(`[创建频道] CID: ${ch.cid}`);
|
|
571
|
+
}
|
|
572
|
+
catch { }
|
|
573
|
+
await saveChannels(allChannels);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
catch (e) {
|
|
578
|
+
console.log(`[创建频道] ${name} 后台生成 DID 失败`);
|
|
579
|
+
}
|
|
580
|
+
}, 100);
|
|
581
|
+
}
|
|
582
|
+
catch (err) {
|
|
583
|
+
console.error('[创建频道] 错误:', err);
|
|
584
|
+
res.status(500).json({ error: err.message });
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
// 创建新 Session(在现有 Channel 下)
|
|
588
|
+
app.post('/channels/:channelId/sessions', async (req, res) => {
|
|
589
|
+
try {
|
|
590
|
+
const { channelId } = req.params;
|
|
591
|
+
const channels = await loadChannels();
|
|
592
|
+
const channel = channels.find(c => c.id === channelId);
|
|
593
|
+
if (!channel) {
|
|
594
|
+
return res.status(404).json({ error: 'Channel not found' });
|
|
595
|
+
}
|
|
596
|
+
const sessionId = `sess_${Date.now()}`;
|
|
597
|
+
const session = {
|
|
598
|
+
id: sessionId,
|
|
599
|
+
createdAt: new Date().toISOString(),
|
|
600
|
+
messageCount: 0,
|
|
601
|
+
preview: ''
|
|
602
|
+
};
|
|
603
|
+
if (!channel.sessions) {
|
|
604
|
+
channel.sessions = [];
|
|
605
|
+
}
|
|
606
|
+
channel.sessions.push(session);
|
|
607
|
+
channel.currentSessionId = sessionId;
|
|
608
|
+
channel.updatedAt = new Date().toISOString();
|
|
609
|
+
await saveChannels(channels);
|
|
610
|
+
await saveSession({ channelId, sessionId, messages: [], lastUpdated: new Date().toISOString() });
|
|
611
|
+
res.json({ session, currentSessionId: sessionId });
|
|
612
|
+
}
|
|
613
|
+
catch (err) {
|
|
614
|
+
console.error('[创建Session] 错误:', err);
|
|
615
|
+
res.status(500).json({ error: err.message });
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
// 获取 Channel 下的所有 Sessions
|
|
619
|
+
app.get('/channels/:channelId/sessions', async (req, res) => {
|
|
620
|
+
try {
|
|
621
|
+
const { channelId } = req.params;
|
|
622
|
+
const channels = await loadChannels();
|
|
623
|
+
const channel = channels.find(c => c.id === channelId);
|
|
624
|
+
if (!channel) {
|
|
625
|
+
return res.status(404).json({ error: 'Channel not found' });
|
|
626
|
+
}
|
|
627
|
+
res.json({
|
|
628
|
+
sessions: channel.sessions || [],
|
|
629
|
+
currentSessionId: channel.currentSessionId
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
catch (err) {
|
|
633
|
+
console.error('[获取Sessions] 错误:', err);
|
|
634
|
+
res.status(500).json({ error: err.message });
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
// 切换 Session
|
|
638
|
+
app.post('/channels/:channelId/sessions/:sessionId/switch', async (req, res) => {
|
|
639
|
+
try {
|
|
640
|
+
const { channelId, sessionId } = req.params;
|
|
641
|
+
const channels = await loadChannels();
|
|
642
|
+
const channel = channels.find(c => c.id === channelId);
|
|
643
|
+
if (!channel) {
|
|
644
|
+
return res.status(404).json({ error: 'Channel not found' });
|
|
645
|
+
}
|
|
646
|
+
channel.currentSessionId = sessionId;
|
|
647
|
+
channel.updatedAt = new Date().toISOString();
|
|
648
|
+
await saveChannels(channels);
|
|
649
|
+
res.json({ ok: true, currentSessionId: sessionId });
|
|
650
|
+
}
|
|
651
|
+
catch (err) {
|
|
652
|
+
console.error('[切换Session] 错误:', err);
|
|
653
|
+
res.status(500).json({ error: err.message });
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
app.delete('/channels/:channelId', async (req, res) => {
|
|
657
|
+
try {
|
|
658
|
+
const { channelId } = req.params;
|
|
659
|
+
const channels = await loadChannels();
|
|
660
|
+
const index = channels.findIndex(c => c.id === channelId);
|
|
661
|
+
if (index === -1) {
|
|
662
|
+
return res.status(404).json({ error: 'Channel not found' });
|
|
663
|
+
}
|
|
664
|
+
channels.splice(index, 1);
|
|
665
|
+
await saveChannels(channels);
|
|
666
|
+
try {
|
|
667
|
+
await fs.unlink(path.join(SESSION_CACHE_PATH, `${channelId}.json`));
|
|
668
|
+
}
|
|
669
|
+
catch { }
|
|
670
|
+
res.json({ ok: true });
|
|
671
|
+
}
|
|
672
|
+
catch (err) {
|
|
673
|
+
res.status(500).json({ error: err.message });
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
app.patch('/channels/:channelId', async (req, res) => {
|
|
677
|
+
try {
|
|
678
|
+
const { channelId } = req.params;
|
|
679
|
+
const { name } = req.body;
|
|
680
|
+
if (!name) {
|
|
681
|
+
return res.status(400).json({ error: 'Name required' });
|
|
682
|
+
}
|
|
683
|
+
const channels = await loadChannels();
|
|
684
|
+
const channel = channels.find(c => c.id === channelId);
|
|
685
|
+
if (!channel) {
|
|
686
|
+
return res.status(404).json({ error: 'Channel not found' });
|
|
687
|
+
}
|
|
688
|
+
channel.name = name;
|
|
689
|
+
channel.updatedAt = new Date().toISOString();
|
|
690
|
+
await saveChannels(channels);
|
|
691
|
+
res.json(channel);
|
|
692
|
+
}
|
|
693
|
+
catch (err) {
|
|
694
|
+
res.status(500).json({ error: err.message });
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
app.get('/sessions/:channelId', async (req, res) => {
|
|
698
|
+
try {
|
|
699
|
+
const session = await loadSession(req.params.channelId);
|
|
700
|
+
res.json(session || { channelId: req.params.channelId, messages: [], lastUpdated: null });
|
|
701
|
+
}
|
|
702
|
+
catch (err) {
|
|
703
|
+
res.status(500).json({ error: err.message });
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
app.get('/theme', async (req, res) => {
|
|
707
|
+
try {
|
|
708
|
+
const themeData = await loadTheme();
|
|
709
|
+
res.json(themeData);
|
|
710
|
+
}
|
|
711
|
+
catch (err) {
|
|
712
|
+
res.json({ theme: 'light', agentId: '' });
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
app.post('/theme', async (req, res) => {
|
|
716
|
+
try {
|
|
717
|
+
const { theme, agentId } = req.body;
|
|
718
|
+
if (theme !== 'light' && theme !== 'dark') {
|
|
719
|
+
return res.status(400).json({ error: 'Invalid theme' });
|
|
720
|
+
}
|
|
721
|
+
await saveTheme(theme, agentId || '');
|
|
722
|
+
res.json({ ok: true });
|
|
723
|
+
}
|
|
724
|
+
catch (err) {
|
|
725
|
+
res.status(500).json({ error: err.message });
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
// 重新生成回复
|
|
729
|
+
app.post('/regenerate', async (req, res) => {
|
|
730
|
+
const { channelId, userMessage } = req.body;
|
|
731
|
+
if (!channelId) {
|
|
732
|
+
return res.status(400).json({ error: 'No channelId provided' });
|
|
733
|
+
}
|
|
734
|
+
if (!userMessage) {
|
|
735
|
+
return res.status(400).json({ error: 'No userMessage provided' });
|
|
736
|
+
}
|
|
737
|
+
try {
|
|
738
|
+
const channels = await loadChannels();
|
|
739
|
+
const channel = channels.find(c => c.id === channelId);
|
|
740
|
+
const currentSessionId = channel?.currentSessionId || 'default';
|
|
741
|
+
const realChannelDid = channel?.did || '';
|
|
742
|
+
const realChannelName = channel?.name || '';
|
|
743
|
+
const realChannelDidDoc = channel?.didDocument;
|
|
744
|
+
// 通知前端开始重新生成
|
|
745
|
+
broadcast({ type: 'regenerating', channelId }, channelId);
|
|
746
|
+
const agent = await getAgentForChannel(channelId, realChannelDid, realChannelName, realChannelDidDoc);
|
|
747
|
+
let fullResponse = '';
|
|
748
|
+
const streamCallback = (event) => {
|
|
749
|
+
if (event.type === 'token' || event.type === 'thinking') {
|
|
750
|
+
broadcast({ type: 'stream', streamType: event.type, content: event.content }, channelId);
|
|
751
|
+
}
|
|
752
|
+
else if (event.type === 'status' || event.type === 'tool') {
|
|
753
|
+
broadcast({ type: 'status', tool: event.tool, content: event.content }, channelId);
|
|
754
|
+
}
|
|
755
|
+
else if (event.type === 'error') {
|
|
756
|
+
broadcast({ type: 'error', content: event.content }, channelId);
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
// 重新生成时只发送用户消息
|
|
760
|
+
fullResponse = await agent.promptStream(userMessage, streamCallback);
|
|
761
|
+
broadcast({ type: 'ai', content: fullResponse }, channelId);
|
|
762
|
+
// 更新 session
|
|
763
|
+
const existingSession = await loadSession(channelId, currentSessionId);
|
|
764
|
+
if (existingSession && existingSession.messages.length > 0) {
|
|
765
|
+
// 移除最后一个 AI 消息,替换为新的
|
|
766
|
+
const lastAiIndex = existingSession.messages.map((m) => m.type).lastIndexOf('ai');
|
|
767
|
+
if (lastAiIndex !== -1) {
|
|
768
|
+
existingSession.messages = existingSession.messages.slice(0, lastAiIndex);
|
|
769
|
+
}
|
|
770
|
+
existingSession.messages.push({
|
|
771
|
+
id: crypto.randomUUID(),
|
|
772
|
+
type: 'ai',
|
|
773
|
+
content: fullResponse,
|
|
774
|
+
timestamp: new Date().toISOString()
|
|
775
|
+
});
|
|
776
|
+
existingSession.lastUpdated = new Date().toISOString();
|
|
777
|
+
await saveSession(existingSession);
|
|
778
|
+
}
|
|
779
|
+
broadcast({ type: 'done' }, channelId);
|
|
780
|
+
res.json({ ok: true });
|
|
781
|
+
}
|
|
782
|
+
catch (err) {
|
|
783
|
+
broadcast({ type: 'error', content: err.message }, channelId);
|
|
784
|
+
broadcast({ type: 'done' }, channelId);
|
|
785
|
+
res.status(500).json({ error: err.message });
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
// ==================== Task Queue API ====================
|
|
789
|
+
// 获取所有任务
|
|
790
|
+
app.get('/api/tasks', async (req, res) => {
|
|
791
|
+
try {
|
|
792
|
+
const tasks = await loadTaskQueue();
|
|
793
|
+
res.json(tasks);
|
|
794
|
+
}
|
|
795
|
+
catch (err) {
|
|
796
|
+
res.status(500).json({ error: err.message });
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
// 创建新任务
|
|
800
|
+
app.post('/api/tasks', async (req, res) => {
|
|
801
|
+
try {
|
|
802
|
+
const { type, title, description, steps } = req.body;
|
|
803
|
+
if (!type || !title) {
|
|
804
|
+
return res.status(400).json({ error: 'type and title required' });
|
|
805
|
+
}
|
|
806
|
+
const tasks = await loadTaskQueue();
|
|
807
|
+
const task = {
|
|
808
|
+
id: `task_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`,
|
|
809
|
+
type,
|
|
810
|
+
title,
|
|
811
|
+
description,
|
|
812
|
+
status: 'pending',
|
|
813
|
+
progress: 0,
|
|
814
|
+
createdAt: new Date().toISOString(),
|
|
815
|
+
updatedAt: new Date().toISOString(),
|
|
816
|
+
steps: steps?.map((s, i) => ({
|
|
817
|
+
id: `step_${i}`,
|
|
818
|
+
name: s,
|
|
819
|
+
status: 'pending'
|
|
820
|
+
}))
|
|
821
|
+
};
|
|
822
|
+
tasks.push(task);
|
|
823
|
+
await saveTaskQueue(tasks);
|
|
824
|
+
res.json(task);
|
|
825
|
+
}
|
|
826
|
+
catch (err) {
|
|
827
|
+
res.status(500).json({ error: err.message });
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
// 获取单个任务
|
|
831
|
+
app.get('/api/tasks/:taskId', async (req, res) => {
|
|
832
|
+
try {
|
|
833
|
+
const { taskId } = req.params;
|
|
834
|
+
const tasks = await loadTaskQueue();
|
|
835
|
+
const task = tasks.find(t => t.id === taskId);
|
|
836
|
+
if (!task) {
|
|
837
|
+
return res.status(404).json({ error: 'Task not found' });
|
|
838
|
+
}
|
|
839
|
+
res.json(task);
|
|
840
|
+
}
|
|
841
|
+
catch (err) {
|
|
842
|
+
res.status(500).json({ error: err.message });
|
|
843
|
+
}
|
|
844
|
+
});
|
|
845
|
+
// 更新任务
|
|
846
|
+
app.patch('/api/tasks/:taskId', async (req, res) => {
|
|
847
|
+
try {
|
|
848
|
+
const { taskId } = req.params;
|
|
849
|
+
const { status, currentStep } = req.body;
|
|
850
|
+
const tasks = await loadTaskQueue();
|
|
851
|
+
const taskIndex = tasks.findIndex(t => t.id === taskId);
|
|
852
|
+
if (taskIndex === -1) {
|
|
853
|
+
return res.status(404).json({ error: 'Task not found' });
|
|
854
|
+
}
|
|
855
|
+
if (status) {
|
|
856
|
+
tasks[taskIndex].status = status;
|
|
857
|
+
}
|
|
858
|
+
if (currentStep !== undefined) {
|
|
859
|
+
tasks[taskIndex].currentStep = currentStep;
|
|
860
|
+
}
|
|
861
|
+
tasks[taskIndex].updatedAt = new Date().toISOString();
|
|
862
|
+
await saveTaskQueue(tasks);
|
|
863
|
+
res.json(tasks[taskIndex]);
|
|
864
|
+
}
|
|
865
|
+
catch (err) {
|
|
866
|
+
res.status(500).json({ error: err.message });
|
|
867
|
+
}
|
|
868
|
+
});
|
|
869
|
+
// 删除任务
|
|
870
|
+
app.delete('/api/tasks/:taskId', async (req, res) => {
|
|
871
|
+
try {
|
|
872
|
+
const { taskId } = req.params;
|
|
873
|
+
const tasks = await loadTaskQueue();
|
|
874
|
+
const filtered = tasks.filter(t => t.id !== taskId);
|
|
875
|
+
await saveTaskQueue(filtered);
|
|
876
|
+
res.json({ ok: true });
|
|
877
|
+
}
|
|
878
|
+
catch (err) {
|
|
879
|
+
res.status(500).json({ error: err.message });
|
|
880
|
+
}
|
|
881
|
+
});
|
|
882
|
+
// 执行任务(自动执行下一步)
|
|
883
|
+
app.post('/api/tasks/:taskId/execute', async (req, res) => {
|
|
884
|
+
try {
|
|
885
|
+
const { taskId } = req.params;
|
|
886
|
+
const { channelId } = req.body;
|
|
887
|
+
if (!channelId) {
|
|
888
|
+
return res.status(400).json({ error: 'channelId required' });
|
|
889
|
+
}
|
|
890
|
+
const tasks = await loadTaskQueue();
|
|
891
|
+
const task = tasks.find(t => t.id === taskId);
|
|
892
|
+
if (!task) {
|
|
893
|
+
return res.status(404).json({ error: 'Task not found' });
|
|
894
|
+
}
|
|
895
|
+
if (isExecutingTask) {
|
|
896
|
+
return res.status(409).json({ error: 'Another task is currently executing' });
|
|
897
|
+
}
|
|
898
|
+
// 异步执行任务
|
|
899
|
+
executeTask(task, channelId);
|
|
900
|
+
res.json({ ok: true, taskId: task.id });
|
|
901
|
+
}
|
|
902
|
+
catch (err) {
|
|
903
|
+
res.status(500).json({ error: err.message });
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
// 执行下一个待处理任务
|
|
907
|
+
app.post('/api/tasks/execute-next', async (req, res) => {
|
|
908
|
+
try {
|
|
909
|
+
const { channelId } = req.body;
|
|
910
|
+
if (!channelId) {
|
|
911
|
+
return res.status(400).json({ error: 'channelId required' });
|
|
912
|
+
}
|
|
913
|
+
const tasks = await loadTaskQueue();
|
|
914
|
+
const nextTask = tasks.find(t => t.status === 'pending');
|
|
915
|
+
if (!nextTask) {
|
|
916
|
+
return res.json({ ok: false, message: 'No pending tasks' });
|
|
917
|
+
}
|
|
918
|
+
if (isExecutingTask) {
|
|
919
|
+
return res.status(409).json({ error: 'Another task is currently executing' });
|
|
920
|
+
}
|
|
921
|
+
// 异步执行任务
|
|
922
|
+
executeTask(nextTask, channelId);
|
|
923
|
+
res.json({ ok: true, taskId: nextTask.id });
|
|
924
|
+
}
|
|
925
|
+
catch (err) {
|
|
926
|
+
res.status(500).json({ error: err.message });
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
// 创建并执行工作流
|
|
930
|
+
app.post('/api/workflow', async (req, res) => {
|
|
931
|
+
try {
|
|
932
|
+
const { channelId, title, steps } = req.body;
|
|
933
|
+
if (!channelId || !steps || !Array.isArray(steps)) {
|
|
934
|
+
return res.status(400).json({ error: 'channelId and steps required' });
|
|
935
|
+
}
|
|
936
|
+
const tasks = await loadTaskQueue();
|
|
937
|
+
const task = {
|
|
938
|
+
id: `wf_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`,
|
|
939
|
+
type: 'workflow',
|
|
940
|
+
title: title || '工作流',
|
|
941
|
+
description: `包含 ${steps.length} 个步骤的工作流`,
|
|
942
|
+
status: 'pending',
|
|
943
|
+
progress: 0,
|
|
944
|
+
createdAt: new Date().toISOString(),
|
|
945
|
+
updatedAt: new Date().toISOString(),
|
|
946
|
+
steps: steps.map((s, i) => ({
|
|
947
|
+
id: `step_${i}`,
|
|
948
|
+
name: s,
|
|
949
|
+
status: 'pending'
|
|
950
|
+
})),
|
|
951
|
+
currentStep: 0
|
|
952
|
+
};
|
|
953
|
+
tasks.push(task);
|
|
954
|
+
await saveTaskQueue(tasks);
|
|
955
|
+
// 自动开始执行
|
|
956
|
+
if (!isExecutingTask) {
|
|
957
|
+
executeTask(task, channelId);
|
|
958
|
+
}
|
|
959
|
+
res.json({ ok: true, task });
|
|
960
|
+
}
|
|
961
|
+
catch (err) {
|
|
962
|
+
res.status(500).json({ error: err.message });
|
|
963
|
+
}
|
|
964
|
+
});
|
|
965
|
+
// ==================== LLM 配置 API ====================
|
|
966
|
+
// 获取所有 LLM 配置
|
|
967
|
+
app.get('/api/llm-config', async (req, res) => {
|
|
968
|
+
try {
|
|
969
|
+
const config = await llmConfigStore.getConfig();
|
|
970
|
+
const providerInfo = llmConfigStore.getAllProviderInfo();
|
|
971
|
+
// 隐藏 API Key
|
|
972
|
+
const safeConfig = {
|
|
973
|
+
...config,
|
|
974
|
+
providers: Object.fromEntries(Object.entries(config.providers).map(([key, val]) => [
|
|
975
|
+
key,
|
|
976
|
+
{ ...val, apiKey: val.apiKey ? '***' + val.apiKey.slice(-4) : '' }
|
|
977
|
+
])),
|
|
978
|
+
providerInfo
|
|
979
|
+
};
|
|
980
|
+
res.json(safeConfig);
|
|
981
|
+
}
|
|
982
|
+
catch (err) {
|
|
983
|
+
res.status(500).json({ error: err.message });
|
|
984
|
+
}
|
|
985
|
+
});
|
|
986
|
+
// 更新 LLM 配置
|
|
987
|
+
app.post('/api/llm-config', async (req, res) => {
|
|
988
|
+
try {
|
|
989
|
+
const { provider, config } = req.body;
|
|
990
|
+
if (!provider || !config) {
|
|
991
|
+
return res.status(400).json({ error: 'provider and config required' });
|
|
992
|
+
}
|
|
993
|
+
await llmConfigStore.updateProvider(provider, config);
|
|
994
|
+
// 如果是活跃供应商,重新初始化 Pi SDK
|
|
995
|
+
const currentActive = await llmConfigStore.getActiveProvider();
|
|
996
|
+
if (provider === currentActive) {
|
|
997
|
+
const newConfig = await llmConfigStore.getActiveProviderConfig();
|
|
998
|
+
if (newConfig) {
|
|
999
|
+
initMinimax({
|
|
1000
|
+
provider,
|
|
1001
|
+
apiKey: newConfig.apiKey || undefined,
|
|
1002
|
+
baseUrl: newConfig.baseUrl || undefined,
|
|
1003
|
+
model: newConfig.model || undefined
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
res.json({ ok: true });
|
|
1008
|
+
}
|
|
1009
|
+
catch (err) {
|
|
1010
|
+
res.status(500).json({ error: err.message });
|
|
1011
|
+
}
|
|
1012
|
+
});
|
|
1013
|
+
// 设置活跃供应商
|
|
1014
|
+
app.post('/api/llm-provider', async (req, res) => {
|
|
1015
|
+
try {
|
|
1016
|
+
const { provider } = req.body;
|
|
1017
|
+
if (!provider) {
|
|
1018
|
+
return res.status(400).json({ error: 'provider required' });
|
|
1019
|
+
}
|
|
1020
|
+
await llmConfigStore.setActiveProvider(provider);
|
|
1021
|
+
// 重新初始化 Pi SDK
|
|
1022
|
+
const config = await llmConfigStore.getActiveProviderConfig();
|
|
1023
|
+
if (config) {
|
|
1024
|
+
initMinimax({
|
|
1025
|
+
provider: provider,
|
|
1026
|
+
apiKey: config.apiKey || undefined,
|
|
1027
|
+
baseUrl: config.baseUrl || undefined,
|
|
1028
|
+
model: config.model || undefined
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
res.json({ ok: true, provider });
|
|
1032
|
+
}
|
|
1033
|
+
catch (err) {
|
|
1034
|
+
res.status(500).json({ error: err.message });
|
|
1035
|
+
}
|
|
1036
|
+
});
|
|
1037
|
+
// 测试供应商连接
|
|
1038
|
+
app.post('/api/llm-test', async (req, res) => {
|
|
1039
|
+
try {
|
|
1040
|
+
const { provider } = req.body;
|
|
1041
|
+
if (!provider) {
|
|
1042
|
+
return res.status(400).json({ error: 'provider required' });
|
|
1043
|
+
}
|
|
1044
|
+
const result = await llmConfigStore.testProvider(provider);
|
|
1045
|
+
res.json(result);
|
|
1046
|
+
}
|
|
1047
|
+
catch (err) {
|
|
1048
|
+
res.status(500).json({ error: err.message });
|
|
1049
|
+
}
|
|
1050
|
+
});
|
|
1051
|
+
// ==================== P2P Network API ====================
|
|
1052
|
+
// 获取当前身份
|
|
1053
|
+
app.get('/api/identity', async (_req, res) => {
|
|
1054
|
+
console.log('收到 /api/identity 请求');
|
|
1055
|
+
console.log('p2pIdentity.did:', p2pIdentity.did);
|
|
1056
|
+
try {
|
|
1057
|
+
res.json({
|
|
1058
|
+
did: p2pIdentity.did,
|
|
1059
|
+
name: p2pIdentity.name,
|
|
1060
|
+
publicKey: p2pIdentity.publicKey
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
catch (err) {
|
|
1064
|
+
console.error('API identity 错误:', err);
|
|
1065
|
+
res.status(500).json({ error: err.message });
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
// 获取已连接的节点
|
|
1069
|
+
app.get('/api/peers', async (_req, res) => {
|
|
1070
|
+
try {
|
|
1071
|
+
if (!p2pCommunicator) {
|
|
1072
|
+
res.json([]);
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
const connections = p2pCommunicator.getConnections();
|
|
1076
|
+
const peers = connections.map((conn) => ({
|
|
1077
|
+
id: conn.publicKey.substring(0, 16),
|
|
1078
|
+
publicKey: conn.publicKey,
|
|
1079
|
+
peerId: conn.publicKey
|
|
1080
|
+
}));
|
|
1081
|
+
res.json(peers);
|
|
1082
|
+
}
|
|
1083
|
+
catch (err) {
|
|
1084
|
+
res.status(500).json({ error: err.message });
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
1087
|
+
// 获取已发现的所有节点(包括通过 CID 解析的)
|
|
1088
|
+
app.get('/api/discovered-peers', async (_req, res) => {
|
|
1089
|
+
try {
|
|
1090
|
+
// 从全局状态获取已发现的节点
|
|
1091
|
+
const discovered = global.discoveredAgents || [];
|
|
1092
|
+
res.json(discovered);
|
|
1093
|
+
}
|
|
1094
|
+
catch (err) {
|
|
1095
|
+
res.status(500).json({ error: err.message });
|
|
1096
|
+
}
|
|
1097
|
+
});
|
|
1098
|
+
// ==================== iroh P2P API ====================
|
|
1099
|
+
// 初始化 iroh P2P(带持久化)
|
|
1100
|
+
app.post('/api/iroh/init', async (_req, res) => {
|
|
1101
|
+
try {
|
|
1102
|
+
if (irohInitialized && irohNodeInfo) {
|
|
1103
|
+
res.json({ ok: true, ...irohNodeInfo });
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
console.log('[iroh API] 初始化 iroh...');
|
|
1107
|
+
// 启动 iroh(启用持久化)
|
|
1108
|
+
await irohTransport.start(undefined, true);
|
|
1109
|
+
const nodeId = irohTransport.getNodeId() || '';
|
|
1110
|
+
console.log(`[iroh API] iroh 节点 ID: ${nodeId.substring(0, 20)}...`);
|
|
1111
|
+
// 生成 DID
|
|
1112
|
+
const keyPair = KeyManager.generate();
|
|
1113
|
+
const did = keyPair.did;
|
|
1114
|
+
// 构建节点信息文档
|
|
1115
|
+
const nodeDoc = {
|
|
1116
|
+
id: did,
|
|
1117
|
+
name: `bolloon-web-${Date.now()}`,
|
|
1118
|
+
version: '1.0',
|
|
1119
|
+
capabilities: ['chat', 'ai', 'judgment-injection', 'web-interface'],
|
|
1120
|
+
interests: ['ai', 'p2p', 'judgment-system'],
|
|
1121
|
+
irohNodeId: nodeId,
|
|
1122
|
+
channels: [{ id: 'main', name: '主对话' }],
|
|
1123
|
+
createdAt: new Date().toISOString()
|
|
1124
|
+
};
|
|
1125
|
+
// 发布到 IPFS(可选,如果 IPFS 不可用则跳过)
|
|
1126
|
+
let cid = '';
|
|
1127
|
+
try {
|
|
1128
|
+
const formData = new FormData();
|
|
1129
|
+
const blob = new Blob([JSON.stringify(nodeDoc)], { type: 'application/json' });
|
|
1130
|
+
formData.append('file', blob, 'node-info.json');
|
|
1131
|
+
const ipfsRes = await fetch(`${IPFS_ENDPOINT}/api/v0/add`, {
|
|
1132
|
+
method: 'POST',
|
|
1133
|
+
body: formData,
|
|
1134
|
+
signal: AbortSignal.timeout(5000)
|
|
1135
|
+
});
|
|
1136
|
+
const ipfsResult = await ipfsRes.text();
|
|
1137
|
+
const cidMatch = ipfsResult.match(/"Hash":"([^"]+)"/);
|
|
1138
|
+
cid = cidMatch ? cidMatch[1] : '';
|
|
1139
|
+
console.log(`[iroh API] CID 发布成功: ${cid.substring(0, 20)}...`);
|
|
1140
|
+
}
|
|
1141
|
+
catch (ipfsErr) {
|
|
1142
|
+
console.warn('[iroh API] IPFS 不可用,跳过 CID 发布:', ipfsErr.message);
|
|
1143
|
+
// 生成一个假的 CID 用于本地测试(格式:Qm + 44个随机字符)
|
|
1144
|
+
const randomPart = Array.from({ length: 44 }, () => Math.random().toString(36)[2]).join('').substring(0, 44);
|
|
1145
|
+
cid = `Qm${randomPart}`;
|
|
1146
|
+
console.log(`[iroh API] 使用本地 CID: ${cid.substring(0, 20)}...`);
|
|
1147
|
+
}
|
|
1148
|
+
irohNodeInfo = {
|
|
1149
|
+
did,
|
|
1150
|
+
cid,
|
|
1151
|
+
irohNodeId: nodeId,
|
|
1152
|
+
name: nodeDoc.name,
|
|
1153
|
+
initialized: true
|
|
1154
|
+
};
|
|
1155
|
+
irohInitialized = true;
|
|
1156
|
+
// 设置消息处理
|
|
1157
|
+
irohTransport.onMessage('chat', (msg) => {
|
|
1158
|
+
const content = new TextDecoder().decode(msg.payload);
|
|
1159
|
+
console.log(`[iroh] 收到消息 from ${msg.from.substring(0, 12)}...`);
|
|
1160
|
+
// 通过 SSE 广播给所有客户端
|
|
1161
|
+
broadcast({
|
|
1162
|
+
type: 'p2p_message',
|
|
1163
|
+
from: msg.from,
|
|
1164
|
+
content,
|
|
1165
|
+
timestamp: Date.now()
|
|
1166
|
+
}, 'p2p-global');
|
|
1167
|
+
});
|
|
1168
|
+
irohTransport.onMessage('ai-dialogue', (msg) => {
|
|
1169
|
+
const content = new TextDecoder().decode(msg.payload);
|
|
1170
|
+
console.log(`[iroh] 收到 AI 对话 from ${msg.from.substring(0, 12)}...`);
|
|
1171
|
+
broadcast({
|
|
1172
|
+
type: 'p2p_message',
|
|
1173
|
+
content,
|
|
1174
|
+
timestamp: Date.now()
|
|
1175
|
+
}, 'p2p-global');
|
|
1176
|
+
});
|
|
1177
|
+
console.log(`[iroh API] 初始化完成: DID=${did}, CID=${cid}`);
|
|
1178
|
+
res.json({ ok: true, ...irohNodeInfo });
|
|
1179
|
+
}
|
|
1180
|
+
catch (err) {
|
|
1181
|
+
console.error('[iroh API] 初始化失败:', err);
|
|
1182
|
+
res.status(500).json({ error: err.message });
|
|
1183
|
+
}
|
|
1184
|
+
});
|
|
1185
|
+
// 获取 iroh 节点信息
|
|
1186
|
+
app.get('/api/iroh/info', async (_req, res) => {
|
|
1187
|
+
if (!irohInitialized || !irohNodeInfo) {
|
|
1188
|
+
res.json({ initialized: false });
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
res.json({
|
|
1192
|
+
initialized: true,
|
|
1193
|
+
did: irohNodeInfo.did,
|
|
1194
|
+
cid: irohNodeInfo.cid,
|
|
1195
|
+
irohNodeId: irohNodeInfo.irohNodeId,
|
|
1196
|
+
name: irohNodeInfo.name
|
|
1197
|
+
});
|
|
1198
|
+
});
|
|
1199
|
+
// 通过 CID 或 Node ID 连接到其他节点
|
|
1200
|
+
app.post('/api/iroh/connect', async (req, res) => {
|
|
1201
|
+
try {
|
|
1202
|
+
const { cid } = req.body;
|
|
1203
|
+
if (!cid) {
|
|
1204
|
+
return res.status(400).json({ error: 'CID required' });
|
|
1205
|
+
}
|
|
1206
|
+
if (!irohInitialized) {
|
|
1207
|
+
return res.status(500).json({ error: 'iroh not initialized' });
|
|
1208
|
+
}
|
|
1209
|
+
let targetNodeId;
|
|
1210
|
+
let nodeName = 'Unknown';
|
|
1211
|
+
console.log(`[iroh API] 连接到: ${cid}`);
|
|
1212
|
+
// 检查是 Node ID(64字符十六进制)还是 CID
|
|
1213
|
+
const isNodeId = /^[a-f0-9]{64}$/i.test(cid);
|
|
1214
|
+
if (isNodeId) {
|
|
1215
|
+
// 直接使用 Node ID
|
|
1216
|
+
targetNodeId = cid;
|
|
1217
|
+
console.log(`[iroh API] 使用直接 Node ID: ${targetNodeId.substring(0, 20)}...`);
|
|
1218
|
+
}
|
|
1219
|
+
else {
|
|
1220
|
+
// 从 IPFS 获取节点信息
|
|
1221
|
+
try {
|
|
1222
|
+
const ipfsRes = await fetch(`${IPFS_ENDPOINT}/api/v0/cat?arg=${cid}`, {
|
|
1223
|
+
method: 'POST'
|
|
1224
|
+
});
|
|
1225
|
+
const content = await ipfsRes.text();
|
|
1226
|
+
const doc = JSON.parse(content);
|
|
1227
|
+
if (!doc.irohNodeId) {
|
|
1228
|
+
return res.status(400).json({ error: '节点信息中不包含 irohNodeId' });
|
|
1229
|
+
}
|
|
1230
|
+
targetNodeId = doc.irohNodeId;
|
|
1231
|
+
nodeName = doc.name || 'Unknown';
|
|
1232
|
+
console.log(`[iroh API] 从 IPFS 获取节点: ${targetNodeId.substring(0, 20)}...`);
|
|
1233
|
+
}
|
|
1234
|
+
catch {
|
|
1235
|
+
return res.status(400).json({ error: '无法从 CID 获取节点信息,请确认 CID 有效' });
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
// 发送连接消息
|
|
1239
|
+
const message = JSON.stringify({
|
|
1240
|
+
type: 'hello',
|
|
1241
|
+
from: irohNodeInfo?.irohNodeId,
|
|
1242
|
+
name: irohNodeInfo?.name,
|
|
1243
|
+
timestamp: Date.now()
|
|
1244
|
+
});
|
|
1245
|
+
const success = await irohTransport.sendMessage(targetNodeId, 'chat', new TextEncoder().encode(message));
|
|
1246
|
+
if (success) {
|
|
1247
|
+
console.log(`[iroh API] 连接成功!`);
|
|
1248
|
+
res.json({
|
|
1249
|
+
ok: true,
|
|
1250
|
+
targetNodeId,
|
|
1251
|
+
nodeName
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
else {
|
|
1255
|
+
console.log(`[iroh API] 连接失败(对方可能离线)`);
|
|
1256
|
+
res.json({
|
|
1257
|
+
ok: false,
|
|
1258
|
+
error: '连接失败,对方可能离线',
|
|
1259
|
+
targetNodeId
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
catch (err) {
|
|
1264
|
+
console.error('[iroh API] 连接错误:', err);
|
|
1265
|
+
res.status(500).json({ error: err.message });
|
|
1266
|
+
}
|
|
1267
|
+
});
|
|
1268
|
+
// 发送消息给指定节点
|
|
1269
|
+
app.post('/api/iroh/send', async (req, res) => {
|
|
1270
|
+
try {
|
|
1271
|
+
const { targetNodeId, type, content } = req.body;
|
|
1272
|
+
if (!targetNodeId || !content) {
|
|
1273
|
+
return res.status(400).json({ error: 'targetNodeId and content required' });
|
|
1274
|
+
}
|
|
1275
|
+
if (!irohInitialized) {
|
|
1276
|
+
return res.status(500).json({ error: 'iroh not initialized' });
|
|
1277
|
+
}
|
|
1278
|
+
const messageType = type || 'chat';
|
|
1279
|
+
const success = await irohTransport.sendMessage(targetNodeId, messageType, new TextEncoder().encode(content));
|
|
1280
|
+
res.json({ ok: success });
|
|
1281
|
+
}
|
|
1282
|
+
catch (err) {
|
|
1283
|
+
console.error('[iroh API] 发送消息错误:', err);
|
|
1284
|
+
res.status(500).json({ error: err.message });
|
|
1285
|
+
}
|
|
1286
|
+
});
|
|
1287
|
+
// 获取已连接的 iroh 节点列表
|
|
1288
|
+
app.get('/api/iroh/peers', async (_req, res) => {
|
|
1289
|
+
try {
|
|
1290
|
+
if (!irohInitialized) {
|
|
1291
|
+
res.json([]);
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
const peers = irohTransport.getConnectedPeers();
|
|
1295
|
+
res.json(peers.map((nodeId) => ({
|
|
1296
|
+
nodeId,
|
|
1297
|
+
shortId: nodeId.substring(0, 16)
|
|
1298
|
+
})));
|
|
1299
|
+
}
|
|
1300
|
+
catch (err) {
|
|
1301
|
+
res.status(500).json({ error: err.message });
|
|
1302
|
+
}
|
|
1303
|
+
});
|
|
1304
|
+
// 获取离线消息数量
|
|
1305
|
+
app.get('/api/iroh/offline-count', async (_req, res) => {
|
|
1306
|
+
try {
|
|
1307
|
+
if (!irohInitialized) {
|
|
1308
|
+
res.json({ count: 0 });
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
const count = irohTransport.getPendingOfflineCount();
|
|
1312
|
+
res.json({ count });
|
|
1313
|
+
}
|
|
1314
|
+
catch (err) {
|
|
1315
|
+
res.json({ count: 0 });
|
|
1316
|
+
}
|
|
1317
|
+
});
|
|
1318
|
+
// 获取当前频道的身份信息
|
|
1319
|
+
app.get('/api/channel-identity/:channelId', async (req, res) => {
|
|
1320
|
+
try {
|
|
1321
|
+
const { channelId } = req.params;
|
|
1322
|
+
const channels = await loadChannels();
|
|
1323
|
+
const channel = channels.find(c => c.id === channelId);
|
|
1324
|
+
if (!channel) {
|
|
1325
|
+
return res.status(404).json({ error: 'Channel not found' });
|
|
1326
|
+
}
|
|
1327
|
+
res.json({
|
|
1328
|
+
did: channel.did || '',
|
|
1329
|
+
cid: channel.cid || '',
|
|
1330
|
+
publicKey: channel.publicKey || '',
|
|
1331
|
+
name: channel.name
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
catch (err) {
|
|
1335
|
+
res.status(500).json({ error: err.message });
|
|
1336
|
+
}
|
|
1337
|
+
});
|
|
1338
|
+
// 通过 DID/CID 连接远程智能体
|
|
1339
|
+
app.post('/api/connect', async (req, res) => {
|
|
1340
|
+
try {
|
|
1341
|
+
const { did, cid, ipnsName } = req.body;
|
|
1342
|
+
if (!did && !cid && !ipnsName) {
|
|
1343
|
+
return res.status(400).json({ error: 'DID, CID or IPNS name required' });
|
|
1344
|
+
}
|
|
1345
|
+
console.log(`[连接] 尝试连接 DID: ${did}, CID: ${cid}, IPNS: ${ipnsName}`);
|
|
1346
|
+
let doc = null;
|
|
1347
|
+
// 1. 通过 CID 或 IPNS 解析 DiapDoc
|
|
1348
|
+
if (cid || ipnsName) {
|
|
1349
|
+
try {
|
|
1350
|
+
const { IpfsClient } = await import('@diap/sdk');
|
|
1351
|
+
const ipfs = new IpfsClient('http://127.0.0.1:5001', null);
|
|
1352
|
+
let resolvedCid = cid;
|
|
1353
|
+
if (ipnsName) {
|
|
1354
|
+
resolvedCid = await ipfs.resolveIpns(ipnsName);
|
|
1355
|
+
}
|
|
1356
|
+
if (resolvedCid) {
|
|
1357
|
+
const content = await ipfs.get(resolvedCid);
|
|
1358
|
+
doc = JSON.parse(content);
|
|
1359
|
+
console.log(`[连接] 解析 DiapDoc 成功: ${doc.name}`);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
catch (e) {
|
|
1363
|
+
console.warn(`[连接] 解析 IPFS 内容失败:`, e);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
// 2. 如果有 DID,检查是否已连接
|
|
1367
|
+
if (did) {
|
|
1368
|
+
// 广播连接请求
|
|
1369
|
+
if (p2pCommunicator) {
|
|
1370
|
+
const payload = JSON.stringify({
|
|
1371
|
+
type: 'connect_request',
|
|
1372
|
+
requesterDid: did,
|
|
1373
|
+
targetDid: did,
|
|
1374
|
+
timestamp: Date.now()
|
|
1375
|
+
});
|
|
1376
|
+
// 广播到网络
|
|
1377
|
+
console.log(`[连接] 广播连接请求: ${did}`);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
// 3. 将解析的文档添加到已发现列表
|
|
1381
|
+
if (doc) {
|
|
1382
|
+
const discovered = global.discoveredAgents || [];
|
|
1383
|
+
const existing = discovered.findIndex((a) => a.did === doc.id);
|
|
1384
|
+
if (existing >= 0) {
|
|
1385
|
+
discovered[existing] = { ...discovered[existing], ...doc, lastSeen: Date.now() };
|
|
1386
|
+
}
|
|
1387
|
+
else {
|
|
1388
|
+
discovered.push({
|
|
1389
|
+
did: doc.id || doc.did,
|
|
1390
|
+
name: doc.name,
|
|
1391
|
+
capabilities: doc.capabilities || [],
|
|
1392
|
+
interests: doc.interests || [],
|
|
1393
|
+
channels: doc.channels || [],
|
|
1394
|
+
cid: cid,
|
|
1395
|
+
ipnsName: ipnsName,
|
|
1396
|
+
lastSeen: Date.now()
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
global.discoveredAgents = discovered;
|
|
1400
|
+
// 广播发现事件到前端
|
|
1401
|
+
broadcast({ type: 'peer_discovered', peer: doc });
|
|
1402
|
+
}
|
|
1403
|
+
res.json({
|
|
1404
|
+
ok: true,
|
|
1405
|
+
did: doc?.id || did,
|
|
1406
|
+
name: doc?.name,
|
|
1407
|
+
capabilities: doc?.capabilities || [],
|
|
1408
|
+
channels: doc?.channels || [],
|
|
1409
|
+
message: doc ? 'DiapDoc 解析成功' : '连接请求已发送'
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
catch (err) {
|
|
1413
|
+
res.status(500).json({ error: err.message });
|
|
1414
|
+
}
|
|
1415
|
+
});
|
|
1416
|
+
// 发送 P2P 消息
|
|
1417
|
+
app.post('/api/message-p2p', async (req, res) => {
|
|
1418
|
+
try {
|
|
1419
|
+
const { peerId, did, message } = req.body;
|
|
1420
|
+
if (!message) {
|
|
1421
|
+
return res.status(400).json({ error: 'message required' });
|
|
1422
|
+
}
|
|
1423
|
+
let targetPeerId = peerId;
|
|
1424
|
+
// 如果没有 peerId,通过 DID 查找
|
|
1425
|
+
if (!targetPeerId && did) {
|
|
1426
|
+
const discovered = global.discoveredAgents || [];
|
|
1427
|
+
const peer = discovered.find((a) => a.did === did);
|
|
1428
|
+
if (peer) {
|
|
1429
|
+
targetPeerId = peer.peerId;
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
if (!targetPeerId) {
|
|
1433
|
+
// 如果没有 P2P 连接,将消息存储到本地队列
|
|
1434
|
+
const messageQueue = global.messageQueue || [];
|
|
1435
|
+
messageQueue.push({
|
|
1436
|
+
did,
|
|
1437
|
+
message,
|
|
1438
|
+
timestamp: Date.now(),
|
|
1439
|
+
status: 'pending'
|
|
1440
|
+
});
|
|
1441
|
+
global.messageQueue = messageQueue;
|
|
1442
|
+
res.json({ ok: true, queued: true, message: '消息已加入队列,等待对方上线' });
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
// 通过 P2P 发送消息(如果可用)
|
|
1446
|
+
try {
|
|
1447
|
+
const comm = p2pCommunicator;
|
|
1448
|
+
if (comm && typeof comm.send === 'function') {
|
|
1449
|
+
await comm.send(message, targetPeerId);
|
|
1450
|
+
res.json({ ok: true, sent: true });
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
catch { }
|
|
1455
|
+
// 如果 P2P 不可用,消息已在上面加入队列
|
|
1456
|
+
res.json({ ok: true, queued: true, message: '消息已加入队列,等待对方上线' });
|
|
1457
|
+
}
|
|
1458
|
+
catch (err) {
|
|
1459
|
+
res.status(500).json({ error: err.message });
|
|
1460
|
+
}
|
|
1461
|
+
});
|
|
1462
|
+
// 获取待接收的消息队列
|
|
1463
|
+
app.get('/api/peer-messages', async (_req, res) => {
|
|
1464
|
+
try {
|
|
1465
|
+
const messageQueue = global.messageQueue || [];
|
|
1466
|
+
const pendingMessages = messageQueue.filter((m) => m.status === 'pending');
|
|
1467
|
+
res.json(pendingMessages);
|
|
1468
|
+
}
|
|
1469
|
+
catch (err) {
|
|
1470
|
+
res.status(500).json({ error: err.message });
|
|
1471
|
+
}
|
|
1472
|
+
});
|
|
1473
|
+
// 标记消息已读
|
|
1474
|
+
app.post('/api/peer-messages/:messageId/read', async (req, res) => {
|
|
1475
|
+
try {
|
|
1476
|
+
const { messageId } = req.params;
|
|
1477
|
+
const messageQueue = global.messageQueue || [];
|
|
1478
|
+
const msg = messageQueue.find((m) => m.id === messageId);
|
|
1479
|
+
if (msg) {
|
|
1480
|
+
msg.status = 'read';
|
|
1481
|
+
}
|
|
1482
|
+
res.json({ ok: true });
|
|
1483
|
+
}
|
|
1484
|
+
catch (err) {
|
|
1485
|
+
res.status(500).json({ error: err.message });
|
|
1486
|
+
}
|
|
1487
|
+
});
|
|
1488
|
+
// ==================== P2P 连接进度 SSE ====================
|
|
1489
|
+
// 连接进度流(用于实时显示解析进度)
|
|
1490
|
+
const connectProgressClients = new Map();
|
|
1491
|
+
app.get('/api/p2p/connect/progress', async (req, res) => {
|
|
1492
|
+
const sessionId = crypto.randomUUID();
|
|
1493
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
1494
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
1495
|
+
res.setHeader('Connection', 'keep-alive');
|
|
1496
|
+
res.write(`data: ${JSON.stringify({ type: 'start', sessionId })}\n\n`);
|
|
1497
|
+
connectProgressClients.set(sessionId, res);
|
|
1498
|
+
req.on('close', () => {
|
|
1499
|
+
connectProgressClients.delete(sessionId);
|
|
1500
|
+
});
|
|
1501
|
+
});
|
|
1502
|
+
function emitConnectProgress(sessionId, data) {
|
|
1503
|
+
const client = connectProgressClients.get(sessionId);
|
|
1504
|
+
if (client) {
|
|
1505
|
+
client.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
// 取消连接
|
|
1509
|
+
app.post('/api/p2p/connect/cancel', async (req, res) => {
|
|
1510
|
+
const { sessionId } = req.body;
|
|
1511
|
+
if (sessionId && connectProgressClients.has(sessionId)) {
|
|
1512
|
+
connectProgressClients.get(sessionId).end();
|
|
1513
|
+
connectProgressClients.delete(sessionId);
|
|
1514
|
+
}
|
|
1515
|
+
res.json({ ok: true });
|
|
1516
|
+
});
|
|
1517
|
+
// ==================== P2P 连接历史 API ====================
|
|
1518
|
+
const P2P_HISTORY_PATH = path.join(SHARED_SESSION_PATH, 'p2p-history.json');
|
|
1519
|
+
async function loadP2PHistory() {
|
|
1520
|
+
try {
|
|
1521
|
+
const data = await fs.readFile(P2P_HISTORY_PATH, 'utf-8');
|
|
1522
|
+
return JSON.parse(data);
|
|
1523
|
+
}
|
|
1524
|
+
catch {
|
|
1525
|
+
return [];
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
async function saveP2PHistory(history) {
|
|
1529
|
+
await fs.writeFile(P2P_HISTORY_PATH, JSON.stringify(history, null, 2));
|
|
1530
|
+
}
|
|
1531
|
+
// 获取连接历史
|
|
1532
|
+
app.get('/api/p2p/history', async (_req, res) => {
|
|
1533
|
+
try {
|
|
1534
|
+
const history = await loadP2PHistory();
|
|
1535
|
+
res.json(history);
|
|
1536
|
+
}
|
|
1537
|
+
catch (err) {
|
|
1538
|
+
res.status(500).json({ error: err.message });
|
|
1539
|
+
}
|
|
1540
|
+
});
|
|
1541
|
+
// 添加到连接历史
|
|
1542
|
+
app.post('/api/p2p/history', async (req, res) => {
|
|
1543
|
+
try {
|
|
1544
|
+
const history = await loadP2PHistory();
|
|
1545
|
+
const entry = req.body;
|
|
1546
|
+
// 检查是否已存在
|
|
1547
|
+
const existingIndex = history.findIndex((h) => h.did === entry.did);
|
|
1548
|
+
if (existingIndex >= 0) {
|
|
1549
|
+
history[existingIndex] = { ...history[existingIndex], ...entry, lastConnectedAt: Date.now() };
|
|
1550
|
+
}
|
|
1551
|
+
else {
|
|
1552
|
+
history.unshift({ ...entry, id: crypto.randomUUID(), lastConnectedAt: Date.now(), lastMessageAt: 0, totalMessages: 0 });
|
|
1553
|
+
}
|
|
1554
|
+
await saveP2PHistory(history);
|
|
1555
|
+
res.json({ ok: true });
|
|
1556
|
+
}
|
|
1557
|
+
catch (err) {
|
|
1558
|
+
res.status(500).json({ error: err.message });
|
|
1559
|
+
}
|
|
1560
|
+
});
|
|
1561
|
+
// 更新连接历史
|
|
1562
|
+
app.patch('/api/p2p/history/:id', async (req, res) => {
|
|
1563
|
+
try {
|
|
1564
|
+
const history = await loadP2PHistory();
|
|
1565
|
+
const { id } = req.params;
|
|
1566
|
+
const updates = req.body;
|
|
1567
|
+
const index = history.findIndex((h) => h.id === id);
|
|
1568
|
+
if (index >= 0) {
|
|
1569
|
+
history[index] = { ...history[index], ...updates };
|
|
1570
|
+
await saveP2PHistory(history);
|
|
1571
|
+
}
|
|
1572
|
+
res.json({ ok: true });
|
|
1573
|
+
}
|
|
1574
|
+
catch (err) {
|
|
1575
|
+
res.status(500).json({ error: err.message });
|
|
1576
|
+
}
|
|
1577
|
+
});
|
|
1578
|
+
// 删除连接历史
|
|
1579
|
+
app.delete('/api/p2p/history/:id', async (req, res) => {
|
|
1580
|
+
try {
|
|
1581
|
+
const history = await loadP2PHistory();
|
|
1582
|
+
const { id } = req.params;
|
|
1583
|
+
const filtered = history.filter((h) => h.id !== id);
|
|
1584
|
+
await saveP2PHistory(filtered);
|
|
1585
|
+
res.json({ ok: true });
|
|
1586
|
+
}
|
|
1587
|
+
catch (err) {
|
|
1588
|
+
res.status(500).json({ error: err.message });
|
|
1589
|
+
}
|
|
1590
|
+
});
|
|
1591
|
+
// ==================== P2P 偏好设置 API ====================
|
|
1592
|
+
const P2P_PREFS_PATH = path.join(SHARED_SESSION_PATH, 'p2p-preferences.json');
|
|
1593
|
+
async function loadP2PPreferences() {
|
|
1594
|
+
try {
|
|
1595
|
+
const data = await fs.readFile(P2P_PREFS_PATH, 'utf-8');
|
|
1596
|
+
return JSON.parse(data);
|
|
1597
|
+
}
|
|
1598
|
+
catch {
|
|
1599
|
+
return {
|
|
1600
|
+
autoReconnect: true,
|
|
1601
|
+
autoConnectOnStartup: true,
|
|
1602
|
+
preferredNodes: [],
|
|
1603
|
+
maxOfflineQueue: 100,
|
|
1604
|
+
notifications: {
|
|
1605
|
+
newMessage: true,
|
|
1606
|
+
connectionEstablished: true,
|
|
1607
|
+
peerWentOnline: true,
|
|
1608
|
+
peerWentOffline: true
|
|
1609
|
+
}
|
|
1610
|
+
};
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
async function saveP2PPreferences(prefs) {
|
|
1614
|
+
await fs.writeFile(P2P_PREFS_PATH, JSON.stringify(prefs, null, 2));
|
|
1615
|
+
}
|
|
1616
|
+
app.get('/api/p2p/preferences', async (_req, res) => {
|
|
1617
|
+
try {
|
|
1618
|
+
const prefs = await loadP2PPreferences();
|
|
1619
|
+
res.json(prefs);
|
|
1620
|
+
}
|
|
1621
|
+
catch (err) {
|
|
1622
|
+
res.status(500).json({ error: err.message });
|
|
1623
|
+
}
|
|
1624
|
+
});
|
|
1625
|
+
app.patch('/api/p2p/preferences', async (req, res) => {
|
|
1626
|
+
try {
|
|
1627
|
+
const current = await loadP2PPreferences();
|
|
1628
|
+
const updates = req.body;
|
|
1629
|
+
await saveP2PPreferences({ ...current, ...updates });
|
|
1630
|
+
res.json({ ok: true });
|
|
1631
|
+
}
|
|
1632
|
+
catch (err) {
|
|
1633
|
+
res.status(500).json({ error: err.message });
|
|
1634
|
+
}
|
|
1635
|
+
});
|
|
1636
|
+
// 获取持久连接列表
|
|
1637
|
+
app.get('/api/p2p/persistent-connections', async (_req, res) => {
|
|
1638
|
+
try {
|
|
1639
|
+
const sessionProvider = app.locals.sessionProvider;
|
|
1640
|
+
if (!sessionProvider) {
|
|
1641
|
+
return res.json([]);
|
|
1642
|
+
}
|
|
1643
|
+
const channels = sessionProvider.getAllChannels().filter((ch) => ch.peerId);
|
|
1644
|
+
res.json(channels.map((ch) => ({
|
|
1645
|
+
id: ch.id,
|
|
1646
|
+
peerId: ch.peerId || '',
|
|
1647
|
+
peerDid: ch.peerDid || '',
|
|
1648
|
+
peerName: ch.peerName || 'Unknown',
|
|
1649
|
+
cid: ch.cid || '',
|
|
1650
|
+
status: ch.peerId ? 'connected' : 'disconnected',
|
|
1651
|
+
lastConnectedAt: new Date(ch.updatedAt).getTime(),
|
|
1652
|
+
channelId: ch.id,
|
|
1653
|
+
isAutoConnect: false
|
|
1654
|
+
})));
|
|
1655
|
+
}
|
|
1656
|
+
catch (err) {
|
|
1657
|
+
res.status(500).json({ error: err.message });
|
|
1658
|
+
}
|
|
1659
|
+
});
|
|
1660
|
+
// 更新连接状态
|
|
1661
|
+
app.post('/api/p2p/connection-status', async (req, res) => {
|
|
1662
|
+
try {
|
|
1663
|
+
const { id, status, channelId } = req.body;
|
|
1664
|
+
const sessionProvider = app.locals.sessionProvider;
|
|
1665
|
+
if (sessionProvider && channelId) {
|
|
1666
|
+
await sessionProvider.setChannelInfo(channelId, {
|
|
1667
|
+
peerId: status === 'connected' ? (req.body.peerId || 'connected') : undefined
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
res.json({ ok: true });
|
|
1671
|
+
}
|
|
1672
|
+
catch (err) {
|
|
1673
|
+
res.status(500).json({ error: err.message });
|
|
1674
|
+
}
|
|
1675
|
+
});
|
|
1676
|
+
// 创建对话通道
|
|
1677
|
+
app.post('/api/p2p/create-channel', async (req, res) => {
|
|
1678
|
+
try {
|
|
1679
|
+
const { peerDid, peerName, cid, peerId } = req.body;
|
|
1680
|
+
const sessionProvider = app.locals.sessionProvider;
|
|
1681
|
+
if (!sessionProvider) {
|
|
1682
|
+
return res.status(500).json({ error: 'sessionProvider not available' });
|
|
1683
|
+
}
|
|
1684
|
+
const channel = await sessionProvider.getOrCreatePeerChannel(peerDid, peerName);
|
|
1685
|
+
await sessionProvider.setChannelInfo(channel.id, { peerId: peerId || '', cid: cid || '' });
|
|
1686
|
+
res.json({ channelId: channel.id });
|
|
1687
|
+
}
|
|
1688
|
+
catch (err) {
|
|
1689
|
+
res.status(500).json({ error: err.message });
|
|
1690
|
+
}
|
|
1691
|
+
});
|
|
1692
|
+
// CID 解析
|
|
1693
|
+
app.post('/api/p2p/resolve-cid', async (req, res) => {
|
|
1694
|
+
try {
|
|
1695
|
+
const { cid } = req.body;
|
|
1696
|
+
const { DiapDocParser } = await import('../social/channels/diap-doc-parser.js');
|
|
1697
|
+
const parser = new DiapDocParser();
|
|
1698
|
+
const result = await parser.parseFromCID(cid);
|
|
1699
|
+
res.json(result);
|
|
1700
|
+
}
|
|
1701
|
+
catch (err) {
|
|
1702
|
+
res.status(500).json({ error: err.message });
|
|
1703
|
+
}
|
|
1704
|
+
});
|
|
1705
|
+
// P2P 工具调用
|
|
1706
|
+
app.post('/api/p2p/tool-call', async (req, res) => {
|
|
1707
|
+
try {
|
|
1708
|
+
const { tool, targetDid, payload } = req.body;
|
|
1709
|
+
let result;
|
|
1710
|
+
switch (tool) {
|
|
1711
|
+
case 'system_info':
|
|
1712
|
+
const { getLocalSystemInfo } = await import('./components/p2p/p2p-tools.js');
|
|
1713
|
+
result = getLocalSystemInfo();
|
|
1714
|
+
break;
|
|
1715
|
+
case 'file_list':
|
|
1716
|
+
const { getLocalFileList } = await import('./components/p2p/p2p-tools.js');
|
|
1717
|
+
result = getLocalFileList(payload?.path || '/');
|
|
1718
|
+
break;
|
|
1719
|
+
default:
|
|
1720
|
+
return res.status(400).json({ error: `Unknown tool: ${tool}` });
|
|
1721
|
+
}
|
|
1722
|
+
res.json({ success: true, data: result });
|
|
1723
|
+
}
|
|
1724
|
+
catch (err) {
|
|
1725
|
+
res.status(500).json({ error: err.message });
|
|
1726
|
+
}
|
|
1727
|
+
});
|
|
1728
|
+
// ==================== 24h Heartbeat System ====================
|
|
1729
|
+
let healthMonitor = null;
|
|
1730
|
+
let watchdog = null;
|
|
1731
|
+
// 延迟导入避免循环依赖
|
|
1732
|
+
try {
|
|
1733
|
+
const { createHealthMonitor, createWatchdog } = await import('../heartbeat/index.js');
|
|
1734
|
+
healthMonitor = createHealthMonitor();
|
|
1735
|
+
watchdog = createWatchdog();
|
|
1736
|
+
console.log('[24h] Heartbeat modules loaded');
|
|
1737
|
+
}
|
|
1738
|
+
catch (err) {
|
|
1739
|
+
console.warn('[24h] Failed to load heartbeat modules:', err);
|
|
1740
|
+
}
|
|
1741
|
+
// 健康检查端点
|
|
1742
|
+
app.get('/api/health', async (req, res) => {
|
|
1743
|
+
try {
|
|
1744
|
+
if (!healthMonitor) {
|
|
1745
|
+
res.status(503).json({ error: 'Health monitor not initialized' });
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
const status = await healthMonitor.check();
|
|
1749
|
+
// 记录心跳活跃
|
|
1750
|
+
healthMonitor.recordHeartbeat?.();
|
|
1751
|
+
watchdog?.recordActivity?.('health_check');
|
|
1752
|
+
// 根据状态返回不同 HTTP 状态码
|
|
1753
|
+
const httpStatus = status.status === 'healthy' ? 200 :
|
|
1754
|
+
status.status === 'degraded' ? 200 : 503;
|
|
1755
|
+
res.status(httpStatus).json(status);
|
|
1756
|
+
}
|
|
1757
|
+
catch (err) {
|
|
1758
|
+
res.status(500).json({ error: err.message });
|
|
1759
|
+
}
|
|
1760
|
+
});
|
|
1761
|
+
// 看门狗状态
|
|
1762
|
+
app.get('/api/watchdog', async (req, res) => {
|
|
1763
|
+
try {
|
|
1764
|
+
if (!watchdog) {
|
|
1765
|
+
res.status(503).json({ error: 'Watchdog not initialized' });
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
const state = watchdog.getState();
|
|
1769
|
+
res.json(state);
|
|
1770
|
+
}
|
|
1771
|
+
catch (err) {
|
|
1772
|
+
res.status(500).json({ error: err.message });
|
|
1773
|
+
}
|
|
1774
|
+
});
|
|
1775
|
+
// 看门狗重置
|
|
1776
|
+
app.post('/api/watchdog/reset', async (req, res) => {
|
|
1777
|
+
try {
|
|
1778
|
+
if (watchdog) {
|
|
1779
|
+
watchdog.reset();
|
|
1780
|
+
}
|
|
1781
|
+
res.json({ ok: true });
|
|
1782
|
+
}
|
|
1783
|
+
catch (err) {
|
|
1784
|
+
res.status(500).json({ error: err.message });
|
|
1785
|
+
}
|
|
1786
|
+
});
|
|
1787
|
+
// 启动看门狗监控
|
|
1788
|
+
if (watchdog) {
|
|
1789
|
+
watchdog.start();
|
|
1790
|
+
console.log('[24h] Watchdog started');
|
|
1791
|
+
}
|
|
1792
|
+
// 定期健康检查(不阻塞主服务器启动)
|
|
1793
|
+
if (healthMonitor) {
|
|
1794
|
+
healthMonitor.startPeriodicCheck(60000);
|
|
1795
|
+
console.log('[24h] Health monitor periodic check started');
|
|
1796
|
+
}
|
|
1797
|
+
return new Promise((resolve) => {
|
|
1798
|
+
server.listen(port, () => {
|
|
1799
|
+
console.log(`Web 服务器启动完成: http://localhost:${port}`);
|
|
1800
|
+
console.log('服务器已监听');
|
|
1801
|
+
setInterval(() => {
|
|
1802
|
+
for (const client of sseClients) {
|
|
1803
|
+
client.res.write(': ping\n\n');
|
|
1804
|
+
}
|
|
1805
|
+
}, 30000);
|
|
1806
|
+
resolve({ app, server });
|
|
1807
|
+
});
|
|
1808
|
+
});
|
|
1809
|
+
}
|
|
1810
|
+
function broadcast(data, channelId) {
|
|
1811
|
+
const envelope = { ...data, channelId };
|
|
1812
|
+
const message = `data: ${JSON.stringify(envelope)}\n\n`;
|
|
1813
|
+
console.log(`[broadcast] type=${data.type}, channelId=${channelId}, clients=${sseClients.size}`);
|
|
1814
|
+
for (const client of sseClients) {
|
|
1815
|
+
if (!channelId || client.channelId === channelId) {
|
|
1816
|
+
try {
|
|
1817
|
+
client.res.write(message);
|
|
1818
|
+
}
|
|
1819
|
+
catch (e) {
|
|
1820
|
+
console.error(`[broadcast] 写入失败:`, e.message);
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
function getUserName() {
|
|
1826
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
1827
|
+
const match = home.match(/\/Users\/(\w+)/);
|
|
1828
|
+
if (match)
|
|
1829
|
+
return match[1];
|
|
1830
|
+
const user = process.env.USERNAME || process.env.USER || 'user';
|
|
1831
|
+
return user.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
1832
|
+
}
|
|
1833
|
+
export async function bootstrapIdentity() {
|
|
1834
|
+
console.log('🔐 身份生成...');
|
|
1835
|
+
const kp = KeyManager.generate();
|
|
1836
|
+
const did = kp.did;
|
|
1837
|
+
const username = getUserName();
|
|
1838
|
+
const suffix = did.split(':').pop()?.substring(0, 4);
|
|
1839
|
+
const name = `blln-${username}-${suffix}`;
|
|
1840
|
+
console.log(` DID: ${did.substring(0, 30)}...`);
|
|
1841
|
+
return { keypair: kp, did, name };
|
|
1842
|
+
}
|
|
1843
|
+
export function publishDIDBackground(name, kp) {
|
|
1844
|
+
console.log('📝 IPNS注册(后台)...');
|
|
1845
|
+
let retries = 0;
|
|
1846
|
+
const attempt = async () => {
|
|
1847
|
+
try {
|
|
1848
|
+
const auth = await AgentAuthManager.newWithRemoteIpfs('http://127.0.0.1:5001', 'http://127.0.0.1:8080');
|
|
1849
|
+
await auth.registerAgent({ name, services: [] }, kp, '');
|
|
1850
|
+
console.log('✅ IPNS注册成功');
|
|
1851
|
+
}
|
|
1852
|
+
catch (e) {
|
|
1853
|
+
retries++;
|
|
1854
|
+
if (retries < 10) {
|
|
1855
|
+
setTimeout(attempt, 60000);
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
};
|
|
1859
|
+
setTimeout(attempt, 100);
|
|
1860
|
+
}
|
|
1861
|
+
export async function bootstrapP2P(verifier) {
|
|
1862
|
+
console.log('🌐 P2P连接...');
|
|
1863
|
+
const rawSeed = crypto.getRandomValues(new Uint8Array(32));
|
|
1864
|
+
const comm = createHyperswarmCommunicator({ server: true, client: true, autoConnect: true, maxConnections: 50, seed: rawSeed });
|
|
1865
|
+
await comm.start();
|
|
1866
|
+
const topic = createTopic('bolloon-agent-harness');
|
|
1867
|
+
await comm.joinTopic(topic);
|
|
1868
|
+
console.log(' P2P已就绪');
|
|
1869
|
+
return comm;
|
|
1870
|
+
}
|
|
1871
|
+
export async function openBrowser(url) {
|
|
1872
|
+
const { exec } = await import('child_process');
|
|
1873
|
+
const { platform } = await import('os');
|
|
1874
|
+
const p = platform();
|
|
1875
|
+
let cmd;
|
|
1876
|
+
if (p === 'darwin') {
|
|
1877
|
+
cmd = `open ${url}`;
|
|
1878
|
+
}
|
|
1879
|
+
else if (p === 'win32') {
|
|
1880
|
+
cmd = `start ${url}`;
|
|
1881
|
+
}
|
|
1882
|
+
else {
|
|
1883
|
+
cmd = `xdg-open ${url}`;
|
|
1884
|
+
}
|
|
1885
|
+
exec(cmd, (err) => {
|
|
1886
|
+
if (err) {
|
|
1887
|
+
console.error('打开浏览器失败:', err.message);
|
|
1888
|
+
}
|
|
1889
|
+
});
|
|
1890
|
+
}
|