@bolloon/bolloon-agent 0.1.13 → 0.1.14
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/pi-sdk.js +185 -0
- package/dist/agents/shell-guard.js +354 -0
- package/dist/agents/shell-tool.js +83 -0
- package/dist/agents/skill-loader.js +174 -0
- package/dist/bollharness-integration/context-chain-router.js +3 -3
- package/dist/bollharness-integration/context-router.js +1 -1
- package/dist/heartbeat/Watchdog.js +7 -5
- package/dist/heartbeat/index.js +1 -0
- package/dist/heartbeat/self-improve-bus.js +85 -0
- package/dist/pi-ecosystem-judgment/index.js +1 -2
- package/dist/utils/auto-update.js +44 -12
- package/dist/web/client.js +839 -103
- 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/index.html +88 -8
- package/dist/web/server.js +2360 -0
- package/dist/web/style.css +506 -9
- package/package.json +2 -2
- package/scripts/build-cli.js +11 -1
- package/src/agents/pi-sdk.ts +196 -0
- package/src/agents/shell-guard.ts +417 -0
- package/src/agents/shell-tool.ts +103 -0
- package/src/agents/skill-loader.ts +202 -0
- package/src/bollharness-integration/context-chain-router.ts +3 -3
- package/src/bollharness-integration/context-router.ts +1 -1
- package/src/heartbeat/Watchdog.ts +7 -5
- package/src/heartbeat/index.ts +1 -0
- package/src/heartbeat/self-improve-bus.ts +110 -0
- package/src/types.d.ts +12 -0
- package/src/utils/auto-update.ts +45 -14
- package/src/web/client.js +839 -103
- package/src/web/index.html +88 -8
- package/src/web/server.ts +427 -101
- package/src/web/style.css +506 -9
- package/dist/bollharness-integration/bollharness-integration/context-router-judgment.d.ts +0 -48
- package/dist/bollharness-integration/bollharness-integration/context-router-judgment.js +0 -261
- package/dist/bollharness-integration/bollharness-integration/context-router.d.ts +0 -110
- package/dist/bollharness-integration/bollharness-integration/context-router.js +0 -542
- package/dist/bollharness-integration/bollharness-integration/gate-state-machine.d.ts +0 -87
- package/dist/bollharness-integration/bollharness-integration/gate-state-machine.js +0 -231
- package/dist/bollharness-integration/bollharness-integration/gate-transition-hooks.d.ts +0 -30
- package/dist/bollharness-integration/bollharness-integration/gate-transition-hooks.js +0 -91
- package/dist/bollharness-integration/bollharness-integration/guard-checker.d.ts +0 -105
- package/dist/bollharness-integration/bollharness-integration/guard-checker.js +0 -353
- package/dist/bollharness-integration/bollharness-integration/index.d.ts +0 -66
- package/dist/bollharness-integration/bollharness-integration/index.js +0 -32
- package/dist/bollharness-integration/bollharness-integration/integration.d.ts +0 -219
- package/dist/bollharness-integration/bollharness-integration/integration.js +0 -420
- package/dist/bollharness-integration/bollharness-integration/skill-adapter.d.ts +0 -151
- package/dist/bollharness-integration/bollharness-integration/skill-adapter.js +0 -518
|
@@ -0,0 +1,2360 @@
|
|
|
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
|
+
/** 粗校验链上地址格式 — 不做 EIP-55 校验, 避免阻塞 UI; 失败返回空字符串 */
|
|
28
|
+
function isValidWalletAddress(addr) {
|
|
29
|
+
if (typeof addr !== 'string')
|
|
30
|
+
return '';
|
|
31
|
+
const a = addr.trim();
|
|
32
|
+
if (!a)
|
|
33
|
+
return '';
|
|
34
|
+
// EVM: 0x + 40 hex chars
|
|
35
|
+
if (/^0x[0-9a-fA-F]{40}$/.test(a))
|
|
36
|
+
return a;
|
|
37
|
+
// Sui / Aptos: 0x + 64 hex chars
|
|
38
|
+
if (/^0x[0-9a-fA-F]{64}$/.test(a))
|
|
39
|
+
return a;
|
|
40
|
+
// Solana: base58, 32-44 chars
|
|
41
|
+
if (/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(a) && !a.startsWith('0x'))
|
|
42
|
+
return a;
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
45
|
+
async function loadChannels() {
|
|
46
|
+
try {
|
|
47
|
+
const data = await fs.readFile(CHANNELS_PATH, 'utf-8');
|
|
48
|
+
return JSON.parse(data);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function saveChannels(channels) {
|
|
55
|
+
// 写盘前剥掉任何遗留的 didDocument 字段, 防止历史脏数据撑大文件
|
|
56
|
+
const sanitized = channels.map(ch => {
|
|
57
|
+
const { didDocument: _omit, ...rest } = ch;
|
|
58
|
+
return rest;
|
|
59
|
+
});
|
|
60
|
+
const jsonStr = JSON.stringify(sanitized, null, 2);
|
|
61
|
+
console.log('[saveChannels] 保存频道数据, 数量:', sanitized.length);
|
|
62
|
+
console.log('[saveChannels] JSON 长度:', jsonStr.length);
|
|
63
|
+
await fs.writeFile(CHANNELS_PATH, jsonStr);
|
|
64
|
+
// 写盘即令缓存失效: 用 lastChannelsWriteAt 标记, getChannelsWithDID 会检查
|
|
65
|
+
lastChannelsWriteAt = Date.now();
|
|
66
|
+
}
|
|
67
|
+
// 模块级: 最近一次 channels.json 写盘时间. saveChannels 在模块顶层,
|
|
68
|
+
// getChannelsWithDID 在 createWebServer 内部, 跨作用域用模块变量桥接.
|
|
69
|
+
let lastChannelsWriteAt = 0;
|
|
70
|
+
async function loadSession(channelId, sessionId) {
|
|
71
|
+
// sessionId is optional for backward compatibility; if provided, load specific session
|
|
72
|
+
const key = sessionId ? `${channelId}:${sessionId}` : channelId;
|
|
73
|
+
const sessionPath = path.join(SESSION_CACHE_PATH, `${key}.json`);
|
|
74
|
+
try {
|
|
75
|
+
// 内存保护: 拒绝加载过大的 session 文件 (> 50MB 视为异常, 避免 OOM)
|
|
76
|
+
const stat = await fs.stat(sessionPath);
|
|
77
|
+
if (stat.size > 50 * 1024 * 1024) {
|
|
78
|
+
console.warn(`[loadSession] session 过大 (${stat.size} bytes): ${key}`);
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const data = await fs.readFile(sessionPath, 'utf-8');
|
|
82
|
+
return JSON.parse(data);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function saveSession(session) {
|
|
89
|
+
const key = session.sessionId ? `${session.channelId}:${session.sessionId}` : session.channelId;
|
|
90
|
+
const sessionPath = path.join(SESSION_CACHE_PATH, `${key}.json`);
|
|
91
|
+
await fs.writeFile(sessionPath, JSON.stringify(session, null, 2));
|
|
92
|
+
}
|
|
93
|
+
async function loadTheme() {
|
|
94
|
+
try {
|
|
95
|
+
const data = await fs.readFile(THEME_PATH, 'utf-8');
|
|
96
|
+
return JSON.parse(data);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return { theme: 'light', agentId: '' };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async function saveTheme(theme, agentId) {
|
|
103
|
+
await fs.writeFile(THEME_PATH, JSON.stringify({ theme, agentId }, null, 2));
|
|
104
|
+
}
|
|
105
|
+
// ==================== Task Queue & Workflow System ====================
|
|
106
|
+
const TASK_QUEUE_PATH = path.join(SHARED_SESSION_PATH, 'task-queue.json');
|
|
107
|
+
const WORKFLOW_STATE_PATH = path.join(SHARED_SESSION_PATH, 'workflow-state.json');
|
|
108
|
+
async function loadTaskQueue() {
|
|
109
|
+
try {
|
|
110
|
+
const data = await fs.readFile(TASK_QUEUE_PATH, 'utf-8');
|
|
111
|
+
return JSON.parse(data);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async function saveTaskQueue(tasks) {
|
|
118
|
+
await fs.writeFile(TASK_QUEUE_PATH, JSON.stringify(tasks, null, 2));
|
|
119
|
+
}
|
|
120
|
+
async function loadWorkflowState(channelId) {
|
|
121
|
+
try {
|
|
122
|
+
const data = await fs.readFile(WORKFLOW_STATE_PATH, 'utf-8');
|
|
123
|
+
const states = JSON.parse(data);
|
|
124
|
+
return states.find(s => s.channelId === channelId) || null;
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async function saveWorkflowState(state) {
|
|
131
|
+
try {
|
|
132
|
+
const data = await fs.readFile(WORKFLOW_STATE_PATH, 'utf-8');
|
|
133
|
+
const states = JSON.parse(data);
|
|
134
|
+
const index = states.findIndex(s => s.channelId === state.channelId);
|
|
135
|
+
if (index >= 0) {
|
|
136
|
+
states[index] = state;
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
states.push(state);
|
|
140
|
+
}
|
|
141
|
+
await fs.writeFile(WORKFLOW_STATE_PATH, JSON.stringify(states, null, 2));
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
await fs.writeFile(WORKFLOW_STATE_PATH, JSON.stringify([state], null, 2));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
let isExecutingTask = false;
|
|
148
|
+
let executionTaskId = null;
|
|
149
|
+
async function executeTask(task, channelId) {
|
|
150
|
+
if (isExecutingTask)
|
|
151
|
+
return;
|
|
152
|
+
isExecutingTask = true;
|
|
153
|
+
executionTaskId = task.id;
|
|
154
|
+
const agent = await getAgentForChannel(channelId);
|
|
155
|
+
const tasks = await loadTaskQueue();
|
|
156
|
+
const taskIndex = tasks.findIndex(t => t.id === task.id);
|
|
157
|
+
if (taskIndex >= 0) {
|
|
158
|
+
tasks[taskIndex].status = 'running';
|
|
159
|
+
tasks[taskIndex].updatedAt = new Date().toISOString();
|
|
160
|
+
await saveTaskQueue(tasks);
|
|
161
|
+
}
|
|
162
|
+
broadcast({ type: 'task_status', taskId: task.id, status: 'running', progress: 0 }, channelId);
|
|
163
|
+
try {
|
|
164
|
+
let result = '';
|
|
165
|
+
switch (task.type) {
|
|
166
|
+
case 'chat':
|
|
167
|
+
if (task.description) {
|
|
168
|
+
broadcast({ type: 'status', content: `执行任务: ${task.title}` }, channelId);
|
|
169
|
+
result = await agent.prompt(task.description);
|
|
170
|
+
}
|
|
171
|
+
break;
|
|
172
|
+
case 'read':
|
|
173
|
+
if (task.description) {
|
|
174
|
+
broadcast({ type: 'status', content: `读取文档: ${task.description}` }, channelId);
|
|
175
|
+
const content = await documentReader.read(task.description);
|
|
176
|
+
result = `📄 文档读取完成\n\n${content.text.substring(0, 500)}${content.text.length > 500 ? '...' : ''}`;
|
|
177
|
+
}
|
|
178
|
+
break;
|
|
179
|
+
case 'summarize':
|
|
180
|
+
if (task.description) {
|
|
181
|
+
broadcast({ type: 'status', content: `总结文档: ${task.description}` }, channelId);
|
|
182
|
+
const content = await documentReader.read(task.description);
|
|
183
|
+
const llm = getMinimax();
|
|
184
|
+
const summary = await llm.summarize(content.text);
|
|
185
|
+
result = `📝 文档总结:\n\n${summary.summary}`;
|
|
186
|
+
}
|
|
187
|
+
break;
|
|
188
|
+
case 'workflow':
|
|
189
|
+
// 执行多步骤工作流
|
|
190
|
+
if (task.steps && task.steps.length > 0) {
|
|
191
|
+
let loopCount = 0;
|
|
192
|
+
for (let i = 0; i < task.steps.length; i++) {
|
|
193
|
+
// 广播循环开始
|
|
194
|
+
loopCount++;
|
|
195
|
+
broadcast({ type: 'workflow_loop', loopCount, content: `开始步骤 ${i + 1}/${task.steps.length}: ${task.steps[i].name}` }, channelId);
|
|
196
|
+
task.steps[i].status = 'running';
|
|
197
|
+
broadcast({ type: 'task_status', taskId: task.id, status: 'running', currentStep: i, totalSteps: task.steps.length }, channelId);
|
|
198
|
+
broadcast({ type: 'workflow_step', step: `步骤 ${i + 1}`, content: `执行中: ${task.steps[i].name}` }, channelId);
|
|
199
|
+
// 执行步骤 - 模拟流式输出
|
|
200
|
+
for (let j = 0; j < 3; j++) {
|
|
201
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
202
|
+
broadcast({ type: 'workflow_step', step: `步骤 ${i + 1}`, content: `执行中... (${(j + 1) * 33}%)` }, channelId);
|
|
203
|
+
}
|
|
204
|
+
task.steps[i].status = 'completed';
|
|
205
|
+
task.progress = Math.round(((i + 1) / task.steps.length) * 100);
|
|
206
|
+
broadcast({ type: 'workflow_step', step: `步骤 ${i + 1}`, content: `✅ 完成: ${task.steps[i].name}` }, channelId);
|
|
207
|
+
broadcast({ type: 'workflow_loop', loopCount, status: 'completed', content: `步骤 ${i + 1} 完成` }, channelId);
|
|
208
|
+
broadcast({ type: 'task_status', taskId: task.id, progress: task.progress }, channelId);
|
|
209
|
+
}
|
|
210
|
+
result = '✅ 工作流执行完成';
|
|
211
|
+
broadcast({ type: 'workflow_loop', loopCount, status: 'finished', content: result }, channelId);
|
|
212
|
+
}
|
|
213
|
+
break;
|
|
214
|
+
default:
|
|
215
|
+
result = '未知任务类型';
|
|
216
|
+
}
|
|
217
|
+
// 更新任务状态
|
|
218
|
+
const tasks = await loadTaskQueue();
|
|
219
|
+
const idx = tasks.findIndex(t => t.id === task.id);
|
|
220
|
+
if (idx >= 0) {
|
|
221
|
+
tasks[idx].status = 'completed';
|
|
222
|
+
tasks[idx].progress = 100;
|
|
223
|
+
tasks[idx].result = result;
|
|
224
|
+
tasks[idx].updatedAt = new Date().toISOString();
|
|
225
|
+
await saveTaskQueue(tasks);
|
|
226
|
+
}
|
|
227
|
+
broadcast({ type: 'task_status', taskId: task.id, status: 'completed', progress: 100, result }, channelId);
|
|
228
|
+
broadcast({ type: 'ai', content: result }, channelId);
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
const tasks = await loadTaskQueue();
|
|
232
|
+
const idx = tasks.findIndex(t => t.id === task.id);
|
|
233
|
+
if (idx >= 0) {
|
|
234
|
+
tasks[idx].status = 'failed';
|
|
235
|
+
tasks[idx].error = error.message;
|
|
236
|
+
tasks[idx].updatedAt = new Date().toISOString();
|
|
237
|
+
await saveTaskQueue(tasks);
|
|
238
|
+
}
|
|
239
|
+
broadcast({ type: 'task_status', taskId: task.id, status: 'failed', error: error.message }, channelId);
|
|
240
|
+
broadcast({ type: 'error', content: `任务执行失败: ${error.message}` }, channelId);
|
|
241
|
+
}
|
|
242
|
+
isExecutingTask = false;
|
|
243
|
+
executionTaskId = null;
|
|
244
|
+
}
|
|
245
|
+
let sseClients = new Set();
|
|
246
|
+
let channelSessions = new Map(); // key: channelId
|
|
247
|
+
let sessionMessages = new Map(); // key: channelId + sessionId
|
|
248
|
+
async function getAgentForChannel(channelId, channelDid, channelName, channelDidDoc) {
|
|
249
|
+
// 获取当前 channel 的 currentSessionId
|
|
250
|
+
const channels = await loadChannels();
|
|
251
|
+
const channel = channels.find(c => c.id === channelId);
|
|
252
|
+
const currentSessionId = channel?.currentSessionId || 'default';
|
|
253
|
+
const sessionKey = `${channelId}:${currentSessionId}`;
|
|
254
|
+
console.log(`[Agent] 获取频道 ${channelId} 的 session, sessionKey = ${sessionKey}`);
|
|
255
|
+
const existingSession = channelSessions.get(sessionKey);
|
|
256
|
+
// 如果已有 session,检查是否需要更新 identity
|
|
257
|
+
if (existingSession) {
|
|
258
|
+
console.log(`[Agent] 找到现有 session: ${sessionKey}`);
|
|
259
|
+
const currentIdentity = existingSession.getIdentity();
|
|
260
|
+
// 如果当前 identity 没有真实 DID,或者 DID 与频道的 DID 不匹配,需要重建
|
|
261
|
+
let needsUpdate = !currentIdentity.did.startsWith('did:pi:') ||
|
|
262
|
+
(channelDid && !currentIdentity.did.includes(channelId));
|
|
263
|
+
if (!needsUpdate && channelDid && currentIdentity.did !== channelDid) {
|
|
264
|
+
needsUpdate = true;
|
|
265
|
+
}
|
|
266
|
+
if (needsUpdate && channelDid) {
|
|
267
|
+
// 更新现有 session 的 identity
|
|
268
|
+
existingSession.updateIdentity({
|
|
269
|
+
did: channelDid,
|
|
270
|
+
name: channelName || `Channel-${channelId.slice(-6)}`,
|
|
271
|
+
publicKey: '',
|
|
272
|
+
createdAt: Date.now(),
|
|
273
|
+
cid: channelDidDoc?.cid,
|
|
274
|
+
ipnsName: channelDidDoc?.ipnsName
|
|
275
|
+
});
|
|
276
|
+
console.log(`[Agent] 频道 ${channelId} 身份更新: DID = ${channelDid}`);
|
|
277
|
+
}
|
|
278
|
+
return existingSession;
|
|
279
|
+
}
|
|
280
|
+
// 构建频道的身份文档 (从 didDocRef 拿 cid/ipnsName, 不读整份 didDocument)
|
|
281
|
+
const identityDoc = channelDid ? {
|
|
282
|
+
did: channelDid,
|
|
283
|
+
name: channelName || `Channel-${channelId.slice(-6)}`,
|
|
284
|
+
publicKey: '',
|
|
285
|
+
createdAt: Date.now(),
|
|
286
|
+
cid: channelDidDoc?.cid,
|
|
287
|
+
ipnsName: channelDidDoc?.ipnsName
|
|
288
|
+
} : undefined;
|
|
289
|
+
console.log(`[Agent] 创建新 session: ${sessionKey}`);
|
|
290
|
+
const session = await createAgentSession({
|
|
291
|
+
cwd: process.cwd(),
|
|
292
|
+
peerId: `channel-${channelId}:${currentSessionId}`,
|
|
293
|
+
identityDoc
|
|
294
|
+
}, true); // forceNew: true 强制创建新实例
|
|
295
|
+
channelSessions.set(sessionKey, session);
|
|
296
|
+
if (channelDid) {
|
|
297
|
+
console.log(`[Agent] 新建频道 ${channelId} session, DID = ${channelDid}, sessionId = ${currentSessionId}`);
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
console.log(`[Agent] 新建频道 ${channelId} session, 使用默认身份, sessionId = ${currentSessionId}`);
|
|
301
|
+
}
|
|
302
|
+
return session;
|
|
303
|
+
}
|
|
304
|
+
export async function createWebServer(port = 3000) {
|
|
305
|
+
// 防止 P2P DHT 超时等错误导致进程崩溃
|
|
306
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
307
|
+
console.error('[警告] 未处理的 Promise 拒绝:', reason);
|
|
308
|
+
});
|
|
309
|
+
// 重置旧的 agent session,确保使用新的 LLM 配置
|
|
310
|
+
const { resetAgentSession } = await import('../agents/pi-sdk.js');
|
|
311
|
+
resetAgentSession();
|
|
312
|
+
// 初始化 LLM(从配置文件读取 MiniMax 配置)
|
|
313
|
+
initMinimax();
|
|
314
|
+
// ==================== P2P DIAP 身份初始化 ====================
|
|
315
|
+
let p2pIdentity = {
|
|
316
|
+
did: '',
|
|
317
|
+
name: '',
|
|
318
|
+
publicKey: '',
|
|
319
|
+
keypair: null
|
|
320
|
+
};
|
|
321
|
+
let p2pCommunicator = null;
|
|
322
|
+
try {
|
|
323
|
+
console.log('开始生成 P2P 身份...');
|
|
324
|
+
// 生成 DIAP 身份
|
|
325
|
+
const kp = KeyManager.generate();
|
|
326
|
+
console.log('KeyManager.generate() 完成, kp:', !!kp, 'kp.did:', kp?.did);
|
|
327
|
+
console.log('kp.publicKey:', kp?.publicKey);
|
|
328
|
+
const did = kp.did || 'did:unknown:123456';
|
|
329
|
+
console.log(`DID: ${did}`);
|
|
330
|
+
const username = 'web-user';
|
|
331
|
+
const suffix = did?.split(':').pop()?.substring(0, 4) || 'xxxx';
|
|
332
|
+
const name = `blln-${username}-${suffix}`;
|
|
333
|
+
p2pIdentity = {
|
|
334
|
+
did: did || '',
|
|
335
|
+
name,
|
|
336
|
+
publicKey: Buffer.from(kp.publicKey).toString('hex'),
|
|
337
|
+
keypair: kp
|
|
338
|
+
};
|
|
339
|
+
console.log(`P2P 身份已生成: ${p2pIdentity.did}`);
|
|
340
|
+
// 尝试发布 DID 到 IPFS
|
|
341
|
+
try {
|
|
342
|
+
const auth = await AgentAuthManager.newWithRemoteIpfs('http://127.0.0.1:5001', 'http://127.0.0.1:8080');
|
|
343
|
+
await auth.registerAgent({ name, services: [] }, kp, '');
|
|
344
|
+
console.log('P2P DID 已发布到 IPFS');
|
|
345
|
+
}
|
|
346
|
+
catch (e) {
|
|
347
|
+
console.log('P2P DID 本地模式运行');
|
|
348
|
+
}
|
|
349
|
+
// 初始化 P2P 通信器
|
|
350
|
+
try {
|
|
351
|
+
const rawSeed = crypto.getRandomValues(new Uint8Array(32));
|
|
352
|
+
p2pCommunicator = createHyperswarmCommunicator({
|
|
353
|
+
server: true,
|
|
354
|
+
client: true,
|
|
355
|
+
autoConnect: true,
|
|
356
|
+
maxConnections: 50,
|
|
357
|
+
seed: rawSeed
|
|
358
|
+
});
|
|
359
|
+
p2pCommunicator.on('connection', (conn) => {
|
|
360
|
+
console.log(`P2P 连接: ${conn.publicKey.substring(0, 8)}...`);
|
|
361
|
+
});
|
|
362
|
+
p2pCommunicator.on('message', async (msg, conn) => {
|
|
363
|
+
const content = new TextDecoder().decode(msg.content);
|
|
364
|
+
console.log(`P2P 收到消息: ${content.substring(0, 50)}...`);
|
|
365
|
+
// 可以在这里处理接收到的消息
|
|
366
|
+
broadcast({ type: 'p2p_message', from: conn.publicKey.substring(0, 8), content }, undefined);
|
|
367
|
+
});
|
|
368
|
+
await p2pCommunicator.start();
|
|
369
|
+
const topic = createTopic('bolloon-agent-harness');
|
|
370
|
+
await p2pCommunicator.joinTopic(topic);
|
|
371
|
+
console.log(`P2P 网络已就绪`);
|
|
372
|
+
}
|
|
373
|
+
catch (e) {
|
|
374
|
+
console.log(`P2P 网络初始化失败: ${e.message}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
catch (e) {
|
|
378
|
+
console.log(`P2P 身份初始化失败: ${e.message}`);
|
|
379
|
+
}
|
|
380
|
+
const app = express();
|
|
381
|
+
const server = createServer(app);
|
|
382
|
+
await ensureSessionDirs();
|
|
383
|
+
app.use(express.json());
|
|
384
|
+
app.use((req, res, next) => {
|
|
385
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
386
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
387
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
388
|
+
if (req.method === 'OPTIONS') {
|
|
389
|
+
return res.status(200).end();
|
|
390
|
+
}
|
|
391
|
+
next();
|
|
392
|
+
});
|
|
393
|
+
app.use(express.static(webRoot));
|
|
394
|
+
app.get('/', (req, res) => {
|
|
395
|
+
res.sendFile(join(webRoot, 'index.html'));
|
|
396
|
+
});
|
|
397
|
+
app.get('/api-config', (req, res) => {
|
|
398
|
+
res.sendFile(join(webRoot, 'api-config.html'));
|
|
399
|
+
});
|
|
400
|
+
app.get('/events', (req, res) => {
|
|
401
|
+
const channelId = req.query.channelId;
|
|
402
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
403
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
404
|
+
res.setHeader('Connection', 'keep-alive');
|
|
405
|
+
// 反向代理 (nginx/cloudflair) 需要: 禁用缓冲 + 立即 flush
|
|
406
|
+
res.setHeader('X-Accel-Buffering', 'no');
|
|
407
|
+
res.flushHeaders();
|
|
408
|
+
const clientInfo = { res, channelId };
|
|
409
|
+
sseClients.add(clientInfo);
|
|
410
|
+
console.log(`[SSE] 客户端连接 channelId=${channelId || '(broadcast)'}, 总数=${sseClients.size}`);
|
|
411
|
+
req.on('close', () => {
|
|
412
|
+
sseClients.delete(clientInfo);
|
|
413
|
+
try {
|
|
414
|
+
res.end();
|
|
415
|
+
}
|
|
416
|
+
catch { }
|
|
417
|
+
console.log(`[SSE] 客户端断开 channelId=${channelId || '(broadcast)'}, 剩余=${sseClients.size}`);
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
app.post('/message', async (req, res) => {
|
|
421
|
+
const { text, channelId, channelDid } = req.body;
|
|
422
|
+
if (!text) {
|
|
423
|
+
return res.status(400).json({ error: 'No text provided' });
|
|
424
|
+
}
|
|
425
|
+
if (!channelId) {
|
|
426
|
+
return res.status(400).json({ error: 'No channelId provided' });
|
|
427
|
+
}
|
|
428
|
+
// 获取频道信息(只取轻量引用, 不再读完整 DID 文档)
|
|
429
|
+
const channels = await loadChannels();
|
|
430
|
+
const channel = channels.find(c => c.id === channelId);
|
|
431
|
+
const currentSessionId = channel?.currentSessionId || 'default';
|
|
432
|
+
const realChannelDid = channelDid || channel?.did || '';
|
|
433
|
+
const realChannelName = channel?.name || '';
|
|
434
|
+
const realChannelDidDoc = channel?.didDocRef;
|
|
435
|
+
broadcast({ type: 'user', content: text }, channelId);
|
|
436
|
+
// 提前捕获 wallet/autoTools 到本地变量, 避免下面 try 块内的 inner const channel
|
|
437
|
+
// (line ~638) 与这里外层的 const channel 形成 shadowing 让 TS 误报"使用前未声明"
|
|
438
|
+
const boundWalletAddress = channel?.walletAddress;
|
|
439
|
+
const autoToolsEnabled = channel?.autoInvokeTools !== false; // 默认开启
|
|
440
|
+
try {
|
|
441
|
+
const agent = await getAgentForChannel(channelId, realChannelDid, realChannelName, realChannelDidDoc);
|
|
442
|
+
let fullResponse = '';
|
|
443
|
+
const streamCallback = (event) => {
|
|
444
|
+
// 同时发送给流式显示和工作流显示
|
|
445
|
+
if (event.type === 'token' || event.type === 'thinking') {
|
|
446
|
+
broadcast({ type: 'stream', streamType: event.type, content: event.content }, channelId);
|
|
447
|
+
// 同时作为 workflow_step 显示(用于动态 loop 循环)
|
|
448
|
+
if (event.content) {
|
|
449
|
+
broadcast({ type: 'workflow_step', step: 'AI 思考', content: event.content.substring(0, 100) }, channelId);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
else if (event.type === 'status' || event.type === 'tool') {
|
|
453
|
+
broadcast({ type: 'status', tool: event.tool, content: event.content }, channelId);
|
|
454
|
+
broadcast({ type: 'workflow_step', step: event.tool || '系统', content: event.content }, channelId);
|
|
455
|
+
console.log(`[SSE 广播] workflow_step: step=${event.tool}, content="${event.content?.substring(0, 80)}..."`);
|
|
456
|
+
}
|
|
457
|
+
else if (event.type === 'error') {
|
|
458
|
+
broadcast({ type: 'error', content: event.content }, channelId);
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
console.log(`[消息处理] 开始处理用户消息, channelId: ${channelId}, sessionId: ${currentSessionId}`);
|
|
462
|
+
// 将真实 DID 作为上下文前缀,让 AI 使用真实的 DID 而不是自己编造的
|
|
463
|
+
let contextHint = '';
|
|
464
|
+
if (realChannelDid)
|
|
465
|
+
contextHint += `[系统上下文] 当前频道名称: ${realChannelName}, 你的真实 DID: ${realChannelDid}\n`;
|
|
466
|
+
if (boundWalletAddress) {
|
|
467
|
+
contextHint += `[系统上下文] 已绑定的加密钱包地址: ${boundWalletAddress}。当用户授权或启用自动工具调用时, 可使用该地址发起链上操作。\n`;
|
|
468
|
+
}
|
|
469
|
+
if (autoToolsEnabled) {
|
|
470
|
+
contextHint += `[系统上下文] 自动工具调用已开启: 你可以使用受信任的本地工具 (shell / 文件 / skill) 而无需逐次询问用户。\n`;
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
contextHint += `[系统上下文] 自动工具调用已关闭: 每次执行工具前必须先与用户确认。\n`;
|
|
474
|
+
}
|
|
475
|
+
if (contextHint)
|
|
476
|
+
contextHint += '\n';
|
|
477
|
+
fullResponse = await agent.promptStream(contextHint + text, streamCallback);
|
|
478
|
+
broadcast({ type: 'ai', content: fullResponse }, channelId);
|
|
479
|
+
const existingSession = await loadSession(channelId, currentSessionId);
|
|
480
|
+
const session = existingSession || { channelId, sessionId: currentSessionId, messages: [], lastUpdated: new Date().toISOString() };
|
|
481
|
+
session.sessionId = currentSessionId;
|
|
482
|
+
session.messages.push({ id: crypto.randomUUID(), type: 'user', content: text, timestamp: new Date().toISOString() });
|
|
483
|
+
session.messages.push({ id: crypto.randomUUID(), type: 'ai', content: fullResponse, timestamp: new Date().toISOString() });
|
|
484
|
+
session.lastUpdated = new Date().toISOString();
|
|
485
|
+
await saveSession(session);
|
|
486
|
+
const channels = await loadChannels();
|
|
487
|
+
const channel = channels.find(c => c.id === channelId);
|
|
488
|
+
if (channel && channel.name === '智能体') {
|
|
489
|
+
const renameSuggestion = await agent.suggestRename(session.messages);
|
|
490
|
+
if (renameSuggestion) {
|
|
491
|
+
channel.name = renameSuggestion;
|
|
492
|
+
await saveChannels(channels);
|
|
493
|
+
broadcast({ type: 'renamed', channelId, newName: renameSuggestion }, channelId);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (channel) {
|
|
497
|
+
channel.updatedAt = new Date().toISOString();
|
|
498
|
+
await saveChannels(channels);
|
|
499
|
+
}
|
|
500
|
+
broadcast({ type: 'done' }, channelId);
|
|
501
|
+
res.json({ ok: true });
|
|
502
|
+
}
|
|
503
|
+
catch (err) {
|
|
504
|
+
broadcast({ type: 'error', content: err.message }, channelId);
|
|
505
|
+
broadcast({ type: 'done' }, channelId);
|
|
506
|
+
res.status(500).json({ error: err.message });
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
// ---------- 频道元数据后台修复队列 ----------
|
|
510
|
+
// 关键点: 旧实现会在每次 GET /channels 时同步执行 KeyManager.generate() + IPFS POST,
|
|
511
|
+
// 多频道场景下持续分配密钥对 + 发起 HTTP 请求, 几轮就会把 Node 内存撑爆。
|
|
512
|
+
// 新实现: 入队 + 节流(2s) + 单飞, 立刻返回当前 channels, 修复异步进行。
|
|
513
|
+
const didFixQueue = new Set(); // 待修复的 channelId
|
|
514
|
+
let didFixRunning = false;
|
|
515
|
+
let didFixTimer = null;
|
|
516
|
+
function scheduleDidFix(channelId) {
|
|
517
|
+
didFixQueue.add(channelId);
|
|
518
|
+
if (didFixTimer)
|
|
519
|
+
return;
|
|
520
|
+
didFixTimer = setTimeout(() => {
|
|
521
|
+
didFixTimer = null;
|
|
522
|
+
void runDidFixOnce();
|
|
523
|
+
}, 2000);
|
|
524
|
+
}
|
|
525
|
+
async function runDidFixOnce() {
|
|
526
|
+
if (didFixRunning)
|
|
527
|
+
return;
|
|
528
|
+
didFixRunning = true;
|
|
529
|
+
try {
|
|
530
|
+
while (didFixQueue.size > 0) {
|
|
531
|
+
const id = didFixQueue.values().next().value;
|
|
532
|
+
didFixQueue.delete(id);
|
|
533
|
+
try {
|
|
534
|
+
await fixOneChannelDID(id);
|
|
535
|
+
}
|
|
536
|
+
catch (e) {
|
|
537
|
+
console.log(`[DID 修复] ${id} 失败: ${e.message}`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
finally {
|
|
542
|
+
didFixRunning = false;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
async function fixOneChannelDID(channelId) {
|
|
546
|
+
const channels = await loadChannels();
|
|
547
|
+
const channel = channels.find(c => c.id === channelId);
|
|
548
|
+
if (!channel)
|
|
549
|
+
return;
|
|
550
|
+
const didMissing = !channel.did || channel.did === 'undefined' || channel.did === 'null' || channel.did === '';
|
|
551
|
+
if (!didMissing)
|
|
552
|
+
return;
|
|
553
|
+
let kp;
|
|
554
|
+
try {
|
|
555
|
+
kp = KeyManager.generate();
|
|
556
|
+
}
|
|
557
|
+
catch {
|
|
558
|
+
kp = null;
|
|
559
|
+
}
|
|
560
|
+
if (kp && kp.did) {
|
|
561
|
+
channel.did = kp.did;
|
|
562
|
+
channel.publicKey = Buffer.from(kp.publicKey).toString('hex');
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
// 兜底: 用 channelId 派生, 不阻塞 UI
|
|
566
|
+
channel.did = `did:web:${channel.id}`;
|
|
567
|
+
channel.publicKey = `pk_${channel.id}`;
|
|
568
|
+
}
|
|
569
|
+
console.log(`[DID 修复] ${channel.name} DID = ${channel.did}`);
|
|
570
|
+
// IPFS 注册: 失败也无所谓, 后续可重试
|
|
571
|
+
try {
|
|
572
|
+
const auth = await AgentAuthManager.newWithRemoteIpfs('http://127.0.0.1:5001', 'http://127.0.0.1:8080');
|
|
573
|
+
const result = await auth.registerAgent({ name: channel.name, services: [] }, kp, '');
|
|
574
|
+
channel.cid = result.cid || channel.cid;
|
|
575
|
+
// 关键: 不再保存整份 didDocument, 只留 cid/ipnsName 两个引用字段
|
|
576
|
+
if (result.didDocument) {
|
|
577
|
+
channel.didDocRef = {
|
|
578
|
+
cid: result.cid,
|
|
579
|
+
ipnsName: result.didDocument?.ipnsName
|
|
580
|
+
};
|
|
581
|
+
delete channel.didDocument;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
// IPFS 不可用, 跳过 — 下次再试
|
|
586
|
+
}
|
|
587
|
+
await saveChannels(channels);
|
|
588
|
+
}
|
|
589
|
+
// 频道列表响应缓存: 短时间内重复请求走缓存, 避免每次重读 + 重序列化 channels.json
|
|
590
|
+
// 跨作用域 (saveChannels 在模块顶层, 本函数在 createWebServer 内) 用 lastChannelsWriteAt 协调失效
|
|
591
|
+
const channelsCache = { data: null, cachedAt: 0 };
|
|
592
|
+
const CHANNELS_CACHE_TTL_MS = 500;
|
|
593
|
+
/** 获取频道列表 — 立即返回, 缺 DID 的频道入队后台修复 */
|
|
594
|
+
async function getChannelsWithDID() {
|
|
595
|
+
const now = Date.now();
|
|
596
|
+
// 缓存命中: 数据有效 AND 在写盘之后 AND 在 TTL 内
|
|
597
|
+
if (channelsCache.data && channelsCache.cachedAt > lastChannelsWriteAt && channelsCache.cachedAt + CHANNELS_CACHE_TTL_MS > now) {
|
|
598
|
+
return channelsCache.data;
|
|
599
|
+
}
|
|
600
|
+
const channels = await loadChannels();
|
|
601
|
+
// 防御性剥除: 任何旧 channels.json 残留的 didDocument 都不返回给客户端
|
|
602
|
+
const sanitized = channels.map(ch => {
|
|
603
|
+
const { didDocument: _omit, ...rest } = ch;
|
|
604
|
+
return rest;
|
|
605
|
+
});
|
|
606
|
+
for (const channel of sanitized) {
|
|
607
|
+
const didMissing = !channel.did || channel.did === 'undefined' || channel.did === 'null' || channel.did === '';
|
|
608
|
+
if (didMissing) {
|
|
609
|
+
scheduleDidFix(channel.id);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
channelsCache.data = sanitized;
|
|
613
|
+
channelsCache.cachedAt = now;
|
|
614
|
+
return sanitized;
|
|
615
|
+
}
|
|
616
|
+
app.get('/channels', async (_req, res) => {
|
|
617
|
+
try {
|
|
618
|
+
console.log('[API] /channels 被调用');
|
|
619
|
+
const channels = await getChannelsWithDID();
|
|
620
|
+
console.log('[获取频道] 返回', channels.length, '个');
|
|
621
|
+
channels.forEach((ch, i) => {
|
|
622
|
+
console.log(` [${i}] ${ch.name} - did: ${ch.did || '无'} - cid: ${ch.cid || '无'}`);
|
|
623
|
+
});
|
|
624
|
+
res.json(channels);
|
|
625
|
+
}
|
|
626
|
+
catch (err) {
|
|
627
|
+
console.error('[API] /channels 错误:', err);
|
|
628
|
+
res.status(500).json({ error: err.message });
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
app.post('/channels', async (req, res) => {
|
|
632
|
+
try {
|
|
633
|
+
const { name, agentId, walletAddress, autoInvokeTools } = req.body;
|
|
634
|
+
console.log(`[创建频道] 收到请求: name=${name}, agentId=${agentId}, wallet=${walletAddress ? 'yes' : 'no'}`);
|
|
635
|
+
if (!name || !agentId) {
|
|
636
|
+
return res.status(400).json({ error: 'name and agentId required' });
|
|
637
|
+
}
|
|
638
|
+
const channels = await loadChannels();
|
|
639
|
+
const id = `ch_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
|
|
640
|
+
// 校验钱包地址格式 (粗校验: 0x + 40 hex / Solana base58 / Sui 0x+64)
|
|
641
|
+
const validWallet = isValidWalletAddress(walletAddress);
|
|
642
|
+
// 先创建频道(不阻塞等待 DID 生成)
|
|
643
|
+
const channel = {
|
|
644
|
+
id,
|
|
645
|
+
name,
|
|
646
|
+
agentId,
|
|
647
|
+
createdAt: new Date().toISOString(),
|
|
648
|
+
updatedAt: new Date().toISOString(),
|
|
649
|
+
currentSessionId: `sess_${Date.now()}`,
|
|
650
|
+
walletAddress: validWallet || undefined,
|
|
651
|
+
walletRegisteredAt: validWallet ? new Date().toISOString() : undefined,
|
|
652
|
+
autoInvokeTools: autoInvokeTools !== false, // 默认 true
|
|
653
|
+
sessions: [{
|
|
654
|
+
id: `sess_${Date.now()}`,
|
|
655
|
+
createdAt: new Date().toISOString(),
|
|
656
|
+
messageCount: 0,
|
|
657
|
+
preview: ''
|
|
658
|
+
}]
|
|
659
|
+
};
|
|
660
|
+
console.log(`[创建频道] 先保存频道 ID: ${id}`);
|
|
661
|
+
channels.push(channel);
|
|
662
|
+
await saveChannels(channels);
|
|
663
|
+
await saveSession({ channelId: id, sessionId: 'default', messages: [], lastUpdated: new Date().toISOString() });
|
|
664
|
+
res.json(channel);
|
|
665
|
+
// 后台生成 DID — 用统一的修复队列, 避免每个 POST 都启动独立 setTimeout
|
|
666
|
+
console.log(`[创建频道] 加入 DID 修复队列...`);
|
|
667
|
+
scheduleDidFix(id);
|
|
668
|
+
}
|
|
669
|
+
catch (err) {
|
|
670
|
+
console.error('[创建频道] 错误:', err);
|
|
671
|
+
res.status(500).json({ error: err.message });
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
// 创建新 Session(在现有 Channel 下)
|
|
675
|
+
app.post('/channels/:channelId/sessions', async (req, res) => {
|
|
676
|
+
try {
|
|
677
|
+
const { channelId } = req.params;
|
|
678
|
+
const channels = await loadChannels();
|
|
679
|
+
const channel = channels.find(c => c.id === channelId);
|
|
680
|
+
if (!channel) {
|
|
681
|
+
return res.status(404).json({ error: 'Channel not found' });
|
|
682
|
+
}
|
|
683
|
+
const sessionId = `sess_${Date.now()}`;
|
|
684
|
+
const session = {
|
|
685
|
+
id: sessionId,
|
|
686
|
+
createdAt: new Date().toISOString(),
|
|
687
|
+
messageCount: 0,
|
|
688
|
+
preview: ''
|
|
689
|
+
};
|
|
690
|
+
if (!channel.sessions) {
|
|
691
|
+
channel.sessions = [];
|
|
692
|
+
}
|
|
693
|
+
channel.sessions.push(session);
|
|
694
|
+
channel.currentSessionId = sessionId;
|
|
695
|
+
channel.updatedAt = new Date().toISOString();
|
|
696
|
+
await saveChannels(channels);
|
|
697
|
+
await saveSession({ channelId, sessionId, messages: [], lastUpdated: new Date().toISOString() });
|
|
698
|
+
res.json({ session, currentSessionId: sessionId });
|
|
699
|
+
}
|
|
700
|
+
catch (err) {
|
|
701
|
+
console.error('[创建Session] 错误:', err);
|
|
702
|
+
res.status(500).json({ error: err.message });
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
// 获取 Channel 下的所有 Sessions
|
|
706
|
+
app.get('/channels/:channelId/sessions', async (req, res) => {
|
|
707
|
+
try {
|
|
708
|
+
const { channelId } = req.params;
|
|
709
|
+
const channels = await loadChannels();
|
|
710
|
+
const channel = channels.find(c => c.id === channelId);
|
|
711
|
+
if (!channel) {
|
|
712
|
+
return res.status(404).json({ error: 'Channel not found' });
|
|
713
|
+
}
|
|
714
|
+
res.json({
|
|
715
|
+
sessions: channel.sessions || [],
|
|
716
|
+
currentSessionId: channel.currentSessionId
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
catch (err) {
|
|
720
|
+
console.error('[获取Sessions] 错误:', err);
|
|
721
|
+
res.status(500).json({ error: err.message });
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
// 切换 Session
|
|
725
|
+
app.post('/channels/:channelId/sessions/:sessionId/switch', async (req, res) => {
|
|
726
|
+
try {
|
|
727
|
+
const { channelId, sessionId } = req.params;
|
|
728
|
+
const channels = await loadChannels();
|
|
729
|
+
const channel = channels.find(c => c.id === channelId);
|
|
730
|
+
if (!channel) {
|
|
731
|
+
return res.status(404).json({ error: 'Channel not found' });
|
|
732
|
+
}
|
|
733
|
+
channel.currentSessionId = sessionId;
|
|
734
|
+
channel.updatedAt = new Date().toISOString();
|
|
735
|
+
await saveChannels(channels);
|
|
736
|
+
res.json({ ok: true, currentSessionId: sessionId });
|
|
737
|
+
}
|
|
738
|
+
catch (err) {
|
|
739
|
+
console.error('[切换Session] 错误:', err);
|
|
740
|
+
res.status(500).json({ error: err.message });
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
// 删除单个 Session
|
|
744
|
+
app.delete('/channels/:channelId/sessions/:sessionId', async (req, res) => {
|
|
745
|
+
try {
|
|
746
|
+
const { channelId, sessionId } = req.params;
|
|
747
|
+
const channels = await loadChannels();
|
|
748
|
+
const channel = channels.find(c => c.id === channelId);
|
|
749
|
+
if (!channel) {
|
|
750
|
+
return res.status(404).json({ error: 'Channel not found' });
|
|
751
|
+
}
|
|
752
|
+
// 不允许删除最后一个 session —— 至少要保留一个
|
|
753
|
+
if (!channel.sessions || channel.sessions.length <= 1) {
|
|
754
|
+
return res.status(400).json({ error: 'At least one session is required' });
|
|
755
|
+
}
|
|
756
|
+
const sessionIndex = channel.sessions.findIndex(s => s.id === sessionId);
|
|
757
|
+
if (sessionIndex === -1) {
|
|
758
|
+
return res.status(404).json({ error: 'Session not found' });
|
|
759
|
+
}
|
|
760
|
+
channel.sessions.splice(sessionIndex, 1);
|
|
761
|
+
// 如果删除的是当前 session,切换到列表里的第一个
|
|
762
|
+
if (channel.currentSessionId === sessionId) {
|
|
763
|
+
const nextSession = channel.sessions[0];
|
|
764
|
+
channel.currentSessionId = nextSession.id;
|
|
765
|
+
}
|
|
766
|
+
channel.updatedAt = new Date().toISOString();
|
|
767
|
+
await saveChannels(channels);
|
|
768
|
+
// 删除 session 文件
|
|
769
|
+
try {
|
|
770
|
+
await fs.unlink(path.join(SESSION_CACHE_PATH, `${channelId}:${sessionId}.json`));
|
|
771
|
+
}
|
|
772
|
+
catch { }
|
|
773
|
+
res.json({ ok: true, currentSessionId: channel.currentSessionId });
|
|
774
|
+
}
|
|
775
|
+
catch (err) {
|
|
776
|
+
console.error('[删除Session] 错误:', err);
|
|
777
|
+
res.status(500).json({ error: err.message });
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
app.delete('/channels/:channelId', async (req, res) => {
|
|
781
|
+
try {
|
|
782
|
+
const { channelId } = req.params;
|
|
783
|
+
const channels = await loadChannels();
|
|
784
|
+
const index = channels.findIndex(c => c.id === channelId);
|
|
785
|
+
if (index === -1) {
|
|
786
|
+
return res.status(404).json({ error: 'Channel not found' });
|
|
787
|
+
}
|
|
788
|
+
const channel = channels[index];
|
|
789
|
+
channels.splice(index, 1);
|
|
790
|
+
await saveChannels(channels);
|
|
791
|
+
// 清理该 channel 名下所有的 session 文件 + 默认 session 文件
|
|
792
|
+
const candidates = new Set([`${channelId}.json`]);
|
|
793
|
+
if (channel.sessions) {
|
|
794
|
+
channel.sessions.forEach(s => candidates.add(`${channelId}:${s.id}.json`));
|
|
795
|
+
}
|
|
796
|
+
for (const filename of candidates) {
|
|
797
|
+
try {
|
|
798
|
+
await fs.unlink(path.join(SESSION_CACHE_PATH, filename));
|
|
799
|
+
}
|
|
800
|
+
catch { }
|
|
801
|
+
}
|
|
802
|
+
res.json({ ok: true });
|
|
803
|
+
}
|
|
804
|
+
catch (err) {
|
|
805
|
+
res.status(500).json({ error: err.message });
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
app.patch('/channels/:channelId', async (req, res) => {
|
|
809
|
+
try {
|
|
810
|
+
const { channelId } = req.params;
|
|
811
|
+
const { name, walletAddress, autoInvokeTools } = req.body;
|
|
812
|
+
const channels = await loadChannels();
|
|
813
|
+
const channel = channels.find(c => c.id === channelId);
|
|
814
|
+
if (!channel) {
|
|
815
|
+
return res.status(404).json({ error: 'Channel not found' });
|
|
816
|
+
}
|
|
817
|
+
if (typeof name === 'string' && name.trim()) {
|
|
818
|
+
channel.name = name.trim();
|
|
819
|
+
}
|
|
820
|
+
// walletAddress 允许 null/'' 来解绑
|
|
821
|
+
if (walletAddress !== undefined) {
|
|
822
|
+
if (walletAddress === null || walletAddress === '') {
|
|
823
|
+
channel.walletAddress = undefined;
|
|
824
|
+
channel.walletRegisteredAt = undefined;
|
|
825
|
+
}
|
|
826
|
+
else {
|
|
827
|
+
const valid = isValidWalletAddress(walletAddress);
|
|
828
|
+
if (!valid) {
|
|
829
|
+
return res.status(400).json({ error: 'Invalid wallet address format' });
|
|
830
|
+
}
|
|
831
|
+
channel.walletAddress = valid;
|
|
832
|
+
channel.walletRegisteredAt = channel.walletRegisteredAt || new Date().toISOString();
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
if (typeof autoInvokeTools === 'boolean') {
|
|
836
|
+
channel.autoInvokeTools = autoInvokeTools;
|
|
837
|
+
}
|
|
838
|
+
channel.updatedAt = new Date().toISOString();
|
|
839
|
+
await saveChannels(channels);
|
|
840
|
+
res.json(channel);
|
|
841
|
+
}
|
|
842
|
+
catch (err) {
|
|
843
|
+
res.status(500).json({ error: err.message });
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
app.get('/sessions/:channelId', async (req, res) => {
|
|
847
|
+
try {
|
|
848
|
+
const session = await loadSession(req.params.channelId, req.query.sessionId);
|
|
849
|
+
res.json(session || { channelId: req.params.channelId, sessionId: req.query.sessionId || 'default', messages: [], lastUpdated: null });
|
|
850
|
+
}
|
|
851
|
+
catch (err) {
|
|
852
|
+
res.status(500).json({ error: err.message });
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
// 增量追加消息到 session (前端落盘用, 避免丢消息)
|
|
856
|
+
// body: { message: { type, content, timestamp? } }
|
|
857
|
+
app.patch('/sessions/:channelId/:sessionId', async (req, res) => {
|
|
858
|
+
try {
|
|
859
|
+
const { channelId, sessionId } = req.params;
|
|
860
|
+
const { message } = req.body || {};
|
|
861
|
+
if (!message || (message.type !== 'user' && message.type !== 'ai') || typeof message.content !== 'string') {
|
|
862
|
+
return res.status(400).json({ error: 'invalid message' });
|
|
863
|
+
}
|
|
864
|
+
const existing = await loadSession(channelId, sessionId);
|
|
865
|
+
const session = existing || { channelId, sessionId, messages: [], lastUpdated: new Date().toISOString() };
|
|
866
|
+
session.sessionId = sessionId;
|
|
867
|
+
// 去重: 跳过与最后一条完全相同的 (避免 SSE 重复推导致双写)
|
|
868
|
+
const last = session.messages[session.messages.length - 1];
|
|
869
|
+
if (last && last.type === message.type && last.content === message.content) {
|
|
870
|
+
return res.json({ ok: true, count: session.messages.length, deduped: true });
|
|
871
|
+
}
|
|
872
|
+
session.messages.push({
|
|
873
|
+
id: message.id || crypto.randomUUID(),
|
|
874
|
+
type: message.type,
|
|
875
|
+
content: message.content,
|
|
876
|
+
timestamp: message.timestamp || new Date().toISOString()
|
|
877
|
+
});
|
|
878
|
+
session.lastUpdated = new Date().toISOString();
|
|
879
|
+
await saveSession(session);
|
|
880
|
+
res.json({ ok: true, count: session.messages.length });
|
|
881
|
+
}
|
|
882
|
+
catch (err) {
|
|
883
|
+
res.status(500).json({ error: err.message });
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
app.get('/theme', async (req, res) => {
|
|
887
|
+
try {
|
|
888
|
+
const themeData = await loadTheme();
|
|
889
|
+
res.json(themeData);
|
|
890
|
+
}
|
|
891
|
+
catch (err) {
|
|
892
|
+
res.json({ theme: 'light', agentId: '' });
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
app.post('/theme', async (req, res) => {
|
|
896
|
+
try {
|
|
897
|
+
const { theme, agentId } = req.body;
|
|
898
|
+
if (theme !== 'light' && theme !== 'dark') {
|
|
899
|
+
return res.status(400).json({ error: 'Invalid theme' });
|
|
900
|
+
}
|
|
901
|
+
await saveTheme(theme, agentId || '');
|
|
902
|
+
res.json({ ok: true });
|
|
903
|
+
}
|
|
904
|
+
catch (err) {
|
|
905
|
+
res.status(500).json({ error: err.message });
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
// 重新生成回复
|
|
909
|
+
app.post('/regenerate', async (req, res) => {
|
|
910
|
+
const { channelId, userMessage } = req.body;
|
|
911
|
+
if (!channelId) {
|
|
912
|
+
return res.status(400).json({ error: 'No channelId provided' });
|
|
913
|
+
}
|
|
914
|
+
if (!userMessage) {
|
|
915
|
+
return res.status(400).json({ error: 'No userMessage provided' });
|
|
916
|
+
}
|
|
917
|
+
try {
|
|
918
|
+
const channels = await loadChannels();
|
|
919
|
+
const channel = channels.find(c => c.id === channelId);
|
|
920
|
+
const currentSessionId = channel?.currentSessionId || 'default';
|
|
921
|
+
const realChannelDid = channel?.did || '';
|
|
922
|
+
const realChannelName = channel?.name || '';
|
|
923
|
+
const realChannelDidDoc = channel?.didDocRef;
|
|
924
|
+
// 通知前端开始重新生成
|
|
925
|
+
broadcast({ type: 'regenerating', channelId }, channelId);
|
|
926
|
+
const agent = await getAgentForChannel(channelId, realChannelDid, realChannelName, realChannelDidDoc);
|
|
927
|
+
let fullResponse = '';
|
|
928
|
+
const streamCallback = (event) => {
|
|
929
|
+
if (event.type === 'token' || event.type === 'thinking') {
|
|
930
|
+
broadcast({ type: 'stream', streamType: event.type, content: event.content }, channelId);
|
|
931
|
+
}
|
|
932
|
+
else if (event.type === 'status' || event.type === 'tool') {
|
|
933
|
+
broadcast({ type: 'status', tool: event.tool, content: event.content }, channelId);
|
|
934
|
+
}
|
|
935
|
+
else if (event.type === 'error') {
|
|
936
|
+
broadcast({ type: 'error', content: event.content }, channelId);
|
|
937
|
+
}
|
|
938
|
+
};
|
|
939
|
+
// 重新生成时只发送用户消息
|
|
940
|
+
fullResponse = await agent.promptStream(userMessage, streamCallback);
|
|
941
|
+
broadcast({ type: 'ai', content: fullResponse }, channelId);
|
|
942
|
+
// 更新 session
|
|
943
|
+
const existingSession = await loadSession(channelId, currentSessionId);
|
|
944
|
+
if (existingSession && existingSession.messages.length > 0) {
|
|
945
|
+
// 移除最后一个 AI 消息,替换为新的
|
|
946
|
+
const lastAiIndex = existingSession.messages.map((m) => m.type).lastIndexOf('ai');
|
|
947
|
+
if (lastAiIndex !== -1) {
|
|
948
|
+
existingSession.messages = existingSession.messages.slice(0, lastAiIndex);
|
|
949
|
+
}
|
|
950
|
+
existingSession.messages.push({
|
|
951
|
+
id: crypto.randomUUID(),
|
|
952
|
+
type: 'ai',
|
|
953
|
+
content: fullResponse,
|
|
954
|
+
timestamp: new Date().toISOString()
|
|
955
|
+
});
|
|
956
|
+
existingSession.lastUpdated = new Date().toISOString();
|
|
957
|
+
await saveSession(existingSession);
|
|
958
|
+
}
|
|
959
|
+
broadcast({ type: 'done' }, channelId);
|
|
960
|
+
res.json({ ok: true });
|
|
961
|
+
}
|
|
962
|
+
catch (err) {
|
|
963
|
+
broadcast({ type: 'error', content: err.message }, channelId);
|
|
964
|
+
broadcast({ type: 'done' }, channelId);
|
|
965
|
+
res.status(500).json({ error: err.message });
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
// ==================== Task Queue API ====================
|
|
969
|
+
// 获取所有任务
|
|
970
|
+
app.get('/api/tasks', async (req, res) => {
|
|
971
|
+
try {
|
|
972
|
+
const tasks = await loadTaskQueue();
|
|
973
|
+
res.json(tasks);
|
|
974
|
+
}
|
|
975
|
+
catch (err) {
|
|
976
|
+
res.status(500).json({ error: err.message });
|
|
977
|
+
}
|
|
978
|
+
});
|
|
979
|
+
// 创建新任务
|
|
980
|
+
app.post('/api/tasks', async (req, res) => {
|
|
981
|
+
try {
|
|
982
|
+
const { type, title, description, steps } = req.body;
|
|
983
|
+
if (!type || !title) {
|
|
984
|
+
return res.status(400).json({ error: 'type and title required' });
|
|
985
|
+
}
|
|
986
|
+
const tasks = await loadTaskQueue();
|
|
987
|
+
const task = {
|
|
988
|
+
id: `task_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`,
|
|
989
|
+
type,
|
|
990
|
+
title,
|
|
991
|
+
description,
|
|
992
|
+
status: 'pending',
|
|
993
|
+
progress: 0,
|
|
994
|
+
createdAt: new Date().toISOString(),
|
|
995
|
+
updatedAt: new Date().toISOString(),
|
|
996
|
+
steps: steps?.map((s, i) => ({
|
|
997
|
+
id: `step_${i}`,
|
|
998
|
+
name: s,
|
|
999
|
+
status: 'pending'
|
|
1000
|
+
}))
|
|
1001
|
+
};
|
|
1002
|
+
tasks.push(task);
|
|
1003
|
+
await saveTaskQueue(tasks);
|
|
1004
|
+
res.json(task);
|
|
1005
|
+
}
|
|
1006
|
+
catch (err) {
|
|
1007
|
+
res.status(500).json({ error: err.message });
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1010
|
+
// 获取单个任务
|
|
1011
|
+
app.get('/api/tasks/:taskId', async (req, res) => {
|
|
1012
|
+
try {
|
|
1013
|
+
const { taskId } = req.params;
|
|
1014
|
+
const tasks = await loadTaskQueue();
|
|
1015
|
+
const task = tasks.find(t => t.id === taskId);
|
|
1016
|
+
if (!task) {
|
|
1017
|
+
return res.status(404).json({ error: 'Task not found' });
|
|
1018
|
+
}
|
|
1019
|
+
res.json(task);
|
|
1020
|
+
}
|
|
1021
|
+
catch (err) {
|
|
1022
|
+
res.status(500).json({ error: err.message });
|
|
1023
|
+
}
|
|
1024
|
+
});
|
|
1025
|
+
// 更新任务
|
|
1026
|
+
app.patch('/api/tasks/:taskId', async (req, res) => {
|
|
1027
|
+
try {
|
|
1028
|
+
const { taskId } = req.params;
|
|
1029
|
+
const { status, currentStep } = req.body;
|
|
1030
|
+
const tasks = await loadTaskQueue();
|
|
1031
|
+
const taskIndex = tasks.findIndex(t => t.id === taskId);
|
|
1032
|
+
if (taskIndex === -1) {
|
|
1033
|
+
return res.status(404).json({ error: 'Task not found' });
|
|
1034
|
+
}
|
|
1035
|
+
if (status) {
|
|
1036
|
+
tasks[taskIndex].status = status;
|
|
1037
|
+
}
|
|
1038
|
+
if (currentStep !== undefined) {
|
|
1039
|
+
tasks[taskIndex].currentStep = currentStep;
|
|
1040
|
+
}
|
|
1041
|
+
tasks[taskIndex].updatedAt = new Date().toISOString();
|
|
1042
|
+
await saveTaskQueue(tasks);
|
|
1043
|
+
res.json(tasks[taskIndex]);
|
|
1044
|
+
}
|
|
1045
|
+
catch (err) {
|
|
1046
|
+
res.status(500).json({ error: err.message });
|
|
1047
|
+
}
|
|
1048
|
+
});
|
|
1049
|
+
// 删除任务
|
|
1050
|
+
app.delete('/api/tasks/:taskId', async (req, res) => {
|
|
1051
|
+
try {
|
|
1052
|
+
const { taskId } = req.params;
|
|
1053
|
+
const tasks = await loadTaskQueue();
|
|
1054
|
+
const filtered = tasks.filter(t => t.id !== taskId);
|
|
1055
|
+
await saveTaskQueue(filtered);
|
|
1056
|
+
res.json({ ok: true });
|
|
1057
|
+
}
|
|
1058
|
+
catch (err) {
|
|
1059
|
+
res.status(500).json({ error: err.message });
|
|
1060
|
+
}
|
|
1061
|
+
});
|
|
1062
|
+
// 执行任务(自动执行下一步)
|
|
1063
|
+
app.post('/api/tasks/:taskId/execute', async (req, res) => {
|
|
1064
|
+
try {
|
|
1065
|
+
const { taskId } = req.params;
|
|
1066
|
+
const { channelId } = req.body;
|
|
1067
|
+
if (!channelId) {
|
|
1068
|
+
return res.status(400).json({ error: 'channelId required' });
|
|
1069
|
+
}
|
|
1070
|
+
const tasks = await loadTaskQueue();
|
|
1071
|
+
const task = tasks.find(t => t.id === taskId);
|
|
1072
|
+
if (!task) {
|
|
1073
|
+
return res.status(404).json({ error: 'Task not found' });
|
|
1074
|
+
}
|
|
1075
|
+
if (isExecutingTask) {
|
|
1076
|
+
return res.status(409).json({ error: 'Another task is currently executing' });
|
|
1077
|
+
}
|
|
1078
|
+
// 异步执行任务
|
|
1079
|
+
executeTask(task, channelId);
|
|
1080
|
+
res.json({ ok: true, taskId: task.id });
|
|
1081
|
+
}
|
|
1082
|
+
catch (err) {
|
|
1083
|
+
res.status(500).json({ error: err.message });
|
|
1084
|
+
}
|
|
1085
|
+
});
|
|
1086
|
+
// 执行下一个待处理任务
|
|
1087
|
+
app.post('/api/tasks/execute-next', async (req, res) => {
|
|
1088
|
+
try {
|
|
1089
|
+
const { channelId } = req.body;
|
|
1090
|
+
if (!channelId) {
|
|
1091
|
+
return res.status(400).json({ error: 'channelId required' });
|
|
1092
|
+
}
|
|
1093
|
+
const tasks = await loadTaskQueue();
|
|
1094
|
+
const nextTask = tasks.find(t => t.status === 'pending');
|
|
1095
|
+
if (!nextTask) {
|
|
1096
|
+
return res.json({ ok: false, message: 'No pending tasks' });
|
|
1097
|
+
}
|
|
1098
|
+
if (isExecutingTask) {
|
|
1099
|
+
return res.status(409).json({ error: 'Another task is currently executing' });
|
|
1100
|
+
}
|
|
1101
|
+
// 异步执行任务
|
|
1102
|
+
executeTask(nextTask, channelId);
|
|
1103
|
+
res.json({ ok: true, taskId: nextTask.id });
|
|
1104
|
+
}
|
|
1105
|
+
catch (err) {
|
|
1106
|
+
res.status(500).json({ error: err.message });
|
|
1107
|
+
}
|
|
1108
|
+
});
|
|
1109
|
+
// 创建并执行工作流
|
|
1110
|
+
app.post('/api/workflow', async (req, res) => {
|
|
1111
|
+
try {
|
|
1112
|
+
const { channelId, title, steps } = req.body;
|
|
1113
|
+
if (!channelId || !steps || !Array.isArray(steps)) {
|
|
1114
|
+
return res.status(400).json({ error: 'channelId and steps required' });
|
|
1115
|
+
}
|
|
1116
|
+
const tasks = await loadTaskQueue();
|
|
1117
|
+
const task = {
|
|
1118
|
+
id: `wf_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`,
|
|
1119
|
+
type: 'workflow',
|
|
1120
|
+
title: title || '工作流',
|
|
1121
|
+
description: `包含 ${steps.length} 个步骤的工作流`,
|
|
1122
|
+
status: 'pending',
|
|
1123
|
+
progress: 0,
|
|
1124
|
+
createdAt: new Date().toISOString(),
|
|
1125
|
+
updatedAt: new Date().toISOString(),
|
|
1126
|
+
steps: steps.map((s, i) => ({
|
|
1127
|
+
id: `step_${i}`,
|
|
1128
|
+
name: s,
|
|
1129
|
+
status: 'pending'
|
|
1130
|
+
})),
|
|
1131
|
+
currentStep: 0
|
|
1132
|
+
};
|
|
1133
|
+
tasks.push(task);
|
|
1134
|
+
await saveTaskQueue(tasks);
|
|
1135
|
+
// 自动开始执行
|
|
1136
|
+
if (!isExecutingTask) {
|
|
1137
|
+
executeTask(task, channelId);
|
|
1138
|
+
}
|
|
1139
|
+
res.json({ ok: true, task });
|
|
1140
|
+
}
|
|
1141
|
+
catch (err) {
|
|
1142
|
+
res.status(500).json({ error: err.message });
|
|
1143
|
+
}
|
|
1144
|
+
});
|
|
1145
|
+
// ==================== LLM 配置 API ====================
|
|
1146
|
+
// 获取所有 LLM 配置
|
|
1147
|
+
app.get('/api/llm-config', async (req, res) => {
|
|
1148
|
+
try {
|
|
1149
|
+
const config = await llmConfigStore.getConfig();
|
|
1150
|
+
const providerInfo = llmConfigStore.getAllProviderInfo();
|
|
1151
|
+
// 隐藏 API Key
|
|
1152
|
+
const safeConfig = {
|
|
1153
|
+
...config,
|
|
1154
|
+
providers: Object.fromEntries(Object.entries(config.providers).map(([key, val]) => [
|
|
1155
|
+
key,
|
|
1156
|
+
{ ...val, apiKey: val.apiKey ? '***' + val.apiKey.slice(-4) : '' }
|
|
1157
|
+
])),
|
|
1158
|
+
providerInfo
|
|
1159
|
+
};
|
|
1160
|
+
res.json(safeConfig);
|
|
1161
|
+
}
|
|
1162
|
+
catch (err) {
|
|
1163
|
+
res.status(500).json({ error: err.message });
|
|
1164
|
+
}
|
|
1165
|
+
});
|
|
1166
|
+
// 更新 LLM 配置
|
|
1167
|
+
app.post('/api/llm-config', async (req, res) => {
|
|
1168
|
+
try {
|
|
1169
|
+
const { provider, config } = req.body;
|
|
1170
|
+
if (!provider || !config) {
|
|
1171
|
+
return res.status(400).json({ error: 'provider and config required' });
|
|
1172
|
+
}
|
|
1173
|
+
await llmConfigStore.updateProvider(provider, config);
|
|
1174
|
+
// 如果是活跃供应商,重新初始化 Pi SDK
|
|
1175
|
+
const currentActive = await llmConfigStore.getActiveProvider();
|
|
1176
|
+
if (provider === currentActive) {
|
|
1177
|
+
const newConfig = await llmConfigStore.getActiveProviderConfig();
|
|
1178
|
+
if (newConfig) {
|
|
1179
|
+
initMinimax({
|
|
1180
|
+
provider,
|
|
1181
|
+
apiKey: newConfig.apiKey || undefined,
|
|
1182
|
+
baseUrl: newConfig.baseUrl || undefined,
|
|
1183
|
+
model: newConfig.model || undefined
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
res.json({ ok: true });
|
|
1188
|
+
}
|
|
1189
|
+
catch (err) {
|
|
1190
|
+
res.status(500).json({ error: err.message });
|
|
1191
|
+
}
|
|
1192
|
+
});
|
|
1193
|
+
// 设置活跃供应商
|
|
1194
|
+
app.post('/api/llm-provider', async (req, res) => {
|
|
1195
|
+
try {
|
|
1196
|
+
const { provider } = req.body;
|
|
1197
|
+
if (!provider) {
|
|
1198
|
+
return res.status(400).json({ error: 'provider required' });
|
|
1199
|
+
}
|
|
1200
|
+
await llmConfigStore.setActiveProvider(provider);
|
|
1201
|
+
// 重新初始化 Pi SDK
|
|
1202
|
+
const config = await llmConfigStore.getActiveProviderConfig();
|
|
1203
|
+
if (config) {
|
|
1204
|
+
initMinimax({
|
|
1205
|
+
provider: provider,
|
|
1206
|
+
apiKey: config.apiKey || undefined,
|
|
1207
|
+
baseUrl: config.baseUrl || undefined,
|
|
1208
|
+
model: config.model || undefined
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
res.json({ ok: true, provider });
|
|
1212
|
+
}
|
|
1213
|
+
catch (err) {
|
|
1214
|
+
res.status(500).json({ error: err.message });
|
|
1215
|
+
}
|
|
1216
|
+
});
|
|
1217
|
+
// 测试供应商连接
|
|
1218
|
+
app.post('/api/llm-test', async (req, res) => {
|
|
1219
|
+
try {
|
|
1220
|
+
const { provider } = req.body;
|
|
1221
|
+
if (!provider) {
|
|
1222
|
+
return res.status(400).json({ error: 'provider required' });
|
|
1223
|
+
}
|
|
1224
|
+
const result = await llmConfigStore.testProvider(provider);
|
|
1225
|
+
res.json(result);
|
|
1226
|
+
}
|
|
1227
|
+
catch (err) {
|
|
1228
|
+
res.status(500).json({ error: err.message });
|
|
1229
|
+
}
|
|
1230
|
+
});
|
|
1231
|
+
// 统一 AI 解析入口:CLI / 接收方节点 调这里完成 LLM + judgment + harness
|
|
1232
|
+
// 入参: { text, mimeType, fileName, fromNodeId, source }
|
|
1233
|
+
// 出参: { summary, qualityScore, judgmentId?, gateArtifact? }
|
|
1234
|
+
app.post('/api/ai-parse', async (req, res) => {
|
|
1235
|
+
try {
|
|
1236
|
+
const { text, mimeType, fileName, fromNodeId, source } = req.body || {};
|
|
1237
|
+
if (!text || !fileName) {
|
|
1238
|
+
return res.status(400).json({ error: 'text and fileName required' });
|
|
1239
|
+
}
|
|
1240
|
+
const truncated = text.length > 6000 ? text.substring(0, 6000) + '...[截断]' : text;
|
|
1241
|
+
const prompt = `请分析以下 ${mimeType || 'text'} 文档,并给出 (1) 一句话中文摘要 (2) 三个关键要点 (3) 质量评分(0-1)。\n\n文件名: ${fileName}\n\n内容:\n${truncated}`;
|
|
1242
|
+
// 1. LLM 解析
|
|
1243
|
+
const llm = getMinimax();
|
|
1244
|
+
const t0 = Date.now();
|
|
1245
|
+
const llmResult = await llm.summarize(prompt);
|
|
1246
|
+
const dt = Date.now() - t0;
|
|
1247
|
+
const out = {
|
|
1248
|
+
ok: true,
|
|
1249
|
+
summary: llmResult.summary,
|
|
1250
|
+
qualityScore: llmResult.qualityScore,
|
|
1251
|
+
latencyMs: dt,
|
|
1252
|
+
mimeType: mimeType || 'text/plain',
|
|
1253
|
+
fileName,
|
|
1254
|
+
};
|
|
1255
|
+
// 2. 蒸馏为 judgment (异步,失败不影响主返回)
|
|
1256
|
+
try {
|
|
1257
|
+
const judgmentMod = await import('../pi-ecosystem-judgment/index.js');
|
|
1258
|
+
await judgmentMod.initializeJudgmentStore();
|
|
1259
|
+
const j = await judgmentMod.createJudgment({
|
|
1260
|
+
type: 'trajectory',
|
|
1261
|
+
content: `AI 解析 ${fileName}: ${llmResult.summary.slice(0, 200)}`,
|
|
1262
|
+
source: 'agent',
|
|
1263
|
+
confidence: Math.min(1, llmResult.qualityScore),
|
|
1264
|
+
context: `ai-parse:${mimeType || 'text'}:${source || 'p2p'}`,
|
|
1265
|
+
evidence: {
|
|
1266
|
+
trajectory: [{
|
|
1267
|
+
timestamp: new Date().toISOString(),
|
|
1268
|
+
action: `parse:${fileName}`,
|
|
1269
|
+
outcome: `score=${llmResult.qualityScore.toFixed(2)}`,
|
|
1270
|
+
approved: true,
|
|
1271
|
+
}],
|
|
1272
|
+
},
|
|
1273
|
+
});
|
|
1274
|
+
out.judgmentId = j.id;
|
|
1275
|
+
}
|
|
1276
|
+
catch (e) {
|
|
1277
|
+
out.judgmentError = e.message;
|
|
1278
|
+
}
|
|
1279
|
+
// 3. 在 harness 落产物 (异步,失败不影响)
|
|
1280
|
+
try {
|
|
1281
|
+
const harnessMod = await import('../bollharness-integration/index.js');
|
|
1282
|
+
const gate = new harnessMod.GateStateMachine();
|
|
1283
|
+
gate.submitArtifact(`ai-parse:${fileName}`, {
|
|
1284
|
+
summary: llmResult.summary,
|
|
1285
|
+
score: llmResult.qualityScore,
|
|
1286
|
+
fromNodeId: fromNodeId || null,
|
|
1287
|
+
parsedAt: Date.now(),
|
|
1288
|
+
});
|
|
1289
|
+
out.gateArtifact = `ai-parse:${fileName}`;
|
|
1290
|
+
}
|
|
1291
|
+
catch (e) {
|
|
1292
|
+
out.gateError = e.message;
|
|
1293
|
+
}
|
|
1294
|
+
res.json(out);
|
|
1295
|
+
}
|
|
1296
|
+
catch (err) {
|
|
1297
|
+
res.status(500).json({ error: err.message });
|
|
1298
|
+
}
|
|
1299
|
+
});
|
|
1300
|
+
// ==================== P2P Network API ====================
|
|
1301
|
+
// 获取当前身份
|
|
1302
|
+
app.get('/api/identity', async (_req, res) => {
|
|
1303
|
+
console.log('收到 /api/identity 请求');
|
|
1304
|
+
console.log('p2pIdentity.did:', p2pIdentity.did);
|
|
1305
|
+
try {
|
|
1306
|
+
res.json({
|
|
1307
|
+
did: p2pIdentity.did,
|
|
1308
|
+
name: p2pIdentity.name,
|
|
1309
|
+
publicKey: p2pIdentity.publicKey
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
catch (err) {
|
|
1313
|
+
console.error('API identity 错误:', err);
|
|
1314
|
+
res.status(500).json({ error: err.message });
|
|
1315
|
+
}
|
|
1316
|
+
});
|
|
1317
|
+
// 获取已连接的节点
|
|
1318
|
+
app.get('/api/peers', async (_req, res) => {
|
|
1319
|
+
try {
|
|
1320
|
+
if (!p2pCommunicator) {
|
|
1321
|
+
res.json([]);
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
const connections = p2pCommunicator.getConnections();
|
|
1325
|
+
const peers = connections.map((conn) => ({
|
|
1326
|
+
id: conn.publicKey.substring(0, 16),
|
|
1327
|
+
publicKey: conn.publicKey,
|
|
1328
|
+
peerId: conn.publicKey
|
|
1329
|
+
}));
|
|
1330
|
+
res.json(peers);
|
|
1331
|
+
}
|
|
1332
|
+
catch (err) {
|
|
1333
|
+
res.status(500).json({ error: err.message });
|
|
1334
|
+
}
|
|
1335
|
+
});
|
|
1336
|
+
// 获取已发现的所有节点(包括通过 CID 解析的)
|
|
1337
|
+
app.get('/api/discovered-peers', async (_req, res) => {
|
|
1338
|
+
try {
|
|
1339
|
+
// 从全局状态获取已发现的节点
|
|
1340
|
+
const discovered = global.discoveredAgents || [];
|
|
1341
|
+
res.json(discovered);
|
|
1342
|
+
}
|
|
1343
|
+
catch (err) {
|
|
1344
|
+
res.status(500).json({ error: err.message });
|
|
1345
|
+
}
|
|
1346
|
+
});
|
|
1347
|
+
// ==================== iroh P2P API ====================
|
|
1348
|
+
// 初始化 iroh P2P(带持久化)
|
|
1349
|
+
app.post('/api/iroh/init', async (_req, res) => {
|
|
1350
|
+
try {
|
|
1351
|
+
if (irohInitialized && irohNodeInfo) {
|
|
1352
|
+
res.json({ ok: true, ...irohNodeInfo });
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
console.log('[iroh API] 初始化 iroh...');
|
|
1356
|
+
// 启动 iroh(启用持久化)
|
|
1357
|
+
await irohTransport.start(undefined, true);
|
|
1358
|
+
const nodeId = irohTransport.getNodeId() || '';
|
|
1359
|
+
console.log(`[iroh API] iroh 节点 ID: ${nodeId.substring(0, 20)}...`);
|
|
1360
|
+
// 生成 DID
|
|
1361
|
+
const keyPair = KeyManager.generate();
|
|
1362
|
+
const did = keyPair.did;
|
|
1363
|
+
// 构建节点信息文档
|
|
1364
|
+
const nodeDoc = {
|
|
1365
|
+
id: did,
|
|
1366
|
+
name: `bolloon-web-${Date.now()}`,
|
|
1367
|
+
version: '1.0',
|
|
1368
|
+
capabilities: ['chat', 'ai', 'judgment-injection', 'web-interface'],
|
|
1369
|
+
interests: ['ai', 'p2p', 'judgment-system'],
|
|
1370
|
+
irohNodeId: nodeId,
|
|
1371
|
+
channels: [{ id: 'main', name: '主对话' }],
|
|
1372
|
+
createdAt: new Date().toISOString()
|
|
1373
|
+
};
|
|
1374
|
+
// 发布到 IPFS(可选,如果 IPFS 不可用则跳过)
|
|
1375
|
+
let cid = '';
|
|
1376
|
+
try {
|
|
1377
|
+
const formData = new FormData();
|
|
1378
|
+
const blob = new Blob([JSON.stringify(nodeDoc)], { type: 'application/json' });
|
|
1379
|
+
formData.append('file', blob, 'node-info.json');
|
|
1380
|
+
const ipfsRes = await fetch(`${IPFS_ENDPOINT}/api/v0/add`, {
|
|
1381
|
+
method: 'POST',
|
|
1382
|
+
body: formData,
|
|
1383
|
+
signal: AbortSignal.timeout(5000)
|
|
1384
|
+
});
|
|
1385
|
+
const ipfsResult = await ipfsRes.text();
|
|
1386
|
+
const cidMatch = ipfsResult.match(/"Hash":"([^"]+)"/);
|
|
1387
|
+
cid = cidMatch ? cidMatch[1] : '';
|
|
1388
|
+
console.log(`[iroh API] CID 发布成功: ${cid.substring(0, 20)}...`);
|
|
1389
|
+
}
|
|
1390
|
+
catch (ipfsErr) {
|
|
1391
|
+
console.warn('[iroh API] IPFS 不可用,跳过 CID 发布:', ipfsErr.message);
|
|
1392
|
+
// 生成一个假的 CID 用于本地测试(格式:Qm + 44个随机字符)
|
|
1393
|
+
const randomPart = Array.from({ length: 44 }, () => Math.random().toString(36)[2]).join('').substring(0, 44);
|
|
1394
|
+
cid = `Qm${randomPart}`;
|
|
1395
|
+
console.log(`[iroh API] 使用本地 CID: ${cid.substring(0, 20)}...`);
|
|
1396
|
+
}
|
|
1397
|
+
irohNodeInfo = {
|
|
1398
|
+
did,
|
|
1399
|
+
cid,
|
|
1400
|
+
irohNodeId: nodeId,
|
|
1401
|
+
name: nodeDoc.name,
|
|
1402
|
+
initialized: true
|
|
1403
|
+
};
|
|
1404
|
+
irohInitialized = true;
|
|
1405
|
+
// 设置消息处理
|
|
1406
|
+
irohTransport.onMessage('chat', (msg) => {
|
|
1407
|
+
const content = new TextDecoder().decode(msg.payload);
|
|
1408
|
+
console.log(`[iroh] 收到消息 from ${msg.from.substring(0, 12)}...`);
|
|
1409
|
+
// 通过 SSE 广播给所有客户端
|
|
1410
|
+
broadcast({
|
|
1411
|
+
type: 'p2p_message',
|
|
1412
|
+
from: msg.from,
|
|
1413
|
+
content,
|
|
1414
|
+
timestamp: Date.now()
|
|
1415
|
+
}, 'p2p-global');
|
|
1416
|
+
});
|
|
1417
|
+
irohTransport.onMessage('ai-dialogue', (msg) => {
|
|
1418
|
+
const content = new TextDecoder().decode(msg.payload);
|
|
1419
|
+
console.log(`[iroh] 收到 AI 对话 from ${msg.from.substring(0, 12)}...`);
|
|
1420
|
+
broadcast({
|
|
1421
|
+
type: 'p2p_message',
|
|
1422
|
+
content,
|
|
1423
|
+
timestamp: Date.now()
|
|
1424
|
+
}, 'p2p-global');
|
|
1425
|
+
});
|
|
1426
|
+
console.log(`[iroh API] 初始化完成: DID=${did}, CID=${cid}`);
|
|
1427
|
+
res.json({ ok: true, ...irohNodeInfo });
|
|
1428
|
+
}
|
|
1429
|
+
catch (err) {
|
|
1430
|
+
console.error('[iroh API] 初始化失败:', err);
|
|
1431
|
+
res.status(500).json({ error: err.message });
|
|
1432
|
+
}
|
|
1433
|
+
});
|
|
1434
|
+
// 获取 iroh 节点信息
|
|
1435
|
+
app.get('/api/iroh/info', async (_req, res) => {
|
|
1436
|
+
if (!irohInitialized || !irohNodeInfo) {
|
|
1437
|
+
res.json({ initialized: false });
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
res.json({
|
|
1441
|
+
initialized: true,
|
|
1442
|
+
did: irohNodeInfo.did,
|
|
1443
|
+
cid: irohNodeInfo.cid,
|
|
1444
|
+
irohNodeId: irohNodeInfo.irohNodeId,
|
|
1445
|
+
name: irohNodeInfo.name
|
|
1446
|
+
});
|
|
1447
|
+
});
|
|
1448
|
+
// 通过 CID 或 Node ID 连接到其他节点
|
|
1449
|
+
app.post('/api/iroh/connect', async (req, res) => {
|
|
1450
|
+
try {
|
|
1451
|
+
const { cid } = req.body;
|
|
1452
|
+
if (!cid) {
|
|
1453
|
+
return res.status(400).json({ error: 'CID required' });
|
|
1454
|
+
}
|
|
1455
|
+
if (!irohInitialized) {
|
|
1456
|
+
return res.status(500).json({ error: 'iroh not initialized' });
|
|
1457
|
+
}
|
|
1458
|
+
let targetNodeId;
|
|
1459
|
+
let nodeName = 'Unknown';
|
|
1460
|
+
console.log(`[iroh API] 连接到: ${cid}`);
|
|
1461
|
+
// 检查是 Node ID(64字符十六进制)还是 CID
|
|
1462
|
+
const isNodeId = /^[a-f0-9]{64}$/i.test(cid);
|
|
1463
|
+
if (isNodeId) {
|
|
1464
|
+
// 直接使用 Node ID
|
|
1465
|
+
targetNodeId = cid;
|
|
1466
|
+
console.log(`[iroh API] 使用直接 Node ID: ${targetNodeId.substring(0, 20)}...`);
|
|
1467
|
+
}
|
|
1468
|
+
else {
|
|
1469
|
+
// 从 IPFS 获取节点信息
|
|
1470
|
+
try {
|
|
1471
|
+
const ipfsRes = await fetch(`${IPFS_ENDPOINT}/api/v0/cat?arg=${cid}`, {
|
|
1472
|
+
method: 'POST'
|
|
1473
|
+
});
|
|
1474
|
+
const content = await ipfsRes.text();
|
|
1475
|
+
const doc = JSON.parse(content);
|
|
1476
|
+
if (!doc.irohNodeId) {
|
|
1477
|
+
return res.status(400).json({ error: '节点信息中不包含 irohNodeId' });
|
|
1478
|
+
}
|
|
1479
|
+
targetNodeId = doc.irohNodeId;
|
|
1480
|
+
nodeName = doc.name || 'Unknown';
|
|
1481
|
+
console.log(`[iroh API] 从 IPFS 获取节点: ${targetNodeId.substring(0, 20)}...`);
|
|
1482
|
+
}
|
|
1483
|
+
catch {
|
|
1484
|
+
return res.status(400).json({ error: '无法从 CID 获取节点信息,请确认 CID 有效' });
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
// 发送连接消息
|
|
1488
|
+
const message = JSON.stringify({
|
|
1489
|
+
type: 'hello',
|
|
1490
|
+
from: irohNodeInfo?.irohNodeId,
|
|
1491
|
+
name: irohNodeInfo?.name,
|
|
1492
|
+
timestamp: Date.now()
|
|
1493
|
+
});
|
|
1494
|
+
const success = await irohTransport.sendMessage(targetNodeId, 'chat', new TextEncoder().encode(message));
|
|
1495
|
+
if (success) {
|
|
1496
|
+
console.log(`[iroh API] 连接成功!`);
|
|
1497
|
+
res.json({
|
|
1498
|
+
ok: true,
|
|
1499
|
+
targetNodeId,
|
|
1500
|
+
nodeName
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1503
|
+
else {
|
|
1504
|
+
console.log(`[iroh API] 连接失败(对方可能离线)`);
|
|
1505
|
+
res.json({
|
|
1506
|
+
ok: false,
|
|
1507
|
+
error: '连接失败,对方可能离线',
|
|
1508
|
+
targetNodeId
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
catch (err) {
|
|
1513
|
+
console.error('[iroh API] 连接错误:', err);
|
|
1514
|
+
res.status(500).json({ error: err.message });
|
|
1515
|
+
}
|
|
1516
|
+
});
|
|
1517
|
+
// 发送消息给指定节点
|
|
1518
|
+
app.post('/api/iroh/send', async (req, res) => {
|
|
1519
|
+
try {
|
|
1520
|
+
const { targetNodeId, type, content } = req.body;
|
|
1521
|
+
if (!targetNodeId || !content) {
|
|
1522
|
+
return res.status(400).json({ error: 'targetNodeId and content required' });
|
|
1523
|
+
}
|
|
1524
|
+
if (!irohInitialized) {
|
|
1525
|
+
return res.status(500).json({ error: 'iroh not initialized' });
|
|
1526
|
+
}
|
|
1527
|
+
const messageType = type || 'chat';
|
|
1528
|
+
const success = await irohTransport.sendMessage(targetNodeId, messageType, new TextEncoder().encode(content));
|
|
1529
|
+
res.json({ ok: success });
|
|
1530
|
+
}
|
|
1531
|
+
catch (err) {
|
|
1532
|
+
console.error('[iroh API] 发送消息错误:', err);
|
|
1533
|
+
res.status(500).json({ error: err.message });
|
|
1534
|
+
}
|
|
1535
|
+
});
|
|
1536
|
+
// 获取已连接的 iroh 节点列表
|
|
1537
|
+
app.get('/api/iroh/peers', async (_req, res) => {
|
|
1538
|
+
try {
|
|
1539
|
+
if (!irohInitialized) {
|
|
1540
|
+
res.json([]);
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
const peers = irohTransport.getConnectedPeers();
|
|
1544
|
+
res.json(peers.map((nodeId) => ({
|
|
1545
|
+
nodeId,
|
|
1546
|
+
shortId: nodeId.substring(0, 16)
|
|
1547
|
+
})));
|
|
1548
|
+
}
|
|
1549
|
+
catch (err) {
|
|
1550
|
+
res.status(500).json({ error: err.message });
|
|
1551
|
+
}
|
|
1552
|
+
});
|
|
1553
|
+
// 获取离线消息数量
|
|
1554
|
+
app.get('/api/iroh/offline-count', async (_req, res) => {
|
|
1555
|
+
try {
|
|
1556
|
+
if (!irohInitialized) {
|
|
1557
|
+
res.json({ count: 0 });
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
const count = irohTransport.getPendingOfflineCount();
|
|
1561
|
+
res.json({ count });
|
|
1562
|
+
}
|
|
1563
|
+
catch (err) {
|
|
1564
|
+
res.json({ count: 0 });
|
|
1565
|
+
}
|
|
1566
|
+
});
|
|
1567
|
+
// 获取当前频道的身份信息
|
|
1568
|
+
app.get('/api/channel-identity/:channelId', async (req, res) => {
|
|
1569
|
+
try {
|
|
1570
|
+
const { channelId } = req.params;
|
|
1571
|
+
const channels = await loadChannels();
|
|
1572
|
+
const channel = channels.find(c => c.id === channelId);
|
|
1573
|
+
if (!channel) {
|
|
1574
|
+
return res.status(404).json({ error: 'Channel not found' });
|
|
1575
|
+
}
|
|
1576
|
+
res.json({
|
|
1577
|
+
did: channel.did || '',
|
|
1578
|
+
cid: channel.cid || '',
|
|
1579
|
+
publicKey: channel.publicKey || '',
|
|
1580
|
+
name: channel.name
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
catch (err) {
|
|
1584
|
+
res.status(500).json({ error: err.message });
|
|
1585
|
+
}
|
|
1586
|
+
});
|
|
1587
|
+
// 通过 DID/CID 连接远程智能体
|
|
1588
|
+
app.post('/api/connect', async (req, res) => {
|
|
1589
|
+
try {
|
|
1590
|
+
const { did, cid, ipnsName } = req.body;
|
|
1591
|
+
if (!did && !cid && !ipnsName) {
|
|
1592
|
+
return res.status(400).json({ error: 'DID, CID or IPNS name required' });
|
|
1593
|
+
}
|
|
1594
|
+
console.log(`[连接] 尝试连接 DID: ${did}, CID: ${cid}, IPNS: ${ipnsName}`);
|
|
1595
|
+
let doc = null;
|
|
1596
|
+
// 1. 通过 CID 或 IPNS 解析 DiapDoc
|
|
1597
|
+
if (cid || ipnsName) {
|
|
1598
|
+
try {
|
|
1599
|
+
const { IpfsClient } = await import('@diap/sdk');
|
|
1600
|
+
const ipfs = new IpfsClient('http://127.0.0.1:5001', null);
|
|
1601
|
+
let resolvedCid = cid;
|
|
1602
|
+
if (ipnsName) {
|
|
1603
|
+
resolvedCid = await ipfs.resolveIpns(ipnsName);
|
|
1604
|
+
}
|
|
1605
|
+
if (resolvedCid) {
|
|
1606
|
+
const content = await ipfs.get(resolvedCid);
|
|
1607
|
+
doc = JSON.parse(content);
|
|
1608
|
+
console.log(`[连接] 解析 DiapDoc 成功: ${doc.name}`);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
catch (e) {
|
|
1612
|
+
console.warn(`[连接] 解析 IPFS 内容失败:`, e);
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
// 2. 如果有 DID,检查是否已连接
|
|
1616
|
+
if (did) {
|
|
1617
|
+
// 广播连接请求
|
|
1618
|
+
if (p2pCommunicator) {
|
|
1619
|
+
const payload = JSON.stringify({
|
|
1620
|
+
type: 'connect_request',
|
|
1621
|
+
requesterDid: did,
|
|
1622
|
+
targetDid: did,
|
|
1623
|
+
timestamp: Date.now()
|
|
1624
|
+
});
|
|
1625
|
+
// 广播到网络
|
|
1626
|
+
console.log(`[连接] 广播连接请求: ${did}`);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
// 3. 将解析的文档添加到已发现列表
|
|
1630
|
+
if (doc) {
|
|
1631
|
+
const discovered = global.discoveredAgents || [];
|
|
1632
|
+
const existing = discovered.findIndex((a) => a.did === doc.id);
|
|
1633
|
+
if (existing >= 0) {
|
|
1634
|
+
discovered[existing] = { ...discovered[existing], ...doc, lastSeen: Date.now() };
|
|
1635
|
+
}
|
|
1636
|
+
else {
|
|
1637
|
+
discovered.push({
|
|
1638
|
+
did: doc.id || doc.did,
|
|
1639
|
+
name: doc.name,
|
|
1640
|
+
capabilities: doc.capabilities || [],
|
|
1641
|
+
interests: doc.interests || [],
|
|
1642
|
+
channels: doc.channels || [],
|
|
1643
|
+
cid: cid,
|
|
1644
|
+
ipnsName: ipnsName,
|
|
1645
|
+
lastSeen: Date.now()
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
global.discoveredAgents = discovered;
|
|
1649
|
+
// 广播发现事件到前端
|
|
1650
|
+
broadcast({ type: 'peer_discovered', peer: doc });
|
|
1651
|
+
}
|
|
1652
|
+
res.json({
|
|
1653
|
+
ok: true,
|
|
1654
|
+
did: doc?.id || did,
|
|
1655
|
+
name: doc?.name,
|
|
1656
|
+
capabilities: doc?.capabilities || [],
|
|
1657
|
+
channels: doc?.channels || [],
|
|
1658
|
+
message: doc ? 'DiapDoc 解析成功' : '连接请求已发送'
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
catch (err) {
|
|
1662
|
+
res.status(500).json({ error: err.message });
|
|
1663
|
+
}
|
|
1664
|
+
});
|
|
1665
|
+
// 发送 P2P 消息
|
|
1666
|
+
app.post('/api/message-p2p', async (req, res) => {
|
|
1667
|
+
try {
|
|
1668
|
+
const { peerId, did, message } = req.body;
|
|
1669
|
+
if (!message) {
|
|
1670
|
+
return res.status(400).json({ error: 'message required' });
|
|
1671
|
+
}
|
|
1672
|
+
let targetPeerId = peerId;
|
|
1673
|
+
// 如果没有 peerId,通过 DID 查找
|
|
1674
|
+
if (!targetPeerId && did) {
|
|
1675
|
+
const discovered = global.discoveredAgents || [];
|
|
1676
|
+
const peer = discovered.find((a) => a.did === did);
|
|
1677
|
+
if (peer) {
|
|
1678
|
+
targetPeerId = peer.peerId;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
if (!targetPeerId) {
|
|
1682
|
+
// 如果没有 P2P 连接,将消息存储到本地队列
|
|
1683
|
+
const messageQueue = global.messageQueue || [];
|
|
1684
|
+
messageQueue.push({
|
|
1685
|
+
did,
|
|
1686
|
+
message,
|
|
1687
|
+
timestamp: Date.now(),
|
|
1688
|
+
status: 'pending'
|
|
1689
|
+
});
|
|
1690
|
+
global.messageQueue = messageQueue;
|
|
1691
|
+
res.json({ ok: true, queued: true, message: '消息已加入队列,等待对方上线' });
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
// 通过 P2P 发送消息(如果可用)
|
|
1695
|
+
try {
|
|
1696
|
+
const comm = p2pCommunicator;
|
|
1697
|
+
if (comm && typeof comm.send === 'function') {
|
|
1698
|
+
await comm.send(message, targetPeerId);
|
|
1699
|
+
res.json({ ok: true, sent: true });
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
catch { }
|
|
1704
|
+
// 如果 P2P 不可用,消息已在上面加入队列
|
|
1705
|
+
res.json({ ok: true, queued: true, message: '消息已加入队列,等待对方上线' });
|
|
1706
|
+
}
|
|
1707
|
+
catch (err) {
|
|
1708
|
+
res.status(500).json({ error: err.message });
|
|
1709
|
+
}
|
|
1710
|
+
});
|
|
1711
|
+
// 获取待接收的消息队列
|
|
1712
|
+
app.get('/api/peer-messages', async (_req, res) => {
|
|
1713
|
+
try {
|
|
1714
|
+
const messageQueue = global.messageQueue || [];
|
|
1715
|
+
const pendingMessages = messageQueue.filter((m) => m.status === 'pending');
|
|
1716
|
+
res.json(pendingMessages);
|
|
1717
|
+
}
|
|
1718
|
+
catch (err) {
|
|
1719
|
+
res.status(500).json({ error: err.message });
|
|
1720
|
+
}
|
|
1721
|
+
});
|
|
1722
|
+
// Chat inbox: 列出所有 peer 的 inbox + outbox
|
|
1723
|
+
app.get('/api/chat/inbox', async (_req, res) => {
|
|
1724
|
+
try {
|
|
1725
|
+
const { getInbox } = await import('../agents/p2p-chat-tools.js');
|
|
1726
|
+
const entries = await getInbox();
|
|
1727
|
+
// 按 status 分组, 时间倒序
|
|
1728
|
+
const grouped = {
|
|
1729
|
+
received: entries.filter((e) => e.status === 'received'),
|
|
1730
|
+
drafted: entries.filter((e) => e.status === 'drafted'),
|
|
1731
|
+
sent: entries.filter((e) => e.status === 'sent'),
|
|
1732
|
+
dismissed: entries.filter((e) => e.status === 'dismissed'),
|
|
1733
|
+
};
|
|
1734
|
+
res.json({ total: entries.length, grouped, all: entries });
|
|
1735
|
+
}
|
|
1736
|
+
catch (err) {
|
|
1737
|
+
res.status(500).json({ error: err.message });
|
|
1738
|
+
}
|
|
1739
|
+
});
|
|
1740
|
+
// 触发 processPendingInbox (手动 wake-up)
|
|
1741
|
+
app.post('/api/chat/process-pending', async (_req, res) => {
|
|
1742
|
+
try {
|
|
1743
|
+
const { processPendingInbox } = await import('../agents/p2p-chat-tools.js');
|
|
1744
|
+
const r = await processPendingInbox();
|
|
1745
|
+
res.json({ ok: true, ...r });
|
|
1746
|
+
}
|
|
1747
|
+
catch (err) {
|
|
1748
|
+
res.status(500).json({ error: err.message });
|
|
1749
|
+
}
|
|
1750
|
+
});
|
|
1751
|
+
// 主人审阅: 批准 draft
|
|
1752
|
+
app.post('/api/chat/approve', async (req, res) => {
|
|
1753
|
+
try {
|
|
1754
|
+
const { messageId, peerDID, finalText } = req.body || {};
|
|
1755
|
+
if (!messageId || !peerDID)
|
|
1756
|
+
return res.status(400).json({ error: 'messageId and peerDID required' });
|
|
1757
|
+
const { approveAndSend } = await import('../agents/p2p-chat-tools.js');
|
|
1758
|
+
const ok = await approveAndSend(messageId, peerDID, finalText);
|
|
1759
|
+
res.json({ ok, messageId });
|
|
1760
|
+
}
|
|
1761
|
+
catch (err) {
|
|
1762
|
+
res.status(500).json({ error: err.message });
|
|
1763
|
+
}
|
|
1764
|
+
});
|
|
1765
|
+
// 主人审阅: 丢弃 draft
|
|
1766
|
+
app.post('/api/chat/dismiss', async (req, res) => {
|
|
1767
|
+
try {
|
|
1768
|
+
const { messageId, peerDID } = req.body || {};
|
|
1769
|
+
if (!messageId || !peerDID)
|
|
1770
|
+
return res.status(400).json({ error: 'messageId and peerDID required' });
|
|
1771
|
+
const { dismissDraft } = await import('../agents/p2p-chat-tools.js');
|
|
1772
|
+
const ok = await dismissDraft(messageId, peerDID);
|
|
1773
|
+
res.json({ ok, messageId });
|
|
1774
|
+
}
|
|
1775
|
+
catch (err) {
|
|
1776
|
+
res.status(500).json({ error: err.message });
|
|
1777
|
+
}
|
|
1778
|
+
});
|
|
1779
|
+
// 标记消息已读
|
|
1780
|
+
app.post('/api/peer-messages/:messageId/read', async (req, res) => {
|
|
1781
|
+
try {
|
|
1782
|
+
const { messageId } = req.params;
|
|
1783
|
+
const messageQueue = global.messageQueue || [];
|
|
1784
|
+
const msg = messageQueue.find((m) => m.id === messageId);
|
|
1785
|
+
if (msg) {
|
|
1786
|
+
msg.status = 'read';
|
|
1787
|
+
}
|
|
1788
|
+
res.json({ ok: true });
|
|
1789
|
+
}
|
|
1790
|
+
catch (err) {
|
|
1791
|
+
res.status(500).json({ error: err.message });
|
|
1792
|
+
}
|
|
1793
|
+
});
|
|
1794
|
+
// ==================== P2P 连接进度 SSE ====================
|
|
1795
|
+
// 连接进度流(用于实时显示解析进度)
|
|
1796
|
+
const connectProgressClients = new Map();
|
|
1797
|
+
app.get('/api/p2p/connect/progress', async (req, res) => {
|
|
1798
|
+
const sessionId = crypto.randomUUID();
|
|
1799
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
1800
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
1801
|
+
res.setHeader('Connection', 'keep-alive');
|
|
1802
|
+
res.write(`data: ${JSON.stringify({ type: 'start', sessionId })}\n\n`);
|
|
1803
|
+
connectProgressClients.set(sessionId, res);
|
|
1804
|
+
req.on('close', () => {
|
|
1805
|
+
connectProgressClients.delete(sessionId);
|
|
1806
|
+
});
|
|
1807
|
+
});
|
|
1808
|
+
function emitConnectProgress(sessionId, data) {
|
|
1809
|
+
const client = connectProgressClients.get(sessionId);
|
|
1810
|
+
if (client) {
|
|
1811
|
+
client.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
// 取消连接
|
|
1815
|
+
app.post('/api/p2p/connect/cancel', async (req, res) => {
|
|
1816
|
+
const { sessionId } = req.body;
|
|
1817
|
+
if (sessionId && connectProgressClients.has(sessionId)) {
|
|
1818
|
+
connectProgressClients.get(sessionId).end();
|
|
1819
|
+
connectProgressClients.delete(sessionId);
|
|
1820
|
+
}
|
|
1821
|
+
res.json({ ok: true });
|
|
1822
|
+
});
|
|
1823
|
+
// ==================== P2P 连接历史 API ====================
|
|
1824
|
+
const P2P_HISTORY_PATH = path.join(SHARED_SESSION_PATH, 'p2p-history.json');
|
|
1825
|
+
async function loadP2PHistory() {
|
|
1826
|
+
try {
|
|
1827
|
+
const data = await fs.readFile(P2P_HISTORY_PATH, 'utf-8');
|
|
1828
|
+
return JSON.parse(data);
|
|
1829
|
+
}
|
|
1830
|
+
catch {
|
|
1831
|
+
return [];
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
async function saveP2PHistory(history) {
|
|
1835
|
+
await fs.writeFile(P2P_HISTORY_PATH, JSON.stringify(history, null, 2));
|
|
1836
|
+
}
|
|
1837
|
+
// 获取连接历史
|
|
1838
|
+
app.get('/api/p2p/history', async (_req, res) => {
|
|
1839
|
+
try {
|
|
1840
|
+
const history = await loadP2PHistory();
|
|
1841
|
+
res.json(history);
|
|
1842
|
+
}
|
|
1843
|
+
catch (err) {
|
|
1844
|
+
res.status(500).json({ error: err.message });
|
|
1845
|
+
}
|
|
1846
|
+
});
|
|
1847
|
+
// 添加到连接历史
|
|
1848
|
+
app.post('/api/p2p/history', async (req, res) => {
|
|
1849
|
+
try {
|
|
1850
|
+
const history = await loadP2PHistory();
|
|
1851
|
+
const entry = req.body;
|
|
1852
|
+
// 检查是否已存在
|
|
1853
|
+
const existingIndex = history.findIndex((h) => h.did === entry.did);
|
|
1854
|
+
if (existingIndex >= 0) {
|
|
1855
|
+
history[existingIndex] = { ...history[existingIndex], ...entry, lastConnectedAt: Date.now() };
|
|
1856
|
+
}
|
|
1857
|
+
else {
|
|
1858
|
+
history.unshift({ ...entry, id: crypto.randomUUID(), lastConnectedAt: Date.now(), lastMessageAt: 0, totalMessages: 0 });
|
|
1859
|
+
}
|
|
1860
|
+
await saveP2PHistory(history);
|
|
1861
|
+
res.json({ ok: true });
|
|
1862
|
+
}
|
|
1863
|
+
catch (err) {
|
|
1864
|
+
res.status(500).json({ error: err.message });
|
|
1865
|
+
}
|
|
1866
|
+
});
|
|
1867
|
+
// 更新连接历史
|
|
1868
|
+
app.patch('/api/p2p/history/:id', async (req, res) => {
|
|
1869
|
+
try {
|
|
1870
|
+
const history = await loadP2PHistory();
|
|
1871
|
+
const { id } = req.params;
|
|
1872
|
+
const updates = req.body;
|
|
1873
|
+
const index = history.findIndex((h) => h.id === id);
|
|
1874
|
+
if (index >= 0) {
|
|
1875
|
+
history[index] = { ...history[index], ...updates };
|
|
1876
|
+
await saveP2PHistory(history);
|
|
1877
|
+
}
|
|
1878
|
+
res.json({ ok: true });
|
|
1879
|
+
}
|
|
1880
|
+
catch (err) {
|
|
1881
|
+
res.status(500).json({ error: err.message });
|
|
1882
|
+
}
|
|
1883
|
+
});
|
|
1884
|
+
// 删除连接历史
|
|
1885
|
+
app.delete('/api/p2p/history/:id', async (req, res) => {
|
|
1886
|
+
try {
|
|
1887
|
+
const history = await loadP2PHistory();
|
|
1888
|
+
const { id } = req.params;
|
|
1889
|
+
const filtered = history.filter((h) => h.id !== id);
|
|
1890
|
+
await saveP2PHistory(filtered);
|
|
1891
|
+
res.json({ ok: true });
|
|
1892
|
+
}
|
|
1893
|
+
catch (err) {
|
|
1894
|
+
res.status(500).json({ error: err.message });
|
|
1895
|
+
}
|
|
1896
|
+
});
|
|
1897
|
+
// ==================== P2P 偏好设置 API ====================
|
|
1898
|
+
const P2P_PREFS_PATH = path.join(SHARED_SESSION_PATH, 'p2p-preferences.json');
|
|
1899
|
+
async function loadP2PPreferences() {
|
|
1900
|
+
try {
|
|
1901
|
+
const data = await fs.readFile(P2P_PREFS_PATH, 'utf-8');
|
|
1902
|
+
return JSON.parse(data);
|
|
1903
|
+
}
|
|
1904
|
+
catch {
|
|
1905
|
+
return {
|
|
1906
|
+
autoReconnect: true,
|
|
1907
|
+
autoConnectOnStartup: true,
|
|
1908
|
+
preferredNodes: [],
|
|
1909
|
+
maxOfflineQueue: 100,
|
|
1910
|
+
notifications: {
|
|
1911
|
+
newMessage: true,
|
|
1912
|
+
connectionEstablished: true,
|
|
1913
|
+
peerWentOnline: true,
|
|
1914
|
+
peerWentOffline: true
|
|
1915
|
+
}
|
|
1916
|
+
};
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
async function saveP2PPreferences(prefs) {
|
|
1920
|
+
await fs.writeFile(P2P_PREFS_PATH, JSON.stringify(prefs, null, 2));
|
|
1921
|
+
}
|
|
1922
|
+
app.get('/api/p2p/preferences', async (_req, res) => {
|
|
1923
|
+
try {
|
|
1924
|
+
const prefs = await loadP2PPreferences();
|
|
1925
|
+
res.json(prefs);
|
|
1926
|
+
}
|
|
1927
|
+
catch (err) {
|
|
1928
|
+
res.status(500).json({ error: err.message });
|
|
1929
|
+
}
|
|
1930
|
+
});
|
|
1931
|
+
app.patch('/api/p2p/preferences', async (req, res) => {
|
|
1932
|
+
try {
|
|
1933
|
+
const current = await loadP2PPreferences();
|
|
1934
|
+
const updates = req.body;
|
|
1935
|
+
await saveP2PPreferences({ ...current, ...updates });
|
|
1936
|
+
res.json({ ok: true });
|
|
1937
|
+
}
|
|
1938
|
+
catch (err) {
|
|
1939
|
+
res.status(500).json({ error: err.message });
|
|
1940
|
+
}
|
|
1941
|
+
});
|
|
1942
|
+
// 获取持久连接列表
|
|
1943
|
+
app.get('/api/p2p/persistent-connections', async (_req, res) => {
|
|
1944
|
+
try {
|
|
1945
|
+
const sessionProvider = app.locals.sessionProvider;
|
|
1946
|
+
if (!sessionProvider) {
|
|
1947
|
+
return res.json([]);
|
|
1948
|
+
}
|
|
1949
|
+
const channels = sessionProvider.getAllChannels().filter((ch) => ch.peerId);
|
|
1950
|
+
res.json(channels.map((ch) => ({
|
|
1951
|
+
id: ch.id,
|
|
1952
|
+
peerId: ch.peerId || '',
|
|
1953
|
+
peerDid: ch.peerDid || '',
|
|
1954
|
+
peerName: ch.peerName || 'Unknown',
|
|
1955
|
+
cid: ch.cid || '',
|
|
1956
|
+
status: ch.peerId ? 'connected' : 'disconnected',
|
|
1957
|
+
lastConnectedAt: new Date(ch.updatedAt).getTime(),
|
|
1958
|
+
channelId: ch.id,
|
|
1959
|
+
isAutoConnect: false
|
|
1960
|
+
})));
|
|
1961
|
+
}
|
|
1962
|
+
catch (err) {
|
|
1963
|
+
res.status(500).json({ error: err.message });
|
|
1964
|
+
}
|
|
1965
|
+
});
|
|
1966
|
+
// 更新连接状态
|
|
1967
|
+
app.post('/api/p2p/connection-status', async (req, res) => {
|
|
1968
|
+
try {
|
|
1969
|
+
const { id, status, channelId } = req.body;
|
|
1970
|
+
const sessionProvider = app.locals.sessionProvider;
|
|
1971
|
+
if (sessionProvider && channelId) {
|
|
1972
|
+
await sessionProvider.setChannelInfo(channelId, {
|
|
1973
|
+
peerId: status === 'connected' ? (req.body.peerId || 'connected') : undefined
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
res.json({ ok: true });
|
|
1977
|
+
}
|
|
1978
|
+
catch (err) {
|
|
1979
|
+
res.status(500).json({ error: err.message });
|
|
1980
|
+
}
|
|
1981
|
+
});
|
|
1982
|
+
// 创建对话通道
|
|
1983
|
+
app.post('/api/p2p/create-channel', async (req, res) => {
|
|
1984
|
+
try {
|
|
1985
|
+
const { peerDid, peerName, cid, peerId } = req.body;
|
|
1986
|
+
const sessionProvider = app.locals.sessionProvider;
|
|
1987
|
+
if (!sessionProvider) {
|
|
1988
|
+
return res.status(500).json({ error: 'sessionProvider not available' });
|
|
1989
|
+
}
|
|
1990
|
+
const channel = await sessionProvider.getOrCreatePeerChannel(peerDid, peerName);
|
|
1991
|
+
await sessionProvider.setChannelInfo(channel.id, { peerId: peerId || '', cid: cid || '' });
|
|
1992
|
+
res.json({ channelId: channel.id });
|
|
1993
|
+
}
|
|
1994
|
+
catch (err) {
|
|
1995
|
+
res.status(500).json({ error: err.message });
|
|
1996
|
+
}
|
|
1997
|
+
});
|
|
1998
|
+
// CID 解析
|
|
1999
|
+
app.post('/api/p2p/resolve-cid', async (req, res) => {
|
|
2000
|
+
try {
|
|
2001
|
+
const { cid } = req.body;
|
|
2002
|
+
const { DiapDocParser } = await import('../social/channels/diap-doc-parser.js');
|
|
2003
|
+
const parser = new DiapDocParser();
|
|
2004
|
+
const result = await parser.parseFromCID(cid);
|
|
2005
|
+
res.json(result);
|
|
2006
|
+
}
|
|
2007
|
+
catch (err) {
|
|
2008
|
+
res.status(500).json({ error: err.message });
|
|
2009
|
+
}
|
|
2010
|
+
});
|
|
2011
|
+
// P2P 工具调用
|
|
2012
|
+
app.post('/api/p2p/tool-call', async (req, res) => {
|
|
2013
|
+
try {
|
|
2014
|
+
const { tool, targetDid, payload } = req.body;
|
|
2015
|
+
let result;
|
|
2016
|
+
switch (tool) {
|
|
2017
|
+
case 'system_info':
|
|
2018
|
+
const { getLocalSystemInfo } = await import('./components/p2p/p2p-tools.js');
|
|
2019
|
+
result = getLocalSystemInfo();
|
|
2020
|
+
break;
|
|
2021
|
+
case 'file_list':
|
|
2022
|
+
const { getLocalFileList } = await import('./components/p2p/p2p-tools.js');
|
|
2023
|
+
result = getLocalFileList(payload?.path || '/');
|
|
2024
|
+
break;
|
|
2025
|
+
default:
|
|
2026
|
+
return res.status(400).json({ error: `Unknown tool: ${tool}` });
|
|
2027
|
+
}
|
|
2028
|
+
res.json({ success: true, data: result });
|
|
2029
|
+
}
|
|
2030
|
+
catch (err) {
|
|
2031
|
+
res.status(500).json({ error: err.message });
|
|
2032
|
+
}
|
|
2033
|
+
});
|
|
2034
|
+
// ==================== 24h Heartbeat System ====================
|
|
2035
|
+
let healthMonitor = null;
|
|
2036
|
+
let watchdog = null;
|
|
2037
|
+
// 延迟导入避免循环依赖
|
|
2038
|
+
try {
|
|
2039
|
+
const { createHealthMonitor, createWatchdog } = await import('../heartbeat/index.js');
|
|
2040
|
+
healthMonitor = createHealthMonitor();
|
|
2041
|
+
watchdog = createWatchdog();
|
|
2042
|
+
console.log('[24h] Heartbeat modules loaded');
|
|
2043
|
+
}
|
|
2044
|
+
catch (err) {
|
|
2045
|
+
console.warn('[24h] Failed to load heartbeat modules:', err);
|
|
2046
|
+
}
|
|
2047
|
+
// 健康检查端点
|
|
2048
|
+
app.get('/api/health', async (req, res) => {
|
|
2049
|
+
try {
|
|
2050
|
+
if (!healthMonitor) {
|
|
2051
|
+
res.status(503).json({ error: 'Health monitor not initialized' });
|
|
2052
|
+
return;
|
|
2053
|
+
}
|
|
2054
|
+
const status = await healthMonitor.check();
|
|
2055
|
+
// 记录心跳活跃
|
|
2056
|
+
healthMonitor.recordHeartbeat?.();
|
|
2057
|
+
watchdog?.recordActivity?.('health_check');
|
|
2058
|
+
// 根据状态返回不同 HTTP 状态码
|
|
2059
|
+
const httpStatus = status.status === 'healthy' ? 200 :
|
|
2060
|
+
status.status === 'degraded' ? 200 : 503;
|
|
2061
|
+
res.status(httpStatus).json(status);
|
|
2062
|
+
}
|
|
2063
|
+
catch (err) {
|
|
2064
|
+
res.status(500).json({ error: err.message });
|
|
2065
|
+
}
|
|
2066
|
+
});
|
|
2067
|
+
// 看门狗状态
|
|
2068
|
+
app.get('/api/watchdog', async (req, res) => {
|
|
2069
|
+
try {
|
|
2070
|
+
if (!watchdog) {
|
|
2071
|
+
res.status(503).json({ error: 'Watchdog not initialized' });
|
|
2072
|
+
return;
|
|
2073
|
+
}
|
|
2074
|
+
const state = watchdog.getState();
|
|
2075
|
+
res.json(state);
|
|
2076
|
+
}
|
|
2077
|
+
catch (err) {
|
|
2078
|
+
res.status(500).json({ error: err.message });
|
|
2079
|
+
}
|
|
2080
|
+
});
|
|
2081
|
+
// 看门狗重置
|
|
2082
|
+
app.post('/api/watchdog/reset', async (req, res) => {
|
|
2083
|
+
try {
|
|
2084
|
+
if (watchdog) {
|
|
2085
|
+
watchdog.reset();
|
|
2086
|
+
}
|
|
2087
|
+
res.json({ ok: true });
|
|
2088
|
+
}
|
|
2089
|
+
catch (err) {
|
|
2090
|
+
res.status(500).json({ error: err.message });
|
|
2091
|
+
}
|
|
2092
|
+
});
|
|
2093
|
+
// 启动看门狗监控
|
|
2094
|
+
if (watchdog) {
|
|
2095
|
+
// level 1 (内存爆) → 进程自杀, 依赖外层 supervisor / 用户重启 (Windows 任务计划/手动)
|
|
2096
|
+
// 否则 Node.js 高 GC 压力下 HTTP 响应丢失, 客户端 fetch 永远 pending
|
|
2097
|
+
watchdog.registerRestartStrategy(1, () => {
|
|
2098
|
+
console.error('[Watchdog] memory critical, 进程退出 (期望外层重启)');
|
|
2099
|
+
setTimeout(() => process.exit(1), 100);
|
|
2100
|
+
});
|
|
2101
|
+
watchdog.start();
|
|
2102
|
+
console.log('[24h] Watchdog started');
|
|
2103
|
+
}
|
|
2104
|
+
// 定期健康检查(不阻塞主服务器启动)
|
|
2105
|
+
if (healthMonitor) {
|
|
2106
|
+
healthMonitor.startPeriodicCheck(60000);
|
|
2107
|
+
console.log('[24h] Health monitor periodic check started');
|
|
2108
|
+
}
|
|
2109
|
+
// ==================== Self-Improve 端点 ====================
|
|
2110
|
+
// 查看当前策略 (白名单 / 黑名单)
|
|
2111
|
+
app.get('/api/self-improve/policy', async (_req, res) => {
|
|
2112
|
+
const { loadPolicy } = await import('../agents/shell-guard.js');
|
|
2113
|
+
const policy = loadPolicy(true); // 强制重读
|
|
2114
|
+
if (!policy) {
|
|
2115
|
+
res.status(500).json({ error: '策略加载失败, 当前用硬编码兜底' });
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
2118
|
+
res.json(policy);
|
|
2119
|
+
});
|
|
2120
|
+
// 更新策略 (白名单 / 黑名单)
|
|
2121
|
+
// **仅供人手动调用**, 不会暴露给 AI
|
|
2122
|
+
app.put('/api/self-improve/policy', async (req, res) => {
|
|
2123
|
+
const { writePolicy, auditShellCall } = await import('../agents/shell-guard.js');
|
|
2124
|
+
const newPolicy = req.body;
|
|
2125
|
+
if (!newPolicy || typeof newPolicy !== 'object') {
|
|
2126
|
+
res.status(400).json({ error: 'body 必须是对象' });
|
|
2127
|
+
return;
|
|
2128
|
+
}
|
|
2129
|
+
// 极简校验
|
|
2130
|
+
if (!Array.isArray(newPolicy.commandAllowlist) || !Array.isArray(newPolicy.pathAllowlist) || !Array.isArray(newPolicy.pathDenylist)) {
|
|
2131
|
+
res.status(400).json({ error: 'commandAllowlist/pathAllowlist/pathDenylist 必须是数组' });
|
|
2132
|
+
return;
|
|
2133
|
+
}
|
|
2134
|
+
try {
|
|
2135
|
+
const success = writePolicy(newPolicy);
|
|
2136
|
+
if (success) {
|
|
2137
|
+
auditShellCall('allowed', 'api:PUT:/api/self-improve/policy', [], `人类用户更新策略`);
|
|
2138
|
+
res.json({ ok: true, message: '策略已更新, 60 秒内生效' });
|
|
2139
|
+
}
|
|
2140
|
+
else {
|
|
2141
|
+
res.status(500).json({ error: '写入策略文件失败' });
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
catch (err) {
|
|
2145
|
+
res.status(500).json({ error: err.message });
|
|
2146
|
+
}
|
|
2147
|
+
});
|
|
2148
|
+
// 查看审计日志
|
|
2149
|
+
app.get('/api/self-improve/audit', async (_req, res) => {
|
|
2150
|
+
try {
|
|
2151
|
+
const { POLICY_AUDIT_PATH_PUBLIC } = await import('../agents/shell-guard.js');
|
|
2152
|
+
const fs = await import('fs/promises');
|
|
2153
|
+
const auditPath = POLICY_AUDIT_PATH_PUBLIC;
|
|
2154
|
+
const exists = await fs.stat(auditPath).then(() => true).catch(() => false);
|
|
2155
|
+
if (!exists) {
|
|
2156
|
+
res.json([]);
|
|
2157
|
+
return;
|
|
2158
|
+
}
|
|
2159
|
+
const content = await fs.readFile(auditPath, 'utf-8');
|
|
2160
|
+
const lines = content.split('\n').filter(Boolean).slice(-200); // 最近 200 条
|
|
2161
|
+
const entries = lines.map((l) => {
|
|
2162
|
+
try {
|
|
2163
|
+
return JSON.parse(l);
|
|
2164
|
+
}
|
|
2165
|
+
catch {
|
|
2166
|
+
return { raw: l };
|
|
2167
|
+
}
|
|
2168
|
+
});
|
|
2169
|
+
res.json(entries);
|
|
2170
|
+
}
|
|
2171
|
+
catch (err) {
|
|
2172
|
+
res.status(500).json({ error: err.message });
|
|
2173
|
+
}
|
|
2174
|
+
});
|
|
2175
|
+
// 手动触发 (供前端按钮 / 调试用)
|
|
2176
|
+
app.post('/api/self-improve/trigger', async (req, res) => {
|
|
2177
|
+
const { goal, kind } = req.body || {};
|
|
2178
|
+
const { reportSelfImproveEvent } = await import('../heartbeat/self-improve-bus.js');
|
|
2179
|
+
const result = reportSelfImproveEvent({
|
|
2180
|
+
kind: kind || 'user-requested',
|
|
2181
|
+
details: String(goal || '用户手动触发')
|
|
2182
|
+
});
|
|
2183
|
+
res.json(result);
|
|
2184
|
+
});
|
|
2185
|
+
// 事件历史 (供前端显示 / 调试)
|
|
2186
|
+
app.get('/api/self-improve/history', async (_req, res) => {
|
|
2187
|
+
const { getEventHistory } = await import('../heartbeat/self-improve-bus.js');
|
|
2188
|
+
res.json(getEventHistory());
|
|
2189
|
+
});
|
|
2190
|
+
// 健康检查错误数 ≥ 2 -> 触发自改信号
|
|
2191
|
+
if (healthMonitor) {
|
|
2192
|
+
healthMonitor.startPeriodicCheck(60000, (status) => {
|
|
2193
|
+
const errorCount = Object.values(status.checks)
|
|
2194
|
+
.filter((c) => c.status === 'error').length;
|
|
2195
|
+
if (errorCount >= 2) {
|
|
2196
|
+
import('../heartbeat/self-improve-bus.js').then(({ reportSelfImproveEvent }) => {
|
|
2197
|
+
const failedKeys = Object.entries(status.checks)
|
|
2198
|
+
.filter(([_, c]) => c.status === 'error').map(([k]) => k).join(', ');
|
|
2199
|
+
reportSelfImproveEvent({
|
|
2200
|
+
kind: 'silent-timeout',
|
|
2201
|
+
details: `健康检查有 ${errorCount} 项失败: ${failedKeys}`
|
|
2202
|
+
});
|
|
2203
|
+
});
|
|
2204
|
+
}
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2207
|
+
// 安装自改总线 -> SSE 桥
|
|
2208
|
+
void installSelfImproveHook();
|
|
2209
|
+
return new Promise((resolve) => {
|
|
2210
|
+
server.listen(port, () => {
|
|
2211
|
+
console.log(`Web 服务器启动完成: http://localhost:${port}`);
|
|
2212
|
+
console.log('服务器已监听');
|
|
2213
|
+
// 安装 chat bus -> SSE 桥 (供前端 inbox UI 实时刷新)
|
|
2214
|
+
void installChatBusHook();
|
|
2215
|
+
setInterval(() => {
|
|
2216
|
+
for (const client of sseClients) {
|
|
2217
|
+
client.res.write(': ping\n\n');
|
|
2218
|
+
}
|
|
2219
|
+
}, 30000);
|
|
2220
|
+
resolve({ app, server });
|
|
2221
|
+
});
|
|
2222
|
+
});
|
|
2223
|
+
}
|
|
2224
|
+
function broadcast(data, channelId) {
|
|
2225
|
+
const envelope = { ...data, channelId };
|
|
2226
|
+
const message = `data: ${JSON.stringify(envelope)}\n\n`;
|
|
2227
|
+
console.log(`[broadcast] type=${data.type}, channelId=${channelId}, clients=${sseClients.size}`);
|
|
2228
|
+
for (const client of sseClients) {
|
|
2229
|
+
if (!channelId || client.channelId === channelId) {
|
|
2230
|
+
try {
|
|
2231
|
+
client.res.write(message);
|
|
2232
|
+
}
|
|
2233
|
+
catch (e) {
|
|
2234
|
+
console.error(`[broadcast] 写入失败:`, e.message);
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
// ============================================================================
|
|
2240
|
+
// Chat 事件总线 -> SSE 桥 (供前端 inbox UI 用)
|
|
2241
|
+
// ============================================================================
|
|
2242
|
+
let chatBusHookInstalled = false;
|
|
2243
|
+
async function installChatBusHook() {
|
|
2244
|
+
if (chatBusHookInstalled)
|
|
2245
|
+
return;
|
|
2246
|
+
chatBusHookInstalled = true;
|
|
2247
|
+
try {
|
|
2248
|
+
const { chatEventBus } = await import('../agents/p2p-chat-tools.js');
|
|
2249
|
+
chatEventBus.on('chat', (ev) => {
|
|
2250
|
+
// 推送给所有 SSE 客户端 (channelId 留空 = 广播)
|
|
2251
|
+
broadcast({ type: 'chat_event', chatKind: ev.kind, payload: ev }, undefined);
|
|
2252
|
+
});
|
|
2253
|
+
console.log('[chat-bus] SSE bridge installed');
|
|
2254
|
+
}
|
|
2255
|
+
catch (e) {
|
|
2256
|
+
console.warn('[chat-bus] install failed:', e.message);
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
// ============================================================================
|
|
2260
|
+
// Self-Improve Bus -> SSE 桥 (供前端 / 用户看到自改触发)
|
|
2261
|
+
// ============================================================================
|
|
2262
|
+
let selfImproveHookInstalled = false;
|
|
2263
|
+
async function installSelfImproveHook() {
|
|
2264
|
+
if (selfImproveHookInstalled)
|
|
2265
|
+
return;
|
|
2266
|
+
selfImproveHookInstalled = true;
|
|
2267
|
+
try {
|
|
2268
|
+
const { onSelfImproveTrigger } = await import('../heartbeat/self-improve-bus.js');
|
|
2269
|
+
const { runSelfImproveLoop } = await import('../agents/pi-sdk.js');
|
|
2270
|
+
// 监听自改事件 -> 跑循环 + 广播到前端
|
|
2271
|
+
onSelfImproveTrigger(async (event, goal) => {
|
|
2272
|
+
broadcast({
|
|
2273
|
+
type: 'self_improve_triggered',
|
|
2274
|
+
eventKind: event.kind,
|
|
2275
|
+
details: event.details,
|
|
2276
|
+
goal,
|
|
2277
|
+
ts: Date.now()
|
|
2278
|
+
}, undefined);
|
|
2279
|
+
// 实际跑循环 (创分支等)
|
|
2280
|
+
const result = await runSelfImproveLoop(goal);
|
|
2281
|
+
broadcast({
|
|
2282
|
+
type: 'self_improve_result',
|
|
2283
|
+
success: result.success,
|
|
2284
|
+
output: result.output,
|
|
2285
|
+
error: result.error,
|
|
2286
|
+
ts: Date.now()
|
|
2287
|
+
}, undefined);
|
|
2288
|
+
});
|
|
2289
|
+
console.log('[self-improve] SSE bridge installed');
|
|
2290
|
+
}
|
|
2291
|
+
catch (e) {
|
|
2292
|
+
console.warn('[self-improve] install failed:', e.message);
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
function getUserName() {
|
|
2296
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
2297
|
+
const match = home.match(/\/Users\/(\w+)/);
|
|
2298
|
+
if (match)
|
|
2299
|
+
return match[1];
|
|
2300
|
+
const user = process.env.USERNAME || process.env.USER || 'user';
|
|
2301
|
+
return user.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
2302
|
+
}
|
|
2303
|
+
export async function bootstrapIdentity() {
|
|
2304
|
+
console.log('🔐 身份生成...');
|
|
2305
|
+
const kp = KeyManager.generate();
|
|
2306
|
+
const did = kp.did;
|
|
2307
|
+
const username = getUserName();
|
|
2308
|
+
const suffix = did.split(':').pop()?.substring(0, 4);
|
|
2309
|
+
const name = `blln-${username}-${suffix}`;
|
|
2310
|
+
console.log(` DID: ${did.substring(0, 30)}...`);
|
|
2311
|
+
return { keypair: kp, did, name };
|
|
2312
|
+
}
|
|
2313
|
+
export function publishDIDBackground(name, kp) {
|
|
2314
|
+
console.log('📝 IPNS注册(后台)...');
|
|
2315
|
+
let retries = 0;
|
|
2316
|
+
const attempt = async () => {
|
|
2317
|
+
try {
|
|
2318
|
+
const auth = await AgentAuthManager.newWithRemoteIpfs('http://127.0.0.1:5001', 'http://127.0.0.1:8080');
|
|
2319
|
+
await auth.registerAgent({ name, services: [] }, kp, '');
|
|
2320
|
+
console.log('✅ IPNS注册成功');
|
|
2321
|
+
}
|
|
2322
|
+
catch (e) {
|
|
2323
|
+
retries++;
|
|
2324
|
+
if (retries < 10) {
|
|
2325
|
+
setTimeout(attempt, 60000);
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
};
|
|
2329
|
+
setTimeout(attempt, 100);
|
|
2330
|
+
}
|
|
2331
|
+
export async function bootstrapP2P(verifier) {
|
|
2332
|
+
console.log('🌐 P2P连接...');
|
|
2333
|
+
const rawSeed = crypto.getRandomValues(new Uint8Array(32));
|
|
2334
|
+
const comm = createHyperswarmCommunicator({ server: true, client: true, autoConnect: true, maxConnections: 50, seed: rawSeed });
|
|
2335
|
+
await comm.start();
|
|
2336
|
+
const topic = createTopic('bolloon-agent-harness');
|
|
2337
|
+
await comm.joinTopic(topic);
|
|
2338
|
+
console.log(' P2P已就绪');
|
|
2339
|
+
return comm;
|
|
2340
|
+
}
|
|
2341
|
+
export async function openBrowser(url) {
|
|
2342
|
+
const { exec } = await import('child_process');
|
|
2343
|
+
const { platform } = await import('os');
|
|
2344
|
+
const p = platform();
|
|
2345
|
+
let cmd;
|
|
2346
|
+
if (p === 'darwin') {
|
|
2347
|
+
cmd = `open ${url}`;
|
|
2348
|
+
}
|
|
2349
|
+
else if (p === 'win32') {
|
|
2350
|
+
cmd = `start ${url}`;
|
|
2351
|
+
}
|
|
2352
|
+
else {
|
|
2353
|
+
cmd = `xdg-open ${url}`;
|
|
2354
|
+
}
|
|
2355
|
+
exec(cmd, (err) => {
|
|
2356
|
+
if (err) {
|
|
2357
|
+
console.error('打开浏览器失败:', err.message);
|
|
2358
|
+
}
|
|
2359
|
+
});
|
|
2360
|
+
}
|