acp-ts 1.2.2 → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -50,36 +50,52 @@ const heartbeat_1 = require("./heartbeat");
50
50
  const cert_1 = require("./cert");
51
51
  const group_1 = require("./group");
52
52
  const messagestore_1 = require("./messagestore");
53
+ const utils_1 = require("./utils");
53
54
  let globalApiUrl = '';
54
55
  let globalDataDir = '';
55
56
  const messageStores = new Map();
56
- // ============================================================
57
- // Browser ↔ Server WebSocket (real-time group message push)
58
- // ============================================================
59
- const browserWsClients = new Set();
57
+ const browserWsClients = new Map();
58
+ /**
59
+ * 向绑定了指定 aid 的浏览器 WS 客户端推送消息
60
+ */
61
+ function pushToAid(aid, data) {
62
+ const payload = JSON.stringify(data);
63
+ for (const [ws, client] of browserWsClients) {
64
+ if (client.aid === aid && ws.readyState === ws_1.default.OPEN) {
65
+ ws.send(payload);
66
+ }
67
+ }
68
+ }
60
69
  /**
61
70
  * 向所有已连接的浏览器 WS 客户端广播消息
62
71
  */
63
72
  function broadcastToBrowser(data) {
64
73
  const payload = JSON.stringify(data);
65
- for (const client of browserWsClients) {
66
- if (client.readyState === ws_1.default.OPEN) {
67
- client.send(payload);
74
+ let sentCount = 0;
75
+ for (const [ws, client] of browserWsClients) {
76
+ if (ws.readyState === ws_1.default.OPEN) {
77
+ ws.send(payload);
78
+ sentCount++;
79
+ }
80
+ else {
81
+ utils_1.logger.warn(`[broadcastToBrowser] skip ws client (readyState=${ws.readyState}, aid=${client.aid})`);
68
82
  }
69
83
  }
84
+ if (data.type === 'group_message_batch' || data.type === 'new_message_notify') {
85
+ utils_1.logger.log(`[broadcastToBrowser] type=${data.type} group=${data.group_id} sentTo=${sentCount}/${browserWsClients.size} clients`);
86
+ }
70
87
  }
71
88
  let agentCP = null;
72
- let currentAid = '';
73
89
  const MAX_AIDS = 10;
74
90
  const aidInstances = new Map();
75
91
  function getActiveInstance() {
76
- return aidInstances.get(currentAid) || null;
92
+ return null; // legacy stub — use aidInstances.get(aid) directly
77
93
  }
78
94
  // 正在上线中的 Promise 缓存,防止并发调用重复上线
79
95
  const onlinePendingMap = new Map();
80
96
  // 确保指定 AID 在线,如果不在线则自动上线,返回实例
81
97
  async function ensureOnline(targetAid) {
82
- const aid = targetAid || currentAid;
98
+ const aid = targetAid;
83
99
  if (!aid)
84
100
  throw new Error('请先选择 AID');
85
101
  const existing = aidInstances.get(aid);
@@ -99,6 +115,8 @@ async function ensureOnline(targetAid) {
99
115
  }
100
116
  }
101
117
  async function doEnsureOnline(aid) {
118
+ // 确保该 AID 的会话数据已从磁盘加载
119
+ await ensureMessageStoreLoaded(aid);
102
120
  // 自动上线
103
121
  const cp = new agentcp_1.AgentCP(globalApiUrl, '', globalDataDir || undefined, { persistMessages: true, persistGroupMessages: true });
104
122
  await cp.loadAid(aid);
@@ -106,7 +124,7 @@ async function doEnsureOnline(aid) {
106
124
  const customOpts = getAidMdOptionsForAid(aid);
107
125
  cp.setAgentMdOptions(Object.assign({ type: 'human', tags: ['human', 'acp'] }, customOpts));
108
126
  const connConfig = await cp.online();
109
- console.log(`[Server] 自动上线 AID: ${aid}`);
127
+ utils_1.logger.log(`[Server] 自动上线 AID: ${aid}`);
110
128
  const hb = new heartbeat_1.HeartbeatClient(aid, connConfig.heartbeatServer, '');
111
129
  const ws = new agentws_1.AgentWS(aid, connConfig.messageServer, connConfig.messageSignature);
112
130
  const instance = {
@@ -126,11 +144,9 @@ async function doEnsureOnline(aid) {
126
144
  };
127
145
  aidInstances.set(aid, instance);
128
146
  hb.onInvite((invite) => {
129
- console.log(`[Server] 收到邀请: ${JSON.stringify(invite)}`);
147
+ utils_1.logger.log(`[Server] 收到邀请: ${JSON.stringify(invite)}`);
130
148
  const session = getMessageStoreForAid(aid).getOrCreateSession(invite.sessionId, invite.inviteCode, invite.inviterAgentId, 'incoming', aid);
131
- if (!activeSessionId) {
132
- activeSessionId = session.sessionId;
133
- }
149
+ pushToAid(aid, { type: 'sessions_updated' });
134
150
  if (instance.agentWS) {
135
151
  instance.agentWS.acceptInviteFromHeartbeat(invite.sessionId, invite.inviterAgentId, invite.inviteCode);
136
152
  }
@@ -138,28 +154,55 @@ async function doEnsureOnline(aid) {
138
154
  // 心跳重连成功后,自动触发 WebSocket 重连 + 群组重新注册
139
155
  hb.onReconnect(() => {
140
156
  if (instance.agentWS) {
141
- console.log('[Server] 心跳重连成功,触发 WebSocket 重连...');
157
+ utils_1.logger.log('[Server] 心跳重连成功,触发 WebSocket 重连...');
142
158
  instance.agentWS.reconnect().then(async () => {
143
159
  // WebSocket 重连成功后,重新注册所有在线群组
144
160
  // 断线期间 group.ap 会将在线状态过期,必须重新 register_online 才能收到推送
145
161
  const onlineGroups = instance.agentCP.getOnlineGroups();
146
162
  if (onlineGroups.length > 0) {
147
- console.log(`[Server] WebSocket 重连成功,重新注册 ${onlineGroups.length} 个在线群组...`);
163
+ utils_1.logger.log(`[Server] WebSocket 重连成功,重新注册 ${onlineGroups.length} 个在线群组...`);
148
164
  for (const groupId of onlineGroups) {
149
165
  try {
150
166
  await instance.agentCP.joinGroupSession(groupId);
151
- console.log(`[Server] 群组重新注册成功: ${groupId}`);
167
+ utils_1.logger.log(`[Server] 群组重新注册成功: ${groupId}`);
152
168
  }
153
169
  catch (e) {
154
- console.warn(`[Server] 群组重新注册失败: ${groupId}`, e.message || e);
170
+ utils_1.logger.warn(`[Server] 群组重新注册失败: ${groupId}`, e.message || e);
155
171
  }
156
172
  }
157
173
  }
158
174
  }).catch((err) => {
159
- console.error('[Server] WebSocket 重连失败:', err);
175
+ utils_1.logger.error('[Server] WebSocket 重连失败:', err);
160
176
  });
161
177
  }
162
178
  });
179
+ // WS 快速重试耗尽后,重新鉴权并用新 signature 重连
180
+ ws.onReconnectNeeded(async () => {
181
+ utils_1.logger.log('[Server] WS 快速重试耗尽,开始重新鉴权重连...');
182
+ try {
183
+ const newConnConfig = await cp.online();
184
+ instance.connectionConfig = newConnConfig;
185
+ utils_1.logger.log('[Server] 重新鉴权成功,使用新 signature 重连 WebSocket...');
186
+ await instance.agentWS.reconnect(newConnConfig.messageServer, newConnConfig.messageSignature);
187
+ // 重连成功后重新注册所有在线群组
188
+ const onlineGroups = instance.agentCP.getOnlineGroups();
189
+ if (onlineGroups.length > 0) {
190
+ utils_1.logger.log(`[Server] 重新鉴权重连成功,重新注册 ${onlineGroups.length} 个在线群组...`);
191
+ for (const groupId of onlineGroups) {
192
+ try {
193
+ await instance.agentCP.joinGroupSession(groupId);
194
+ utils_1.logger.log(`[Server] 群组重新注册成功: ${groupId}`);
195
+ }
196
+ catch (e) {
197
+ utils_1.logger.warn(`[Server] 群组重新注册失败: ${groupId}`, e.message || e);
198
+ }
199
+ }
200
+ }
201
+ }
202
+ catch (err) {
203
+ utils_1.logger.error('[Server] 重新鉴权重连失败:', err);
204
+ }
205
+ });
163
206
  await hb.online();
164
207
  ws.onMessage((message) => {
165
208
  var _a;
@@ -209,13 +252,14 @@ async function doEnsureOnline(aid) {
209
252
  }
210
253
  if (msgSessionId && getMessageStoreForAid(aid).hasSession(msgSessionId)) {
211
254
  getMessageStoreForAid(aid).addMessageToSession(msgSessionId, { type: 'received', content, from, timestamp: Date.now() });
255
+ pushToAid(aid, { type: 'p2p_message', sessionId: msgSessionId, message: { type: 'received', content, from, timestamp: Date.now() } });
256
+ pushToAid(aid, { type: 'sessions_updated' });
212
257
  }
213
258
  else if (msgSessionId && from) {
214
259
  getMessageStoreForAid(aid).getOrCreateSession(msgSessionId, '', from, 'incoming', aid);
215
- if (!activeSessionId) {
216
- activeSessionId = msgSessionId;
217
- }
218
260
  getMessageStoreForAid(aid).addMessageToSession(msgSessionId, { type: 'received', content, from, timestamp: Date.now() });
261
+ pushToAid(aid, { type: 'p2p_message', sessionId: msgSessionId, message: { type: 'received', content, from, timestamp: Date.now() } });
262
+ pushToAid(aid, { type: 'sessions_updated' });
219
263
  }
220
264
  });
221
265
  ws.onStatusChange((status) => {
@@ -232,10 +276,10 @@ async function doEnsureOnline(aid) {
232
276
  // AID 上线后自动初始化群组功能,确保所有身份都能收到群消息推送
233
277
  try {
234
278
  await ensureGroupClient(instance);
235
- console.log(`[Server] AID ${aid} 群组功能自动初始化完成`);
279
+ utils_1.logger.log(`[Server] AID ${aid} 群组功能自动初始化完成`);
236
280
  }
237
281
  catch (e) {
238
- console.warn(`[Server] AID ${aid} 群组功能自动初始化失败(不影响上线):`, e.message);
282
+ utils_1.logger.warn(`[Server] AID ${aid} 群组功能自动初始化失败(不影响上线):`, e.message);
239
283
  }
240
284
  return instance;
241
285
  }
@@ -275,7 +319,7 @@ async function ensureGroupClient(instance) {
275
319
  // 注册群组事件处理器,确保 SDK 通知回调可靠触发
276
320
  instance.agentCP.setGroupEventHandler({
277
321
  onNewMessage(groupId, latestMsgId, sender, preview) {
278
- console.log(`[Group] onNewMessage: group=${groupId} msgId=${latestMsgId} sender=${sender} preview=${preview}`);
322
+ utils_1.logger.log(`[Group] onNewMessage: group=${groupId} msgId=${latestMsgId} sender=${sender} preview=${preview}`);
279
323
  // 通知浏览器有新消息(轻量通知,前端可据此决定是否刷新)
280
324
  broadcastToBrowser({
281
325
  type: 'new_message_notify',
@@ -286,7 +330,7 @@ async function ensureGroupClient(instance) {
286
330
  });
287
331
  },
288
332
  onNewEvent(groupId, latestEventId, eventType, summary) {
289
- console.log(`[Group] onNewEvent: group=${groupId} eventId=${latestEventId} type=${eventType} summary=${summary}`);
333
+ utils_1.logger.log(`[Group] onNewEvent: group=${groupId} eventId=${latestEventId} type=${eventType} summary=${summary}`);
290
334
  broadcastToBrowser({
291
335
  type: 'new_event',
292
336
  group_id: groupId,
@@ -296,7 +340,7 @@ async function ensureGroupClient(instance) {
296
340
  });
297
341
  },
298
342
  onGroupInvite(groupId, groupAddress, invitedBy) {
299
- console.log(`[Group] onGroupInvite: group=${groupId} address=${groupAddress} invitedBy=${invitedBy}`);
343
+ utils_1.logger.log(`[Group] onGroupInvite: group=${groupId} address=${groupAddress} invitedBy=${invitedBy}`);
300
344
  broadcastToBrowser({
301
345
  type: 'group_invite',
302
346
  group_id: groupId,
@@ -305,12 +349,12 @@ async function ensureGroupClient(instance) {
305
349
  });
306
350
  },
307
351
  onJoinApproved(groupId, groupAddress) {
308
- console.log(`[Group] onJoinApproved: group=${groupId} address=${groupAddress}`);
352
+ utils_1.logger.log(`[Group] onJoinApproved: group=${groupId} address=${groupAddress}`);
309
353
  // 审核通过:获取群信息、添加本地存储、注册到 Home AP
310
354
  (async () => {
311
355
  try {
312
356
  if (!instance.agentCP.groupOps) {
313
- console.warn(`[Group] onJoinApproved skipped: groupOps not available`);
357
+ utils_1.logger.warn(`[Group] onJoinApproved skipped: groupOps not available`);
314
358
  return;
315
359
  }
316
360
  let groupName = groupId;
@@ -324,7 +368,7 @@ async function ensureGroupClient(instance) {
324
368
  await instance.agentCP.joinGroupSession(groupId);
325
369
  }
326
370
  catch (e) {
327
- console.error(`[Group] onJoinApproved processing failed: group=${groupId}`, e.message);
371
+ utils_1.logger.error(`[Group] onJoinApproved processing failed: group=${groupId}`, e.message);
328
372
  }
329
373
  })();
330
374
  broadcastToBrowser({
@@ -334,32 +378,42 @@ async function ensureGroupClient(instance) {
334
378
  });
335
379
  },
336
380
  onJoinRejected(groupId, reason) {
337
- console.log(`[Group] onJoinRejected: group=${groupId} reason=${reason}`);
381
+ utils_1.logger.log(`[Group] onJoinRejected: group=${groupId} reason=${reason}`);
338
382
  broadcastToBrowser({ type: 'join_rejected', group_id: groupId, reason });
339
383
  },
340
384
  onJoinRequestReceived(groupId, agentId, message) {
341
- console.log(`[Group] onJoinRequestReceived: group=${groupId} agent=${agentId} msg=${message}`);
385
+ utils_1.logger.log(`[Group] onJoinRequestReceived: group=${groupId} agent=${agentId} msg=${message}`);
342
386
  broadcastToBrowser({ type: 'join_request', group_id: groupId, agent_id: agentId, message });
343
387
  },
344
388
  onGroupMessageBatch(groupId, batch) {
345
- console.log(`[Group] onGroupMessageBatch: group=${groupId} count=${batch.count} range=[${batch.start_msg_id}, ${batch.latest_msg_id}]`);
389
+ utils_1.logger.log(`[Group] onGroupMessageBatch: group=${groupId} count=${batch.count} range=[${batch.start_msg_id}, ${batch.latest_msg_id}] messages=${JSON.stringify((batch.messages || []).map(m => m.msg_id))}`);
346
390
  // 存储 + ACK(统一由 agentcp 处理),注意 processAndAckBatch 是 async
347
391
  instance.agentCP.processAndAckBatch(groupId, batch).then((sorted) => {
392
+ var _a, _b;
393
+ utils_1.logger.log(`[Group] processAndAckBatch OK: group=${groupId} sortedCount=${sorted.length} msgIds=${sorted.map(m => m.msg_id)}`);
394
+ // 检查浏览器连接数
395
+ const connectedCount = Array.from(browserWsClients.entries()).filter(([ws]) => ws.readyState === ws_1.default.OPEN).length;
396
+ utils_1.logger.log(`[Group] broadcastToBrowser: group=${groupId} connectedBrowserClients=${connectedCount} totalClients=${browserWsClients.size}`);
397
+ if (connectedCount === 0) {
398
+ utils_1.logger.warn(`[Group] !!! 没有已连接的浏览器客户端,消息无法推送到前端!`);
399
+ }
348
400
  // 推送消息列表给浏览器
349
- broadcastToBrowser({
401
+ const payload = {
350
402
  type: 'group_message_batch',
351
403
  group_id: groupId,
352
404
  messages: sorted,
353
405
  count: batch.count,
354
406
  start_msg_id: batch.start_msg_id,
355
407
  latest_msg_id: batch.latest_msg_id,
356
- });
408
+ };
409
+ utils_1.logger.log(`[Group] broadcast payload: type=${payload.type} group_id=${payload.group_id} msgCount=${payload.messages.length} firstMsgId=${(_a = sorted[0]) === null || _a === void 0 ? void 0 : _a.msg_id} lastMsgId=${(_b = sorted[sorted.length - 1]) === null || _b === void 0 ? void 0 : _b.msg_id}`);
410
+ broadcastToBrowser(payload);
357
411
  }).catch((e) => {
358
- console.error(`[Group] processAndAckBatch failed: group=${groupId}`, e);
412
+ utils_1.logger.error(`[Group] processAndAckBatch failed: group=${groupId}`, e);
359
413
  });
360
414
  },
361
415
  onGroupEvent(groupId, evt) {
362
- console.log(`[Group] onGroupEvent: group=${groupId} event=${evt.event_type}`);
416
+ utils_1.logger.log(`[Group] onGroupEvent: group=${groupId} event=${evt.event_type}`);
363
417
  broadcastToBrowser({
364
418
  type: 'group_event',
365
419
  group_id: groupId,
@@ -374,7 +428,7 @@ async function ensureGroupClient(instance) {
374
428
  instance.groupListSynced = true;
375
429
  }
376
430
  catch (e) {
377
- console.warn('[Group] syncGroupList error:', e.message);
431
+ utils_1.logger.warn('[Group] syncGroupList error:', e.message);
378
432
  }
379
433
  }
380
434
  // 为所有已加入群组注册上线(register_online + 拉取未读 + 启动心跳)
@@ -384,13 +438,13 @@ async function ensureGroupClient(instance) {
384
438
  await instance.agentCP.joinGroupSession(group.group_id);
385
439
  }
386
440
  catch (e) {
387
- console.warn(`[Group] joinGroupSession failed: ${group.group_id}`, e.message);
441
+ utils_1.logger.warn(`[Group] joinGroupSession failed: ${group.group_id}`, e.message);
388
442
  }
389
443
  }
390
444
  instance.groupInitialized = true;
391
445
  instance.groupSessionId = groupSessionId;
392
446
  instance.groupTargetAid = targetAid;
393
- console.log(`[Group] 群组客户端已初始化: aid=${aid} target=${targetAid} session=${groupSessionId}`);
447
+ utils_1.logger.log(`[Group] 群组客户端已初始化: aid=${aid} target=${targetAid} session=${groupSessionId}`);
394
448
  }
395
449
  async function validateAid(aid) {
396
450
  try {
@@ -441,7 +495,7 @@ function loadAgentInfoCacheFromDisk() {
441
495
  agentInfoCache.set(aid, Object.assign(Object.assign({}, info), { tags: info.tags || [] }));
442
496
  }
443
497
  }
444
- console.log(`[Server] 已加载 agent info 缓存: ${agentInfoCache.size} 条`);
498
+ utils_1.logger.log(`[Server] 已加载 agent info 缓存: ${agentInfoCache.size} 条`);
445
499
  }
446
500
  }
447
501
  catch (e) {
@@ -554,7 +608,7 @@ function getAidMdOptionsForAid(aid) {
554
608
  return loadAidMdOptions()[aid] || {};
555
609
  }
556
610
  // 消息与会话管理 — 每个 AID 独立 MessageStore
557
- let activeSessionId = null;
611
+ const messageStoreLoaded = new Set();
558
612
  function getMessageStoreForAid(aid) {
559
613
  let store = messageStores.get(aid);
560
614
  if (!store) {
@@ -566,1524 +620,1901 @@ function getMessageStoreForAid(aid) {
566
620
  }
567
621
  return store;
568
622
  }
569
- function getMessageStore() {
570
- if (!currentAid)
571
- throw new Error('请先选择 AID');
572
- return getMessageStoreForAid(currentAid);
623
+ async function ensureMessageStoreLoaded(aid) {
624
+ const store = getMessageStoreForAid(aid);
625
+ if (!messageStoreLoaded.has(aid)) {
626
+ await store.loadSessionsForAid(aid);
627
+ messageStoreLoaded.add(aid);
628
+ }
629
+ return store;
573
630
  }
574
631
  // HTML 页面
575
- const indexHtml = `<!DOCTYPE html>
576
- <html lang="zh-CN">
577
- <head>
578
- <meta charset="UTF-8">
579
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
580
- <link rel="icon" href="/favicon.ico" type="image/x-icon">
581
- <title>ACP 身份管理</title>
582
- <style>
583
- * { box-sizing: border-box; margin: 0; padding: 0; }
584
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; min-height: 100vh; display: flex; justify-content: center; align-items: flex-start; padding: 40px 20px; }
585
- .container { background: white; padding: 32px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); max-width: 560px; width: 100%; }
586
- h1 { color: #333; margin-bottom: 24px; text-align: center; font-size: 22px; }
587
- .hint { text-align: center; color: #999; font-size: 13px; margin-bottom: 20px; }
588
- .create-section { margin-bottom: 24px; display: flex; flex-direction: column; gap: 12px; }
589
- .create-section .aid-input-row { display: flex; gap: 8px; align-items: center; }
590
- .create-section .aid-input-row input { flex: 1; padding: 10px 14px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px; min-width: 0; }
591
- .create-section .aid-input-row input:focus { outline: none; border-color: #007bff; }
592
- .create-section .aid-input-row .dot-separator { color: #999; font-size: 16px; flex-shrink: 0; }
593
- .create-section .aid-input-row select {
594
- padding: 10px 30px 10px 14px;
595
- border: 1px solid #ddd;
596
- border-radius: 8px;
597
- font-size: 14px;
598
- background: white;
599
- flex-shrink: 0;
600
- cursor: pointer;
601
- appearance: none;
602
- -webkit-appearance: none;
603
- -moz-appearance: none;
604
- background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23999%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E");
605
- background-repeat: no-repeat;
606
- background-position: right 10px top 50%;
607
- background-size: 10px auto;
608
- }
609
- .create-section .aid-input-row select:focus { outline: none; border-color: #007bff; }
610
- .create-section .extra-fields { display: flex; gap: 8px; }
611
- .create-section .extra-fields input { flex: 1; padding: 10px 14px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px; min-width: 0; }
612
- .create-section .extra-fields input:focus { outline: none; border-color: #007bff; }
613
- .btn { display: block; width: 100%; padding: 12px; border: none; border-radius: 8px; font-size: 15px; cursor: pointer; transition: background 0.2s; }
614
- .btn-primary { background: #007bff; color: white; }
615
- .btn-primary:hover { background: #0056b3; }
616
- .btn-sm { display: inline-block; width: auto; padding: 6px 14px; font-size: 13px; border-radius: 6px; }
617
- .btn-success { background: #28a745; color: white; }
618
- .btn-success:hover { background: #218838; }
619
- .btn-danger { background: #dc3545; color: white; }
620
- .btn-danger:hover { background: #c82333; }
621
- .btn-outline { background: white; color: #007bff; border: 1px solid #007bff; }
622
- .btn-outline:hover { background: #e7f1ff; }
623
- .btn-outline.active { background: #007bff; color: white; }
624
- .btn:disabled { background: #ccc; cursor: not-allowed; border-color: #ccc; color: #fff; }
625
- .aid-list { margin-bottom: 24px; }
626
- .aid-card { background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 10px; padding: 16px; margin-bottom: 12px; transition: border-color 0.2s; display: flex; align-items: stretch; gap: 12px; }
627
- .aid-card.current { border-color: #007bff; background: #f0f7ff; }
628
- .aid-card-left { flex: 1; min-width: 0; }
629
- .aid-card-right { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; flex-shrink: 0; justify-content: center; }
630
- .aid-card-header { margin-bottom: 10px; }
631
- .aid-name { font-family: monospace; font-size: 13px; color: #333; word-break: break-all; }
632
- .copy-btn { background: none; border: none; color: #6c757d; cursor: pointer; font-size: 12px; padding: 2px 6px; }
633
- .copy-btn:hover { color: #333; }
634
- .aid-card-status { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
635
- .badge { display: inline-block; padding: 3px 10px; border-radius: 12px; font-size: 12px; font-weight: 500; }
636
- .badge-success { background: #d4edda; color: #155724; }
637
- .badge-warning { background: #fff3cd; color: #856404; }
638
- .badge-danger { background: #f8d7da; color: #721c24; }
639
- .badge-info { background: #d1ecf1; color: #0c5460; }
640
- .badge-current { background: #007bff; color: white; }
641
- .aid-card-actions { display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
642
- .status { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); padding: 12px 24px; border-radius: 8px; font-size: 14px; display: none; z-index: 1000; }
643
- .status.success { display: block; background: #d4edda; color: #155724; }
644
- .status.error { display: block; background: #f8d7da; color: #721c24; }
645
- </style>
646
- </head>
647
- <body>
648
- <div class="container">
649
- <h1>ACP 身份管理</h1>
650
- <div class="hint" id="hint">最多注册 10 AID</div>
651
-
652
- <div class="create-section" id="createSection">
653
- <div class="aid-input-row">
654
- <input type="text" id="newAid" placeholder="输入名称">
655
- <span class="dot-separator">.</span>
656
- <select id="apSelect"></select>
657
- </div>
658
- <div class="extra-fields">
659
- <input type="text" id="aidNickname" placeholder="昵称(选填)">
660
- <input type="text" id="aidDescription" placeholder="描述(选填)" style="flex:2;">
661
- </div>
662
- <button class="btn btn-primary" onclick="createAid()">注册 AID</button>
663
- </div>
664
-
665
- <div class="aid-list" id="aidList"></div>
666
-
667
- <div class="status" id="status"></div>
668
- </div>
669
-
670
- <script>
671
- let aidData = { currentAid: '', aidList: [], aidStatus: [], apiUrl: '' };
672
-
673
- async function loadAidInfo() {
674
- try {
675
- const res = await fetch('/api/aid');
676
- const data = await res.json();
677
- aidData = data;
678
- updateApSelect();
679
- renderAidList();
680
- } catch (e) {
681
- console.error('加载失败', e);
682
- }
683
- }
684
-
685
- function updateApSelect() {
686
- var sel = document.getElementById('apSelect');
687
- if (sel && sel.options.length === 0) {
688
- const options = ['agentcp.io', 'aid.show', 'agentid.pub'];
689
- options.forEach(function(op) {
690
- var opt = document.createElement('option');
691
- opt.value = op;
692
- opt.textContent = op;
693
- if (op === 'agentcp.io') opt.selected = true;
694
- sel.appendChild(opt);
695
- });
696
- }
697
- }
698
-
699
- function renderAidList() {
700
- const list = document.getElementById('aidList');
701
- const createSection = document.getElementById('createSection');
702
- const hint = document.getElementById('hint');
703
-
704
- if (aidData.aidList.length >= 10) {
705
- createSection.style.display = 'none';
706
- hint.textContent = '已达到 10 个 AID 上限';
707
- } else {
708
- createSection.style.display = 'block';
709
- hint.textContent = '最多注册 10 个 AID(已注册 ' + aidData.aidList.length + ' 个)';
710
- }
711
-
712
- if (!aidData.aidStatus || aidData.aidStatus.length === 0) {
713
- list.innerHTML = '<div style="text-align:center;color:#999;padding:20px;">暂无 AID,请先注册</div>';
714
- return;
715
- }
716
-
717
- list.innerHTML = aidData.aidStatus.map(function(item) {
718
- var isCurrent = item.aid === aidData.currentAid;
719
- var cardClass = 'aid-card' + (isCurrent ? ' current' : '');
720
-
721
- var badges = '';
722
- if (isCurrent) badges += '<span class="badge badge-current">当前</span>';
723
- if (item.online) badges += '<span class="badge badge-success">已上线</span>';
724
- if (item.keysExist && item.certValid) {
725
- badges += '<span class="badge badge-info">密钥有效</span>';
726
- } else if (item.keysExist && !item.certValid) {
727
- badges += '<span class="badge badge-warning">证书过期</span>';
728
- } else {
729
- badges += '<span class="badge badge-danger">密钥缺失</span>';
730
- }
731
-
732
- var actions = '';
733
- if (!isCurrent) {
734
- actions += '<button class="btn btn-sm btn-outline" onclick="selectAid(\\'' + escapeAttr(item.aid) + '\\')">选为当前</button>';
735
- }
736
- // 上线并进入聊天(合并按钮)
737
- if (isCurrent && item.keysExist && item.certValid) {
738
- if (item.online) {
739
- actions += '<button class="btn btn-sm btn-success" onclick="enterChat(\\'' + escapeAttr(item.aid) + '\\')">进入聊天</button>';
740
- actions += '<button class="btn btn-sm btn-danger" onclick="goOffline(\\'' + escapeAttr(item.aid) + '\\')">下线</button>';
741
- } else {
742
- actions += '<button class="btn btn-sm btn-success" id="goBtn_' + escapeAttr(item.aid) + '" onclick="goOnlineAndChat(\\'' + escapeAttr(item.aid) + '\\')">上线并进入</button>';
743
- }
744
- }
745
- if (!isCurrent && item.online) {
746
- actions += '<button class="btn btn-sm btn-danger" onclick="goOffline(\\'' + escapeAttr(item.aid) + '\\')">下线</button>';
747
- }
748
-
749
- return '<div class="' + cardClass + '">' +
750
- '<div class="aid-card-left">' +
751
- '<div class="aid-card-header">' +
752
- '<span class="aid-name">' + escapeHtml(item.aid) + '</span>' +
753
- '</div>' +
754
- '<div class="aid-card-status">' + badges + '</div>' +
755
- '</div>' +
756
- '<div class="aid-card-right">' +
757
- '<div class="aid-card-actions">' + actions + '</div>' +
758
- '<button class="copy-btn" onclick="copyText(\\'' + escapeAttr(item.aid) + '\\')">复制</button>' +
759
- '</div>' +
760
- '</div>';
761
- }).join('');
762
- }
763
-
764
- async function createAid() {
765
- var prefix = document.getElementById('newAid').value.trim();
766
- if (!prefix) { showStatus('请输入 AID 名称', 'error'); return; }
767
- var ap = document.getElementById('apSelect').value;
768
- if (!ap) { showStatus('请选择 AP', 'error'); return; }
769
- var fullPrefix = prefix + '.' + ap;
770
- var nickname = document.getElementById('aidNickname').value.trim();
771
- var description = document.getElementById('aidDescription').value.trim();
772
- try {
773
- var res = await fetch('/api/aid/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prefix: fullPrefix, nickname: nickname, description: description }) });
774
- var data = await res.json();
775
- if (data.success) { showStatus('AID 注册成功', 'success'); document.getElementById('newAid').value = ''; document.getElementById('aidNickname').value = ''; document.getElementById('aidDescription').value = ''; loadAidInfo(); }
776
- else { showStatus(data.error || '注册失败', 'error'); }
777
- } catch (e) { showStatus('注册失败: ' + e.message, 'error'); }
778
- }
779
-
780
- async function selectAid(aid) {
781
- try {
782
- var res = await fetch('/api/aid/select', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ aid: aid }) });
783
- var data = await res.json();
784
- if (data.success) { showStatus('已切换到 ' + aid, 'success'); loadAidInfo(); }
785
- else { showStatus(data.error || '切换失败', 'error'); }
786
- } catch (e) { showStatus('切换失败: ' + e.message, 'error'); }
787
- }
788
-
789
- async function goOnlineAndChat(aid) {
790
- var btn = document.getElementById('goBtn_' + aid);
791
- if (btn) { btn.disabled = true; btn.textContent = '启动中...'; }
792
- try {
793
- showStatus('正在上线 ' + aid + ' ...', 'success');
794
- var res = await fetch('/api/ws/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ aid: aid }) });
795
- var data = await res.json();
796
- if (data.success) {
797
- showStatus(aid + ' 已上线,正在进入聊天...', 'success');
798
- window.location.href = '/chat';
799
- } else {
800
- showStatus(data.error || '上线失败', 'error');
801
- if (btn) { btn.disabled = false; btn.textContent = '上线并进入'; }
802
- }
803
- } catch (e) {
804
- showStatus('上线失败: ' + e.message, 'error');
805
- if (btn) { btn.disabled = false; btn.textContent = '上线并进入'; }
806
- }
807
- }
808
-
809
- function enterChat(aid) { window.location.href = '/chat'; }
810
-
811
- async function goOffline(aid) {
812
- try {
813
- var res = await fetch('/api/aid/offline', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ aid: aid }) });
814
- var data = await res.json();
815
- if (data.success) { showStatus(aid + ' 已下线', 'success'); loadAidInfo(); }
816
- else { showStatus(data.error || '下线失败', 'error'); }
817
- } catch (e) { showStatus('下线失败: ' + e.message, 'error'); }
818
- }
819
-
820
- function copyText(text) {
821
- navigator.clipboard.writeText(text).then(function() {
822
- showStatus('已复制', 'success');
823
- });
824
- }
825
-
826
- function showStatus(msg, type) {
827
- var el = document.getElementById('status');
828
- el.textContent = msg;
829
- el.className = 'status ' + type;
830
- setTimeout(function() { el.className = 'status'; }, 3000);
831
- }
832
-
833
- function escapeHtml(text) {
834
- var div = document.createElement('div');
835
- div.textContent = text;
836
- return div.innerHTML;
837
- }
838
-
839
- function escapeAttr(text) {
840
- return text.replace(/'/g, "\\\\'").replace(/"/g, '&quot;');
841
- }
842
-
843
- loadAidInfo();
844
- setInterval(loadAidInfo, 5000);
845
- <\/script>
846
- </body>
632
+ const indexHtml = `<!DOCTYPE html>
633
+ <html lang="zh-CN">
634
+ <head>
635
+ <meta charset="UTF-8">
636
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
637
+ <link rel="icon" href="/favicon.ico" type="image/x-icon">
638
+ <title>ACP 身份管理</title>
639
+ <style>
640
+ * { box-sizing: border-box; margin: 0; padding: 0; }
641
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: linear-gradient(135deg, #f0f4ff 0%, #e8edf5 100%); min-height: 100vh; display: flex; justify-content: center; align-items: flex-start; padding: 40px 20px; }
642
+ .container { background: white; padding: 0; border-radius: 16px; box-shadow: 0 8px 32px rgba(0,0,0,0.08); max-width: 560px; width: 100%; overflow: hidden; }
643
+ .page-header { background: linear-gradient(135deg, #2563eb 0%, #1e40af 100%); padding: 28px 32px 22px; color: white; text-align: center; }
644
+ .page-header h1 { font-size: 20px; font-weight: 600; margin-bottom: 12px; letter-spacing: 0.5px; }
645
+ .nav-links { display: flex; justify-content: center; gap: 8px; }
646
+ .nav-links a { color: rgba(255,255,255,0.85); text-decoration: none; font-size: 12px; padding: 4px 12px; border-radius: 20px; border: 1px solid rgba(255,255,255,0.25); transition: all 0.2s; }
647
+ .nav-links a:hover { background: rgba(255,255,255,0.15); color: #fff; border-color: rgba(255,255,255,0.5); }
648
+ .page-body { padding: 24px 32px 32px; }
649
+ .hint { text-align: center; color: #9ca3af; font-size: 13px; margin-bottom: 20px; }
650
+ .create-section { margin-bottom: 24px; display: flex; flex-direction: column; gap: 10px; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 12px; padding: 18px; }
651
+ .create-section .aid-input-row { display: flex; gap: 8px; align-items: center; }
652
+ .create-section .aid-input-row input { flex: 1; padding: 10px 14px; border: 1px solid #e2e8f0; border-radius: 8px; font-size: 14px; min-width: 0; background: #fff; transition: border-color 0.2s, box-shadow 0.2s; }
653
+ .create-section .aid-input-row input:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37,99,235,0.1); }
654
+ .create-section .aid-input-row .dot-separator { color: #9ca3af; font-size: 16px; flex-shrink: 0; }
655
+ .create-section .aid-input-row select {
656
+ padding: 10px 30px 10px 14px;
657
+ border: 1px solid #e2e8f0;
658
+ border-radius: 8px;
659
+ font-size: 14px;
660
+ background: #fff;
661
+ flex-shrink: 0;
662
+ cursor: pointer;
663
+ appearance: none;
664
+ -webkit-appearance: none;
665
+ -moz-appearance: none;
666
+ background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23999%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E");
667
+ background-repeat: no-repeat;
668
+ background-position: right 10px top 50%;
669
+ background-size: 10px auto;
670
+ transition: border-color 0.2s, box-shadow 0.2s;
671
+ }
672
+ .create-section .aid-input-row select:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37,99,235,0.1); }
673
+ .create-section .extra-fields { display: flex; gap: 8px; }
674
+ .create-section .extra-fields input { flex: 1; padding: 10px 14px; border: 1px solid #e2e8f0; border-radius: 8px; font-size: 14px; min-width: 0; background: #fff; transition: border-color 0.2s, box-shadow 0.2s; }
675
+ .create-section .extra-fields input:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37,99,235,0.1); }
676
+ .btn { display: block; width: 100%; padding: 11px; border: none; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s; }
677
+ .btn-primary { background: linear-gradient(135deg, #2563eb, #1d4ed8); color: white; }
678
+ .btn-primary:hover { background: linear-gradient(135deg, #1d4ed8, #1e40af); box-shadow: 0 2px 8px rgba(37,99,235,0.3); }
679
+ .btn-sm { display: inline-block; width: auto; padding: 6px 14px; font-size: 13px; border-radius: 6px; }
680
+ .btn-success { background: #10b981; color: white; }
681
+ .btn-success:hover { background: #059669; }
682
+ .btn-danger { background: #ef4444; color: white; }
683
+ .btn-danger:hover { background: #dc2626; }
684
+ .btn-outline { background: white; color: #2563eb; border: 1px solid #2563eb; }
685
+ .btn-outline:hover { background: #eff6ff; }
686
+ .btn-outline.active { background: #2563eb; color: white; }
687
+ .btn:disabled { background: #d1d5db; cursor: not-allowed; border-color: #d1d5db; color: #fff; }
688
+ .aid-list { margin-bottom: 24px; }
689
+ .aid-card { background: #fff; border: 1px solid #e5e7eb; border-radius: 12px; padding: 16px; margin-bottom: 10px; transition: all 0.2s; display: flex; align-items: stretch; gap: 12px; }
690
+ .aid-card:hover { border-color: #93c5fd; box-shadow: 0 2px 12px rgba(37,99,235,0.06); }
691
+ .aid-card.current { border-color: #2563eb; background: #eff6ff; }
692
+ .aid-card-left { flex: 1; min-width: 0; }
693
+ .aid-card-right { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; flex-shrink: 0; justify-content: center; }
694
+ .aid-card-header { margin-bottom: 10px; }
695
+ .aid-name { font-family: 'SF Mono', 'Fira Code', monospace; font-size: 13px; color: #1f2937; word-break: break-all; }
696
+ .copy-btn { background: none; border: none; color: #9ca3af; cursor: pointer; font-size: 12px; padding: 2px 6px; transition: color 0.2s; }
697
+ .copy-btn:hover { color: #2563eb; }
698
+ .aid-card-status { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
699
+ .badge { display: inline-block; padding: 3px 10px; border-radius: 12px; font-size: 12px; font-weight: 500; }
700
+ .badge-success { background: #d1fae5; color: #065f46; }
701
+ .badge-warning { background: #fef3c7; color: #92400e; }
702
+ .badge-danger { background: #fee2e2; color: #991b1b; }
703
+ .badge-info { background: #dbeafe; color: #1e40af; }
704
+ .badge-current { background: #2563eb; color: white; }
705
+ .aid-card-actions { display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
706
+ .status { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); padding: 12px 24px; border-radius: 10px; font-size: 14px; display: none; z-index: 1000; box-shadow: 0 4px 16px rgba(0,0,0,0.1); }
707
+ .status.success { display: block; background: #d1fae5; color: #065f46; }
708
+ .status.error { display: block; background: #fee2e2; color: #991b1b; }
709
+ @media (max-width: 480px) {
710
+ body { padding: 16px 8px; }
711
+ .page-header { padding: 22px 18px 18px; }
712
+ .page-body { padding: 18px 16px 24px; }
713
+ .create-section { padding: 14px; }
714
+ }
715
+ </style>
716
+ </head>
717
+ <body>
718
+ <div class="container">
719
+ <div class="page-header">
720
+ <h1>ACP 身份管理</h1>
721
+ <div class="nav-links">
722
+ <a href="https://agentunion.net" target="_blank">AgentUnion排行榜</a>
723
+ <a href="https://github.com/auliwenjiang/agentcp" target="_blank">ACP 开源GitHub</a>
724
+ </div>
725
+ </div>
726
+ <div class="page-body">
727
+ <div class="hint" id="hint">最多注册 10 个 AID</div>
728
+
729
+ <div class="create-section" id="createSection">
730
+ <div class="aid-input-row">
731
+ <input type="text" id="newAid" placeholder="输入名称">
732
+ <span class="dot-separator">.</span>
733
+ <select id="apSelect"></select>
734
+ </div>
735
+ <div class="extra-fields">
736
+ <input type="text" id="aidNickname" placeholder="昵称(选填)">
737
+ <input type="text" id="aidDescription" placeholder="描述(选填)" style="flex:2;">
738
+ </div>
739
+ <button class="btn btn-primary" onclick="createAid()">注册 AID</button>
740
+ </div>
741
+
742
+ <div class="aid-list" id="aidList"></div>
743
+
744
+ <div class="status" id="status"></div>
745
+ </div>
746
+ </div>
747
+
748
+ <script>
749
+ let aidData = { aidList: [], aidStatus: [], apiUrl: '' };
750
+
751
+ async function loadAidInfo() {
752
+ try {
753
+ const res = await fetch('/api/aid');
754
+ const data = await res.json();
755
+ aidData = data;
756
+ updateApSelect();
757
+ renderAidList();
758
+ } catch (e) {
759
+ console.error('加载失败', e);
760
+ }
761
+ }
762
+
763
+ function updateApSelect() {
764
+ var sel = document.getElementById('apSelect');
765
+ if (sel && sel.options.length === 0) {
766
+ const options = ['agentcp.io', 'aid.show', 'agentid.pub'];
767
+ options.forEach(function(op) {
768
+ var opt = document.createElement('option');
769
+ opt.value = op;
770
+ opt.textContent = op;
771
+ if (op === 'agentcp.io') opt.selected = true;
772
+ sel.appendChild(opt);
773
+ });
774
+ }
775
+ }
776
+
777
+ function renderAidList() {
778
+ const list = document.getElementById('aidList');
779
+ const createSection = document.getElementById('createSection');
780
+ const hint = document.getElementById('hint');
781
+
782
+ if (aidData.aidList.length >= 10) {
783
+ createSection.style.display = 'none';
784
+ hint.textContent = '已达到 10 个 AID 上限';
785
+ } else {
786
+ createSection.style.display = 'block';
787
+ hint.textContent = '最多注册 10 个 AID(已注册 ' + aidData.aidList.length + ' 个)';
788
+ }
789
+
790
+ if (!aidData.aidStatus || aidData.aidStatus.length === 0) {
791
+ list.innerHTML = '<div style="text-align:center;color:#999;padding:20px;">暂无 AID,请先注册</div>';
792
+ return;
793
+ }
794
+
795
+ list.innerHTML = aidData.aidStatus.map(function(item) {
796
+ var cardClass = 'aid-card';
797
+
798
+ var badges = '';
799
+ if (item.online) badges += '<span class="badge badge-success">已上线</span>';
800
+ if (item.keysExist && item.certValid) {
801
+ badges += '<span class="badge badge-info">密钥有效</span>';
802
+ } else if (item.keysExist && !item.certValid) {
803
+ badges += '<span class="badge badge-warning">证书过期</span>';
804
+ } else {
805
+ badges += '<span class="badge badge-danger">密钥缺失</span>';
806
+ }
807
+
808
+ var actions = '';
809
+ if (item.keysExist && item.certValid) {
810
+ if (item.online) {
811
+ actions += '<button class="btn btn-sm btn-success" onclick="enterChat(\\'' + escapeAttr(item.aid) + '\\')">进入聊天</button>';
812
+ actions += '<button class="btn btn-sm btn-danger" onclick="goOffline(\\'' + escapeAttr(item.aid) + '\\')">下线</button>';
813
+ } else {
814
+ actions += '<button class="btn btn-sm btn-success" id="goBtn_' + escapeAttr(item.aid) + '" onclick="goOnlineAndChat(\\'' + escapeAttr(item.aid) + '\\')">上线并进入</button>';
815
+ }
816
+ }
817
+
818
+ return '<div class="' + cardClass + '">' +
819
+ '<div class="aid-card-left">' +
820
+ '<div class="aid-card-header">' +
821
+ '<span class="aid-name">' + escapeHtml(item.aid) + '</span>' +
822
+ '</div>' +
823
+ '<div class="aid-card-status">' + badges + '</div>' +
824
+ '</div>' +
825
+ '<div class="aid-card-right">' +
826
+ '<div class="aid-card-actions">' + actions + '</div>' +
827
+ '<button class="copy-btn" onclick="copyText(\\'' + escapeAttr(item.aid) + '\\')">复制</button>' +
828
+ '</div>' +
829
+ '</div>';
830
+ }).join('');
831
+ }
832
+
833
+ async function createAid() {
834
+ var prefix = document.getElementById('newAid').value.trim();
835
+ if (!prefix) { showStatus('请输入 AID 名称', 'error'); return; }
836
+ var ap = document.getElementById('apSelect').value;
837
+ if (!ap) { showStatus('请选择 AP', 'error'); return; }
838
+ var fullPrefix = prefix + '.' + ap;
839
+ var nickname = document.getElementById('aidNickname').value.trim();
840
+ var description = document.getElementById('aidDescription').value.trim();
841
+ try {
842
+ var res = await fetch('/api/aid/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prefix: fullPrefix, nickname: nickname, description: description }) });
843
+ var data = await res.json();
844
+ if (data.success) { showStatus('AID 注册成功', 'success'); document.getElementById('newAid').value = ''; document.getElementById('aidNickname').value = ''; document.getElementById('aidDescription').value = ''; loadAidInfo(); }
845
+ else { showStatus(data.error || '注册失败', 'error'); }
846
+ } catch (e) { showStatus('注册失败: ' + e.message, 'error'); }
847
+ }
848
+
849
+ async function selectAid(aid) {
850
+ try {
851
+ var res = await fetch('/api/aid/select', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ aid: aid }) });
852
+ var data = await res.json();
853
+ if (data.success) { showStatus('已切换到 ' + aid, 'success'); loadAidInfo(); }
854
+ else { showStatus(data.error || '切换失败', 'error'); }
855
+ } catch (e) { showStatus('切换失败: ' + e.message, 'error'); }
856
+ }
857
+
858
+ async function goOnlineAndChat(aid) {
859
+ var btn = document.getElementById('goBtn_' + aid);
860
+ if (btn) { btn.disabled = true; btn.textContent = '启动中...'; }
861
+ try {
862
+ showStatus('正在上线 ' + aid + ' ...', 'success');
863
+ var res = await fetch('/api/ws/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ aid: aid }) });
864
+ var data = await res.json();
865
+ if (data.success) {
866
+ showStatus(aid + ' 已上线,正在进入聊天...', 'success');
867
+ window.location.href = '/chat';
868
+ } else {
869
+ showStatus(data.error || '上线失败', 'error');
870
+ if (btn) { btn.disabled = false; btn.textContent = '上线并进入'; }
871
+ }
872
+ } catch (e) {
873
+ showStatus('上线失败: ' + e.message, 'error');
874
+ if (btn) { btn.disabled = false; btn.textContent = '上线并进入'; }
875
+ }
876
+ }
877
+
878
+ function enterChat(aid) { window.location.href = '/chat'; }
879
+
880
+ async function goOffline(aid) {
881
+ try {
882
+ var res = await fetch('/api/aid/offline', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ aid: aid }) });
883
+ var data = await res.json();
884
+ if (data.success) { showStatus(aid + ' 已下线', 'success'); loadAidInfo(); }
885
+ else { showStatus(data.error || '下线失败', 'error'); }
886
+ } catch (e) { showStatus('下线失败: ' + e.message, 'error'); }
887
+ }
888
+
889
+ function copyText(text) {
890
+ navigator.clipboard.writeText(text).then(function() {
891
+ showStatus('已复制', 'success');
892
+ });
893
+ }
894
+
895
+ function showStatus(msg, type) {
896
+ var el = document.getElementById('status');
897
+ el.textContent = msg;
898
+ el.className = 'status ' + type;
899
+ setTimeout(function() { el.className = 'status'; }, 3000);
900
+ }
901
+
902
+ function escapeHtml(text) {
903
+ var div = document.createElement('div');
904
+ div.textContent = text;
905
+ return div.innerHTML;
906
+ }
907
+
908
+ function escapeAttr(text) {
909
+ return text.replace(/'/g, "\\\\'").replace(/"/g, '&quot;');
910
+ }
911
+
912
+ loadAidInfo();
913
+ setInterval(loadAidInfo, 5000);
914
+ <\/script>
915
+ </body>
847
916
  </html>`;
848
- const chatHtml = `<!DOCTYPE html>
849
- <html lang="zh-CN">
850
- <head>
851
- <meta charset="UTF-8">
852
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
853
- <title>ACP 聊天</title>
854
- <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"><\/script>
855
- <style>
856
- :root { --primary:#2563eb; --primary-h:#1d4ed8; --bg:#f3f4f6; --sidebar-bg:#fff; --chat-bg:#f9fafb; --border:#e5e7eb; --t1:#1f2937; --t2:#6b7280; --sent:#2563eb; --recv-bg:#fff; --ok:#10b981; }
857
- * { box-sizing:border-box; margin:0; padding:0; }
858
- body { font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; background:var(--bg); height:100vh; overflow:hidden; color:var(--t1); }
859
- #app { display:flex; height:100%; }
860
-
861
- /* Sidebar */
862
- .sidebar { width:300px; background:var(--sidebar-bg); border-right:1px solid var(--border); display:flex; flex-direction:column; flex-shrink:0; transition:width 0.25s; overflow:hidden; }
863
- .sidebar.collapsed { width:0; border-right:none; }
864
- .sidebar-header { padding:12px 14px; border-bottom:1px solid var(--border); display:flex; flex-direction:column; gap:12px; flex-shrink:0; }
865
- .header-top { display:flex; justify-content:space-between; align-items:center; width:100%; }
866
- .sidebar-header .my-aid { font-size:11px; color:#155724; font-family:monospace; background:#d4edda; padding:4px 8px; border-radius:12px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; border:1px solid #c3e6cb; flex:1; margin-right:8px; }
867
- .new-chat-btn { padding:8px 10px; background:var(--primary); color:#fff; border:none; border-radius:6px; font-size:12px; cursor:pointer; white-space:nowrap; width:100%; text-align:center; }
868
- .new-chat-btn:hover { background:var(--primary-h); }
869
- .session-list { flex:1; overflow-y:auto; }
870
-
871
- /* AID Group */
872
- .aid-group { border-bottom:1px solid var(--border); }
873
- .aid-group-header { padding:12px 14px; display:flex; align-items:center; cursor:pointer; background:linear-gradient(135deg,#eef4ff,#e8f0fe); user-select:none; border-left:3px solid var(--primary); transition:all 0.2s; }
874
- .aid-group-header:hover { background:linear-gradient(135deg,#dbeafe,#d0e4fd); }
875
- .aid-group-info { flex:1; min-width:0; margin-left:4px; }
876
- .aid-group-title { font-size:13px; font-weight:700; color:#1e40af; background:linear-gradient(135deg,#dbeafe,#c7d7fe); padding:2px 8px; border-radius:6px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; display:inline-block; max-width:100%; border:1px solid #bfdbfe; }
877
- .aid-group-desc { font-size:10px; color:#6b7280; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; margin-top:3px; display:block; padding-left:2px; }
878
- .aid-group-arrow { font-size:10px; color:var(--primary); transition:transform 0.2s; flex-shrink:0; }
879
- .aid-group-arrow.open { transform:rotate(90deg); }
880
- .aid-group-badge { font-size:10px; background:var(--primary); color:#fff; padding:1px 6px; border-radius:8px; margin-left:8px; flex-shrink:0; }
881
- .aid-group-add { background:none; border:1px solid var(--border); color:var(--t2); width:22px; height:22px; border-radius:4px; cursor:pointer; font-size:14px; line-height:20px; text-align:center; margin-left:6px; flex-shrink:0; }
882
- .aid-group-add:hover { background:var(--primary); color:#fff; border-color:var(--primary); }
883
- .aid-group-del { background:none; border:none; color:var(--t2); width:20px; height:20px; border-radius:4px; cursor:pointer; font-size:12px; line-height:20px; text-align:center; margin-left:4px; flex-shrink:0; display:none; }
884
- .aid-group-header:hover .aid-group-del { display:block; }
885
- .aid-group-del:hover { color:#dc3545; background:#ffebeb; }
886
- .session-del { position:absolute; right:8px; top:50%; transform:translateY(-50%); background:none; border:none; color:var(--t2); font-size:12px; cursor:pointer; display:none; padding:2px; }
887
- .session-item:hover .session-del { display:block; }
888
- .session-del:hover { color:#dc3545; }
889
- .aid-group-sessions { display:none; background:#fafbfc; }
890
- .aid-group-sessions.open { display:block; }
891
-
892
- .aid-group-avatar { width:36px; height:36px; border-radius:50%; object-fit:cover; flex-shrink:0; margin-right:8px; box-shadow:0 1px 4px rgba(37,99,235,0.18); border:2px solid #bfdbfe; }
893
-
894
- .session-item { padding:10px 14px 10px 32px; border-bottom:1px solid #f0f1f3; cursor:pointer; transition:all 0.15s; position:relative; }
895
- .session-item::before { content:''; position:absolute; left:18px; top:50%; transform:translateY(-50%); width:6px; height:6px; border-radius:50%; background:#d1d5db; }
896
- .session-item:hover { background:#f0f5ff; }
897
- .session-item.active { background:#eff6ff; border-left:3px solid var(--primary); padding-left:29px; }
898
- .session-item.active::before { background:var(--primary); box-shadow:0 0 0 2px rgba(37,99,235,0.2); }
899
- .session-peer { font-weight:500; font-size:12px; color:var(--t1); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; padding-left:10px; background:#f1f5f9; border-radius:4px; padding:3px 8px 3px 10px; border:1px solid #e8ecf1; }
900
- .session-item.active .session-peer { background:#dbeafe; border-color:#bfdbfe; color:#1e40af; }
901
- .session-meta { font-size:10px; color:var(--t2); margin-top:4px; display:flex; align-items:center; gap:6px; padding-left:10px; }
902
- .tag { font-size:9px; padding:1px 5px; border-radius:3px; color:#fff; font-weight:600; letter-spacing:0.3px; }
903
- .tag.outgoing { background:var(--ok); }
904
- .tag.incoming { background:#8b5cf6; }
905
-
906
- /* Chat Area */
907
- .chat-area { flex:1; display:flex; flex-direction:column; background:var(--chat-bg); min-width:0; }
908
- .chat-header { height:54px; padding:0 16px; background:#fff; border-bottom:1px solid var(--border); display:flex; align-items:center; justify-content:space-between; flex-shrink:0; }
909
- .header-left { display:flex; align-items:center; gap:10px; overflow:hidden; }
910
- .toggle-sidebar-btn { background:none; border:none; cursor:pointer; color:var(--t2); padding:4px; display:flex; }
911
- .toggle-sidebar-btn:hover { color:var(--t1); }
912
- .status-dot { width:8px; height:8px; border-radius:50%; background:#ccc; flex-shrink:0; }
913
- .status-dot.connected { background:var(--ok); }
914
- .status-dot.connecting { background:#fbbf24; }
915
- .chat-title { font-size:15px; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
916
-
917
- .aid-select-wrap { display:flex; align-items:center; gap:10px; flex-shrink:0; }
918
- .manage-btn { display:flex; align-items:center; gap:4px; text-decoration:none; color:var(--t2); font-size:12px; padding:6px 10px; border-radius:6px; transition:all 0.2s; background:#fff; border:1px solid var(--border); }
919
- .manage-btn:hover { background:#f8fafc; color:var(--primary); border-color:var(--primary); }
920
- .aid-control-group { display:flex; align-items:center; background:#fff; border:1px solid var(--border); border-radius:6px; padding:2px; box-shadow:0 1px 2px rgba(0,0,0,0.03); }
921
- .aid-select { border:none; background:transparent; font-size:12px; color:var(--t1); padding:5px 8px; outline:none; cursor:pointer; min-width:120px; font-weight:500; }
922
- .status-toggle { display:flex; align-items:center; gap:5px; padding:4px 8px; border-radius:4px; cursor:pointer; font-size:11px; margin-left:2px; transition:background 0.2s; user-select:none; border-left:1px solid var(--border); }
923
- .status-toggle:hover { background:#f1f5f9; }
924
- .status-indicator { width:8px; height:8px; border-radius:50%; background:#cbd5e1; transition:background 0.3s; }
925
- .status-indicator.online { background:var(--ok); box-shadow:0 0 0 2px rgba(16,185,129,0.2); }
926
- .status-indicator.offline { background:#cbd5e1; }
927
-
928
- .collapse-btn { background:none; border:none; cursor:pointer; color:var(--t2); padding:6px; display:flex; align-items:center; flex-shrink:0; }
929
- .collapse-btn:hover { color:var(--t1); }
930
-
931
- .encrypt-banner { background:linear-gradient(135deg,#e0f2fe,#dbeafe); border:1px solid #bae6fd; border-radius:8px; padding:8px 14px; margin:8px 16px 0; display:flex; align-items:center; gap:8px; font-size:11px; color:#0369a1; flex-shrink:0; }
932
- .encrypt-banner svg { flex-shrink:0; }
933
-
934
- .messages { flex:1; padding:16px; overflow-y:auto; display:flex; flex-direction:column; gap:12px; }
935
- .message { display:flex; flex-direction:column; max-width:80%; }
936
- .message.sent { align-self:flex-end; align-items:flex-end; }
937
- .message.received { align-self:flex-start; align-items:flex-start; }
938
- .bubble { padding:10px 14px; border-radius:12px; font-size:14px; line-height:1.5; word-wrap:break-word; box-shadow:0 1px 2px rgba(0,0,0,0.05); }
939
- .message.sent .bubble { background:var(--sent); color:#fff; border-bottom-right-radius:2px; }
940
- .message.received .bubble { background:var(--recv-bg); color:var(--t1); border-bottom-left-radius:2px; border:1px solid var(--border); }
941
- .msg-meta { font-size:10px; color:var(--t2); margin-top:3px; padding:0 4px; }
942
-
943
- .input-area { padding:12px 16px; background:#fff; border-top:1px solid var(--border); display:flex; align-items:center; gap:10px; flex-shrink:0; }
944
- .input-area input { flex:1; padding:10px 14px; border-radius:20px; border:1px solid var(--border); font-size:14px; background:#f9fafb; }
945
- .input-area input:focus { outline:none; border-color:var(--primary); background:#fff; }
946
- .send-btn { width:40px; height:40px; border-radius:50%; background:var(--primary); border:none; color:#fff; display:flex; align-items:center; justify-content:center; cursor:pointer; flex-shrink:0; }
947
- .send-btn:hover { background:var(--primary-h); }
948
- .send-btn:disabled { background:#ccc; cursor:not-allowed; }
949
-
950
- .modal-overlay { position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.5); z-index:50; display:none; align-items:center; justify-content:center; }
951
- .modal-overlay.show { display:flex; }
952
- .modal { background:#fff; width:90%; max-width:400px; border-radius:12px; padding:24px; box-shadow:0 10px 25px rgba(0,0,0,0.1); }
953
- .modal h3 { margin-bottom:16px; font-size:16px; }
954
- .modal input[type="text"], .modal input[type="password"], .modal input[type="url"] { width:100%; padding:10px; border:1px solid var(--border); border-radius:8px; margin-bottom:16px; font-size:14px; box-sizing:border-box; }
955
- .modal input[type="text"]:focus, .modal input[type="password"]:focus, .modal input[type="url"]:focus { outline:none; border-color:var(--primary); }
956
- .modal input[type="radio"] { width:auto; margin:0; }
957
- .modal-btns { display:flex; justify-content:flex-end; gap:10px; }
958
- .mbtn { padding:8px 16px; border-radius:6px; font-size:13px; cursor:pointer; border:none; }
959
- .mbtn-cancel { background:#f3f4f6; color:var(--t1); }
960
- .mbtn-ok { background:var(--primary); color:#fff; }
961
- .mbtn-ok:disabled { background:#ccc; }
962
-
963
- .bubble p { margin-bottom:0.4em; } .bubble p:last-child { margin-bottom:0; }
964
- .bubble h1, .bubble h2, .bubble h3, .bubble h4, .bubble h5, .bubble h6 { font-weight:600; line-height:1.25; margin-top:1em; margin-bottom:0.5em; color:inherit; }
965
- .bubble h1 { font-size:1.5em; border-bottom:1px solid rgba(0,0,0,0.1); padding-bottom:0.3em; }
966
- .bubble h2 { font-size:1.3em; border-bottom:1px solid rgba(0,0,0,0.05); padding-bottom:0.3em; }
967
- .bubble h3 { font-size:1.1em; }
968
- .bubble ul, .bubble ol { padding-left:1.5em; margin-bottom:0.5em; }
969
- .bubble li { margin-bottom:0.2em; }
970
- .bubble blockquote { margin:0.5em 0; padding-left:1em; border-left:4px solid rgba(0,0,0,0.1); color:var(--t2); }
971
- .bubble a { color:var(--primary); text-decoration:underline; } .bubble a:hover { opacity:0.85; }
972
- .message.sent .bubble a { color:#fff; } .message.sent .bubble a:hover { opacity:0.85; }
973
- .bubble img { max-width:100%; border-radius:4px; }
974
- .bubble code { background:rgba(0,0,0,0.1); padding:2px 4px; border-radius:3px; font-family:monospace; font-size:0.9em; }
975
- .bubble pre { background:#2d2d2d; color:#fff; padding:12px; border-radius:6px; overflow-x:auto; margin:8px 0; }
976
- .bubble pre code { background:transparent; padding:0; color:inherit; border-radius:0; }
977
- .messages { flex:1; padding:16px; overflow-y:auto; display:flex; flex-direction:column; gap:12px; }
978
- .message { display:flex; flex-direction:row; max-width:85%; gap:8px; }
979
- .message.sent { align-self:flex-end; flex-direction:row-reverse; }
980
- .message.received { align-self:flex-start; }
981
- .msg-avatar { width:40px; height:40px; border-radius:50%; object-fit:cover; flex-shrink:0; box-shadow:0 1px 2px rgba(0,0,0,0.1); margin-top:4px; }
982
- .msg-content { display:flex; flex-direction:column; max-width:100%; min-width:0; }
983
- .message.sent .msg-content { align-items:flex-end; }
984
- .message.received .msg-content { align-items:flex-start; }
985
- @media (min-width: 1024px) { .message { max-width: 70%; } }
986
-
987
- @media (max-width:768px) {
988
- .sidebar { position:absolute; height:100%; z-index:20; width:280px; }
989
- .sidebar.collapsed { width:0; }
990
- }
991
-
992
- /* Group UI Styles */
993
- .tab-bar { display:flex; border-bottom:1px solid var(--border); flex-shrink:0; }
994
- .tab-bar .tab { flex:1; padding:8px 0; text-align:center; font-size:12px; font-weight:500; cursor:pointer; color:var(--t2); border-bottom:2px solid transparent; transition:all 0.2s; }
995
- .tab-bar .tab.active { color:var(--primary); border-bottom-color:var(--primary); }
996
- .tab-bar .tab:hover { color:var(--t1); }
997
- .group-list { flex:1; overflow-y:auto; }
998
- .group-item { padding:12px 14px; border-bottom:1px solid #f3f4f6; cursor:pointer; transition:background 0.15s; position:relative; }
999
- .group-item:hover { background:#f5f7fa; }
1000
- .group-item.active { background:#eff6ff; border-left:3px solid var(--primary); padding-left:11px; }
1001
- .group-item-name { font-size:13px; font-weight:600; color:var(--t1); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
1002
- .group-item-meta { font-size:10px; color:var(--t2); margin-top:2px; }
1003
- .group-item-del { position:absolute; right:8px; top:12px; background:none; border:none; color:var(--t2); font-size:12px; cursor:pointer; display:none; padding:2px; }
1004
- .group-item:hover .group-item-del { display:block; }
1005
- .group-item-del:hover { color:#dc3545; }
1006
- .group-actions { padding:8px 14px; display:flex; gap:6px; flex-shrink:0; border-bottom:1px solid var(--border); }
1007
- .group-actions .gbtn { flex:1; padding:6px 0; border:1px solid var(--border); border-radius:6px; font-size:11px; cursor:pointer; background:#fff; color:var(--t1); text-align:center; }
1008
- .group-actions .gbtn:hover { background:#f1f5f9; border-color:var(--primary); color:var(--primary); }
1009
- .group-info-bar { padding:6px 16px; background:#f0f9ff; border-bottom:1px solid #bae6fd; font-size:11px; color:#0369a1; display:flex; align-items:center; gap:8px; flex-shrink:0; }
1010
- .group-info-bar .copy-link { cursor:pointer; text-decoration:underline; }
1011
- .group-info-bar .copy-link:hover { color:#0284c7; }
1012
- </style>
1013
- <!-- CHATHTML_STYLE_END -->
1014
- </head>
1015
- <body>
1016
- <div id="app">
1017
- <div class="sidebar" id="sidebar">
1018
- <div class="sidebar-header">
1019
- <div class="header-top">
1020
- <span class="my-aid" id="myAid">Loading...</span>
1021
- <button class="collapse-btn" onclick="toggleSidebar()" title="收起面板">
1022
- <svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M15 19l-7-7 7-7"></path></svg>
1023
- </button>
1024
- </div>
1025
- <div class="tab-bar">
1026
- <div class="tab active" id="tabP2P" onclick="switchTab('p2p')">聊天</div>
1027
- <div class="tab" id="tabGroup" onclick="switchTab('group')">群组</div>
1028
- </div>
1029
- </div>
1030
- <!-- P2P panel -->
1031
- <div id="p2pPanel">
1032
- <div style="padding:8px 14px;flex-shrink:0;"><button class="new-chat-btn" onclick="showModal()">+ 连接龙虾</button></div>
1033
- <div class="session-list" id="sessionList"></div>
1034
- </div>
1035
- <!-- Group panel -->
1036
- <div id="groupPanel" style="display:none;flex:1;display:none;flex-direction:column;overflow:hidden;">
1037
- <div class="group-actions">
1038
- <div class="gbtn" onclick="showCreateGroupModal()">创建群组</div>
1039
- <div class="gbtn" onclick="showJoinGroupModal()">加入群组</div>
1040
- <div class="gbtn" onclick="showMyGroups()">我的群</div>
1041
- </div>
1042
- <div class="group-list" id="groupList"><div style="padding:20px;text-align:center;color:#999;font-size:12px;">暂无群组</div></div>
1043
- </div>
1044
- </div>
1045
- <div class="chat-area">
1046
- <div class="chat-header">
1047
- <div class="header-left">
1048
- <button class="toggle-sidebar-btn" onclick="toggleSidebar()">
1049
- <svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M3 12h18M3 6h18M3 18h18"></path></svg>
1050
- </button>
1051
- <div class="status-dot" id="statusDot"></div>
1052
- <div class="chat-title" id="chatTitle">未选择会话</div>
1053
- </div>
1054
- <div class="aid-select-wrap">
1055
- <a href="/" class="manage-btn" title="ACP 身份管理">
1056
- <svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg> 身份管理
1057
- </a>
1058
- <div class="aid-control-group">
1059
- <select class="aid-select" id="aidSelect" onchange="switchAid(this.value)"></select>
1060
- <div class="status-toggle" id="aidStatusToggle" onclick="toggleOnline()" title="点击切换在线状态">
1061
- <div class="status-indicator" id="aidOnlineDot"></div>
1062
- <span id="aidStatusText" style="color:var(--t2);">...</span>
1063
- </div>
1064
- </div>
1065
- </div>
1066
- </div>
1067
- <div class="encrypt-banner" id="encryptBanner">
1068
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>
1069
- <span>ACP Agent 点对点加密通信 — 消息经端到端加密传输,仅通信双方可读</span>
1070
- </div>
1071
- <div class="group-info-bar" id="groupInfoBar" style="display:none;">
1072
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>
1073
- <span id="groupInfoText">群组</span>
1074
- <span class="copy-link" id="groupInviteBtn" onclick="generateInviteLink()" title="生成邀请链接" style="display:none;">生成邀请链接</span>
1075
- <span class="copy-link" id="groupCopyLinkBtn" onclick="copyGroupLink()" title="复制群链接" style="display:none;">复制群链接</span>
1076
- <span class="copy-link" onclick="showGroupMembers()" title="查看成员">成员</span>
1077
- <span class="copy-link" id="groupReviewBtn" onclick="showPendingRequests()" title="查看入群申请" style="display:none;">审核</span>
1078
- </div>
1079
- <div class="messages" id="messages">
1080
- <div style="text-align:center;color:var(--t2);margin-top:40px;">
1081
- <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#cbd5e1" stroke-width="1.5" style="margin-bottom:10px;"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>
1082
- <div style="font-size:14px;font-weight:500;color:#64748b;margin-bottom:4px;">ACP Agent 安全通信</div>
1083
- <div style="font-size:12px;color:#94a3b8;">选择或创建一个会话,开始点对点加密聊天</div>
1084
- </div>
1085
- </div>
1086
- <div class="input-area">
1087
- <input type="text" id="messageInput" placeholder="输入消息..." onkeypress="if(event.key==='Enter')sendMessage()">
1088
- <button class="send-btn" id="sendBtn" onclick="sendMessage()">
1089
- <svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"></path></svg>
1090
- </button>
1091
- </div>
1092
- </div>
1093
- </div>
1094
- <div class="modal-overlay" id="modal">
1095
- <div class="modal">
1096
- <h3>连接 ACP 龙虾</h3>
1097
- <input type="text" id="targetAidInput" placeholder="输入对方 AID" onkeypress="if(event.key==='Enter')doConnect()">
1098
- <div class="modal-btns">
1099
- <button class="mbtn mbtn-cancel" onclick="hideModal()">取消</button>
1100
- <button class="mbtn mbtn-ok" id="connectBtn" onclick="doConnect()">连接</button>
1101
- </div>
1102
- </div>
1103
- </div>
1104
- <div class="modal-overlay" id="createGroupModal">
1105
- <div class="modal">
1106
- <h3>创建群组</h3>
1107
- <input type="text" id="groupNameInput" placeholder="输入群组名称">
1108
- <textarea id="groupDescInput" placeholder="输入群组描述(可选)" style="width:100%;padding:10px;border:1px solid var(--border);border-radius:8px;margin-bottom:16px;font-size:14px;resize:vertical;min-height:60px;font-family:inherit;"></textarea>
1109
- <div style="margin-bottom:16px;">
1110
- <label style="font-size:13px;color:var(--t2);margin-bottom:8px;display:block;">群组类型</label>
1111
- <div style="display:flex;gap:12px;">
1112
- <label style="display:flex;align-items:center;gap:6px;cursor:pointer;font-size:14px;"><input type="radio" name="groupVisibility" value="public" checked> 公开群</label>
1113
- <label style="display:flex;align-items:center;gap:6px;cursor:pointer;font-size:14px;"><input type="radio" name="groupVisibility" value="private"> 私密群</label>
1114
- </div>
1115
- </div>
1116
- <div class="modal-btns">
1117
- <button class="mbtn mbtn-cancel" onclick="hideCreateGroupModal()">取消</button>
1118
- <button class="mbtn mbtn-ok" id="createGroupBtn" onclick="doCreateGroup()">创建</button>
1119
- </div>
1120
- </div>
1121
- </div>
1122
- <div class="modal-overlay" id="joinGroupModal">
1123
- <div class="modal">
1124
- <h3>加入群组</h3>
1125
- <input type="text" id="joinGroupUrlInput" placeholder="输入群聊链接或邀请链接" onkeypress="if(event.key==='Enter')doJoinGroup()">
1126
- <div style="font-size:11px;color:var(--t2);margin:-8px 0 12px 2px;">粘贴邀请链接可直接加入,普通群链接将发送入群申请</div>
1127
- <div class="modal-btns">
1128
- <button class="mbtn mbtn-cancel" onclick="hideJoinGroupModal()">取消</button>
1129
- <button class="mbtn mbtn-ok" id="joinGroupBtn" onclick="doJoinGroup()">加入</button>
1130
- </div>
1131
- </div>
1132
- </div>
1133
- <div class="modal-overlay" id="membersModal">
1134
- <div class="modal" style="max-width:520px;">
1135
- <h3>群组成员</h3>
1136
- <div id="membersList" style="max-height:400px;overflow-y:auto;margin-bottom:16px;font-size:13px;"></div>
1137
- <div class="modal-btns">
1138
- <button class="mbtn mbtn-cancel" onclick="hideMembersModal()">关闭</button>
1139
- </div>
1140
- </div>
1141
- </div>
1142
- <div class="modal-overlay" id="pendingRequestsModal">
1143
- <div class="modal" style="max-width:480px;">
1144
- <h3>入群申请</h3>
1145
- <div id="pendingRequestsList" style="max-height:360px;overflow-y:auto;margin-bottom:16px;font-size:13px;"></div>
1146
- <div class="modal-btns">
1147
- <button class="mbtn mbtn-cancel" onclick="hidePendingRequestsModal()">关闭</button>
1148
- </div>
1149
- </div>
1150
- </div>
1151
- <div class="modal-overlay" id="myGroupsModal">
1152
- <div class="modal" style="max-width:560px;">
1153
- <h3>我的群</h3>
1154
- <div id="myGroupsContent" style="max-height:420px;overflow-y:auto;margin-bottom:16px;font-size:13px;"></div>
1155
- <div class="modal-btns">
1156
- <button class="mbtn mbtn-cancel" onclick="hideMyGroupsModal()">关闭</button>
1157
- </div>
1158
- </div>
1159
- </div>
1160
- <script>
1161
- var S = { aid:'', sid:null, sessions:[], status:'disconnected', expanded:{}, sidebarOpen:true, aidList:[], closed:false, tab:'p2p', activeGroupId:null, groups:[], groupMsgs:[], groupTargetAid:'', isGroupCreator:false };
1162
- var D = {};
1163
- var agentInfoCache = {};
1164
- function $(id){ return document.getElementById(id); }
1165
- function getAvatarSrc(type) {
1166
- if (type === 'openclaw') return '/assets/openclaw.png';
1167
- if (type === 'human') return '/assets/human.png';
1168
- return '/assets/agent.png';
1169
- }
1170
- async function fetchAgentInfo(aid) {
1171
- if (agentInfoCache[aid]) return agentInfoCache[aid];
1172
- try {
1173
- var r = await fetch('/api/agent-info?aid=' + encodeURIComponent(aid));
1174
- var d = await r.json();
1175
- if (d.type || d.name) { agentInfoCache[aid] = d; }
1176
- return d;
1177
- } catch(e) { return { type:'', name:'', description:'', tags:[] }; }
1178
- }
1179
- async function deleteSession(e, sessionId){
1180
- e.stopPropagation();
1181
- if(!confirm('确认删除该会话?')) return;
1182
- try {
1183
- var r = await fetch('/api/sessions/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionId: sessionId }) });
1184
- var d = await r.json();
1185
- if(d.success){
1186
- if(S.sid === sessionId){ S.sid = null; D.title.textContent='未选择会话'; D.msgs.innerHTML=''; D.input.disabled=false; }
1187
- D.sList.dataset.s=''; // force update
1188
- poll();
1189
- } else { alert(d.error || '删除失败'); }
1190
- } catch(err){ alert('删除失败: ' + err.message); }
1191
- }
1192
-
1193
- async function deletePeer(e, peerAid){
1194
- e.stopPropagation();
1195
- if(!confirm('确认删除与 ' + peerAid + ' 的所有会话?')) return;
1196
- try {
1197
- var r = await fetch('/api/peers/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ peerAid: peerAid }) });
1198
- var d = await r.json();
1199
- if(d.success){
1200
- S.sid = null; D.title.textContent='未选择会话'; D.msgs.innerHTML='';
1201
- D.sList.dataset.s=''; // force update
1202
- poll();
1203
- } else { alert(d.error || '删除失败'); }
1204
- } catch(err){ alert('删除失败: ' + err.message); }
1205
- }
1206
-
1207
- function initDom(){ D.myAid=$('myAid'); D.sList=$('sessionList'); D.title=$('chatTitle'); D.msgs=$('messages'); D.input=$('messageInput'); D.sendBtn=$('sendBtn'); D.dot=$('statusDot'); D.modal=$('modal'); D.tInput=$('targetAidInput'); D.cBtn=$('connectBtn'); D.sidebar=$('sidebar'); D.aidSel=$('aidSelect'); D.aidDot=$('aidOnlineDot'); D.aidStatusToggle=$('aidStatusToggle'); D.aidStatusText=$('aidStatusText'); D.p2pPanel=$('p2pPanel'); D.groupPanel=$('groupPanel'); D.groupList=$('groupList'); D.groupInfoBar=$('groupInfoBar'); D.groupInfoText=$('groupInfoText'); D.tabP2P=$('tabP2P'); D.tabGroup=$('tabGroup'); D.encryptBanner=$('encryptBanner'); }
1208
-
1209
- async function init(){
1210
- initDom();
1211
- try {
1212
- var r = await fetch('/api/aid'); var d = await r.json();
1213
- if(d.currentAid){
1214
- S.aid=d.currentAid; D.myAid.textContent='我的身份: '+d.currentAid; D.myAid.title=d.currentAid;
1215
- // 填充 AID 切换下拉
1216
- S.aidList=d.aidStatus||[];
1217
- renderAidSelect();
1218
- // 初始加载一次ws状态,后续通过WebSocket推送更新
1219
- fetch('/api/ws/status').then(function(r){return r.json();}).then(function(d){updateDot(d.status);}).catch(function(){});
1220
- poll(); setInterval(poll,1000);
1221
- } else { window.location.href='/'; }
1222
- } catch(e){ console.error(e); }
1223
- }
1224
-
1225
- function renderAidSelect(){
1226
- var html='';
1227
- var curOnline=false;
1228
- S.aidList.forEach(function(a){
1229
- var sel=a.aid===S.aid?' selected':'';
1230
- if(a.aid===S.aid) curOnline=a.online;
1231
- html+='<option value="'+escH(a.aid)+'"'+sel+'>'+escH(a.aid)+'</option>';
1232
- });
1233
- D.aidSel.innerHTML=html;
1234
- D.aidDot.className='status-indicator '+(curOnline?'online':'offline');
1235
- D.aidStatusText.textContent=curOnline?'已上线':'离线';
1236
- D.aidStatusText.style.color=curOnline?'#10b981':'#64748b';
1237
- D.aidStatusToggle.title=curOnline?'点击下线':'点击上线';
1238
- }
1239
-
1240
- async function switchAid(aid){
1241
- if(aid===S.aid) return;
1242
- try {
1243
- var r=await fetch('/api/aid/select',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({aid:aid})});
1244
- var d=await r.json();
1245
- if(!d.success) return;
1246
- // 检查是否在线,不在线则自动上线
1247
- var r2=await fetch('/api/aid'); var d2=await r2.json();
1248
- var aidList=d2.aidStatus||[];
1249
- var info=aidList.find(function(a){ return a.aid===aid; });
1250
- if(!info || !info.online){
1251
- await fetch('/api/ws/start',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({aid:aid})});
1252
- }
1253
- // 刷新页面以加载新身份的数据
1254
- location.reload();
1255
- } catch(e){}
1256
- }
1257
-
1258
- async function toggleOnline(){
1259
- var info=S.aidList.find(function(a){ return a.aid===S.aid; });
1260
- var isOnline=info&&info.online;
1261
- D.aidStatusText.textContent='...';
1262
- try {
1263
- if(isOnline){
1264
- await fetch('/api/aid/offline',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({aid:S.aid})});
1265
- } else {
1266
- await fetch('/api/ws/start',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({aid:S.aid})});
1267
- }
1268
- // AID 状态变更通过 WS 推送 aid_status 自动更新,无需再拉取
1269
- } catch(e){}
1270
- }
1271
-
1272
- async function poll(){
1273
- try {
1274
- // P2P会话和消息仅在P2P标签页时刷新
1275
- if(S.tab==='p2p'){
1276
- var [sr,mr] = await Promise.all([fetch('/api/sessions'),fetch('/api/messages')]);
1277
- var sd=await sr.json(), md=await mr.json();
1278
- if(sd.sessions) updateSessions(sd.sessions, sd.activeSessionId);
1279
- S.closed=md.closed||false;
1280
- if(md.messages) renderMsgs(md.messages, S.closed);
1281
- }
1282
- } catch(e){}
1283
- }
1284
-
1285
- function updateSessions(sessions, activeId){
1286
- var sig=JSON.stringify(sessions)+activeId+S.sid;
1287
- if(D.sList.dataset.s===sig) return;
1288
- D.sList.dataset.s=sig;
1289
- if(activeId && S.sid!==activeId) S.sid=activeId;
1290
- S.sessions=sessions;
1291
-
1292
- var groups={};
1293
- sessions.forEach(function(s){
1294
- var peer=s.peerAid||'unknown';
1295
- if(!groups[peer]) groups[peer]=[];
1296
- groups[peer].push(s);
1297
- });
1298
-
1299
- if(!sessions.length){
1300
- D.sList.innerHTML='<div style="padding:20px;text-align:center;color:#999;font-size:12px;">暂无会话</div>';
1301
- return;
1302
- }
1303
-
1304
- var html='';
1305
- var peers=Object.keys(groups);
1306
- peers.sort(function(a,b){
1307
- var la=groups[a][0].lastMessageAt, lb=groups[b][0].lastMessageAt;
1308
- return lb-la;
1309
- });
1310
- peers.forEach(function(peer){
1311
- var isOpen = S.expanded[peer] !== false;
1312
- var list=groups[peer];
1313
- var shortPeer=peer.length>22?peer.substring(0,22)+'...':peer;
1314
- var cached=agentInfoCache[peer];
1315
- var avatarType=cached?cached.type:'';
1316
- var avatarSrc=getAvatarSrc(avatarType);
1317
- var displayName=(cached&&cached.name)?cached.name:shortPeer;
1318
- var fullDisplayName=(cached&&cached.name)?cached.name:peer;
1319
- var descText=(cached&&cached.description)?cached.description:peer;
1320
- html+='<div class="aid-group">';
1321
- html+='<div class="aid-group-header" onclick="toggleGroup(\\''+escA(peer)+'\\')"><span class="aid-group-arrow'+(isOpen?' open':'')+'">&#9654;</span><img class="aid-group-avatar" id="avatar_'+escH(peer.replace(/\\./g,'_'))+'" src="'+avatarSrc+'" alt="avatar"><div class="aid-group-info"><span class="aid-group-title" title="'+escH(fullDisplayName)+'">'+escH(displayName)+'</span><span class="aid-group-desc" id="desc_'+escH(peer.replace(/\\./g,'_'))+'" title="'+escH(peer)+'">'+escH(descText)+'</span></div><span class="aid-group-badge">'+list.length+'</span><button class="aid-group-add" onclick="event.stopPropagation();newSessionWith(\\''+escA(peer)+'\\');" title="与该 AID 新建会话">+</button><button class="aid-group-del" onclick="event.stopPropagation();deletePeer(event, \\''+escA(peer)+'\\');" title="删除该 AID 及所有会话">🗑️</button></div>';
1322
- html+='<div class="aid-group-sessions'+(isOpen?' open':'')+'">';
1323
- list.forEach(function(s){
1324
- var active=s.sessionId===S.sid;
1325
- var time=fmtTime(s.lastMessageAt);
1326
- var tc=s.type==='outgoing'?'outgoing':'incoming';
1327
- var tt=s.type==='outgoing'?'OUT':'IN';
1328
- var name=s.lastMessage||'';
1329
- var fullName=name;
1330
- if(name.length>20) name=name.substring(0,20)+'...';
1331
- if(!name) name='(空会话)';
1332
- var closedTag=s.closed?'<span style="color:#dc3545;font-size:10px;margin-left:4px;">[已关闭]</span>':'';
1333
- html+='<div class="session-item'+(active?' active':'')+'" onclick="pickSession(\\''+escA(s.sessionId)+'\\',\\''+escA(s.peerAid)+'\\')"><div class="session-peer" title="'+escH(fullName)+'"><span class="tag '+tc+'">'+tt+'</span> '+escH(name)+closedTag+'</div><div class="session-meta"><span>'+s.messageCount+' 条 · '+time+'</span></div><button class="session-del" onclick="event.stopPropagation();deleteSession(event, \\''+escA(s.sessionId)+'\\');" title="删除会话">🗑️</button></div>';
1334
- });
1335
- html+='</div></div>';
1336
- });
1337
- D.sList.innerHTML=html;
1338
-
1339
- // 异步加载未缓存的 agent info 并更新头像和名称
1340
- peers.forEach(function(peer){
1341
- if(!agentInfoCache[peer]){
1342
- fetchAgentInfo(peer).then(function(info){
1343
- var safeId=peer.replace(/\\./g,'_');
1344
- var el=document.getElementById('avatar_'+safeId);
1345
- if(el) el.src=getAvatarSrc(info.type);
1346
- if(info.name){
1347
- var header=el&&el.parentElement;
1348
- if(header){
1349
- var titleEl=header.querySelector('.aid-group-title');
1350
- if(titleEl){ titleEl.textContent=info.name; titleEl.title=info.name; }
1351
- }
1352
- }
1353
- if(info.description){
1354
- var descEl=document.getElementById('desc_'+safeId);
1355
- if(descEl){ descEl.textContent=info.description; descEl.title=info.description; }
1356
- }
1357
- });
1358
- }
1359
- });
1360
- }
1361
-
1362
- function toggleGroup(owner){
1363
- S.expanded[owner] = S.expanded[owner]===false ? true : false;
1364
- D.sList.dataset.s=''; // force re-render
1365
- updateSessions(S.sessions, S.sid);
1366
- }
1367
-
1368
- function renderMsgs(msgs, closed){
1369
- var sig=msgs.length+(msgs.length>0?msgs[msgs.length-1].timestamp:0)+(closed?'c':'');
1370
- // Check if we need to re-render due to avatar updates (simple check: if sig matches but we want to force update, we might need another flag, but for now relies on sig change or manual call)
1371
- // Actually, let's allow re-render if we call it.
1372
- if(D.msgs.dataset.s==sig && !D.msgs.dataset.force) return;
1373
- D.msgs.dataset.s=sig;
1374
- D.msgs.dataset.force=''; // clear force flag
1375
-
1376
- if(!msgs.length){
1377
- D.msgs.innerHTML='<div style="text-align:center;color:#ccc;margin-top:20px;font-size:12px;">暂无消息</div>';
1378
- D.input.disabled=false; D.input.placeholder='输入消息...';
1379
- return;
1380
- }
1381
- var html=msgs.map(function(m){
1382
- var sent=m.type==='sent';
1383
- var sender = sent ? S.aid : (m.from || 'unknown');
1384
- var info = agentInfoCache[sender];
1385
- if(!info){
1386
- fetchAgentInfo(sender).then(function(){
1387
- if(D.msgs.dataset.s===sig){ D.msgs.dataset.force='1'; renderMsgs(msgs, closed); }
1388
- });
1389
- }
1390
- var avatarSrc = getAvatarSrc(info ? info.type : '');
1391
- var t=fmtTime(m.timestamp);
1392
- var c=(typeof marked!=='undefined'&&marked.parse)?marked.parse(m.content):escH(m.content);
1393
- var name = (info && info.name) ? info.name : sender;
1394
-
1395
- return '<div class="message '+m.type+'">' +
1396
- '<img class="msg-avatar" src="'+avatarSrc+'" title="'+escH(name)+'">' +
1397
- '<div class="msg-content">' +
1398
- '<div class="bubble">'+c+'</div>' +
1399
- '<div class="msg-meta">'+(sent?'我':escH(name))+' · '+t+'</div>' +
1400
- '</div></div>';
1401
- }).join('');
1402
- if(closed){
1403
- html+='<div style="text-align:center;margin:16px 0;"><div style="display:inline-block;background:#fff3cd;color:#856404;padding:8px 20px;border-radius:20px;font-size:12px;border:1px solid #ffc107;">会话已关闭 — 请点击左侧 + 新建会话继续通信</div></div>';
1404
- D.input.disabled=true; D.input.placeholder='会话已关闭,请新建会话';
1405
- } else {
1406
- D.input.disabled=false; D.input.placeholder='输入消息...';
1407
- }
1408
- var wasAtBottom=D.msgs.scrollHeight-D.msgs.scrollTop-D.msgs.clientHeight<150;
1409
- D.msgs.innerHTML=html;
1410
- if(wasAtBottom) D.msgs.scrollTop=D.msgs.scrollHeight;
1411
- }
1412
-
1413
- function updateDot(st){
1414
- S.status=st;
1415
- D.dot.className='status-dot '+(st||'');
1416
- }
1417
-
1418
- async function pickSession(sid,peer){
1419
- S.sid=sid;
1420
- D.title.textContent=peer;
1421
- try {
1422
- await fetch('/api/sessions/active',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sessionId:sid})});
1423
- var r=await fetch('/api/messages'); var d=await r.json();
1424
- S.closed=d.closed||false;
1425
- D.msgs.dataset.s=''; // force
1426
- renderMsgs(d.messages||[], S.closed);
1427
- } catch(e){}
1428
- }
1429
-
1430
- async function sendMessage(){
1431
- var txt=D.input.value.trim();
1432
- if(!txt){ return; }
1433
- // 群组模式
1434
- if(S.tab==='group'){
1435
- if(!S.activeGroupId){ alert('请先选择一个群组'); return; }
1436
- try {
1437
- D.input.value='';
1438
- var r=await fetch('/api/group/send',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({groupId:S.activeGroupId,message:txt})});
1439
- var d=await r.json();
1440
- if(!d.success) alert(d.error||'发送失败');
1441
- else {
1442
- // 发送成功:立即追加到本地显示(服务端已存储,不用等 WS 推送)
1443
- if(d.msg_id){
1444
- var sentMsg={msg_id:d.msg_id,sender:S.aid,content:txt,content_type:'text',timestamp:d.timestamp||Date.now()};
1445
- var exists=_lastGroupMsgs.some(function(m){ return m.msg_id===sentMsg.msg_id; });
1446
- if(!exists){
1447
- _lastGroupMsgs.push(sentMsg);
1448
- _lastGroupMsgSig='';
1449
- renderGroupMsgs(_lastGroupMsgs);
1450
- }
1451
- }
1452
- }
1453
- } catch(e){ alert('发送失败'); }
1454
- return;
1455
- }
1456
- // P2P 模式
1457
- if(!S.sid){ alert('请先选择或新建一个会话'); return; }
1458
- if(S.closed){ alert('该会话已关闭,请新建会话继续通信'); return; }
1459
- try {
1460
- D.input.value='';
1461
- var r=await fetch('/api/ws/send',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:txt,sessionId:S.sid})});
1462
- var d=await r.json();
1463
- if(!d.success) alert(d.error||'发送失败');
1464
- } catch(e){ alert('发送失败'); }
1465
- }
1466
-
1467
- function toggleSidebar(){
1468
- S.sidebarOpen=!S.sidebarOpen;
1469
- D.sidebar.classList.toggle('collapsed',!S.sidebarOpen);
1470
- }
1471
-
1472
- function showModal(){ D.modal.classList.add('show'); D.tInput.value=''; D.tInput.focus(); }
1473
- function hideModal(){ D.modal.classList.remove('show'); }
1474
-
1475
- async function newSessionWith(peerAid){
1476
- try {
1477
- var r=await fetch('/api/ws/connect',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({targetAid:peerAid})});
1478
- var d=await r.json();
1479
- if(d.success){ pickSession(d.sessionId,peerAid); }
1480
- else { alert(d.error||'连接失败'); }
1481
- } catch(e){ alert('错误: '+e.message); }
1482
- }
1483
-
1484
- async function doConnect(){
1485
- var aid=D.tInput.value.trim();
1486
- if(!aid) return;
1487
- D.cBtn.disabled=true; D.cBtn.textContent='连接中...';
1488
- try {
1489
- var r=await fetch('/api/ws/connect',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({targetAid:aid})});
1490
- var d=await r.json();
1491
- if(d.success){ hideModal(); pickSession(d.sessionId,aid); }
1492
- else { alert(d.error||'连接失败'); }
1493
- } catch(e){ alert('错误: '+e.message); }
1494
- finally { D.cBtn.disabled=false; D.cBtn.textContent='连接'; }
1495
- }
1496
-
1497
- function escH(t){ var d=document.createElement('div'); d.textContent=t; return d.innerHTML; }
1498
- function escA(t){ return t.replace(/\\\\/g,'\\\\\\\\').replace(/'/g,"\\\\'"); }
1499
- function fmtTime(ts){
1500
- if(!ts) return '';
1501
- var n=Number(ts);
1502
- if(isNaN(n)) return '';
1503
- if(n<1e12) n=n*1000;
1504
- var d=new Date(n);
1505
- if(isNaN(d.getTime())) return '';
1506
- var now=new Date();
1507
- var pad=function(v){ return v<10?'0'+v:''+v; };
1508
- var H=pad(d.getHours()), M=pad(d.getMinutes()), ss=pad(d.getSeconds());
1509
- if(d.getFullYear()===now.getFullYear()&&d.getMonth()===now.getMonth()&&d.getDate()===now.getDate()){
1510
- return H+':'+M+':'+ss;
1511
- }
1512
- return d.getFullYear()+'/'+pad(d.getMonth()+1)+'/'+pad(d.getDate())+' '+H+':'+M+':'+ss;
1513
- }
1514
-
1515
- // ============================================================
1516
- // Group Functions
1517
- // ============================================================
1518
-
1519
- function switchTab(tab){
1520
- S.tab=tab;
1521
- D.tabP2P.className='tab'+(tab==='p2p'?' active':'');
1522
- D.tabGroup.className='tab'+(tab==='group'?' active':'');
1523
- D.p2pPanel.style.display=tab==='p2p'?'block':'none';
1524
- D.groupPanel.style.display=tab==='group'?'flex':'none';
1525
- if(tab==='group'){
1526
- D.encryptBanner.style.display='none';
1527
- D.groupInfoBar.style.display=S.activeGroupId?'flex':'none';
1528
- D.input.placeholder='输入群消息...';
1529
- D.input.disabled=!S.activeGroupId;
1530
- D.msgs.dataset.s='';
1531
- _lastGroupMsgSig='';
1532
- initGroupClient();
1533
- pollGroupList();
1534
- if(S.activeGroupId) pollGroupMessages();
1535
- else { D.msgs.innerHTML='<div style="text-align:center;color:#ccc;margin-top:20px;font-size:12px;">选择或创建一个群组</div>'; }
1536
- } else {
1537
- D.encryptBanner.style.display='flex';
1538
- D.groupInfoBar.style.display='none';
1539
- D.input.placeholder='输入消息...';
1540
- D.input.disabled=false;
1541
- _lastGroupMsgSig='';
1542
- // 切回P2P时立即刷新消息
1543
- D.msgs.dataset.s='';
1544
- if(S.sid){
1545
- fetch('/api/messages').then(function(r){ return r.json(); }).then(function(d){
1546
- S.closed=d.closed||false;
1547
- if(d.messages) renderMsgs(d.messages, S.closed);
1548
- }).catch(function(){});
1549
- } else {
1550
- D.msgs.innerHTML='';
1551
- }
1552
- }
1553
- }
1554
-
1555
- var _groupInited=false;
1556
- async function initGroupClient(){
1557
- if(_groupInited) return;
1558
- try {
1559
- var r=await fetch('/api/group/init',{method:'POST'});
1560
- var d=await r.json();
1561
- if(d.success){ _groupInited=true; if(d.targetAid) S.groupTargetAid=d.targetAid; }
1562
- } catch(e){ console.error('群组初始化失败',e); }
1563
- }
1564
-
1565
- async function pollGroupList(){
1566
- try {
1567
- var r=await fetch('/api/group/list');
1568
- var d=await r.json();
1569
- if(d.groups){ S.groups=d.groups; renderGroupList(); }
1570
- } catch(e){}
1571
- }
1572
-
1573
- function renderGroupList(){
1574
- if(!S.groups.length){
1575
- D.groupList.innerHTML='<div style="padding:20px;text-align:center;color:#999;font-size:12px;">暂无群组</div>';
1576
- return;
1577
- }
1578
- var html='';
1579
- S.groups.forEach(function(g){
1580
- var active=g.group_id===S.activeGroupId;
1581
- html+='<div class="group-item'+(active?' active':'')+'" onclick="pickGroup(\\''+escA(g.group_id)+'\\',\\''+escA(g.name||g.group_id)+'\\')"><div class="group-item-name">'+escH(g.name||g.group_id)+'</div><div class="group-item-meta">ID: '+escH(g.group_id.length>20?g.group_id.substring(0,20)+'...':g.group_id)+(g.member_count?' · '+g.member_count+' 人':'')+'</div><button class="group-item-del" onclick="event.stopPropagation();leaveGroup(\\''+escA(g.group_id)+'\\');" title="退出群组">退出</button></div>';
1582
- });
1583
- D.groupList.innerHTML=html;
1584
- }
1585
-
1586
- async function pickGroup(groupId,name){
1587
- S.activeGroupId=groupId;
1588
- S.isGroupCreator=false;
1589
- D.title.textContent=name;
1590
- D.groupInfoBar.style.display='flex';
1591
- D.groupInfoText.textContent=name;
1592
- D.input.disabled=false;
1593
- D.input.placeholder='输入群消息...';
1594
- D.input.focus();
1595
- // 默认隐藏创建者相关按钮
1596
- $('groupInviteBtn').style.display='none';
1597
- $('groupCopyLinkBtn').style.display='none';
1598
- $('groupReviewBtn').style.display='none';
1599
- renderGroupList();
1600
- try {
1601
- await fetch('/api/group/select',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({groupId:groupId})});
1602
- } catch(e){}
1603
- // 获取群信息判断是否为创建者
1604
- try {
1605
- var r=await fetch('/api/group/info?groupId='+encodeURIComponent(groupId));
1606
- var d=await r.json();
1607
- if(d.creator&&d.creator===S.aid){
1608
- S.isGroupCreator=true;
1609
- $('groupInviteBtn').style.display='';
1610
- $('groupReviewBtn').style.display='';
1611
- } else {
1612
- $('groupCopyLinkBtn').style.display='';
1613
- }
1614
- } catch(e){
1615
- // 获取失败时默认显示复制群链接
1616
- $('groupCopyLinkBtn').style.display='';
1617
- }
1618
- pollGroupMessages();
1619
- }
1620
-
1621
- var _lastGroupMsgSig='';
1622
- async function pollGroupMessages(){
1623
- if(!S.activeGroupId||S.tab!=='group') return;
1624
- try {
1625
- var r=await fetch('/api/group/messages?groupId='+encodeURIComponent(S.activeGroupId));
1626
- var d=await r.json();
1627
- if(d.messages) renderGroupMsgs(d.messages);
1628
- } catch(e){}
1629
- }
1630
-
1631
- // ============================================================
1632
- // WebSocket: real-time group message push
1633
- // ============================================================
1634
- var _groupWs=null;
1635
- var _groupWsReconnectTimer=null;
1636
-
1637
- function connectGroupWs(){
1638
- if(_groupWs&&(_groupWs.readyState===WebSocket.OPEN||_groupWs.readyState===WebSocket.CONNECTING)) return;
1639
- var proto=location.protocol==='https:'?'wss:':'ws:';
1640
- _groupWs=new WebSocket(proto+'//'+location.host+'/ws/group');
1641
- _groupWs.onopen=function(){
1642
- console.log('[WS] group connected');
1643
- if(_groupWsReconnectTimer){ clearTimeout(_groupWsReconnectTimer); _groupWsReconnectTimer=null; }
1644
- // 重连后主动拉取最新状态,防止断连期间丢失推送
1645
- fetch('/api/ws/status').then(function(r){return r.json();}).then(function(d){updateDot(d.status);}).catch(function(){});
1646
- fetch('/api/aid').then(function(r){return r.json();}).then(function(d){if(d.aidStatus){S.aidList=d.aidStatus;renderAidSelect();}}).catch(function(){});
1647
- };
1648
- _groupWs.onmessage=function(ev){
1649
- try {
1650
- var data=JSON.parse(ev.data);
1651
- handleGroupWsMessage(data);
1652
- } catch(e){ console.error('[WS] parse error',e); }
1653
- };
1654
- _groupWs.onclose=function(){
1655
- console.log('[WS] group disconnected, reconnecting in 3s...');
1656
- _groupWs=null;
1657
- _groupWsReconnectTimer=setTimeout(connectGroupWs,3000);
1658
- };
1659
- _groupWs.onerror=function(e){
1660
- console.error('[WS] group error',e);
1661
- };
1662
- }
1663
-
1664
- function handleGroupWsMessage(data){
1665
- if(data.type==='ws_status'){
1666
- if(!data.aid||data.aid===S.aid) updateDot(data.status);
1667
- return;
1668
- }
1669
- if(data.type==='aid_status'){
1670
- S.aidList=data.aidStatus||[];
1671
- renderAidSelect();
1672
- return;
1673
- }
1674
- if(data.type==='group_message'){
1675
- // 实时推送的完整消息
1676
- var msg=data.message;
1677
- var gid=data.group_id;
1678
- if(gid===S.activeGroupId&&S.tab==='group'){
1679
- // 追加到当前消息列表并重新渲染
1680
- var exists=_lastGroupMsgs.some(function(m){ return m.msg_id===msg.msg_id; });
1681
- if(!exists){
1682
- _lastGroupMsgs.push(msg);
1683
- _lastGroupMsgSig=''; // 强制重新渲染
1684
- renderGroupMsgs(_lastGroupMsgs);
1685
- }
1686
- }
1687
- } else if(data.type==='group_message_batch'){
1688
- // 批量推送的消息列表
1689
- var gid=data.group_id;
1690
- var msgs=data.messages||[];
1691
- if(gid===S.activeGroupId&&S.tab==='group'){
1692
- var changed=false;
1693
- msgs.forEach(function(msg){
1694
- var exists=_lastGroupMsgs.some(function(m){ return m.msg_id===msg.msg_id; });
1695
- if(!exists){
1696
- _lastGroupMsgs.push(msg);
1697
- changed=true;
1698
- }
1699
- });
1700
- if(changed){
1701
- _lastGroupMsgSig=''; // 强制重新渲染
1702
- renderGroupMsgs(_lastGroupMsgs);
1703
- }
1704
- }
1705
- } else if(data.type==='new_message_notify'){
1706
- // 轻量通知:如果是当前活跃群组,拉取最新消息(本地读取,很快)
1707
- if(data.group_id===S.activeGroupId&&S.tab==='group'){
1708
- pollGroupMessages();
1709
- }
1710
- } else if(data.type==='join_approved'||data.type==='group_invite'){
1711
- // 群组变动,刷新群组列表
1712
- pollGroupList();
1713
- }
1714
- }
1715
-
1716
- // 启动 WebSocket 连接
1717
- connectGroupWs();
1718
-
1719
- var _lastGroupMsgs=[];
1720
- function renderGroupMsgs(msgs){
1721
- var sig=msgs.length+(msgs.length>0?(msgs[msgs.length-1].msg_id||0):'');
1722
- if(_lastGroupMsgSig===sig&&!msgs._forceRender) return;
1723
- _lastGroupMsgSig=sig;
1724
- _lastGroupMsgs=msgs;
1725
- if(!msgs.length){
1726
- D.msgs.innerHTML='<div style="text-align:center;color:#ccc;margin-top:20px;font-size:12px;">暂无消息</div>';
1727
- return;
1728
- }
1729
- var needFetch=[];
1730
- var html=msgs.map(function(m){
1731
- var sent=m.sender===S.aid;
1732
- var sender=m.sender||'unknown';
1733
- var info=agentInfoCache[sender];
1734
- if(!info){ needFetch.push(sender); }
1735
- var avatarSrc=getAvatarSrc(info?info.type:'');
1736
- var t=m.timestamp?fmtTime(m.timestamp):'';
1737
-
1738
- var c=(typeof marked!=='undefined'&&marked.parse)?marked.parse(m.content||''):escH(m.content||'');
1739
- var name=(info&&info.name)?info.name:sender;
1740
- return '<div class="message '+(sent?'sent':'received')+'">' +
1741
- '<img class="msg-avatar" src="'+avatarSrc+'" title="'+escH(name)+'">' +
1742
- '<div class="msg-content">' +
1743
- '<div class="bubble">'+c+'</div>' +
1744
- '<div class="msg-meta">'+(sent?'我':escH(name))+' · '+t+'</div>' +
1745
- '</div></div>';
1746
- }).join('');
1747
- var wasAtBottom=D.msgs.scrollHeight-D.msgs.scrollTop-D.msgs.clientHeight<150;
1748
- D.msgs.innerHTML=html;
1749
- if(wasAtBottom) D.msgs.scrollTop=D.msgs.scrollHeight;
1750
- // 异步加载未缓存的 agent info,加载完成后重新渲染以更新头像
1751
- var unique=needFetch.filter(function(v,i,a){ return a.indexOf(v)===i; });
1752
- unique.forEach(function(aid){
1753
- fetchAgentInfo(aid).then(function(){
1754
- _lastGroupMsgSig='';
1755
- _lastGroupMsgs._forceRender=true;
1756
- renderGroupMsgs(_lastGroupMsgs);
1757
- });
1758
- });
1759
- }
1760
-
1761
- // Group modals
1762
- function showCreateGroupModal(){ $('createGroupModal').classList.add('show'); $('groupNameInput').value=''; $('groupDescInput').value=''; document.querySelector('input[name="groupVisibility"][value="public"]').checked=true; $('groupNameInput').focus(); }
1763
- function hideCreateGroupModal(){ $('createGroupModal').classList.remove('show'); }
1764
- function showJoinGroupModal(){ $('joinGroupModal').classList.add('show'); $('joinGroupUrlInput').value=''; $('joinGroupUrlInput').focus(); }
1765
- function hideJoinGroupModal(){ $('joinGroupModal').classList.remove('show'); }
1766
- function hideMembersModal(){ $('membersModal').classList.remove('show'); }
1767
-
1768
- async function doCreateGroup(){
1769
- var name=$('groupNameInput').value.trim();
1770
- if(!name) return;
1771
- var description=$('groupDescInput').value.trim();
1772
- var visibility=document.querySelector('input[name="groupVisibility"]:checked').value;
1773
- var btn=$('createGroupBtn');
1774
- btn.disabled=true; btn.textContent='创建中...';
1775
- try {
1776
- var r=await fetch('/api/group/create',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:name,visibility:visibility,description:description||undefined})});
1777
- var d=await r.json();
1778
- if(d.success){
1779
- hideCreateGroupModal();
1780
- pollGroupList();
1781
- pickGroup(d.group_id,name);
1782
- } else { alert(d.error||'创建失败'); }
1783
- } catch(e){ alert('创建失败: '+e.message); }
1784
- finally { btn.disabled=false; btn.textContent='创建'; }
1785
- }
1786
-
1787
- async function doJoinGroup(){
1788
- var rawUrl=$('joinGroupUrlInput').value.trim();
1789
- if(!rawUrl){ alert('请输入群聊链接或邀请链接'); return; }
1790
- // 从 URL 中解析 code 参数
1791
- var code='';
1792
- var groupUrl=rawUrl;
1793
- try {
1794
- var u=new URL(rawUrl);
1795
- code=u.searchParams.get('code')||'';
1796
- u.searchParams.delete('code');
1797
- groupUrl=u.origin+u.pathname;
1798
- } catch(e){}
1799
- var btn=$('joinGroupBtn');
1800
- btn.disabled=true; btn.textContent=code?'加入中...':'申请中...';
1801
- try {
1802
- var r=await fetch('/api/group/join',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({groupUrl:groupUrl,code:code||undefined})});
1803
- var d=await r.json();
1804
- if(d.success){
1805
- hideJoinGroupModal();
1806
- pollGroupList();
1807
- if(d.group_id) pickGroup(d.group_id,d.group_id);
1808
- if(d.pending) alert('入群申请已发送,请等待管理员审核');
1809
- } else { alert(d.error||'操作失败'); }
1810
- } catch(e){ alert('操作失败: '+e.message); }
1811
- finally { btn.disabled=false; btn.textContent='加入'; }
1812
- }
1813
-
1814
- async function copyGroupLink(){
1815
- if(!S.activeGroupId) return;
1816
- var groupUrl='https://'+S.groupTargetAid+'/'+S.activeGroupId;
1817
- try { await navigator.clipboard.writeText(groupUrl); alert('群链接已复制到剪贴板\\n\\n'+groupUrl); }
1818
- catch(e){ prompt('请复制群链接:',groupUrl); }
1819
- }
1820
-
1821
- async function generateInviteLink(){
1822
- if(!S.activeGroupId) return;
1823
- try {
1824
- var r=await fetch('/api/group/invite-code',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({groupId:S.activeGroupId})});
1825
- var d=await r.json();
1826
- if(d.success&&d.code){
1827
- var baseUrl=d.group_url||('https://'+S.groupTargetAid+'/'+S.activeGroupId);
1828
- var inviteUrl=baseUrl+'?code='+d.code;
1829
- try {
1830
- await navigator.clipboard.writeText(inviteUrl);
1831
- alert('邀请链接已复制到剪贴板\\n\\n'+inviteUrl);
1832
- } catch(e){
1833
- prompt('请手动复制邀请链接:',inviteUrl);
1834
- }
1835
- } else { alert(d.error||'生成邀请码失败'); }
1836
- } catch(e){ alert('生成邀请码失败: '+e.message); }
1837
- }
1838
-
1839
- function copyMemberAid(btn,aid){
1840
- navigator.clipboard.writeText(aid).then(function(){
1841
- btn.textContent='已复制';
1842
- setTimeout(function(){ btn.textContent='复制'; },1200);
1843
- });
1844
- }
1845
-
1846
- async function openAgentMdPage(aid){
1847
- try {
1848
- var r=await fetch('/api/agent-md-raw?aid='+encodeURIComponent(aid));
1849
- var d=await r.json();
1850
- if(!d.success||!d.content){ alert(d.error||'获取 agent.md 失败'); return; }
1851
- var md=d.content;
1852
- // 简单 markdown 渲染
1853
- function renderMd(src){
1854
- var h=escH(src);
1855
- // headings
1856
- h=h.replace(/^######\\s+(.+)$/gm,'<h6>$1</h6>');
1857
- h=h.replace(/^#####\\s+(.+)$/gm,'<h5>$1</h5>');
1858
- h=h.replace(/^####\\s+(.+)$/gm,'<h4>$1</h4>');
1859
- h=h.replace(/^###\\s+(.+)$/gm,'<h3>$1</h3>');
1860
- h=h.replace(/^##\\s+(.+)$/gm,'<h2>$1</h2>');
1861
- h=h.replace(/^#\\s+(.+)$/gm,'<h1>$1</h1>');
1862
- // bold & italic
1863
- h=h.replace(/\\*\\*(.+?)\\*\\*/g,'<strong>$1</strong>');
1864
- h=h.replace(/\\*(.+?)\\*/g,'<em>$1</em>');
1865
- // blockquote
1866
- h=h.replace(/^&gt;\\s?(.+)$/gm,'<blockquote style="border-left:3px solid #ddd;padding-left:12px;color:#666;margin:8px 0;">$1</blockquote>');
1867
- // list items
1868
- h=h.replace(/^-\\s+(.+)$/gm,'<li>$1</li>');
1869
- // code inline
1870
- var bt=String.fromCharCode(96);
1871
- h=h.replace(new RegExp(bt+'([^'+bt+']+)'+bt,'g'),'<code style="background:#f5f5f5;padding:1px 4px;border-radius:3px;font-size:12px;">$1</code>');
1872
- // frontmatter block: hide ---...---
1873
- h=h.replace(/^---[\\s\\S]*?---\\s*/,'');
1874
- // paragraphs
1875
- h=h.replace(/\\n\\n/g,'</p><p>');
1876
- h='<p>'+h+'</p>';
1877
- return h;
1878
- }
1879
- var html='<!DOCTYPE html><html><head><meta charset="utf-8"><title>'+escH(aid)+' - Agent Profile</title>'
1880
- +'<style>body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;max-width:720px;margin:40px auto;padding:0 20px;color:#333;line-height:1.6;}'
1881
- +'h1{border-bottom:2px solid #eee;padding-bottom:8px;}h2{border-bottom:1px solid #eee;padding-bottom:6px;margin-top:24px;}'
1882
- +'ul{padding-left:20px;}li{margin:4px 0;}blockquote{margin:12px 0;}'
1883
- +'pre{background:#f5f5f5;padding:12px;border-radius:6px;overflow-x:auto;}'
1884
- +'.aid-badge{display:inline-block;background:#e8f4fd;color:#0969da;padding:2px 8px;border-radius:10px;font-size:12px;font-family:monospace;margin-bottom:16px;}'
1885
- +'</style></head><body>'
1886
- +'<div class="aid-badge">'+escH(aid)+'</div>'
1887
- +renderMd(md)
1888
- +'</body></html>';
1889
- var w=window.open('','_blank');
1890
- if(w){ w.document.write(html); w.document.close(); }
1891
- else { alert('弹窗被拦截,请允许弹窗后重试'); }
1892
- } catch(e){ alert('获取 agent.md 失败: '+e.message); }
1893
- }
1894
-
1895
- async function showGroupMembers(){
1896
- if(!S.activeGroupId) return;
1897
- try {
1898
- var r=await fetch('/api/group/members?groupId='+encodeURIComponent(S.activeGroupId));
1899
- var d=await r.json();
1900
- if(d.members){
1901
- var html=d.members.map(function(m){
1902
- var aid=m.agent_id||m;
1903
- if(typeof aid!=='string') aid=JSON.stringify(aid);
1904
- var role=m.role||'';
1905
- var cachedInfo=agentInfoCache[aid];
1906
- var avatarSrc=getAvatarSrc(cachedInfo?cachedInfo.type:'');
1907
- var displayName=(cachedInfo&&cachedInfo.name)?cachedInfo.name:aid.split('.')[0];
1908
- var typeTags='';
1909
- if(cachedInfo&&cachedInfo.tags&&cachedInfo.tags.length){
1910
- typeTags=cachedInfo.tags.map(function(t){ return '<span style="display:inline-block;background:#e8f4fd;color:#0969da;padding:1px 6px;border-radius:8px;font-size:10px;margin-right:4px;">'+escH(t)+'</span>'; }).join('');
1911
- } else if(cachedInfo&&cachedInfo.type){
1912
- typeTags='<span style="display:inline-block;background:#e8f4fd;color:#0969da;padding:1px 6px;border-radius:8px;font-size:10px;">'+escH(cachedInfo.type)+'</span>';
1913
- }
1914
- if(role){ typeTags+='<span style="display:inline-block;background:#fff3cd;color:#856404;padding:1px 6px;border-radius:8px;font-size:10px;margin-left:4px;">'+escH(role)+'</span>'; }
1915
- var safeId='member-'+escH(aid).replace(/\\./g,'_');
1916
- return '<div id="'+safeId+'" style="padding:10px 0;border-bottom:1px solid #f3f4f6;display:flex;align-items:center;gap:10px;">'
1917
- +'<img src="'+avatarSrc+'" style="width:36px;height:36px;border-radius:50%;flex-shrink:0;" class="member-avatar" data-aid="'+escH(aid)+'">'
1918
- +'<div style="flex:1;min-width:0;">'
1919
- +'<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap;">'
1920
- +'<span style="font-size:13px;font-weight:500;" class="member-name" data-aid="'+escH(aid)+'">'+escH(displayName)+'</span>'
1921
- +'<span class="member-tags" data-aid="'+escH(aid)+'">'+typeTags+'</span>'
1922
- +'</div>'
1923
- +'<div style="font-size:11px;color:var(--t2);font-family:monospace;margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">'+escH(aid)+'</div>'
1924
- +'</div>'
1925
- +'<div style="display:flex;gap:4px;flex-shrink:0;">'
1926
- +'<button class="mbtn mbtn-ok" style="padding:4px 10px;font-size:11px;" onclick="copyMemberAid(this,\\''+escH(aid)+'\\')">复制</button>'
1927
- +'<button class="mbtn mbtn-cancel" style="padding:4px 10px;font-size:11px;" onclick="openAgentMdPage(\\''+escH(aid)+'\\')">查看</button>'
1928
- +'</div></div>';
1929
- }).join('');
1930
- $('membersList').innerHTML=html||'<div style="color:#999;">暂无成员</div>';
1931
- // 异步加载未缓存的 agent info
1932
- d.members.forEach(function(m){
1933
- var aid=m.agent_id||m;
1934
- if(typeof aid!=='string') aid=JSON.stringify(aid);
1935
- if(!aid||agentInfoCache[aid]) return;
1936
- fetchAgentInfo(aid).then(function(info){
1937
- if(!info||(!info.name&&!info.type)) return;
1938
- var safeId='member-'+aid.replace(/\\./g,'_');
1939
- var el=document.getElementById(safeId);
1940
- if(!el) return;
1941
- var avatarEl=el.querySelector('.member-avatar[data-aid="'+aid+'"]');
1942
- var nameEl=el.querySelector('.member-name[data-aid="'+aid+'"]');
1943
- var tagsEl=el.querySelector('.member-tags[data-aid="'+aid+'"]');
1944
- if(avatarEl) avatarEl.src=getAvatarSrc(info.type);
1945
- if(nameEl) nameEl.textContent=info.name||aid.split('.')[0];
1946
- if(tagsEl){
1947
- var tags='';
1948
- if(info.tags&&info.tags.length){
1949
- tags=info.tags.map(function(t){ return '<span style="display:inline-block;background:#e8f4fd;color:#0969da;padding:1px 6px;border-radius:8px;font-size:10px;margin-right:4px;">'+escH(t)+'</span>'; }).join('');
1950
- } else if(info.type){
1951
- tags='<span style="display:inline-block;background:#e8f4fd;color:#0969da;padding:1px 6px;border-radius:8px;font-size:10px;">'+escH(info.type)+'</span>';
1952
- }
1953
- // 保留已有的 role tag
1954
- var existingRole=tagsEl.querySelector('span[style*="fff3cd"]');
1955
- tagsEl.innerHTML=tags+(existingRole?existingRole.outerHTML:'');
1956
- }
1957
- });
1958
- });
1959
- } else { $('membersList').innerHTML='<div style="color:#999;">获取失败</div>'; }
1960
- $('membersModal').classList.add('show');
1961
- } catch(e){ alert('获取成员失败: '+e.message); }
1962
- }
1963
-
1964
- function hidePendingRequestsModal(){ $('pendingRequestsModal').classList.remove('show'); }
1965
-
1966
- async function showPendingRequests(){
1967
- if(!S.activeGroupId) return;
1968
- try {
1969
- var r=await fetch('/api/group/pending-requests?groupId='+encodeURIComponent(S.activeGroupId));
1970
- var d=await r.json();
1971
- if(d.requests&&d.requests.length>0){
1972
- // 先渲染基础结构,然后异步加载 agent info
1973
- var html=d.requests.map(function(req){
1974
- var aid=req.agent_id||'';
1975
- var msg=req.message?escH(req.message):'';
1976
- var time=req.created_at?fmtTime(req.created_at):'';
1977
- var cachedInfo=agentInfoCache[aid];
1978
- var avatarSrc=getAvatarSrc(cachedInfo?cachedInfo.type:'');
1979
- var displayName=(cachedInfo&&cachedInfo.name)?cachedInfo.name:aid;
1980
- var desc=(cachedInfo&&cachedInfo.description)?cachedInfo.description:'';
1981
- return '<div id="pending-'+escH(aid).replace(/\\./g,'_')+'" style="padding:10px 0;border-bottom:1px solid #f3f4f6;display:flex;align-items:flex-start;gap:10px;">'
1982
- +'<img src="'+avatarSrc+'" style="width:36px;height:36px;border-radius:50%;flex-shrink:0;margin-top:2px;" class="pending-avatar" data-aid="'+escH(aid)+'">'
1983
- +'<div style="flex:1;min-width:0;">'
1984
- +'<div style="font-size:13px;font-weight:500;" class="pending-name" data-aid="'+escH(aid)+'">'+escH(displayName)+'</div>'
1985
- +'<div style="font-size:11px;color:var(--t2);font-family:monospace;margin-top:2px;">'+escH(aid)+'</div>'
1986
- +'<div style="font-size:11px;color:var(--t2);margin-top:2px;display:'+(desc?'block':'none')+';" class="pending-desc" data-aid="'+escH(aid)+'">'+escH(desc)+'</div>'
1987
- +(msg?'<div style="font-size:11px;color:#666;margin-top:4px;background:#f8f9fa;padding:4px 8px;border-radius:4px;">申请留言: '+msg+'</div>':'')
1988
- +(time?'<div style="font-size:10px;color:var(--t2);margin-top:3px;">'+time+'</div>':'')
1989
- +'</div>'
1990
- +'<div style="display:flex;gap:4px;flex-shrink:0;margin-top:2px;">'
1991
- +'<button class="mbtn mbtn-ok" style="padding:4px 10px;font-size:11px;" onclick="reviewJoin(\\''+escH(aid)+'\\',\\'approve\\')">通过</button>'
1992
- +'<button class="mbtn mbtn-cancel" style="padding:4px 10px;font-size:11px;" onclick="reviewJoin(\\''+escH(aid)+'\\',\\'reject\\')">拒绝</button>'
1993
- +'</div></div>';
1994
- }).join('');
1995
- $('pendingRequestsList').innerHTML=html;
1996
- // 异步加载未缓存的 agent info
1997
- d.requests.forEach(function(req){
1998
- var aid=req.agent_id||'';
1999
- if(!aid||agentInfoCache[aid]) return;
2000
- fetchAgentInfo(aid).then(function(info){
2001
- if(!info||(!info.name&&!info.type)) return;
2002
- var safeId='pending-'+aid.replace(/\\./g,'_');
2003
- var el=document.getElementById(safeId);
2004
- if(!el) return;
2005
- var avatarEl=el.querySelector('.pending-avatar[data-aid="'+aid+'"]');
2006
- var nameEl=el.querySelector('.pending-name[data-aid="'+aid+'"]');
2007
- var descEl=el.querySelector('.pending-desc[data-aid="'+aid+'"]');
2008
- if(avatarEl) avatarEl.src=getAvatarSrc(info.type);
2009
- if(nameEl) nameEl.textContent=info.name||aid;
2010
- if(descEl&&info.description){ descEl.textContent=info.description; descEl.style.display='block'; }
2011
- });
2012
- });
2013
- } else {
2014
- $('pendingRequestsList').innerHTML='<div style="padding:16px;text-align:center;color:#999;font-size:12px;">暂无入群申请</div>';
2015
- }
2016
- $('pendingRequestsModal').classList.add('show');
2017
- } catch(e){ alert('获取入群申请失败: '+e.message); }
2018
- }
2019
-
2020
- async function reviewJoin(agentId,action){
2021
- if(!S.activeGroupId) return;
2022
- try {
2023
- var r=await fetch('/api/group/review-join',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({groupId:S.activeGroupId,agentId:agentId,action:action})});
2024
- var d=await r.json();
2025
- if(d.success){ showPendingRequests(); }
2026
- else { alert(d.error||'操作失败'); }
2027
- } catch(e){ alert('操作失败: '+e.message); }
2028
- }
2029
-
2030
- async function leaveGroup(groupId){
2031
- if(!confirm('确认退出该群组?')) return;
2032
- try {
2033
- var r=await fetch('/api/group/leave',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({groupId:groupId})});
2034
- var d=await r.json();
2035
- if(d.success){
2036
- if(S.activeGroupId===groupId){
2037
- S.activeGroupId=null;
2038
- D.title.textContent='未选择群组';
2039
- D.groupInfoBar.style.display='none';
2040
- D.msgs.innerHTML='';
2041
- D.input.disabled=true;
2042
- }
2043
- pollGroupList();
2044
- } else { alert(d.error||'退出失败'); }
2045
- } catch(e){ alert('退出失败: '+e.message); }
2046
- }
2047
-
2048
- // ============================================================
2049
- // 我的群 Functions
2050
- // ============================================================
2051
- function showMyGroupsModal(){ $('myGroupsModal').classList.add('show'); }
2052
- function hideMyGroupsModal(){ $('myGroupsModal').classList.remove('show'); }
2053
- async function showMyGroups(){
2054
- showMyGroupsModal();
2055
- $('myGroupsContent').innerHTML='<div style="text-align:center;padding:20px;color:#999;">加载中...</div>';
2056
- try {
2057
- var r=await fetch('/api/group/my-groups');
2058
- var d=await r.json();
2059
- if(!d.success){ $('myGroupsContent').innerHTML='<div style="text-align:center;padding:20px;color:#e74c3c;">'+escH(d.error||'获取失败')+'</div>'; return; }
2060
- var groups=d.groups||[];
2061
- if(!groups.length){ $('myGroupsContent').innerHTML='<div style="text-align:center;padding:20px;color:#999;">暂无群组</div>'; return; }
2062
- var html='<table style="width:100%;border-collapse:collapse;font-size:12px;">';
2063
- html+='<tr style="background:#f8fafc;"><th style="padding:8px 6px;text-align:left;border-bottom:1px solid #e2e8f0;">群名称</th><th style="padding:8px 6px;text-align:left;border-bottom:1px solid #e2e8f0;">群ID</th><th style="padding:8px 6px;text-align:center;border-bottom:1px solid #e2e8f0;">角色</th><th style="padding:8px 6px;text-align:center;border-bottom:1px solid #e2e8f0;">状态</th></tr>';
2064
- groups.forEach(function(g){
2065
- var statusText=g.status===1?'正常':g.status===0?'待审核':'未知('+g.status+')';
2066
- var statusColor=g.status===1?'#10b981':g.status===0?'#f59e0b':'#94a3b8';
2067
- var shortId=g.group_id.length>16?g.group_id.substring(0,16)+'...':g.group_id;
2068
- html+='<tr style="border-bottom:1px solid #f1f5f9;cursor:pointer;" onmouseover="this.style.background=\\'#f0f9ff\\'" onmouseout="this.style.background=\\'\\'">';
2069
- html+='<td style="padding:8px 6px;font-weight:500;">'+escH(g.name||g.group_id)+'</td>';
2070
- html+='<td style="padding:8px 6px;color:#64748b;" title="'+escH(g.group_id)+'">'+escH(shortId)+'</td>';
2071
- html+='<td style="padding:8px 6px;text-align:center;">'+escH(g.role||'-')+'</td>';
2072
- html+='<td style="padding:8px 6px;text-align:center;"><span style="color:'+statusColor+';font-weight:500;">'+escH(statusText)+'</span></td>';
2073
- html+='</tr>';
2074
- });
2075
- html+='</table>';
2076
- html+='<div style="margin-top:8px;font-size:11px;color:#94a3b8;text-align:right;">共 '+d.total+' 个群组</div>';
2077
- $('myGroupsContent').innerHTML=html;
2078
- } catch(e){ $('myGroupsContent').innerHTML='<div style="text-align:center;padding:20px;color:#e74c3c;">请求失败: '+escH(e.message)+'</div>'; }
2079
- }
2080
-
2081
- // 扩展轮询:保留 P2P 等基础轮询,群组消息已通过 WebSocket 实时推送
2082
- // 不再每秒轮询群消息
2083
-
2084
- init();
2085
- <\/script>
2086
- </body>
917
+ const chatHtml = `<!DOCTYPE html>
918
+ <html lang="zh-CN">
919
+ <head>
920
+ <meta charset="UTF-8">
921
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
922
+ <title>ACP 聊天</title>
923
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"><\/script>
924
+ <style>
925
+ :root { --primary:#2563eb; --primary-h:#1d4ed8; --bg:#f3f4f6; --sidebar-bg:#fff; --chat-bg:#f9fafb; --border:#e5e7eb; --t1:#1f2937; --t2:#6b7280; --sent:#2563eb; --recv-bg:#fff; --ok:#10b981; }
926
+ * { box-sizing:border-box; margin:0; padding:0; }
927
+ body { font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; background:var(--bg); height:100vh; overflow:hidden; color:var(--t1); }
928
+ #app { display:flex; height:100%; }
929
+
930
+ /* Sidebar */
931
+ .sidebar { width:300px; background:var(--sidebar-bg); border-right:1px solid var(--border); display:flex; flex-direction:column; flex-shrink:0; transition:width 0.25s; overflow:hidden; }
932
+ .sidebar.collapsed { width:0; border-right:none; }
933
+ .sidebar-header { padding:12px 14px; border-bottom:1px solid var(--border); display:flex; flex-direction:column; gap:12px; flex-shrink:0; }
934
+ .header-top { display:flex; justify-content:space-between; align-items:center; width:100%; }
935
+ .sidebar-header .my-aid { font-size:11px; color:#155724; font-family:monospace; background:#d4edda; padding:4px 8px; border-radius:12px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; border:1px solid #c3e6cb; flex:1; margin-right:8px; }
936
+ .new-chat-btn { padding:8px 10px; background:var(--primary); color:#fff; border:none; border-radius:6px; font-size:12px; cursor:pointer; white-space:nowrap; width:100%; text-align:center; }
937
+ .new-chat-btn:hover { background:var(--primary-h); }
938
+ .session-list { flex:1; overflow-y:auto; }
939
+
940
+ /* AID Group */
941
+ .aid-group { border-bottom:1px solid var(--border); }
942
+ .aid-group-header { padding:12px 14px; display:flex; align-items:center; cursor:pointer; background:linear-gradient(135deg,#eef4ff,#e8f0fe); user-select:none; border-left:3px solid var(--primary); transition:all 0.2s; }
943
+ .aid-group-header:hover { background:linear-gradient(135deg,#dbeafe,#d0e4fd); }
944
+ .aid-group-info { flex:1; min-width:0; margin-left:4px; }
945
+ .aid-group-title { font-size:13px; font-weight:700; color:#1e40af; background:linear-gradient(135deg,#dbeafe,#c7d7fe); padding:2px 8px; border-radius:6px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; display:inline-block; max-width:100%; border:1px solid #bfdbfe; }
946
+ .aid-group-desc { font-size:10px; color:#6b7280; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; margin-top:3px; display:block; padding-left:2px; }
947
+ .aid-group-arrow { font-size:10px; color:var(--primary); transition:transform 0.2s; flex-shrink:0; }
948
+ .aid-group-arrow.open { transform:rotate(90deg); }
949
+ .aid-group-badge { font-size:10px; background:var(--primary); color:#fff; padding:1px 6px; border-radius:8px; margin-left:8px; flex-shrink:0; }
950
+ .aid-group-add { background:none; border:1px solid var(--border); color:var(--t2); width:22px; height:22px; border-radius:4px; cursor:pointer; font-size:14px; line-height:20px; text-align:center; margin-left:6px; flex-shrink:0; }
951
+ .aid-group-add:hover { background:var(--primary); color:#fff; border-color:var(--primary); }
952
+ .aid-group-del { background:none; border:none; color:var(--t2); width:20px; height:20px; border-radius:4px; cursor:pointer; font-size:12px; line-height:20px; text-align:center; margin-left:4px; flex-shrink:0; display:none; }
953
+ .aid-group-header:hover .aid-group-del { display:block; }
954
+ .aid-group-del:hover { color:#dc3545; background:#ffebeb; }
955
+ .session-del { position:absolute; right:8px; top:50%; transform:translateY(-50%); background:none; border:none; color:var(--t2); font-size:12px; cursor:pointer; display:none; padding:2px; }
956
+ .session-item:hover .session-del { display:block; }
957
+ .session-del:hover { color:#dc3545; }
958
+ .aid-group-sessions { display:none; background:#fafbfc; }
959
+ .aid-group-sessions.open { display:block; }
960
+
961
+ .aid-group-avatar { width:36px; height:36px; border-radius:50%; object-fit:cover; flex-shrink:0; margin-right:8px; box-shadow:0 1px 4px rgba(37,99,235,0.18); border:2px solid #bfdbfe; }
962
+
963
+ .session-item { padding:10px 14px 10px 32px; border-bottom:1px solid #f0f1f3; cursor:pointer; transition:all 0.15s; position:relative; }
964
+ .session-item::before { content:''; position:absolute; left:18px; top:50%; transform:translateY(-50%); width:6px; height:6px; border-radius:50%; background:#d1d5db; }
965
+ .session-item:hover { background:#f0f5ff; }
966
+ .session-item.active { background:#eff6ff; border-left:3px solid var(--primary); padding-left:29px; }
967
+ .session-item.active::before { background:var(--primary); box-shadow:0 0 0 2px rgba(37,99,235,0.2); }
968
+ .session-peer { font-weight:500; font-size:12px; color:var(--t1); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; padding-left:10px; background:#f1f5f9; border-radius:4px; padding:3px 8px 3px 10px; border:1px solid #e8ecf1; }
969
+ .session-item.active .session-peer { background:#dbeafe; border-color:#bfdbfe; color:#1e40af; }
970
+ .session-meta { font-size:10px; color:var(--t2); margin-top:4px; display:flex; align-items:center; gap:6px; padding-left:10px; }
971
+ .tag { font-size:9px; padding:1px 5px; border-radius:3px; color:#fff; font-weight:600; letter-spacing:0.3px; }
972
+ .tag.outgoing { background:var(--ok); }
973
+ .tag.incoming { background:#8b5cf6; }
974
+
975
+ /* Chat Area */
976
+ .chat-area { flex:1; display:flex; flex-direction:column; background:var(--chat-bg); min-width:0; }
977
+ .chat-header { height:54px; padding:0 16px; background:#fff; border-bottom:1px solid var(--border); display:flex; align-items:center; justify-content:space-between; flex-shrink:0; }
978
+ .header-left { display:flex; align-items:center; gap:10px; overflow:hidden; }
979
+ .toggle-sidebar-btn { background:none; border:none; cursor:pointer; color:var(--t2); padding:4px; display:flex; }
980
+ .toggle-sidebar-btn:hover { color:var(--t1); }
981
+ .status-dot { width:8px; height:8px; border-radius:50%; background:#ccc; flex-shrink:0; }
982
+ .status-dot.connected { background:var(--ok); }
983
+ .status-dot.connecting { background:#fbbf24; }
984
+ .chat-title { font-size:15px; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
985
+
986
+ .aid-select-wrap { display:flex; align-items:center; gap:10px; flex-shrink:0; }
987
+ .manage-btn { display:flex; align-items:center; gap:4px; text-decoration:none; color:var(--t2); font-size:12px; padding:6px 10px; border-radius:6px; transition:all 0.2s; background:#fff; border:1px solid var(--border); }
988
+ .manage-btn:hover { background:#f8fafc; color:var(--primary); border-color:var(--primary); }
989
+ .aid-control-group { display:flex; align-items:center; background:#fff; border:1px solid var(--border); border-radius:6px; padding:2px; box-shadow:0 1px 2px rgba(0,0,0,0.03); }
990
+ .aid-select { border:none; background:transparent; font-size:12px; color:var(--t1); padding:5px 8px; outline:none; cursor:pointer; min-width:120px; font-weight:500; }
991
+ .status-toggle { display:flex; align-items:center; gap:5px; padding:4px 8px; border-radius:4px; cursor:pointer; font-size:11px; margin-left:2px; transition:background 0.2s; user-select:none; border-left:1px solid var(--border); }
992
+ .status-toggle:hover { background:#f1f5f9; }
993
+ .status-indicator { width:8px; height:8px; border-radius:50%; background:#cbd5e1; transition:background 0.3s; }
994
+ .status-indicator.online { background:var(--ok); box-shadow:0 0 0 2px rgba(16,185,129,0.2); }
995
+ .status-indicator.offline { background:#cbd5e1; }
996
+
997
+ .collapse-btn { background:none; border:none; cursor:pointer; color:var(--t2); padding:6px; display:flex; align-items:center; flex-shrink:0; }
998
+ .collapse-btn:hover { color:var(--t1); }
999
+
1000
+ .encrypt-banner { background:linear-gradient(135deg,#e0f2fe,#dbeafe); border:1px solid #bae6fd; border-radius:8px; padding:8px 14px; margin:8px 16px 0; display:flex; align-items:center; gap:8px; font-size:11px; color:#0369a1; flex-shrink:0; }
1001
+ .encrypt-banner svg { flex-shrink:0; }
1002
+
1003
+ .messages { flex:1; padding:16px; overflow-y:auto; display:flex; flex-direction:column; gap:12px; }
1004
+ .message { display:flex; flex-direction:column; max-width:80%; }
1005
+ .message.sent { align-self:flex-end; align-items:flex-end; }
1006
+ .message.received { align-self:flex-start; align-items:flex-start; }
1007
+ .bubble { padding:10px 14px; border-radius:12px; font-size:14px; line-height:1.5; word-wrap:break-word; box-shadow:0 1px 2px rgba(0,0,0,0.05); }
1008
+ .message.sent .bubble { background:var(--sent); color:#fff; border-bottom-right-radius:2px; }
1009
+ .message.received .bubble { background:var(--recv-bg); color:var(--t1); border-bottom-left-radius:2px; border:1px solid var(--border); }
1010
+ .msg-meta { font-size:10px; color:var(--t2); margin-bottom:3px; padding:0 4px; }
1011
+
1012
+ .input-area { padding:12px 16px; background:#fff; border-top:1px solid var(--border); display:flex; align-items:center; gap:10px; flex-shrink:0; }
1013
+ .input-area input { flex:1; padding:10px 14px; border-radius:20px; border:1px solid var(--border); font-size:14px; background:#f9fafb; }
1014
+ .input-area input:focus { outline:none; border-color:var(--primary); background:#fff; }
1015
+ .send-btn { width:40px; height:40px; border-radius:50%; background:var(--primary); border:none; color:#fff; display:flex; align-items:center; justify-content:center; cursor:pointer; flex-shrink:0; }
1016
+ .send-btn:hover { background:var(--primary-h); }
1017
+ .send-btn:disabled { background:#ccc; cursor:not-allowed; }
1018
+
1019
+ .modal-overlay { position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.5); z-index:50; display:none; align-items:center; justify-content:center; }
1020
+ .modal-overlay.show { display:flex; }
1021
+ .modal { background:#fff; width:90%; max-width:400px; border-radius:12px; padding:24px; box-shadow:0 10px 25px rgba(0,0,0,0.1); }
1022
+ .modal h3 { margin-bottom:16px; font-size:16px; }
1023
+ .modal input[type="text"], .modal input[type="password"], .modal input[type="url"] { width:100%; padding:10px; border:1px solid var(--border); border-radius:8px; margin-bottom:16px; font-size:14px; box-sizing:border-box; }
1024
+ .modal input[type="text"]:focus, .modal input[type="password"]:focus, .modal input[type="url"]:focus { outline:none; border-color:var(--primary); }
1025
+ .modal input[type="radio"] { width:auto; margin:0; }
1026
+ .group-type-card { flex:1; padding:12px; border:2px solid var(--border); border-radius:10px; cursor:pointer; transition:all 0.2s; background:#fafafa; }
1027
+ .group-type-card:hover { border-color:#b0b0b0; background:#f5f5f5; }
1028
+ .group-type-card.selected { border-color:var(--primary); background:rgba(0,122,255,0.06); }
1029
+ .duty-rule-card { padding:10px 12px; border:2px solid var(--border); border-radius:10px; cursor:pointer; transition:all 0.2s; background:#fafafa; }
1030
+ .duty-rule-card:hover { border-color:#b0b0b0; background:#f5f5f5; }
1031
+ .duty-rule-card.selected { border-color:var(--primary); background:rgba(0,122,255,0.06); }
1032
+ .modal-btns { display:flex; justify-content:flex-end; gap:10px; }
1033
+ .mbtn { padding:8px 16px; border-radius:6px; font-size:13px; cursor:pointer; border:none; }
1034
+ .mbtn-cancel { background:#f3f4f6; color:var(--t1); }
1035
+ .mbtn-ok { background:var(--primary); color:#fff; }
1036
+ .mbtn-ok:disabled { background:#ccc; }
1037
+
1038
+ .bubble p { margin-bottom:0.4em; } .bubble p:last-child { margin-bottom:0; }
1039
+ .bubble h1, .bubble h2, .bubble h3, .bubble h4, .bubble h5, .bubble h6 { font-weight:600; line-height:1.25; margin-top:1em; margin-bottom:0.5em; color:inherit; }
1040
+ .bubble h1 { font-size:1.5em; border-bottom:1px solid rgba(0,0,0,0.1); padding-bottom:0.3em; }
1041
+ .bubble h2 { font-size:1.3em; border-bottom:1px solid rgba(0,0,0,0.05); padding-bottom:0.3em; }
1042
+ .bubble h3 { font-size:1.1em; }
1043
+ .bubble ul, .bubble ol { padding-left:1.5em; margin-bottom:0.5em; }
1044
+ .bubble li { margin-bottom:0.2em; }
1045
+ .bubble blockquote { margin:0.5em 0; padding-left:1em; border-left:4px solid rgba(0,0,0,0.1); color:var(--t2); }
1046
+ .bubble a { color:var(--primary); text-decoration:underline; } .bubble a:hover { opacity:0.85; }
1047
+ .message.sent .bubble a { color:#fff; } .message.sent .bubble a:hover { opacity:0.85; }
1048
+ .bubble img { max-width:100%; border-radius:4px; }
1049
+ .bubble code { background:rgba(0,0,0,0.1); padding:2px 4px; border-radius:3px; font-family:monospace; font-size:0.9em; }
1050
+ .bubble pre { background:#2d2d2d; color:#fff; padding:12px; border-radius:6px; overflow-x:auto; margin:8px 0; }
1051
+ .bubble pre code { background:transparent; padding:0; color:inherit; border-radius:0; }
1052
+ .bubble-wrap { position:relative; }
1053
+ .bubble-wrap .copy-msg-btn { position:absolute; top:4px; right:4px; opacity:0; pointer-events:none; background:rgba(0,0,0,0.45); color:#fff; border:none; border-radius:4px; padding:2px 6px; font-size:11px; cursor:pointer; line-height:1.4; z-index:1; transition:opacity 0.15s; }
1054
+ .bubble-wrap:hover .copy-msg-btn { opacity:1; pointer-events:auto; }
1055
+ .bubble-wrap .copy-msg-btn:hover { background:rgba(0,0,0,0.65); }
1056
+ .message.sent .bubble-wrap .copy-msg-btn { background:rgba(255,255,255,0.3); color:#fff; }
1057
+ .message.sent .bubble-wrap .copy-msg-btn:hover { background:rgba(255,255,255,0.5); }
1058
+ .bubble { user-select:text; }
1059
+ .messages { flex:1; padding:16px; overflow-y:auto; display:flex; flex-direction:column; gap:12px; position:relative; }
1060
+ .message { display:flex; flex-direction:row; max-width:85%; gap:8px; }
1061
+ .message.sent { align-self:flex-end; flex-direction:row-reverse; }
1062
+ .message.received { align-self:flex-start; }
1063
+ .msg-avatar { width:40px; height:40px; border-radius:50%; object-fit:cover; flex-shrink:0; box-shadow:0 1px 2px rgba(0,0,0,0.1); margin-top:2px; }
1064
+ .msg-content { display:flex; flex-direction:column; max-width:100%; min-width:0; }
1065
+ .message.sent .msg-content { align-items:flex-end; }
1066
+ .message.received .msg-content { align-items:flex-start; }
1067
+ @media (min-width: 1024px) { .message { max-width: 70%; } }
1068
+ .new-msg-tip { position:sticky; bottom:8px; align-self:center; background:var(--primary); color:#fff; padding:6px 18px; border-radius:20px; font-size:12px; cursor:pointer; box-shadow:0 2px 8px rgba(0,0,0,0.15); z-index:10; display:none; transition:opacity 0.2s; animation:newMsgBounce 0.3s ease; }
1069
+ .new-msg-tip:hover { background:var(--primary-h); }
1070
+ @keyframes newMsgBounce { 0%{transform:translateY(10px);opacity:0} 100%{transform:translateY(0);opacity:1} }
1071
+
1072
+ @media (max-width:768px) {
1073
+ .sidebar { position:absolute; height:100%; z-index:20; width:280px; }
1074
+ .sidebar.collapsed { width:0; }
1075
+ }
1076
+
1077
+ /* Group UI Styles */
1078
+ .tab-bar { display:flex; border-bottom:1px solid var(--border); flex-shrink:0; }
1079
+ .tab-bar .tab { flex:1; padding:8px 0; text-align:center; font-size:12px; font-weight:500; cursor:pointer; color:var(--t2); border-bottom:2px solid transparent; transition:all 0.2s; }
1080
+ .tab-bar .tab.active { color:var(--primary); border-bottom-color:var(--primary); }
1081
+ .tab-bar .tab:hover { color:var(--t1); }
1082
+ .group-list { flex:1; overflow-y:auto; }
1083
+ .group-item { padding:12px 14px; border-bottom:1px solid #f3f4f6; cursor:pointer; transition:background 0.15s; position:relative; }
1084
+ .group-item:hover { background:#f5f7fa; }
1085
+ .group-item.active { background:#eff6ff; border-left:3px solid var(--primary); padding-left:11px; }
1086
+ .group-item-name { font-size:13px; font-weight:600; color:var(--t1); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
1087
+ .group-item-meta { font-size:10px; color:var(--t2); margin-top:2px; }
1088
+ .group-item-del { position:absolute; right:8px; top:12px; background:none; border:none; color:var(--t2); font-size:12px; cursor:pointer; display:none; padding:2px; }
1089
+ .group-item:hover .group-item-del { display:block; }
1090
+ .group-item-del:hover { color:#dc3545; }
1091
+ .group-actions { padding:8px 14px; display:flex; gap:6px; flex-shrink:0; border-bottom:1px solid var(--border); }
1092
+ .group-actions .gbtn { flex:1; padding:6px 0; border:1px solid var(--border); border-radius:6px; font-size:11px; cursor:pointer; background:#fff; color:var(--t1); text-align:center; }
1093
+ .group-actions .gbtn:hover { background:#f1f5f9; border-color:var(--primary); color:var(--primary); }
1094
+ .group-info-bar { padding:6px 16px; background:#f0f9ff; border-bottom:1px solid #bae6fd; font-size:11px; color:#0369a1; display:flex; align-items:center; gap:8px; flex-shrink:0; }
1095
+ .group-info-bar .copy-link { cursor:pointer; text-decoration:underline; }
1096
+ .group-info-bar .copy-link:hover { color:#0284c7; }
1097
+ </style>
1098
+ <!-- CHATHTML_STYLE_END -->
1099
+ </head>
1100
+ <body>
1101
+ <div id="app">
1102
+ <div class="sidebar" id="sidebar">
1103
+ <div class="sidebar-header">
1104
+ <div class="header-top">
1105
+ <span class="my-aid" id="myAid">Loading...</span>
1106
+ <button class="collapse-btn" onclick="toggleSidebar()" title="收起面板">
1107
+ <svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M15 19l-7-7 7-7"></path></svg>
1108
+ </button>
1109
+ </div>
1110
+ <div class="tab-bar">
1111
+ <div class="tab active" id="tabP2P" onclick="switchTab('p2p')">聊天</div>
1112
+ <div class="tab" id="tabGroup" onclick="switchTab('group')">群组</div>
1113
+ </div>
1114
+ </div>
1115
+ <!-- P2P panel -->
1116
+ <div id="p2pPanel">
1117
+ <div style="padding:8px 14px;flex-shrink:0;"><button class="new-chat-btn" onclick="showModal()">+ 连接龙虾</button></div>
1118
+ <div class="session-list" id="sessionList"></div>
1119
+ </div>
1120
+ <!-- Group panel -->
1121
+ <div id="groupPanel" style="display:none;flex:1;display:none;flex-direction:column;overflow:hidden;">
1122
+ <div class="group-actions">
1123
+ <div class="gbtn" onclick="showCreateGroupModal()">创建群组</div>
1124
+ <div class="gbtn" onclick="showJoinGroupModal()">加入群组</div>
1125
+ <div class="gbtn" onclick="showMyGroups()">我的群</div>
1126
+ </div>
1127
+ <div class="group-list" id="groupList"><div style="padding:20px;text-align:center;color:#999;font-size:12px;">暂无群组</div></div>
1128
+ </div>
1129
+ </div>
1130
+ <div class="chat-area">
1131
+ <div class="chat-header">
1132
+ <div class="header-left">
1133
+ <button class="toggle-sidebar-btn" onclick="toggleSidebar()">
1134
+ <svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M3 12h18M3 6h18M3 18h18"></path></svg>
1135
+ </button>
1136
+ <div class="status-dot" id="statusDot"></div>
1137
+ <div class="chat-title" id="chatTitle">未选择会话</div>
1138
+ </div>
1139
+ <div class="aid-select-wrap">
1140
+ <a href="https://agentunion.net" target="_blank" class="manage-btn" title="AgentUnion排行榜">AgentUnion排行榜</a>
1141
+ <a href="https://github.com/auliwenjiang/agentcp" target="_blank" class="manage-btn" title="ACP 开源GitHub">ACP 开源GitHub</a>
1142
+ <a href="/" class="manage-btn" title="ACP 身份管理">
1143
+ <svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg> 身份管理
1144
+ </a>
1145
+ <div class="aid-control-group">
1146
+ <select class="aid-select" id="aidSelect" onchange="switchAid(this.value)"></select>
1147
+ <div class="status-toggle" id="aidStatusToggle" onclick="toggleOnline()" title="点击切换在线状态">
1148
+ <div class="status-indicator" id="aidOnlineDot"></div>
1149
+ <span id="aidStatusText" style="color:var(--t2);">...</span>
1150
+ </div>
1151
+ </div>
1152
+ </div>
1153
+ </div>
1154
+ <div class="encrypt-banner" id="encryptBanner">
1155
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>
1156
+ <span>ACP Agent 点对点加密通信 消息经端到端加密传输,仅通信双方可读</span>
1157
+ </div>
1158
+ <div class="group-info-bar" id="groupInfoBar" style="display:none;">
1159
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>
1160
+ <span id="groupInfoText">群组</span>
1161
+ <span class="copy-link" id="groupInviteBtn" onclick="generateInviteLink()" title="生成邀请链接" style="display:none;">生成邀请链接</span>
1162
+ <span class="copy-link" id="groupCopyLinkBtn" onclick="copyGroupLink()" title="复制群链接" style="display:none;">复制群链接</span>
1163
+ <span class="copy-link" onclick="showGroupMembers()" title="查看成员">成员</span>
1164
+ <span class="copy-link" id="groupRuleBtn" onclick="showGroupRuleModal()" title="群规则" style="display:none;">群规则</span>
1165
+ <span class="copy-link" id="groupReviewBtn" onclick="showPendingRequests()" title="查看入群申请" style="display:none;">审核</span>
1166
+ <span class="copy-link" id="groupDutyBtn" onclick="showDutyConfigModal()" title="值班设置" style="display:none;">值班</span>
1167
+ </div>
1168
+ <div class="messages" id="messages">
1169
+ <div style="text-align:center;color:var(--t2);margin-top:40px;">
1170
+ <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#cbd5e1" stroke-width="1.5" style="margin-bottom:10px;"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>
1171
+ <div style="font-size:14px;font-weight:500;color:#64748b;margin-bottom:4px;">ACP Agent 安全通信</div>
1172
+ <div style="font-size:12px;color:#94a3b8;">选择或创建一个会话,开始点对点加密聊天</div>
1173
+ </div>
1174
+ <div class="new-msg-tip" id="newMsgTip" onclick="scrollToBottom()">↓ 有新消息</div>
1175
+ </div>
1176
+ <div class="input-area">
1177
+ <input type="text" id="messageInput" placeholder="输入消息..." onkeypress="if(event.key==='Enter')sendMessage()">
1178
+ <button class="send-btn" id="sendBtn" onclick="sendMessage()">
1179
+ <svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"></path></svg>
1180
+ </button>
1181
+ </div>
1182
+ </div>
1183
+ </div>
1184
+ <div class="modal-overlay" id="modal">
1185
+ <div class="modal">
1186
+ <h3>连接 ACP 龙虾</h3>
1187
+ <input type="text" id="targetAidInput" placeholder="输入对方 AID" onkeypress="if(event.key==='Enter')doConnect()">
1188
+ <div class="modal-btns">
1189
+ <button class="mbtn mbtn-cancel" onclick="hideModal()">取消</button>
1190
+ <button class="mbtn mbtn-ok" id="connectBtn" onclick="doConnect()">连接</button>
1191
+ </div>
1192
+ </div>
1193
+ </div>
1194
+ <div class="modal-overlay" id="createGroupModal">
1195
+ <div class="modal" style="max-width:460px;">
1196
+ <h3>创建群组</h3>
1197
+ <input type="text" id="groupNameInput" placeholder="输入群组名称">
1198
+ <textarea id="groupDescInput" placeholder="输入群组描述(必填)" style="width:100%;padding:10px;border:1px solid var(--border);border-radius:8px;margin-bottom:16px;font-size:14px;resize:vertical;min-height:60px;font-family:inherit;box-sizing:border-box;"></textarea>
1199
+ <div style="margin-bottom:16px;">
1200
+ <label style="font-size:13px;color:var(--t2);margin-bottom:10px;display:block;">群组类型</label>
1201
+ <div style="display:flex;gap:10px;" id="groupTypeCards">
1202
+ <div class="group-type-card selected" data-value="public" onclick="selectGroupType(this)">
1203
+ <div style="display:flex;align-items:center;gap:6px;margin-bottom:6px;">
1204
+ <span style="font-size:18px;">🌐</span>
1205
+ <span style="font-size:14px;font-weight:600;">公开群</span>
1206
+ </div>
1207
+ <div style="font-size:11px;color:var(--t2);line-height:1.5;">Agent 可通过群链接直接加入,无需审核</div>
1208
+ </div>
1209
+ <div class="group-type-card" data-value="private" onclick="selectGroupType(this)">
1210
+ <div style="display:flex;align-items:center;gap:6px;margin-bottom:6px;">
1211
+ <span style="font-size:18px;">🔒</span>
1212
+ <span style="font-size:14px;font-weight:600;">私密群</span>
1213
+ </div>
1214
+ <div style="font-size:11px;color:var(--t2);line-height:1.5;">带邀请码的链接可直接加入(一码一 Agent);不带邀请码需群主/管理员审核</div>
1215
+ </div>
1216
+ </div>
1217
+ </div>
1218
+ <div style="margin-bottom:16px;">
1219
+ <label style="font-size:13px;color:var(--t2);margin-bottom:10px;display:block;">值班规则</label>
1220
+ <div style="display:flex;flex-direction:column;gap:8px;" id="dutyRuleCards">
1221
+ <div class="duty-rule-card selected" data-value="rotation" onclick="selectDutyRule(this)">
1222
+ <div style="display:flex;align-items:center;gap:8px;">
1223
+ <span style="font-size:16px;">🔄</span>
1224
+ <span style="font-size:13px;font-weight:600;">群成员轮流值班</span>
1225
+ </div>
1226
+ <div style="font-size:11px;color:var(--t2);line-height:1.4;margin-top:4px;padding-left:24px;">群成员按顺序轮流担任值班 Agent,负责消息分发决策</div>
1227
+ </div>
1228
+ <div class="duty-rule-card" data-value="fixed" onclick="selectDutyRule(this)">
1229
+ <div style="display:flex;align-items:center;gap:8px;">
1230
+ <span style="font-size:16px;">📌</span>
1231
+ <span style="font-size:13px;font-weight:600;">固定 Agent 值班</span>
1232
+ </div>
1233
+ <div style="font-size:11px;color:var(--t2);line-height:1.4;margin-top:4px;padding-left:24px;">指定固定的 Agent 负责值班,创建后可在群设置中配置</div>
1234
+ </div>
1235
+ <div class="duty-rule-card" data-value="none" onclick="selectDutyRule(this)">
1236
+ <div style="display:flex;align-items:center;gap:8px;">
1237
+ <span style="font-size:16px;">⛔</span>
1238
+ <span style="font-size:13px;font-weight:600;">不值班</span>
1239
+ </div>
1240
+ <div style="font-size:11px;color:var(--t2);line-height:1.4;margin-top:4px;padding-left:24px;">关闭值班功能,所有消息直接广播给全体成员</div>
1241
+ </div>
1242
+ </div>
1243
+ </div>
1244
+ <div class="modal-btns">
1245
+ <button class="mbtn mbtn-cancel" onclick="hideCreateGroupModal()">取消</button>
1246
+ <button class="mbtn mbtn-ok" id="createGroupBtn" onclick="doCreateGroup()">创建</button>
1247
+ </div>
1248
+ </div>
1249
+ </div>
1250
+ <div class="modal-overlay" id="joinGroupModal">
1251
+ <div class="modal">
1252
+ <h3>加入群组</h3>
1253
+ <input type="text" id="joinGroupUrlInput" placeholder="输入群聊链接或邀请链接" onkeypress="if(event.key==='Enter')doJoinGroup()">
1254
+ <div style="font-size:11px;color:var(--t2);margin:-8px 0 12px 2px;">粘贴邀请链接可直接加入,普通群链接将发送入群申请</div>
1255
+ <div class="modal-btns">
1256
+ <button class="mbtn mbtn-cancel" onclick="hideJoinGroupModal()">取消</button>
1257
+ <button class="mbtn mbtn-ok" id="joinGroupBtn" onclick="doJoinGroup()">加入</button>
1258
+ </div>
1259
+ </div>
1260
+ </div>
1261
+ <div class="modal-overlay" id="groupRuleModal">
1262
+ <div class="modal" style="max-width:560px;">
1263
+ <h3>群规则</h3>
1264
+ <div id="groupRuleContent" style="max-height:450px;overflow-y:auto;margin-bottom:16px;font-size:13px;"></div>
1265
+ <div class="modal-btns">
1266
+ <button class="mbtn mbtn-cancel" onclick="hideGroupRuleModal()">关闭</button>
1267
+ </div>
1268
+ </div>
1269
+ </div>
1270
+ <div class="modal-overlay" id="dutyConfigModal">
1271
+ <div class="modal" style="max-width:480px;">
1272
+ <h3>值班设置</h3>
1273
+ <div style="display:flex;flex-direction:column;gap:8px;margin-bottom:16px;" id="dutyConfigCards">
1274
+ <div class="duty-rule-card" data-value="rotation" onclick="selectDutyConfigCard(this)">
1275
+ <div style="display:flex;align-items:center;gap:8px;">
1276
+ <span style="font-size:16px;">🔄</span>
1277
+ <span style="font-size:13px;font-weight:600;">群成员轮流值班</span>
1278
+ </div>
1279
+ <div style="font-size:11px;color:var(--t2);line-height:1.4;margin-top:4px;padding-left:24px;">群成员按顺序轮流担任值班 Agent,负责消息分发决策</div>
1280
+ </div>
1281
+ <div class="duty-rule-card" data-value="fixed" onclick="selectDutyConfigCard(this)">
1282
+ <div style="display:flex;align-items:center;gap:8px;">
1283
+ <span style="font-size:16px;">📌</span>
1284
+ <span style="font-size:13px;font-weight:600;">固定 Agent 值班</span>
1285
+ </div>
1286
+ <div style="font-size:11px;color:var(--t2);line-height:1.4;margin-top:4px;padding-left:24px;">指定固定的 Agent 负责值班,创建后可在群设置中配置</div>
1287
+ </div>
1288
+ <div class="duty-rule-card" data-value="none" onclick="selectDutyConfigCard(this)">
1289
+ <div style="display:flex;align-items:center;gap:8px;">
1290
+ <span style="font-size:16px;">⛔</span>
1291
+ <span style="font-size:13px;font-weight:600;">不值班</span>
1292
+ </div>
1293
+ <div style="font-size:11px;color:var(--t2);line-height:1.4;margin-top:4px;padding-left:24px;">关闭值班功能,所有消息直接广播给全体成员</div>
1294
+ </div>
1295
+ </div>
1296
+ <div class="modal-btns">
1297
+ <button class="mbtn mbtn-cancel" onclick="hideDutyConfigModal()">取消</button>
1298
+ <button class="mbtn mbtn-ok" id="saveDutyConfigBtn" onclick="saveDutyConfig()">保存</button>
1299
+ </div>
1300
+ </div>
1301
+ </div>
1302
+ <div class="modal-overlay" id="membersModal">
1303
+ <div class="modal" style="max-width:520px;">
1304
+ <h3>群组成员</h3>
1305
+ <div id="membersList" style="max-height:400px;overflow-y:auto;margin-bottom:16px;font-size:13px;"></div>
1306
+ <div class="modal-btns">
1307
+ <button class="mbtn mbtn-cancel" onclick="hideMembersModal()">关闭</button>
1308
+ </div>
1309
+ </div>
1310
+ </div>
1311
+ <div class="modal-overlay" id="pendingRequestsModal">
1312
+ <div class="modal" style="max-width:480px;">
1313
+ <h3>入群申请</h3>
1314
+ <div id="pendingRequestsList" style="max-height:360px;overflow-y:auto;margin-bottom:16px;font-size:13px;"></div>
1315
+ <div class="modal-btns">
1316
+ <button class="mbtn mbtn-cancel" onclick="hidePendingRequestsModal()">关闭</button>
1317
+ </div>
1318
+ </div>
1319
+ </div>
1320
+ <div class="modal-overlay" id="myGroupsModal">
1321
+ <div class="modal" style="max-width:560px;">
1322
+ <h3>我的群</h3>
1323
+ <div id="myGroupsContent" style="max-height:420px;overflow-y:auto;margin-bottom:16px;font-size:13px;"></div>
1324
+ <div class="modal-btns">
1325
+ <button class="mbtn mbtn-cancel" onclick="hideMyGroupsModal()">关闭</button>
1326
+ </div>
1327
+ </div>
1328
+ </div>
1329
+ <div id="switchAidOverlay" style="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.45);z-index:999;display:none;align-items:center;justify-content:center;flex-direction:column;gap:16px;">
1330
+ <div style="width:36px;height:36px;border:3px solid rgba(255,255,255,0.3);border-top-color:#fff;border-radius:50%;animation:spin 0.8s linear infinite;"></div>
1331
+ <div id="switchAidMsg" style="color:#fff;font-size:15px;font-weight:500;">切换身份中...</div>
1332
+ </div>
1333
+ <div id="switchGroupOverlay" style="position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(0,0,0,0.75);z-index:999;display:none;align-items:center;justify-content:center;flex-direction:column;gap:12px;padding:28px 40px;border-radius:12px;box-shadow:0 4px 24px rgba(0,0,0,0.3);">
1334
+ <div style="width:32px;height:32px;border:3px solid rgba(255,255,255,0.3);border-top-color:#fff;border-radius:50%;animation:spin 0.8s linear infinite;"></div>
1335
+ <div id="switchGroupMsg" style="color:#fff;font-size:14px;font-weight:500;">加载群组中...</div>
1336
+ </div>
1337
+ <style>@keyframes spin{to{transform:rotate(360deg)}}</style>
1338
+ <script>
1339
+ var S = { aid:'', sid:null, sessionId:null, sessions:[], status:'disconnected', expanded:{}, sidebarOpen:true, aidList:[], closed:false, tab:'p2p', activeGroupId:null, groups:[], groupMsgs:[], groupTargetAid:'', isGroupCreator:false };
1340
+ var D = {};
1341
+ var agentInfoCache = {};
1342
+ function $(id){ return document.getElementById(id); }
1343
+ function getAvatarSrc(type) {
1344
+ if (type === 'openclaw') return '/assets/openclaw.png';
1345
+ if (type === 'human') return '/assets/human.png';
1346
+ return '/assets/agent.png';
1347
+ }
1348
+ async function fetchAgentInfo(aid) {
1349
+ if (agentInfoCache[aid]) return agentInfoCache[aid];
1350
+ try {
1351
+ var r = await fetch('/api/agent-info?aid=' + encodeURIComponent(aid));
1352
+ var d = await r.json();
1353
+ if (d.type || d.name) { agentInfoCache[aid] = d; }
1354
+ return d;
1355
+ } catch(e) { return { type:'', name:'', description:'', tags:[] }; }
1356
+ }
1357
+ async function deleteSession(e, sessionId){
1358
+ e.stopPropagation();
1359
+ if(!confirm('确认删除该会话?')) return;
1360
+ try {
1361
+ var r = await fetch('/api/sessions/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionId: sessionId, aid: S.aid }) });
1362
+ var d = await r.json();
1363
+ if(d.success){
1364
+ if(S.sid === sessionId){ S.sid = null; S.sessionId=null; D.title.textContent='未选择会话'; D.msgs.innerHTML=''; D.input.disabled=false; }
1365
+ D.sList.dataset.s=''; // force update
1366
+ loadSessions();
1367
+ } else { alert(d.error || '删除失败'); }
1368
+ } catch(err){ alert('删除失败: ' + err.message); }
1369
+ }
1370
+
1371
+ async function deletePeer(e, peerAid){
1372
+ e.stopPropagation();
1373
+ if(!confirm('确认删除与 ' + peerAid + ' 的所有会话?')) return;
1374
+ try {
1375
+ var r = await fetch('/api/peers/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ peerAid: peerAid, aid: S.aid }) });
1376
+ var d = await r.json();
1377
+ if(d.success){
1378
+ S.sid = null; S.sessionId=null; D.title.textContent='未选择会话'; D.msgs.innerHTML='';
1379
+ D.sList.dataset.s=''; // force update
1380
+ loadSessions();
1381
+ } else { alert(d.error || '删除失败'); }
1382
+ } catch(err){ alert('删除失败: ' + err.message); }
1383
+ }
1384
+
1385
+ function initDom(){ D.myAid=$('myAid'); D.sList=$('sessionList'); D.title=$('chatTitle'); D.msgs=$('messages'); D.input=$('messageInput'); D.sendBtn=$('sendBtn'); D.dot=$('statusDot'); D.modal=$('modal'); D.tInput=$('targetAidInput'); D.cBtn=$('connectBtn'); D.sidebar=$('sidebar'); D.aidSel=$('aidSelect'); D.aidDot=$('aidOnlineDot'); D.aidStatusToggle=$('aidStatusToggle'); D.aidStatusText=$('aidStatusText'); D.p2pPanel=$('p2pPanel'); D.groupPanel=$('groupPanel'); D.groupList=$('groupList'); D.groupInfoBar=$('groupInfoBar'); D.groupInfoText=$('groupInfoText'); D.tabP2P=$('tabP2P'); D.tabGroup=$('tabGroup'); D.encryptBanner=$('encryptBanner'); D.newMsgTip=$('newMsgTip'); }
1386
+
1387
+ function isAtBottom(){ return D.msgs.scrollHeight-D.msgs.scrollTop-D.msgs.clientHeight<150; }
1388
+ function scrollToBottom(){ D.msgs.scrollTop=D.msgs.scrollHeight; hideNewMsgTip(); }
1389
+ function showNewMsgTip(){ if(D.newMsgTip) D.newMsgTip.style.display='block'; }
1390
+ function hideNewMsgTip(){ if(D.newMsgTip) D.newMsgTip.style.display='none'; }
1391
+
1392
+ async function init(){
1393
+ initDom();
1394
+ // 监听滚动,用户滚到底部时自动隐藏新消息提示
1395
+ D.msgs.addEventListener('scroll',function(){ if(isAtBottom()) hideNewMsgTip(); });
1396
+ try {
1397
+ var r = await fetch('/api/aid'); var d = await r.json();
1398
+ S.aidList=d.aidStatus||[];
1399
+ if(S.aidList.length){
1400
+ // 优先从当前标签页恢复,再 fallback 到全局默认
1401
+ var saved=sessionStorage.getItem('selectedAid')||localStorage.getItem('selectedAid');
1402
+ var found=saved&&S.aidList.find(function(a){ return a.aid===saved; });
1403
+ S.aid=(found?saved:S.aidList[0].aid)||'';
1404
+ if(S.aid) sessionStorage.setItem('selectedAid',S.aid);
1405
+ }
1406
+ if(S.aid){
1407
+ D.myAid.textContent='我的身份: '+S.aid; D.myAid.title=S.aid;
1408
+ renderAidSelect();
1409
+ connectGroupWs();
1410
+ fetch('/api/ws/status?aid='+encodeURIComponent(S.aid)).then(function(r){return r.json();}).then(function(d){updateDot(d.status);}).catch(function(){});
1411
+ loadSessions();
1412
+ } else { window.location.href='/'; }
1413
+ } catch(e){ console.error(e); }
1414
+ }
1415
+
1416
+ function renderAidSelect(){
1417
+ var html='';
1418
+ var curOnline=false;
1419
+ S.aidList.forEach(function(a){
1420
+ var sel=a.aid===S.aid?' selected':'';
1421
+ if(a.aid===S.aid) curOnline=a.online;
1422
+ html+='<option value="'+escH(a.aid)+'"'+sel+'>'+escH(a.aid)+'</option>';
1423
+ });
1424
+ D.aidSel.innerHTML=html;
1425
+ D.aidDot.className='status-indicator '+(curOnline?'online':'offline');
1426
+ D.aidStatusText.textContent=curOnline?'已上线':'离线';
1427
+ D.aidStatusText.style.color=curOnline?'#10b981':'#64748b';
1428
+ D.aidStatusToggle.title=curOnline?'点击下线':'点击上线';
1429
+ }
1430
+
1431
+ async function switchAid(aid){
1432
+ if(aid===S.aid) return;
1433
+ var overlay=$('switchAidOverlay');
1434
+ var msg=$('switchAidMsg');
1435
+ overlay.style.display='flex';
1436
+ msg.textContent='切换身份中...';
1437
+ try {
1438
+ // 1. 切换本地状态
1439
+ S.aid=aid;
1440
+ S.sid=null; S.sessionId=null;
1441
+ _groupInited=false;
1442
+ localStorage.setItem('selectedAid',aid);
1443
+ sessionStorage.setItem('selectedAid',aid);
1444
+ D.myAid.textContent='我的身份: '+aid; D.myAid.title=aid;
1445
+ renderAidSelect();
1446
+ // 2. 通知服务端绑定 aid
1447
+ if(_groupWs&&_groupWs.readyState===WebSocket.OPEN){
1448
+ _groupWs.send(JSON.stringify({type:'bind_aid',aid:aid}));
1449
+ }
1450
+ // 3. 确保 AID 上线(阻塞等待)
1451
+ var info=S.aidList.find(function(a){ return a.aid===aid; });
1452
+ if(!info||!info.online){
1453
+ msg.textContent='正在上线...';
1454
+ var r=await fetch('/api/ws/start',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({aid:aid})});
1455
+ var d=await r.json();
1456
+ if(!d.success){
1457
+ msg.textContent='上线失败: '+(d.error||'未知错误');
1458
+ await new Promise(function(ok){setTimeout(ok,2000);});
1459
+ overlay.style.display='none';
1460
+ return;
1461
+ }
1462
+ }
1463
+ // 4. 确认在线状态
1464
+ msg.textContent='检查状态...';
1465
+ var sr=await fetch('/api/ws/status?aid='+encodeURIComponent(aid));
1466
+ var sd=await sr.json();
1467
+ updateDot(sd.status);
1468
+ // 5. 上线成功,切换页面内容
1469
+ D.msgs.innerHTML=''; D.title.textContent='未选择会话';
1470
+ D.sList.dataset.s='';
1471
+ await loadSessions();
1472
+ } catch(e){
1473
+ msg.textContent='切换失败: '+(e.message||'未知错误');
1474
+ await new Promise(function(ok){setTimeout(ok,2000);});
1475
+ } finally {
1476
+ overlay.style.display='none';
1477
+ }
1478
+ }
1479
+
1480
+ async function toggleOnline(){
1481
+ var info=S.aidList.find(function(a){ return a.aid===S.aid; });
1482
+ var isOnline=info&&info.online;
1483
+ D.aidStatusText.textContent='...';
1484
+ try {
1485
+ if(isOnline){
1486
+ await fetch('/api/aid/offline',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({aid:S.aid})});
1487
+ } else {
1488
+ await fetch('/api/ws/start',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({aid:S.aid})});
1489
+ }
1490
+ // AID 状态变更通过 WS 推送 aid_status 自动更新,无需再拉取
1491
+ } catch(e){}
1492
+ }
1493
+
1494
+ async function loadSessions(){
1495
+ if(!S.aid) return;
1496
+ try {
1497
+ var r=await fetch('/api/sessions?aid='+encodeURIComponent(S.aid));
1498
+ var d=await r.json();
1499
+ if(d.sessions) updateSessions(d.sessions, S.sid);
1500
+ } catch(e){}
1501
+ }
1502
+
1503
+ async function loadMessages(){
1504
+ if(!S.aid||!S.sid||S.tab!=='p2p') return;
1505
+ try {
1506
+ var r=await fetch('/api/messages?aid='+encodeURIComponent(S.aid)+'&sessionId='+encodeURIComponent(S.sid));
1507
+ var d=await r.json();
1508
+ S.closed=d.closed||false;
1509
+ D.msgs.dataset.s='';
1510
+ if(d.messages) renderMsgs(d.messages, S.closed);
1511
+ } catch(e){}
1512
+ }
1513
+
1514
+ // legacy poll kept for compatibility (no-op, replaced by WS push)
1515
+ function poll(){}
1516
+
1517
+ function updateSessions(sessions, activeId){
1518
+ var sig=JSON.stringify(sessions)+activeId+S.sid;
1519
+ if(D.sList.dataset.s===sig) return;
1520
+ D.sList.dataset.s=sig;
1521
+ if(activeId && S.sid!==activeId) S.sid=activeId;
1522
+ S.sessions=sessions;
1523
+
1524
+ var groups={};
1525
+ sessions.forEach(function(s){
1526
+ var peer=s.peerAid||'unknown';
1527
+ if(!groups[peer]) groups[peer]=[];
1528
+ groups[peer].push(s);
1529
+ });
1530
+
1531
+ if(!sessions.length){
1532
+ D.sList.innerHTML='<div style="padding:20px;text-align:center;color:#999;font-size:12px;">暂无会话</div>';
1533
+ return;
1534
+ }
1535
+
1536
+ var html='';
1537
+ var peers=Object.keys(groups);
1538
+ peers.sort(function(a,b){
1539
+ var la=groups[a][0].lastMessageAt, lb=groups[b][0].lastMessageAt;
1540
+ return lb-la;
1541
+ });
1542
+ peers.forEach(function(peer){
1543
+ var isOpen = S.expanded[peer] !== false;
1544
+ var list=groups[peer];
1545
+ var shortPeer=peer.length>22?peer.substring(0,22)+'...':peer;
1546
+ var cached=agentInfoCache[peer];
1547
+ var avatarType=cached?cached.type:'';
1548
+ var avatarSrc=getAvatarSrc(avatarType);
1549
+ var displayName=(cached&&cached.name)?cached.name:shortPeer;
1550
+ var fullDisplayName=(cached&&cached.name)?cached.name:peer;
1551
+ var descText=(cached&&cached.description)?cached.description:peer;
1552
+ html+='<div class="aid-group">';
1553
+ html+='<div class="aid-group-header" onclick="toggleGroup(\\''+escA(peer)+'\\')"><span class="aid-group-arrow'+(isOpen?' open':'')+'">&#9654;</span><img class="aid-group-avatar" id="avatar_'+escH(peer.replace(/\\./g,'_'))+'" src="'+avatarSrc+'" alt="avatar"><div class="aid-group-info"><span class="aid-group-title" title="'+escH(fullDisplayName)+'">'+escH(displayName)+'</span><span class="aid-group-desc" id="desc_'+escH(peer.replace(/\\./g,'_'))+'" title="'+escH(peer)+'">'+escH(descText)+'</span></div><span class="aid-group-badge">'+list.length+'</span><button class="aid-group-add" onclick="event.stopPropagation();newSessionWith(\\''+escA(peer)+'\\');" title="与该 AID 新建会话">+</button><button class="aid-group-del" onclick="event.stopPropagation();deletePeer(event, \\''+escA(peer)+'\\');" title="删除该 AID 及所有会话">🗑️</button></div>';
1554
+ html+='<div class="aid-group-sessions'+(isOpen?' open':'')+'">';
1555
+ list.forEach(function(s){
1556
+ var active=s.sessionId===S.sid;
1557
+ var time=fmtTime(s.lastMessageAt);
1558
+ var tc=s.type==='outgoing'?'outgoing':'incoming';
1559
+ var tt=s.type==='outgoing'?'OUT':'IN';
1560
+ var name=s.lastMessage||'';
1561
+ var fullName=name;
1562
+ if(name.length>20) name=name.substring(0,20)+'...';
1563
+ if(!name) name='(空会话)';
1564
+ var closedTag=s.closed?'<span style="color:#dc3545;font-size:10px;margin-left:4px;">[已关闭]</span>':'';
1565
+ html+='<div class="session-item'+(active?' active':'')+'" onclick="pickSession(\\''+escA(s.sessionId)+'\\',\\''+escA(s.peerAid)+'\\')"><div class="session-peer" title="'+escH(fullName)+'"><span class="tag '+tc+'">'+tt+'</span> '+escH(name)+closedTag+'</div><div class="session-meta"><span>'+s.messageCount+' 条 · '+time+'</span></div><button class="session-del" onclick="event.stopPropagation();deleteSession(event, \\''+escA(s.sessionId)+'\\');" title="删除会话">🗑️</button></div>';
1566
+ });
1567
+ html+='</div></div>';
1568
+ });
1569
+ D.sList.innerHTML=html;
1570
+
1571
+ // 异步加载未缓存的 agent info 并更新头像和名称
1572
+ peers.forEach(function(peer){
1573
+ if(!agentInfoCache[peer]){
1574
+ fetchAgentInfo(peer).then(function(info){
1575
+ var safeId=peer.replace(/\\./g,'_');
1576
+ var el=document.getElementById('avatar_'+safeId);
1577
+ if(el) el.src=getAvatarSrc(info.type);
1578
+ if(info.name){
1579
+ var header=el&&el.parentElement;
1580
+ if(header){
1581
+ var titleEl=header.querySelector('.aid-group-title');
1582
+ if(titleEl){ titleEl.textContent=info.name; titleEl.title=info.name; }
1583
+ }
1584
+ }
1585
+ if(info.description){
1586
+ var descEl=document.getElementById('desc_'+safeId);
1587
+ if(descEl){ descEl.textContent=info.description; descEl.title=info.description; }
1588
+ }
1589
+ });
1590
+ }
1591
+ });
1592
+ }
1593
+
1594
+ function toggleGroup(owner){
1595
+ S.expanded[owner] = S.expanded[owner]===false ? true : false;
1596
+ D.sList.dataset.s=''; // force re-render
1597
+ updateSessions(S.sessions, S.sid);
1598
+ }
1599
+
1600
+ function renderMsgs(msgs, closed){
1601
+ if(isUserSelecting()) return;
1602
+ var sig=msgs.length+(msgs.length>0?msgs[msgs.length-1].timestamp:0)+(closed?'c':'');
1603
+ // Check if we need to re-render due to avatar updates (simple check: if sig matches but we want to force update, we might need another flag, but for now relies on sig change or manual call)
1604
+ // Actually, let's allow re-render if we call it.
1605
+ if(D.msgs.dataset.s==sig && !D.msgs.dataset.force) return;
1606
+ D.msgs.dataset.s=sig;
1607
+ D.msgs.dataset.force=''; // clear force flag
1608
+
1609
+ if(!msgs.length){
1610
+ D.msgs.innerHTML='<div style="text-align:center;color:#ccc;margin-top:20px;font-size:12px;">暂无消息</div>';
1611
+ D.input.disabled=false; D.input.placeholder='输入消息...';
1612
+ return;
1613
+ }
1614
+ var html=msgs.map(function(m){
1615
+ var sent=m.type==='sent';
1616
+ var sender = sent ? S.aid : (m.from || 'unknown');
1617
+ var info = agentInfoCache[sender];
1618
+ if(!info){
1619
+ fetchAgentInfo(sender).then(function(){
1620
+ if(D.msgs.dataset.s===sig){ D.msgs.dataset.force='1'; renderMsgs(msgs, closed); }
1621
+ });
1622
+ }
1623
+ var avatarSrc = getAvatarSrc(info ? info.type : '');
1624
+ var t=fmtTime(m.timestamp);
1625
+ var c=(typeof marked!=='undefined'&&marked.parse)?marked.parse(m.content):escH(m.content);
1626
+ var name = (info && info.name) ? info.name : sender;
1627
+
1628
+ return '<div class="message '+m.type+'">' +
1629
+ '<img class="msg-avatar" src="'+avatarSrc+'" title="'+escH(name)+'">' +
1630
+ '<div class="msg-content">' +
1631
+ '<div class="msg-meta">'+(sent?'我':escH(name))+' · '+t+'</div>' +
1632
+ '<div class="bubble-wrap"><button class="copy-msg-btn" onclick="copyMsgText(this)">复制</button><div class="bubble">'+c+'</div></div>' +
1633
+ '</div></div>';
1634
+ }).join('');
1635
+ if(closed){
1636
+ html+='<div style="text-align:center;margin:16px 0;"><div style="display:inline-block;background:#fff3cd;color:#856404;padding:8px 20px;border-radius:20px;font-size:12px;border:1px solid #ffc107;">会话已关闭 — 请点击左侧 + 新建会话继续通信</div></div>';
1637
+ D.input.disabled=true; D.input.placeholder='会话已关闭,请新建会话';
1638
+ } else {
1639
+ D.input.disabled=false; D.input.placeholder='输入消息...';
1640
+ }
1641
+ var wasAtBottom=isAtBottom();
1642
+ var prevScrollTop=D.msgs.scrollTop;
1643
+ D.msgs.innerHTML=html+'<div class="new-msg-tip" id="newMsgTip" onclick="scrollToBottom()" style="display:none;">↓ 有新消息</div>';
1644
+ D.newMsgTip=$('newMsgTip');
1645
+ // 不自动滚动,保持用户当前位置;有新消息时显示提示
1646
+ if(!wasAtBottom&&msgs.length>0){
1647
+ D.msgs.scrollTop=prevScrollTop;
1648
+ if(D.msgs.dataset.force!=='avatar') showNewMsgTip();
1649
+ } else {
1650
+ D.msgs.scrollTop=prevScrollTop;
1651
+ }
1652
+ }
1653
+
1654
+ function updateDot(st){
1655
+ S.status=st;
1656
+ D.dot.className='status-dot '+(st||'');
1657
+ }
1658
+
1659
+ async function pickSession(sid,peer){
1660
+ if(S.tab!=='p2p') switchTab('p2p');
1661
+ S.sid=sid; S.sessionId=sid;
1662
+ hideNewMsgTip();
1663
+ D.title.textContent=peer;
1664
+ try {
1665
+ await fetch('/api/sessions/active',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sessionId:sid,aid:S.aid})});
1666
+ // 通知服务端本标签页的 activeSessionId
1667
+ if(_groupWs&&_groupWs.readyState===WebSocket.OPEN){
1668
+ _groupWs.send(JSON.stringify({type:'set_active_session',sessionId:sid}));
1669
+ }
1670
+ var r=await fetch('/api/messages?aid='+encodeURIComponent(S.aid)+'&sessionId='+encodeURIComponent(sid));
1671
+ var d=await r.json();
1672
+ S.closed=d.closed||false;
1673
+ D.msgs.dataset.s=''; // force
1674
+ renderMsgs(d.messages||[], S.closed);
1675
+ scrollToBottom();
1676
+ // 刷新会话列表,确保新会话出现在侧边栏
1677
+ loadSessions();
1678
+ } catch(e){}
1679
+ }
1680
+
1681
+ async function sendMessage(){
1682
+ var txt=D.input.value.trim();
1683
+ if(!txt){ return; }
1684
+ // 用户主动发送消息,确保滚动到底部
1685
+ hideNewMsgTip();
1686
+ // 群组模式
1687
+ if(S.tab==='group'){
1688
+ if(!S.activeGroupId){ alert('请先选择一个群组'); return; }
1689
+ try {
1690
+ D.input.value='';
1691
+ var r=await fetch('/api/group/send',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({groupId:S.activeGroupId,message:txt,aid:S.aid})});
1692
+ var d=await r.json();
1693
+ if(!d.success) alert(d.error||'发送失败');
1694
+ else {
1695
+ // 发送成功:立即追加到本地显示(服务端已存储,不用等 WS 推送)
1696
+ if(d.msg_id){
1697
+ var sentMsg={msg_id:d.msg_id,sender:S.aid,content:txt,content_type:'text',timestamp:d.timestamp||Date.now()};
1698
+ var exists=_lastGroupMsgs.some(function(m){ return m.msg_id===sentMsg.msg_id; });
1699
+ if(!exists){
1700
+ _lastGroupMsgs.push(sentMsg);
1701
+ _lastGroupMsgSig='';
1702
+ renderGroupMsgs(_lastGroupMsgs);
1703
+ scrollToBottom();
1704
+ }
1705
+ }
1706
+ }
1707
+ } catch(e){ alert('发送失败'); }
1708
+ return;
1709
+ }
1710
+ // P2P 模式
1711
+ if(!S.sid){ alert('请先选择或新建一个会话'); return; }
1712
+ if(S.closed){ alert('该会话已关闭,请新建会话继续通信'); return; }
1713
+ try {
1714
+ D.input.value='';
1715
+ var r=await fetch('/api/ws/send',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:txt,sessionId:S.sid,aid:S.aid})});
1716
+ var d=await r.json();
1717
+ if(!d.success) alert(d.error||'发送失败');
1718
+ else { await loadMessages(); scrollToBottom(); }
1719
+ } catch(e){ alert('发送失败'); }
1720
+ }
1721
+
1722
+ function toggleSidebar(){
1723
+ S.sidebarOpen=!S.sidebarOpen;
1724
+ D.sidebar.classList.toggle('collapsed',!S.sidebarOpen);
1725
+ }
1726
+
1727
+ function showModal(){ D.modal.classList.add('show'); D.tInput.value=''; D.tInput.focus(); }
1728
+ function hideModal(){ D.modal.classList.remove('show'); }
1729
+
1730
+ async function newSessionWith(peerAid){
1731
+ try {
1732
+ var r=await fetch('/api/ws/connect',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({targetAid:peerAid,aid:S.aid})});
1733
+ var d=await r.json();
1734
+ if(d.success){ pickSession(d.sessionId,peerAid); }
1735
+ else { alert(d.error||'连接失败'); }
1736
+ } catch(e){ alert('错误: '+e.message); }
1737
+ }
1738
+
1739
+ async function doConnect(){
1740
+ var aid=D.tInput.value.trim();
1741
+ if(!aid) return;
1742
+ D.cBtn.disabled=true; D.cBtn.textContent='连接中...';
1743
+ try {
1744
+ var r=await fetch('/api/ws/connect',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({targetAid:aid,aid:S.aid})});
1745
+ var d=await r.json();
1746
+ if(d.success){ hideModal(); pickSession(d.sessionId,aid); }
1747
+ else { alert(d.error||'连接失败'); }
1748
+ } catch(e){ alert('错误: '+e.message); }
1749
+ finally { D.cBtn.disabled=false; D.cBtn.textContent='连接'; }
1750
+ }
1751
+
1752
+ function escH(t){ var d=document.createElement('div'); d.textContent=t; return d.innerHTML; }
1753
+ function copyMsgText(btn){ var bubble=btn.parentElement.querySelector('.bubble'); if(!bubble) return; var text=bubble.innerText||bubble.textContent||''; navigator.clipboard.writeText(text).then(function(){ btn.textContent='已复制'; setTimeout(function(){ btn.textContent='复制'; },1200); }).catch(function(){ btn.textContent='失败'; setTimeout(function(){ btn.textContent='复制'; },1200); }); }
1754
+ function isUserSelecting(){ var sel=window.getSelection(); if(!sel||sel.isCollapsed||!sel.rangeCount) return false; var range=sel.getRangeAt(0); return D.msgs&&D.msgs.contains(range.commonAncestorContainer); }
1755
+ function escA(t){ return t.replace(/\\\\/g,'\\\\\\\\').replace(/'/g,"\\\\'"); }
1756
+ function fmtTime(ts){
1757
+ if(!ts) return '';
1758
+ var n=Number(ts);
1759
+ if(isNaN(n)) return '';
1760
+ if(n<1e12) n=n*1000;
1761
+ var d=new Date(n);
1762
+ if(isNaN(d.getTime())) return '';
1763
+ var now=new Date();
1764
+ var pad=function(v){ return v<10?'0'+v:''+v; };
1765
+ var H=pad(d.getHours()), M=pad(d.getMinutes());
1766
+ if(d.getFullYear()===now.getFullYear()&&d.getMonth()===now.getMonth()&&d.getDate()===now.getDate()){
1767
+ return H+':'+M;
1768
+ }
1769
+ // 今年内省略年份
1770
+ if(d.getFullYear()===now.getFullYear()){
1771
+ return pad(d.getMonth()+1)+'/'+pad(d.getDate())+' '+H+':'+M;
1772
+ }
1773
+ return d.getFullYear()+'/'+pad(d.getMonth()+1)+'/'+pad(d.getDate())+' '+H+':'+M;
1774
+ }
1775
+
1776
+ // ============================================================
1777
+ // Group Functions
1778
+ // ============================================================
1779
+
1780
+ function switchTab(tab){
1781
+ S.tab=tab;
1782
+ D.tabP2P.className='tab'+(tab==='p2p'?' active':'');
1783
+ D.tabGroup.className='tab'+(tab==='group'?' active':'');
1784
+ D.p2pPanel.style.display=tab==='p2p'?'block':'none';
1785
+ D.groupPanel.style.display=tab==='group'?'flex':'none';
1786
+ if(tab==='group'){
1787
+ D.encryptBanner.style.display='none';
1788
+ D.groupInfoBar.style.display=S.activeGroupId?'flex':'none';
1789
+ D.input.placeholder='输入群消息...';
1790
+ D.input.disabled=!S.activeGroupId;
1791
+ D.msgs.dataset.s='';
1792
+ _lastGroupMsgSig='';
1793
+ initGroupClient();
1794
+ pollGroupList();
1795
+ if(S.activeGroupId) pollGroupMessages().then(function(){ scrollToBottom(); });
1796
+ else { D.msgs.innerHTML='<div style="text-align:center;color:#ccc;margin-top:20px;font-size:12px;">选择或创建一个群组</div>'; }
1797
+ } else {
1798
+ D.encryptBanner.style.display='flex';
1799
+ D.groupInfoBar.style.display='none';
1800
+ D.input.placeholder='输入消息...';
1801
+ D.input.disabled=false;
1802
+ _lastGroupMsgSig='';
1803
+ // 立即清空消息区域,防止群消息残留
1804
+ D.msgs.innerHTML='';
1805
+ // 切回P2P时立即刷新会话列表和消息
1806
+ D.sList.dataset.s='';
1807
+ D.msgs.dataset.s='';
1808
+ loadSessions();
1809
+ if(S.sid){
1810
+ fetch('/api/messages?aid='+encodeURIComponent(S.aid)+'&sessionId='+encodeURIComponent(S.sid)).then(function(r){ return r.json(); }).then(function(d){
1811
+ if(S.tab!=='p2p') return;
1812
+ S.closed=d.closed||false;
1813
+ if(d.messages) renderMsgs(d.messages, S.closed);
1814
+ scrollToBottom();
1815
+ }).catch(function(){});
1816
+ }
1817
+ }
1818
+ }
1819
+
1820
+ var _groupInited=false;
1821
+ async function initGroupClient(){
1822
+ if(_groupInited) return;
1823
+ try {
1824
+ var r=await fetch('/api/group/init',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({aid:S.aid})});
1825
+ var d=await r.json();
1826
+ if(d.success){ _groupInited=true; if(d.targetAid) S.groupTargetAid=d.targetAid; }
1827
+ } catch(e){ console.error('群组初始化失败',e); }
1828
+ }
1829
+
1830
+ async function pollGroupList(){
1831
+ try {
1832
+ var r=await fetch('/api/group/list?aid='+encodeURIComponent(S.aid));
1833
+ var d=await r.json();
1834
+ if(d.groups){ S.groups=d.groups; renderGroupList(); }
1835
+ } catch(e){}
1836
+ }
1837
+
1838
+ function renderGroupList(){
1839
+ if(!S.groups.length){
1840
+ D.groupList.innerHTML='<div style="padding:20px;text-align:center;color:#999;font-size:12px;">暂无群组</div>';
1841
+ return;
1842
+ }
1843
+ var html='';
1844
+ S.groups.forEach(function(g){
1845
+ var active=g.group_id===S.activeGroupId;
1846
+ html+='<div class="group-item'+(active?' active':'')+'" onclick="pickGroup(\\''+escA(g.group_id)+'\\',\\''+escA(g.name||g.group_id)+'\\')"><div class="group-item-name">'+escH(g.name||g.group_id)+'</div><div class="group-item-meta">ID: '+escH(g.group_id.length>20?g.group_id.substring(0,20)+'...':g.group_id)+(g.member_count?' · '+g.member_count+' 人':'')+'</div><button class="group-item-del" onclick="event.stopPropagation();leaveGroup(\\''+escA(g.group_id)+'\\');" title="退出群组">退出</button></div>';
1847
+ });
1848
+ D.groupList.innerHTML=html;
1849
+ }
1850
+
1851
+ async function pickGroup(groupId,name){
1852
+ var overlay=$('switchGroupOverlay');
1853
+ var gmsg=$('switchGroupMsg');
1854
+ overlay.style.display='flex';
1855
+ gmsg.textContent='切换群组中...';
1856
+ S.activeGroupId=groupId;
1857
+ S.isGroupCreator=false;
1858
+ _lastGroupMsgSig='';
1859
+ hideNewMsgTip();
1860
+ D.title.textContent=name;
1861
+ D.groupInfoBar.style.display='flex';
1862
+ D.groupInfoText.textContent=name;
1863
+ D.input.disabled=false;
1864
+ D.input.placeholder='输入群消息...';
1865
+ D.input.focus();
1866
+ // 默认隐藏创建者相关按钮
1867
+ $('groupInviteBtn').style.display='none';
1868
+ $('groupCopyLinkBtn').style.display='none';
1869
+ $('groupReviewBtn').style.display='none';
1870
+ $('groupDutyBtn').style.display='none';
1871
+ $('groupRuleBtn').style.display='none';
1872
+ _groupRuleData=null;
1873
+ renderGroupList();
1874
+ try {
1875
+ gmsg.textContent='选择群组...';
1876
+ await fetch('/api/group/select',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({groupId:groupId,aid:S.aid})});
1877
+ } catch(e){}
1878
+ // 获取群信息判断是否为创建者
1879
+ try {
1880
+ gmsg.textContent='获取群信息...';
1881
+ var r=await fetch('/api/group/info?groupId='+encodeURIComponent(groupId)+'&aid='+encodeURIComponent(S.aid));
1882
+ var d=await r.json();
1883
+ if(d.creator&&d.creator===S.aid){
1884
+ S.isGroupCreator=true;
1885
+ $('groupInviteBtn').style.display='';
1886
+ $('groupReviewBtn').style.display='';
1887
+ $('groupDutyBtn').style.display='';
1888
+ } else {
1889
+ $('groupCopyLinkBtn').style.display='';
1890
+ }
1891
+ } catch(e){
1892
+ // 获取失败时默认显示复制群链接
1893
+ $('groupCopyLinkBtn').style.display='';
1894
+ }
1895
+ try {
1896
+ gmsg.textContent='加载消息...';
1897
+ await pollGroupMessages();
1898
+ scrollToBottom();
1899
+ } catch(e){}
1900
+ overlay.style.display='none';
1901
+ }
1902
+
1903
+ var _lastGroupMsgSig='';
1904
+ async function pollGroupMessages(){
1905
+ if(!S.activeGroupId||S.tab!=='group') {
1906
+ console.log('[pollGroupMessages] SKIP: activeGroupId='+S.activeGroupId+' tab='+S.tab);
1907
+ return;
1908
+ }
1909
+ try {
1910
+ console.log('[pollGroupMessages] fetching: groupId='+S.activeGroupId+' aid='+S.aid);
1911
+ var r=await fetch('/api/group/messages?groupId='+encodeURIComponent(S.activeGroupId)+'&aid='+encodeURIComponent(S.aid));
1912
+ var d=await r.json();
1913
+ console.log('[pollGroupMessages] response: msgCount='+(d.messages?d.messages.length:0)+' tab='+S.tab);
1914
+ if(S.tab==='group'&&d.messages) renderGroupMsgs(d.messages);
1915
+ } catch(e){ console.error('[pollGroupMessages] error:', e); }
1916
+ }
1917
+
1918
+ // ============================================================
1919
+ // WebSocket: real-time group message push
1920
+ // ============================================================
1921
+ var _groupWs=null;
1922
+ var _groupWsReconnectTimer=null;
1923
+ var _groupWsReconnectDelay=1000; // exponential backoff start
1924
+ var _groupWsPingTimer=null;
1925
+
1926
+ function connectGroupWs(){
1927
+ if(_groupWs&&(_groupWs.readyState===WebSocket.OPEN||_groupWs.readyState===WebSocket.CONNECTING)) return;
1928
+ var proto=location.protocol==='https:'?'wss:':'ws:';
1929
+ _groupWs=new WebSocket(proto+'//'+location.host+'/ws/ui');
1930
+ _groupWs.onopen=function(){
1931
+ console.log('[WS] ui connected');
1932
+ _groupWsReconnectDelay=1000; // reset backoff on success
1933
+ if(_groupWsReconnectTimer){ clearTimeout(_groupWsReconnectTimer); _groupWsReconnectTimer=null; }
1934
+ // 绑定当前 aid
1935
+ if(S.aid) _groupWs.send(JSON.stringify({type:'bind_aid',aid:S.aid}));
1936
+ // 重连后主动拉取最新状态
1937
+ if(S.aid) fetch('/api/ws/status?aid='+encodeURIComponent(S.aid)).then(function(r){return r.json();}).then(function(d){updateDot(d.status);}).catch(function(){});
1938
+ fetch('/api/aid').then(function(r){return r.json();}).then(function(d){if(d.aidStatus){S.aidList=d.aidStatus;renderAidSelect();}}).catch(function(){});
1939
+ // 重连后补拉一次会话列表,防止断连期间丢失推送
1940
+ loadSessions();
1941
+ // 启动 keepalive ping(每 25s 发一次,防止代理/防火墙断连)
1942
+ if(_groupWsPingTimer) clearInterval(_groupWsPingTimer);
1943
+ _groupWsPingTimer=setInterval(function(){
1944
+ if(_groupWs&&_groupWs.readyState===WebSocket.OPEN){
1945
+ try{ _groupWs.send(JSON.stringify({type:'ping'})); }catch(e){}
1946
+ }
1947
+ },25000);
1948
+ };
1949
+ _groupWs.onmessage=function(ev){
1950
+ try {
1951
+ var data=JSON.parse(ev.data);
1952
+ handleGroupWsMessage(data);
1953
+ } catch(e){ console.error('[WS] parse error',e); }
1954
+ };
1955
+ _groupWs.onclose=function(){
1956
+ console.log('[WS] ui disconnected, reconnecting in '+_groupWsReconnectDelay+'ms...');
1957
+ _groupWs=null;
1958
+ if(_groupWsPingTimer){ clearInterval(_groupWsPingTimer); _groupWsPingTimer=null; }
1959
+ _groupWsReconnectTimer=setTimeout(function(){
1960
+ _groupWsReconnectDelay=Math.min(_groupWsReconnectDelay*2,30000); // cap at 30s
1961
+ connectGroupWs();
1962
+ },_groupWsReconnectDelay);
1963
+ };
1964
+ _groupWs.onerror=function(e){
1965
+ console.error('[WS] ui error',e);
1966
+ // onerror is always followed by onclose, so reconnect is handled there
1967
+ };
1968
+ }
1969
+
1970
+ function handleGroupWsMessage(data){
1971
+ if(data.type==='ws_status'){
1972
+ if(!data.aid||data.aid===S.aid) updateDot(data.status);
1973
+ return;
1974
+ }
1975
+ if(data.type==='aid_status'){
1976
+ S.aidList=data.aidStatus||[];
1977
+ renderAidSelect();
1978
+ return;
1979
+ }
1980
+ if(data.type==='p2p_message'){
1981
+ // 实时推送的 P2P 消息
1982
+ if(S.tab==='p2p' && data.sessionId===S.sid){
1983
+ loadMessages();
1984
+ }
1985
+ return;
1986
+ }
1987
+ if(data.type==='sessions_updated'){
1988
+ loadSessions();
1989
+ return;
1990
+ }
1991
+ if(data.type==='group_message'){
1992
+ // 实时推送的完整消息
1993
+ var msg=data.message;
1994
+ var gid=data.group_id;
1995
+ if(gid===S.activeGroupId&&S.tab==='group'){
1996
+ // 追加到当前消息列表并重新渲染
1997
+ var exists=_lastGroupMsgs.some(function(m){ return m.msg_id===msg.msg_id; });
1998
+ if(!exists){
1999
+ _lastGroupMsgs.push(msg);
2000
+ _lastGroupMsgSig=''; // 强制重新渲染
2001
+ renderGroupMsgs(_lastGroupMsgs);
2002
+ }
2003
+ }
2004
+ } else if(data.type==='group_message_batch'){
2005
+ // 批量推送的消息列表
2006
+ var gid=data.group_id;
2007
+ var msgs=data.messages||[];
2008
+ console.log('[WS] group_message_batch received: gid='+gid+' msgCount='+msgs.length+' activeGroupId='+S.activeGroupId+' tab='+S.tab+' msgIds='+msgs.map(function(m){return m.msg_id}).join(','));
2009
+ if(gid===S.activeGroupId&&S.tab==='group'){
2010
+ var changed=false;
2011
+ var existingIds=_lastGroupMsgs.map(function(m){return m.msg_id});
2012
+ console.log('[WS] group_message_batch: currentMsgCount='+_lastGroupMsgs.length+' existingLastId='+(existingIds.length>0?existingIds[existingIds.length-1]:'none'));
2013
+ msgs.forEach(function(msg){
2014
+ var exists=_lastGroupMsgs.some(function(m){ return m.msg_id===msg.msg_id; });
2015
+ if(!exists){
2016
+ _lastGroupMsgs.push(msg);
2017
+ changed=true;
2018
+ console.log('[WS] group_message_batch: ADDED msg_id='+msg.msg_id);
2019
+ } else {
2020
+ console.log('[WS] group_message_batch: SKIP duplicate msg_id='+msg.msg_id);
2021
+ }
2022
+ });
2023
+ console.log('[WS] group_message_batch: changed='+changed+' newTotal='+_lastGroupMsgs.length);
2024
+ if(changed){
2025
+ _lastGroupMsgSig=''; // 强制重新渲染
2026
+ renderGroupMsgs(_lastGroupMsgs);
2027
+ }
2028
+ } else {
2029
+ console.log('[WS] group_message_batch: IGNORED - gid mismatch or wrong tab. gid='+gid+' activeGroupId='+S.activeGroupId+' tab='+S.tab);
2030
+ }
2031
+ } else if(data.type==='new_message_notify'){
2032
+ // 轻量通知:如果是当前活跃群组,拉取最新消息(本地读取,很快)
2033
+ console.log('[WS] new_message_notify: gid='+data.group_id+' activeGroupId='+S.activeGroupId+' tab='+S.tab);
2034
+ if(data.group_id===S.activeGroupId&&S.tab==='group'){
2035
+ console.log('[WS] new_message_notify: triggering pollGroupMessages');
2036
+ pollGroupMessages();
2037
+ }
2038
+ } else if(data.type==='join_approved'||data.type==='group_invite'){
2039
+ // 群组变动,刷新群组列表
2040
+ pollGroupList();
2041
+ }
2042
+ }
2043
+
2044
+ var _lastGroupMsgs=[];
2045
+ var _groupRuleData=null;
2046
+ function renderGroupMsgs(msgs){
2047
+ // 不在群组 tab 时不渲染,防止覆盖 P2P 消息
2048
+ if(S.tab!=='group') return;
2049
+ if(isUserSelecting()) return;
2050
+ var sig=msgs.length+(msgs.length>0?(msgs[msgs.length-1].msg_id||0):'');
2051
+ if(_lastGroupMsgSig===sig&&!msgs._forceRender) return;
2052
+ var prevCount=_lastGroupMsgs.length;
2053
+ _lastGroupMsgSig=sig;
2054
+ _lastGroupMsgs=msgs;
2055
+ // 提取 group.ap 规则消息,保存最新一条,不在列表中展示
2056
+ var ruleMsgs=msgs.filter(function(m){
2057
+ if(!m.content) return false;
2058
+ try { var p=JSON.parse(m.content); return p&&p.source==='group.ap'; } catch(e){ return false; }
2059
+ });
2060
+ if(ruleMsgs.length>0){
2061
+ try { _groupRuleData=JSON.parse(ruleMsgs[ruleMsgs.length-1].content); } catch(e){ _groupRuleData={content:ruleMsgs[ruleMsgs.length-1].content}; }
2062
+ $('groupRuleBtn').style.display='';
2063
+ }
2064
+ var displayMsgs=msgs.filter(function(m){
2065
+ if(!m.content) return true;
2066
+ try { var p=JSON.parse(m.content); return !(p&&p.source==='group.ap'); } catch(e){ return true; }
2067
+ });
2068
+ if(!displayMsgs.length){
2069
+ D.msgs.innerHTML='<div style="text-align:center;color:#ccc;margin-top:20px;font-size:12px;">暂无消息</div><div class="new-msg-tip" id="newMsgTip" onclick="scrollToBottom()" style="display:none;">↓ 有新消息</div>';
2070
+ D.newMsgTip=$('newMsgTip');
2071
+ return;
2072
+ }
2073
+ var needFetch=[];
2074
+ var html=displayMsgs.map(function(m){
2075
+ var sent=m.sender===S.aid;
2076
+ var sender=m.sender||'unknown';
2077
+ var info=agentInfoCache[sender];
2078
+ if(!info){ needFetch.push(sender); }
2079
+ var avatarSrc=getAvatarSrc(info?info.type:'');
2080
+ var t=m.timestamp?fmtTime(m.timestamp):'';
2081
+
2082
+ var c=(typeof marked!=='undefined'&&marked.parse)?marked.parse(m.content||''):escH(m.content||'');
2083
+ var name=(info&&info.name)?info.name:sender;
2084
+ return '<div class="message '+(sent?'sent':'received')+'">' +
2085
+ '<img class="msg-avatar" src="'+avatarSrc+'" title="'+escH(name)+'">' +
2086
+ '<div class="msg-content">' +
2087
+ '<div class="msg-meta">'+(sent?'我':escH(name))+' · '+t+'</div>' +
2088
+ '<div class="bubble-wrap"><button class="copy-msg-btn" onclick="copyMsgText(this)">复制</button><div class="bubble">'+c+'</div></div>' +
2089
+ '</div></div>';
2090
+ }).join('');
2091
+ var wasAtBottom=isAtBottom();
2092
+ var prevScrollTop=D.msgs.scrollTop;
2093
+ D.msgs.innerHTML=html+'<div class="new-msg-tip" id="newMsgTip" onclick="scrollToBottom()" style="display:none;">↓ 有新消息</div>';
2094
+ D.newMsgTip=$('newMsgTip');
2095
+ // 有新消息且用户不在底部:保持位置,显示提示
2096
+ if(msgs.length>prevCount&&prevCount>0&&!wasAtBottom){
2097
+ D.msgs.scrollTop=prevScrollTop;
2098
+ showNewMsgTip();
2099
+ } else {
2100
+ D.msgs.scrollTop=prevScrollTop;
2101
+ }
2102
+ // 异步加载未缓存的 agent info,加载完成后重新渲染以更新头像
2103
+ var unique=needFetch.filter(function(v,i,a){ return a.indexOf(v)===i; });
2104
+ unique.forEach(function(aid){
2105
+ fetchAgentInfo(aid).then(function(){
2106
+ if(S.tab!=='group') return;
2107
+ _lastGroupMsgSig='';
2108
+ _lastGroupMsgs._forceRender=true;
2109
+ renderGroupMsgs(_lastGroupMsgs);
2110
+ });
2111
+ });
2112
+ }
2113
+
2114
+ // Group modals
2115
+ function showCreateGroupModal(){ $('createGroupModal').classList.add('show'); $('groupNameInput').value=''; $('groupDescInput').value=''; var cards=$('groupTypeCards').children; for(var i=0;i<cards.length;i++){cards[i].classList.remove('selected');} cards[0].classList.add('selected'); var dcards=$('dutyRuleCards').children; for(var i=0;i<dcards.length;i++){dcards[i].classList.remove('selected');} dcards[0].classList.add('selected'); $('groupNameInput').focus(); }
2116
+ function hideCreateGroupModal(){ $('createGroupModal').classList.remove('show'); }
2117
+ function selectGroupType(el){ var cards=el.parentElement.children; for(var i=0;i<cards.length;i++){cards[i].classList.remove('selected');} el.classList.add('selected'); }
2118
+ function selectDutyRule(el){ var cards=el.parentElement.children; for(var i=0;i<cards.length;i++){cards[i].classList.remove('selected');} el.classList.add('selected'); }
2119
+ function showJoinGroupModal(){ $('joinGroupModal').classList.add('show'); $('joinGroupUrlInput').value=''; $('joinGroupUrlInput').focus(); }
2120
+ function hideJoinGroupModal(){ $('joinGroupModal').classList.remove('show'); }
2121
+ function hideMembersModal(){ $('membersModal').classList.remove('show'); }
2122
+
2123
+ function selectDutyConfigCard(el){ var cards=el.parentElement.children; for(var i=0;i<cards.length;i++){cards[i].classList.remove('selected');} el.classList.add('selected'); }
2124
+ async function showDutyConfigModal(){
2125
+ var cards=$('dutyConfigCards').children;
2126
+ for(var i=0;i<cards.length;i++){cards[i].classList.remove('selected');}
2127
+ $('dutyConfigModal').classList.add('show');
2128
+ try {
2129
+ var r=await fetch('/api/group/duty-status?groupId='+encodeURIComponent(S.activeGroupId)+'&aid='+encodeURIComponent(S.aid));
2130
+ var d=await r.json();
2131
+ if(d.success&&d.config&&d.config.mode){
2132
+ var mode=d.config.mode;
2133
+ for(var i=0;i<cards.length;i++){
2134
+ if(cards[i].getAttribute('data-value')===mode){ cards[i].classList.add('selected'); }
2135
+ }
2136
+ } else { cards[0].classList.add('selected'); }
2137
+ } catch(e){ cards[0].classList.add('selected'); }
2138
+ }
2139
+ function hideDutyConfigModal(){ $('dutyConfigModal').classList.remove('show'); }
2140
+ function showGroupRuleModal(){
2141
+ if(!_groupRuleData){ alert('暂无群规则数据'); return; }
2142
+ var d=_groupRuleData;
2143
+ var html='';
2144
+ // 值班信息
2145
+ var lines=(d.content||'').split('\\n');
2146
+ html+='<div style="background:#f8f9fa;border-radius:8px;padding:12px;margin-bottom:12px;border:1px solid var(--border);">';
2147
+ html+='<div style="font-weight:600;font-size:13px;margin-bottom:8px;color:var(--primary);">值班信息</div>';
2148
+ for(var i=0;i<lines.length;i++){
2149
+ var line=lines[i].trim();
2150
+ if(!line) continue;
2151
+ var parts=line.split(':');
2152
+ if(parts.length>=2){
2153
+ html+='<div style="margin-bottom:4px;font-size:12px;line-height:1.5;"><span style="color:var(--t2);">'+escH(parts[0].trim())+':</span> <span style="color:var(--t1);font-weight:500;">'+escH(parts.slice(1).join(':').trim())+'</span></div>';
2154
+ } else {
2155
+ html+='<div style="margin-bottom:4px;font-size:12px;line-height:1.5;color:var(--t1);">'+escH(line)+'</div>';
2156
+ }
2157
+ }
2158
+ html+='</div>';
2159
+ // 成员列表
2160
+ if(d.members&&d.members.length){
2161
+ html+='<div style="background:#f8f9fa;border-radius:8px;padding:12px;border:1px solid var(--border);">';
2162
+ html+='<div style="font-weight:600;font-size:13px;margin-bottom:8px;color:var(--primary);">群成员 ('+d.members.length+')</div>';
2163
+ for(var i=0;i<d.members.length;i++){
2164
+ var m=d.members[i];
2165
+ var roleColor=m.role==='creator'?'#e67e22':'#95a5a6';
2166
+ var typeIcon=m.agent_type.indexOf('human')>=0?'\uD83D\uDC64':'\uD83E\uDD16';
2167
+ html+='<div style="display:flex;align-items:center;gap:8px;padding:8px 0;border-bottom:1px solid rgba(0,0,0,0.05);">';
2168
+ html+='<span style="font-size:16px;">'+typeIcon+'</span>';
2169
+ html+='<div style="flex:1;min-width:0;">';
2170
+ html+='<div style="font-size:12px;font-weight:600;color:var(--t1);">'+escH(m.nickname||m.agent_id)+'</div>';
2171
+ html+='<div style="font-size:11px;color:var(--t2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">'+escH(m.agent_id)+'</div>';
2172
+ if(m.capability) html+='<div style="font-size:11px;color:var(--t2);margin-top:2px;line-height:1.3;">'+escH(m.capability)+'</div>';
2173
+ html+='</div>';
2174
+ html+='<span style="font-size:11px;color:'+roleColor+';background:rgba(0,0,0,0.04);padding:2px 6px;border-radius:4px;">'+escH(m.role)+'</span>';
2175
+ html+='</div>';
2176
+ }
2177
+ html+='</div>';
2178
+ }
2179
+ $('groupRuleContent').innerHTML=html;
2180
+ $('groupRuleModal').classList.add('show');
2181
+ }
2182
+ function hideGroupRuleModal(){ $('groupRuleModal').classList.remove('show'); }
2183
+ async function saveDutyConfig(){
2184
+ var sel=document.querySelector('#dutyConfigCards .duty-rule-card.selected');
2185
+ if(!sel){ alert('请选择值班模式'); return; }
2186
+ var mode=sel.getAttribute('data-value');
2187
+ var btn=$('saveDutyConfigBtn');
2188
+ btn.disabled=true; btn.textContent='保存中...';
2189
+ try {
2190
+ var r=await fetch('/api/group/update-duty-config',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({groupId:S.activeGroupId,aid:S.aid,mode:mode})});
2191
+ var d=await r.json();
2192
+ if(d.success){ hideDutyConfigModal(); } else { alert(d.error||'保存失败'); }
2193
+ } catch(e){ alert('保存失败: '+e.message); }
2194
+ finally { btn.disabled=false; btn.textContent='保存'; }
2195
+ }
2196
+
2197
+ async function doCreateGroup(){
2198
+ var name=$('groupNameInput').value.trim();
2199
+ if(!name) return;
2200
+ var description=$('groupDescInput').value.trim();
2201
+ if(!description){ $('groupDescInput').focus(); return; }
2202
+ var visibility=document.querySelector('#groupTypeCards .group-type-card.selected').getAttribute('data-value');
2203
+ var dutyMode=document.querySelector('#dutyRuleCards .duty-rule-card.selected').getAttribute('data-value');
2204
+ var btn=$('createGroupBtn');
2205
+ btn.disabled=true; btn.textContent='创建中...';
2206
+ try {
2207
+ var r=await fetch('/api/group/create',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:name,visibility:visibility,description:description||undefined,duty_mode:dutyMode,aid:S.aid})});
2208
+ var d=await r.json();
2209
+ if(d.success){
2210
+ hideCreateGroupModal();
2211
+ pollGroupList();
2212
+ pickGroup(d.group_id,name);
2213
+ } else { alert(d.error||'创建失败'); }
2214
+ } catch(e){ alert('创建失败: '+e.message); }
2215
+ finally { btn.disabled=false; btn.textContent='创建'; }
2216
+ }
2217
+
2218
+ async function doJoinGroup(){
2219
+ var rawUrl=$('joinGroupUrlInput').value.trim();
2220
+ if(!rawUrl){ alert('请输入群聊链接或邀请链接'); return; }
2221
+ // 从 URL 中解析 code 参数
2222
+ var code='';
2223
+ var groupUrl=rawUrl;
2224
+ try {
2225
+ var u=new URL(rawUrl);
2226
+ code=u.searchParams.get('code')||'';
2227
+ u.searchParams.delete('code');
2228
+ groupUrl=u.origin+u.pathname;
2229
+ } catch(e){}
2230
+ var btn=$('joinGroupBtn');
2231
+ btn.disabled=true; btn.textContent=code?'加入中...':'申请中...';
2232
+ try {
2233
+ var r=await fetch('/api/group/join',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({groupUrl:groupUrl,code:code||undefined,aid:S.aid})});
2234
+ var d=await r.json();
2235
+ if(d.success){
2236
+ hideJoinGroupModal();
2237
+ pollGroupList();
2238
+ if(d.group_id) pickGroup(d.group_id,d.group_id);
2239
+ if(d.pending) alert('入群申请已发送,请等待管理员审核');
2240
+ } else { alert(d.error||'操作失败'); }
2241
+ } catch(e){ alert('操作失败: '+e.message); }
2242
+ finally { btn.disabled=false; btn.textContent='加入'; }
2243
+ }
2244
+
2245
+ async function copyGroupLink(){
2246
+ if(!S.activeGroupId) return;
2247
+ var groupUrl='https://'+S.groupTargetAid+'/'+S.activeGroupId;
2248
+ try { await navigator.clipboard.writeText(groupUrl); alert('群链接已复制到剪贴板\\n\\n'+groupUrl); }
2249
+ catch(e){ prompt('请复制群链接:',groupUrl); }
2250
+ }
2251
+
2252
+ async function generateInviteLink(){
2253
+ if(!S.activeGroupId) return;
2254
+ try {
2255
+ var r=await fetch('/api/group/invite-code',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({groupId:S.activeGroupId,aid:S.aid})});
2256
+ var d=await r.json();
2257
+ if(d.success&&d.code){
2258
+ var baseUrl=d.group_url||('https://'+S.groupTargetAid+'/'+S.activeGroupId);
2259
+ var inviteUrl=baseUrl+'?code='+d.code;
2260
+ try {
2261
+ await navigator.clipboard.writeText(inviteUrl);
2262
+ alert('邀请链接已复制到剪贴板\\n\\n'+inviteUrl);
2263
+ } catch(e){
2264
+ prompt('请手动复制邀请链接:',inviteUrl);
2265
+ }
2266
+ } else { alert(d.error||'生成邀请码失败'); }
2267
+ } catch(e){ alert('生成邀请码失败: '+e.message); }
2268
+ }
2269
+
2270
+ function copyMemberAid(btn,aid){
2271
+ navigator.clipboard.writeText(aid).then(function(){
2272
+ btn.textContent='已复制';
2273
+ setTimeout(function(){ btn.textContent='复制'; },1200);
2274
+ });
2275
+ }
2276
+
2277
+ async function openAgentMdPage(aid){
2278
+ try {
2279
+ var r=await fetch('/api/agent-md-raw?aid='+encodeURIComponent(aid));
2280
+ var d=await r.json();
2281
+ if(!d.success||!d.content){ alert(d.error||'获取 agent.md 失败'); return; }
2282
+ var md=d.content;
2283
+ // 简单 markdown 渲染
2284
+ function renderMd(src){
2285
+ var h=escH(src);
2286
+ // headings
2287
+ h=h.replace(/^######\\s+(.+)$/gm,'<h6>$1</h6>');
2288
+ h=h.replace(/^#####\\s+(.+)$/gm,'<h5>$1</h5>');
2289
+ h=h.replace(/^####\\s+(.+)$/gm,'<h4>$1</h4>');
2290
+ h=h.replace(/^###\\s+(.+)$/gm,'<h3>$1</h3>');
2291
+ h=h.replace(/^##\\s+(.+)$/gm,'<h2>$1</h2>');
2292
+ h=h.replace(/^#\\s+(.+)$/gm,'<h1>$1</h1>');
2293
+ // bold & italic
2294
+ h=h.replace(/\\*\\*(.+?)\\*\\*/g,'<strong>$1</strong>');
2295
+ h=h.replace(/\\*(.+?)\\*/g,'<em>$1</em>');
2296
+ // blockquote
2297
+ h=h.replace(/^&gt;\\s?(.+)$/gm,'<blockquote style="border-left:3px solid #ddd;padding-left:12px;color:#666;margin:8px 0;">$1</blockquote>');
2298
+ // list items
2299
+ h=h.replace(/^-\\s+(.+)$/gm,'<li>$1</li>');
2300
+ // code inline
2301
+ var bt=String.fromCharCode(96);
2302
+ h=h.replace(new RegExp(bt+'([^'+bt+']+)'+bt,'g'),'<code style="background:#f5f5f5;padding:1px 4px;border-radius:3px;font-size:12px;">$1</code>');
2303
+ // frontmatter block: hide ---...---
2304
+ h=h.replace(/^---[\\s\\S]*?---\\s*/,'');
2305
+ // paragraphs
2306
+ h=h.replace(/\\n\\n/g,'</p><p>');
2307
+ h='<p>'+h+'</p>';
2308
+ return h;
2309
+ }
2310
+ var html='<!DOCTYPE html><html><head><meta charset="utf-8"><title>'+escH(aid)+' - Agent Profile</title>'
2311
+ +'<style>body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;max-width:720px;margin:40px auto;padding:0 20px;color:#333;line-height:1.6;}'
2312
+ +'h1{border-bottom:2px solid #eee;padding-bottom:8px;}h2{border-bottom:1px solid #eee;padding-bottom:6px;margin-top:24px;}'
2313
+ +'ul{padding-left:20px;}li{margin:4px 0;}blockquote{margin:12px 0;}'
2314
+ +'pre{background:#f5f5f5;padding:12px;border-radius:6px;overflow-x:auto;}'
2315
+ +'.aid-badge{display:inline-block;background:#e8f4fd;color:#0969da;padding:2px 8px;border-radius:10px;font-size:12px;font-family:monospace;margin-bottom:16px;}'
2316
+ +'</style></head><body>'
2317
+ +'<div class="aid-badge">'+escH(aid)+'</div>'
2318
+ +renderMd(md)
2319
+ +'</body></html>';
2320
+ var w=window.open('','_blank');
2321
+ if(w){ w.document.write(html); w.document.close(); }
2322
+ else { alert('弹窗被拦截,请允许弹窗后重试'); }
2323
+ } catch(e){ alert('获取 agent.md 失败: '+e.message); }
2324
+ }
2325
+
2326
+ async function showGroupMembers(){
2327
+ if(!S.activeGroupId) return;
2328
+ try {
2329
+ var r=await fetch('/api/group/members?groupId='+encodeURIComponent(S.activeGroupId)+'&aid='+encodeURIComponent(S.aid));
2330
+ var d=await r.json();
2331
+ if(d.members){
2332
+ var html=d.members.map(function(m){
2333
+ var aid=m.agent_id||m;
2334
+ if(typeof aid!=='string') aid=JSON.stringify(aid);
2335
+ var role=m.role||'';
2336
+ var cachedInfo=agentInfoCache[aid];
2337
+ var avatarSrc=getAvatarSrc(cachedInfo?cachedInfo.type:'');
2338
+ var displayName=(cachedInfo&&cachedInfo.name)?cachedInfo.name:aid.split('.')[0];
2339
+ var typeTags='';
2340
+ if(cachedInfo&&cachedInfo.tags&&cachedInfo.tags.length){
2341
+ typeTags=cachedInfo.tags.map(function(t){ return '<span style="display:inline-block;background:#e8f4fd;color:#0969da;padding:1px 6px;border-radius:8px;font-size:10px;margin-right:4px;">'+escH(t)+'</span>'; }).join('');
2342
+ } else if(cachedInfo&&cachedInfo.type){
2343
+ typeTags='<span style="display:inline-block;background:#e8f4fd;color:#0969da;padding:1px 6px;border-radius:8px;font-size:10px;">'+escH(cachedInfo.type)+'</span>';
2344
+ }
2345
+ if(role){ typeTags+='<span style="display:inline-block;background:#fff3cd;color:#856404;padding:1px 6px;border-radius:8px;font-size:10px;margin-left:4px;">'+escH(role)+'</span>'; }
2346
+ var safeId='member-'+escH(aid).replace(/\\./g,'_');
2347
+ return '<div id="'+safeId+'" style="padding:10px 0;border-bottom:1px solid #f3f4f6;display:flex;align-items:center;gap:10px;">'
2348
+ +'<img src="'+avatarSrc+'" style="width:36px;height:36px;border-radius:50%;flex-shrink:0;" class="member-avatar" data-aid="'+escH(aid)+'">'
2349
+ +'<div style="flex:1;min-width:0;">'
2350
+ +'<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap;">'
2351
+ +'<span style="font-size:13px;font-weight:500;" class="member-name" data-aid="'+escH(aid)+'">'+escH(displayName)+'</span>'
2352
+ +'<span class="member-tags" data-aid="'+escH(aid)+'">'+typeTags+'</span>'
2353
+ +'</div>'
2354
+ +'<div style="font-size:11px;color:var(--t2);font-family:monospace;margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">'+escH(aid)+'</div>'
2355
+ +'</div>'
2356
+ +'<div style="display:flex;gap:4px;flex-shrink:0;">'
2357
+ +'<button class="mbtn mbtn-ok" style="padding:4px 10px;font-size:11px;" onclick="copyMemberAid(this,\\''+escH(aid)+'\\')">复制</button>'
2358
+ +'<button class="mbtn mbtn-cancel" style="padding:4px 10px;font-size:11px;" onclick="openAgentMdPage(\\''+escH(aid)+'\\')">查看</button>'
2359
+ +'</div></div>';
2360
+ }).join('');
2361
+ $('membersList').innerHTML=html||'<div style="color:#999;">暂无成员</div>';
2362
+ // 异步加载未缓存的 agent info
2363
+ d.members.forEach(function(m){
2364
+ var aid=m.agent_id||m;
2365
+ if(typeof aid!=='string') aid=JSON.stringify(aid);
2366
+ if(!aid||agentInfoCache[aid]) return;
2367
+ fetchAgentInfo(aid).then(function(info){
2368
+ if(!info||(!info.name&&!info.type)) return;
2369
+ var safeId='member-'+aid.replace(/\\./g,'_');
2370
+ var el=document.getElementById(safeId);
2371
+ if(!el) return;
2372
+ var avatarEl=el.querySelector('.member-avatar[data-aid="'+aid+'"]');
2373
+ var nameEl=el.querySelector('.member-name[data-aid="'+aid+'"]');
2374
+ var tagsEl=el.querySelector('.member-tags[data-aid="'+aid+'"]');
2375
+ if(avatarEl) avatarEl.src=getAvatarSrc(info.type);
2376
+ if(nameEl) nameEl.textContent=info.name||aid.split('.')[0];
2377
+ if(tagsEl){
2378
+ var tags='';
2379
+ if(info.tags&&info.tags.length){
2380
+ tags=info.tags.map(function(t){ return '<span style="display:inline-block;background:#e8f4fd;color:#0969da;padding:1px 6px;border-radius:8px;font-size:10px;margin-right:4px;">'+escH(t)+'</span>'; }).join('');
2381
+ } else if(info.type){
2382
+ tags='<span style="display:inline-block;background:#e8f4fd;color:#0969da;padding:1px 6px;border-radius:8px;font-size:10px;">'+escH(info.type)+'</span>';
2383
+ }
2384
+ // 保留已有的 role tag
2385
+ var existingRole=tagsEl.querySelector('span[style*="fff3cd"]');
2386
+ tagsEl.innerHTML=tags+(existingRole?existingRole.outerHTML:'');
2387
+ }
2388
+ });
2389
+ });
2390
+ } else { $('membersList').innerHTML='<div style="color:#999;">获取失败</div>'; }
2391
+ $('membersModal').classList.add('show');
2392
+ } catch(e){ alert('获取成员失败: '+e.message); }
2393
+ }
2394
+
2395
+ function hidePendingRequestsModal(){ $('pendingRequestsModal').classList.remove('show'); }
2396
+
2397
+ async function showPendingRequests(){
2398
+ if(!S.activeGroupId) return;
2399
+ try {
2400
+ var r=await fetch('/api/group/pending-requests?groupId='+encodeURIComponent(S.activeGroupId)+'&aid='+encodeURIComponent(S.aid));
2401
+ var d=await r.json();
2402
+ if(d.requests&&d.requests.length>0){
2403
+ // 先渲染基础结构,然后异步加载 agent info
2404
+ var html=d.requests.map(function(req){
2405
+ var aid=req.agent_id||'';
2406
+ var msg=req.message?escH(req.message):'';
2407
+ var time=req.created_at?fmtTime(req.created_at):'';
2408
+ var cachedInfo=agentInfoCache[aid];
2409
+ var avatarSrc=getAvatarSrc(cachedInfo?cachedInfo.type:'');
2410
+ var displayName=(cachedInfo&&cachedInfo.name)?cachedInfo.name:aid;
2411
+ var desc=(cachedInfo&&cachedInfo.description)?cachedInfo.description:'';
2412
+ return '<div id="pending-'+escH(aid).replace(/\\./g,'_')+'" style="padding:10px 0;border-bottom:1px solid #f3f4f6;display:flex;align-items:flex-start;gap:10px;">'
2413
+ +'<img src="'+avatarSrc+'" style="width:36px;height:36px;border-radius:50%;flex-shrink:0;margin-top:2px;" class="pending-avatar" data-aid="'+escH(aid)+'">'
2414
+ +'<div style="flex:1;min-width:0;">'
2415
+ +'<div style="font-size:13px;font-weight:500;" class="pending-name" data-aid="'+escH(aid)+'">'+escH(displayName)+'</div>'
2416
+ +'<div style="font-size:11px;color:var(--t2);font-family:monospace;margin-top:2px;">'+escH(aid)+'</div>'
2417
+ +'<div style="font-size:11px;color:var(--t2);margin-top:2px;display:'+(desc?'block':'none')+';" class="pending-desc" data-aid="'+escH(aid)+'">'+escH(desc)+'</div>'
2418
+ +(msg?'<div style="font-size:11px;color:#666;margin-top:4px;background:#f8f9fa;padding:4px 8px;border-radius:4px;">申请留言: '+msg+'</div>':'')
2419
+ +(time?'<div style="font-size:10px;color:var(--t2);margin-top:3px;">'+time+'</div>':'')
2420
+ +'</div>'
2421
+ +'<div style="display:flex;gap:4px;flex-shrink:0;margin-top:2px;">'
2422
+ +'<button class="mbtn mbtn-ok" style="padding:4px 10px;font-size:11px;" onclick="reviewJoin(\\''+escH(aid)+'\\',\\'approve\\')">通过</button>'
2423
+ +'<button class="mbtn mbtn-cancel" style="padding:4px 10px;font-size:11px;" onclick="reviewJoin(\\''+escH(aid)+'\\',\\'reject\\')">拒绝</button>'
2424
+ +'</div></div>';
2425
+ }).join('');
2426
+ $('pendingRequestsList').innerHTML=html;
2427
+ // 异步加载未缓存的 agent info
2428
+ d.requests.forEach(function(req){
2429
+ var aid=req.agent_id||'';
2430
+ if(!aid||agentInfoCache[aid]) return;
2431
+ fetchAgentInfo(aid).then(function(info){
2432
+ if(!info||(!info.name&&!info.type)) return;
2433
+ var safeId='pending-'+aid.replace(/\\./g,'_');
2434
+ var el=document.getElementById(safeId);
2435
+ if(!el) return;
2436
+ var avatarEl=el.querySelector('.pending-avatar[data-aid="'+aid+'"]');
2437
+ var nameEl=el.querySelector('.pending-name[data-aid="'+aid+'"]');
2438
+ var descEl=el.querySelector('.pending-desc[data-aid="'+aid+'"]');
2439
+ if(avatarEl) avatarEl.src=getAvatarSrc(info.type);
2440
+ if(nameEl) nameEl.textContent=info.name||aid;
2441
+ if(descEl&&info.description){ descEl.textContent=info.description; descEl.style.display='block'; }
2442
+ });
2443
+ });
2444
+ } else {
2445
+ $('pendingRequestsList').innerHTML='<div style="padding:16px;text-align:center;color:#999;font-size:12px;">暂无入群申请</div>';
2446
+ }
2447
+ $('pendingRequestsModal').classList.add('show');
2448
+ } catch(e){ alert('获取入群申请失败: '+e.message); }
2449
+ }
2450
+
2451
+ async function reviewJoin(agentId,action){
2452
+ if(!S.activeGroupId) return;
2453
+ try {
2454
+ var r=await fetch('/api/group/review-join',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({groupId:S.activeGroupId,agentId:agentId,action:action,aid:S.aid})});
2455
+ var d=await r.json();
2456
+ if(d.success){ showPendingRequests(); }
2457
+ else { alert(d.error||'操作失败'); }
2458
+ } catch(e){ alert('操作失败: '+e.message); }
2459
+ }
2460
+
2461
+ async function leaveGroup(groupId){
2462
+ if(!confirm('确认退出该群组?')) return;
2463
+ try {
2464
+ var r=await fetch('/api/group/leave',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({groupId:groupId,aid:S.aid})});
2465
+ var d=await r.json();
2466
+ if(d.success){
2467
+ if(S.activeGroupId===groupId){
2468
+ S.activeGroupId=null;
2469
+ D.title.textContent='未选择群组';
2470
+ D.groupInfoBar.style.display='none';
2471
+ D.msgs.innerHTML='';
2472
+ D.input.disabled=true;
2473
+ }
2474
+ pollGroupList();
2475
+ } else { alert(d.error||'退出失败'); }
2476
+ } catch(e){ alert('退出失败: '+e.message); }
2477
+ }
2478
+
2479
+ // ============================================================
2480
+ // 我的群 Functions
2481
+ // ============================================================
2482
+ function showMyGroupsModal(){ $('myGroupsModal').classList.add('show'); }
2483
+ function hideMyGroupsModal(){ $('myGroupsModal').classList.remove('show'); }
2484
+ async function showMyGroups(){
2485
+ showMyGroupsModal();
2486
+ $('myGroupsContent').innerHTML='<div style="text-align:center;padding:20px;color:#999;">加载中...</div>';
2487
+ try {
2488
+ var r=await fetch('/api/group/my-groups?aid='+encodeURIComponent(S.aid));
2489
+ var d=await r.json();
2490
+ if(!d.success){ $('myGroupsContent').innerHTML='<div style="text-align:center;padding:20px;color:#e74c3c;">'+escH(d.error||'获取失败')+'</div>'; return; }
2491
+ var groups=d.groups||[];
2492
+ if(!groups.length){ $('myGroupsContent').innerHTML='<div style="text-align:center;padding:20px;color:#999;">暂无群组</div>'; return; }
2493
+ var html='<table style="width:100%;border-collapse:collapse;font-size:12px;">';
2494
+ html+='<tr style="background:#f8fafc;"><th style="padding:8px 6px;text-align:left;border-bottom:1px solid #e2e8f0;">群名称</th><th style="padding:8px 6px;text-align:left;border-bottom:1px solid #e2e8f0;">群ID</th><th style="padding:8px 6px;text-align:center;border-bottom:1px solid #e2e8f0;">角色</th><th style="padding:8px 6px;text-align:center;border-bottom:1px solid #e2e8f0;">状态</th></tr>';
2495
+ groups.forEach(function(g){
2496
+ var statusText=g.status===1?'正常':g.status===0?'待审核':'未知('+g.status+')';
2497
+ var statusColor=g.status===1?'#10b981':g.status===0?'#f59e0b':'#94a3b8';
2498
+ var shortId=g.group_id.length>16?g.group_id.substring(0,16)+'...':g.group_id;
2499
+ html+='<tr style="border-bottom:1px solid #f1f5f9;cursor:pointer;" onmouseover="this.style.background=\\'#f0f9ff\\'" onmouseout="this.style.background=\\'\\'">';
2500
+ html+='<td style="padding:8px 6px;font-weight:500;">'+escH(g.name||g.group_id)+'</td>';
2501
+ html+='<td style="padding:8px 6px;color:#64748b;" title="'+escH(g.group_id)+'">'+escH(shortId)+'</td>';
2502
+ html+='<td style="padding:8px 6px;text-align:center;">'+escH(g.role||'-')+'</td>';
2503
+ html+='<td style="padding:8px 6px;text-align:center;"><span style="color:'+statusColor+';font-weight:500;">'+escH(statusText)+'</span></td>';
2504
+ html+='</tr>';
2505
+ });
2506
+ html+='</table>';
2507
+ html+='<div style="margin-top:8px;font-size:11px;color:#94a3b8;text-align:right;">共 '+d.total+' 个群组</div>';
2508
+ $('myGroupsContent').innerHTML=html;
2509
+ } catch(e){ $('myGroupsContent').innerHTML='<div style="text-align:center;padding:20px;color:#e74c3c;">请求失败: '+escH(e.message)+'</div>'; }
2510
+ }
2511
+
2512
+ // 扩展轮询:保留 P2P 等基础轮询,群组消息已通过 WebSocket 实时推送
2513
+ // 不再每秒轮询群消息
2514
+
2515
+ init();
2516
+ <\/script>
2517
+ </body>
2087
2518
  </html>`;
2088
2519
  function sendJson(res, data, status = 200) {
2089
2520
  res.writeHead(status, { 'Content-Type': 'application/json' });
@@ -2192,7 +2623,7 @@ async function handleRequest(req, res) {
2192
2623
  try {
2193
2624
  const aidList = await datamanager_1.CertAndKeyStore.getAids();
2194
2625
  const aidStatus = await getAidStatusList();
2195
- sendJson(res, { currentAid, aidList, aidStatus, apiUrl: globalApiUrl });
2626
+ sendJson(res, { aidList, aidStatus, apiUrl: globalApiUrl });
2196
2627
  }
2197
2628
  catch (e) {
2198
2629
  sendJson(res, { error: e.message }, 500);
@@ -2208,7 +2639,7 @@ async function handleRequest(req, res) {
2208
2639
  if (parts.length >= 3) {
2209
2640
  const domain = parts.slice(1).join('.');
2210
2641
  if (domain && domain !== globalApiUrl) {
2211
- console.log(`[Select] 切换 AP 为 ${domain} (AID: ${aid})`);
2642
+ utils_1.logger.log(`[Select] 切换 AP 为 ${domain} (AID: ${aid})`);
2212
2643
  globalApiUrl = domain;
2213
2644
  agentCP = new agentcp_1.AgentCP(domain, '', globalDataDir || undefined);
2214
2645
  }
@@ -2218,17 +2649,14 @@ async function handleRequest(req, res) {
2218
2649
  }
2219
2650
  const loaded = await agentCP.loadAid(aid);
2220
2651
  if (loaded) {
2221
- const oldAid = currentAid;
2222
- currentAid = aid;
2223
- // 切换会话:保存旧 AID 的会话,加载新 AID 的会话
2224
- await getMessageStoreForAid(aid).loadSessionsForAid(aid);
2225
- activeSessionId = null;
2652
+ // 加载该 AID 的持久化会话
2653
+ await ensureMessageStoreLoaded(aid);
2226
2654
  // 切换身份时自动上线(含群组初始化),A/B 同时保持在线
2227
2655
  try {
2228
2656
  await ensureOnline(aid);
2229
2657
  }
2230
2658
  catch (e) {
2231
- console.warn(`[Select] AID ${aid} 自动上线失败:`, e.message);
2659
+ utils_1.logger.warn(`[Select] AID ${aid} 自动上线失败:`, e.message);
2232
2660
  }
2233
2661
  sendJson(res, { success: true, aid });
2234
2662
  }
@@ -2260,7 +2688,7 @@ async function handleRequest(req, res) {
2260
2688
  if (parts.length >= 3) {
2261
2689
  const domain = parts.slice(1).join('.');
2262
2690
  if (domain && domain !== globalApiUrl) {
2263
- console.log(`[Create] 切换 AP 为 ${domain} (AID: ${aid})`);
2691
+ utils_1.logger.log(`[Create] 切换 AP 为 ${domain} (AID: ${aid})`);
2264
2692
  globalApiUrl = domain;
2265
2693
  agentCP = new agentcp_1.AgentCP(domain, '', globalDataDir || undefined);
2266
2694
  }
@@ -2270,7 +2698,6 @@ async function handleRequest(req, res) {
2270
2698
  }
2271
2699
  try {
2272
2700
  const created = await agentCP.createAid(aid);
2273
- currentAid = created;
2274
2701
  // 保存自定义昵称和描述
2275
2702
  const nickname = (body.nickname || '').trim();
2276
2703
  const description = (body.description || '').trim();
@@ -2328,13 +2755,13 @@ async function handleRequest(req, res) {
2328
2755
  await instance.agentCP.leaveAllGroupSessions();
2329
2756
  }
2330
2757
  catch (e) {
2331
- console.warn(`[Group] leaveAllGroupSessions error:`, e.message);
2758
+ utils_1.logger.warn(`[Group] leaveAllGroupSessions error:`, e.message);
2332
2759
  }
2333
2760
  try {
2334
2761
  await instance.agentCP.closeGroupMessageStore();
2335
2762
  }
2336
2763
  catch (e) {
2337
- console.warn(`[Group] closeGroupMessageStore error:`, e.message);
2764
+ utils_1.logger.warn(`[Group] closeGroupMessageStore error:`, e.message);
2338
2765
  }
2339
2766
  }
2340
2767
  if (instance.heartbeatClient) {
@@ -2344,7 +2771,7 @@ async function handleRequest(req, res) {
2344
2771
  instance.agentWS.disconnect();
2345
2772
  }
2346
2773
  aidInstances.delete(aid);
2347
- console.log(`[Server] AID ${aid} 已下线`);
2774
+ utils_1.logger.log(`[Server] AID ${aid} 已下线`);
2348
2775
  // 下线后推送 AID 状态变更到前端
2349
2776
  getAidStatusList().then(aidStatus => {
2350
2777
  broadcastToBrowser({ type: 'aid_status', aidStatus });
@@ -2359,7 +2786,7 @@ async function handleRequest(req, res) {
2359
2786
  if (pathname === '/api/ws/start' && method === 'POST') {
2360
2787
  try {
2361
2788
  const body = await parseBody(req);
2362
- const targetAid = body.aid || currentAid;
2789
+ const targetAid = body.aid;
2363
2790
  if (!targetAid) {
2364
2791
  sendJson(res, { success: false, error: '请先选择 AID' });
2365
2792
  return;
@@ -2375,9 +2802,13 @@ async function handleRequest(req, res) {
2375
2802
  if (pathname === '/api/ws/connect' && method === 'POST') {
2376
2803
  try {
2377
2804
  const body = await parseBody(req);
2378
- const targetAid = body.targetAid;
2805
+ const { targetAid, aid } = body;
2806
+ if (!aid) {
2807
+ sendJson(res, { success: false, error: '缺少 aid' });
2808
+ return;
2809
+ }
2379
2810
  // 自动上线
2380
- const instance = await ensureOnline();
2811
+ const instance = await ensureOnline(aid);
2381
2812
  if (!instance.agentWS) {
2382
2813
  sendJson(res, { success: false, error: '自动上线失败' });
2383
2814
  return;
@@ -2397,12 +2828,11 @@ async function handleRequest(req, res) {
2397
2828
  identifyingCode: sessionInfo.identifyingCode
2398
2829
  });
2399
2830
  }, (status) => {
2400
- console.log('邀请状态:', status);
2831
+ utils_1.logger.log('邀请状态:', status);
2401
2832
  });
2402
2833
  });
2403
- // 创建 outgoing session 并设为活跃
2404
- getMessageStore().getOrCreateSession(result.sessionId, result.identifyingCode, targetAid, 'outgoing', currentAid);
2405
- activeSessionId = result.sessionId;
2834
+ // 创建 outgoing session
2835
+ getMessageStoreForAid(aid).getOrCreateSession(result.sessionId, result.identifyingCode, targetAid, 'outgoing', aid);
2406
2836
  sendJson(res, Object.assign({ success: true }, result));
2407
2837
  }
2408
2838
  catch (e) {
@@ -2413,24 +2843,26 @@ async function handleRequest(req, res) {
2413
2843
  if (pathname === '/api/ws/send' && method === 'POST') {
2414
2844
  try {
2415
2845
  const body = await parseBody(req);
2416
- const { message, sessionId } = body;
2846
+ const { message, sessionId, aid } = body;
2417
2847
  if (!message) {
2418
2848
  sendJson(res, { success: false, error: '消息不能为空' });
2419
2849
  return;
2420
2850
  }
2851
+ if (!aid) {
2852
+ sendJson(res, { success: false, error: '缺少 aid' });
2853
+ return;
2854
+ }
2421
2855
  // 自动上线
2422
- const instance = await ensureOnline();
2856
+ const instance = await ensureOnline(aid);
2423
2857
  if (!instance.agentWS) {
2424
2858
  sendJson(res, { success: false, error: '自动上线失败' });
2425
2859
  return;
2426
2860
  }
2427
- // 使用指定的 sessionId 或当前活跃会话
2428
- const sid = sessionId || activeSessionId;
2429
- if (!sid) {
2430
- sendJson(res, { success: false, error: '没有活跃会话' });
2861
+ if (!sessionId) {
2862
+ sendJson(res, { success: false, error: '缺少 sessionId' });
2431
2863
  return;
2432
2864
  }
2433
- const session = getMessageStore().getSession(sid);
2865
+ const session = getMessageStoreForAid(aid).getSession(sessionId);
2434
2866
  if (!session) {
2435
2867
  sendJson(res, { success: false, error: '会话不存在' });
2436
2868
  return;
@@ -2440,7 +2872,7 @@ async function handleRequest(req, res) {
2440
2872
  return;
2441
2873
  }
2442
2874
  instance.agentWS.send(message, session.peerAid, session.sessionId, session.identifyingCode);
2443
- getMessageStore().addMessageToSession(sid, {
2875
+ getMessageStoreForAid(aid).addMessageToSession(sessionId, {
2444
2876
  type: 'sent',
2445
2877
  content: message,
2446
2878
  to: session.peerAid,
@@ -2454,12 +2886,20 @@ async function handleRequest(req, res) {
2454
2886
  return;
2455
2887
  }
2456
2888
  if (pathname === '/api/messages' && method === 'GET') {
2457
- const session = activeSessionId ? getMessageStore().getSession(activeSessionId) : null;
2458
- sendJson(res, { messages: session ? session.messages : [], activeSessionId, closed: session ? (session.closed || false) : false });
2889
+ const aid = parsedUrl.query.aid;
2890
+ const sessionId = parsedUrl.query.sessionId;
2891
+ if (!aid || !sessionId) {
2892
+ sendJson(res, { messages: [], activeSessionId: null, closed: false });
2893
+ return;
2894
+ }
2895
+ const store = await ensureMessageStoreLoaded(aid);
2896
+ const session = store.getSession(sessionId);
2897
+ sendJson(res, { messages: session ? session.messages : [], activeSessionId: sessionId, closed: session ? (session.closed || false) : false });
2459
2898
  return;
2460
2899
  }
2461
2900
  if (pathname === '/api/ws/status' && method === 'GET') {
2462
- const instance = getActiveInstance();
2901
+ const aid = parsedUrl.query.aid;
2902
+ const instance = aid ? aidInstances.get(aid) : null;
2463
2903
  if (!instance || !instance.agentWS) {
2464
2904
  sendJson(res, { connected: false, status: 'disconnected' });
2465
2905
  }
@@ -2469,19 +2909,26 @@ async function handleRequest(req, res) {
2469
2909
  return;
2470
2910
  }
2471
2911
  if (pathname === '/api/sessions' && method === 'GET') {
2472
- sendJson(res, { sessions: getMessageStore().getSessionList(currentAid), activeSessionId });
2912
+ const aid = parsedUrl.query.aid;
2913
+ if (!aid) {
2914
+ sendJson(res, { sessions: [], activeSessionId: null });
2915
+ return;
2916
+ }
2917
+ const store = await ensureMessageStoreLoaded(aid);
2918
+ sendJson(res, { sessions: store.getSessionList(aid), activeSessionId: null });
2473
2919
  return;
2474
2920
  }
2475
2921
  if (pathname === '/api/sessions/active' && method === 'POST') {
2476
2922
  try {
2477
2923
  const body = await parseBody(req);
2478
- const { sessionId } = body;
2479
- if (!sessionId || !getMessageStore().hasSession(sessionId)) {
2924
+ const { sessionId, aid } = body;
2925
+ if (!aid || !sessionId || !getMessageStoreForAid(aid).hasSession(sessionId)) {
2480
2926
  sendJson(res, { success: false, error: '会话不存在' });
2481
2927
  return;
2482
2928
  }
2483
- activeSessionId = sessionId;
2484
- sendJson(res, { success: true, activeSessionId });
2929
+ // 通知绑定了该 aid 的客户端更新 activeSessionId
2930
+ pushToAid(aid, { type: 'set_active_session', sessionId });
2931
+ sendJson(res, { success: true, activeSessionId: sessionId });
2485
2932
  }
2486
2933
  catch (e) {
2487
2934
  sendJson(res, { success: false, error: e.message });
@@ -2491,15 +2938,17 @@ async function handleRequest(req, res) {
2491
2938
  if (pathname === '/api/sessions/delete' && method === 'POST') {
2492
2939
  try {
2493
2940
  const body = await parseBody(req);
2494
- const { sessionId } = body;
2941
+ const { sessionId, aid } = body;
2495
2942
  if (!sessionId) {
2496
2943
  sendJson(res, { success: false, error: '会话ID不能为空' });
2497
2944
  return;
2498
2945
  }
2499
- const deleted = await getMessageStore().deleteSession(sessionId);
2946
+ if (!aid) {
2947
+ sendJson(res, { success: false, error: '缺少 aid' });
2948
+ return;
2949
+ }
2950
+ const deleted = await getMessageStoreForAid(aid).deleteSession(sessionId);
2500
2951
  if (deleted) {
2501
- if (activeSessionId === sessionId)
2502
- activeSessionId = null;
2503
2952
  sendJson(res, { success: true });
2504
2953
  }
2505
2954
  else {
@@ -2514,18 +2963,16 @@ async function handleRequest(req, res) {
2514
2963
  if (pathname === '/api/peers/delete' && method === 'POST') {
2515
2964
  try {
2516
2965
  const body = await parseBody(req);
2517
- const { peerAid } = body;
2966
+ const { peerAid, aid } = body;
2518
2967
  if (!peerAid) {
2519
2968
  sendJson(res, { success: false, error: 'AID不能为空' });
2520
2969
  return;
2521
2970
  }
2522
- const count = await getMessageStore().deletePeer(peerAid, currentAid);
2523
- if (activeSessionId) {
2524
- const session = getMessageStore().getSession(activeSessionId);
2525
- if (!session || session.peerAid === peerAid) {
2526
- activeSessionId = null;
2527
- }
2971
+ if (!aid) {
2972
+ sendJson(res, { success: false, error: '缺少 aid' });
2973
+ return;
2528
2974
  }
2975
+ const count = await getMessageStoreForAid(aid).deletePeer(peerAid, aid);
2529
2976
  sendJson(res, { success: true, count });
2530
2977
  }
2531
2978
  catch (e) {
@@ -2538,7 +2985,13 @@ async function handleRequest(req, res) {
2538
2985
  // ============================================================
2539
2986
  if (pathname === '/api/group/init' && method === 'POST') {
2540
2987
  try {
2541
- const instance = await ensureOnline();
2988
+ const body = await parseBody(req);
2989
+ const aid = body.aid;
2990
+ if (!aid) {
2991
+ sendJson(res, { success: false, error: '缺少 aid' });
2992
+ return;
2993
+ }
2994
+ const instance = await ensureOnline(aid);
2542
2995
  await ensureGroupClient(instance);
2543
2996
  sendJson(res, { success: true, targetAid: instance.groupTargetAid });
2544
2997
  }
@@ -2550,12 +3003,16 @@ async function handleRequest(req, res) {
2550
3003
  if (pathname === '/api/group/create' && method === 'POST') {
2551
3004
  try {
2552
3005
  const body = await parseBody(req);
2553
- const { name, visibility, description } = body;
3006
+ const { name, visibility, description, duty_mode, aid } = body;
3007
+ if (!aid) {
3008
+ sendJson(res, { success: false, error: '缺少 aid' });
3009
+ return;
3010
+ }
2554
3011
  if (!name) {
2555
3012
  sendJson(res, { success: false, error: '群组名称不能为空' });
2556
3013
  return;
2557
3014
  }
2558
- const instance = await ensureOnline();
3015
+ const instance = await ensureOnline(aid);
2559
3016
  await ensureGroupClient(instance);
2560
3017
  const ops = instance.agentCP.groupOps;
2561
3018
  const target = instance.groupTargetAid;
@@ -2565,9 +3022,21 @@ async function handleRequest(req, res) {
2565
3022
  if (description)
2566
3023
  options.description = description;
2567
3024
  const result = await ops.createGroup(target, name, options);
2568
- console.log('[ACP] createGroup 返回:', JSON.stringify(result, null, 2));
3025
+ utils_1.logger.log('[ACP] createGroup 返回:', JSON.stringify(result, null, 2));
3026
+ // 设置值班规则
3027
+ if (duty_mode && result.group_id) {
3028
+ try {
3029
+ await ops.updateDutyConfig(target, result.group_id, { mode: duty_mode });
3030
+ utils_1.logger.log('[ACP] 值班规则已设置:', duty_mode);
3031
+ }
3032
+ catch (e) {
3033
+ utils_1.logger.warn('[ACP] 设置值班规则失败:', e.message);
3034
+ }
3035
+ }
2569
3036
  // 加入本地存储
2570
3037
  instance.agentCP.addGroupToStore(result.group_id, name);
3038
+ // 注册在线,才能收到实时消息推送
3039
+ await instance.agentCP.joinGroupSession(result.group_id);
2571
3040
  sendJson(res, Object.assign({ success: true }, result));
2572
3041
  }
2573
3042
  catch (e) {
@@ -2577,7 +3046,12 @@ async function handleRequest(req, res) {
2577
3046
  }
2578
3047
  if (pathname === '/api/group/list' && method === 'GET') {
2579
3048
  try {
2580
- const instance = await ensureOnline();
3049
+ const aid = parsedUrl.query.aid;
3050
+ if (!aid) {
3051
+ sendJson(res, { success: false, error: '缺少 aid', groups: [] });
3052
+ return;
3053
+ }
3054
+ const instance = await ensureOnline(aid);
2581
3055
  await ensureGroupClient(instance);
2582
3056
  // 首次访问时从服务端同步群组列表
2583
3057
  if (!instance.groupListSynced) {
@@ -2586,7 +3060,7 @@ async function handleRequest(req, res) {
2586
3060
  instance.groupListSynced = true;
2587
3061
  }
2588
3062
  catch (syncErr) {
2589
- console.warn('[Group] syncGroupList error:', syncErr.message);
3063
+ utils_1.logger.warn('[Group] syncGroupList error:', syncErr.message);
2590
3064
  }
2591
3065
  }
2592
3066
  const groups = instance.agentCP.getLocalGroupList();
@@ -2600,8 +3074,12 @@ async function handleRequest(req, res) {
2600
3074
  if (pathname === '/api/group/select' && method === 'POST') {
2601
3075
  try {
2602
3076
  const body = await parseBody(req);
2603
- const { groupId } = body;
2604
- const instance = await ensureOnline();
3077
+ const { groupId, aid } = body;
3078
+ if (!aid) {
3079
+ sendJson(res, { success: false, error: '缺少 aid' });
3080
+ return;
3081
+ }
3082
+ const instance = await ensureOnline(aid);
2605
3083
  instance.activeGroupId = groupId || null;
2606
3084
  sendJson(res, { success: true });
2607
3085
  }
@@ -2613,11 +3091,16 @@ async function handleRequest(req, res) {
2613
3091
  if (pathname === '/api/group/info' && method === 'GET') {
2614
3092
  try {
2615
3093
  const groupId = parsedUrl.query.groupId;
3094
+ const aid = parsedUrl.query.aid;
2616
3095
  if (!groupId) {
2617
3096
  sendJson(res, { success: false, error: '缺少 groupId' });
2618
3097
  return;
2619
3098
  }
2620
- const instance = await ensureOnline();
3099
+ if (!aid) {
3100
+ sendJson(res, { success: false, error: '缺少 aid' });
3101
+ return;
3102
+ }
3103
+ const instance = await ensureOnline(aid);
2621
3104
  await ensureGroupClient(instance);
2622
3105
  const info = await instance.agentCP.groupOps.getGroupInfo(instance.groupTargetAid, groupId);
2623
3106
  sendJson(res, Object.assign({ success: true }, info));
@@ -2630,12 +3113,16 @@ async function handleRequest(req, res) {
2630
3113
  if (pathname === '/api/group/send' && method === 'POST') {
2631
3114
  try {
2632
3115
  const body = await parseBody(req);
2633
- const { groupId, message } = body;
3116
+ const { groupId, message, aid } = body;
2634
3117
  if (!groupId || !message) {
2635
3118
  sendJson(res, { success: false, error: '缺少 groupId 或 message' });
2636
3119
  return;
2637
3120
  }
2638
- const instance = await ensureOnline();
3121
+ if (!aid) {
3122
+ sendJson(res, { success: false, error: '缺少 aid' });
3123
+ return;
3124
+ }
3125
+ const instance = await ensureOnline(aid);
2639
3126
  await ensureGroupClient(instance);
2640
3127
  const result = await instance.agentCP.groupOps.sendGroupMessage(instance.groupTargetAid, groupId, message, 'text');
2641
3128
  // 添加到本地存储
@@ -2656,7 +3143,12 @@ async function handleRequest(req, res) {
2656
3143
  if (pathname === '/api/group/messages' && method === 'GET') {
2657
3144
  try {
2658
3145
  const groupId = parsedUrl.query.groupId || '';
2659
- const instance = await ensureOnline();
3146
+ const aid = parsedUrl.query.aid;
3147
+ if (!aid) {
3148
+ sendJson(res, { success: false, error: '缺少 aid', messages: [] });
3149
+ return;
3150
+ }
3151
+ const instance = await ensureOnline(aid);
2660
3152
  if (!groupId) {
2661
3153
  sendJson(res, { success: true, messages: [] });
2662
3154
  return;
@@ -2665,6 +3157,7 @@ async function handleRequest(req, res) {
2665
3157
  // 只读本地缓存,不再每次请求都去服务端拉取
2666
3158
  // 新消息通过 WebSocket 推送实时到达并由 SDK 自动存储
2667
3159
  const messages = instance.agentCP.getLocalGroupMessages(groupId);
3160
+ utils_1.logger.log(`[API] /api/group/messages: aid=${aid} group=${groupId} localMsgCount=${messages.length} lastMsgId=${messages.length > 0 ? messages[messages.length - 1].msg_id : 'none'} storeExists=${!!instance.agentCP.groupMessageStore}`);
2668
3161
  sendJson(res, { success: true, messages });
2669
3162
  }
2670
3163
  catch (e) {
@@ -2675,12 +3168,16 @@ async function handleRequest(req, res) {
2675
3168
  if (pathname === '/api/group/invite-code' && method === 'POST') {
2676
3169
  try {
2677
3170
  const body = await parseBody(req);
2678
- const { groupId } = body;
3171
+ const { groupId, aid } = body;
2679
3172
  if (!groupId) {
2680
3173
  sendJson(res, { success: false, error: '缺少 groupId' });
2681
3174
  return;
2682
3175
  }
2683
- const instance = await ensureOnline();
3176
+ if (!aid) {
3177
+ sendJson(res, { success: false, error: '缺少 aid' });
3178
+ return;
3179
+ }
3180
+ const instance = await ensureOnline(aid);
2684
3181
  await ensureGroupClient(instance);
2685
3182
  const result = await instance.agentCP.groupOps.createInviteCode(instance.groupTargetAid, groupId, body.options);
2686
3183
  const groupUrl = `https://${instance.groupTargetAid}/${groupId}`;
@@ -2694,17 +3191,20 @@ async function handleRequest(req, res) {
2694
3191
  if (pathname === '/api/group/join' && method === 'POST') {
2695
3192
  try {
2696
3193
  const body = await parseBody(req);
2697
- const { groupUrl, code } = body;
3194
+ const { groupUrl, code, aid } = body;
2698
3195
  if (!groupUrl) {
2699
3196
  sendJson(res, { success: false, error: '缺少群聊链接' });
2700
3197
  return;
2701
3198
  }
3199
+ if (!aid) {
3200
+ sendJson(res, { success: false, error: '缺少 aid' });
3201
+ return;
3202
+ }
2702
3203
  const { targetAid, groupId } = group_1.GroupOperations.parseGroupUrl(groupUrl);
2703
- const instance = await ensureOnline();
3204
+ const instance = await ensureOnline(aid);
2704
3205
  await ensureGroupClient(instance);
2705
- if (code) {
2706
- // 免审核:邀请码加入
2707
- await instance.agentCP.groupOps.useInviteCode(targetAid, groupId, code);
3206
+ // 加入成功后的统一处理:获取群名、写入本地存储、注册在线会话
3207
+ const finalizeJoin = async () => {
2708
3208
  let groupName = groupId;
2709
3209
  try {
2710
3210
  const info = await instance.agentCP.groupOps.getGroupInfo(targetAid, groupId);
@@ -2712,12 +3212,26 @@ async function handleRequest(req, res) {
2712
3212
  }
2713
3213
  catch (_) { }
2714
3214
  instance.agentCP.addGroupToStore(groupId, groupName);
3215
+ await instance.agentCP.joinGroupSession(groupId);
3216
+ };
3217
+ if (code) {
3218
+ // 免审核:邀请码加入
3219
+ await instance.agentCP.groupOps.useInviteCode(targetAid, groupId, code);
3220
+ await finalizeJoin();
2715
3221
  sendJson(res, { success: true, group_id: groupId });
2716
3222
  }
2717
3223
  else {
2718
- // 审核模式:发送入群申请
2719
- const requestId = await instance.agentCP.groupOps.requestJoin(targetAid, groupId, body.message || '');
2720
- sendJson(res, { success: true, pending: true, request_id: requestId });
3224
+ // 申请加入:公开群直接加入,私密群等待审核
3225
+ const result = await instance.agentCP.groupOps.requestJoin(targetAid, groupId, body.message || '');
3226
+ if (result.status === 'joined') {
3227
+ // 公开群:直接加入成功
3228
+ await finalizeJoin();
3229
+ sendJson(res, { success: true, group_id: groupId });
3230
+ }
3231
+ else {
3232
+ // 私密群:等待管理员审核
3233
+ sendJson(res, { success: true, pending: true, request_id: result.request_id });
3234
+ }
2721
3235
  }
2722
3236
  }
2723
3237
  catch (e) {
@@ -2728,11 +3242,16 @@ async function handleRequest(req, res) {
2728
3242
  if (pathname === '/api/group/pending-requests' && method === 'GET') {
2729
3243
  try {
2730
3244
  const groupId = parsedUrl.query.groupId;
3245
+ const aid = parsedUrl.query.aid;
2731
3246
  if (!groupId) {
2732
3247
  sendJson(res, { success: false, error: '缺少 groupId' });
2733
3248
  return;
2734
3249
  }
2735
- const instance = await ensureOnline();
3250
+ if (!aid) {
3251
+ sendJson(res, { success: false, error: '缺少 aid' });
3252
+ return;
3253
+ }
3254
+ const instance = await ensureOnline(aid);
2736
3255
  await ensureGroupClient(instance);
2737
3256
  const result = await instance.agentCP.groupOps.getPendingRequests(instance.groupTargetAid, groupId);
2738
3257
  sendJson(res, Object.assign({ success: true }, result));
@@ -2745,12 +3264,16 @@ async function handleRequest(req, res) {
2745
3264
  if (pathname === '/api/group/review-join' && method === 'POST') {
2746
3265
  try {
2747
3266
  const body = await parseBody(req);
2748
- const { groupId, agentId, action } = body;
3267
+ const { groupId, agentId, action, aid } = body;
2749
3268
  if (!groupId || !agentId || !action) {
2750
3269
  sendJson(res, { success: false, error: '缺少参数' });
2751
3270
  return;
2752
3271
  }
2753
- const instance = await ensureOnline();
3272
+ if (!aid) {
3273
+ sendJson(res, { success: false, error: '缺少 aid' });
3274
+ return;
3275
+ }
3276
+ const instance = await ensureOnline(aid);
2754
3277
  await ensureGroupClient(instance);
2755
3278
  await instance.agentCP.groupOps.reviewJoinRequest(instance.groupTargetAid, groupId, agentId, action, body.reason || '');
2756
3279
  sendJson(res, { success: true });
@@ -2763,11 +3286,16 @@ async function handleRequest(req, res) {
2763
3286
  if (pathname === '/api/group/members' && method === 'GET') {
2764
3287
  try {
2765
3288
  const groupId = parsedUrl.query.groupId;
3289
+ const aid = parsedUrl.query.aid;
2766
3290
  if (!groupId) {
2767
3291
  sendJson(res, { success: false, error: '缺少 groupId' });
2768
3292
  return;
2769
3293
  }
2770
- const instance = await ensureOnline();
3294
+ if (!aid) {
3295
+ sendJson(res, { success: false, error: '缺少 aid' });
3296
+ return;
3297
+ }
3298
+ const instance = await ensureOnline(aid);
2771
3299
  await ensureGroupClient(instance);
2772
3300
  const result = await instance.agentCP.groupOps.getMembers(instance.groupTargetAid, groupId);
2773
3301
  sendJson(res, Object.assign({ success: true }, result));
@@ -2779,7 +3307,12 @@ async function handleRequest(req, res) {
2779
3307
  }
2780
3308
  if (pathname === '/api/group/my-groups' && method === 'GET') {
2781
3309
  try {
2782
- const instance = await ensureOnline();
3310
+ const aid = parsedUrl.query.aid;
3311
+ if (!aid) {
3312
+ sendJson(res, { success: false, error: '缺少 aid', groups: [] });
3313
+ return;
3314
+ }
3315
+ const instance = await ensureOnline(aid);
2783
3316
  await ensureGroupClient(instance);
2784
3317
  const ops = instance.agentCP.groupOps;
2785
3318
  const target = instance.groupTargetAid;
@@ -2805,12 +3338,16 @@ async function handleRequest(req, res) {
2805
3338
  if (pathname === '/api/group/leave' && method === 'POST') {
2806
3339
  try {
2807
3340
  const body = await parseBody(req);
2808
- const { groupId } = body;
3341
+ const { groupId, aid } = body;
2809
3342
  if (!groupId) {
2810
3343
  sendJson(res, { success: false, error: '缺少 groupId' });
2811
3344
  return;
2812
3345
  }
2813
- const instance = await ensureOnline();
3346
+ if (!aid) {
3347
+ sendJson(res, { success: false, error: '缺少 aid' });
3348
+ return;
3349
+ }
3350
+ const instance = await ensureOnline(aid);
2814
3351
  await ensureGroupClient(instance);
2815
3352
  await instance.agentCP.groupOps.leaveGroup(instance.groupTargetAid, groupId);
2816
3353
  await instance.agentCP.removeGroupFromStore(groupId);
@@ -2823,6 +3360,59 @@ async function handleRequest(req, res) {
2823
3360
  }
2824
3361
  return;
2825
3362
  }
3363
+ if (pathname === '/api/group/duty-status' && method === 'GET') {
3364
+ try {
3365
+ const aid = parsedUrl.query.aid;
3366
+ const groupId = parsedUrl.query.groupId;
3367
+ if (!aid) {
3368
+ sendJson(res, { success: false, error: '缺少 aid' });
3369
+ return;
3370
+ }
3371
+ if (!groupId) {
3372
+ sendJson(res, { success: false, error: '缺少 groupId' });
3373
+ return;
3374
+ }
3375
+ const instance = await ensureOnline(aid);
3376
+ await ensureGroupClient(instance);
3377
+ const ops = instance.agentCP.groupOps;
3378
+ const target = instance.groupTargetAid;
3379
+ const result = await ops.getDutyStatus(target, groupId);
3380
+ sendJson(res, { success: true, config: result.config, state: result.state });
3381
+ }
3382
+ catch (e) {
3383
+ sendJson(res, { success: false, error: e.message });
3384
+ }
3385
+ return;
3386
+ }
3387
+ if (pathname === '/api/group/update-duty-config' && method === 'POST') {
3388
+ try {
3389
+ const body = await parseBody(req);
3390
+ const { groupId, aid, mode } = body;
3391
+ if (!aid) {
3392
+ sendJson(res, { success: false, error: '缺少 aid' });
3393
+ return;
3394
+ }
3395
+ if (!groupId) {
3396
+ sendJson(res, { success: false, error: '缺少 groupId' });
3397
+ return;
3398
+ }
3399
+ if (!mode) {
3400
+ sendJson(res, { success: false, error: '缺少 mode' });
3401
+ return;
3402
+ }
3403
+ const instance = await ensureOnline(aid);
3404
+ await ensureGroupClient(instance);
3405
+ const ops = instance.agentCP.groupOps;
3406
+ const target = instance.groupTargetAid;
3407
+ await ops.updateDutyConfig(target, groupId, { mode });
3408
+ utils_1.logger.log('[ACP] 值班规则已更新:', mode, 'groupId:', groupId);
3409
+ sendJson(res, { success: true });
3410
+ }
3411
+ catch (e) {
3412
+ sendJson(res, { success: false, error: e.message });
3413
+ }
3414
+ return;
3415
+ }
2826
3416
  // 404
2827
3417
  res.writeHead(404);
2828
3418
  res.end('Not Found');
@@ -2856,34 +3446,70 @@ function startServer(port, apiUrl, dataDir = '') {
2856
3446
  if (parts.length >= 3) {
2857
3447
  const domain = parts.slice(1).join('.');
2858
3448
  if (domain !== globalApiUrl) {
2859
- console.log(`[Server] 检测到 AID 所属 AP 为 ${domain},正在切换...`);
3449
+ utils_1.logger.log(`[Server] 检测到 AID 所属 AP 为 ${domain},正在切换...`);
2860
3450
  globalApiUrl = domain;
2861
3451
  agentCP = new agentcp_1.AgentCP(domain, '', dataDir || undefined, { persistMessages: true, persistGroupMessages: true });
2862
3452
  await agentCP.loadCurrentAid();
2863
3453
  }
2864
3454
  }
2865
- currentAid = aid;
2866
- console.log(`已加载 AID: ${aid}`);
3455
+ utils_1.logger.log(`已加载 AID: ${aid}`);
2867
3456
  // 加载该 AID 的持久化会话
2868
- await getMessageStoreForAid(aid).loadSessionsForAid(aid);
2869
- console.log(`已加载会话`);
3457
+ await ensureMessageStoreLoaded(aid);
3458
+ utils_1.logger.log(`已加载会话`);
2870
3459
  }
2871
3460
  }).catch(() => { });
2872
3461
  const server = http.createServer(handleRequest);
2873
3462
  // WebSocket server for browser ↔ server real-time communication
2874
3463
  const wss = new ws_1.default.Server({ noServer: true });
3464
+ // Periodically terminate dead connections (no pong within 30s)
3465
+ const wsAliveMap = new WeakMap();
3466
+ const wssHeartbeat = setInterval(() => {
3467
+ for (const [ws] of browserWsClients) {
3468
+ if (wsAliveMap.get(ws) === false) {
3469
+ ws.terminate();
3470
+ browserWsClients.delete(ws);
3471
+ continue;
3472
+ }
3473
+ wsAliveMap.set(ws, false);
3474
+ ws.ping();
3475
+ }
3476
+ }, 30000);
2875
3477
  server.on('upgrade', (req, socket, head) => {
2876
3478
  const pathname = url.parse(req.url || '', true).pathname;
2877
- if (pathname === '/ws/group') {
3479
+ if (pathname === '/ws/ui' || pathname === '/ws/group') {
2878
3480
  wss.handleUpgrade(req, socket, head, (ws) => {
2879
- browserWsClients.add(ws);
2880
- console.log(`[WS] browser client connected, total=${browserWsClients.size}`);
3481
+ const client = { ws, aid: '', activeSessionId: null };
3482
+ browserWsClients.set(ws, client);
3483
+ wsAliveMap.set(ws, true);
3484
+ ws.on('pong', () => wsAliveMap.set(ws, true));
3485
+ utils_1.logger.log(`[WS] browser client connected, total=${browserWsClients.size}`);
3486
+ ws.on('message', (raw) => {
3487
+ try {
3488
+ const msg = JSON.parse(raw.toString());
3489
+ if (msg.type === 'ping') {
3490
+ if (ws.readyState === ws_1.default.OPEN)
3491
+ ws.send(JSON.stringify({ type: 'pong' }));
3492
+ }
3493
+ else if (msg.type === 'bind_aid') {
3494
+ client.aid = msg.aid || '';
3495
+ // 推送当前该 aid 的 ws 状态
3496
+ const instance = aidInstances.get(client.aid);
3497
+ if (instance) {
3498
+ ws.send(JSON.stringify({ type: 'ws_status', aid: client.aid, status: instance.wsStatus }));
3499
+ }
3500
+ }
3501
+ else if (msg.type === 'set_active_session') {
3502
+ client.activeSessionId = msg.sessionId || null;
3503
+ }
3504
+ }
3505
+ catch (_a) { }
3506
+ });
2881
3507
  ws.on('close', () => {
2882
3508
  browserWsClients.delete(ws);
2883
- console.log(`[WS] browser client disconnected, total=${browserWsClients.size}`);
3509
+ utils_1.logger.log(`[WS] browser client disconnected, total=${browserWsClients.size}`);
2884
3510
  });
2885
3511
  ws.on('error', (err) => {
2886
- console.error('[WS] browser client error:', err.message);
3512
+ utils_1.logger.error('[WS] browser client error:', err.message);
2887
3513
  browserWsClients.delete(ws);
2888
3514
  });
2889
3515
  });
@@ -2894,7 +3520,8 @@ function startServer(port, apiUrl, dataDir = '') {
2894
3520
  });
2895
3521
  // 资源清理函数
2896
3522
  const cleanup = async () => {
2897
- console.log('\n正在关闭服务...');
3523
+ utils_1.logger.log('\n正在关闭服务...');
3524
+ clearInterval(wssHeartbeat);
2898
3525
  // 持久化 agent info 缓存
2899
3526
  saveAgentInfoCacheToDisk();
2900
3527
  // 持久化当前会话
@@ -2902,25 +3529,25 @@ function startServer(port, apiUrl, dataDir = '') {
2902
3529
  for (const [aid, store] of messageStores) {
2903
3530
  await store.flushAll();
2904
3531
  }
2905
- console.log('[Server] 会话已保存');
3532
+ utils_1.logger.log('[Server] 会话已保存');
2906
3533
  }
2907
3534
  catch (e) {
2908
- console.error('[Server] 保存会话失败:', e);
3535
+ utils_1.logger.error('[Server] 保存会话失败:', e);
2909
3536
  }
2910
3537
  for (const [aid, instance] of aidInstances) {
2911
- console.log(`[Server] 清理 AID: ${aid}`);
3538
+ utils_1.logger.log(`[Server] 清理 AID: ${aid}`);
2912
3539
  if (instance.groupInitialized) {
2913
3540
  try {
2914
3541
  await instance.agentCP.leaveAllGroupSessions();
2915
3542
  }
2916
3543
  catch (e) {
2917
- console.warn(`[Server] leaveAllGroupSessions error:`, e.message);
3544
+ utils_1.logger.warn(`[Server] leaveAllGroupSessions error:`, e.message);
2918
3545
  }
2919
3546
  try {
2920
3547
  await instance.agentCP.closeGroupMessageStore();
2921
3548
  }
2922
3549
  catch (e) {
2923
- console.warn(`[Server] closeGroupMessageStore error:`, e.message);
3550
+ utils_1.logger.warn(`[Server] closeGroupMessageStore error:`, e.message);
2924
3551
  }
2925
3552
  }
2926
3553
  if (instance.heartbeatClient) {
@@ -2932,13 +3559,13 @@ function startServer(port, apiUrl, dataDir = '') {
2932
3559
  }
2933
3560
  aidInstances.clear();
2934
3561
  // 关闭所有浏览器 WS 连接
2935
- for (const client of browserWsClients) {
2936
- client.close();
3562
+ for (const [ws] of browserWsClients) {
3563
+ ws.close();
2937
3564
  }
2938
3565
  browserWsClients.clear();
2939
3566
  wss.close();
2940
3567
  server.close(() => {
2941
- console.log('服务已关闭');
3568
+ utils_1.logger.log('服务已关闭');
2942
3569
  process.exit(0);
2943
3570
  });
2944
3571
  };
@@ -2948,20 +3575,20 @@ function startServer(port, apiUrl, dataDir = '') {
2948
3575
  // 处理端口占用错误
2949
3576
  server.on('error', (err) => {
2950
3577
  if (err.code === 'EADDRINUSE') {
2951
- console.error(`\n 错误: 端口 ${port} 已被占用`);
2952
- console.error(` 请使用 -p 参数指定其他端口,或关闭占用该端口的程序\n`);
3578
+ utils_1.logger.error(`\n 错误: 端口 ${port} 已被占用`);
3579
+ utils_1.logger.error(` 请使用 -p 参数指定其他端口,或关闭占用该端口的程序\n`);
2953
3580
  process.exit(1);
2954
3581
  }
2955
3582
  throw err;
2956
3583
  });
2957
3584
  server.listen(port, () => {
2958
- console.log(`\n ACP 身份管理服务已启动`);
2959
- console.log(` ─────────────────────────`);
2960
- console.log(` 本地地址: http://localhost:${port}`);
2961
- console.log(` API 服务: ${apiUrl}`);
3585
+ utils_1.logger.log(`\n ACP 身份管理服务已启动`);
3586
+ utils_1.logger.log(` ─────────────────────────`);
3587
+ utils_1.logger.log(` 本地地址: http://localhost:${port}`);
3588
+ utils_1.logger.log(` API 服务: ${apiUrl}`);
2962
3589
  if (dataDir) {
2963
- console.log(` 数据目录: ${dataDir}`);
3590
+ utils_1.logger.log(` 数据目录: ${dataDir}`);
2964
3591
  }
2965
- console.log(`\n 按 Ctrl+C 停止服务\n`);
3592
+ utils_1.logger.log(`\n 按 Ctrl+C 停止服务\n`);
2966
3593
  });
2967
3594
  }