@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
package/dist/web/client.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// marked 库可能从 CDN 加载失败, 这里做安全降级 (避免 ReferenceError 让 addMessage 整体崩溃)
|
|
2
|
+
if (typeof marked === 'undefined') {
|
|
3
|
+
window.marked = { parse: (text) => String(text).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br>') };
|
|
4
|
+
}
|
|
5
|
+
|
|
1
6
|
const messagesEl = document.getElementById('messages');
|
|
2
7
|
const input = document.getElementById('input');
|
|
3
8
|
const sendBtn = document.getElementById('send');
|
|
@@ -10,7 +15,7 @@ const newChannelInput = document.getElementById('new-channel-input');
|
|
|
10
15
|
const channelNameEl = document.getElementById('channel-name');
|
|
11
16
|
const loadSessionBtn = document.getElementById('load-session-btn');
|
|
12
17
|
const sessionFileInput = document.getElementById('session-file-input');
|
|
13
|
-
const newSessionBtn = document.getElementById('new-session-btn');
|
|
18
|
+
const newSessionBtn = document.getElementById('new-session-btn'); // 兼容旧引用(右上角按钮已移除)
|
|
14
19
|
|
|
15
20
|
let eventSources = new Map(); // channelId -> EventSource
|
|
16
21
|
let currentChannelId = null;
|
|
@@ -19,11 +24,13 @@ let channels = [];
|
|
|
19
24
|
let isSidebarCollapsed = false;
|
|
20
25
|
let reconnectAttempts = new Map(); // channelId -> attempts
|
|
21
26
|
let reconnectTimers = new Map(); // channelId -> timer
|
|
27
|
+
let heartbeatTimers = new Map(); // channelId -> setInterval handle (防止泄漏)
|
|
22
28
|
let lastUserCommand = ''; // 防止用户消息重复显示
|
|
23
29
|
let lastAiContent = ''; // 防止 AI 消息重复显示
|
|
24
30
|
let messagesContainers = new Map(); // channelId -> messages container div
|
|
25
31
|
let sessionMessages = new Map(); // channelId:sessionId -> messages array
|
|
26
32
|
let currentSessionId = null; // 当前显示的 session ID
|
|
33
|
+
let expandedAgents = new Set(); // 当前展开的 agent(channel) id 集合
|
|
27
34
|
|
|
28
35
|
function generateId() {
|
|
29
36
|
return crypto.randomUUID();
|
|
@@ -112,21 +119,13 @@ async function createChannel(name) {
|
|
|
112
119
|
channels.push(channel);
|
|
113
120
|
renderChannels();
|
|
114
121
|
selectChannel(channel.id);
|
|
115
|
-
newChannelInput.value = '';
|
|
122
|
+
if (newChannelInput) newChannelInput.value = '';
|
|
116
123
|
|
|
117
124
|
// 后台更新 DID(如果还没有的话)
|
|
118
125
|
if (!channel.did || channel.did === 'undefined') {
|
|
119
126
|
console.log('[创建频道] 后台生成 DID...');
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
fetch('/channels').then(res => res.json()).then(allChannels => {
|
|
123
|
-
channels = allChannels;
|
|
124
|
-
const updated = channels.find(c => c.id === channel.id);
|
|
125
|
-
if (updated && updated.did && updated.did !== 'undefined') {
|
|
126
|
-
console.log('[创建频道] DID 生成完成:', updated.did);
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
}, 2000);
|
|
127
|
+
// 复用全局 channelRefreshTimer, 把多个刷新请求合并成一个 1.5s 后的请求
|
|
128
|
+
scheduleChannelsRefresh();
|
|
130
129
|
}
|
|
131
130
|
} catch (err) {
|
|
132
131
|
console.error('Failed to create channel:', err);
|
|
@@ -135,15 +134,25 @@ async function createChannel(name) {
|
|
|
135
134
|
|
|
136
135
|
async function deleteChannel(channelId, e) {
|
|
137
136
|
e.stopPropagation();
|
|
137
|
+
if (!confirm('确定要删除该智能体及其所有会话吗?此操作不可撤销。')) return;
|
|
138
138
|
try {
|
|
139
139
|
await fetch(`/channels/${channelId}`, { method: 'DELETE' });
|
|
140
140
|
channels = channels.filter(c => c.id !== channelId);
|
|
141
|
+
expandedAgents.delete(channelId);
|
|
142
|
+
|
|
143
|
+
// 释放浏览器侧引用 + DOM, 避免长时间使用后内存累积
|
|
144
|
+
cleanupChannelState(channelId);
|
|
145
|
+
|
|
141
146
|
if (currentChannelId === channelId) {
|
|
142
147
|
currentChannelId = channels[0]?.id || null;
|
|
148
|
+
currentSessionId = null;
|
|
143
149
|
if (currentChannelId) {
|
|
144
|
-
|
|
150
|
+
const ch = channels.find(c => c.id === currentChannelId);
|
|
151
|
+
if (channelNameEl) channelNameEl.textContent = ch?.name || 'Bolloon Agent';
|
|
152
|
+
await selectChannel(currentChannelId);
|
|
145
153
|
} else {
|
|
146
154
|
messagesEl.innerHTML = '';
|
|
155
|
+
if (channelNameEl) channelNameEl.textContent = 'Bolloon Agent';
|
|
147
156
|
}
|
|
148
157
|
}
|
|
149
158
|
renderChannels();
|
|
@@ -153,6 +162,38 @@ async function deleteChannel(channelId, e) {
|
|
|
153
162
|
}
|
|
154
163
|
}
|
|
155
164
|
|
|
165
|
+
/** 释放一个 channel 在浏览器侧占用的所有资源 (DOM 容器, SSE, 缓存 session 消息) */
|
|
166
|
+
function cleanupChannelState(channelId) {
|
|
167
|
+
// 1. SSE 连接
|
|
168
|
+
if (eventSources.has(channelId)) {
|
|
169
|
+
try { eventSources.get(channelId).close(); } catch {}
|
|
170
|
+
eventSources.delete(channelId);
|
|
171
|
+
}
|
|
172
|
+
// 2. 心跳 + 重连 timer
|
|
173
|
+
if (heartbeatTimers.has(channelId)) {
|
|
174
|
+
clearInterval(heartbeatTimers.get(channelId));
|
|
175
|
+
heartbeatTimers.delete(channelId);
|
|
176
|
+
}
|
|
177
|
+
if (reconnectTimers.has(channelId)) {
|
|
178
|
+
clearTimeout(reconnectTimers.get(channelId));
|
|
179
|
+
reconnectTimers.delete(channelId);
|
|
180
|
+
}
|
|
181
|
+
reconnectAttempts.delete(channelId);
|
|
182
|
+
// 3. 消息容器 DOM — 真从 #messages 里移除, 不只是隐藏
|
|
183
|
+
const container = messagesContainers.get(channelId);
|
|
184
|
+
if (container && container.parentNode) {
|
|
185
|
+
container.parentNode.removeChild(container);
|
|
186
|
+
}
|
|
187
|
+
messagesContainers.delete(channelId);
|
|
188
|
+
// 4. 缓存的所有 session 消息 (按 channel:session 索引)
|
|
189
|
+
const prefix = `${channelId}:`;
|
|
190
|
+
for (const key of sessionMessages.keys()) {
|
|
191
|
+
if (key === channelId || key.startsWith(prefix)) {
|
|
192
|
+
sessionMessages.delete(key);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
156
197
|
async function createNewSession() {
|
|
157
198
|
if (!currentChannelId) {
|
|
158
199
|
console.log('[新会话] 没有选中的频道');
|
|
@@ -160,18 +201,7 @@ async function createNewSession() {
|
|
|
160
201
|
}
|
|
161
202
|
try {
|
|
162
203
|
// 保存当前 session 的消息
|
|
163
|
-
|
|
164
|
-
if (channel && currentSessionId) {
|
|
165
|
-
const container = messagesContainers.get(currentChannelId);
|
|
166
|
-
if (container) {
|
|
167
|
-
const messages = Array.from(container.querySelectorAll('.message')).map(msg => ({
|
|
168
|
-
type: msg.classList.contains('message-user') ? 'user' : 'ai',
|
|
169
|
-
content: msg.querySelector('.message-content')?.textContent || ''
|
|
170
|
-
}));
|
|
171
|
-
sessionMessages.set(`${currentChannelId}:${currentSessionId}`, messages);
|
|
172
|
-
console.log('[新会话] 保存旧 session 消息:', messages.length);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
204
|
+
saveCurrentSessionMessages();
|
|
175
205
|
|
|
176
206
|
const res = await fetch(`/channels/${currentChannelId}/sessions`, {
|
|
177
207
|
method: 'POST'
|
|
@@ -180,6 +210,7 @@ async function createNewSession() {
|
|
|
180
210
|
console.log('[新会话] 创建成功:', data);
|
|
181
211
|
|
|
182
212
|
// 更新本地频道数据
|
|
213
|
+
const channel = channels.find(c => c.id === currentChannelId);
|
|
183
214
|
if (channel) {
|
|
184
215
|
if (!channel.sessions) channel.sessions = [];
|
|
185
216
|
channel.sessions.push(data.session);
|
|
@@ -187,7 +218,6 @@ async function createNewSession() {
|
|
|
187
218
|
}
|
|
188
219
|
|
|
189
220
|
// 切换到新 session
|
|
190
|
-
const oldSessionId = currentSessionId;
|
|
191
221
|
currentSessionId = data.currentSessionId;
|
|
192
222
|
|
|
193
223
|
// 清空容器并加载新 session
|
|
@@ -198,41 +228,345 @@ async function createNewSession() {
|
|
|
198
228
|
addMessage('你好!新会话已开始,有什么我可以帮你的吗?', 'ai', false, container);
|
|
199
229
|
}
|
|
200
230
|
|
|
231
|
+
// 展开当前智能体,刷新侧边栏让新会话显示出来
|
|
232
|
+
expandedAgents.add(currentChannelId);
|
|
233
|
+
renderChannels();
|
|
234
|
+
|
|
201
235
|
console.log('[新会话] 已切换到:', data.currentSessionId);
|
|
202
236
|
} catch (err) {
|
|
203
237
|
console.error('Failed to create new session:', err);
|
|
204
238
|
}
|
|
205
239
|
}
|
|
206
240
|
|
|
241
|
+
async function createNewSessionForChannel(channelId, e) {
|
|
242
|
+
if (e) e.stopPropagation();
|
|
243
|
+
if (!channelId) return;
|
|
244
|
+
|
|
245
|
+
// 给自己创建:复用统一的 createNewSession
|
|
246
|
+
if (channelId === currentChannelId) {
|
|
247
|
+
if (currentSessionId) saveCurrentSessionMessages();
|
|
248
|
+
await createNewSession();
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// 给别的智能体创建:后端建好后直接 re-fetch 一次保持本地与后端一致
|
|
253
|
+
try {
|
|
254
|
+
const res = await fetch(`/channels/${channelId}/sessions`, { method: 'POST' });
|
|
255
|
+
if (!res.ok) throw new Error('create session failed');
|
|
256
|
+
const data = await res.json();
|
|
257
|
+
const channel = channels.find(c => c.id === channelId);
|
|
258
|
+
if (channel) {
|
|
259
|
+
if (!channel.sessions) channel.sessions = [];
|
|
260
|
+
channel.sessions.push(data.session);
|
|
261
|
+
channel.currentSessionId = data.currentSessionId;
|
|
262
|
+
}
|
|
263
|
+
expandedAgents.add(channelId);
|
|
264
|
+
renderChannels();
|
|
265
|
+
} catch (err) {
|
|
266
|
+
console.error('Failed to create new session:', err);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function switchSession(channelId, sessionId, e) {
|
|
271
|
+
if (e) e.stopPropagation();
|
|
272
|
+
if (!channelId || !sessionId) return;
|
|
273
|
+
if (channelId === currentChannelId && sessionId === currentSessionId) return;
|
|
274
|
+
|
|
275
|
+
// 先保存当前 session 的本地消息
|
|
276
|
+
if (currentChannelId && currentSessionId) {
|
|
277
|
+
saveCurrentSessionMessages();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
const res = await fetch(`/channels/${channelId}/sessions/${sessionId}/switch`, { method: 'POST' });
|
|
282
|
+
if (!res.ok) throw new Error('switch failed');
|
|
283
|
+
const channel = channels.find(c => c.id === channelId);
|
|
284
|
+
if (channel) {
|
|
285
|
+
channel.currentSessionId = sessionId;
|
|
286
|
+
await saveChannels();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// 切换到目标 agent + session
|
|
290
|
+
await selectChannel(channelId, sessionId);
|
|
291
|
+
renderChannels();
|
|
292
|
+
} catch (err) {
|
|
293
|
+
console.error('Failed to switch session:', err);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function deleteSession(channelId, sessionId, e) {
|
|
298
|
+
if (e) e.stopPropagation();
|
|
299
|
+
if (!confirm('确定要删除该会话吗?此操作不可撤销。')) return;
|
|
300
|
+
try {
|
|
301
|
+
const res = await fetch(`/channels/${channelId}/sessions/${sessionId}`, { method: 'DELETE' });
|
|
302
|
+
if (!res.ok) {
|
|
303
|
+
const err = await res.json().catch(() => ({}));
|
|
304
|
+
alert(err.error || '删除失败');
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const data = await res.json();
|
|
308
|
+
const channel = channels.find(c => c.id === channelId);
|
|
309
|
+
if (channel) {
|
|
310
|
+
if (channel.sessions) {
|
|
311
|
+
channel.sessions = channel.sessions.filter(s => s.id !== sessionId);
|
|
312
|
+
}
|
|
313
|
+
if (data.currentSessionId) {
|
|
314
|
+
channel.currentSessionId = data.currentSessionId;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// 如果删的是当前打开的会话,切换到新的当前会话
|
|
319
|
+
if (channelId === currentChannelId && sessionId === currentSessionId) {
|
|
320
|
+
if (data.currentSessionId) {
|
|
321
|
+
currentSessionId = data.currentSessionId;
|
|
322
|
+
const container = messagesContainers.get(channelId);
|
|
323
|
+
if (container) container.innerHTML = '';
|
|
324
|
+
await loadSession(channelId);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
renderChannels();
|
|
328
|
+
} catch (err) {
|
|
329
|
+
console.error('Failed to delete session:', err);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
let _saveSessionMessagesDirty = false;
|
|
334
|
+
let _saveSessionMessagesTimer = null;
|
|
335
|
+
function saveCurrentSessionMessages() {
|
|
336
|
+
if (!currentChannelId || !currentSessionId) return;
|
|
337
|
+
// 内存保护: 多次快速调用合并成一个, 避免在切会话时反复 .textContent 读 DOM
|
|
338
|
+
// (每个 textContent 会序列化整棵子树, 200 条消息 = 几百 MB 临时字符串)
|
|
339
|
+
_saveSessionMessagesDirty = true;
|
|
340
|
+
if (_saveSessionMessagesTimer) return;
|
|
341
|
+
_saveSessionMessagesTimer = setTimeout(() => {
|
|
342
|
+
_saveSessionMessagesTimer = null;
|
|
343
|
+
if (!_saveSessionMessagesDirty) return;
|
|
344
|
+
_saveSessionMessagesDirty = false;
|
|
345
|
+
if (!currentChannelId || !currentSessionId) return;
|
|
346
|
+
const container = messagesContainers.get(currentChannelId);
|
|
347
|
+
if (!container) return;
|
|
348
|
+
const messages = Array.from(container.querySelectorAll('.message')).map(msg => ({
|
|
349
|
+
type: msg.classList.contains('message-user') ? 'user' : 'ai',
|
|
350
|
+
content: msg.querySelector('.message-content')?.textContent || ''
|
|
351
|
+
}));
|
|
352
|
+
if (messages.length > 0) {
|
|
353
|
+
sessionMessages.set(`${currentChannelId}:${currentSessionId}`, messages);
|
|
354
|
+
}
|
|
355
|
+
}, 50);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function saveChannels() {
|
|
359
|
+
// 简单地 re-fetch,保持本地 channels 与服务端一致
|
|
360
|
+
// 改成走 scheduleChannelsRefresh, 多个调用合并成一个请求 — 减少内存峰值和后端压力
|
|
361
|
+
scheduleChannelsRefresh();
|
|
362
|
+
await new Promise(r => setTimeout(r, 600));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
let channelRefreshTimer = null;
|
|
366
|
+
let channelRefreshInFlight = null;
|
|
367
|
+
function scheduleChannelsRefresh() {
|
|
368
|
+
if (channelRefreshTimer) return;
|
|
369
|
+
channelRefreshTimer = setTimeout(async () => {
|
|
370
|
+
channelRefreshTimer = null;
|
|
371
|
+
if (channelRefreshInFlight) return channelRefreshInFlight;
|
|
372
|
+
channelRefreshInFlight = (async () => {
|
|
373
|
+
try {
|
|
374
|
+
const res = await fetch('/channels');
|
|
375
|
+
if (res.ok) {
|
|
376
|
+
const fresh = await res.json();
|
|
377
|
+
channels = fresh;
|
|
378
|
+
renderChannels();
|
|
379
|
+
}
|
|
380
|
+
} catch (err) {
|
|
381
|
+
console.error('Failed to re-fetch channels:', err);
|
|
382
|
+
} finally {
|
|
383
|
+
channelRefreshInFlight = null;
|
|
384
|
+
}
|
|
385
|
+
})();
|
|
386
|
+
return channelRefreshInFlight;
|
|
387
|
+
}, 800);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function toggleAgentExpand(channelId, e) {
|
|
391
|
+
if (e) e.stopPropagation();
|
|
392
|
+
if (expandedAgents.has(channelId)) {
|
|
393
|
+
expandedAgents.delete(channelId);
|
|
394
|
+
} else {
|
|
395
|
+
expandedAgents.add(channelId);
|
|
396
|
+
}
|
|
397
|
+
renderChannels();
|
|
398
|
+
}
|
|
399
|
+
|
|
207
400
|
function renderChannels() {
|
|
208
401
|
if (!channelList) return;
|
|
209
402
|
channelList.innerHTML = '';
|
|
210
403
|
|
|
211
|
-
// 使用 DocumentFragment 减少 DOM 操作
|
|
212
404
|
const fragment = document.createDocumentFragment();
|
|
213
405
|
|
|
406
|
+
// 滚动可见性监听只绑定一次 (channelList 是同一个 DOM 节点,
|
|
407
|
+
// renderChannels 每次清空 innerHTML 都会重渲, 不能重复 addEventListener)
|
|
408
|
+
if (!channelList._scrollListenersBound) {
|
|
409
|
+
const onUserScroll = () => {
|
|
410
|
+
channelList.classList.add('is-scrolling');
|
|
411
|
+
if (channelList._scrollIdleTimer) clearTimeout(channelList._scrollIdleTimer);
|
|
412
|
+
channelList._scrollIdleTimer = setTimeout(() => {
|
|
413
|
+
channelList.classList.remove('is-scrolling');
|
|
414
|
+
}, 1200);
|
|
415
|
+
};
|
|
416
|
+
channelList.addEventListener('wheel', onUserScroll, { passive: true });
|
|
417
|
+
channelList.addEventListener('touchmove', onUserScroll, { passive: true });
|
|
418
|
+
channelList.addEventListener('keydown', (ev) => {
|
|
419
|
+
if (['ArrowUp', 'ArrowDown', 'PageUp', 'PageDown', 'Home', 'End'].includes(ev.key)) {
|
|
420
|
+
onUserScroll();
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
channelList._scrollListenersBound = true;
|
|
424
|
+
}
|
|
425
|
+
|
|
214
426
|
channels.forEach(ch => {
|
|
215
427
|
const li = document.createElement('li');
|
|
216
|
-
|
|
217
|
-
li.
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
428
|
+
const isExpanded = expandedAgents.has(ch.id);
|
|
429
|
+
li.className = `agent-group ${isExpanded ? 'expanded' : ''}`;
|
|
430
|
+
li.dataset.channelId = ch.id;
|
|
431
|
+
|
|
432
|
+
// --- 智能体行 ---
|
|
433
|
+
const row = document.createElement('div');
|
|
434
|
+
row.className = `agent-row ${ch.id === currentChannelId ? 'active' : ''}`;
|
|
435
|
+
|
|
436
|
+
// 找到当前智能体(如果它是激活的)的当前 session
|
|
437
|
+
const currentSess = (ch.id === currentChannelId && Array.isArray(ch.sessions))
|
|
438
|
+
? ch.sessions.find(s => s.id === ch.currentSessionId)
|
|
439
|
+
: null;
|
|
440
|
+
const currentSessLabel = currentSess ? formatSessionName(currentSess) : '';
|
|
441
|
+
const sessionCount = Array.isArray(ch.sessions) ? ch.sessions.length : 0;
|
|
442
|
+
|
|
443
|
+
const walletBadge = ch.walletAddress
|
|
444
|
+
? `<span class="agent-wallet-badge" title="已绑定钱包: ${escapeHtml(ch.walletAddress)}">⛓</span>`
|
|
445
|
+
: '';
|
|
446
|
+
const toolsBadge = ch.autoInvokeTools
|
|
447
|
+
? `<span class="agent-tools-badge" title="自动工具调用已开启">⚡</span>`
|
|
448
|
+
: '';
|
|
449
|
+
|
|
450
|
+
row.innerHTML = `
|
|
451
|
+
<svg class="agent-caret" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
452
|
+
<polyline points="9 18 15 12 9 6"></polyline>
|
|
453
|
+
</svg>
|
|
222
454
|
<div class="channel-icon">💬</div>
|
|
223
|
-
<span class="channel-name">${ch.name}</span>
|
|
224
|
-
<
|
|
455
|
+
<span class="channel-name" title="${escapeHtml(ch.name)}">${escapeHtml(ch.name)}</span>
|
|
456
|
+
<span class="agent-row-meta">
|
|
457
|
+
${walletBadge}
|
|
458
|
+
${toolsBadge}
|
|
459
|
+
${sessionCount > 1 ? `<span class="agent-session-count" title="${sessionCount} 个会话">${sessionCount}</span>` : ''}
|
|
460
|
+
${currentSessLabel ? `<span class="agent-current-session" title="当前会话:${escapeHtml(currentSessLabel)}">· ${escapeHtml(currentSessLabel)}</span>` : ''}
|
|
461
|
+
<button class="agent-config-btn" title="配置智能体 (钱包 / 工具)">
|
|
462
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
463
|
+
<circle cx="12" cy="12" r="3"></circle>
|
|
464
|
+
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
|
|
465
|
+
</svg>
|
|
466
|
+
</button>
|
|
467
|
+
<button class="channel-delete" title="删除智能体">×</button>
|
|
468
|
+
<button class="agent-new-session" title="新建会话">
|
|
469
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
470
|
+
<line x1="12" y1="5" x2="12" y2="19"></line>
|
|
471
|
+
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
472
|
+
</svg>
|
|
473
|
+
</button>
|
|
474
|
+
</span>
|
|
225
475
|
`;
|
|
226
476
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
477
|
+
// 行点击:切换展开;点击名字/图标区域则切到该智能体
|
|
478
|
+
row.addEventListener('click', (ev) => {
|
|
479
|
+
// 如果点在删除/新会话/配置按钮上, 单独处理
|
|
480
|
+
if (ev.target.closest('.channel-delete')
|
|
481
|
+
|| ev.target.closest('.agent-new-session')
|
|
482
|
+
|| ev.target.closest('.agent-config-btn')) return;
|
|
483
|
+
if (ev.target.closest('.agent-caret')) {
|
|
484
|
+
toggleAgentExpand(ch.id, ev);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
toggleAgentExpand(ch.id, ev);
|
|
488
|
+
if (ch.id !== currentChannelId) {
|
|
489
|
+
expandSidebar();
|
|
490
|
+
selectChannel(ch.id);
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// 智能体删除
|
|
495
|
+
row.querySelector('.channel-delete').addEventListener('click', (ev) => deleteChannel(ch.id, ev));
|
|
496
|
+
// 新会话按钮
|
|
497
|
+
row.querySelector('.agent-new-session').addEventListener('click', (ev) => createNewSessionForChannel(ch.id, ev));
|
|
498
|
+
// 配置按钮: 打开同一个 modal 编辑已有智能体
|
|
499
|
+
row.querySelector('.agent-config-btn').addEventListener('click', (ev) => {
|
|
500
|
+
ev.stopPropagation();
|
|
501
|
+
openAgentAddModal(ch);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
li.appendChild(row);
|
|
505
|
+
|
|
506
|
+
// --- Session 列表(仅展开时渲染 DOM)---
|
|
507
|
+
const sessionUl = document.createElement('ul');
|
|
508
|
+
sessionUl.className = 'session-list';
|
|
509
|
+
if (isExpanded) {
|
|
510
|
+
const sessions = Array.isArray(ch.sessions) ? ch.sessions : [];
|
|
511
|
+
sessions.forEach(sess => {
|
|
512
|
+
const sessLi = document.createElement('li');
|
|
513
|
+
const isActive = ch.id === currentChannelId && sess.id === ch.currentSessionId;
|
|
514
|
+
sessLi.className = `session-item ${isActive ? 'active' : ''}`;
|
|
515
|
+
sessLi.innerHTML = `
|
|
516
|
+
<span class="session-name" title="${escapeHtml(formatSessionName(sess))}">${escapeHtml(formatSessionName(sess))}</span>
|
|
517
|
+
<button class="session-delete" title="删除会话">×</button>
|
|
518
|
+
`;
|
|
519
|
+
sessLi.addEventListener('click', (ev) => {
|
|
520
|
+
if (ev.target.closest('.session-delete')) return;
|
|
521
|
+
switchSession(ch.id, sess.id, ev);
|
|
522
|
+
});
|
|
523
|
+
sessLi.querySelector('.session-delete').addEventListener('click', (ev) => deleteSession(ch.id, sess.id, ev));
|
|
524
|
+
sessionUl.appendChild(sessLi);
|
|
525
|
+
});
|
|
230
526
|
}
|
|
527
|
+
li.appendChild(sessionUl);
|
|
231
528
|
|
|
232
529
|
fragment.appendChild(li);
|
|
233
530
|
});
|
|
234
531
|
|
|
235
532
|
channelList.appendChild(fragment);
|
|
533
|
+
|
|
534
|
+
// header 钱包徽章计数: 只在 channels 变化时刷新, 避免每次 renderChannels 都重算
|
|
535
|
+
refreshWalletBadge();
|
|
536
|
+
|
|
537
|
+
// 把当前激活的 channel 平滑滚到视口内 — 用户切换后不会看不到
|
|
538
|
+
// 只在非用户主动滚动状态下执行, 避免与正在进行的滚动冲突
|
|
539
|
+
if (currentChannelId) {
|
|
540
|
+
requestAnimationFrame(() => scrollActiveChannelIntoView(false));
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** 把当前激活的 channel 滚到侧边栏视口内 */
|
|
545
|
+
function scrollActiveChannelIntoView(smooth = true) {
|
|
546
|
+
if (!channelList || !currentChannelId) return;
|
|
547
|
+
const active = channelList.querySelector(`.agent-group[data-channel-id="${currentChannelId}"]`);
|
|
548
|
+
if (!active) return;
|
|
549
|
+
const listRect = channelList.getBoundingClientRect();
|
|
550
|
+
const itemRect = active.getBoundingClientRect();
|
|
551
|
+
const margin = 24; // 视口上下各留 24px
|
|
552
|
+
if (itemRect.top < listRect.top + margin) {
|
|
553
|
+
channelList.scrollBy({ top: itemRect.top - listRect.top - margin, behavior: smooth ? 'smooth' : 'auto' });
|
|
554
|
+
} else if (itemRect.bottom > listRect.bottom - margin) {
|
|
555
|
+
channelList.scrollBy({ top: itemRect.bottom - listRect.bottom + margin, behavior: smooth ? 'smooth' : 'auto' });
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function formatSessionName(sess) {
|
|
560
|
+
if (!sess) return '新会话';
|
|
561
|
+
if (sess.preview && sess.preview.trim()) return sess.preview.trim();
|
|
562
|
+
const id = sess.id || '';
|
|
563
|
+
return id ? `会话 ${id.slice(-6)}` : '新会话';
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function escapeHtml(s) {
|
|
567
|
+
return String(s ?? '').replace(/[&<>"']/g, (c) => ({
|
|
568
|
+
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
|
569
|
+
}[c]));
|
|
236
570
|
}
|
|
237
571
|
|
|
238
572
|
function renderCollapsedChannels() {
|
|
@@ -252,7 +586,7 @@ function ensureMessageContainer(channelId) {
|
|
|
252
586
|
}
|
|
253
587
|
|
|
254
588
|
function showChannelView(channelId) {
|
|
255
|
-
// Hide all channel message containers
|
|
589
|
+
// Hide all channel message containers (不要 innerHTML='' 销毁, 保留以便快速切换)
|
|
256
590
|
messagesContainers.forEach((container, cid) => {
|
|
257
591
|
container.style.display = 'none';
|
|
258
592
|
});
|
|
@@ -261,30 +595,10 @@ function showChannelView(channelId) {
|
|
|
261
595
|
if (container) {
|
|
262
596
|
container.style.display = 'block';
|
|
263
597
|
}
|
|
264
|
-
// Update messagesEl reference for functions that use it directly
|
|
265
|
-
messagesEl.innerHTML = '';
|
|
266
|
-
if (container) {
|
|
267
|
-
messagesEl.appendChild(container);
|
|
268
|
-
}
|
|
269
598
|
}
|
|
270
599
|
|
|
271
|
-
async function selectChannel(channelId) {
|
|
272
|
-
console.log('[selectChannel] 开始切换到:', channelId);
|
|
273
|
-
|
|
274
|
-
// 保存当前 session 的消息
|
|
275
|
-
if (currentChannelId && currentSessionId) {
|
|
276
|
-
const container = messagesContainers.get(currentChannelId);
|
|
277
|
-
if (container) {
|
|
278
|
-
const messages = Array.from(container.querySelectorAll('.message')).map(msg => ({
|
|
279
|
-
type: msg.classList.contains('message-user') ? 'user' : 'ai',
|
|
280
|
-
content: msg.querySelector('.message-content')?.textContent || ''
|
|
281
|
-
}));
|
|
282
|
-
if (messages.length > 0) {
|
|
283
|
-
sessionMessages.set(`${currentChannelId}:${currentSessionId}`, messages);
|
|
284
|
-
console.log('[selectChannel] 保存 session 消息:', messages.length);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
600
|
+
async function selectChannel(channelId, targetSessionId = null) {
|
|
601
|
+
console.log('[selectChannel] 开始切换到:', channelId, 'targetSession:', targetSessionId);
|
|
288
602
|
|
|
289
603
|
// 立即更新当前频道 ID
|
|
290
604
|
currentChannelId = channelId;
|
|
@@ -294,7 +608,12 @@ async function selectChannel(channelId) {
|
|
|
294
608
|
const channel = channels.find(c => c.id === channelId);
|
|
295
609
|
if (channel) {
|
|
296
610
|
if (channelNameEl) channelNameEl.textContent = channel.name;
|
|
297
|
-
currentSessionId = channel.currentSessionId || 'default';
|
|
611
|
+
currentSessionId = targetSessionId || channel.currentSessionId || 'default';
|
|
612
|
+
if (targetSessionId) {
|
|
613
|
+
channel.currentSessionId = targetSessionId;
|
|
614
|
+
}
|
|
615
|
+
// 自动展开当前智能体的会话列表,让用户能切换会话
|
|
616
|
+
expandedAgents.add(channelId);
|
|
298
617
|
console.log('[selectChannel] 频道:', channel.name, 'session:', currentSessionId);
|
|
299
618
|
}
|
|
300
619
|
|
|
@@ -312,40 +631,30 @@ async function selectChannel(channelId) {
|
|
|
312
631
|
connect(channelId);
|
|
313
632
|
}
|
|
314
633
|
|
|
315
|
-
//
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
}
|
|
325
|
-
} else if (container.innerHTML.trim() === '') {
|
|
326
|
-
// 如果容器是空的,加载 session
|
|
327
|
-
try {
|
|
328
|
-
const res = await fetch(`/sessions/${channelId}`);
|
|
329
|
-
const session = await res.json();
|
|
330
|
-
if (session.messages && session.messages.length > 0) {
|
|
331
|
-
session.messages.forEach(msg => {
|
|
332
|
-
addMessage(msg.content, msg.type, false, container);
|
|
333
|
-
});
|
|
334
|
-
} else {
|
|
335
|
-
addMessage('你好!我是 Bolloon Agent。有什么我可以帮你的吗?', 'ai', false, container);
|
|
336
|
-
}
|
|
337
|
-
} catch (err) {
|
|
338
|
-
console.error('[selectChannel] 加载 session 失败:', err);
|
|
634
|
+
// 直接从 server 拉 session 消息 (container 跨 session 共享, 先清空再加载)
|
|
635
|
+
container.innerHTML = '';
|
|
636
|
+
try {
|
|
637
|
+
const res = await fetch(`/sessions/${channelId}?sessionId=${encodeURIComponent(currentSessionId)}`);
|
|
638
|
+
const session = await res.json();
|
|
639
|
+
if (session.messages && session.messages.length > 0) {
|
|
640
|
+
session.messages.forEach(msg => {
|
|
641
|
+
addMessage(msg.content, msg.type, false, container);
|
|
642
|
+
});
|
|
643
|
+
} else {
|
|
339
644
|
addMessage('你好!我是 Bolloon Agent。有什么我可以帮你的吗?', 'ai', false, container);
|
|
340
645
|
}
|
|
646
|
+
} catch (err) {
|
|
647
|
+
console.error('[selectChannel] 加载 session 失败:', err);
|
|
648
|
+
addMessage('你好!我是 Bolloon Agent。有什么我可以帮你的吗?', 'ai', false, container);
|
|
341
649
|
}
|
|
342
650
|
}
|
|
343
651
|
|
|
344
|
-
async function loadSession(channelId) {
|
|
652
|
+
async function loadSession(channelId, sessionId = null) {
|
|
345
653
|
const container = messagesContainers.get(channelId);
|
|
346
654
|
if (!container) return;
|
|
655
|
+
const targetSessionId = sessionId || currentSessionId || 'default';
|
|
347
656
|
try {
|
|
348
|
-
const res = await fetch(`/sessions/${channelId}`);
|
|
657
|
+
const res = await fetch(`/sessions/${channelId}?sessionId=${encodeURIComponent(targetSessionId)}`);
|
|
349
658
|
const session = await res.json();
|
|
350
659
|
container.innerHTML = '';
|
|
351
660
|
if (session.messages && session.messages.length > 0) {
|
|
@@ -364,6 +673,18 @@ async function loadSession(channelId) {
|
|
|
364
673
|
|
|
365
674
|
function addMessage(content, type, save = true, container) {
|
|
366
675
|
const msgContainer = container || messagesContainers.get(currentChannelId) || messagesEl;
|
|
676
|
+
|
|
677
|
+
// 浏览器侧内存保护: 单个 channel 的消息容器超过 MAX_MESSAGES_PER_CHANNEL
|
|
678
|
+
// 就从最旧的开始淘汰。SSE 流式场景下不淘汰 (save=true 时不裁剪),
|
|
679
|
+
// 因为流消息一般很短; 只对长会话加载 (save=false) 做上限。
|
|
680
|
+
// 上限是 200 条: 大约相当于 100 轮对话, 足够日常使用, 又把 DOM 控制在 5MB 以内.
|
|
681
|
+
if (!save && msgContainer && msgContainer.children.length > 200) {
|
|
682
|
+
const toRemove = msgContainer.children.length - 200;
|
|
683
|
+
for (let i = 0; i < toRemove; i++) {
|
|
684
|
+
const first = msgContainer.firstElementChild;
|
|
685
|
+
if (first) msgContainer.removeChild(first);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
367
688
|
// 去重:只有 save=true 时(来自 SSE)才去重,save=false 时(来自 session 加载)直接显示
|
|
368
689
|
if (save) {
|
|
369
690
|
const lastContent = type === 'user' ? lastUserCommand : lastAiContent;
|
|
@@ -937,6 +1258,12 @@ function connect(channelId) {
|
|
|
937
1258
|
reconnectTimers.delete(targetChannelId);
|
|
938
1259
|
}
|
|
939
1260
|
|
|
1261
|
+
// 清除该频道的心跳定时器 (防止多次调用 connect 导致 setInterval 累积)
|
|
1262
|
+
if (heartbeatTimers.has(targetChannelId)) {
|
|
1263
|
+
clearInterval(heartbeatTimers.get(targetChannelId));
|
|
1264
|
+
heartbeatTimers.delete(targetChannelId);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
940
1267
|
// 关闭该频道的旧连接
|
|
941
1268
|
if (eventSources.has(targetChannelId)) {
|
|
942
1269
|
eventSources.get(targetChannelId).close();
|
|
@@ -958,24 +1285,54 @@ function connect(channelId) {
|
|
|
958
1285
|
reconnectAttempts.set(targetChannelId, 0);
|
|
959
1286
|
};
|
|
960
1287
|
|
|
1288
|
+
// 心跳超时: 如果 60s 没收到任何数据 (含 ping), 强制重建
|
|
1289
|
+
// 覆盖网络半开 / 浏览器没触发 onerror 的情况
|
|
1290
|
+
let lastEventTime = Date.now();
|
|
1291
|
+
const heartbeatTimer = setInterval(() => {
|
|
1292
|
+
if (!eventSources.has(targetChannelId)) {
|
|
1293
|
+
clearInterval(heartbeatTimer);
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
if (Date.now() - lastEventTime > 60000) {
|
|
1297
|
+
console.warn('[SSE] 60s 无数据, 强制重建连接:', targetChannelId);
|
|
1298
|
+
clearInterval(heartbeatTimer);
|
|
1299
|
+
try { eventSource.close(); } catch {}
|
|
1300
|
+
eventSources.delete(targetChannelId);
|
|
1301
|
+
// 退避重连 (有上限)
|
|
1302
|
+
const attempts = (reconnectAttempts.get(targetChannelId) || 0) + 1;
|
|
1303
|
+
reconnectAttempts.set(targetChannelId, attempts);
|
|
1304
|
+
const delay = Math.min(1000 * Math.pow(2, attempts - 1), 15000);
|
|
1305
|
+
const timer = setTimeout(() => connect(targetChannelId), delay);
|
|
1306
|
+
reconnectTimers.set(targetChannelId, timer);
|
|
1307
|
+
}
|
|
1308
|
+
}, 10000);
|
|
1309
|
+
|
|
1310
|
+
// onerror: 不要手动 close — 浏览器 EventSource 会自动重连
|
|
1311
|
+
// 我们只需在 readyState 永久 CLOSED 时 (罕见) 才介入
|
|
961
1312
|
eventSource.onerror = () => {
|
|
962
|
-
console.
|
|
963
|
-
eventSource.
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1313
|
+
console.warn('[SSE] 错误, 浏览器自动重连中:', targetChannelId, 'readyState=', eventSource.readyState);
|
|
1314
|
+
if (eventSource.readyState === EventSource.CLOSED) {
|
|
1315
|
+
// 浏览器放弃重连, 我们接手
|
|
1316
|
+
clearInterval(heartbeatTimer);
|
|
1317
|
+
eventSources.delete(targetChannelId);
|
|
1318
|
+
const attempts = (reconnectAttempts.get(targetChannelId) || 0) + 1;
|
|
1319
|
+
reconnectAttempts.set(targetChannelId, attempts);
|
|
1320
|
+
const delay = Math.min(1000 * Math.pow(2, attempts - 1), 15000);
|
|
1321
|
+
const timer = setTimeout(() => connect(targetChannelId), delay);
|
|
1322
|
+
reconnectTimers.set(targetChannelId, timer);
|
|
1323
|
+
}
|
|
969
1324
|
};
|
|
970
1325
|
|
|
971
1326
|
eventSource.onmessage = (e) => {
|
|
1327
|
+
lastEventTime = Date.now();
|
|
972
1328
|
try {
|
|
973
1329
|
const data = JSON.parse(e.data);
|
|
974
1330
|
const msgChannelId = data.channelId || targetChannelId;
|
|
975
1331
|
console.log('[SSE] 收到消息:', data.type, 'channelId:', msgChannelId);
|
|
976
1332
|
|
|
977
|
-
//
|
|
978
|
-
|
|
1333
|
+
// 路由消息到正确的频道
|
|
1334
|
+
// 只有 envelope.channelId 存在且与目标不同时才丢弃 (空/undefined 视为广播给自己)
|
|
1335
|
+
if (msgChannelId && msgChannelId !== targetChannelId) {
|
|
979
1336
|
console.log('[SSE] 忽略非目标频道消息');
|
|
980
1337
|
return;
|
|
981
1338
|
}
|
|
@@ -1000,6 +1357,11 @@ function connect(channelId) {
|
|
|
1000
1357
|
handleStatusEvent(data, container);
|
|
1001
1358
|
} else if (data.type === 'done') {
|
|
1002
1359
|
hideTyping();
|
|
1360
|
+
// AI 回复完, 把最后一条 ai 消息落盘 (兜底, 避免 server saveSession 漏写)
|
|
1361
|
+
const lastAi = container.querySelector('.message-ai:last-of-type .message-content');
|
|
1362
|
+
if (lastAi) {
|
|
1363
|
+
persistLastMessageToServer('ai', lastAi.textContent || '');
|
|
1364
|
+
}
|
|
1003
1365
|
} else if (data.type === 'renamed') {
|
|
1004
1366
|
const channel = channels.find(c => c.id === data.channelId);
|
|
1005
1367
|
if (channel) {
|
|
@@ -1032,6 +1394,9 @@ async function sendMessage() {
|
|
|
1032
1394
|
input.value = '';
|
|
1033
1395
|
showTyping();
|
|
1034
1396
|
|
|
1397
|
+
// 立即把用户消息落盘, 避免切走再切回时丢失
|
|
1398
|
+
persistLastMessageToServer('user', text);
|
|
1399
|
+
|
|
1035
1400
|
// 获取当前频道的 DID
|
|
1036
1401
|
const channel = channels.find(c => c.id === currentChannelId);
|
|
1037
1402
|
const channelDid = channel?.did || '';
|
|
@@ -1059,6 +1424,21 @@ async function sendMessage() {
|
|
|
1059
1424
|
}
|
|
1060
1425
|
}
|
|
1061
1426
|
|
|
1427
|
+
// 主动落盘: 把当前 channelId/sessionId 最后一条消息 PATCH 到 server
|
|
1428
|
+
// fire-and-forget, 失败只打日志, 不影响 UI
|
|
1429
|
+
function persistLastMessageToServer(type, content) {
|
|
1430
|
+
if (!currentChannelId || !currentSessionId) return;
|
|
1431
|
+
fetch(`/sessions/${currentChannelId}/${currentSessionId}`, {
|
|
1432
|
+
method: 'PATCH',
|
|
1433
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1434
|
+
body: JSON.stringify({
|
|
1435
|
+
message: { type, content, timestamp: new Date().toISOString() }
|
|
1436
|
+
})
|
|
1437
|
+
}).catch(err => {
|
|
1438
|
+
console.warn('[persist] 落盘失败:', err);
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1062
1442
|
sendBtn.addEventListener('click', sendMessage);
|
|
1063
1443
|
input.addEventListener('keydown', (e) => {
|
|
1064
1444
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
@@ -1078,6 +1458,24 @@ if (apiConfigBtn) {
|
|
|
1078
1458
|
});
|
|
1079
1459
|
}
|
|
1080
1460
|
|
|
1461
|
+
// 钱包管理按钮
|
|
1462
|
+
const walletBtn = document.getElementById('wallet-btn');
|
|
1463
|
+
const walletBadge = document.getElementById('wallet-badge');
|
|
1464
|
+
if (walletBtn) {
|
|
1465
|
+
walletBtn.addEventListener('click', openWalletModal);
|
|
1466
|
+
}
|
|
1467
|
+
/** 刷新 header 钱包徽章: 统计已绑定钱包的智能体数 */
|
|
1468
|
+
function refreshWalletBadge() {
|
|
1469
|
+
if (!walletBadge) return;
|
|
1470
|
+
const count = channels.filter(c => c.walletAddress).length;
|
|
1471
|
+
if (count > 0) {
|
|
1472
|
+
walletBadge.textContent = String(count);
|
|
1473
|
+
walletBadge.style.display = '';
|
|
1474
|
+
} else {
|
|
1475
|
+
walletBadge.style.display = 'none';
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1081
1479
|
if (sidebarToggle) {
|
|
1082
1480
|
sidebarToggle.addEventListener('click', toggleSidebar);
|
|
1083
1481
|
}
|
|
@@ -1125,12 +1523,6 @@ if (newChannelBtn) {
|
|
|
1125
1523
|
});
|
|
1126
1524
|
}
|
|
1127
1525
|
|
|
1128
|
-
if (newSessionBtn) {
|
|
1129
|
-
newSessionBtn.addEventListener('click', () => {
|
|
1130
|
-
createNewSession();
|
|
1131
|
-
});
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
1526
|
if (newChannelInput) {
|
|
1135
1527
|
newChannelInput.addEventListener('keydown', (e) => {
|
|
1136
1528
|
if (e.key === 'Enter') {
|
|
@@ -1562,6 +1954,350 @@ connect = async function() {
|
|
|
1562
1954
|
};
|
|
1563
1955
|
};
|
|
1564
1956
|
|
|
1957
|
+
// =====================================================
|
|
1958
|
+
// 钱包管理 (header 钱包按钮 → 全局管理面板)
|
|
1959
|
+
// =====================================================
|
|
1960
|
+
const walletModal = document.getElementById('wallet-modal');
|
|
1961
|
+
const walletModalClose = document.getElementById('wallet-modal-close');
|
|
1962
|
+
const walletBindAddress = document.getElementById('wallet-bind-address');
|
|
1963
|
+
const walletGenerateBtn = document.getElementById('wallet-generate-btn');
|
|
1964
|
+
const walletAutoTools = document.getElementById('wallet-auto-tools');
|
|
1965
|
+
const walletBindBtn = document.getElementById('wallet-bind-btn');
|
|
1966
|
+
const walletUnbindBtn = document.getElementById('wallet-unbind-btn');
|
|
1967
|
+
const walletNewInfo = document.getElementById('wallet-new-info');
|
|
1968
|
+
const walletListEl = document.getElementById('wallet-list');
|
|
1969
|
+
|
|
1970
|
+
/** 本次会话生成的私钥, 仅用于提示, 永不上传 */
|
|
1971
|
+
let walletModalPendingSecret = null;
|
|
1972
|
+
|
|
1973
|
+
function openWalletModal() {
|
|
1974
|
+
if (!walletModal) return;
|
|
1975
|
+
walletModalPendingSecret = null;
|
|
1976
|
+
walletNewInfo.style.display = 'none';
|
|
1977
|
+
walletNewInfo.innerHTML = '';
|
|
1978
|
+
walletBindAddress.value = '';
|
|
1979
|
+
// 用当前 channel 的状态预填
|
|
1980
|
+
const ch = channels.find(c => c.id === currentChannelId);
|
|
1981
|
+
if (ch) {
|
|
1982
|
+
walletBindAddress.value = ch.walletAddress || '';
|
|
1983
|
+
walletAutoTools.checked = !!ch.autoInvokeTools;
|
|
1984
|
+
}
|
|
1985
|
+
renderWalletList();
|
|
1986
|
+
walletModal.classList.add('active');
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
function closeWalletModal() {
|
|
1990
|
+
if (!walletModal) return;
|
|
1991
|
+
walletModal.classList.remove('active');
|
|
1992
|
+
walletModalPendingSecret = null;
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
if (walletModalClose) walletModalClose.addEventListener('click', closeWalletModal);
|
|
1996
|
+
|
|
1997
|
+
if (walletGenerateBtn) {
|
|
1998
|
+
walletGenerateBtn.addEventListener('click', () => {
|
|
1999
|
+
const { address, privateKeyHex } = generateLocalWallet();
|
|
2000
|
+
walletBindAddress.value = address;
|
|
2001
|
+
walletModalPendingSecret = privateKeyHex;
|
|
2002
|
+
walletNewInfo.style.display = 'block';
|
|
2003
|
+
walletNewInfo.innerHTML = `
|
|
2004
|
+
✓ 已生成本地钱包<br>
|
|
2005
|
+
<strong>地址:</strong> <code>${escapeHtml(address)}</code><br>
|
|
2006
|
+
<strong>私钥 (本次会话, 刷新即丢):</strong> <code style="color:#f88;">${escapeHtml(privateKeyHex)}</code><br>
|
|
2007
|
+
<small style="color:#f88;">⚠ 关闭页面后无法找回。仅地址会发送到服务端。</small>
|
|
2008
|
+
`;
|
|
2009
|
+
});
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
if (walletBindBtn) {
|
|
2013
|
+
walletBindBtn.addEventListener('click', async () => {
|
|
2014
|
+
if (!currentChannelId) {
|
|
2015
|
+
alert('请先在侧边栏选择一个智能体');
|
|
2016
|
+
return;
|
|
2017
|
+
}
|
|
2018
|
+
const address = (walletBindAddress.value || '').trim();
|
|
2019
|
+
if (!address) {
|
|
2020
|
+
alert('请输入钱包地址或点击「生成」');
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
2023
|
+
try {
|
|
2024
|
+
const res = await fetch(`/channels/${currentChannelId}`, {
|
|
2025
|
+
method: 'PATCH',
|
|
2026
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2027
|
+
body: JSON.stringify({
|
|
2028
|
+
walletAddress: address,
|
|
2029
|
+
autoInvokeTools: !!walletAutoTools.checked
|
|
2030
|
+
})
|
|
2031
|
+
});
|
|
2032
|
+
if (!res.ok) {
|
|
2033
|
+
const err = await res.json().catch(() => ({}));
|
|
2034
|
+
throw new Error(err.error || 'bind failed');
|
|
2035
|
+
}
|
|
2036
|
+
const updated = await res.json();
|
|
2037
|
+
const idx = channels.findIndex(c => c.id === currentChannelId);
|
|
2038
|
+
if (idx >= 0) channels[idx] = updated;
|
|
2039
|
+
renderChannels();
|
|
2040
|
+
renderWalletList();
|
|
2041
|
+
walletModalPendingSecret = null;
|
|
2042
|
+
} catch (err) {
|
|
2043
|
+
alert('绑定失败: ' + err.message);
|
|
2044
|
+
}
|
|
2045
|
+
});
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
if (walletUnbindBtn) {
|
|
2049
|
+
walletUnbindBtn.addEventListener('click', async () => {
|
|
2050
|
+
if (!currentChannelId) {
|
|
2051
|
+
alert('请先选择一个智能体');
|
|
2052
|
+
return;
|
|
2053
|
+
}
|
|
2054
|
+
if (!confirm('解绑当前智能体的钱包?')) return;
|
|
2055
|
+
try {
|
|
2056
|
+
const res = await fetch(`/channels/${currentChannelId}`, {
|
|
2057
|
+
method: 'PATCH',
|
|
2058
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2059
|
+
body: JSON.stringify({ walletAddress: null })
|
|
2060
|
+
});
|
|
2061
|
+
if (!res.ok) throw new Error('unbind failed');
|
|
2062
|
+
const updated = await res.json();
|
|
2063
|
+
const idx = channels.findIndex(c => c.id === currentChannelId);
|
|
2064
|
+
if (idx >= 0) channels[idx] = updated;
|
|
2065
|
+
walletBindAddress.value = '';
|
|
2066
|
+
renderChannels();
|
|
2067
|
+
renderWalletList();
|
|
2068
|
+
} catch (err) {
|
|
2069
|
+
alert('解绑失败: ' + err.message);
|
|
2070
|
+
}
|
|
2071
|
+
});
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
/** 渲染"所有已绑定钱包"列表 */
|
|
2075
|
+
function renderWalletList() {
|
|
2076
|
+
if (!walletListEl) return;
|
|
2077
|
+
const bound = channels.filter(c => c.walletAddress);
|
|
2078
|
+
if (bound.length === 0) {
|
|
2079
|
+
walletListEl.innerHTML = '<div class="wallet-empty">暂未绑定钱包</div>';
|
|
2080
|
+
return;
|
|
2081
|
+
}
|
|
2082
|
+
// 用 DocumentFragment 避免多次 reflow
|
|
2083
|
+
const frag = document.createDocumentFragment();
|
|
2084
|
+
bound.forEach(ch => {
|
|
2085
|
+
const isActive = ch.id === currentChannelId;
|
|
2086
|
+
const chain = detectChain(ch.walletAddress);
|
|
2087
|
+
const row = document.createElement('div');
|
|
2088
|
+
row.className = 'wallet-row' + (isActive ? ' is-active' : '');
|
|
2089
|
+
row.innerHTML = `
|
|
2090
|
+
<span class="wallet-chain">${escapeHtml(chain)}</span>
|
|
2091
|
+
<div class="wallet-info">
|
|
2092
|
+
<span class="wallet-agent" title="${escapeHtml(ch.name)}">${escapeHtml(ch.name)}</span>
|
|
2093
|
+
<span class="wallet-address" title="${escapeHtml(ch.walletAddress)}">${escapeHtml(ch.walletAddress)}</span>
|
|
2094
|
+
</div>
|
|
2095
|
+
<div class="wallet-actions">
|
|
2096
|
+
<button class="wallet-mini-btn" data-action="copy" data-addr="${escapeHtml(ch.walletAddress)}" title="复制地址">
|
|
2097
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
2098
|
+
<rect x="9" y="9" width="13" height="13" rx="2"></rect>
|
|
2099
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
|
2100
|
+
</svg>
|
|
2101
|
+
</button>
|
|
2102
|
+
<button class="wallet-mini-btn" data-action="goto" data-id="${escapeHtml(ch.id)}" title="切换到该智能体">
|
|
2103
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
2104
|
+
<path d="M5 12h14M12 5l7 7-7 7"></path>
|
|
2105
|
+
</svg>
|
|
2106
|
+
</button>
|
|
2107
|
+
<button class="wallet-mini-btn" data-action="unbind" data-id="${escapeHtml(ch.id)}" title="解绑">
|
|
2108
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
2109
|
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
2110
|
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
2111
|
+
</svg>
|
|
2112
|
+
</button>
|
|
2113
|
+
</div>
|
|
2114
|
+
`;
|
|
2115
|
+
frag.appendChild(row);
|
|
2116
|
+
});
|
|
2117
|
+
walletListEl.innerHTML = '';
|
|
2118
|
+
walletListEl.appendChild(frag);
|
|
2119
|
+
|
|
2120
|
+
// 事件委托: 一次绑定处理三个动作
|
|
2121
|
+
walletListEl.onclick = async (ev) => {
|
|
2122
|
+
const btn = ev.target.closest('button[data-action]');
|
|
2123
|
+
if (!btn) return;
|
|
2124
|
+
const action = btn.dataset.action;
|
|
2125
|
+
if (action === 'copy') {
|
|
2126
|
+
try {
|
|
2127
|
+
await navigator.clipboard.writeText(btn.dataset.addr);
|
|
2128
|
+
btn.style.background = 'var(--accent)';
|
|
2129
|
+
btn.style.color = 'var(--bg)';
|
|
2130
|
+
setTimeout(() => { btn.style.background = ''; btn.style.color = ''; }, 800);
|
|
2131
|
+
} catch {}
|
|
2132
|
+
} else if (action === 'goto') {
|
|
2133
|
+
closeWalletModal();
|
|
2134
|
+
selectChannel(btn.dataset.id);
|
|
2135
|
+
} else if (action === 'unbind') {
|
|
2136
|
+
if (!confirm('解绑该智能体的钱包?')) return;
|
|
2137
|
+
try {
|
|
2138
|
+
const res = await fetch(`/channels/${btn.dataset.id}`, {
|
|
2139
|
+
method: 'PATCH',
|
|
2140
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2141
|
+
body: JSON.stringify({ walletAddress: null })
|
|
2142
|
+
});
|
|
2143
|
+
if (!res.ok) throw new Error('unbind failed');
|
|
2144
|
+
const updated = await res.json();
|
|
2145
|
+
const idx = channels.findIndex(c => c.id === btn.dataset.id);
|
|
2146
|
+
if (idx >= 0) channels[idx] = updated;
|
|
2147
|
+
renderChannels();
|
|
2148
|
+
renderWalletList();
|
|
2149
|
+
if (btn.dataset.id === currentChannelId) walletBindAddress.value = '';
|
|
2150
|
+
} catch (err) {
|
|
2151
|
+
alert('解绑失败: ' + err.message);
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
function detectChain(addr) {
|
|
2158
|
+
if (!addr) return '?';
|
|
2159
|
+
if (/^0x[0-9a-fA-F]{40}$/.test(addr)) return 'EVM';
|
|
2160
|
+
if (/^0x[0-9a-fA-F]{64}$/.test(addr)) return 'SUI';
|
|
2161
|
+
if (/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(addr)) return 'SOL';
|
|
2162
|
+
return '?';
|
|
2163
|
+
}
|
|
2164
|
+
|
|
1565
2165
|
// 启动应用
|
|
1566
2166
|
init();
|
|
1567
2167
|
|
|
2168
|
+
// =====================================================
|
|
2169
|
+
// 智能体目录:catalog add 按钮 + 钱包注册 + 自动工具调用
|
|
2170
|
+
// =====================================================
|
|
2171
|
+
const catalogAddBtn = document.getElementById('catalog-add-btn');
|
|
2172
|
+
const agentAddModal = document.getElementById('agent-add-modal');
|
|
2173
|
+
const agentAddTitle = document.getElementById('agent-add-title');
|
|
2174
|
+
const agentAddModalClose = document.getElementById('agent-add-modal-close');
|
|
2175
|
+
const agentAddName = document.getElementById('agent-add-name');
|
|
2176
|
+
const agentAddWallet = document.getElementById('agent-add-wallet');
|
|
2177
|
+
const agentAddAutoTools = document.getElementById('agent-add-auto-tools');
|
|
2178
|
+
const agentAddConfirmBtn = document.getElementById('agent-add-confirm-btn');
|
|
2179
|
+
const agentAddCancelBtn = document.getElementById('agent-add-cancel-btn');
|
|
2180
|
+
const agentAddWalletInfo = document.getElementById('agent-add-wallet-info');
|
|
2181
|
+
const agentGenerateWalletBtn = document.getElementById('agent-generate-wallet-btn');
|
|
2182
|
+
|
|
2183
|
+
/** 客户端只为提示, 不向服务端发送私钥 */
|
|
2184
|
+
let pendingWalletSecret = null;
|
|
2185
|
+
|
|
2186
|
+
function openAgentAddModal(existingChannel) {
|
|
2187
|
+
if (!agentAddModal) return;
|
|
2188
|
+
if (existingChannel) {
|
|
2189
|
+
agentAddTitle.textContent = '配置智能体:' + existingChannel.name;
|
|
2190
|
+
agentAddName.value = existingChannel.name || '';
|
|
2191
|
+
agentAddName.readOnly = true; // 改名走 PATCH
|
|
2192
|
+
agentAddWallet.value = existingChannel.walletAddress || '';
|
|
2193
|
+
agentAddAutoTools.checked = !!existingChannel.autoInvokeTools;
|
|
2194
|
+
agentAddConfirmBtn.dataset.mode = 'update';
|
|
2195
|
+
agentAddConfirmBtn.dataset.channelId = existingChannel.id;
|
|
2196
|
+
} else {
|
|
2197
|
+
agentAddTitle.textContent = '添加智能体';
|
|
2198
|
+
agentAddName.value = '';
|
|
2199
|
+
agentAddName.readOnly = false;
|
|
2200
|
+
agentAddWallet.value = '';
|
|
2201
|
+
agentAddAutoTools.checked = true;
|
|
2202
|
+
agentAddConfirmBtn.dataset.mode = 'create';
|
|
2203
|
+
delete agentAddConfirmBtn.dataset.channelId;
|
|
2204
|
+
}
|
|
2205
|
+
agentAddWalletInfo.style.display = 'none';
|
|
2206
|
+
agentAddWalletInfo.innerHTML = '';
|
|
2207
|
+
pendingWalletSecret = null;
|
|
2208
|
+
agentAddModal.classList.add('active');
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
function closeAgentAddModal() {
|
|
2212
|
+
if (!agentAddModal) return;
|
|
2213
|
+
agentAddModal.classList.remove('active');
|
|
2214
|
+
pendingWalletSecret = null;
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
/** 本地生成一个 EVM 风格地址 — 仅用于演示; 生产应使用 ethers/wagmi 等真实库 */
|
|
2218
|
+
function generateLocalWallet() {
|
|
2219
|
+
const bytes = new Uint8Array(32);
|
|
2220
|
+
crypto.getRandomValues(bytes);
|
|
2221
|
+
const privateKeyHex = Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
2222
|
+
// 简化: 取末 20 字节当 address, 不做 keccak, 仅作为占位
|
|
2223
|
+
const addrBytes = bytes.slice(12);
|
|
2224
|
+
const address = '0x' + Array.from(addrBytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
2225
|
+
return { address, privateKeyHex };
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
if (agentGenerateWalletBtn) {
|
|
2229
|
+
agentGenerateWalletBtn.addEventListener('click', () => {
|
|
2230
|
+
try {
|
|
2231
|
+
const { address, privateKeyHex } = generateLocalWallet();
|
|
2232
|
+
agentAddWallet.value = address;
|
|
2233
|
+
pendingWalletSecret = privateKeyHex;
|
|
2234
|
+
agentAddWalletInfo.style.display = 'block';
|
|
2235
|
+
agentAddWalletInfo.innerHTML = `
|
|
2236
|
+
✓ 已生成本地钱包<br>
|
|
2237
|
+
<strong>地址:</strong> <code>${escapeHtml(address)}</code><br>
|
|
2238
|
+
<strong>私钥 (仅本次会话, 刷新即丢):</strong> <code style="color:#f88;">${escapeHtml(privateKeyHex)}</code><br>
|
|
2239
|
+
<small style="color:#f88;">⚠ 请抄写并妥善保存私钥, 关闭页面后无法找回。仅地址会发送到服务端。</small>
|
|
2240
|
+
`;
|
|
2241
|
+
} catch (err) {
|
|
2242
|
+
agentAddWalletInfo.style.display = 'block';
|
|
2243
|
+
agentAddWalletInfo.innerHTML = '✗ 生成钱包失败: ' + escapeHtml(err.message);
|
|
2244
|
+
}
|
|
2245
|
+
});
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
if (catalogAddBtn) {
|
|
2249
|
+
catalogAddBtn.addEventListener('click', () => openAgentAddModal(null));
|
|
2250
|
+
}
|
|
2251
|
+
if (agentAddModalClose) agentAddModalClose.addEventListener('click', closeAgentAddModal);
|
|
2252
|
+
if (agentAddCancelBtn) agentAddCancelBtn.addEventListener('click', closeAgentAddModal);
|
|
2253
|
+
|
|
2254
|
+
if (agentAddConfirmBtn) {
|
|
2255
|
+
agentAddConfirmBtn.addEventListener('click', async () => {
|
|
2256
|
+
const mode = agentAddConfirmBtn.dataset.mode || 'create';
|
|
2257
|
+
const name = (agentAddName.value || '').trim();
|
|
2258
|
+
if (!name && mode === 'create') {
|
|
2259
|
+
alert('请输入智能体名称');
|
|
2260
|
+
return;
|
|
2261
|
+
}
|
|
2262
|
+
const walletAddress = (agentAddWallet.value || '').trim();
|
|
2263
|
+
const autoInvokeTools = !!agentAddAutoTools.checked;
|
|
2264
|
+
|
|
2265
|
+
try {
|
|
2266
|
+
if (mode === 'create') {
|
|
2267
|
+
const res = await fetch('/channels', {
|
|
2268
|
+
method: 'POST',
|
|
2269
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2270
|
+
body: JSON.stringify({
|
|
2271
|
+
name,
|
|
2272
|
+
agentId: currentAgentId,
|
|
2273
|
+
walletAddress: walletAddress || undefined,
|
|
2274
|
+
autoInvokeTools
|
|
2275
|
+
})
|
|
2276
|
+
});
|
|
2277
|
+
if (!res.ok) throw new Error('create failed');
|
|
2278
|
+
const channel = await res.json();
|
|
2279
|
+
channels.push(channel);
|
|
2280
|
+
renderChannels();
|
|
2281
|
+
selectChannel(channel.id);
|
|
2282
|
+
} else {
|
|
2283
|
+
// update
|
|
2284
|
+
const channelId = agentAddConfirmBtn.dataset.channelId;
|
|
2285
|
+
const res = await fetch(`/channels/${channelId}`, {
|
|
2286
|
+
method: 'PATCH',
|
|
2287
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2288
|
+
body: JSON.stringify({ walletAddress: walletAddress || null, autoInvokeTools })
|
|
2289
|
+
});
|
|
2290
|
+
if (!res.ok) throw new Error('update failed');
|
|
2291
|
+
const updated = await res.json();
|
|
2292
|
+
const idx = channels.findIndex(c => c.id === channelId);
|
|
2293
|
+
if (idx >= 0) channels[idx] = updated;
|
|
2294
|
+
renderChannels();
|
|
2295
|
+
}
|
|
2296
|
+
closeAgentAddModal();
|
|
2297
|
+
} catch (err) {
|
|
2298
|
+
console.error('Failed to save agent:', err);
|
|
2299
|
+
alert('保存失败: ' + err.message);
|
|
2300
|
+
}
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2303
|
+
|