acp-ts 1.2.3 → 1.2.5

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,6 +50,7 @@ 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();
@@ -70,11 +71,19 @@ function pushToAid(aid, data) {
70
71
  */
71
72
  function broadcastToBrowser(data) {
72
73
  const payload = JSON.stringify(data);
73
- for (const [ws] of browserWsClients) {
74
+ let sentCount = 0;
75
+ for (const [ws, client] of browserWsClients) {
74
76
  if (ws.readyState === ws_1.default.OPEN) {
75
77
  ws.send(payload);
78
+ sentCount++;
79
+ }
80
+ else {
81
+ utils_1.logger.warn(`[broadcastToBrowser] skip ws client (readyState=${ws.readyState}, aid=${client.aid})`);
76
82
  }
77
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
+ }
78
87
  }
79
88
  let agentCP = null;
80
89
  const MAX_AIDS = 10;
@@ -115,7 +124,7 @@ async function doEnsureOnline(aid) {
115
124
  const customOpts = getAidMdOptionsForAid(aid);
116
125
  cp.setAgentMdOptions(Object.assign({ type: 'human', tags: ['human', 'acp'] }, customOpts));
117
126
  const connConfig = await cp.online();
118
- console.log(`[Server] 自动上线 AID: ${aid}`);
127
+ utils_1.logger.log(`[Server] 自动上线 AID: ${aid}`);
119
128
  const hb = new heartbeat_1.HeartbeatClient(aid, connConfig.heartbeatServer, '');
120
129
  const ws = new agentws_1.AgentWS(aid, connConfig.messageServer, connConfig.messageSignature);
121
130
  const instance = {
@@ -135,7 +144,7 @@ async function doEnsureOnline(aid) {
135
144
  };
136
145
  aidInstances.set(aid, instance);
137
146
  hb.onInvite((invite) => {
138
- console.log(`[Server] 收到邀请: ${JSON.stringify(invite)}`);
147
+ utils_1.logger.log(`[Server] 收到邀请: ${JSON.stringify(invite)}`);
139
148
  const session = getMessageStoreForAid(aid).getOrCreateSession(invite.sessionId, invite.inviteCode, invite.inviterAgentId, 'incoming', aid);
140
149
  pushToAid(aid, { type: 'sessions_updated' });
141
150
  if (instance.agentWS) {
@@ -145,28 +154,55 @@ async function doEnsureOnline(aid) {
145
154
  // 心跳重连成功后,自动触发 WebSocket 重连 + 群组重新注册
146
155
  hb.onReconnect(() => {
147
156
  if (instance.agentWS) {
148
- console.log('[Server] 心跳重连成功,触发 WebSocket 重连...');
157
+ utils_1.logger.log('[Server] 心跳重连成功,触发 WebSocket 重连...');
149
158
  instance.agentWS.reconnect().then(async () => {
150
159
  // WebSocket 重连成功后,重新注册所有在线群组
151
160
  // 断线期间 group.ap 会将在线状态过期,必须重新 register_online 才能收到推送
152
161
  const onlineGroups = instance.agentCP.getOnlineGroups();
153
162
  if (onlineGroups.length > 0) {
154
- console.log(`[Server] WebSocket 重连成功,重新注册 ${onlineGroups.length} 个在线群组...`);
163
+ utils_1.logger.log(`[Server] WebSocket 重连成功,重新注册 ${onlineGroups.length} 个在线群组...`);
155
164
  for (const groupId of onlineGroups) {
156
165
  try {
157
166
  await instance.agentCP.joinGroupSession(groupId);
158
- console.log(`[Server] 群组重新注册成功: ${groupId}`);
167
+ utils_1.logger.log(`[Server] 群组重新注册成功: ${groupId}`);
159
168
  }
160
169
  catch (e) {
161
- console.warn(`[Server] 群组重新注册失败: ${groupId}`, e.message || e);
170
+ utils_1.logger.warn(`[Server] 群组重新注册失败: ${groupId}`, e.message || e);
162
171
  }
163
172
  }
164
173
  }
165
174
  }).catch((err) => {
166
- console.error('[Server] WebSocket 重连失败:', err);
175
+ utils_1.logger.error('[Server] WebSocket 重连失败:', err);
167
176
  });
168
177
  }
169
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
+ });
170
206
  await hb.online();
171
207
  ws.onMessage((message) => {
172
208
  var _a;
@@ -194,7 +230,7 @@ async function doEnsureOnline(aid) {
194
230
  try {
195
231
  const parsed = JSON.parse(msgContent);
196
232
  if (Array.isArray(parsed) && parsed.length > 0) {
197
- content = parsed.map((item) => item.content || '').join('');
233
+ content = parsed.map((item) => (item && item.content) || '').join('');
198
234
  }
199
235
  else if (parsed.content) {
200
236
  content = parsed.content;
@@ -240,10 +276,10 @@ async function doEnsureOnline(aid) {
240
276
  // AID 上线后自动初始化群组功能,确保所有身份都能收到群消息推送
241
277
  try {
242
278
  await ensureGroupClient(instance);
243
- console.log(`[Server] AID ${aid} 群组功能自动初始化完成`);
279
+ utils_1.logger.log(`[Server] AID ${aid} 群组功能自动初始化完成`);
244
280
  }
245
281
  catch (e) {
246
- console.warn(`[Server] AID ${aid} 群组功能自动初始化失败(不影响上线):`, e.message);
282
+ utils_1.logger.warn(`[Server] AID ${aid} 群组功能自动初始化失败(不影响上线):`, e.message);
247
283
  }
248
284
  return instance;
249
285
  }
@@ -283,7 +319,7 @@ async function ensureGroupClient(instance) {
283
319
  // 注册群组事件处理器,确保 SDK 通知回调可靠触发
284
320
  instance.agentCP.setGroupEventHandler({
285
321
  onNewMessage(groupId, latestMsgId, sender, preview) {
286
- 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}`);
287
323
  // 通知浏览器有新消息(轻量通知,前端可据此决定是否刷新)
288
324
  broadcastToBrowser({
289
325
  type: 'new_message_notify',
@@ -294,7 +330,7 @@ async function ensureGroupClient(instance) {
294
330
  });
295
331
  },
296
332
  onNewEvent(groupId, latestEventId, eventType, summary) {
297
- 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}`);
298
334
  broadcastToBrowser({
299
335
  type: 'new_event',
300
336
  group_id: groupId,
@@ -304,7 +340,7 @@ async function ensureGroupClient(instance) {
304
340
  });
305
341
  },
306
342
  onGroupInvite(groupId, groupAddress, invitedBy) {
307
- console.log(`[Group] onGroupInvite: group=${groupId} address=${groupAddress} invitedBy=${invitedBy}`);
343
+ utils_1.logger.log(`[Group] onGroupInvite: group=${groupId} address=${groupAddress} invitedBy=${invitedBy}`);
308
344
  broadcastToBrowser({
309
345
  type: 'group_invite',
310
346
  group_id: groupId,
@@ -313,18 +349,18 @@ async function ensureGroupClient(instance) {
313
349
  });
314
350
  },
315
351
  onJoinApproved(groupId, groupAddress) {
316
- console.log(`[Group] onJoinApproved: group=${groupId} address=${groupAddress}`);
352
+ utils_1.logger.log(`[Group] onJoinApproved: group=${groupId} address=${groupAddress}`);
317
353
  // 审核通过:获取群信息、添加本地存储、注册到 Home AP
318
354
  (async () => {
319
355
  try {
320
356
  if (!instance.agentCP.groupOps) {
321
- console.warn(`[Group] onJoinApproved skipped: groupOps not available`);
357
+ utils_1.logger.warn(`[Group] onJoinApproved skipped: groupOps not available`);
322
358
  return;
323
359
  }
324
360
  let groupName = groupId;
325
361
  try {
326
362
  const info = await instance.agentCP.groupOps.getGroupInfo(instance.groupTargetAid, groupId);
327
- groupName = info.name || groupId;
363
+ groupName = (info && info.name) || groupId;
328
364
  }
329
365
  catch (_) { }
330
366
  instance.agentCP.addGroupToStore(groupId, groupName);
@@ -332,7 +368,7 @@ async function ensureGroupClient(instance) {
332
368
  await instance.agentCP.joinGroupSession(groupId);
333
369
  }
334
370
  catch (e) {
335
- console.error(`[Group] onJoinApproved processing failed: group=${groupId}`, e.message);
371
+ utils_1.logger.error(`[Group] onJoinApproved processing failed: group=${groupId}`, e.message);
336
372
  }
337
373
  })();
338
374
  broadcastToBrowser({
@@ -342,46 +378,49 @@ async function ensureGroupClient(instance) {
342
378
  });
343
379
  },
344
380
  onJoinRejected(groupId, reason) {
345
- console.log(`[Group] onJoinRejected: group=${groupId} reason=${reason}`);
381
+ utils_1.logger.log(`[Group] onJoinRejected: group=${groupId} reason=${reason}`);
346
382
  broadcastToBrowser({ type: 'join_rejected', group_id: groupId, reason });
347
383
  },
348
384
  onJoinRequestReceived(groupId, agentId, message) {
349
- console.log(`[Group] onJoinRequestReceived: group=${groupId} agent=${agentId} msg=${message}`);
385
+ utils_1.logger.log(`[Group] onJoinRequestReceived: group=${groupId} agent=${agentId} msg=${message}`);
350
386
  broadcastToBrowser({ type: 'join_request', group_id: groupId, agent_id: agentId, message });
351
387
  },
352
388
  onGroupMessageBatch(groupId, batch) {
353
- console.log(`[Group] onGroupMessageBatch: group=${groupId} count=${batch.count} range=[${batch.start_msg_id}, ${batch.latest_msg_id}]`);
389
+ const batchMessages = batch.messages || [];
390
+ utils_1.logger.log(`[Group] onGroupMessageBatch: group=${groupId} count=${batch.count} range=[${batch.start_msg_id}, ${batch.latest_msg_id}] messages=${JSON.stringify(batchMessages.map(m => m.msg_id))}`);
354
391
  // 存储 + ACK(统一由 agentcp 处理),注意 processAndAckBatch 是 async
355
392
  instance.agentCP.processAndAckBatch(groupId, batch).then((sorted) => {
393
+ var _a, _b;
394
+ utils_1.logger.log(`[Group] processAndAckBatch OK: group=${groupId} sortedCount=${sorted.length} msgIds=${sorted.map(m => m.msg_id)}`);
395
+ // 检查浏览器连接数
396
+ const connectedCount = Array.from(browserWsClients.entries()).filter(([ws]) => ws.readyState === ws_1.default.OPEN).length;
397
+ utils_1.logger.log(`[Group] broadcastToBrowser: group=${groupId} connectedBrowserClients=${connectedCount} totalClients=${browserWsClients.size}`);
398
+ if (connectedCount === 0) {
399
+ utils_1.logger.warn(`[Group] !!! 没有已连接的浏览器客户端,消息无法推送到前端!`);
400
+ }
356
401
  // 推送消息列表给浏览器
357
- broadcastToBrowser({
402
+ const payload = {
358
403
  type: 'group_message_batch',
359
404
  group_id: groupId,
360
405
  messages: sorted,
361
406
  count: batch.count,
362
407
  start_msg_id: batch.start_msg_id,
363
408
  latest_msg_id: batch.latest_msg_id,
364
- });
409
+ };
410
+ 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}`);
411
+ broadcastToBrowser(payload);
365
412
  }).catch((e) => {
366
- console.error(`[Group] processAndAckBatch failed: group=${groupId}`, e);
413
+ utils_1.logger.error(`[Group] processAndAckBatch failed: group=${groupId}`, e);
367
414
  });
368
415
  },
369
416
  onGroupEvent(groupId, evt) {
370
- console.log(`[Group] onGroupEvent: group=${groupId} event=${evt.event_type}`);
417
+ utils_1.logger.log(`[Group] onGroupEvent: group=${groupId} event=${evt.event_type}`);
371
418
  broadcastToBrowser({
372
419
  type: 'group_event',
373
420
  group_id: groupId,
374
421
  event: evt,
375
422
  });
376
423
  },
377
- onDutyDispatch(groupId, context) {
378
- console.log(`[Group] onDutyDispatch: group=${groupId} original_msg_id=${context.original_msg_id} sender=${context.sender_id}`);
379
- broadcastToBrowser({
380
- type: 'duty_dispatch',
381
- group_id: groupId,
382
- context,
383
- });
384
- },
385
424
  });
386
425
  // 同步群组列表(如未同步过)
387
426
  if (!instance.groupListSynced) {
@@ -390,7 +429,7 @@ async function ensureGroupClient(instance) {
390
429
  instance.groupListSynced = true;
391
430
  }
392
431
  catch (e) {
393
- console.warn('[Group] syncGroupList error:', e.message);
432
+ utils_1.logger.warn('[Group] syncGroupList error:', e.message);
394
433
  }
395
434
  }
396
435
  // 为所有已加入群组注册上线(register_online + 拉取未读 + 启动心跳)
@@ -400,13 +439,13 @@ async function ensureGroupClient(instance) {
400
439
  await instance.agentCP.joinGroupSession(group.group_id);
401
440
  }
402
441
  catch (e) {
403
- console.warn(`[Group] joinGroupSession failed: ${group.group_id}`, e.message);
442
+ utils_1.logger.warn(`[Group] joinGroupSession failed: ${group.group_id}`, e.message);
404
443
  }
405
444
  }
406
445
  instance.groupInitialized = true;
407
446
  instance.groupSessionId = groupSessionId;
408
447
  instance.groupTargetAid = targetAid;
409
- console.log(`[Group] 群组客户端已初始化: aid=${aid} target=${targetAid} session=${groupSessionId}`);
448
+ utils_1.logger.log(`[Group] 群组客户端已初始化: aid=${aid} target=${targetAid} session=${groupSessionId}`);
410
449
  }
411
450
  async function validateAid(aid) {
412
451
  try {
@@ -442,7 +481,7 @@ async function getAidStatusList() {
442
481
  const agentInfoCache = new Map();
443
482
  const AGENT_INFO_CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
444
483
  function getAgentInfoCachePath() {
445
- const dir = globalDataDir || process.cwd();
484
+ const dir = globalDataDir || datamanager_1.DEFAULT_ACP_DIR;
446
485
  return path.join(dir, 'AIDs', '.agent-info-cache.json');
447
486
  }
448
487
  function loadAgentInfoCacheFromDisk() {
@@ -457,7 +496,7 @@ function loadAgentInfoCacheFromDisk() {
457
496
  agentInfoCache.set(aid, Object.assign(Object.assign({}, info), { tags: info.tags || [] }));
458
497
  }
459
498
  }
460
- console.log(`[Server] 已加载 agent info 缓存: ${agentInfoCache.size} 条`);
499
+ utils_1.logger.log(`[Server] 已加载 agent info 缓存: ${agentInfoCache.size} 条`);
461
500
  }
462
501
  }
463
502
  catch (e) {
@@ -541,7 +580,7 @@ async function getAgentInfo(aid) {
541
580
  }
542
581
  // 每个 AID 的自定义 agent.md 选项 (昵称、描述)
543
582
  function getAidMdOptionsPath() {
544
- const dir = globalDataDir || process.cwd();
583
+ const dir = globalDataDir || datamanager_1.DEFAULT_ACP_DIR;
545
584
  return path.join(dir, 'AIDs', '.aid-md-options.json');
546
585
  }
547
586
  function loadAidMdOptions() {
@@ -576,7 +615,7 @@ function getMessageStoreForAid(aid) {
576
615
  if (!store) {
577
616
  store = new messagestore_1.MessageStore({
578
617
  persistMessages: true,
579
- basePath: globalDataDir || process.cwd(),
618
+ basePath: globalDataDir || datamanager_1.DEFAULT_ACP_DIR,
580
619
  });
581
620
  messageStores.set(aid, store);
582
621
  }
@@ -591,1640 +630,2026 @@ async function ensureMessageStoreLoaded(aid) {
591
630
  return store;
592
631
  }
593
632
  // HTML 页面
594
- const indexHtml = `<!DOCTYPE html>
595
- <html lang="zh-CN">
596
- <head>
597
- <meta charset="UTF-8">
598
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
599
- <link rel="icon" href="/favicon.ico" type="image/x-icon">
600
- <title>ACP 身份管理</title>
601
- <style>
602
- * { box-sizing: border-box; margin: 0; padding: 0; }
603
- 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; }
604
- .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; }
605
- .page-header { background: linear-gradient(135deg, #2563eb 0%, #1e40af 100%); padding: 28px 32px 22px; color: white; text-align: center; }
606
- .page-header h1 { font-size: 20px; font-weight: 600; margin-bottom: 12px; letter-spacing: 0.5px; }
607
- .nav-links { display: flex; justify-content: center; gap: 8px; }
608
- .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; }
609
- .nav-links a:hover { background: rgba(255,255,255,0.15); color: #fff; border-color: rgba(255,255,255,0.5); }
610
- .page-body { padding: 24px 32px 32px; }
611
- .hint { text-align: center; color: #9ca3af; font-size: 13px; margin-bottom: 20px; }
612
- .create-section { margin-bottom: 24px; display: flex; flex-direction: column; gap: 10px; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 12px; padding: 18px; }
613
- .create-section .aid-input-row { display: flex; gap: 8px; align-items: center; }
614
- .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; }
615
- .create-section .aid-input-row input:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37,99,235,0.1); }
616
- .create-section .aid-input-row .dot-separator { color: #9ca3af; font-size: 16px; flex-shrink: 0; }
617
- .create-section .aid-input-row select {
618
- padding: 10px 30px 10px 14px;
619
- border: 1px solid #e2e8f0;
620
- border-radius: 8px;
621
- font-size: 14px;
622
- background: #fff;
623
- flex-shrink: 0;
624
- cursor: pointer;
625
- appearance: none;
626
- -webkit-appearance: none;
627
- -moz-appearance: none;
628
- 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");
629
- background-repeat: no-repeat;
630
- background-position: right 10px top 50%;
631
- background-size: 10px auto;
632
- transition: border-color 0.2s, box-shadow 0.2s;
633
- }
634
- .create-section .aid-input-row select:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37,99,235,0.1); }
635
- .create-section .extra-fields { display: flex; gap: 8px; }
636
- .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; }
637
- .create-section .extra-fields input:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37,99,235,0.1); }
638
- .btn { display: block; width: 100%; padding: 11px; border: none; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s; }
639
- .btn-primary { background: linear-gradient(135deg, #2563eb, #1d4ed8); color: white; }
640
- .btn-primary:hover { background: linear-gradient(135deg, #1d4ed8, #1e40af); box-shadow: 0 2px 8px rgba(37,99,235,0.3); }
641
- .btn-sm { display: inline-block; width: auto; padding: 6px 14px; font-size: 13px; border-radius: 6px; }
642
- .btn-success { background: #10b981; color: white; }
643
- .btn-success:hover { background: #059669; }
644
- .btn-danger { background: #ef4444; color: white; }
645
- .btn-danger:hover { background: #dc2626; }
646
- .btn-outline { background: white; color: #2563eb; border: 1px solid #2563eb; }
647
- .btn-outline:hover { background: #eff6ff; }
648
- .btn-outline.active { background: #2563eb; color: white; }
649
- .btn:disabled { background: #d1d5db; cursor: not-allowed; border-color: #d1d5db; color: #fff; }
650
- .aid-list { margin-bottom: 24px; }
651
- .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; }
652
- .aid-card:hover { border-color: #93c5fd; box-shadow: 0 2px 12px rgba(37,99,235,0.06); }
653
- .aid-card.current { border-color: #2563eb; background: #eff6ff; }
654
- .aid-card-left { flex: 1; min-width: 0; }
655
- .aid-card-right { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; flex-shrink: 0; justify-content: center; }
656
- .aid-card-header { margin-bottom: 10px; }
657
- .aid-name { font-family: 'SF Mono', 'Fira Code', monospace; font-size: 13px; color: #1f2937; word-break: break-all; }
658
- .copy-btn { background: none; border: none; color: #9ca3af; cursor: pointer; font-size: 12px; padding: 2px 6px; transition: color 0.2s; }
659
- .copy-btn:hover { color: #2563eb; }
660
- .aid-card-status { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
661
- .badge { display: inline-block; padding: 3px 10px; border-radius: 12px; font-size: 12px; font-weight: 500; }
662
- .badge-success { background: #d1fae5; color: #065f46; }
663
- .badge-warning { background: #fef3c7; color: #92400e; }
664
- .badge-danger { background: #fee2e2; color: #991b1b; }
665
- .badge-info { background: #dbeafe; color: #1e40af; }
666
- .badge-current { background: #2563eb; color: white; }
667
- .aid-card-actions { display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
668
- .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); }
669
- .status.success { display: block; background: #d1fae5; color: #065f46; }
670
- .status.error { display: block; background: #fee2e2; color: #991b1b; }
671
- @media (max-width: 480px) {
672
- body { padding: 16px 8px; }
673
- .page-header { padding: 22px 18px 18px; }
674
- .page-body { padding: 18px 16px 24px; }
675
- .create-section { padding: 14px; }
676
- }
677
- </style>
678
- </head>
679
- <body>
680
- <div class="container">
681
- <div class="page-header">
682
- <h1>ACP 身份管理</h1>
683
- <div class="nav-links">
684
- <a href="https://agentunion.net" target="_blank">AgentUnion排行榜</a>
685
- <a href="https://github.com/auliwenjiang/agentcp" target="_blank">ACP 开源GitHub</a>
686
- </div>
687
- </div>
688
- <div class="page-body">
689
- <div class="hint" id="hint">最多注册 10 个 AID</div>
690
-
691
- <div class="create-section" id="createSection">
692
- <div class="aid-input-row">
693
- <input type="text" id="newAid" placeholder="输入名称">
694
- <span class="dot-separator">.</span>
695
- <select id="apSelect"></select>
696
- </div>
697
- <div class="extra-fields">
698
- <input type="text" id="aidNickname" placeholder="昵称(选填)">
699
- <input type="text" id="aidDescription" placeholder="描述(选填)" style="flex:2;">
700
- </div>
701
- <button class="btn btn-primary" onclick="createAid()">注册 AID</button>
702
- </div>
703
-
704
- <div class="aid-list" id="aidList"></div>
705
-
706
- <div class="status" id="status"></div>
707
- </div>
708
- </div>
709
-
710
- <script>
711
- let aidData = { aidList: [], aidStatus: [], apiUrl: '' };
712
-
713
- async function loadAidInfo() {
714
- try {
715
- const res = await fetch('/api/aid');
716
- const data = await res.json();
717
- aidData = data;
718
- updateApSelect();
719
- renderAidList();
720
- } catch (e) {
721
- console.error('加载失败', e);
722
- }
723
- }
724
-
725
- function updateApSelect() {
726
- var sel = document.getElementById('apSelect');
727
- if (sel && sel.options.length === 0) {
728
- const options = ['agentcp.io', 'aid.show', 'agentid.pub'];
729
- options.forEach(function(op) {
730
- var opt = document.createElement('option');
731
- opt.value = op;
732
- opt.textContent = op;
733
- if (op === 'agentcp.io') opt.selected = true;
734
- sel.appendChild(opt);
735
- });
736
- }
737
- }
738
-
739
- function renderAidList() {
740
- const list = document.getElementById('aidList');
741
- const createSection = document.getElementById('createSection');
742
- const hint = document.getElementById('hint');
743
-
744
- if (aidData.aidList.length >= 10) {
745
- createSection.style.display = 'none';
746
- hint.textContent = '已达到 10 个 AID 上限';
747
- } else {
748
- createSection.style.display = 'block';
749
- hint.textContent = '最多注册 10 个 AID(已注册 ' + aidData.aidList.length + ' 个)';
750
- }
751
-
752
- if (!aidData.aidStatus || aidData.aidStatus.length === 0) {
753
- list.innerHTML = '<div style="text-align:center;color:#999;padding:20px;">暂无 AID,请先注册</div>';
754
- return;
755
- }
756
-
757
- list.innerHTML = aidData.aidStatus.map(function(item) {
758
- var cardClass = 'aid-card';
759
-
760
- var badges = '';
761
- if (item.online) badges += '<span class="badge badge-success">已上线</span>';
762
- if (item.keysExist && item.certValid) {
763
- badges += '<span class="badge badge-info">密钥有效</span>';
764
- } else if (item.keysExist && !item.certValid) {
765
- badges += '<span class="badge badge-warning">证书过期</span>';
766
- } else {
767
- badges += '<span class="badge badge-danger">密钥缺失</span>';
768
- }
769
-
770
- var actions = '';
771
- if (item.keysExist && item.certValid) {
772
- if (item.online) {
773
- actions += '<button class="btn btn-sm btn-success" onclick="enterChat(\\'' + escapeAttr(item.aid) + '\\')">进入聊天</button>';
774
- actions += '<button class="btn btn-sm btn-danger" onclick="goOffline(\\'' + escapeAttr(item.aid) + '\\')">下线</button>';
775
- } else {
776
- actions += '<button class="btn btn-sm btn-success" id="goBtn_' + escapeAttr(item.aid) + '" onclick="goOnlineAndChat(\\'' + escapeAttr(item.aid) + '\\')">上线并进入</button>';
777
- }
778
- }
779
-
780
- return '<div class="' + cardClass + '">' +
781
- '<div class="aid-card-left">' +
782
- '<div class="aid-card-header">' +
783
- '<span class="aid-name">' + escapeHtml(item.aid) + '</span>' +
784
- '</div>' +
785
- '<div class="aid-card-status">' + badges + '</div>' +
786
- '</div>' +
787
- '<div class="aid-card-right">' +
788
- '<div class="aid-card-actions">' + actions + '</div>' +
789
- '<button class="copy-btn" onclick="copyText(\\'' + escapeAttr(item.aid) + '\\')">复制</button>' +
790
- '</div>' +
791
- '</div>';
792
- }).join('');
793
- }
794
-
795
- async function createAid() {
796
- var prefix = document.getElementById('newAid').value.trim();
797
- if (!prefix) { showStatus('请输入 AID 名称', 'error'); return; }
798
- var ap = document.getElementById('apSelect').value;
799
- if (!ap) { showStatus('请选择 AP', 'error'); return; }
800
- var fullPrefix = prefix + '.' + ap;
801
- var nickname = document.getElementById('aidNickname').value.trim();
802
- var description = document.getElementById('aidDescription').value.trim();
803
- try {
804
- var res = await fetch('/api/aid/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prefix: fullPrefix, nickname: nickname, description: description }) });
805
- var data = await res.json();
806
- if (data.success) { showStatus('AID 注册成功', 'success'); document.getElementById('newAid').value = ''; document.getElementById('aidNickname').value = ''; document.getElementById('aidDescription').value = ''; loadAidInfo(); }
807
- else { showStatus(data.error || '注册失败', 'error'); }
808
- } catch (e) { showStatus('注册失败: ' + e.message, 'error'); }
809
- }
810
-
811
- async function selectAid(aid) {
812
- try {
813
- var res = await fetch('/api/aid/select', { 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
- async function goOnlineAndChat(aid) {
821
- var btn = document.getElementById('goBtn_' + aid);
822
- if (btn) { btn.disabled = true; btn.textContent = '启动中...'; }
823
- try {
824
- showStatus('正在上线 ' + aid + ' ...', 'success');
825
- var res = await fetch('/api/ws/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ aid: aid }) });
826
- var data = await res.json();
827
- if (data.success) {
828
- showStatus(aid + ' 已上线,正在进入聊天...', 'success');
829
- window.location.href = '/chat';
830
- } else {
831
- showStatus(data.error || '上线失败', 'error');
832
- if (btn) { btn.disabled = false; btn.textContent = '上线并进入'; }
833
- }
834
- } catch (e) {
835
- showStatus('上线失败: ' + e.message, 'error');
836
- if (btn) { btn.disabled = false; btn.textContent = '上线并进入'; }
837
- }
838
- }
839
-
840
- function enterChat(aid) { window.location.href = '/chat'; }
841
-
842
- async function goOffline(aid) {
843
- try {
844
- var res = await fetch('/api/aid/offline', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ aid: aid }) });
845
- var data = await res.json();
846
- if (data.success) { showStatus(aid + ' 已下线', 'success'); loadAidInfo(); }
847
- else { showStatus(data.error || '下线失败', 'error'); }
848
- } catch (e) { showStatus('下线失败: ' + e.message, 'error'); }
849
- }
850
-
851
- function copyText(text) {
852
- navigator.clipboard.writeText(text).then(function() {
853
- showStatus('已复制', 'success');
854
- });
855
- }
856
-
857
- function showStatus(msg, type) {
858
- var el = document.getElementById('status');
859
- el.textContent = msg;
860
- el.className = 'status ' + type;
861
- setTimeout(function() { el.className = 'status'; }, 3000);
862
- }
863
-
864
- function escapeHtml(text) {
865
- var div = document.createElement('div');
866
- div.textContent = text;
867
- return div.innerHTML;
868
- }
869
-
870
- function escapeAttr(text) {
871
- return text.replace(/'/g, "\\\\'").replace(/"/g, '&quot;');
872
- }
873
-
874
- loadAidInfo();
875
- setInterval(loadAidInfo, 5000);
876
- <\/script>
877
- </body>
633
+ const indexHtml = `<!DOCTYPE html>
634
+ <html lang="zh-CN">
635
+ <head>
636
+ <meta charset="UTF-8">
637
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
638
+ <link rel="icon" href="/favicon.ico" type="image/x-icon">
639
+ <title>ACP 身份管理</title>
640
+ <style>
641
+ * { box-sizing: border-box; margin: 0; padding: 0; }
642
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
643
+ ::-webkit-scrollbar-track { background: transparent; }
644
+ ::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.15); border-radius: 3px; }
645
+ ::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,0.25); }
646
+ 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; }
647
+ .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; }
648
+ .page-header { background: linear-gradient(135deg, #2563eb 0%, #1e40af 100%); padding: 28px 32px 22px; color: white; text-align: center; }
649
+ .page-header h1 { font-size: 20px; font-weight: 600; margin-bottom: 12px; letter-spacing: 0.5px; }
650
+ .nav-links { display: flex; justify-content: center; gap: 8px; }
651
+ .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; }
652
+ .nav-links a:hover { background: rgba(255,255,255,0.15); color: #fff; border-color: rgba(255,255,255,0.5); }
653
+ .page-body { padding: 24px 32px 32px; }
654
+ .hint { text-align: center; color: #9ca3af; font-size: 13px; margin-bottom: 20px; }
655
+ .create-section { margin-bottom: 24px; display: flex; flex-direction: column; gap: 10px; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 12px; padding: 18px; }
656
+ .create-section .aid-input-row { display: flex; gap: 8px; align-items: center; }
657
+ .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; }
658
+ .create-section .aid-input-row input:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37,99,235,0.1); }
659
+ .create-section .aid-input-row .dot-separator { color: #9ca3af; font-size: 16px; flex-shrink: 0; }
660
+ .create-section .aid-input-row select {
661
+ padding: 10px 30px 10px 14px;
662
+ border: 1px solid #e2e8f0;
663
+ border-radius: 8px;
664
+ font-size: 14px;
665
+ background: #fff;
666
+ flex-shrink: 0;
667
+ cursor: pointer;
668
+ appearance: none;
669
+ -webkit-appearance: none;
670
+ -moz-appearance: none;
671
+ 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");
672
+ background-repeat: no-repeat;
673
+ background-position: right 10px top 50%;
674
+ background-size: 10px auto;
675
+ transition: border-color 0.2s, box-shadow 0.2s;
676
+ }
677
+ .create-section .aid-input-row select:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37,99,235,0.1); }
678
+ .create-section .extra-fields { display: flex; gap: 8px; }
679
+ .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; }
680
+ .create-section .extra-fields input:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37,99,235,0.1); }
681
+ .btn { display: block; width: 100%; padding: 11px; border: none; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s; }
682
+ .btn:active { transform: scale(0.98); }
683
+ .btn-primary { background: linear-gradient(135deg, #2563eb, #1d4ed8); color: white; }
684
+ .btn-primary:hover { background: linear-gradient(135deg, #1d4ed8, #1e40af); box-shadow: 0 2px 8px rgba(37,99,235,0.3); }
685
+ .btn-sm { display: inline-block; width: auto; padding: 6px 14px; font-size: 13px; border-radius: 6px; }
686
+ .btn-success { background: #10b981; color: white; }
687
+ .btn-success:hover { background: #059669; }
688
+ .btn-danger { background: #ef4444; color: white; }
689
+ .btn-danger:hover { background: #dc2626; }
690
+ .btn-outline { background: white; color: #2563eb; border: 1px solid #2563eb; }
691
+ .btn-outline:hover { background: #eff6ff; }
692
+ .btn-outline.active { background: #2563eb; color: white; }
693
+ .btn:disabled { background: #d1d5db; cursor: not-allowed; border-color: #d1d5db; color: #fff; transform: none; }
694
+ .aid-list { margin-bottom: 24px; }
695
+ .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; }
696
+ .aid-card:hover { border-color: #93c5fd; box-shadow: 0 4px 12px rgba(37,99,235,0.08); transform: translateY(-1px); }
697
+ .aid-card.current { border-color: #2563eb; background: #eff6ff; }
698
+ .aid-card-left { flex: 1; min-width: 0; }
699
+ .aid-card-right { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; flex-shrink: 0; justify-content: center; }
700
+ .aid-card-header { margin-bottom: 10px; }
701
+ .aid-name { font-family: 'SF Mono', 'Fira Code', monospace; font-size: 13px; color: #1f2937; word-break: break-all; }
702
+ .copy-btn { background: none; border: none; color: #9ca3af; cursor: pointer; font-size: 12px; padding: 2px 6px; transition: color 0.2s; }
703
+ .copy-btn:hover { color: #2563eb; }
704
+ .aid-card-status { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
705
+ .badge { display: inline-block; padding: 3px 10px; border-radius: 12px; font-size: 12px; font-weight: 500; }
706
+ .badge-success { background: #d1fae5; color: #065f46; }
707
+ .badge-warning { background: #fef3c7; color: #92400e; }
708
+ .badge-danger { background: #fee2e2; color: #991b1b; }
709
+ .badge-info { background: #dbeafe; color: #1e40af; }
710
+ .badge-current { background: #2563eb; color: white; }
711
+ .aid-card-actions { display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
712
+ .status { position: fixed; top: 20px; left: 50%; transform: translate(-50%, -10px); padding: 12px 24px; border-radius: 10px; font-size: 14px; opacity: 0; visibility: hidden; z-index: 1000; box-shadow: 0 4px 16px rgba(0,0,0,0.1); transition: all 0.3s cubic-bezier(0.16,1,0.3,1); }
713
+ .status.success { opacity: 1; visibility: visible; transform: translate(-50%, 0); background: #d1fae5; color: #065f46; border: 1px solid #a7f3d0; }
714
+ .status.error { opacity: 1; visibility: visible; transform: translate(-50%, 0); background: #fee2e2; color: #991b1b; border: 1px solid #fecaca; }
715
+ @media (max-width: 480px) {
716
+ body { padding: 16px 8px; }
717
+ .page-header { padding: 22px 18px 18px; }
718
+ .page-body { padding: 18px 16px 24px; }
719
+ .create-section { padding: 14px; }
720
+ }
721
+ </style>
722
+ </head>
723
+ <body>
724
+ <div class="container">
725
+ <div class="page-header">
726
+ <h1>ACP 身份管理</h1>
727
+ <div class="nav-links">
728
+ <a href="https://agentunion.net" target="_blank">AgentUnion排行榜</a>
729
+ <a href="https://github.com/auliwenjiang/agentcp" target="_blank">ACP 开源GitHub</a>
730
+ </div>
731
+ </div>
732
+ <div class="page-body">
733
+ <div class="hint" id="hint">最多注册 10 个 AID</div>
734
+
735
+ <div class="create-section" id="createSection">
736
+ <div class="aid-input-row">
737
+ <input type="text" id="newAid" placeholder="输入名称">
738
+ <span class="dot-separator">.</span>
739
+ <select id="apSelect"></select>
740
+ </div>
741
+ <div class="extra-fields">
742
+ <input type="text" id="aidNickname" placeholder="昵称(选填)">
743
+ <input type="text" id="aidDescription" placeholder="描述(选填)" style="flex:2;">
744
+ </div>
745
+ <button class="btn btn-primary" onclick="createAid()">注册 AID</button>
746
+ </div>
747
+
748
+ <div class="aid-list" id="aidList"></div>
749
+
750
+ <div class="status" id="status"></div>
751
+ </div>
752
+ </div>
753
+
754
+ <script>
755
+ let aidData = { aidList: [], aidStatus: [], apiUrl: '' };
756
+
757
+ async function loadAidInfo() {
758
+ try {
759
+ const res = await fetch('/api/aid');
760
+ const data = await res.json();
761
+ aidData = data;
762
+ updateApSelect();
763
+ renderAidList();
764
+ } catch (e) {
765
+ console.error('加载失败', e);
766
+ }
767
+ }
768
+
769
+ function updateApSelect() {
770
+ var sel = document.getElementById('apSelect');
771
+ if (sel && sel.options.length === 0) {
772
+ const options = ['agentcp.io', 'aid.show', 'agentid.pub'];
773
+ options.forEach(function(op) {
774
+ var opt = document.createElement('option');
775
+ opt.value = op;
776
+ opt.textContent = op;
777
+ if (op === 'agentcp.io') opt.selected = true;
778
+ sel.appendChild(opt);
779
+ });
780
+ }
781
+ }
782
+
783
+ function renderAidList() {
784
+ const list = document.getElementById('aidList');
785
+ const createSection = document.getElementById('createSection');
786
+ const hint = document.getElementById('hint');
787
+
788
+ if (aidData.aidList.length >= 10) {
789
+ createSection.style.display = 'none';
790
+ hint.textContent = '已达到 10 个 AID 上限';
791
+ } else {
792
+ createSection.style.display = 'block';
793
+ hint.textContent = '最多注册 10 个 AID(已注册 ' + aidData.aidList.length + ' 个)';
794
+ }
795
+
796
+ if (!aidData.aidStatus || aidData.aidStatus.length === 0) {
797
+ list.innerHTML = '<div style="text-align:center;color:#999;padding:20px;">暂无 AID,请先注册</div>';
798
+ return;
799
+ }
800
+
801
+ list.innerHTML = aidData.aidStatus.map(function(item) {
802
+ var cardClass = 'aid-card';
803
+
804
+ var badges = '';
805
+ if (item.online) badges += '<span class="badge badge-success">已上线</span>';
806
+ if (item.keysExist && item.certValid) {
807
+ badges += '<span class="badge badge-info">密钥有效</span>';
808
+ } else if (item.keysExist && !item.certValid) {
809
+ badges += '<span class="badge badge-warning">证书过期</span>';
810
+ } else {
811
+ badges += '<span class="badge badge-danger">密钥缺失</span>';
812
+ }
813
+
814
+ var actions = '';
815
+ if (item.keysExist && item.certValid) {
816
+ if (item.online) {
817
+ actions += '<button class="btn btn-sm btn-success" onclick="enterChat(\\'' + escapeAttr(item.aid) + '\\')">进入聊天</button>';
818
+ actions += '<button class="btn btn-sm btn-danger" onclick="goOffline(\\'' + escapeAttr(item.aid) + '\\')">下线</button>';
819
+ } else {
820
+ actions += '<button class="btn btn-sm btn-success" id="goBtn_' + escapeAttr(item.aid) + '" onclick="goOnlineAndChat(\\'' + escapeAttr(item.aid) + '\\')">上线并进入</button>';
821
+ }
822
+ }
823
+
824
+ return '<div class="' + cardClass + '">' +
825
+ '<div class="aid-card-left">' +
826
+ '<div class="aid-card-header">' +
827
+ '<span class="aid-name">' + escapeHtml(item.aid) + '</span>' +
828
+ '</div>' +
829
+ '<div class="aid-card-status">' + badges + '</div>' +
830
+ '</div>' +
831
+ '<div class="aid-card-right">' +
832
+ '<div class="aid-card-actions">' + actions + '</div>' +
833
+ '<button class="copy-btn" onclick="copyText(\\'' + escapeAttr(item.aid) + '\\')">复制</button>' +
834
+ '</div>' +
835
+ '</div>';
836
+ }).join('');
837
+ }
838
+
839
+ async function createAid() {
840
+ var prefix = document.getElementById('newAid').value.trim();
841
+ if (!prefix) { showStatus('请输入 AID 名称', 'error'); return; }
842
+ var ap = document.getElementById('apSelect').value;
843
+ if (!ap) { showStatus('请选择 AP', 'error'); return; }
844
+ var fullPrefix = prefix + '.' + ap;
845
+ var nickname = document.getElementById('aidNickname').value.trim();
846
+ var description = document.getElementById('aidDescription').value.trim();
847
+ try {
848
+ var res = await fetch('/api/aid/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prefix: fullPrefix, nickname: nickname, description: description }) });
849
+ var data = await res.json();
850
+ if (data.success) { showStatus('AID 注册成功', 'success'); document.getElementById('newAid').value = ''; document.getElementById('aidNickname').value = ''; document.getElementById('aidDescription').value = ''; loadAidInfo(); }
851
+ else { showStatus(data.error || '注册失败', 'error'); }
852
+ } catch (e) { showStatus('注册失败: ' + e.message, 'error'); }
853
+ }
854
+
855
+ async function selectAid(aid) {
856
+ try {
857
+ var res = await fetch('/api/aid/select', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ aid: aid }) });
858
+ var data = await res.json();
859
+ if (data.success) { showStatus('已切换到 ' + aid, 'success'); loadAidInfo(); }
860
+ else { showStatus(data.error || '切换失败', 'error'); }
861
+ } catch (e) { showStatus('切换失败: ' + e.message, 'error'); }
862
+ }
863
+
864
+ async function goOnlineAndChat(aid) {
865
+ var btn = document.getElementById('goBtn_' + aid);
866
+ if (btn) { btn.disabled = true; btn.textContent = '启动中...'; }
867
+ try {
868
+ showStatus('正在上线 ' + aid + ' ...', 'success');
869
+ var res = await fetch('/api/ws/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ aid: aid }) });
870
+ var data = await res.json();
871
+ if (data.success) {
872
+ showStatus(aid + ' 已上线,正在进入聊天...', 'success');
873
+ window.location.href = '/chat';
874
+ } else {
875
+ showStatus(data.error || '上线失败', 'error');
876
+ if (btn) { btn.disabled = false; btn.textContent = '上线并进入'; }
877
+ }
878
+ } catch (e) {
879
+ showStatus('上线失败: ' + e.message, 'error');
880
+ if (btn) { btn.disabled = false; btn.textContent = '上线并进入'; }
881
+ }
882
+ }
883
+
884
+ function enterChat(aid) { window.location.href = '/chat'; }
885
+
886
+ async function goOffline(aid) {
887
+ try {
888
+ var res = await fetch('/api/aid/offline', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ aid: aid }) });
889
+ var data = await res.json();
890
+ if (data.success) { showStatus(aid + ' 已下线', 'success'); loadAidInfo(); }
891
+ else { showStatus(data.error || '下线失败', 'error'); }
892
+ } catch (e) { showStatus('下线失败: ' + e.message, 'error'); }
893
+ }
894
+
895
+ function copyText(text) {
896
+ navigator.clipboard.writeText(text).then(function() {
897
+ showStatus('已复制', 'success');
898
+ });
899
+ }
900
+
901
+ let statusTimeout = null;
902
+ function showStatus(msg, type) {
903
+ var el = document.getElementById('status');
904
+ el.textContent = msg;
905
+ el.className = 'status ' + type;
906
+ if (statusTimeout) clearTimeout(statusTimeout);
907
+ statusTimeout = setTimeout(function() { el.className = 'status'; }, 3000);
908
+ }
909
+
910
+ function escapeHtml(text) {
911
+ var div = document.createElement('div');
912
+ div.textContent = text;
913
+ return div.innerHTML;
914
+ }
915
+
916
+ function escapeAttr(text) {
917
+ return text.replace(/'/g, "\\\\'").replace(/"/g, '&quot;');
918
+ }
919
+
920
+ loadAidInfo();
921
+ setInterval(loadAidInfo, 5000);
922
+ <\/script>
923
+ </body>
878
924
  </html>`;
879
- const chatHtml = `<!DOCTYPE html>
880
- <html lang="zh-CN">
881
- <head>
882
- <meta charset="UTF-8">
883
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
884
- <title>ACP 聊天</title>
885
- <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"><\/script>
886
- <style>
887
- :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; }
888
- * { box-sizing:border-box; margin:0; padding:0; }
889
- body { font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; background:var(--bg); height:100vh; overflow:hidden; color:var(--t1); }
890
- #app { display:flex; height:100%; }
891
-
892
- /* Sidebar */
893
- .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; }
894
- .sidebar.collapsed { width:0; border-right:none; }
895
- .sidebar-header { padding:12px 14px; border-bottom:1px solid var(--border); display:flex; flex-direction:column; gap:12px; flex-shrink:0; }
896
- .header-top { display:flex; justify-content:space-between; align-items:center; width:100%; }
897
- .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; }
898
- .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; }
899
- .new-chat-btn:hover { background:var(--primary-h); }
900
- .session-list { flex:1; overflow-y:auto; }
901
-
902
- /* AID Group */
903
- .aid-group { border-bottom:1px solid var(--border); }
904
- .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; }
905
- .aid-group-header:hover { background:linear-gradient(135deg,#dbeafe,#d0e4fd); }
906
- .aid-group-info { flex:1; min-width:0; margin-left:4px; }
907
- .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; }
908
- .aid-group-desc { font-size:10px; color:#6b7280; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; margin-top:3px; display:block; padding-left:2px; }
909
- .aid-group-arrow { font-size:10px; color:var(--primary); transition:transform 0.2s; flex-shrink:0; }
910
- .aid-group-arrow.open { transform:rotate(90deg); }
911
- .aid-group-badge { font-size:10px; background:var(--primary); color:#fff; padding:1px 6px; border-radius:8px; margin-left:8px; flex-shrink:0; }
912
- .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; }
913
- .aid-group-add:hover { background:var(--primary); color:#fff; border-color:var(--primary); }
914
- .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; }
915
- .aid-group-header:hover .aid-group-del { display:block; }
916
- .aid-group-del:hover { color:#dc3545; background:#ffebeb; }
917
- .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; }
918
- .session-item:hover .session-del { display:block; }
919
- .session-del:hover { color:#dc3545; }
920
- .aid-group-sessions { display:none; background:#fafbfc; }
921
- .aid-group-sessions.open { display:block; }
922
-
923
- .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; }
924
-
925
- .session-item { padding:10px 14px 10px 32px; border-bottom:1px solid #f0f1f3; cursor:pointer; transition:all 0.15s; position:relative; }
926
- .session-item::before { content:''; position:absolute; left:18px; top:50%; transform:translateY(-50%); width:6px; height:6px; border-radius:50%; background:#d1d5db; }
927
- .session-item:hover { background:#f0f5ff; }
928
- .session-item.active { background:#eff6ff; border-left:3px solid var(--primary); padding-left:29px; }
929
- .session-item.active::before { background:var(--primary); box-shadow:0 0 0 2px rgba(37,99,235,0.2); }
930
- .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; }
931
- .session-item.active .session-peer { background:#dbeafe; border-color:#bfdbfe; color:#1e40af; }
932
- .session-meta { font-size:10px; color:var(--t2); margin-top:4px; display:flex; align-items:center; gap:6px; padding-left:10px; }
933
- .tag { font-size:9px; padding:1px 5px; border-radius:3px; color:#fff; font-weight:600; letter-spacing:0.3px; }
934
- .tag.outgoing { background:var(--ok); }
935
- .tag.incoming { background:#8b5cf6; }
936
-
937
- /* Chat Area */
938
- .chat-area { flex:1; display:flex; flex-direction:column; background:var(--chat-bg); min-width:0; }
939
- .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; }
940
- .header-left { display:flex; align-items:center; gap:10px; overflow:hidden; }
941
- .toggle-sidebar-btn { background:none; border:none; cursor:pointer; color:var(--t2); padding:4px; display:flex; }
942
- .toggle-sidebar-btn:hover { color:var(--t1); }
943
- .status-dot { width:8px; height:8px; border-radius:50%; background:#ccc; flex-shrink:0; }
944
- .status-dot.connected { background:var(--ok); }
945
- .status-dot.connecting { background:#fbbf24; }
946
- .chat-title { font-size:15px; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
947
-
948
- .aid-select-wrap { display:flex; align-items:center; gap:10px; flex-shrink:0; }
949
- .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); }
950
- .manage-btn:hover { background:#f8fafc; color:var(--primary); border-color:var(--primary); }
951
- .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); }
952
- .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; }
953
- .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); }
954
- .status-toggle:hover { background:#f1f5f9; }
955
- .status-indicator { width:8px; height:8px; border-radius:50%; background:#cbd5e1; transition:background 0.3s; }
956
- .status-indicator.online { background:var(--ok); box-shadow:0 0 0 2px rgba(16,185,129,0.2); }
957
- .status-indicator.offline { background:#cbd5e1; }
958
-
959
- .collapse-btn { background:none; border:none; cursor:pointer; color:var(--t2); padding:6px; display:flex; align-items:center; flex-shrink:0; }
960
- .collapse-btn:hover { color:var(--t1); }
961
-
962
- .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; }
963
- .encrypt-banner svg { flex-shrink:0; }
964
-
965
- .messages { flex:1; padding:16px; overflow-y:auto; display:flex; flex-direction:column; gap:12px; }
966
- .message { display:flex; flex-direction:column; max-width:80%; }
967
- .message.sent { align-self:flex-end; align-items:flex-end; }
968
- .message.received { align-self:flex-start; align-items:flex-start; }
969
- .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); }
970
- .message.sent .bubble { background:var(--sent); color:#fff; border-bottom-right-radius:2px; }
971
- .message.received .bubble { background:var(--recv-bg); color:var(--t1); border-bottom-left-radius:2px; border:1px solid var(--border); }
972
- .msg-meta { font-size:10px; color:var(--t2); margin-bottom:3px; padding:0 4px; }
973
-
974
- .input-area { padding:12px 16px; background:#fff; border-top:1px solid var(--border); display:flex; align-items:center; gap:10px; flex-shrink:0; }
975
- .input-area input { flex:1; padding:10px 14px; border-radius:20px; border:1px solid var(--border); font-size:14px; background:#f9fafb; }
976
- .input-area input:focus { outline:none; border-color:var(--primary); background:#fff; }
977
- .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; }
978
- .send-btn:hover { background:var(--primary-h); }
979
- .send-btn:disabled { background:#ccc; cursor:not-allowed; }
980
-
981
- .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; }
982
- .modal-overlay.show { display:flex; }
983
- .modal { background:#fff; width:90%; max-width:400px; border-radius:12px; padding:24px; box-shadow:0 10px 25px rgba(0,0,0,0.1); }
984
- .modal h3 { margin-bottom:16px; font-size:16px; }
985
- .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; }
986
- .modal input[type="text"]:focus, .modal input[type="password"]:focus, .modal input[type="url"]:focus { outline:none; border-color:var(--primary); }
987
- .modal input[type="radio"] { width:auto; margin:0; }
988
- .modal-btns { display:flex; justify-content:flex-end; gap:10px; }
989
- .mbtn { padding:8px 16px; border-radius:6px; font-size:13px; cursor:pointer; border:none; }
990
- .mbtn-cancel { background:#f3f4f6; color:var(--t1); }
991
- .mbtn-ok { background:var(--primary); color:#fff; }
992
- .mbtn-ok:disabled { background:#ccc; }
993
-
994
- .bubble p { margin-bottom:0.4em; } .bubble p:last-child { margin-bottom:0; }
995
- .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; }
996
- .bubble h1 { font-size:1.5em; border-bottom:1px solid rgba(0,0,0,0.1); padding-bottom:0.3em; }
997
- .bubble h2 { font-size:1.3em; border-bottom:1px solid rgba(0,0,0,0.05); padding-bottom:0.3em; }
998
- .bubble h3 { font-size:1.1em; }
999
- .bubble ul, .bubble ol { padding-left:1.5em; margin-bottom:0.5em; }
1000
- .bubble li { margin-bottom:0.2em; }
1001
- .bubble blockquote { margin:0.5em 0; padding-left:1em; border-left:4px solid rgba(0,0,0,0.1); color:var(--t2); }
1002
- .bubble a { color:var(--primary); text-decoration:underline; } .bubble a:hover { opacity:0.85; }
1003
- .message.sent .bubble a { color:#fff; } .message.sent .bubble a:hover { opacity:0.85; }
1004
- .bubble img { max-width:100%; border-radius:4px; }
1005
- .bubble code { background:rgba(0,0,0,0.1); padding:2px 4px; border-radius:3px; font-family:monospace; font-size:0.9em; }
1006
- .bubble pre { background:#2d2d2d; color:#fff; padding:12px; border-radius:6px; overflow-x:auto; margin:8px 0; }
1007
- .bubble pre code { background:transparent; padding:0; color:inherit; border-radius:0; }
1008
- .messages { flex:1; padding:16px; overflow-y:auto; display:flex; flex-direction:column; gap:12px; position:relative; }
1009
- .message { display:flex; flex-direction:row; max-width:85%; gap:8px; }
1010
- .message.sent { align-self:flex-end; flex-direction:row-reverse; }
1011
- .message.received { align-self:flex-start; }
1012
- .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; }
1013
- .msg-content { display:flex; flex-direction:column; max-width:100%; min-width:0; }
1014
- .message.sent .msg-content { align-items:flex-end; }
1015
- .message.received .msg-content { align-items:flex-start; }
1016
- @media (min-width: 1024px) { .message { max-width: 70%; } }
1017
- .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; }
1018
- .new-msg-tip:hover { background:var(--primary-h); }
1019
- @keyframes newMsgBounce { 0%{transform:translateY(10px);opacity:0} 100%{transform:translateY(0);opacity:1} }
1020
-
1021
- @media (max-width:768px) {
1022
- .sidebar { position:absolute; height:100%; z-index:20; width:280px; }
1023
- .sidebar.collapsed { width:0; }
1024
- }
1025
-
1026
- /* Group UI Styles */
1027
- .tab-bar { display:flex; border-bottom:1px solid var(--border); flex-shrink:0; }
1028
- .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; }
1029
- .tab-bar .tab.active { color:var(--primary); border-bottom-color:var(--primary); }
1030
- .tab-bar .tab:hover { color:var(--t1); }
1031
- .group-list { flex:1; overflow-y:auto; }
1032
- .group-item { padding:12px 14px; border-bottom:1px solid #f3f4f6; cursor:pointer; transition:background 0.15s; position:relative; }
1033
- .group-item:hover { background:#f5f7fa; }
1034
- .group-item.active { background:#eff6ff; border-left:3px solid var(--primary); padding-left:11px; }
1035
- .group-item-name { font-size:13px; font-weight:600; color:var(--t1); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
1036
- .group-item-meta { font-size:10px; color:var(--t2); margin-top:2px; }
1037
- .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; }
1038
- .group-item:hover .group-item-del { display:block; }
1039
- .group-item-del:hover { color:#dc3545; }
1040
- .group-actions { padding:8px 14px; display:flex; gap:6px; flex-shrink:0; border-bottom:1px solid var(--border); }
1041
- .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; }
1042
- .group-actions .gbtn:hover { background:#f1f5f9; border-color:var(--primary); color:var(--primary); }
1043
- .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; }
1044
- .group-info-bar .copy-link { cursor:pointer; text-decoration:underline; }
1045
- .group-info-bar .copy-link:hover { color:#0284c7; }
1046
- </style>
1047
- <!-- CHATHTML_STYLE_END -->
1048
- </head>
1049
- <body>
1050
- <div id="app">
1051
- <div class="sidebar" id="sidebar">
1052
- <div class="sidebar-header">
1053
- <div class="header-top">
1054
- <span class="my-aid" id="myAid">Loading...</span>
1055
- <button class="collapse-btn" onclick="toggleSidebar()" title="收起面板">
1056
- <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>
1057
- </button>
1058
- </div>
1059
- <div class="tab-bar">
1060
- <div class="tab active" id="tabP2P" onclick="switchTab('p2p')">聊天</div>
1061
- <div class="tab" id="tabGroup" onclick="switchTab('group')">群组</div>
1062
- </div>
1063
- </div>
1064
- <!-- P2P panel -->
1065
- <div id="p2pPanel">
1066
- <div style="padding:8px 14px;flex-shrink:0;"><button class="new-chat-btn" onclick="showModal()">+ 连接龙虾</button></div>
1067
- <div class="session-list" id="sessionList"></div>
1068
- </div>
1069
- <!-- Group panel -->
1070
- <div id="groupPanel" style="display:none;flex:1;display:none;flex-direction:column;overflow:hidden;">
1071
- <div class="group-actions">
1072
- <div class="gbtn" onclick="showCreateGroupModal()">创建群组</div>
1073
- <div class="gbtn" onclick="showJoinGroupModal()">加入群组</div>
1074
- <div class="gbtn" onclick="showMyGroups()">我的群</div>
1075
- </div>
1076
- <div class="group-list" id="groupList"><div style="padding:20px;text-align:center;color:#999;font-size:12px;">暂无群组</div></div>
1077
- </div>
1078
- </div>
1079
- <div class="chat-area">
1080
- <div class="chat-header">
1081
- <div class="header-left">
1082
- <button class="toggle-sidebar-btn" onclick="toggleSidebar()">
1083
- <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>
1084
- </button>
1085
- <div class="status-dot" id="statusDot"></div>
1086
- <div class="chat-title" id="chatTitle">未选择会话</div>
1087
- </div>
1088
- <div class="aid-select-wrap">
1089
- <a href="https://agentunion.net" target="_blank" class="manage-btn" title="AgentUnion排行榜">AgentUnion排行榜</a>
1090
- <a href="https://github.com/auliwenjiang/agentcp" target="_blank" class="manage-btn" title="ACP 开源GitHub">ACP 开源GitHub</a>
1091
- <a href="/" class="manage-btn" title="ACP 身份管理">
1092
- <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> 身份管理
1093
- </a>
1094
- <div class="aid-control-group">
1095
- <select class="aid-select" id="aidSelect" onchange="switchAid(this.value)"></select>
1096
- <div class="status-toggle" id="aidStatusToggle" onclick="toggleOnline()" title="点击切换在线状态">
1097
- <div class="status-indicator" id="aidOnlineDot"></div>
1098
- <span id="aidStatusText" style="color:var(--t2);">...</span>
1099
- </div>
1100
- </div>
1101
- </div>
1102
- </div>
1103
- <div class="encrypt-banner" id="encryptBanner">
1104
- <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>
1105
- <span>ACP Agent 点对点加密通信 — 消息经端到端加密传输,仅通信双方可读</span>
1106
- </div>
1107
- <div class="group-info-bar" id="groupInfoBar" style="display:none;">
1108
- <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>
1109
- <span id="groupInfoText">群组</span>
1110
- <span class="copy-link" id="groupInviteBtn" onclick="generateInviteLink()" title="生成邀请链接" style="display:none;">生成邀请链接</span>
1111
- <span class="copy-link" id="groupCopyLinkBtn" onclick="copyGroupLink()" title="复制群链接" style="display:none;">复制群链接</span>
1112
- <span class="copy-link" onclick="showGroupMembers()" title="查看成员">成员</span>
1113
- <span class="copy-link" id="groupReviewBtn" onclick="showPendingRequests()" title="查看入群申请" style="display:none;">审核</span>
1114
- </div>
1115
- <div class="messages" id="messages">
1116
- <div style="text-align:center;color:var(--t2);margin-top:40px;">
1117
- <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>
1118
- <div style="font-size:14px;font-weight:500;color:#64748b;margin-bottom:4px;">ACP Agent 安全通信</div>
1119
- <div style="font-size:12px;color:#94a3b8;">选择或创建一个会话,开始点对点加密聊天</div>
1120
- </div>
1121
- <div class="new-msg-tip" id="newMsgTip" onclick="scrollToBottom()">↓ 有新消息</div>
1122
- </div>
1123
- <div class="input-area">
1124
- <input type="text" id="messageInput" placeholder="输入消息..." onkeypress="if(event.key==='Enter')sendMessage()">
1125
- <button class="send-btn" id="sendBtn" onclick="sendMessage()">
1126
- <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>
1127
- </button>
1128
- </div>
1129
- </div>
1130
- </div>
1131
- <div class="modal-overlay" id="modal">
1132
- <div class="modal">
1133
- <h3>连接 ACP 龙虾</h3>
1134
- <input type="text" id="targetAidInput" placeholder="输入对方 AID" onkeypress="if(event.key==='Enter')doConnect()">
1135
- <div class="modal-btns">
1136
- <button class="mbtn mbtn-cancel" onclick="hideModal()">取消</button>
1137
- <button class="mbtn mbtn-ok" id="connectBtn" onclick="doConnect()">连接</button>
1138
- </div>
1139
- </div>
1140
- </div>
1141
- <div class="modal-overlay" id="createGroupModal">
1142
- <div class="modal">
1143
- <h3>创建群组</h3>
1144
- <input type="text" id="groupNameInput" placeholder="输入群组名称">
1145
- <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>
1146
- <div style="margin-bottom:16px;">
1147
- <label style="font-size:13px;color:var(--t2);margin-bottom:8px;display:block;">群组类型</label>
1148
- <div style="display:flex;gap:12px;">
1149
- <label style="display:flex;align-items:center;gap:6px;cursor:pointer;font-size:14px;"><input type="radio" name="groupVisibility" value="public" checked> 公开群</label>
1150
- <label style="display:flex;align-items:center;gap:6px;cursor:pointer;font-size:14px;"><input type="radio" name="groupVisibility" value="private"> 私密群</label>
1151
- </div>
1152
- </div>
1153
- <div class="modal-btns">
1154
- <button class="mbtn mbtn-cancel" onclick="hideCreateGroupModal()">取消</button>
1155
- <button class="mbtn mbtn-ok" id="createGroupBtn" onclick="doCreateGroup()">创建</button>
1156
- </div>
1157
- </div>
1158
- </div>
1159
- <div class="modal-overlay" id="joinGroupModal">
1160
- <div class="modal">
1161
- <h3>加入群组</h3>
1162
- <input type="text" id="joinGroupUrlInput" placeholder="输入群聊链接或邀请链接" onkeypress="if(event.key==='Enter')doJoinGroup()">
1163
- <div style="font-size:11px;color:var(--t2);margin:-8px 0 12px 2px;">粘贴邀请链接可直接加入,普通群链接将发送入群申请</div>
1164
- <div class="modal-btns">
1165
- <button class="mbtn mbtn-cancel" onclick="hideJoinGroupModal()">取消</button>
1166
- <button class="mbtn mbtn-ok" id="joinGroupBtn" onclick="doJoinGroup()">加入</button>
1167
- </div>
1168
- </div>
1169
- </div>
1170
- <div class="modal-overlay" id="membersModal">
1171
- <div class="modal" style="max-width:520px;">
1172
- <h3>群组成员</h3>
1173
- <div id="membersList" style="max-height:400px;overflow-y:auto;margin-bottom:16px;font-size:13px;"></div>
1174
- <div class="modal-btns">
1175
- <button class="mbtn mbtn-cancel" onclick="hideMembersModal()">关闭</button>
1176
- </div>
1177
- </div>
1178
- </div>
1179
- <div class="modal-overlay" id="pendingRequestsModal">
1180
- <div class="modal" style="max-width:480px;">
1181
- <h3>入群申请</h3>
1182
- <div id="pendingRequestsList" style="max-height:360px;overflow-y:auto;margin-bottom:16px;font-size:13px;"></div>
1183
- <div class="modal-btns">
1184
- <button class="mbtn mbtn-cancel" onclick="hidePendingRequestsModal()">关闭</button>
1185
- </div>
1186
- </div>
1187
- </div>
1188
- <div class="modal-overlay" id="myGroupsModal">
1189
- <div class="modal" style="max-width:560px;">
1190
- <h3>我的群</h3>
1191
- <div id="myGroupsContent" style="max-height:420px;overflow-y:auto;margin-bottom:16px;font-size:13px;"></div>
1192
- <div class="modal-btns">
1193
- <button class="mbtn mbtn-cancel" onclick="hideMyGroupsModal()">关闭</button>
1194
- </div>
1195
- </div>
1196
- </div>
1197
- <script>
1198
- 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 };
1199
- var D = {};
1200
- var agentInfoCache = {};
1201
- function $(id){ return document.getElementById(id); }
1202
- function getAvatarSrc(type) {
1203
- if (type === 'openclaw') return '/assets/openclaw.png';
1204
- if (type === 'human') return '/assets/human.png';
1205
- return '/assets/agent.png';
1206
- }
1207
- async function fetchAgentInfo(aid) {
1208
- if (agentInfoCache[aid]) return agentInfoCache[aid];
1209
- try {
1210
- var r = await fetch('/api/agent-info?aid=' + encodeURIComponent(aid));
1211
- var d = await r.json();
1212
- if (d.type || d.name) { agentInfoCache[aid] = d; }
1213
- return d;
1214
- } catch(e) { return { type:'', name:'', description:'', tags:[] }; }
1215
- }
1216
- async function deleteSession(e, sessionId){
1217
- e.stopPropagation();
1218
- if(!confirm('确认删除该会话?')) return;
1219
- try {
1220
- var r = await fetch('/api/sessions/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionId: sessionId, aid: S.aid }) });
1221
- var d = await r.json();
1222
- if(d.success){
1223
- if(S.sid === sessionId){ S.sid = null; S.sessionId=null; D.title.textContent='未选择会话'; D.msgs.innerHTML=''; D.input.disabled=false; }
1224
- D.sList.dataset.s=''; // force update
1225
- loadSessions();
1226
- } else { alert(d.error || '删除失败'); }
1227
- } catch(err){ alert('删除失败: ' + err.message); }
1228
- }
1229
-
1230
- async function deletePeer(e, peerAid){
1231
- e.stopPropagation();
1232
- if(!confirm('确认删除与 ' + peerAid + ' 的所有会话?')) return;
1233
- try {
1234
- var r = await fetch('/api/peers/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ peerAid: peerAid, aid: S.aid }) });
1235
- var d = await r.json();
1236
- if(d.success){
1237
- S.sid = null; S.sessionId=null; D.title.textContent='未选择会话'; D.msgs.innerHTML='';
1238
- D.sList.dataset.s=''; // force update
1239
- loadSessions();
1240
- } else { alert(d.error || '删除失败'); }
1241
- } catch(err){ alert('删除失败: ' + err.message); }
1242
- }
1243
-
1244
- 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'); }
1245
-
1246
- function isAtBottom(){ return D.msgs.scrollHeight-D.msgs.scrollTop-D.msgs.clientHeight<150; }
1247
- function scrollToBottom(){ D.msgs.scrollTop=D.msgs.scrollHeight; hideNewMsgTip(); }
1248
- function showNewMsgTip(){ if(D.newMsgTip) D.newMsgTip.style.display='block'; }
1249
- function hideNewMsgTip(){ if(D.newMsgTip) D.newMsgTip.style.display='none'; }
1250
-
1251
- async function init(){
1252
- initDom();
1253
- // 监听滚动,用户滚到底部时自动隐藏新消息提示
1254
- D.msgs.addEventListener('scroll',function(){ if(isAtBottom()) hideNewMsgTip(); });
1255
- try {
1256
- var r = await fetch('/api/aid'); var d = await r.json();
1257
- S.aidList=d.aidStatus||[];
1258
- if(S.aidList.length){
1259
- // 优先从当前标签页恢复,再 fallback 到全局默认
1260
- var saved=sessionStorage.getItem('selectedAid')||localStorage.getItem('selectedAid');
1261
- var found=saved&&S.aidList.find(function(a){ return a.aid===saved; });
1262
- S.aid=(found?saved:S.aidList[0].aid)||'';
1263
- if(S.aid) sessionStorage.setItem('selectedAid',S.aid);
1264
- }
1265
- if(S.aid){
1266
- D.myAid.textContent='我的身份: '+S.aid; D.myAid.title=S.aid;
1267
- renderAidSelect();
1268
- connectGroupWs();
1269
- fetch('/api/ws/status?aid='+encodeURIComponent(S.aid)).then(function(r){return r.json();}).then(function(d){updateDot(d.status);}).catch(function(){});
1270
- loadSessions();
1271
- } else { window.location.href='/'; }
1272
- } catch(e){ console.error(e); }
1273
- }
1274
-
1275
- function renderAidSelect(){
1276
- var html='';
1277
- var curOnline=false;
1278
- S.aidList.forEach(function(a){
1279
- var sel=a.aid===S.aid?' selected':'';
1280
- if(a.aid===S.aid) curOnline=a.online;
1281
- html+='<option value="'+escH(a.aid)+'"'+sel+'>'+escH(a.aid)+'</option>';
1282
- });
1283
- D.aidSel.innerHTML=html;
1284
- D.aidDot.className='status-indicator '+(curOnline?'online':'offline');
1285
- D.aidStatusText.textContent=curOnline?'已上线':'离线';
1286
- D.aidStatusText.style.color=curOnline?'#10b981':'#64748b';
1287
- D.aidStatusToggle.title=curOnline?'点击下线':'点击上线';
1288
- }
1289
-
1290
- async function switchAid(aid){
1291
- if(aid===S.aid) return;
1292
- S.aid=aid;
1293
- S.sid=null; S.sessionId=null;
1294
- _groupInited=false; // 切换 aid 后需重新初始化群组客户端
1295
- localStorage.setItem('selectedAid',aid);
1296
- sessionStorage.setItem('selectedAid',aid);
1297
- D.myAid.textContent='我的身份: '+aid; D.myAid.title=aid;
1298
- renderAidSelect();
1299
- // 通知服务端本标签页绑定的 aid(WS 已连接则立即发,否则 onopen 会发)
1300
- if(_groupWs&&_groupWs.readyState===WebSocket.OPEN){
1301
- _groupWs.send(JSON.stringify({type:'bind_aid',aid:aid}));
1302
- }
1303
- // 检查是否在线,不在线则自动上线
1304
- var info=S.aidList.find(function(a){ return a.aid===aid; });
1305
- if(!info||!info.online){
1306
- try {
1307
- await fetch('/api/ws/start',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({aid:aid})});
1308
- } catch(e){}
1309
- }
1310
- D.msgs.innerHTML=''; D.title.textContent='未选择会话';
1311
- D.sList.dataset.s='';
1312
- fetch('/api/ws/status?aid='+encodeURIComponent(aid)).then(function(r){return r.json();}).then(function(d){updateDot(d.status);}).catch(function(){});
1313
- loadSessions();
1314
- }
1315
-
1316
- async function toggleOnline(){
1317
- var info=S.aidList.find(function(a){ return a.aid===S.aid; });
1318
- var isOnline=info&&info.online;
1319
- D.aidStatusText.textContent='...';
1320
- try {
1321
- if(isOnline){
1322
- await fetch('/api/aid/offline',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({aid:S.aid})});
1323
- } else {
1324
- await fetch('/api/ws/start',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({aid:S.aid})});
1325
- }
1326
- // AID 状态变更通过 WS 推送 aid_status 自动更新,无需再拉取
1327
- } catch(e){}
1328
- }
1329
-
1330
- async function loadSessions(){
1331
- if(!S.aid) return;
1332
- try {
1333
- var r=await fetch('/api/sessions?aid='+encodeURIComponent(S.aid));
1334
- var d=await r.json();
1335
- if(d.sessions) updateSessions(d.sessions, S.sid);
1336
- } catch(e){}
1337
- }
1338
-
1339
- async function loadMessages(){
1340
- if(!S.aid||!S.sid||S.tab!=='p2p') return;
1341
- try {
1342
- var r=await fetch('/api/messages?aid='+encodeURIComponent(S.aid)+'&sessionId='+encodeURIComponent(S.sid));
1343
- var d=await r.json();
1344
- S.closed=d.closed||false;
1345
- D.msgs.dataset.s='';
1346
- if(d.messages) renderMsgs(d.messages, S.closed);
1347
- } catch(e){}
1348
- }
1349
-
1350
- // legacy poll kept for compatibility (no-op, replaced by WS push)
1351
- function poll(){}
1352
-
1353
- function updateSessions(sessions, activeId){
1354
- var sig=JSON.stringify(sessions)+activeId+S.sid;
1355
- if(D.sList.dataset.s===sig) return;
1356
- D.sList.dataset.s=sig;
1357
- if(activeId && S.sid!==activeId) S.sid=activeId;
1358
- S.sessions=sessions;
1359
-
1360
- var groups={};
1361
- sessions.forEach(function(s){
1362
- var peer=s.peerAid||'unknown';
1363
- if(!groups[peer]) groups[peer]=[];
1364
- groups[peer].push(s);
1365
- });
1366
-
1367
- if(!sessions.length){
1368
- D.sList.innerHTML='<div style="padding:20px;text-align:center;color:#999;font-size:12px;">暂无会话</div>';
1369
- return;
1370
- }
1371
-
1372
- var html='';
1373
- var peers=Object.keys(groups);
1374
- peers.sort(function(a,b){
1375
- var la=groups[a][0].lastMessageAt, lb=groups[b][0].lastMessageAt;
1376
- return lb-la;
1377
- });
1378
- peers.forEach(function(peer){
1379
- var isOpen = S.expanded[peer] !== false;
1380
- var list=groups[peer];
1381
- var shortPeer=peer.length>22?peer.substring(0,22)+'...':peer;
1382
- var cached=agentInfoCache[peer];
1383
- var avatarType=cached?cached.type:'';
1384
- var avatarSrc=getAvatarSrc(avatarType);
1385
- var displayName=(cached&&cached.name)?cached.name:shortPeer;
1386
- var fullDisplayName=(cached&&cached.name)?cached.name:peer;
1387
- var descText=(cached&&cached.description)?cached.description:peer;
1388
- html+='<div class="aid-group">';
1389
- 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>';
1390
- html+='<div class="aid-group-sessions'+(isOpen?' open':'')+'">';
1391
- list.forEach(function(s){
1392
- var active=s.sessionId===S.sid;
1393
- var time=fmtTime(s.lastMessageAt);
1394
- var tc=s.type==='outgoing'?'outgoing':'incoming';
1395
- var tt=s.type==='outgoing'?'OUT':'IN';
1396
- var name=s.lastMessage||'';
1397
- var fullName=name;
1398
- if(name.length>20) name=name.substring(0,20)+'...';
1399
- if(!name) name='(空会话)';
1400
- var closedTag=s.closed?'<span style="color:#dc3545;font-size:10px;margin-left:4px;">[已关闭]</span>':'';
1401
- 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>';
1402
- });
1403
- html+='</div></div>';
1404
- });
1405
- D.sList.innerHTML=html;
1406
-
1407
- // 异步加载未缓存的 agent info 并更新头像和名称
1408
- peers.forEach(function(peer){
1409
- if(!agentInfoCache[peer]){
1410
- fetchAgentInfo(peer).then(function(info){
1411
- var safeId=peer.replace(/\\./g,'_');
1412
- var el=document.getElementById('avatar_'+safeId);
1413
- if(el) el.src=getAvatarSrc(info.type);
1414
- if(info.name){
1415
- var header=el&&el.parentElement;
1416
- if(header){
1417
- var titleEl=header.querySelector('.aid-group-title');
1418
- if(titleEl){ titleEl.textContent=info.name; titleEl.title=info.name; }
1419
- }
1420
- }
1421
- if(info.description){
1422
- var descEl=document.getElementById('desc_'+safeId);
1423
- if(descEl){ descEl.textContent=info.description; descEl.title=info.description; }
1424
- }
1425
- });
1426
- }
1427
- });
1428
- }
1429
-
1430
- function toggleGroup(owner){
1431
- S.expanded[owner] = S.expanded[owner]===false ? true : false;
1432
- D.sList.dataset.s=''; // force re-render
1433
- updateSessions(S.sessions, S.sid);
1434
- }
1435
-
1436
- function renderMsgs(msgs, closed){
1437
- var sig=msgs.length+(msgs.length>0?msgs[msgs.length-1].timestamp:0)+(closed?'c':'');
1438
- // 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)
1439
- // Actually, let's allow re-render if we call it.
1440
- if(D.msgs.dataset.s==sig && !D.msgs.dataset.force) return;
1441
- D.msgs.dataset.s=sig;
1442
- D.msgs.dataset.force=''; // clear force flag
1443
-
1444
- if(!msgs.length){
1445
- D.msgs.innerHTML='<div style="text-align:center;color:#ccc;margin-top:20px;font-size:12px;">暂无消息</div>';
1446
- D.input.disabled=false; D.input.placeholder='输入消息...';
1447
- return;
1448
- }
1449
- var html=msgs.map(function(m){
1450
- var sent=m.type==='sent';
1451
- var sender = sent ? S.aid : (m.from || 'unknown');
1452
- var info = agentInfoCache[sender];
1453
- if(!info){
1454
- fetchAgentInfo(sender).then(function(){
1455
- if(D.msgs.dataset.s===sig){ D.msgs.dataset.force='1'; renderMsgs(msgs, closed); }
1456
- });
1457
- }
1458
- var avatarSrc = getAvatarSrc(info ? info.type : '');
1459
- var t=fmtTime(m.timestamp);
1460
- var c=(typeof marked!=='undefined'&&marked.parse)?marked.parse(m.content):escH(m.content);
1461
- var name = (info && info.name) ? info.name : sender;
1462
-
1463
- return '<div class="message '+m.type+'">' +
1464
- '<img class="msg-avatar" src="'+avatarSrc+'" title="'+escH(name)+'">' +
1465
- '<div class="msg-content">' +
1466
- '<div class="msg-meta">'+(sent?'我':escH(name))+' · '+t+'</div>' +
1467
- '<div class="bubble">'+c+'</div>' +
1468
- '</div></div>';
1469
- }).join('');
1470
- if(closed){
1471
- 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>';
1472
- D.input.disabled=true; D.input.placeholder='会话已关闭,请新建会话';
1473
- } else {
1474
- D.input.disabled=false; D.input.placeholder='输入消息...';
1475
- }
1476
- var wasAtBottom=isAtBottom();
1477
- var prevScrollTop=D.msgs.scrollTop;
1478
- D.msgs.innerHTML=html+'<div class="new-msg-tip" id="newMsgTip" onclick="scrollToBottom()" style="display:none;">↓ 有新消息</div>';
1479
- D.newMsgTip=$('newMsgTip');
1480
- // 不自动滚动,保持用户当前位置;有新消息时显示提示
1481
- if(!wasAtBottom&&msgs.length>0){
1482
- D.msgs.scrollTop=prevScrollTop;
1483
- if(D.msgs.dataset.force!=='avatar') showNewMsgTip();
1484
- } else {
1485
- D.msgs.scrollTop=prevScrollTop;
1486
- }
1487
- }
1488
-
1489
- function updateDot(st){
1490
- S.status=st;
1491
- D.dot.className='status-dot '+(st||'');
1492
- }
1493
-
1494
- async function pickSession(sid,peer){
1495
- if(S.tab!=='p2p') switchTab('p2p');
1496
- S.sid=sid; S.sessionId=sid;
1497
- hideNewMsgTip();
1498
- D.title.textContent=peer;
1499
- try {
1500
- await fetch('/api/sessions/active',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sessionId:sid,aid:S.aid})});
1501
- // 通知服务端本标签页的 activeSessionId
1502
- if(_groupWs&&_groupWs.readyState===WebSocket.OPEN){
1503
- _groupWs.send(JSON.stringify({type:'set_active_session',sessionId:sid}));
1504
- }
1505
- var r=await fetch('/api/messages?aid='+encodeURIComponent(S.aid)+'&sessionId='+encodeURIComponent(sid));
1506
- var d=await r.json();
1507
- S.closed=d.closed||false;
1508
- D.msgs.dataset.s=''; // force
1509
- renderMsgs(d.messages||[], S.closed);
1510
- scrollToBottom();
1511
- // 刷新会话列表,确保新会话出现在侧边栏
1512
- loadSessions();
1513
- } catch(e){}
1514
- }
1515
-
1516
- async function sendMessage(){
1517
- var txt=D.input.value.trim();
1518
- if(!txt){ return; }
1519
- // 用户主动发送消息,确保滚动到底部
1520
- hideNewMsgTip();
1521
- // 群组模式
1522
- if(S.tab==='group'){
1523
- if(!S.activeGroupId){ alert('请先选择一个群组'); return; }
1524
- try {
1525
- D.input.value='';
1526
- 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})});
1527
- var d=await r.json();
1528
- if(!d.success) alert(d.error||'发送失败');
1529
- else {
1530
- // 发送成功:立即追加到本地显示(服务端已存储,不用等 WS 推送)
1531
- if(d.msg_id){
1532
- var sentMsg={msg_id:d.msg_id,sender:S.aid,content:txt,content_type:'text',timestamp:d.timestamp||Date.now()};
1533
- var exists=_lastGroupMsgs.some(function(m){ return m.msg_id===sentMsg.msg_id; });
1534
- if(!exists){
1535
- _lastGroupMsgs.push(sentMsg);
1536
- _lastGroupMsgSig='';
1537
- renderGroupMsgs(_lastGroupMsgs);
1538
- scrollToBottom();
1539
- }
1540
- }
1541
- }
1542
- } catch(e){ alert('发送失败'); }
1543
- return;
1544
- }
1545
- // P2P 模式
1546
- if(!S.sid){ alert('请先选择或新建一个会话'); return; }
1547
- if(S.closed){ alert('该会话已关闭,请新建会话继续通信'); return; }
1548
- try {
1549
- D.input.value='';
1550
- 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})});
1551
- var d=await r.json();
1552
- if(!d.success) alert(d.error||'发送失败');
1553
- else { await loadMessages(); scrollToBottom(); }
1554
- } catch(e){ alert('发送失败'); }
1555
- }
1556
-
1557
- function toggleSidebar(){
1558
- S.sidebarOpen=!S.sidebarOpen;
1559
- D.sidebar.classList.toggle('collapsed',!S.sidebarOpen);
1560
- }
1561
-
1562
- function showModal(){ D.modal.classList.add('show'); D.tInput.value=''; D.tInput.focus(); }
1563
- function hideModal(){ D.modal.classList.remove('show'); }
1564
-
1565
- async function newSessionWith(peerAid){
1566
- try {
1567
- var r=await fetch('/api/ws/connect',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({targetAid:peerAid,aid:S.aid})});
1568
- var d=await r.json();
1569
- if(d.success){ pickSession(d.sessionId,peerAid); }
1570
- else { alert(d.error||'连接失败'); }
1571
- } catch(e){ alert('错误: '+e.message); }
1572
- }
1573
-
1574
- async function doConnect(){
1575
- var aid=D.tInput.value.trim();
1576
- if(!aid) return;
1577
- D.cBtn.disabled=true; D.cBtn.textContent='连接中...';
1578
- try {
1579
- var r=await fetch('/api/ws/connect',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({targetAid:aid,aid:S.aid})});
1580
- var d=await r.json();
1581
- if(d.success){ hideModal(); pickSession(d.sessionId,aid); }
1582
- else { alert(d.error||'连接失败'); }
1583
- } catch(e){ alert('错误: '+e.message); }
1584
- finally { D.cBtn.disabled=false; D.cBtn.textContent='连接'; }
1585
- }
1586
-
1587
- function escH(t){ var d=document.createElement('div'); d.textContent=t; return d.innerHTML; }
1588
- function escA(t){ return t.replace(/\\\\/g,'\\\\\\\\').replace(/'/g,"\\\\'"); }
1589
- function fmtTime(ts){
1590
- if(!ts) return '';
1591
- var n=Number(ts);
1592
- if(isNaN(n)) return '';
1593
- if(n<1e12) n=n*1000;
1594
- var d=new Date(n);
1595
- if(isNaN(d.getTime())) return '';
1596
- var now=new Date();
1597
- var pad=function(v){ return v<10?'0'+v:''+v; };
1598
- var H=pad(d.getHours()), M=pad(d.getMinutes());
1599
- if(d.getFullYear()===now.getFullYear()&&d.getMonth()===now.getMonth()&&d.getDate()===now.getDate()){
1600
- return H+':'+M;
1601
- }
1602
- // 今年内省略年份
1603
- if(d.getFullYear()===now.getFullYear()){
1604
- return pad(d.getMonth()+1)+'/'+pad(d.getDate())+' '+H+':'+M;
1605
- }
1606
- return d.getFullYear()+'/'+pad(d.getMonth()+1)+'/'+pad(d.getDate())+' '+H+':'+M;
1607
- }
1608
-
1609
- // ============================================================
1610
- // Group Functions
1611
- // ============================================================
1612
-
1613
- function switchTab(tab){
1614
- S.tab=tab;
1615
- D.tabP2P.className='tab'+(tab==='p2p'?' active':'');
1616
- D.tabGroup.className='tab'+(tab==='group'?' active':'');
1617
- D.p2pPanel.style.display=tab==='p2p'?'block':'none';
1618
- D.groupPanel.style.display=tab==='group'?'flex':'none';
1619
- if(tab==='group'){
1620
- D.encryptBanner.style.display='none';
1621
- D.groupInfoBar.style.display=S.activeGroupId?'flex':'none';
1622
- D.input.placeholder='输入群消息...';
1623
- D.input.disabled=!S.activeGroupId;
1624
- D.msgs.dataset.s='';
1625
- _lastGroupMsgSig='';
1626
- initGroupClient();
1627
- pollGroupList();
1628
- if(S.activeGroupId) pollGroupMessages().then(function(){ scrollToBottom(); });
1629
- else { D.msgs.innerHTML='<div style="text-align:center;color:#ccc;margin-top:20px;font-size:12px;">选择或创建一个群组</div>'; }
1630
- } else {
1631
- D.encryptBanner.style.display='flex';
1632
- D.groupInfoBar.style.display='none';
1633
- D.input.placeholder='输入消息...';
1634
- D.input.disabled=false;
1635
- _lastGroupMsgSig='';
1636
- // 立即清空消息区域,防止群消息残留
1637
- D.msgs.innerHTML='';
1638
- // 切回P2P时立即刷新会话列表和消息
1639
- D.sList.dataset.s='';
1640
- D.msgs.dataset.s='';
1641
- loadSessions();
1642
- if(S.sid){
1643
- fetch('/api/messages?aid='+encodeURIComponent(S.aid)+'&sessionId='+encodeURIComponent(S.sid)).then(function(r){ return r.json(); }).then(function(d){
1644
- if(S.tab!=='p2p') return;
1645
- S.closed=d.closed||false;
1646
- if(d.messages) renderMsgs(d.messages, S.closed);
1647
- scrollToBottom();
1648
- }).catch(function(){});
1649
- }
1650
- }
1651
- }
1652
-
1653
- var _groupInited=false;
1654
- async function initGroupClient(){
1655
- if(_groupInited) return;
1656
- try {
1657
- var r=await fetch('/api/group/init',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({aid:S.aid})});
1658
- var d=await r.json();
1659
- if(d.success){ _groupInited=true; if(d.targetAid) S.groupTargetAid=d.targetAid; }
1660
- } catch(e){ console.error('群组初始化失败',e); }
1661
- }
1662
-
1663
- async function pollGroupList(){
1664
- try {
1665
- var r=await fetch('/api/group/list?aid='+encodeURIComponent(S.aid));
1666
- var d=await r.json();
1667
- if(d.groups){ S.groups=d.groups; renderGroupList(); }
1668
- } catch(e){}
1669
- }
1670
-
1671
- function renderGroupList(){
1672
- if(!S.groups.length){
1673
- D.groupList.innerHTML='<div style="padding:20px;text-align:center;color:#999;font-size:12px;">暂无群组</div>';
1674
- return;
1675
- }
1676
- var html='';
1677
- S.groups.forEach(function(g){
1678
- var active=g.group_id===S.activeGroupId;
1679
- 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>';
1680
- });
1681
- D.groupList.innerHTML=html;
1682
- }
1683
-
1684
- async function pickGroup(groupId,name){
1685
- S.activeGroupId=groupId;
1686
- S.isGroupCreator=false;
1687
- _lastGroupMsgSig='';
1688
- hideNewMsgTip();
1689
- D.title.textContent=name;
1690
- D.groupInfoBar.style.display='flex';
1691
- D.groupInfoText.textContent=name;
1692
- D.input.disabled=false;
1693
- D.input.placeholder='输入群消息...';
1694
- D.input.focus();
1695
- // 默认隐藏创建者相关按钮
1696
- $('groupInviteBtn').style.display='none';
1697
- $('groupCopyLinkBtn').style.display='none';
1698
- $('groupReviewBtn').style.display='none';
1699
- renderGroupList();
1700
- try {
1701
- await fetch('/api/group/select',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({groupId:groupId,aid:S.aid})});
1702
- } catch(e){}
1703
- // 获取群信息判断是否为创建者
1704
- try {
1705
- var r=await fetch('/api/group/info?groupId='+encodeURIComponent(groupId)+'&aid='+encodeURIComponent(S.aid));
1706
- var d=await r.json();
1707
- if(d.creator&&d.creator===S.aid){
1708
- S.isGroupCreator=true;
1709
- $('groupInviteBtn').style.display='';
1710
- $('groupReviewBtn').style.display='';
1711
- } else {
1712
- $('groupCopyLinkBtn').style.display='';
1713
- }
1714
- } catch(e){
1715
- // 获取失败时默认显示复制群链接
1716
- $('groupCopyLinkBtn').style.display='';
1717
- }
1718
- pollGroupMessages().then(function(){ scrollToBottom(); });
1719
- }
1720
-
1721
- var _lastGroupMsgSig='';
1722
- async function pollGroupMessages(){
1723
- if(!S.activeGroupId||S.tab!=='group') return;
1724
- try {
1725
- var r=await fetch('/api/group/messages?groupId='+encodeURIComponent(S.activeGroupId)+'&aid='+encodeURIComponent(S.aid));
1726
- var d=await r.json();
1727
- if(S.tab==='group'&&d.messages) renderGroupMsgs(d.messages);
1728
- } catch(e){}
1729
- }
1730
-
1731
- // ============================================================
1732
- // WebSocket: real-time group message push
1733
- // ============================================================
1734
- var _groupWs=null;
1735
- var _groupWsReconnectTimer=null;
1736
- var _groupWsReconnectDelay=1000; // exponential backoff start
1737
- var _groupWsPingTimer=null;
1738
-
1739
- function connectGroupWs(){
1740
- if(_groupWs&&(_groupWs.readyState===WebSocket.OPEN||_groupWs.readyState===WebSocket.CONNECTING)) return;
1741
- var proto=location.protocol==='https:'?'wss:':'ws:';
1742
- _groupWs=new WebSocket(proto+'//'+location.host+'/ws/ui');
1743
- _groupWs.onopen=function(){
1744
- console.log('[WS] ui connected');
1745
- _groupWsReconnectDelay=1000; // reset backoff on success
1746
- if(_groupWsReconnectTimer){ clearTimeout(_groupWsReconnectTimer); _groupWsReconnectTimer=null; }
1747
- // 绑定当前 aid
1748
- if(S.aid) _groupWs.send(JSON.stringify({type:'bind_aid',aid:S.aid}));
1749
- // 重连后主动拉取最新状态
1750
- 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(){});
1751
- fetch('/api/aid').then(function(r){return r.json();}).then(function(d){if(d.aidStatus){S.aidList=d.aidStatus;renderAidSelect();}}).catch(function(){});
1752
- // 重连后补拉一次会话列表,防止断连期间丢失推送
1753
- loadSessions();
1754
- // 启动 keepalive ping(每 25s 发一次,防止代理/防火墙断连)
1755
- if(_groupWsPingTimer) clearInterval(_groupWsPingTimer);
1756
- _groupWsPingTimer=setInterval(function(){
1757
- if(_groupWs&&_groupWs.readyState===WebSocket.OPEN){
1758
- try{ _groupWs.send(JSON.stringify({type:'ping'})); }catch(e){}
1759
- }
1760
- },25000);
1761
- };
1762
- _groupWs.onmessage=function(ev){
1763
- try {
1764
- var data=JSON.parse(ev.data);
1765
- handleGroupWsMessage(data);
1766
- } catch(e){ console.error('[WS] parse error',e); }
1767
- };
1768
- _groupWs.onclose=function(){
1769
- console.log('[WS] ui disconnected, reconnecting in '+_groupWsReconnectDelay+'ms...');
1770
- _groupWs=null;
1771
- if(_groupWsPingTimer){ clearInterval(_groupWsPingTimer); _groupWsPingTimer=null; }
1772
- _groupWsReconnectTimer=setTimeout(function(){
1773
- _groupWsReconnectDelay=Math.min(_groupWsReconnectDelay*2,30000); // cap at 30s
1774
- connectGroupWs();
1775
- },_groupWsReconnectDelay);
1776
- };
1777
- _groupWs.onerror=function(e){
1778
- console.error('[WS] ui error',e);
1779
- // onerror is always followed by onclose, so reconnect is handled there
1780
- };
1781
- }
1782
-
1783
- function handleGroupWsMessage(data){
1784
- if(data.type==='ws_status'){
1785
- if(!data.aid||data.aid===S.aid) updateDot(data.status);
1786
- return;
1787
- }
1788
- if(data.type==='aid_status'){
1789
- S.aidList=data.aidStatus||[];
1790
- renderAidSelect();
1791
- return;
1792
- }
1793
- if(data.type==='p2p_message'){
1794
- // 实时推送的 P2P 消息
1795
- if(S.tab==='p2p' && data.sessionId===S.sid){
1796
- loadMessages();
1797
- }
1798
- return;
1799
- }
1800
- if(data.type==='sessions_updated'){
1801
- loadSessions();
1802
- return;
1803
- }
1804
- if(data.type==='group_message'){
1805
- // 实时推送的完整消息
1806
- var msg=data.message;
1807
- var gid=data.group_id;
1808
- if(gid===S.activeGroupId&&S.tab==='group'){
1809
- // 追加到当前消息列表并重新渲染
1810
- var exists=_lastGroupMsgs.some(function(m){ return m.msg_id===msg.msg_id; });
1811
- if(!exists){
1812
- _lastGroupMsgs.push(msg);
1813
- _lastGroupMsgSig=''; // 强制重新渲染
1814
- renderGroupMsgs(_lastGroupMsgs);
1815
- }
1816
- }
1817
- } else if(data.type==='group_message_batch'){
1818
- // 批量推送的消息列表
1819
- var gid=data.group_id;
1820
- var msgs=data.messages||[];
1821
- if(gid===S.activeGroupId&&S.tab==='group'){
1822
- var changed=false;
1823
- msgs.forEach(function(msg){
1824
- var exists=_lastGroupMsgs.some(function(m){ return m.msg_id===msg.msg_id; });
1825
- if(!exists){
1826
- _lastGroupMsgs.push(msg);
1827
- changed=true;
1828
- }
1829
- });
1830
- if(changed){
1831
- _lastGroupMsgSig=''; // 强制重新渲染
1832
- renderGroupMsgs(_lastGroupMsgs);
1833
- }
1834
- }
1835
- } else if(data.type==='new_message_notify'){
1836
- // 轻量通知:如果是当前活跃群组,拉取最新消息(本地读取,很快)
1837
- if(data.group_id===S.activeGroupId&&S.tab==='group'){
1838
- pollGroupMessages();
1839
- }
1840
- } else if(data.type==='join_approved'||data.type==='group_invite'){
1841
- // 群组变动,刷新群组列表
1842
- pollGroupList();
1843
- }
1844
- }
1845
-
1846
- var _lastGroupMsgs=[];
1847
- function renderGroupMsgs(msgs){
1848
- // 不在群组 tab 时不渲染,防止覆盖 P2P 消息
1849
- if(S.tab!=='group') return;
1850
- var sig=msgs.length+(msgs.length>0?(msgs[msgs.length-1].msg_id||0):'');
1851
- if(_lastGroupMsgSig===sig&&!msgs._forceRender) return;
1852
- var prevCount=_lastGroupMsgs.length;
1853
- _lastGroupMsgSig=sig;
1854
- _lastGroupMsgs=msgs;
1855
- if(!msgs.length){
1856
- 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>';
1857
- D.newMsgTip=$('newMsgTip');
1858
- return;
1859
- }
1860
- var needFetch=[];
1861
- var html=msgs.map(function(m){
1862
- var sent=m.sender===S.aid;
1863
- var sender=m.sender||'unknown';
1864
- var info=agentInfoCache[sender];
1865
- if(!info){ needFetch.push(sender); }
1866
- var avatarSrc=getAvatarSrc(info?info.type:'');
1867
- var t=m.timestamp?fmtTime(m.timestamp):'';
1868
-
1869
- var c=(typeof marked!=='undefined'&&marked.parse)?marked.parse(m.content||''):escH(m.content||'');
1870
- var name=(info&&info.name)?info.name:sender;
1871
- return '<div class="message '+(sent?'sent':'received')+'">' +
1872
- '<img class="msg-avatar" src="'+avatarSrc+'" title="'+escH(name)+'">' +
1873
- '<div class="msg-content">' +
1874
- '<div class="msg-meta">'+(sent?'':escH(name))+' · '+t+'</div>' +
1875
- '<div class="bubble">'+c+'</div>' +
1876
- '</div></div>';
1877
- }).join('');
1878
- var wasAtBottom=isAtBottom();
1879
- var prevScrollTop=D.msgs.scrollTop;
1880
- D.msgs.innerHTML=html+'<div class="new-msg-tip" id="newMsgTip" onclick="scrollToBottom()" style="display:none;">↓ 有新消息</div>';
1881
- D.newMsgTip=$('newMsgTip');
1882
- // 有新消息且用户不在底部:保持位置,显示提示
1883
- if(msgs.length>prevCount&&prevCount>0&&!wasAtBottom){
1884
- D.msgs.scrollTop=prevScrollTop;
1885
- showNewMsgTip();
1886
- } else {
1887
- D.msgs.scrollTop=prevScrollTop;
1888
- }
1889
- // 异步加载未缓存的 agent info,加载完成后重新渲染以更新头像
1890
- var unique=needFetch.filter(function(v,i,a){ return a.indexOf(v)===i; });
1891
- unique.forEach(function(aid){
1892
- fetchAgentInfo(aid).then(function(){
1893
- if(S.tab!=='group') return;
1894
- _lastGroupMsgSig='';
1895
- _lastGroupMsgs._forceRender=true;
1896
- renderGroupMsgs(_lastGroupMsgs);
1897
- });
1898
- });
1899
- }
1900
-
1901
- // Group modals
1902
- function showCreateGroupModal(){ $('createGroupModal').classList.add('show'); $('groupNameInput').value=''; $('groupDescInput').value=''; document.querySelector('input[name="groupVisibility"][value="public"]').checked=true; $('groupNameInput').focus(); }
1903
- function hideCreateGroupModal(){ $('createGroupModal').classList.remove('show'); }
1904
- function showJoinGroupModal(){ $('joinGroupModal').classList.add('show'); $('joinGroupUrlInput').value=''; $('joinGroupUrlInput').focus(); }
1905
- function hideJoinGroupModal(){ $('joinGroupModal').classList.remove('show'); }
1906
- function hideMembersModal(){ $('membersModal').classList.remove('show'); }
1907
-
1908
- async function doCreateGroup(){
1909
- var name=$('groupNameInput').value.trim();
1910
- if(!name) return;
1911
- var description=$('groupDescInput').value.trim();
1912
- if(!description){ $('groupDescInput').focus(); return; }
1913
- var visibility=document.querySelector('input[name="groupVisibility"]:checked').value;
1914
- var btn=$('createGroupBtn');
1915
- btn.disabled=true; btn.textContent='创建中...';
1916
- try {
1917
- var r=await fetch('/api/group/create',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:name,visibility:visibility,description:description||undefined})});
1918
- var d=await r.json();
1919
- if(d.success){
1920
- hideCreateGroupModal();
1921
- pollGroupList();
1922
- pickGroup(d.group_id,name);
1923
- } else { alert(d.error||'创建失败'); }
1924
- } catch(e){ alert('创建失败: '+e.message); }
1925
- finally { btn.disabled=false; btn.textContent='创建'; }
1926
- }
1927
-
1928
- async function doJoinGroup(){
1929
- var rawUrl=$('joinGroupUrlInput').value.trim();
1930
- if(!rawUrl){ alert('请输入群聊链接或邀请链接'); return; }
1931
- // 从 URL 中解析 code 参数
1932
- var code='';
1933
- var groupUrl=rawUrl;
1934
- try {
1935
- var u=new URL(rawUrl);
1936
- code=u.searchParams.get('code')||'';
1937
- u.searchParams.delete('code');
1938
- groupUrl=u.origin+u.pathname;
1939
- } catch(e){}
1940
- var btn=$('joinGroupBtn');
1941
- btn.disabled=true; btn.textContent=code?'加入中...':'申请中...';
1942
- try {
1943
- 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})});
1944
- var d=await r.json();
1945
- if(d.success){
1946
- hideJoinGroupModal();
1947
- pollGroupList();
1948
- if(d.group_id) pickGroup(d.group_id,d.group_id);
1949
- if(d.pending) alert('入群申请已发送,请等待管理员审核');
1950
- } else { alert(d.error||'操作失败'); }
1951
- } catch(e){ alert('操作失败: '+e.message); }
1952
- finally { btn.disabled=false; btn.textContent='加入'; }
1953
- }
1954
-
1955
- async function copyGroupLink(){
1956
- if(!S.activeGroupId) return;
1957
- var groupUrl='https://'+S.groupTargetAid+'/'+S.activeGroupId;
1958
- try { await navigator.clipboard.writeText(groupUrl); alert('群链接已复制到剪贴板\\n\\n'+groupUrl); }
1959
- catch(e){ prompt('请复制群链接:',groupUrl); }
1960
- }
1961
-
1962
- async function generateInviteLink(){
1963
- if(!S.activeGroupId) return;
1964
- try {
1965
- var r=await fetch('/api/group/invite-code',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({groupId:S.activeGroupId,aid:S.aid})});
1966
- var d=await r.json();
1967
- if(d.success&&d.code){
1968
- var baseUrl=d.group_url||('https://'+S.groupTargetAid+'/'+S.activeGroupId);
1969
- var inviteUrl=baseUrl+'?code='+d.code;
1970
- try {
1971
- await navigator.clipboard.writeText(inviteUrl);
1972
- alert('邀请链接已复制到剪贴板\\n\\n'+inviteUrl);
1973
- } catch(e){
1974
- prompt('请手动复制邀请链接:',inviteUrl);
1975
- }
1976
- } else { alert(d.error||'生成邀请码失败'); }
1977
- } catch(e){ alert('生成邀请码失败: '+e.message); }
1978
- }
1979
-
1980
- function copyMemberAid(btn,aid){
1981
- navigator.clipboard.writeText(aid).then(function(){
1982
- btn.textContent='已复制';
1983
- setTimeout(function(){ btn.textContent='复制'; },1200);
1984
- });
1985
- }
1986
-
1987
- async function openAgentMdPage(aid){
1988
- try {
1989
- var r=await fetch('/api/agent-md-raw?aid='+encodeURIComponent(aid));
1990
- var d=await r.json();
1991
- if(!d.success||!d.content){ alert(d.error||'获取 agent.md 失败'); return; }
1992
- var md=d.content;
1993
- // 简单 markdown 渲染
1994
- function renderMd(src){
1995
- var h=escH(src);
1996
- // headings
1997
- h=h.replace(/^######\\s+(.+)$/gm,'<h6>$1</h6>');
1998
- h=h.replace(/^#####\\s+(.+)$/gm,'<h5>$1</h5>');
1999
- h=h.replace(/^####\\s+(.+)$/gm,'<h4>$1</h4>');
2000
- h=h.replace(/^###\\s+(.+)$/gm,'<h3>$1</h3>');
2001
- h=h.replace(/^##\\s+(.+)$/gm,'<h2>$1</h2>');
2002
- h=h.replace(/^#\\s+(.+)$/gm,'<h1>$1</h1>');
2003
- // bold & italic
2004
- h=h.replace(/\\*\\*(.+?)\\*\\*/g,'<strong>$1</strong>');
2005
- h=h.replace(/\\*(.+?)\\*/g,'<em>$1</em>');
2006
- // blockquote
2007
- h=h.replace(/^&gt;\\s?(.+)$/gm,'<blockquote style="border-left:3px solid #ddd;padding-left:12px;color:#666;margin:8px 0;">$1</blockquote>');
2008
- // list items
2009
- h=h.replace(/^-\\s+(.+)$/gm,'<li>$1</li>');
2010
- // code inline
2011
- var bt=String.fromCharCode(96);
2012
- h=h.replace(new RegExp(bt+'([^'+bt+']+)'+bt,'g'),'<code style="background:#f5f5f5;padding:1px 4px;border-radius:3px;font-size:12px;">$1</code>');
2013
- // frontmatter block: hide ---...---
2014
- h=h.replace(/^---[\\s\\S]*?---\\s*/,'');
2015
- // paragraphs
2016
- h=h.replace(/\\n\\n/g,'</p><p>');
2017
- h='<p>'+h+'</p>';
2018
- return h;
2019
- }
2020
- var html='<!DOCTYPE html><html><head><meta charset="utf-8"><title>'+escH(aid)+' - Agent Profile</title>'
2021
- +'<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;}'
2022
- +'h1{border-bottom:2px solid #eee;padding-bottom:8px;}h2{border-bottom:1px solid #eee;padding-bottom:6px;margin-top:24px;}'
2023
- +'ul{padding-left:20px;}li{margin:4px 0;}blockquote{margin:12px 0;}'
2024
- +'pre{background:#f5f5f5;padding:12px;border-radius:6px;overflow-x:auto;}'
2025
- +'.aid-badge{display:inline-block;background:#e8f4fd;color:#0969da;padding:2px 8px;border-radius:10px;font-size:12px;font-family:monospace;margin-bottom:16px;}'
2026
- +'</style></head><body>'
2027
- +'<div class="aid-badge">'+escH(aid)+'</div>'
2028
- +renderMd(md)
2029
- +'</body></html>';
2030
- var w=window.open('','_blank');
2031
- if(w){ w.document.write(html); w.document.close(); }
2032
- else { alert('弹窗被拦截,请允许弹窗后重试'); }
2033
- } catch(e){ alert('获取 agent.md 失败: '+e.message); }
2034
- }
2035
-
2036
- async function showGroupMembers(){
2037
- if(!S.activeGroupId) return;
2038
- try {
2039
- var r=await fetch('/api/group/members?groupId='+encodeURIComponent(S.activeGroupId)+'&aid='+encodeURIComponent(S.aid));
2040
- var d=await r.json();
2041
- if(d.members){
2042
- var html=d.members.map(function(m){
2043
- var aid=m.agent_id||m;
2044
- if(typeof aid!=='string') aid=JSON.stringify(aid);
2045
- var role=m.role||'';
2046
- var cachedInfo=agentInfoCache[aid];
2047
- var avatarSrc=getAvatarSrc(cachedInfo?cachedInfo.type:'');
2048
- var displayName=(cachedInfo&&cachedInfo.name)?cachedInfo.name:aid.split('.')[0];
2049
- var typeTags='';
2050
- if(cachedInfo&&cachedInfo.tags&&cachedInfo.tags.length){
2051
- 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('');
2052
- } else if(cachedInfo&&cachedInfo.type){
2053
- typeTags='<span style="display:inline-block;background:#e8f4fd;color:#0969da;padding:1px 6px;border-radius:8px;font-size:10px;">'+escH(cachedInfo.type)+'</span>';
2054
- }
2055
- 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>'; }
2056
- var safeId='member-'+escH(aid).replace(/\\./g,'_');
2057
- return '<div id="'+safeId+'" style="padding:10px 0;border-bottom:1px solid #f3f4f6;display:flex;align-items:center;gap:10px;">'
2058
- +'<img src="'+avatarSrc+'" style="width:36px;height:36px;border-radius:50%;flex-shrink:0;" class="member-avatar" data-aid="'+escH(aid)+'">'
2059
- +'<div style="flex:1;min-width:0;">'
2060
- +'<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap;">'
2061
- +'<span style="font-size:13px;font-weight:500;" class="member-name" data-aid="'+escH(aid)+'">'+escH(displayName)+'</span>'
2062
- +'<span class="member-tags" data-aid="'+escH(aid)+'">'+typeTags+'</span>'
2063
- +'</div>'
2064
- +'<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>'
2065
- +'</div>'
2066
- +'<div style="display:flex;gap:4px;flex-shrink:0;">'
2067
- +'<button class="mbtn mbtn-ok" style="padding:4px 10px;font-size:11px;" onclick="copyMemberAid(this,\\''+escH(aid)+'\\')">复制</button>'
2068
- +'<button class="mbtn mbtn-cancel" style="padding:4px 10px;font-size:11px;" onclick="openAgentMdPage(\\''+escH(aid)+'\\')">查看</button>'
2069
- +'</div></div>';
2070
- }).join('');
2071
- $('membersList').innerHTML=html||'<div style="color:#999;">暂无成员</div>';
2072
- // 异步加载未缓存的 agent info
2073
- d.members.forEach(function(m){
2074
- var aid=m.agent_id||m;
2075
- if(typeof aid!=='string') aid=JSON.stringify(aid);
2076
- if(!aid||agentInfoCache[aid]) return;
2077
- fetchAgentInfo(aid).then(function(info){
2078
- if(!info||(!info.name&&!info.type)) return;
2079
- var safeId='member-'+aid.replace(/\\./g,'_');
2080
- var el=document.getElementById(safeId);
2081
- if(!el) return;
2082
- var avatarEl=el.querySelector('.member-avatar[data-aid="'+aid+'"]');
2083
- var nameEl=el.querySelector('.member-name[data-aid="'+aid+'"]');
2084
- var tagsEl=el.querySelector('.member-tags[data-aid="'+aid+'"]');
2085
- if(avatarEl) avatarEl.src=getAvatarSrc(info.type);
2086
- if(nameEl) nameEl.textContent=info.name||aid.split('.')[0];
2087
- if(tagsEl){
2088
- var tags='';
2089
- if(info.tags&&info.tags.length){
2090
- 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('');
2091
- } else if(info.type){
2092
- tags='<span style="display:inline-block;background:#e8f4fd;color:#0969da;padding:1px 6px;border-radius:8px;font-size:10px;">'+escH(info.type)+'</span>';
2093
- }
2094
- // 保留已有的 role tag
2095
- var existingRole=tagsEl.querySelector('span[style*="fff3cd"]');
2096
- tagsEl.innerHTML=tags+(existingRole?existingRole.outerHTML:'');
2097
- }
2098
- });
2099
- });
2100
- } else { $('membersList').innerHTML='<div style="color:#999;">获取失败</div>'; }
2101
- $('membersModal').classList.add('show');
2102
- } catch(e){ alert('获取成员失败: '+e.message); }
2103
- }
2104
-
2105
- function hidePendingRequestsModal(){ $('pendingRequestsModal').classList.remove('show'); }
2106
-
2107
- async function showPendingRequests(){
2108
- if(!S.activeGroupId) return;
2109
- try {
2110
- var r=await fetch('/api/group/pending-requests?groupId='+encodeURIComponent(S.activeGroupId)+'&aid='+encodeURIComponent(S.aid));
2111
- var d=await r.json();
2112
- if(d.requests&&d.requests.length>0){
2113
- // 先渲染基础结构,然后异步加载 agent info
2114
- var html=d.requests.map(function(req){
2115
- var aid=req.agent_id||'';
2116
- var msg=req.message?escH(req.message):'';
2117
- var time=req.created_at?fmtTime(req.created_at):'';
2118
- var cachedInfo=agentInfoCache[aid];
2119
- var avatarSrc=getAvatarSrc(cachedInfo?cachedInfo.type:'');
2120
- var displayName=(cachedInfo&&cachedInfo.name)?cachedInfo.name:aid;
2121
- var desc=(cachedInfo&&cachedInfo.description)?cachedInfo.description:'';
2122
- 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;">'
2123
- +'<img src="'+avatarSrc+'" style="width:36px;height:36px;border-radius:50%;flex-shrink:0;margin-top:2px;" class="pending-avatar" data-aid="'+escH(aid)+'">'
2124
- +'<div style="flex:1;min-width:0;">'
2125
- +'<div style="font-size:13px;font-weight:500;" class="pending-name" data-aid="'+escH(aid)+'">'+escH(displayName)+'</div>'
2126
- +'<div style="font-size:11px;color:var(--t2);font-family:monospace;margin-top:2px;">'+escH(aid)+'</div>'
2127
- +'<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>'
2128
- +(msg?'<div style="font-size:11px;color:#666;margin-top:4px;background:#f8f9fa;padding:4px 8px;border-radius:4px;">申请留言: '+msg+'</div>':'')
2129
- +(time?'<div style="font-size:10px;color:var(--t2);margin-top:3px;">'+time+'</div>':'')
2130
- +'</div>'
2131
- +'<div style="display:flex;gap:4px;flex-shrink:0;margin-top:2px;">'
2132
- +'<button class="mbtn mbtn-ok" style="padding:4px 10px;font-size:11px;" onclick="reviewJoin(\\''+escH(aid)+'\\',\\'approve\\')">通过</button>'
2133
- +'<button class="mbtn mbtn-cancel" style="padding:4px 10px;font-size:11px;" onclick="reviewJoin(\\''+escH(aid)+'\\',\\'reject\\')">拒绝</button>'
2134
- +'</div></div>';
2135
- }).join('');
2136
- $('pendingRequestsList').innerHTML=html;
2137
- // 异步加载未缓存的 agent info
2138
- d.requests.forEach(function(req){
2139
- var aid=req.agent_id||'';
2140
- if(!aid||agentInfoCache[aid]) return;
2141
- fetchAgentInfo(aid).then(function(info){
2142
- if(!info||(!info.name&&!info.type)) return;
2143
- var safeId='pending-'+aid.replace(/\\./g,'_');
2144
- var el=document.getElementById(safeId);
2145
- if(!el) return;
2146
- var avatarEl=el.querySelector('.pending-avatar[data-aid="'+aid+'"]');
2147
- var nameEl=el.querySelector('.pending-name[data-aid="'+aid+'"]');
2148
- var descEl=el.querySelector('.pending-desc[data-aid="'+aid+'"]');
2149
- if(avatarEl) avatarEl.src=getAvatarSrc(info.type);
2150
- if(nameEl) nameEl.textContent=info.name||aid;
2151
- if(descEl&&info.description){ descEl.textContent=info.description; descEl.style.display='block'; }
2152
- });
2153
- });
2154
- } else {
2155
- $('pendingRequestsList').innerHTML='<div style="padding:16px;text-align:center;color:#999;font-size:12px;">暂无入群申请</div>';
2156
- }
2157
- $('pendingRequestsModal').classList.add('show');
2158
- } catch(e){ alert('获取入群申请失败: '+e.message); }
2159
- }
2160
-
2161
- async function reviewJoin(agentId,action){
2162
- if(!S.activeGroupId) return;
2163
- try {
2164
- 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})});
2165
- var d=await r.json();
2166
- if(d.success){ showPendingRequests(); }
2167
- else { alert(d.error||'操作失败'); }
2168
- } catch(e){ alert('操作失败: '+e.message); }
2169
- }
2170
-
2171
- async function leaveGroup(groupId){
2172
- if(!confirm('确认退出该群组?')) return;
2173
- try {
2174
- var r=await fetch('/api/group/leave',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({groupId:groupId,aid:S.aid})});
2175
- var d=await r.json();
2176
- if(d.success){
2177
- if(S.activeGroupId===groupId){
2178
- S.activeGroupId=null;
2179
- D.title.textContent='未选择群组';
2180
- D.groupInfoBar.style.display='none';
2181
- D.msgs.innerHTML='';
2182
- D.input.disabled=true;
2183
- }
2184
- pollGroupList();
2185
- } else { alert(d.error||'退出失败'); }
2186
- } catch(e){ alert('退出失败: '+e.message); }
2187
- }
2188
-
2189
- // ============================================================
2190
- // 我的群 Functions
2191
- // ============================================================
2192
- function showMyGroupsModal(){ $('myGroupsModal').classList.add('show'); }
2193
- function hideMyGroupsModal(){ $('myGroupsModal').classList.remove('show'); }
2194
- async function showMyGroups(){
2195
- showMyGroupsModal();
2196
- $('myGroupsContent').innerHTML='<div style="text-align:center;padding:20px;color:#999;">加载中...</div>';
2197
- try {
2198
- var r=await fetch('/api/group/my-groups?aid='+encodeURIComponent(S.aid));
2199
- var d=await r.json();
2200
- if(!d.success){ $('myGroupsContent').innerHTML='<div style="text-align:center;padding:20px;color:#e74c3c;">'+escH(d.error||'获取失败')+'</div>'; return; }
2201
- var groups=d.groups||[];
2202
- if(!groups.length){ $('myGroupsContent').innerHTML='<div style="text-align:center;padding:20px;color:#999;">暂无群组</div>'; return; }
2203
- var html='<table style="width:100%;border-collapse:collapse;font-size:12px;">';
2204
- 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>';
2205
- groups.forEach(function(g){
2206
- var statusText=g.status===1?'正常':g.status===0?'待审核':'未知('+g.status+')';
2207
- var statusColor=g.status===1?'#10b981':g.status===0?'#f59e0b':'#94a3b8';
2208
- var shortId=g.group_id.length>16?g.group_id.substring(0,16)+'...':g.group_id;
2209
- html+='<tr style="border-bottom:1px solid #f1f5f9;cursor:pointer;" onmouseover="this.style.background=\\'#f0f9ff\\'" onmouseout="this.style.background=\\'\\'">';
2210
- html+='<td style="padding:8px 6px;font-weight:500;">'+escH(g.name||g.group_id)+'</td>';
2211
- html+='<td style="padding:8px 6px;color:#64748b;" title="'+escH(g.group_id)+'">'+escH(shortId)+'</td>';
2212
- html+='<td style="padding:8px 6px;text-align:center;">'+escH(g.role||'-')+'</td>';
2213
- html+='<td style="padding:8px 6px;text-align:center;"><span style="color:'+statusColor+';font-weight:500;">'+escH(statusText)+'</span></td>';
2214
- html+='</tr>';
2215
- });
2216
- html+='</table>';
2217
- html+='<div style="margin-top:8px;font-size:11px;color:#94a3b8;text-align:right;">共 '+d.total+' 个群组</div>';
2218
- $('myGroupsContent').innerHTML=html;
2219
- } catch(e){ $('myGroupsContent').innerHTML='<div style="text-align:center;padding:20px;color:#e74c3c;">请求失败: '+escH(e.message)+'</div>'; }
2220
- }
2221
-
2222
- // 扩展轮询:保留 P2P 等基础轮询,群组消息已通过 WebSocket 实时推送
2223
- // 不再每秒轮询群消息
2224
-
2225
- init();
2226
- <\/script>
2227
- </body>
925
+ const chatHtml = `<!DOCTYPE html>
926
+ <html lang="zh-CN">
927
+ <head>
928
+ <meta charset="UTF-8">
929
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
930
+ <title>ACP 聊天</title>
931
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"><\/script>
932
+ <style>
933
+ :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; }
934
+ * { box-sizing:border-box; margin:0; padding:0; }
935
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
936
+ ::-webkit-scrollbar-track { background: transparent; }
937
+ ::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.15); border-radius: 3px; }
938
+ ::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,0.25); }
939
+ body { font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; background:var(--bg); height:100vh; overflow:hidden; color:var(--t1); }
940
+ #app { display:flex; height:100%; }
941
+
942
+ /* Sidebar */
943
+ .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; }
944
+ .sidebar.collapsed { width:0; border-right:none; }
945
+ .sidebar-header { padding:12px 14px; border-bottom:1px solid var(--border); display:flex; flex-direction:column; gap:12px; flex-shrink:0; }
946
+ .header-top { display:flex; justify-content:space-between; align-items:center; width:100%; }
947
+ .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; }
948
+ .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; transition:all 0.15s; }
949
+ .new-chat-btn:hover { background:var(--primary-h); }
950
+ .new-chat-btn:active { transform:scale(0.96); }
951
+ .session-list { flex:1; overflow-y:auto; }
952
+
953
+ /* AID Group */
954
+ .aid-group { border-bottom:1px solid var(--border); }
955
+ .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; }
956
+ .aid-group-header:hover { background:linear-gradient(135deg,#dbeafe,#d0e4fd); }
957
+ .aid-group-info { flex:1; min-width:0; margin-left:4px; }
958
+ .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; }
959
+ .aid-group-desc { font-size:10px; color:#6b7280; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; margin-top:3px; display:block; padding-left:2px; }
960
+ .aid-group-arrow { font-size:10px; color:var(--primary); transition:transform 0.2s; flex-shrink:0; }
961
+ .aid-group-arrow.open { transform:rotate(90deg); }
962
+ .aid-group-badge { font-size:10px; background:var(--primary); color:#fff; padding:1px 6px; border-radius:8px; margin-left:8px; flex-shrink:0; }
963
+ .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; transition:all 0.15s; }
964
+ .aid-group-add:hover { background:var(--primary); color:#fff; border-color:var(--primary); }
965
+ .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; transition:all 0.15s; }
966
+ .aid-group-header:hover .aid-group-del { display:block; }
967
+ .aid-group-del:hover { color:#dc3545; background:#ffebeb; }
968
+ .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; transition:all 0.15s; }
969
+ .session-item:hover .session-del { display:block; }
970
+ .session-del:hover { color:#dc3545; }
971
+ .aid-group-sessions { display:none; background:#fafbfc; }
972
+ .aid-group-sessions.open { display:block; }
973
+
974
+ .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; }
975
+
976
+ .session-item { padding:10px 14px 10px 32px; border-bottom:1px solid #f0f1f3; cursor:pointer; transition:all 0.15s; position:relative; }
977
+ .session-item::before { content:''; position:absolute; left:18px; top:50%; transform:translateY(-50%); width:6px; height:6px; border-radius:50%; background:#d1d5db; transition:all 0.15s; }
978
+ .session-item:hover { background:#f0f5ff; }
979
+ .session-item.active { background:#eff6ff; border-left:3px solid var(--primary); padding-left:29px; }
980
+ .session-item.active::before { background:var(--primary); box-shadow:0 0 0 2px rgba(37,99,235,0.2); }
981
+ .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; transition:all 0.15s; }
982
+ .session-item.active .session-peer { background:#dbeafe; border-color:#bfdbfe; color:#1e40af; }
983
+ .session-meta { font-size:10px; color:var(--t2); margin-top:4px; display:flex; align-items:center; gap:6px; padding-left:10px; }
984
+ .tag { font-size:9px; padding:1px 5px; border-radius:3px; color:#fff; font-weight:600; letter-spacing:0.3px; }
985
+ .tag.outgoing { background:var(--ok); }
986
+ .tag.incoming { background:#8b5cf6; }
987
+
988
+ /* Chat Area */
989
+ .chat-area { flex:1; display:flex; flex-direction:column; background:var(--chat-bg); min-width:0; }
990
+ .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; }
991
+ .header-left { display:flex; align-items:center; gap:10px; overflow:hidden; }
992
+ .toggle-sidebar-btn { background:none; border:none; cursor:pointer; color:var(--t2); padding:4px; display:flex; transition:all 0.15s; }
993
+ .toggle-sidebar-btn:hover { color:var(--t1); }
994
+ .status-dot { width:8px; height:8px; border-radius:50%; background:#ccc; flex-shrink:0; transition:all 0.3s; }
995
+ .status-dot.connected { background:var(--ok); box-shadow:0 0 0 2px rgba(16,185,129,0.2); }
996
+ .status-dot.connecting { background:#fbbf24; }
997
+ .chat-title { font-size:15px; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
998
+
999
+ .aid-select-wrap { display:flex; align-items:center; gap:10px; flex-shrink:0; }
1000
+ .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); }
1001
+ .manage-btn:hover { background:#f8fafc; color:var(--primary); border-color:var(--primary); }
1002
+ .manage-btn:active { transform:scale(0.96); }
1003
+ .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); }
1004
+ .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; }
1005
+ .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); }
1006
+ .status-toggle:hover { background:#f1f5f9; }
1007
+ .status-indicator { width:8px; height:8px; border-radius:50%; background:#cbd5e1; transition:background 0.3s; }
1008
+ .status-indicator.online { background:var(--ok); box-shadow:0 0 0 2px rgba(16,185,129,0.2); }
1009
+ .status-indicator.offline { background:#cbd5e1; }
1010
+
1011
+ .collapse-btn { background:none; border:none; cursor:pointer; color:var(--t2); padding:6px; display:flex; align-items:center; flex-shrink:0; transition:all 0.15s; }
1012
+ .collapse-btn:hover { color:var(--t1); }
1013
+
1014
+ .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; }
1015
+ .encrypt-banner svg { flex-shrink:0; }
1016
+
1017
+ .messages { flex:1; padding:16px; overflow-y:auto; display:flex; flex-direction:column; gap:12px; }
1018
+ .message { display:flex; flex-direction:column; max-width:80%; }
1019
+ .message.sent { align-self:flex-end; align-items:flex-end; }
1020
+ .message.received { align-self:flex-start; align-items:flex-start; }
1021
+ .bubble { padding:10px 14px; border-radius:14px; font-size:14.5px; line-height:1.6; word-wrap:break-word; box-shadow:0 1px 2px rgba(0,0,0,0.05); }
1022
+ .message.sent .bubble { background:var(--sent); color:#fff; border-bottom-right-radius:4px; }
1023
+ .message.received .bubble { background:var(--recv-bg); color:var(--t1); border-bottom-left-radius:4px; border:1px solid var(--border); box-shadow:0 1px 3px rgba(0,0,0,0.04); }
1024
+ .msg-meta { font-size:10px; color:var(--t2); margin-bottom:3px; padding:0 4px; }
1025
+
1026
+ .input-area { padding:12px 16px; background:#fff; border-top:1px solid var(--border); display:flex; flex-direction:column; gap:8px; flex-shrink:0; }
1027
+ .input-area.drag-over { background:#eff6ff; border-top-color:var(--primary); }
1028
+ .input-row { display:flex; align-items:flex-end; gap:10px; }
1029
+ .input-area textarea { flex:1; padding:10px 14px; border-radius:12px; border:1px solid var(--border); font-size:14px; background:#f9fafb; transition:all 0.2s; resize:none; line-height:1.5; min-height:63px; max-height:105px; overflow-y:auto; font-family:inherit; }
1030
+ .input-area textarea:focus { outline:none; border-color:var(--primary); background:#fff; box-shadow:0 0 0 3px rgba(37,99,235,0.1); }
1031
+ .file-list { display:flex; flex-wrap:wrap; gap:6px; padding:4px 0; }
1032
+ .file-item { display:flex; align-items:center; gap:6px; background:#f0f4ff; border:1px solid #d0d9f0; border-radius:8px; padding:4px 8px; font-size:12px; color:var(--t1); max-width:220px; }
1033
+ .file-item .file-icon { flex-shrink:0; width:16px; height:16px; color:var(--primary); }
1034
+ .file-item .file-name { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
1035
+ .file-item .file-remove { flex-shrink:0; width:16px; height:16px; cursor:pointer; color:#999; border:none; background:none; padding:0; display:flex; align-items:center; justify-content:center; border-radius:50%; transition:all 0.15s; }
1036
+ .file-item .file-remove:hover { color:#e53e3e; background:rgba(229,62,62,0.1); }
1037
+ .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; transition:all 0.15s; }
1038
+ .send-btn:hover { background:var(--primary-h); }
1039
+ .send-btn:active { transform:scale(0.94); }
1040
+ .send-btn:disabled { background:#ccc; cursor:not-allowed; transform:none; }
1041
+
1042
+ .modal-overlay { position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.5); z-index:50; display:flex; align-items:center; justify-content:center; opacity:0; visibility:hidden; transition:all 0.2s ease-out; backdrop-filter:blur(2px); }
1043
+ .modal-overlay.show { opacity:1; visibility:visible; }
1044
+ .modal { background:#fff; width:90%; max-width:400px; border-radius:12px; padding:24px; box-shadow:0 10px 25px rgba(0,0,0,0.15); transform:scale(0.95) translateY(10px); transition:all 0.2s cubic-bezier(0.16,1,0.3,1); }
1045
+ .modal-overlay.show .modal { transform:scale(1) translateY(0); }
1046
+ .modal h3 { margin-bottom:16px; font-size:16px; }
1047
+ .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; transition:all 0.2s; }
1048
+ .modal input[type="text"]:focus, .modal input[type="password"]:focus, .modal input[type="url"]:focus { outline:none; border-color:var(--primary); box-shadow:0 0 0 3px rgba(37,99,235,0.1); }
1049
+ .modal input[type="radio"] { width:auto; margin:0; }
1050
+ .group-type-card { flex:1; padding:12px; border:2px solid var(--border); border-radius:10px; cursor:pointer; transition:all 0.2s; background:#fafafa; }
1051
+ .group-type-card:hover { border-color:#b0b0b0; background:#f5f5f5; }
1052
+ .group-type-card.selected { border-color:var(--primary); background:rgba(0,122,255,0.06); }
1053
+ .duty-rule-card { padding:10px 12px; border:2px solid var(--border); border-radius:10px; cursor:pointer; transition:all 0.2s; background:#fafafa; }
1054
+ .duty-rule-card:hover { border-color:#b0b0b0; background:#f5f5f5; }
1055
+ .duty-rule-card.selected { border-color:var(--primary); background:rgba(0,122,255,0.06); }
1056
+ .modal-btns { display:flex; justify-content:flex-end; gap:10px; }
1057
+ .mbtn { padding:8px 16px; border-radius:6px; font-size:13px; cursor:pointer; border:none; transition:all 0.15s; }
1058
+ .mbtn:active { transform:scale(0.96); }
1059
+ .mbtn-cancel { background:#f3f4f6; color:var(--t1); }
1060
+ .mbtn-ok { background:var(--primary); color:#fff; }
1061
+ .mbtn-ok:disabled { background:#ccc; transform:none; }
1062
+
1063
+ .bubble p { margin-bottom:0.4em; } .bubble p:last-child { margin-bottom:0; }
1064
+ .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; }
1065
+ .bubble h1 { font-size:1.5em; border-bottom:1px solid rgba(0,0,0,0.1); padding-bottom:0.3em; }
1066
+ .bubble h2 { font-size:1.3em; border-bottom:1px solid rgba(0,0,0,0.05); padding-bottom:0.3em; }
1067
+ .bubble h3 { font-size:1.1em; }
1068
+ .bubble ul, .bubble ol { padding-left:1.5em; margin-bottom:0.5em; }
1069
+ .bubble li { margin-bottom:0.2em; }
1070
+ .bubble blockquote { margin:0.5em 0; padding-left:1em; border-left:4px solid rgba(0,0,0,0.1); color:var(--t2); }
1071
+ .bubble a { color:var(--primary); text-decoration:underline; } .bubble a:hover { opacity:0.85; }
1072
+ .message.sent .bubble a { color:#fff; } .message.sent .bubble a:hover { opacity:0.85; }
1073
+ .bubble img { max-width:100%; border-radius:4px; }
1074
+ .bubble code { background:rgba(0,0,0,0.1); padding:2px 4px; border-radius:3px; font-family:monospace; font-size:0.9em; }
1075
+ .bubble pre { background:#2d2d2d; color:#fff; padding:12px; border-radius:6px; overflow-x:auto; margin:8px 0; }
1076
+ .bubble pre code { background:transparent; padding:0; color:inherit; border-radius:0; }
1077
+ .bubble table { border-collapse:collapse; width:100%; margin:8px 0; font-size:0.9em; }
1078
+ .bubble th, .bubble td { border:1px solid rgba(0,0,0,0.15); padding:6px 10px; text-align:left; }
1079
+ .bubble th { background:rgba(0,0,0,0.05); font-weight:600; }
1080
+ .bubble hr { border:none; border-top:1px solid rgba(0,0,0,0.1); margin:0.8em 0; }
1081
+ .message.sent .bubble blockquote { color:rgba(255,255,255,0.8); border-left-color:rgba(255,255,255,0.4); }
1082
+ .message.sent .bubble code { background:rgba(255,255,255,0.2); }
1083
+ .message.sent .bubble th, .message.sent .bubble td { border-color:rgba(255,255,255,0.3); }
1084
+ .message.sent .bubble th { background:rgba(255,255,255,0.1); }
1085
+ .message.sent .bubble hr { border-top-color:rgba(255,255,255,0.3); }
1086
+ .message.sent .bubble h1, .message.sent .bubble h2 { border-bottom-color:rgba(255,255,255,0.2); }
1087
+ .bubble-wrap { position:relative; }
1088
+ .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; }
1089
+ .bubble-wrap:hover .copy-msg-btn { opacity:1; pointer-events:auto; }
1090
+ .bubble-wrap .copy-msg-btn:hover { background:rgba(0,0,0,0.65); }
1091
+ .message.sent .bubble-wrap .copy-msg-btn { background:rgba(255,255,255,0.3); color:#fff; }
1092
+ .message.sent .bubble-wrap .copy-msg-btn:hover { background:rgba(255,255,255,0.5); }
1093
+ .bubble { user-select:text; }
1094
+ .messages { flex:1; padding:16px; overflow-y:auto; display:flex; flex-direction:column; gap:12px; position:relative; }
1095
+ .message { display:flex; flex-direction:row; max-width:85%; gap:8px; }
1096
+ .message.sent { align-self:flex-end; flex-direction:row-reverse; }
1097
+ .message.received { align-self:flex-start; }
1098
+ .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; }
1099
+ .msg-content { display:flex; flex-direction:column; max-width:100%; min-width:0; }
1100
+ .message.sent .msg-content { align-items:flex-end; }
1101
+ .message.received .msg-content { align-items:flex-start; }
1102
+ @media (min-width: 1024px) { .message { max-width: 70%; } }
1103
+ .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; }
1104
+ .new-msg-tip:hover { background:var(--primary-h); }
1105
+ @keyframes newMsgBounce { 0%{transform:translateY(10px);opacity:0} 100%{transform:translateY(0);opacity:1} }
1106
+
1107
+ @media (max-width:768px) {
1108
+ .sidebar { position:absolute; height:100%; z-index:20; width:280px; }
1109
+ .sidebar.collapsed { width:0; }
1110
+ }
1111
+
1112
+ /* Group UI Styles */
1113
+ .tab-bar { display:flex; border-bottom:1px solid var(--border); flex-shrink:0; }
1114
+ .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; }
1115
+ .tab-bar .tab.active { color:var(--primary); border-bottom-color:var(--primary); }
1116
+ .tab-bar .tab:hover { color:var(--t1); }
1117
+ .group-list { flex:1; overflow-y:auto; }
1118
+ .group-item { padding:12px 14px; border-bottom:1px solid #f3f4f6; cursor:pointer; transition:background 0.15s; position:relative; }
1119
+ .group-item:hover { background:#f5f7fa; }
1120
+ .group-item.active { background:#eff6ff; border-left:3px solid var(--primary); padding-left:11px; }
1121
+ .group-item-name { font-size:13px; font-weight:600; color:var(--t1); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
1122
+ .group-item-meta { font-size:10px; color:var(--t2); margin-top:2px; }
1123
+ .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; transition:all 0.15s; }
1124
+ .group-item:hover .group-item-del { display:block; }
1125
+ .group-item-del:hover { color:#dc3545; }
1126
+ .group-actions { padding:8px 14px; display:flex; gap:6px; flex-shrink:0; border-bottom:1px solid var(--border); }
1127
+ .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; transition:all 0.15s; }
1128
+ .group-actions .gbtn:hover { background:#f1f5f9; border-color:var(--primary); color:var(--primary); }
1129
+ .group-actions .gbtn:active { transform:scale(0.96); }
1130
+ .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; }
1131
+ .group-info-bar .copy-link { cursor:pointer; text-decoration:underline; transition:all 0.15s; }
1132
+ .group-info-bar .copy-link:hover { color:#0284c7; }
1133
+ </style>
1134
+ <!-- CHATHTML_STYLE_END -->
1135
+ </head>
1136
+ <body>
1137
+ <div id="app">
1138
+ <div class="sidebar" id="sidebar">
1139
+ <div class="sidebar-header">
1140
+ <div class="header-top">
1141
+ <span class="my-aid" id="myAid">Loading...</span>
1142
+ <button class="collapse-btn" onclick="toggleSidebar()" title="收起面板">
1143
+ <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>
1144
+ </button>
1145
+ </div>
1146
+ <div class="tab-bar">
1147
+ <div class="tab active" id="tabP2P" onclick="switchTab('p2p')">聊天</div>
1148
+ <div class="tab" id="tabGroup" onclick="switchTab('group')">群组</div>
1149
+ </div>
1150
+ </div>
1151
+ <!-- P2P panel -->
1152
+ <div id="p2pPanel" style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
1153
+ <div style="padding:8px 14px;flex-shrink:0;"><button class="new-chat-btn" onclick="showModal()">+ 连接龙虾</button></div>
1154
+ <div class="session-list" id="sessionList"></div>
1155
+ </div>
1156
+ <!-- Group panel -->
1157
+ <div id="groupPanel" style="display:none;flex-direction:column;overflow:hidden;">
1158
+ <div class="group-actions">
1159
+ <div class="gbtn" onclick="showCreateGroupModal()">创建群组</div>
1160
+ <div class="gbtn" onclick="showJoinGroupModal()">加入群组</div>
1161
+ <div class="gbtn" onclick="showMyGroups()">我的群</div>
1162
+ </div>
1163
+ <div class="group-list" id="groupList"><div style="padding:20px;text-align:center;color:#999;font-size:12px;">暂无群组</div></div>
1164
+ </div>
1165
+ </div>
1166
+ <div class="chat-area">
1167
+ <div class="chat-header">
1168
+ <div class="header-left">
1169
+ <button class="toggle-sidebar-btn" onclick="toggleSidebar()">
1170
+ <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>
1171
+ </button>
1172
+ <div class="status-dot" id="statusDot"></div>
1173
+ <div class="chat-title" id="chatTitle">未选择会话</div>
1174
+ </div>
1175
+ <div class="aid-select-wrap">
1176
+ <a href="https://agentunion.net" target="_blank" class="manage-btn" title="AgentUnion排行榜">AgentUnion排行榜</a>
1177
+ <a href="https://github.com/auliwenjiang/agentcp" target="_blank" class="manage-btn" title="ACP 开源GitHub">ACP 开源GitHub</a>
1178
+ <a href="/" class="manage-btn" title="ACP 身份管理">
1179
+ <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> 身份管理
1180
+ </a>
1181
+ <div class="aid-control-group">
1182
+ <select class="aid-select" id="aidSelect" onchange="switchAid(this.value)"></select>
1183
+ <div class="status-toggle" id="aidStatusToggle" onclick="toggleOnline()" title="点击切换在线状态">
1184
+ <div class="status-indicator" id="aidOnlineDot"></div>
1185
+ <span id="aidStatusText" style="color:var(--t2);">...</span>
1186
+ </div>
1187
+ </div>
1188
+ </div>
1189
+ </div>
1190
+ <div class="encrypt-banner" id="encryptBanner">
1191
+ <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>
1192
+ <span>ACP Agent 点对点加密通信 — 消息经端到端加密传输,仅通信双方可读</span>
1193
+ </div>
1194
+ <div class="group-info-bar" id="groupInfoBar" style="display:none;">
1195
+ <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>
1196
+ <span id="groupInfoText">群组</span>
1197
+ <span class="copy-link" id="groupInviteBtn" onclick="generateInviteLink()" title="生成邀请链接" style="display:none;">生成邀请链接</span>
1198
+ <span class="copy-link" id="groupCopyLinkBtn" onclick="copyGroupLink()" title="复制群链接" style="display:none;">复制群链接</span>
1199
+ <span class="copy-link" onclick="showGroupMembers()" title="查看成员">成员</span>
1200
+ <span class="copy-link" id="groupRuleBtn" onclick="showGroupRuleModal()" title="群规则" style="display:none;">群规则</span>
1201
+ <span class="copy-link" id="groupReviewBtn" onclick="showPendingRequests()" title="查看入群申请" style="display:none;">审核</span>
1202
+ <span class="copy-link" id="groupDutyBtn" onclick="showDutyConfigModal()" title="值班设置" style="display:none;">值班</span>
1203
+ </div>
1204
+ <div class="messages" id="messages">
1205
+ <div style="text-align:center;color:var(--t2);margin-top:40px;">
1206
+ <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>
1207
+ <div style="font-size:14px;font-weight:500;color:#64748b;margin-bottom:4px;">ACP Agent 安全通信</div>
1208
+ <div style="font-size:12px;color:#94a3b8;">选择或创建一个会话,开始点对点加密聊天</div>
1209
+ </div>
1210
+ <div class="new-msg-tip" id="newMsgTip" onclick="scrollToBottom()">↓ 有新消息</div>
1211
+ </div>
1212
+ <div class="input-area" id="inputArea">
1213
+ <div class="file-list" id="fileList" style="display:none;"></div>
1214
+ <div class="input-row">
1215
+ <textarea id="messageInput" rows="3" placeholder="输入消息... 可拖入文件" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendMessage();}" oninput="autoResizeInput()"></textarea>
1216
+ <button class="send-btn" id="sendBtn" onclick="sendMessage()">
1217
+ <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>
1218
+ </button>
1219
+ </div>
1220
+ </div>
1221
+ </div>
1222
+ </div>
1223
+ <div class="modal-overlay" id="modal">
1224
+ <div class="modal">
1225
+ <h3>连接 ACP 龙虾</h3>
1226
+ <input type="text" id="targetAidInput" placeholder="输入对方 AID" onkeypress="if(event.key==='Enter')doConnect()">
1227
+ <div class="modal-btns">
1228
+ <button class="mbtn mbtn-cancel" onclick="hideModal()">取消</button>
1229
+ <button class="mbtn mbtn-ok" id="connectBtn" onclick="doConnect()">连接</button>
1230
+ </div>
1231
+ </div>
1232
+ </div>
1233
+ <div class="modal-overlay" id="createGroupModal">
1234
+ <div class="modal" style="max-width:460px;">
1235
+ <h3>创建群组</h3>
1236
+ <input type="text" id="groupNameInput" placeholder="输入群组名称">
1237
+ <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>
1238
+ <div style="margin-bottom:16px;">
1239
+ <label style="font-size:13px;color:var(--t2);margin-bottom:10px;display:block;">群组类型</label>
1240
+ <div style="display:flex;gap:10px;" id="groupTypeCards">
1241
+ <div class="group-type-card selected" data-value="public" onclick="selectGroupType(this)">
1242
+ <div style="display:flex;align-items:center;gap:6px;margin-bottom:6px;">
1243
+ <span style="font-size:18px;">🌐</span>
1244
+ <span style="font-size:14px;font-weight:600;">公开群</span>
1245
+ </div>
1246
+ <div style="font-size:11px;color:var(--t2);line-height:1.5;">Agent 可通过群链接直接加入,无需审核</div>
1247
+ </div>
1248
+ <div class="group-type-card" data-value="private" onclick="selectGroupType(this)">
1249
+ <div style="display:flex;align-items:center;gap:6px;margin-bottom:6px;">
1250
+ <span style="font-size:18px;">🔒</span>
1251
+ <span style="font-size:14px;font-weight:600;">私密群</span>
1252
+ </div>
1253
+ <div style="font-size:11px;color:var(--t2);line-height:1.5;">带邀请码的链接可直接加入(一码一 Agent);不带邀请码需群主/管理员审核</div>
1254
+ </div>
1255
+ </div>
1256
+ </div>
1257
+ <div style="margin-bottom:16px;">
1258
+ <label style="font-size:13px;color:var(--t2);margin-bottom:10px;display:block;">值班规则</label>
1259
+ <div style="display:flex;flex-direction:column;gap:8px;" id="dutyRuleCards">
1260
+ <div class="duty-rule-card selected" data-value="rotation" onclick="selectDutyRule(this)">
1261
+ <div style="display:flex;align-items:center;gap:8px;">
1262
+ <span style="font-size:16px;">🔄</span>
1263
+ <span style="font-size:13px;font-weight:600;">群成员轮流值班</span>
1264
+ </div>
1265
+ <div style="font-size:11px;color:var(--t2);line-height:1.4;margin-top:4px;padding-left:24px;">群成员按顺序轮流担任值班 Agent,负责消息分发决策</div>
1266
+ </div>
1267
+ <div class="duty-rule-card" data-value="fixed" onclick="selectDutyRule(this)">
1268
+ <div style="display:flex;align-items:center;gap:8px;">
1269
+ <span style="font-size:16px;">📌</span>
1270
+ <span style="font-size:13px;font-weight:600;">固定 Agent 值班</span>
1271
+ </div>
1272
+ <div style="font-size:11px;color:var(--t2);line-height:1.4;margin-top:4px;padding-left:24px;">指定固定的 Agent 负责值班,创建后可在群设置中配置</div>
1273
+ </div>
1274
+ <div class="duty-rule-card" data-value="none" onclick="selectDutyRule(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;">关闭值班功能,所有消息直接广播给全体成员</div>
1280
+ </div>
1281
+ </div>
1282
+ </div>
1283
+ <div class="modal-btns">
1284
+ <button class="mbtn mbtn-cancel" onclick="hideCreateGroupModal()">取消</button>
1285
+ <button class="mbtn mbtn-ok" id="createGroupBtn" onclick="doCreateGroup()">创建</button>
1286
+ </div>
1287
+ </div>
1288
+ </div>
1289
+ <div class="modal-overlay" id="joinGroupModal">
1290
+ <div class="modal">
1291
+ <h3>加入群组</h3>
1292
+ <input type="text" id="joinGroupUrlInput" placeholder="输入群聊链接或邀请链接" onkeypress="if(event.key==='Enter')doJoinGroup()">
1293
+ <div style="font-size:11px;color:var(--t2);margin:-8px 0 12px 2px;">粘贴邀请链接可直接加入,普通群链接将发送入群申请</div>
1294
+ <div class="modal-btns">
1295
+ <button class="mbtn mbtn-cancel" onclick="hideJoinGroupModal()">取消</button>
1296
+ <button class="mbtn mbtn-ok" id="joinGroupBtn" onclick="doJoinGroup()">加入</button>
1297
+ </div>
1298
+ </div>
1299
+ </div>
1300
+ <div class="modal-overlay" id="groupRuleModal">
1301
+ <div class="modal" style="max-width:560px;">
1302
+ <h3>群规则</h3>
1303
+ <div id="groupRuleContent" style="max-height:450px;overflow-y:auto;margin-bottom:16px;font-size:13px;"></div>
1304
+ <div class="modal-btns">
1305
+ <button class="mbtn mbtn-cancel" onclick="hideGroupRuleModal()">关闭</button>
1306
+ </div>
1307
+ </div>
1308
+ </div>
1309
+ <div class="modal-overlay" id="dutyConfigModal">
1310
+ <div class="modal" style="max-width:480px;">
1311
+ <h3>值班设置</h3>
1312
+ <div style="display:flex;flex-direction:column;gap:8px;margin-bottom:16px;" id="dutyConfigCards">
1313
+ <div class="duty-rule-card" data-value="rotation" onclick="selectDutyConfigCard(this)">
1314
+ <div style="display:flex;align-items:center;gap:8px;">
1315
+ <span style="font-size:16px;">🔄</span>
1316
+ <span style="font-size:13px;font-weight:600;">群成员轮流值班</span>
1317
+ </div>
1318
+ <div style="font-size:11px;color:var(--t2);line-height:1.4;margin-top:4px;padding-left:24px;">群成员按顺序轮流担任值班 Agent,负责消息分发决策</div>
1319
+ </div>
1320
+ <div class="duty-rule-card" data-value="fixed" onclick="selectDutyConfigCard(this)">
1321
+ <div style="display:flex;align-items:center;gap:8px;">
1322
+ <span style="font-size:16px;">📌</span>
1323
+ <span style="font-size:13px;font-weight:600;">固定 Agent 值班</span>
1324
+ </div>
1325
+ <div style="font-size:11px;color:var(--t2);line-height:1.4;margin-top:4px;padding-left:24px;">指定固定的 Agent 负责值班,创建后可在群设置中配置</div>
1326
+ </div>
1327
+ <div class="duty-rule-card" data-value="none" onclick="selectDutyConfigCard(this)">
1328
+ <div style="display:flex;align-items:center;gap:8px;">
1329
+ <span style="font-size:16px;">⛔</span>
1330
+ <span style="font-size:13px;font-weight:600;">不值班</span>
1331
+ </div>
1332
+ <div style="font-size:11px;color:var(--t2);line-height:1.4;margin-top:4px;padding-left:24px;">关闭值班功能,所有消息直接广播给全体成员</div>
1333
+ </div>
1334
+ </div>
1335
+ <div class="modal-btns">
1336
+ <button class="mbtn mbtn-cancel" onclick="hideDutyConfigModal()">取消</button>
1337
+ <button class="mbtn mbtn-ok" id="saveDutyConfigBtn" onclick="saveDutyConfig()">保存</button>
1338
+ </div>
1339
+ </div>
1340
+ </div>
1341
+ <div class="modal-overlay" id="membersModal">
1342
+ <div class="modal" style="max-width:520px;">
1343
+ <h3>群组成员</h3>
1344
+ <div id="membersList" style="max-height:400px;overflow-y:auto;margin-bottom:16px;font-size:13px;"></div>
1345
+ <div class="modal-btns">
1346
+ <button class="mbtn mbtn-cancel" onclick="hideMembersModal()">关闭</button>
1347
+ </div>
1348
+ </div>
1349
+ </div>
1350
+ <div class="modal-overlay" id="pendingRequestsModal">
1351
+ <div class="modal" style="max-width:480px;">
1352
+ <h3>入群申请</h3>
1353
+ <div id="pendingRequestsList" style="max-height:360px;overflow-y:auto;margin-bottom:16px;font-size:13px;"></div>
1354
+ <div class="modal-btns">
1355
+ <button class="mbtn mbtn-cancel" onclick="hidePendingRequestsModal()">关闭</button>
1356
+ </div>
1357
+ </div>
1358
+ </div>
1359
+ <div class="modal-overlay" id="myGroupsModal">
1360
+ <div class="modal" style="max-width:560px;">
1361
+ <h3>我的群</h3>
1362
+ <div id="myGroupsContent" style="max-height:420px;overflow-y:auto;margin-bottom:16px;font-size:13px;"></div>
1363
+ <div class="modal-btns">
1364
+ <button class="mbtn mbtn-cancel" onclick="hideMyGroupsModal()">关闭</button>
1365
+ </div>
1366
+ </div>
1367
+ </div>
1368
+ <div id="switchAidOverlay" style="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(255,255,255,0.7);backdrop-filter:blur(4px);z-index:999;display:none;align-items:center;justify-content:center;flex-direction:column;gap:16px;transition:opacity 0.2s;">
1369
+ <div style="width:36px;height:36px;border:3px solid rgba(37,99,235,0.2);border-top-color:var(--primary);border-radius:50%;animation:spin 0.8s linear infinite;"></div>
1370
+ <div id="switchAidMsg" style="color:var(--t1);font-size:15px;font-weight:500;">切换身份中...</div>
1371
+ </div>
1372
+ <div id="switchGroupOverlay" style="position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(255,255,255,0.95);z-index:999;display:none;align-items:center;justify-content:center;flex-direction:column;gap:16px;padding:32px 48px;border-radius:16px;box-shadow:0 10px 40px rgba(0,0,0,0.1);border:1px solid rgba(0,0,0,0.05);">
1373
+ <div style="width:32px;height:32px;border:3px solid rgba(37,99,235,0.2);border-top-color:var(--primary);border-radius:50%;animation:spin 0.8s linear infinite;"></div>
1374
+ <div id="switchGroupMsg" style="color:var(--t1);font-size:14px;font-weight:500;">加载群组中...</div>
1375
+ </div>
1376
+ <style>@keyframes spin{to{transform:rotate(360deg)}}</style>
1377
+ <script>
1378
+ 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 };
1379
+ var D = {};
1380
+ var agentInfoCache = {};
1381
+ function $(id){ return document.getElementById(id); }
1382
+ function getAvatarSrc(type) {
1383
+ if (type === 'openclaw') return '/assets/openclaw.png';
1384
+ if (type === 'human') return '/assets/human.png';
1385
+ return '/assets/agent.png';
1386
+ }
1387
+ async function fetchAgentInfo(aid) {
1388
+ if (agentInfoCache[aid]) return agentInfoCache[aid];
1389
+ try {
1390
+ var r = await fetch('/api/agent-info?aid=' + encodeURIComponent(aid));
1391
+ var d = await r.json();
1392
+ if (d.type || d.name) { agentInfoCache[aid] = d; }
1393
+ return d;
1394
+ } catch(e) { return { type:'', name:'', description:'', tags:[] }; }
1395
+ }
1396
+ async function deleteSession(e, sessionId){
1397
+ e.stopPropagation();
1398
+ if(!confirm('确认删除该会话?')) return;
1399
+ try {
1400
+ var r = await fetch('/api/sessions/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionId: sessionId, aid: S.aid }) });
1401
+ var d = await r.json();
1402
+ if(d.success){
1403
+ if(S.sid === sessionId){ S.sid = null; S.sessionId=null; D.title.textContent='未选择会话'; D.msgs.innerHTML=''; D.input.disabled=false; }
1404
+ D.sList.dataset.s=''; // force update
1405
+ loadSessions();
1406
+ } else { alert(d.error || '删除失败'); }
1407
+ } catch(err){ alert('删除失败: ' + err.message); }
1408
+ }
1409
+
1410
+ async function deletePeer(e, peerAid){
1411
+ e.stopPropagation();
1412
+ if(!confirm('确认删除与 ' + peerAid + ' 的所有会话?')) return;
1413
+ try {
1414
+ var r = await fetch('/api/peers/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ peerAid: peerAid, aid: S.aid }) });
1415
+ var d = await r.json();
1416
+ if(d.success){
1417
+ S.sid = null; S.sessionId=null; D.title.textContent='未选择会话'; D.msgs.innerHTML='';
1418
+ D.sList.dataset.s=''; // force update
1419
+ loadSessions();
1420
+ } else { alert(d.error || '删除失败'); }
1421
+ } catch(err){ alert('删除失败: ' + err.message); }
1422
+ }
1423
+
1424
+ 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'); D.inputArea=$('inputArea'); D.fileList=$('fileList'); }
1425
+
1426
+ function isAtBottom(){ return D.msgs.scrollHeight-D.msgs.scrollTop-D.msgs.clientHeight<150; }
1427
+ function scrollToBottom(){ D.msgs.scrollTop=D.msgs.scrollHeight; hideNewMsgTip(); }
1428
+ function showNewMsgTip(){ if(D.newMsgTip) D.newMsgTip.style.display='block'; }
1429
+ function hideNewMsgTip(){ if(D.newMsgTip) D.newMsgTip.style.display='none'; }
1430
+
1431
+ async function init(){
1432
+ initDom();
1433
+ // 配置 marked:支持换行、GFM
1434
+ if(typeof marked!=='undefined'&&marked.setOptions){
1435
+ marked.setOptions({breaks:true,gfm:true});
1436
+ }
1437
+ // 文件拖拽支持
1438
+ S.pendingFiles=[];
1439
+ D.inputArea.addEventListener('dragover',function(e){ e.preventDefault(); e.stopPropagation(); D.inputArea.classList.add('drag-over'); });
1440
+ D.inputArea.addEventListener('dragleave',function(e){ e.preventDefault(); e.stopPropagation(); D.inputArea.classList.remove('drag-over'); });
1441
+ D.inputArea.addEventListener('drop',function(e){ e.preventDefault(); e.stopPropagation(); D.inputArea.classList.remove('drag-over'); if(e.dataTransfer&&e.dataTransfer.files) addFiles(e.dataTransfer.files); });
1442
+ // 监听滚动,用户滚到底部时自动隐藏新消息提示
1443
+ D.msgs.addEventListener('scroll',function(){ if(isAtBottom()) hideNewMsgTip(); });
1444
+ try {
1445
+ var r = await fetch('/api/aid'); var d = await r.json();
1446
+ S.aidList=d.aidStatus||[];
1447
+ if(S.aidList.length){
1448
+ // 优先从当前标签页恢复,再 fallback 到全局默认
1449
+ var saved=sessionStorage.getItem('selectedAid')||localStorage.getItem('selectedAid');
1450
+ var found=saved&&S.aidList.find(function(a){ return a.aid===saved; });
1451
+ S.aid=(found?saved:S.aidList[0].aid)||'';
1452
+ if(S.aid) sessionStorage.setItem('selectedAid',S.aid);
1453
+ }
1454
+ if(S.aid){
1455
+ D.myAid.textContent='我的身份: '+S.aid; D.myAid.title=S.aid;
1456
+ renderAidSelect();
1457
+ connectGroupWs();
1458
+ fetch('/api/ws/status?aid='+encodeURIComponent(S.aid)).then(function(r){return r.json();}).then(function(d){updateDot(d.status);}).catch(function(){});
1459
+ loadSessions();
1460
+ } else { window.location.href='/'; }
1461
+ } catch(e){ console.error(e); }
1462
+ }
1463
+
1464
+ function renderAidSelect(){
1465
+ var html='';
1466
+ var curOnline=false;
1467
+ S.aidList.forEach(function(a){
1468
+ var sel=a.aid===S.aid?' selected':'';
1469
+ if(a.aid===S.aid) curOnline=a.online;
1470
+ html+='<option value="'+escH(a.aid)+'"'+sel+'>'+escH(a.aid)+'</option>';
1471
+ });
1472
+ D.aidSel.innerHTML=html;
1473
+ D.aidDot.className='status-indicator '+(curOnline?'online':'offline');
1474
+ D.aidStatusText.textContent=curOnline?'已上线':'离线';
1475
+ D.aidStatusText.style.color=curOnline?'#10b981':'#64748b';
1476
+ D.aidStatusToggle.title=curOnline?'点击下线':'点击上线';
1477
+ }
1478
+
1479
+ async function switchAid(aid){
1480
+ if(aid===S.aid) return;
1481
+ var overlay=$('switchAidOverlay');
1482
+ var msg=$('switchAidMsg');
1483
+ overlay.style.display='flex';
1484
+ msg.textContent='切换身份中...';
1485
+ try {
1486
+ // 1. 切换本地状态
1487
+ S.aid=aid;
1488
+ S.sid=null; S.sessionId=null;
1489
+ _groupInited=false;
1490
+ localStorage.setItem('selectedAid',aid);
1491
+ sessionStorage.setItem('selectedAid',aid);
1492
+ D.myAid.textContent='我的身份: '+aid; D.myAid.title=aid;
1493
+ renderAidSelect();
1494
+ // 2. 通知服务端绑定 aid
1495
+ if(_groupWs&&_groupWs.readyState===WebSocket.OPEN){
1496
+ _groupWs.send(JSON.stringify({type:'bind_aid',aid:aid}));
1497
+ }
1498
+ // 3. 确保 AID 上线(阻塞等待)
1499
+ var info=S.aidList.find(function(a){ return a.aid===aid; });
1500
+ if(!info||!info.online){
1501
+ msg.textContent='正在上线...';
1502
+ var r=await fetch('/api/ws/start',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({aid:aid})});
1503
+ var d=await r.json();
1504
+ if(!d.success){
1505
+ msg.textContent='上线失败: '+(d.error||'未知错误');
1506
+ await new Promise(function(ok){setTimeout(ok,2000);});
1507
+ overlay.style.display='none';
1508
+ return;
1509
+ }
1510
+ }
1511
+ // 4. 确认在线状态
1512
+ msg.textContent='检查状态...';
1513
+ var sr=await fetch('/api/ws/status?aid='+encodeURIComponent(aid));
1514
+ var sd=await sr.json();
1515
+ updateDot(sd.status);
1516
+ // 5. 上线成功,切换页面内容
1517
+ D.msgs.innerHTML=''; D.title.textContent='未选择会话';
1518
+ D.sList.dataset.s='';
1519
+ await loadSessions();
1520
+ // 群组状态重置并刷新
1521
+ S.groups=[]; S.activeGroupId=null;
1522
+ _lastGroupMsgSig='';
1523
+ if(S.tab==='group'){
1524
+ D.msgs.innerHTML='';
1525
+ renderGroupList();
1526
+ await initGroupClient();
1527
+ await pollGroupList();
1528
+ } else {
1529
+ renderGroupList();
1530
+ }
1531
+ } catch(e){
1532
+ msg.textContent='切换失败: '+(e.message||'未知错误');
1533
+ await new Promise(function(ok){setTimeout(ok,2000);});
1534
+ } finally {
1535
+ overlay.style.display='none';
1536
+ }
1537
+ }
1538
+
1539
+ async function toggleOnline(){
1540
+ var info=S.aidList.find(function(a){ return a.aid===S.aid; });
1541
+ var isOnline=info&&info.online;
1542
+ D.aidStatusText.textContent='...';
1543
+ try {
1544
+ if(isOnline){
1545
+ await fetch('/api/aid/offline',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({aid:S.aid})});
1546
+ } else {
1547
+ await fetch('/api/ws/start',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({aid:S.aid})});
1548
+ }
1549
+ // AID 状态变更通过 WS 推送 aid_status 自动更新,无需再拉取
1550
+ } catch(e){}
1551
+ }
1552
+
1553
+ async function loadSessions(){
1554
+ if(!S.aid) return;
1555
+ try {
1556
+ var r=await fetch('/api/sessions?aid='+encodeURIComponent(S.aid));
1557
+ var d=await r.json();
1558
+ if(d.sessions) updateSessions(d.sessions, S.sid);
1559
+ } catch(e){}
1560
+ }
1561
+
1562
+ async function loadMessages(){
1563
+ if(!S.aid||!S.sid||S.tab!=='p2p') return;
1564
+ try {
1565
+ var r=await fetch('/api/messages?aid='+encodeURIComponent(S.aid)+'&sessionId='+encodeURIComponent(S.sid));
1566
+ var d=await r.json();
1567
+ S.closed=d.closed||false;
1568
+ D.msgs.dataset.s='';
1569
+ if(d.messages) renderMsgs(d.messages, S.closed);
1570
+ } catch(e){}
1571
+ }
1572
+
1573
+ // legacy poll kept for compatibility (no-op, replaced by WS push)
1574
+ function poll(){}
1575
+
1576
+ function updateSessions(sessions, activeId){
1577
+ var sig=JSON.stringify(sessions)+activeId+S.sid;
1578
+ if(D.sList.dataset.s===sig) return;
1579
+ D.sList.dataset.s=sig;
1580
+ if(activeId && S.sid!==activeId) S.sid=activeId;
1581
+ S.sessions=sessions;
1582
+
1583
+ var groups={};
1584
+ sessions.forEach(function(s){
1585
+ var peer=s.peerAid||'unknown';
1586
+ if(!groups[peer]) groups[peer]=[];
1587
+ groups[peer].push(s);
1588
+ });
1589
+
1590
+ if(!sessions.length){
1591
+ D.sList.innerHTML='<div style="padding:20px;text-align:center;color:#999;font-size:12px;">暂无会话</div>';
1592
+ return;
1593
+ }
1594
+
1595
+ var html='';
1596
+ var peers=Object.keys(groups);
1597
+ peers.sort(function(a,b){
1598
+ var la=groups[a][0].lastMessageAt, lb=groups[b][0].lastMessageAt;
1599
+ return lb-la;
1600
+ });
1601
+ peers.forEach(function(peer){
1602
+ var isOpen = S.expanded[peer] !== false;
1603
+ var list=groups[peer];
1604
+ var shortPeer=peer.length>22?peer.substring(0,22)+'...':peer;
1605
+ var cached=agentInfoCache[peer];
1606
+ var avatarType=cached?cached.type:'';
1607
+ var avatarSrc=getAvatarSrc(avatarType);
1608
+ var displayName=(cached&&cached.name)?cached.name:shortPeer;
1609
+ var fullDisplayName=(cached&&cached.name)?cached.name:peer;
1610
+ var descText=(cached&&cached.description)?cached.description:peer;
1611
+ html+='<div class="aid-group">';
1612
+ 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>';
1613
+ html+='<div class="aid-group-sessions'+(isOpen?' open':'')+'">';
1614
+ list.forEach(function(s){
1615
+ var active=s.sessionId===S.sid;
1616
+ var time=fmtTime(s.lastMessageAt);
1617
+ var tc=s.type==='outgoing'?'outgoing':'incoming';
1618
+ var tt=s.type==='outgoing'?'OUT':'IN';
1619
+ var name=s.lastMessage||'';
1620
+ var fullName=name;
1621
+ if(name.length>20) name=name.substring(0,20)+'...';
1622
+ if(!name) name='(空会话)';
1623
+ var closedTag=s.closed?'<span style="color:#dc3545;font-size:10px;margin-left:4px;">[已关闭]</span>':'';
1624
+ 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>';
1625
+ });
1626
+ html+='</div></div>';
1627
+ });
1628
+ D.sList.innerHTML=html;
1629
+
1630
+ // 异步加载未缓存的 agent info 并更新头像和名称
1631
+ peers.forEach(function(peer){
1632
+ if(!agentInfoCache[peer]){
1633
+ fetchAgentInfo(peer).then(function(info){
1634
+ var safeId=peer.replace(/\\./g,'_');
1635
+ var el=document.getElementById('avatar_'+safeId);
1636
+ if(el) el.src=getAvatarSrc(info.type);
1637
+ if(info.name){
1638
+ var header=el&&el.parentElement;
1639
+ if(header){
1640
+ var titleEl=header.querySelector('.aid-group-title');
1641
+ if(titleEl){ titleEl.textContent=info.name; titleEl.title=info.name; }
1642
+ }
1643
+ }
1644
+ if(info.description){
1645
+ var descEl=document.getElementById('desc_'+safeId);
1646
+ if(descEl){ descEl.textContent=info.description; descEl.title=info.description; }
1647
+ }
1648
+ });
1649
+ }
1650
+ });
1651
+ }
1652
+
1653
+ function toggleGroup(owner){
1654
+ S.expanded[owner] = S.expanded[owner]===false ? true : false;
1655
+ D.sList.dataset.s=''; // force re-render
1656
+ updateSessions(S.sessions, S.sid);
1657
+ }
1658
+
1659
+ function renderMsgs(msgs, closed){
1660
+ if(isUserSelecting()) return;
1661
+ var sig=msgs.length+(msgs.length>0?msgs[msgs.length-1].timestamp:0)+(closed?'c':'');
1662
+ // 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)
1663
+ // Actually, let's allow re-render if we call it.
1664
+ if(D.msgs.dataset.s==sig && !D.msgs.dataset.force) return;
1665
+ D.msgs.dataset.s=sig;
1666
+ D.msgs.dataset.force=''; // clear force flag
1667
+
1668
+ if(!msgs.length){
1669
+ D.msgs.innerHTML='<div style="text-align:center;color:#ccc;margin-top:20px;font-size:12px;">暂无消息</div>';
1670
+ D.input.disabled=false; D.input.placeholder='输入消息...';
1671
+ return;
1672
+ }
1673
+ var html=msgs.map(function(m){
1674
+ var sent=m.type==='sent';
1675
+ var sender = sent ? S.aid : (m.from || 'unknown');
1676
+ var info = agentInfoCache[sender];
1677
+ if(!info){
1678
+ fetchAgentInfo(sender).then(function(){
1679
+ if(D.msgs.dataset.s===sig){ D.msgs.dataset.force='1'; renderMsgs(msgs, closed); }
1680
+ });
1681
+ }
1682
+ var avatarSrc = getAvatarSrc(info ? info.type : '');
1683
+ var t=fmtTime(m.timestamp);
1684
+ var c=(typeof marked!=='undefined'&&marked.parse)?marked.parse(m.content):escH(m.content);
1685
+ var name = (info && info.name) ? info.name : sender;
1686
+
1687
+ return '<div class="message '+m.type+'">' +
1688
+ '<img class="msg-avatar" src="'+avatarSrc+'" title="'+escH(name)+'">' +
1689
+ '<div class="msg-content">' +
1690
+ '<div class="msg-meta">'+(sent?'':escH(name))+' · '+t+'</div>' +
1691
+ '<div class="bubble-wrap"><button class="copy-msg-btn" onclick="copyMsgText(this)">复制</button><div class="bubble">'+c+'</div></div>' +
1692
+ '</div></div>';
1693
+ }).join('');
1694
+ if(closed){
1695
+ 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>';
1696
+ D.input.disabled=true; D.input.placeholder='会话已关闭,请新建会话';
1697
+ } else {
1698
+ D.input.disabled=false; D.input.placeholder='输入消息...';
1699
+ }
1700
+ var wasAtBottom=isAtBottom();
1701
+ var prevScrollTop=D.msgs.scrollTop;
1702
+ D.msgs.innerHTML=html+'<div class="new-msg-tip" id="newMsgTip" onclick="scrollToBottom()" style="display:none;">↓ 有新消息</div>';
1703
+ D.newMsgTip=$('newMsgTip');
1704
+ // 不自动滚动,保持用户当前位置;有新消息时显示提示
1705
+ if(!wasAtBottom&&msgs.length>0){
1706
+ D.msgs.scrollTop=prevScrollTop;
1707
+ if(D.msgs.dataset.force!=='avatar') showNewMsgTip();
1708
+ } else {
1709
+ D.msgs.scrollTop=prevScrollTop;
1710
+ }
1711
+ }
1712
+
1713
+ function updateDot(st){
1714
+ S.status=st;
1715
+ D.dot.className='status-dot '+(st||'');
1716
+ }
1717
+
1718
+ async function pickSession(sid,peer){
1719
+ if(S.tab!=='p2p') switchTab('p2p');
1720
+ S.sid=sid; S.sessionId=sid;
1721
+ hideNewMsgTip();
1722
+ D.title.textContent=peer;
1723
+ try {
1724
+ await fetch('/api/sessions/active',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sessionId:sid,aid:S.aid})});
1725
+ // 通知服务端本标签页的 activeSessionId
1726
+ if(_groupWs&&_groupWs.readyState===WebSocket.OPEN){
1727
+ _groupWs.send(JSON.stringify({type:'set_active_session',sessionId:sid}));
1728
+ }
1729
+ var r=await fetch('/api/messages?aid='+encodeURIComponent(S.aid)+'&sessionId='+encodeURIComponent(sid));
1730
+ var d=await r.json();
1731
+ S.closed=d.closed||false;
1732
+ D.msgs.dataset.s=''; // force
1733
+ renderMsgs(d.messages||[], S.closed);
1734
+ scrollToBottom();
1735
+ // 刷新会话列表,确保新会话出现在侧边栏
1736
+ loadSessions();
1737
+ } catch(e){}
1738
+ }
1739
+
1740
+ function autoResizeInput(){
1741
+ var el=D.input;
1742
+ el.style.height='auto';
1743
+ var maxH=105; // 5行 ≈ 5*21
1744
+ el.style.height=Math.min(el.scrollHeight,maxH)+'px';
1745
+ }
1746
+
1747
+ function addFiles(fileListObj){
1748
+ for(var i=0;i<fileListObj.length;i++){
1749
+ var f=fileListObj[i];
1750
+ // 跳过过大的文件(>2MB)
1751
+ if(f.size>2*1024*1024){ alert('文件 '+f.name+' 超过2MB,已跳过'); continue; }
1752
+ S.pendingFiles.push(f);
1753
+ }
1754
+ renderFileList();
1755
+ }
1756
+ function removeFile(idx){
1757
+ S.pendingFiles.splice(idx,1);
1758
+ renderFileList();
1759
+ }
1760
+ function renderFileList(){
1761
+ if(!S.pendingFiles.length){ D.fileList.style.display='none'; D.fileList.innerHTML=''; return; }
1762
+ D.fileList.style.display='flex';
1763
+ D.fileList.innerHTML=S.pendingFiles.map(function(f,i){
1764
+ return '<div class="file-item">'+
1765
+ '<svg class="file-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>'+
1766
+ '<span class="file-name" title="'+escH(f.name)+'">'+escH(f.name)+'</span>'+
1767
+ '<button class="file-remove" onclick="removeFile('+i+')" title="移除">'+
1768
+ '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>'+
1769
+ '</button></div>';
1770
+ }).join('');
1771
+ }
1772
+ function readFileAsText(file){
1773
+ return new Promise(function(resolve){
1774
+ var reader=new FileReader();
1775
+ reader.onload=function(){ resolve(reader.result); };
1776
+ reader.onerror=function(){ resolve('[读取失败]'); };
1777
+ reader.readAsText(file);
1778
+ });
1779
+ }
1780
+ async function buildFileContent(){
1781
+ var parts=[];
1782
+ for(var i=0;i<S.pendingFiles.length;i++){
1783
+ var f=S.pendingFiles[i];
1784
+ var content=await readFileAsText(f);
1785
+ parts.push('<file name="'+f.name+'">\\n'+content+'\\n</file>');
1786
+ }
1787
+ return parts.join('\\n');
1788
+ }
1789
+
1790
+ async function sendMessage(){
1791
+ var txt=D.input.value.trim();
1792
+ var hasFiles=S.pendingFiles&&S.pendingFiles.length>0;
1793
+ if(!txt&&!hasFiles){ return; }
1794
+ // 拼接文件内容
1795
+ if(hasFiles){
1796
+ var fileContent=await buildFileContent();
1797
+ txt=txt?(txt+'\\n'+fileContent):fileContent;
1798
+ S.pendingFiles=[];
1799
+ renderFileList();
1800
+ }
1801
+ // 用户主动发送消息,确保滚动到底部
1802
+ hideNewMsgTip();
1803
+
1804
+ // 禁用输入框和发送按钮
1805
+ D.input.disabled = true;
1806
+ D.sendBtn.disabled = true;
1807
+
1808
+ // 群组模式
1809
+ if(S.tab==='group'){
1810
+ if(!S.activeGroupId){ alert('请先选择一个群组'); D.input.disabled = false; D.sendBtn.disabled = false; return; }
1811
+ try {
1812
+ D.input.value=''; D.input.style.height='';
1813
+ 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})});
1814
+ var d=await r.json();
1815
+ if(!d.success) alert(d.error||'发送失败');
1816
+ else {
1817
+ // 发送成功:立即追加到本地显示(服务端已存储,不用等 WS 推送)
1818
+ if(d.msg_id){
1819
+ var sentMsg={msg_id:d.msg_id,sender:S.aid,content:txt,content_type:'text',timestamp:d.timestamp||Date.now()};
1820
+ var exists=_lastGroupMsgs.some(function(m){ return m.msg_id===sentMsg.msg_id; });
1821
+ if(!exists){
1822
+ _lastGroupMsgs.push(sentMsg);
1823
+ _lastGroupMsgSig='';
1824
+ renderGroupMsgs(_lastGroupMsgs);
1825
+ scrollToBottom();
1826
+ }
1827
+ }
1828
+ }
1829
+ } catch(e){ alert('发送失败'); }
1830
+ finally {
1831
+ D.input.disabled = false;
1832
+ D.sendBtn.disabled = false;
1833
+ D.input.focus();
1834
+ }
1835
+ return;
1836
+ }
1837
+ // P2P 模式
1838
+ if(!S.sid){ alert('请先选择或新建一个会话'); D.input.disabled = false; D.sendBtn.disabled = false; return; }
1839
+ if(S.closed){ alert('该会话已关闭,请新建会话继续通信'); D.input.disabled = false; D.sendBtn.disabled = false; return; }
1840
+ try {
1841
+ D.input.value=''; D.input.style.height='';
1842
+ 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})});
1843
+ var d=await r.json();
1844
+ if(!d.success) alert(d.error||'发送失败');
1845
+ else { await loadMessages(); scrollToBottom(); }
1846
+ } catch(e){ alert('发送失败'); }
1847
+ finally {
1848
+ D.input.disabled = false;
1849
+ D.sendBtn.disabled = false;
1850
+ D.input.focus();
1851
+ }
1852
+ }
1853
+
1854
+ function toggleSidebar(){
1855
+ S.sidebarOpen=!S.sidebarOpen;
1856
+ D.sidebar.classList.toggle('collapsed',!S.sidebarOpen);
1857
+ }
1858
+
1859
+ function showModal(){ D.modal.classList.add('show'); D.tInput.value=''; D.tInput.focus(); }
1860
+ function hideModal(){ D.modal.classList.remove('show'); }
1861
+
1862
+ async function newSessionWith(peerAid){
1863
+ try {
1864
+ var r=await fetch('/api/ws/connect',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({targetAid:peerAid,aid:S.aid})});
1865
+ var d=await r.json();
1866
+ if(d.success){ pickSession(d.sessionId,peerAid); }
1867
+ else { alert(d.error||'连接失败'); }
1868
+ } catch(e){ alert('错误: '+e.message); }
1869
+ }
1870
+
1871
+ async function doConnect(){
1872
+ var aid=D.tInput.value.trim();
1873
+ if(!aid) return;
1874
+ D.cBtn.disabled=true; D.cBtn.textContent='连接中...';
1875
+ try {
1876
+ var r=await fetch('/api/ws/connect',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({targetAid:aid,aid:S.aid})});
1877
+ var d=await r.json();
1878
+ if(d.success){ hideModal(); pickSession(d.sessionId,aid); }
1879
+ else { alert(d.error||'连接失败'); }
1880
+ } catch(e){ alert('错误: '+e.message); }
1881
+ finally { D.cBtn.disabled=false; D.cBtn.textContent='连接'; }
1882
+ }
1883
+
1884
+ function escH(t){ var d=document.createElement('div'); d.textContent=t; return d.innerHTML; }
1885
+ 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); }); }
1886
+ 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); }
1887
+ function escA(t){ return t.replace(/\\\\/g,'\\\\\\\\').replace(/'/g,"\\\\'"); }
1888
+ function fmtTime(ts){
1889
+ if(!ts) return '';
1890
+ var n=Number(ts);
1891
+ if(isNaN(n)) return '';
1892
+ if(n<1e12) n=n*1000;
1893
+ var d=new Date(n);
1894
+ if(isNaN(d.getTime())) return '';
1895
+ var now=new Date();
1896
+ var pad=function(v){ return v<10?'0'+v:''+v; };
1897
+ var H=pad(d.getHours()), M=pad(d.getMinutes());
1898
+ if(d.getFullYear()===now.getFullYear()&&d.getMonth()===now.getMonth()&&d.getDate()===now.getDate()){
1899
+ return H+':'+M;
1900
+ }
1901
+ // 今年内省略年份
1902
+ if(d.getFullYear()===now.getFullYear()){
1903
+ return pad(d.getMonth()+1)+'/'+pad(d.getDate())+' '+H+':'+M;
1904
+ }
1905
+ return d.getFullYear()+'/'+pad(d.getMonth()+1)+'/'+pad(d.getDate())+' '+H+':'+M;
1906
+ }
1907
+
1908
+ // ============================================================
1909
+ // Group Functions
1910
+ // ============================================================
1911
+
1912
+ function switchTab(tab){
1913
+ S.tab=tab;
1914
+ D.tabP2P.className='tab'+(tab==='p2p'?' active':'');
1915
+ D.tabGroup.className='tab'+(tab==='group'?' active':'');
1916
+ D.p2pPanel.style.display=tab==='p2p'?'flex':'none';
1917
+ if(tab==='p2p') D.p2pPanel.style.flex='1';
1918
+ D.groupPanel.style.flex=tab==='group'?'1':'';
1919
+ D.groupPanel.style.display=tab==='group'?'flex':'none';
1920
+ if(tab==='group'){
1921
+ D.encryptBanner.style.display='none';
1922
+ D.groupInfoBar.style.display=S.activeGroupId?'flex':'none';
1923
+ D.input.placeholder='输入群消息...';
1924
+ D.input.disabled=!S.activeGroupId;
1925
+ D.msgs.dataset.s='';
1926
+ _lastGroupMsgSig='';
1927
+ initGroupClient();
1928
+ pollGroupList();
1929
+ if(S.activeGroupId) pollGroupMessages().then(function(){ scrollToBottom(); });
1930
+ else { D.msgs.innerHTML='<div style="text-align:center;color:#ccc;margin-top:20px;font-size:12px;">选择或创建一个群组</div>'; }
1931
+ } else {
1932
+ D.encryptBanner.style.display='flex';
1933
+ D.groupInfoBar.style.display='none';
1934
+ D.input.placeholder='输入消息...';
1935
+ D.input.disabled=false;
1936
+ _lastGroupMsgSig='';
1937
+ // 立即清空消息区域,防止群消息残留
1938
+ D.msgs.innerHTML='';
1939
+ // 切回P2P时立即刷新会话列表和消息
1940
+ D.sList.dataset.s='';
1941
+ D.msgs.dataset.s='';
1942
+ loadSessions();
1943
+ if(S.sid){
1944
+ fetch('/api/messages?aid='+encodeURIComponent(S.aid)+'&sessionId='+encodeURIComponent(S.sid)).then(function(r){ return r.json(); }).then(function(d){
1945
+ if(S.tab!=='p2p') return;
1946
+ S.closed=d.closed||false;
1947
+ if(d.messages) renderMsgs(d.messages, S.closed);
1948
+ scrollToBottom();
1949
+ }).catch(function(){});
1950
+ }
1951
+ }
1952
+ }
1953
+
1954
+ var _groupInited=false;
1955
+ async function initGroupClient(){
1956
+ if(_groupInited) return;
1957
+ try {
1958
+ var r=await fetch('/api/group/init',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({aid:S.aid})});
1959
+ var d=await r.json();
1960
+ if(d.success){ _groupInited=true; if(d.targetAid) S.groupTargetAid=d.targetAid; }
1961
+ } catch(e){ console.error('群组初始化失败',e); }
1962
+ }
1963
+
1964
+ async function pollGroupList(){
1965
+ try {
1966
+ var r=await fetch('/api/group/list?aid='+encodeURIComponent(S.aid));
1967
+ var d=await r.json();
1968
+ if(d.groups){ S.groups=d.groups; renderGroupList(); }
1969
+ } catch(e){}
1970
+ }
1971
+
1972
+ function renderGroupList(){
1973
+ if(!S.groups.length){
1974
+ D.groupList.innerHTML='<div style="padding:20px;text-align:center;color:#999;font-size:12px;">暂无群组</div>';
1975
+ return;
1976
+ }
1977
+ var html='';
1978
+ S.groups.forEach(function(g){
1979
+ var active=g.group_id===S.activeGroupId;
1980
+ 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>';
1981
+ });
1982
+ D.groupList.innerHTML=html;
1983
+ }
1984
+
1985
+ async function pickGroup(groupId,name){
1986
+ var overlay=$('switchGroupOverlay');
1987
+ var gmsg=$('switchGroupMsg');
1988
+ overlay.style.display='flex';
1989
+ gmsg.textContent='切换群组中...';
1990
+ S.activeGroupId=groupId;
1991
+ S.isGroupCreator=false;
1992
+ _lastGroupMsgSig='';
1993
+ hideNewMsgTip();
1994
+ D.title.textContent=name;
1995
+ D.groupInfoBar.style.display='flex';
1996
+ D.groupInfoText.textContent=name;
1997
+ D.input.disabled=false;
1998
+ D.input.placeholder='输入群消息...';
1999
+ D.input.focus();
2000
+ // 默认隐藏创建者相关按钮
2001
+ $('groupInviteBtn').style.display='none';
2002
+ $('groupCopyLinkBtn').style.display='none';
2003
+ $('groupReviewBtn').style.display='none';
2004
+ $('groupDutyBtn').style.display='none';
2005
+ $('groupRuleBtn').style.display='none';
2006
+ _groupRuleData=null;
2007
+ renderGroupList();
2008
+ try {
2009
+ gmsg.textContent='选择群组...';
2010
+ await fetch('/api/group/select',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({groupId:groupId,aid:S.aid})});
2011
+ } catch(e){}
2012
+ // 获取群信息判断是否为创建者
2013
+ try {
2014
+ gmsg.textContent='获取群信息...';
2015
+ var r=await fetch('/api/group/info?groupId='+encodeURIComponent(groupId)+'&aid='+encodeURIComponent(S.aid));
2016
+ var d=await r.json();
2017
+ if(d.creator&&d.creator===S.aid){
2018
+ S.isGroupCreator=true;
2019
+ $('groupInviteBtn').style.display='';
2020
+ $('groupReviewBtn').style.display='';
2021
+ $('groupDutyBtn').style.display='';
2022
+ } else {
2023
+ $('groupCopyLinkBtn').style.display='';
2024
+ }
2025
+ } catch(e){
2026
+ // 获取失败时默认显示复制群链接
2027
+ $('groupCopyLinkBtn').style.display='';
2028
+ }
2029
+ try {
2030
+ gmsg.textContent='加载消息...';
2031
+ await pollGroupMessages();
2032
+ scrollToBottom();
2033
+ } catch(e){}
2034
+ overlay.style.display='none';
2035
+ }
2036
+
2037
+ var _lastGroupMsgSig='';
2038
+ async function pollGroupMessages(){
2039
+ if(!S.activeGroupId||S.tab!=='group') {
2040
+ console.log('[pollGroupMessages] SKIP: activeGroupId='+S.activeGroupId+' tab='+S.tab);
2041
+ return;
2042
+ }
2043
+ try {
2044
+ console.log('[pollGroupMessages] fetching: groupId='+S.activeGroupId+' aid='+S.aid);
2045
+ var r=await fetch('/api/group/messages?groupId='+encodeURIComponent(S.activeGroupId)+'&aid='+encodeURIComponent(S.aid));
2046
+ var d=await r.json();
2047
+ console.log('[pollGroupMessages] response: msgCount='+(d.messages?d.messages.length:0)+' tab='+S.tab);
2048
+ if(S.tab==='group'&&Array.isArray(d.messages)) renderGroupMsgs(d.messages);
2049
+ } catch(e){ console.error('[pollGroupMessages] error:', e); }
2050
+ }
2051
+
2052
+ // ============================================================
2053
+ // WebSocket: real-time group message push
2054
+ // ============================================================
2055
+ var _groupWs=null;
2056
+ var _groupWsReconnectTimer=null;
2057
+ var _groupWsReconnectDelay=1000; // exponential backoff start
2058
+ var _groupWsPingTimer=null;
2059
+
2060
+ function connectGroupWs(){
2061
+ if(_groupWs&&(_groupWs.readyState===WebSocket.OPEN||_groupWs.readyState===WebSocket.CONNECTING)) return;
2062
+ var proto=location.protocol==='https:'?'wss:':'ws:';
2063
+ _groupWs=new WebSocket(proto+'//'+location.host+'/ws/ui');
2064
+ _groupWs.onopen=function(){
2065
+ console.log('[WS] ui connected');
2066
+ _groupWsReconnectDelay=1000; // reset backoff on success
2067
+ if(_groupWsReconnectTimer){ clearTimeout(_groupWsReconnectTimer); _groupWsReconnectTimer=null; }
2068
+ // 绑定当前 aid
2069
+ if(S.aid) _groupWs.send(JSON.stringify({type:'bind_aid',aid:S.aid}));
2070
+ // 重连后主动拉取最新状态
2071
+ 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(){});
2072
+ fetch('/api/aid').then(function(r){return r.json();}).then(function(d){if(d.aidStatus){S.aidList=d.aidStatus;renderAidSelect();}}).catch(function(){});
2073
+ // 重连后补拉一次会话列表,防止断连期间丢失推送
2074
+ loadSessions();
2075
+ // 启动 keepalive ping(每 25s 发一次,防止代理/防火墙断连)
2076
+ if(_groupWsPingTimer) clearInterval(_groupWsPingTimer);
2077
+ _groupWsPingTimer=setInterval(function(){
2078
+ if(_groupWs&&_groupWs.readyState===WebSocket.OPEN){
2079
+ try{ _groupWs.send(JSON.stringify({type:'ping'})); }catch(e){}
2080
+ }
2081
+ },25000);
2082
+ };
2083
+ _groupWs.onmessage=function(ev){
2084
+ try {
2085
+ var data=JSON.parse(ev.data);
2086
+ handleGroupWsMessage(data);
2087
+ } catch(e){ console.error('[WS] parse error',e); }
2088
+ };
2089
+ _groupWs.onclose=function(){
2090
+ console.log('[WS] ui disconnected, reconnecting in '+_groupWsReconnectDelay+'ms...');
2091
+ _groupWs=null;
2092
+ if(_groupWsPingTimer){ clearInterval(_groupWsPingTimer); _groupWsPingTimer=null; }
2093
+ _groupWsReconnectTimer=setTimeout(function(){
2094
+ _groupWsReconnectDelay=Math.min(_groupWsReconnectDelay*2,30000); // cap at 30s
2095
+ connectGroupWs();
2096
+ },_groupWsReconnectDelay);
2097
+ };
2098
+ _groupWs.onerror=function(e){
2099
+ console.error('[WS] ui error',e);
2100
+ // onerror is always followed by onclose, so reconnect is handled there
2101
+ };
2102
+ }
2103
+
2104
+ function handleGroupWsMessage(data){
2105
+ if(data.type==='ws_status'){
2106
+ if(!data.aid||data.aid===S.aid) updateDot(data.status);
2107
+ return;
2108
+ }
2109
+ if(data.type==='aid_status'){
2110
+ S.aidList=data.aidStatus||[];
2111
+ renderAidSelect();
2112
+ return;
2113
+ }
2114
+ if(data.type==='p2p_message'){
2115
+ // 实时推送的 P2P 消息
2116
+ if(S.tab==='p2p' && data.sessionId===S.sid){
2117
+ loadMessages();
2118
+ }
2119
+ return;
2120
+ }
2121
+ if(data.type==='sessions_updated'){
2122
+ loadSessions();
2123
+ return;
2124
+ }
2125
+ if(data.type==='group_message'){
2126
+ // 实时推送的完整消息
2127
+ var msg=data.message;
2128
+ var gid=data.group_id;
2129
+ if(gid===S.activeGroupId&&S.tab==='group'){
2130
+ // 追加到当前消息列表并重新渲染
2131
+ var exists=_lastGroupMsgs.some(function(m){ return m.msg_id===msg.msg_id; });
2132
+ if(!exists){
2133
+ _lastGroupMsgs.push(msg);
2134
+ _lastGroupMsgSig=''; // 强制重新渲染
2135
+ renderGroupMsgs(_lastGroupMsgs);
2136
+ }
2137
+ }
2138
+ } else if(data.type==='group_message_batch'){
2139
+ // 批量推送的消息列表
2140
+ var gid=data.group_id;
2141
+ var msgs=data.messages||[];
2142
+ 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(','));
2143
+ if(gid===S.activeGroupId&&S.tab==='group'){
2144
+ var changed=false;
2145
+ var existingIds=_lastGroupMsgs.map(function(m){return m.msg_id});
2146
+ console.log('[WS] group_message_batch: currentMsgCount='+_lastGroupMsgs.length+' existingLastId='+(existingIds.length>0?existingIds[existingIds.length-1]:'none'));
2147
+ msgs.forEach(function(msg){
2148
+ var exists=_lastGroupMsgs.some(function(m){ return m.msg_id===msg.msg_id; });
2149
+ if(!exists){
2150
+ _lastGroupMsgs.push(msg);
2151
+ changed=true;
2152
+ console.log('[WS] group_message_batch: ADDED msg_id='+msg.msg_id);
2153
+ } else {
2154
+ console.log('[WS] group_message_batch: SKIP duplicate msg_id='+msg.msg_id);
2155
+ }
2156
+ });
2157
+ console.log('[WS] group_message_batch: changed='+changed+' newTotal='+_lastGroupMsgs.length);
2158
+ if(changed){
2159
+ _lastGroupMsgSig=''; // 强制重新渲染
2160
+ renderGroupMsgs(_lastGroupMsgs);
2161
+ }
2162
+ } else {
2163
+ console.log('[WS] group_message_batch: IGNORED - gid mismatch or wrong tab. gid='+gid+' activeGroupId='+S.activeGroupId+' tab='+S.tab);
2164
+ }
2165
+ } else if(data.type==='new_message_notify'){
2166
+ // 轻量通知:如果是当前活跃群组,拉取最新消息(本地读取,很快)
2167
+ console.log('[WS] new_message_notify: gid='+data.group_id+' activeGroupId='+S.activeGroupId+' tab='+S.tab);
2168
+ if(data.group_id===S.activeGroupId&&S.tab==='group'){
2169
+ console.log('[WS] new_message_notify: triggering pollGroupMessages');
2170
+ pollGroupMessages();
2171
+ }
2172
+ } else if(data.type==='join_approved'||data.type==='group_invite'){
2173
+ // 群组变动,刷新群组列表
2174
+ pollGroupList();
2175
+ }
2176
+ }
2177
+
2178
+ var _lastGroupMsgs=[];
2179
+ var _groupRuleData=null;
2180
+ function renderGroupMsgs(msgs){
2181
+ if(!Array.isArray(msgs)) msgs=[];
2182
+ // 不在群组 tab 时不渲染,防止覆盖 P2P 消息
2183
+ if(S.tab!=='group') return;
2184
+ if(isUserSelecting()) return;
2185
+ var sig=msgs.length+(msgs.length>0?(msgs[msgs.length-1].msg_id||0):'');
2186
+ if(_lastGroupMsgSig===sig&&!msgs._forceRender) return;
2187
+ var prevCount=_lastGroupMsgs.length;
2188
+ _lastGroupMsgSig=sig;
2189
+ _lastGroupMsgs=msgs;
2190
+ // 提取 group.ap 规则消息,保存最新一条,不在列表中展示
2191
+ var ruleMsgs=msgs.filter(function(m){
2192
+ if(!m.content) return false;
2193
+ try { var p=JSON.parse(m.content); return p&&p.source==='group.ap'; } catch(e){ return false; }
2194
+ });
2195
+ if(ruleMsgs.length>0){
2196
+ try { _groupRuleData=JSON.parse(ruleMsgs[ruleMsgs.length-1].content); } catch(e){ _groupRuleData={content:ruleMsgs[ruleMsgs.length-1].content}; }
2197
+ $('groupRuleBtn').style.display='';
2198
+ }
2199
+ var displayMsgs=msgs.filter(function(m){
2200
+ if(!m.content) return true;
2201
+ try { var p=JSON.parse(m.content); return !(p&&p.source==='group.ap'); } catch(e){ return true; }
2202
+ });
2203
+ if(!displayMsgs.length){
2204
+ 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>';
2205
+ D.newMsgTip=$('newMsgTip');
2206
+ return;
2207
+ }
2208
+ var needFetch=[];
2209
+ var html=displayMsgs.map(function(m){
2210
+ var sent=m.sender===S.aid;
2211
+ var sender=m.sender||'unknown';
2212
+ var info=agentInfoCache[sender];
2213
+ if(!info){ needFetch.push(sender); }
2214
+ var avatarSrc=getAvatarSrc(info?info.type:'');
2215
+ var t=m.timestamp?fmtTime(m.timestamp):'';
2216
+
2217
+ var c=(typeof marked!=='undefined'&&marked.parse)?marked.parse(m.content||''):escH(m.content||'');
2218
+ var name=(info&&info.name)?info.name:sender;
2219
+ return '<div class="message '+(sent?'sent':'received')+'">' +
2220
+ '<img class="msg-avatar" src="'+avatarSrc+'" title="'+escH(name)+'">' +
2221
+ '<div class="msg-content">' +
2222
+ '<div class="msg-meta">'+(sent?'我':escH(name))+' · '+t+'</div>' +
2223
+ '<div class="bubble-wrap"><button class="copy-msg-btn" onclick="copyMsgText(this)">复制</button><div class="bubble">'+c+'</div></div>' +
2224
+ '</div></div>';
2225
+ }).join('');
2226
+ var wasAtBottom=isAtBottom();
2227
+ var prevScrollTop=D.msgs.scrollTop;
2228
+ D.msgs.innerHTML=html+'<div class="new-msg-tip" id="newMsgTip" onclick="scrollToBottom()" style="display:none;">↓ 有新消息</div>';
2229
+ D.newMsgTip=$('newMsgTip');
2230
+ // 有新消息且用户不在底部:保持位置,显示提示
2231
+ if(msgs.length>prevCount&&prevCount>0&&!wasAtBottom){
2232
+ D.msgs.scrollTop=prevScrollTop;
2233
+ showNewMsgTip();
2234
+ } else {
2235
+ D.msgs.scrollTop=prevScrollTop;
2236
+ }
2237
+ // 异步加载未缓存的 agent info,加载完成后重新渲染以更新头像
2238
+ var unique=needFetch.filter(function(v,i,a){ return a.indexOf(v)===i; });
2239
+ unique.forEach(function(aid){
2240
+ fetchAgentInfo(aid).then(function(){
2241
+ if(S.tab!=='group') return;
2242
+ _lastGroupMsgSig='';
2243
+ _lastGroupMsgs._forceRender=true;
2244
+ renderGroupMsgs(_lastGroupMsgs);
2245
+ });
2246
+ });
2247
+ }
2248
+
2249
+ // Group modals
2250
+ 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(); }
2251
+ function hideCreateGroupModal(){ $('createGroupModal').classList.remove('show'); }
2252
+ 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'); }
2253
+ 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'); }
2254
+ function showJoinGroupModal(){ $('joinGroupModal').classList.add('show'); $('joinGroupUrlInput').value=''; $('joinGroupUrlInput').focus(); }
2255
+ function hideJoinGroupModal(){ $('joinGroupModal').classList.remove('show'); }
2256
+ function hideMembersModal(){ $('membersModal').classList.remove('show'); }
2257
+
2258
+ 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'); }
2259
+ async function showDutyConfigModal(){
2260
+ var cards=$('dutyConfigCards').children;
2261
+ for(var i=0;i<cards.length;i++){cards[i].classList.remove('selected');}
2262
+ $('dutyConfigModal').classList.add('show');
2263
+ try {
2264
+ var r=await fetch('/api/group/duty-status?groupId='+encodeURIComponent(S.activeGroupId)+'&aid='+encodeURIComponent(S.aid));
2265
+ var d=await r.json();
2266
+ if(d.success&&d.config&&d.config.mode){
2267
+ var mode=d.config.mode;
2268
+ for(var i=0;i<cards.length;i++){
2269
+ if(cards[i].getAttribute('data-value')===mode){ cards[i].classList.add('selected'); }
2270
+ }
2271
+ } else { cards[0].classList.add('selected'); }
2272
+ } catch(e){ cards[0].classList.add('selected'); }
2273
+ }
2274
+ function hideDutyConfigModal(){ $('dutyConfigModal').classList.remove('show'); }
2275
+ function showGroupRuleModal(){
2276
+ if(!_groupRuleData){ alert('暂无群规则数据'); return; }
2277
+ var d=_groupRuleData;
2278
+ var html='';
2279
+ // 值班信息
2280
+ var lines=(d.content||'').split('\\n');
2281
+ html+='<div style="background:#f8f9fa;border-radius:8px;padding:12px;margin-bottom:12px;border:1px solid var(--border);">';
2282
+ html+='<div style="font-weight:600;font-size:13px;margin-bottom:8px;color:var(--primary);">值班信息</div>';
2283
+ for(var i=0;i<lines.length;i++){
2284
+ var line=lines[i].trim();
2285
+ if(!line) continue;
2286
+ var parts=line.split(':');
2287
+ if(parts.length>=2){
2288
+ 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>';
2289
+ } else {
2290
+ html+='<div style="margin-bottom:4px;font-size:12px;line-height:1.5;color:var(--t1);">'+escH(line)+'</div>';
2291
+ }
2292
+ }
2293
+ html+='</div>';
2294
+ // 成员列表
2295
+ if(d.members&&d.members.length){
2296
+ html+='<div style="background:#f8f9fa;border-radius:8px;padding:12px;border:1px solid var(--border);">';
2297
+ html+='<div style="font-weight:600;font-size:13px;margin-bottom:8px;color:var(--primary);">群成员 ('+d.members.length+')</div>';
2298
+ for(var i=0;i<d.members.length;i++){
2299
+ var m=d.members[i];
2300
+ var roleColor=m.role==='creator'?'#e67e22':'#95a5a6';
2301
+ var typeIcon=m.agent_type.indexOf('human')>=0?'\uD83D\uDC64':'\uD83E\uDD16';
2302
+ html+='<div style="display:flex;align-items:center;gap:8px;padding:8px 0;border-bottom:1px solid rgba(0,0,0,0.05);">';
2303
+ html+='<span style="font-size:16px;">'+typeIcon+'</span>';
2304
+ html+='<div style="flex:1;min-width:0;">';
2305
+ html+='<div style="font-size:12px;font-weight:600;color:var(--t1);">'+escH(m.nickname||m.agent_id)+'</div>';
2306
+ html+='<div style="font-size:11px;color:var(--t2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">'+escH(m.agent_id)+'</div>';
2307
+ if(m.capability) html+='<div style="font-size:11px;color:var(--t2);margin-top:2px;line-height:1.3;">'+escH(m.capability)+'</div>';
2308
+ html+='</div>';
2309
+ 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>';
2310
+ html+='</div>';
2311
+ }
2312
+ html+='</div>';
2313
+ }
2314
+ $('groupRuleContent').innerHTML=html;
2315
+ $('groupRuleModal').classList.add('show');
2316
+ }
2317
+ function hideGroupRuleModal(){ $('groupRuleModal').classList.remove('show'); }
2318
+ async function saveDutyConfig(){
2319
+ var sel=document.querySelector('#dutyConfigCards .duty-rule-card.selected');
2320
+ if(!sel){ alert('请选择值班模式'); return; }
2321
+ var mode=sel.getAttribute('data-value');
2322
+ var btn=$('saveDutyConfigBtn');
2323
+ btn.disabled=true; btn.textContent='保存中...';
2324
+ try {
2325
+ 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})});
2326
+ var d=await r.json();
2327
+ if(d.success){ hideDutyConfigModal(); } else { alert(d.error||'保存失败'); }
2328
+ } catch(e){ alert('保存失败: '+e.message); }
2329
+ finally { btn.disabled=false; btn.textContent='保存'; }
2330
+ }
2331
+
2332
+ async function doCreateGroup(){
2333
+ var name=$('groupNameInput').value.trim();
2334
+ if(!name) return;
2335
+ var description=$('groupDescInput').value.trim();
2336
+ if(!description){ $('groupDescInput').focus(); return; }
2337
+ var visibility=document.querySelector('#groupTypeCards .group-type-card.selected').getAttribute('data-value');
2338
+ var dutyMode=document.querySelector('#dutyRuleCards .duty-rule-card.selected').getAttribute('data-value');
2339
+ var btn=$('createGroupBtn');
2340
+ btn.disabled=true; btn.textContent='创建中...';
2341
+ try {
2342
+ 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})});
2343
+ var d=await r.json();
2344
+ if(d.success){
2345
+ hideCreateGroupModal();
2346
+ pollGroupList();
2347
+ pickGroup(d.group_id,name);
2348
+ } else { alert(d.error||'创建失败'); }
2349
+ } catch(e){ alert('创建失败: '+e.message); }
2350
+ finally { btn.disabled=false; btn.textContent='创建'; }
2351
+ }
2352
+
2353
+ async function doJoinGroup(){
2354
+ var rawUrl=$('joinGroupUrlInput').value.trim();
2355
+ if(!rawUrl){ alert('请输入群聊链接或邀请链接'); return; }
2356
+ // 从 URL 中解析 code 参数
2357
+ var code='';
2358
+ var groupUrl=rawUrl;
2359
+ try {
2360
+ var u=new URL(rawUrl);
2361
+ code=u.searchParams.get('code')||'';
2362
+ u.searchParams.delete('code');
2363
+ groupUrl=u.origin+u.pathname;
2364
+ } catch(e){}
2365
+ var btn=$('joinGroupBtn');
2366
+ btn.disabled=true; btn.textContent=code?'加入中...':'申请中...';
2367
+ try {
2368
+ 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})});
2369
+ var d=await r.json();
2370
+ if(d.success){
2371
+ hideJoinGroupModal();
2372
+ pollGroupList();
2373
+ if(d.group_id) pickGroup(d.group_id,d.group_id);
2374
+ if(d.pending) alert('入群申请已发送,请等待管理员审核');
2375
+ } else { alert(d.error||'操作失败'); }
2376
+ } catch(e){ alert('操作失败: '+e.message); }
2377
+ finally { btn.disabled=false; btn.textContent='加入'; }
2378
+ }
2379
+
2380
+ async function copyGroupLink(){
2381
+ if(!S.activeGroupId) return;
2382
+ var groupUrl='https://'+S.groupTargetAid+'/'+S.activeGroupId;
2383
+ try { await navigator.clipboard.writeText(groupUrl); alert('群链接已复制到剪贴板\\n\\n'+groupUrl); }
2384
+ catch(e){ prompt('请复制群链接:',groupUrl); }
2385
+ }
2386
+
2387
+ async function generateInviteLink(){
2388
+ if(!S.activeGroupId) return;
2389
+ try {
2390
+ var r=await fetch('/api/group/invite-code',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({groupId:S.activeGroupId,aid:S.aid})});
2391
+ var d=await r.json();
2392
+ if(d.success&&d.code){
2393
+ var baseUrl=d.group_url||('https://'+S.groupTargetAid+'/'+S.activeGroupId);
2394
+ var inviteUrl=baseUrl+'?code='+d.code;
2395
+ try {
2396
+ await navigator.clipboard.writeText(inviteUrl);
2397
+ alert('邀请链接已复制到剪贴板\\n\\n'+inviteUrl);
2398
+ } catch(e){
2399
+ prompt('请手动复制邀请链接:',inviteUrl);
2400
+ }
2401
+ } else { alert(d.error||'生成邀请码失败'); }
2402
+ } catch(e){ alert('生成邀请码失败: '+e.message); }
2403
+ }
2404
+
2405
+ function copyMemberAid(btn,aid){
2406
+ navigator.clipboard.writeText(aid).then(function(){
2407
+ btn.textContent='已复制';
2408
+ setTimeout(function(){ btn.textContent='复制'; },1200);
2409
+ });
2410
+ }
2411
+
2412
+ async function openAgentMdPage(aid){
2413
+ try {
2414
+ var r=await fetch('/api/agent-md-raw?aid='+encodeURIComponent(aid));
2415
+ var d=await r.json();
2416
+ if(!d.success||!d.content){ alert(d.error||'获取 agent.md 失败'); return; }
2417
+ var md=d.content;
2418
+ // 简单 markdown 渲染
2419
+ function renderMd(src){
2420
+ var h=escH(src);
2421
+ // headings
2422
+ h=h.replace(/^######\\s+(.+)$/gm,'<h6>$1</h6>');
2423
+ h=h.replace(/^#####\\s+(.+)$/gm,'<h5>$1</h5>');
2424
+ h=h.replace(/^####\\s+(.+)$/gm,'<h4>$1</h4>');
2425
+ h=h.replace(/^###\\s+(.+)$/gm,'<h3>$1</h3>');
2426
+ h=h.replace(/^##\\s+(.+)$/gm,'<h2>$1</h2>');
2427
+ h=h.replace(/^#\\s+(.+)$/gm,'<h1>$1</h1>');
2428
+ // bold & italic
2429
+ h=h.replace(/\\*\\*(.+?)\\*\\*/g,'<strong>$1</strong>');
2430
+ h=h.replace(/\\*(.+?)\\*/g,'<em>$1</em>');
2431
+ // blockquote
2432
+ h=h.replace(/^&gt;\\s?(.+)$/gm,'<blockquote style="border-left:3px solid #ddd;padding-left:12px;color:#666;margin:8px 0;">$1</blockquote>');
2433
+ // list items
2434
+ h=h.replace(/^-\\s+(.+)$/gm,'<li>$1</li>');
2435
+ // code inline
2436
+ var bt=String.fromCharCode(96);
2437
+ h=h.replace(new RegExp(bt+'([^'+bt+']+)'+bt,'g'),'<code style="background:#f5f5f5;padding:1px 4px;border-radius:3px;font-size:12px;">$1</code>');
2438
+ // frontmatter block: hide ---...---
2439
+ h=h.replace(/^---[\\s\\S]*?---\\s*/,'');
2440
+ // paragraphs
2441
+ h=h.replace(/\\n\\n/g,'</p><p>');
2442
+ h='<p>'+h+'</p>';
2443
+ return h;
2444
+ }
2445
+ var html='<!DOCTYPE html><html><head><meta charset="utf-8"><title>'+escH(aid)+' - Agent Profile</title>'
2446
+ +'<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;}'
2447
+ +'h1{border-bottom:2px solid #eee;padding-bottom:8px;}h2{border-bottom:1px solid #eee;padding-bottom:6px;margin-top:24px;}'
2448
+ +'ul{padding-left:20px;}li{margin:4px 0;}blockquote{margin:12px 0;}'
2449
+ +'pre{background:#f5f5f5;padding:12px;border-radius:6px;overflow-x:auto;}'
2450
+ +'.aid-badge{display:inline-block;background:#e8f4fd;color:#0969da;padding:2px 8px;border-radius:10px;font-size:12px;font-family:monospace;margin-bottom:16px;}'
2451
+ +'</style></head><body>'
2452
+ +'<div class="aid-badge">'+escH(aid)+'</div>'
2453
+ +renderMd(md)
2454
+ +'</body></html>';
2455
+ var w=window.open('','_blank');
2456
+ if(w){ w.document.write(html); w.document.close(); }
2457
+ else { alert('弹窗被拦截,请允许弹窗后重试'); }
2458
+ } catch(e){ alert('获取 agent.md 失败: '+e.message); }
2459
+ }
2460
+
2461
+ async function showGroupMembers(){
2462
+ if(!S.activeGroupId) return;
2463
+ try {
2464
+ var r=await fetch('/api/group/members?groupId='+encodeURIComponent(S.activeGroupId)+'&aid='+encodeURIComponent(S.aid));
2465
+ var d=await r.json();
2466
+ if(d.members){
2467
+ var html=d.members.map(function(m){
2468
+ var aid=m.agent_id||m;
2469
+ if(typeof aid!=='string') aid=JSON.stringify(aid);
2470
+ var role=m.role||'';
2471
+ var cachedInfo=agentInfoCache[aid];
2472
+ var avatarSrc=getAvatarSrc(cachedInfo?cachedInfo.type:'');
2473
+ var displayName=(cachedInfo&&cachedInfo.name)?cachedInfo.name:aid.split('.')[0];
2474
+ var typeTags='';
2475
+ if(cachedInfo&&cachedInfo.tags&&cachedInfo.tags.length){
2476
+ 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('');
2477
+ } else if(cachedInfo&&cachedInfo.type){
2478
+ typeTags='<span style="display:inline-block;background:#e8f4fd;color:#0969da;padding:1px 6px;border-radius:8px;font-size:10px;">'+escH(cachedInfo.type)+'</span>';
2479
+ }
2480
+ 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>'; }
2481
+ var safeId='member-'+escH(aid).replace(/\\./g,'_');
2482
+ return '<div id="'+safeId+'" style="padding:10px 0;border-bottom:1px solid #f3f4f6;display:flex;align-items:center;gap:10px;">'
2483
+ +'<img src="'+avatarSrc+'" style="width:36px;height:36px;border-radius:50%;flex-shrink:0;" class="member-avatar" data-aid="'+escH(aid)+'">'
2484
+ +'<div style="flex:1;min-width:0;">'
2485
+ +'<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap;">'
2486
+ +'<span style="font-size:13px;font-weight:500;" class="member-name" data-aid="'+escH(aid)+'">'+escH(displayName)+'</span>'
2487
+ +'<span class="member-tags" data-aid="'+escH(aid)+'">'+typeTags+'</span>'
2488
+ +'</div>'
2489
+ +'<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>'
2490
+ +'</div>'
2491
+ +'<div style="display:flex;gap:4px;flex-shrink:0;">'
2492
+ +'<button class="mbtn mbtn-ok" style="padding:4px 10px;font-size:11px;" onclick="copyMemberAid(this,\\''+escH(aid)+'\\')">复制</button>'
2493
+ +'<button class="mbtn mbtn-cancel" style="padding:4px 10px;font-size:11px;" onclick="openAgentMdPage(\\''+escH(aid)+'\\')">查看</button>'
2494
+ +'</div></div>';
2495
+ }).join('');
2496
+ $('membersList').innerHTML=html||'<div style="color:#999;">暂无成员</div>';
2497
+ // 异步加载未缓存的 agent info
2498
+ d.members.forEach(function(m){
2499
+ var aid=m.agent_id||m;
2500
+ if(typeof aid!=='string') aid=JSON.stringify(aid);
2501
+ if(!aid||agentInfoCache[aid]) return;
2502
+ fetchAgentInfo(aid).then(function(info){
2503
+ if(!info||(!info.name&&!info.type)) return;
2504
+ var safeId='member-'+aid.replace(/\\./g,'_');
2505
+ var el=document.getElementById(safeId);
2506
+ if(!el) return;
2507
+ var avatarEl=el.querySelector('.member-avatar[data-aid="'+aid+'"]');
2508
+ var nameEl=el.querySelector('.member-name[data-aid="'+aid+'"]');
2509
+ var tagsEl=el.querySelector('.member-tags[data-aid="'+aid+'"]');
2510
+ if(avatarEl) avatarEl.src=getAvatarSrc(info.type);
2511
+ if(nameEl) nameEl.textContent=info.name||aid.split('.')[0];
2512
+ if(tagsEl){
2513
+ var tags='';
2514
+ if(info.tags&&info.tags.length){
2515
+ 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('');
2516
+ } else if(info.type){
2517
+ tags='<span style="display:inline-block;background:#e8f4fd;color:#0969da;padding:1px 6px;border-radius:8px;font-size:10px;">'+escH(info.type)+'</span>';
2518
+ }
2519
+ // 保留已有的 role tag
2520
+ var existingRole=tagsEl.querySelector('span[style*="fff3cd"]');
2521
+ tagsEl.innerHTML=tags+(existingRole?existingRole.outerHTML:'');
2522
+ }
2523
+ });
2524
+ });
2525
+ } else { $('membersList').innerHTML='<div style="color:#999;">获取失败</div>'; }
2526
+ $('membersModal').classList.add('show');
2527
+ } catch(e){ alert('获取成员失败: '+e.message); }
2528
+ }
2529
+
2530
+ function hidePendingRequestsModal(){ $('pendingRequestsModal').classList.remove('show'); }
2531
+
2532
+ async function showPendingRequests(){
2533
+ if(!S.activeGroupId) return;
2534
+ try {
2535
+ var r=await fetch('/api/group/pending-requests?groupId='+encodeURIComponent(S.activeGroupId)+'&aid='+encodeURIComponent(S.aid));
2536
+ var d=await r.json();
2537
+ if(d.requests&&d.requests.length>0){
2538
+ // 先渲染基础结构,然后异步加载 agent info
2539
+ var html=d.requests.map(function(req){
2540
+ var aid=req.agent_id||'';
2541
+ var msg=req.message?escH(req.message):'';
2542
+ var time=req.created_at?fmtTime(req.created_at):'';
2543
+ var cachedInfo=agentInfoCache[aid];
2544
+ var avatarSrc=getAvatarSrc(cachedInfo?cachedInfo.type:'');
2545
+ var displayName=(cachedInfo&&cachedInfo.name)?cachedInfo.name:aid;
2546
+ var desc=(cachedInfo&&cachedInfo.description)?cachedInfo.description:'';
2547
+ 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;">'
2548
+ +'<img src="'+avatarSrc+'" style="width:36px;height:36px;border-radius:50%;flex-shrink:0;margin-top:2px;" class="pending-avatar" data-aid="'+escH(aid)+'">'
2549
+ +'<div style="flex:1;min-width:0;">'
2550
+ +'<div style="font-size:13px;font-weight:500;" class="pending-name" data-aid="'+escH(aid)+'">'+escH(displayName)+'</div>'
2551
+ +'<div style="font-size:11px;color:var(--t2);font-family:monospace;margin-top:2px;">'+escH(aid)+'</div>'
2552
+ +'<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>'
2553
+ +(msg?'<div style="font-size:11px;color:#666;margin-top:4px;background:#f8f9fa;padding:4px 8px;border-radius:4px;">申请留言: '+msg+'</div>':'')
2554
+ +(time?'<div style="font-size:10px;color:var(--t2);margin-top:3px;">'+time+'</div>':'')
2555
+ +'</div>'
2556
+ +'<div style="display:flex;gap:4px;flex-shrink:0;margin-top:2px;">'
2557
+ +'<button class="mbtn mbtn-ok" style="padding:4px 10px;font-size:11px;" onclick="reviewJoin(\\''+escH(aid)+'\\',\\'approve\\')">通过</button>'
2558
+ +'<button class="mbtn mbtn-cancel" style="padding:4px 10px;font-size:11px;" onclick="reviewJoin(\\''+escH(aid)+'\\',\\'reject\\')">拒绝</button>'
2559
+ +'</div></div>';
2560
+ }).join('');
2561
+ $('pendingRequestsList').innerHTML=html;
2562
+ // 异步加载未缓存的 agent info
2563
+ d.requests.forEach(function(req){
2564
+ var aid=req.agent_id||'';
2565
+ if(!aid||agentInfoCache[aid]) return;
2566
+ fetchAgentInfo(aid).then(function(info){
2567
+ if(!info||(!info.name&&!info.type)) return;
2568
+ var safeId='pending-'+aid.replace(/\\./g,'_');
2569
+ var el=document.getElementById(safeId);
2570
+ if(!el) return;
2571
+ var avatarEl=el.querySelector('.pending-avatar[data-aid="'+aid+'"]');
2572
+ var nameEl=el.querySelector('.pending-name[data-aid="'+aid+'"]');
2573
+ var descEl=el.querySelector('.pending-desc[data-aid="'+aid+'"]');
2574
+ if(avatarEl) avatarEl.src=getAvatarSrc(info.type);
2575
+ if(nameEl) nameEl.textContent=info.name||aid;
2576
+ if(descEl&&info.description){ descEl.textContent=info.description; descEl.style.display='block'; }
2577
+ });
2578
+ });
2579
+ } else {
2580
+ $('pendingRequestsList').innerHTML='<div style="padding:16px;text-align:center;color:#999;font-size:12px;">暂无入群申请</div>';
2581
+ }
2582
+ $('pendingRequestsModal').classList.add('show');
2583
+ } catch(e){ alert('获取入群申请失败: '+e.message); }
2584
+ }
2585
+
2586
+ async function reviewJoin(agentId,action){
2587
+ if(!S.activeGroupId) return;
2588
+ try {
2589
+ 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})});
2590
+ var d=await r.json();
2591
+ if(d.success){ showPendingRequests(); }
2592
+ else { alert(d.error||'操作失败'); }
2593
+ } catch(e){ alert('操作失败: '+e.message); }
2594
+ }
2595
+
2596
+ async function leaveGroup(groupId){
2597
+ if(!confirm('确认退出该群组?')) return;
2598
+ try {
2599
+ var r=await fetch('/api/group/leave',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({groupId:groupId,aid:S.aid})});
2600
+ var d=await r.json();
2601
+ if(d.success){
2602
+ if(S.activeGroupId===groupId){
2603
+ S.activeGroupId=null;
2604
+ D.title.textContent='未选择群组';
2605
+ D.groupInfoBar.style.display='none';
2606
+ D.msgs.innerHTML='';
2607
+ D.input.disabled=true;
2608
+ }
2609
+ pollGroupList();
2610
+ } else { alert(d.error||'退出失败'); }
2611
+ } catch(e){ alert('退出失败: '+e.message); }
2612
+ }
2613
+
2614
+ // ============================================================
2615
+ // 我的群 Functions
2616
+ // ============================================================
2617
+ function showMyGroupsModal(){ $('myGroupsModal').classList.add('show'); }
2618
+ function hideMyGroupsModal(){ $('myGroupsModal').classList.remove('show'); }
2619
+ async function showMyGroups(){
2620
+ showMyGroupsModal();
2621
+ $('myGroupsContent').innerHTML='<div style="text-align:center;padding:20px;color:#999;">加载中...</div>';
2622
+ try {
2623
+ var r=await fetch('/api/group/my-groups?aid='+encodeURIComponent(S.aid));
2624
+ var d=await r.json();
2625
+ if(!d.success){ $('myGroupsContent').innerHTML='<div style="text-align:center;padding:20px;color:#e74c3c;">'+escH(d.error||'获取失败')+'</div>'; return; }
2626
+ var groups=d.groups||[];
2627
+ if(!groups.length){ $('myGroupsContent').innerHTML='<div style="text-align:center;padding:20px;color:#999;">暂无群组</div>'; return; }
2628
+ var html='<table style="width:100%;border-collapse:collapse;font-size:12px;">';
2629
+ 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>';
2630
+ groups.forEach(function(g){
2631
+ var statusText=g.status===1?'正常':g.status===0?'待审核':'未知('+g.status+')';
2632
+ var statusColor=g.status===1?'#10b981':g.status===0?'#f59e0b':'#94a3b8';
2633
+ var shortId=g.group_id.length>16?g.group_id.substring(0,16)+'...':g.group_id;
2634
+ html+='<tr style="border-bottom:1px solid #f1f5f9;cursor:pointer;" onmouseover="this.style.background=\\'#f0f9ff\\'" onmouseout="this.style.background=\\'\\'">';
2635
+ html+='<td style="padding:8px 6px;font-weight:500;">'+escH(g.name||g.group_id)+'</td>';
2636
+ html+='<td style="padding:8px 6px;color:#64748b;" title="'+escH(g.group_id)+'">'+escH(shortId)+'</td>';
2637
+ html+='<td style="padding:8px 6px;text-align:center;">'+escH(g.role||'-')+'</td>';
2638
+ html+='<td style="padding:8px 6px;text-align:center;"><span style="color:'+statusColor+';font-weight:500;">'+escH(statusText)+'</span></td>';
2639
+ html+='</tr>';
2640
+ });
2641
+ html+='</table>';
2642
+ html+='<div style="margin-top:8px;font-size:11px;color:#94a3b8;text-align:right;">共 '+d.total+' 个群组</div>';
2643
+ $('myGroupsContent').innerHTML=html;
2644
+ } catch(e){ $('myGroupsContent').innerHTML='<div style="text-align:center;padding:20px;color:#e74c3c;">请求失败: '+escH(e.message)+'</div>'; }
2645
+ }
2646
+
2647
+ // 扩展轮询:保留 P2P 等基础轮询,群组消息已通过 WebSocket 实时推送
2648
+ // 不再每秒轮询群消息
2649
+
2650
+ init();
2651
+ <\/script>
2652
+ </body>
2228
2653
  </html>`;
2229
2654
  function sendJson(res, data, status = 200) {
2230
2655
  res.writeHead(status, { 'Content-Type': 'application/json' });
@@ -2349,7 +2774,7 @@ async function handleRequest(req, res) {
2349
2774
  if (parts.length >= 3) {
2350
2775
  const domain = parts.slice(1).join('.');
2351
2776
  if (domain && domain !== globalApiUrl) {
2352
- console.log(`[Select] 切换 AP 为 ${domain} (AID: ${aid})`);
2777
+ utils_1.logger.log(`[Select] 切换 AP 为 ${domain} (AID: ${aid})`);
2353
2778
  globalApiUrl = domain;
2354
2779
  agentCP = new agentcp_1.AgentCP(domain, '', globalDataDir || undefined);
2355
2780
  }
@@ -2366,7 +2791,7 @@ async function handleRequest(req, res) {
2366
2791
  await ensureOnline(aid);
2367
2792
  }
2368
2793
  catch (e) {
2369
- console.warn(`[Select] AID ${aid} 自动上线失败:`, e.message);
2794
+ utils_1.logger.warn(`[Select] AID ${aid} 自动上线失败:`, e.message);
2370
2795
  }
2371
2796
  sendJson(res, { success: true, aid });
2372
2797
  }
@@ -2398,7 +2823,7 @@ async function handleRequest(req, res) {
2398
2823
  if (parts.length >= 3) {
2399
2824
  const domain = parts.slice(1).join('.');
2400
2825
  if (domain && domain !== globalApiUrl) {
2401
- console.log(`[Create] 切换 AP 为 ${domain} (AID: ${aid})`);
2826
+ utils_1.logger.log(`[Create] 切换 AP 为 ${domain} (AID: ${aid})`);
2402
2827
  globalApiUrl = domain;
2403
2828
  agentCP = new agentcp_1.AgentCP(domain, '', globalDataDir || undefined);
2404
2829
  }
@@ -2465,13 +2890,13 @@ async function handleRequest(req, res) {
2465
2890
  await instance.agentCP.leaveAllGroupSessions();
2466
2891
  }
2467
2892
  catch (e) {
2468
- console.warn(`[Group] leaveAllGroupSessions error:`, e.message);
2893
+ utils_1.logger.warn(`[Group] leaveAllGroupSessions error:`, e.message);
2469
2894
  }
2470
2895
  try {
2471
2896
  await instance.agentCP.closeGroupMessageStore();
2472
2897
  }
2473
2898
  catch (e) {
2474
- console.warn(`[Group] closeGroupMessageStore error:`, e.message);
2899
+ utils_1.logger.warn(`[Group] closeGroupMessageStore error:`, e.message);
2475
2900
  }
2476
2901
  }
2477
2902
  if (instance.heartbeatClient) {
@@ -2481,7 +2906,7 @@ async function handleRequest(req, res) {
2481
2906
  instance.agentWS.disconnect();
2482
2907
  }
2483
2908
  aidInstances.delete(aid);
2484
- console.log(`[Server] AID ${aid} 已下线`);
2909
+ utils_1.logger.log(`[Server] AID ${aid} 已下线`);
2485
2910
  // 下线后推送 AID 状态变更到前端
2486
2911
  getAidStatusList().then(aidStatus => {
2487
2912
  broadcastToBrowser({ type: 'aid_status', aidStatus });
@@ -2517,6 +2942,19 @@ async function handleRequest(req, res) {
2517
2942
  sendJson(res, { success: false, error: '缺少 aid' });
2518
2943
  return;
2519
2944
  }
2945
+ // 验证目标 Agent 是否存在
2946
+ try {
2947
+ const agentMdUrl = `https://${targetAid}/agent.md`;
2948
+ const checkRes = await fetch(agentMdUrl, { method: 'GET', signal: AbortSignal.timeout(5000) });
2949
+ if (!checkRes.ok) {
2950
+ sendJson(res, { success: false, error: '该 AGENT 不存在,添加失败' });
2951
+ return;
2952
+ }
2953
+ }
2954
+ catch (_b) {
2955
+ sendJson(res, { success: false, error: '该 AGENT 不存在,添加失败' });
2956
+ return;
2957
+ }
2520
2958
  // 自动上线
2521
2959
  const instance = await ensureOnline(aid);
2522
2960
  if (!instance.agentWS) {
@@ -2538,7 +2976,7 @@ async function handleRequest(req, res) {
2538
2976
  identifyingCode: sessionInfo.identifyingCode
2539
2977
  });
2540
2978
  }, (status) => {
2541
- console.log('邀请状态:', status);
2979
+ utils_1.logger.log('邀请状态:', status);
2542
2980
  });
2543
2981
  });
2544
2982
  // 创建 outgoing session
@@ -2713,7 +3151,7 @@ async function handleRequest(req, res) {
2713
3151
  if (pathname === '/api/group/create' && method === 'POST') {
2714
3152
  try {
2715
3153
  const body = await parseBody(req);
2716
- const { name, visibility, description, aid } = body;
3154
+ const { name, visibility, description, duty_mode, aid } = body;
2717
3155
  if (!aid) {
2718
3156
  sendJson(res, { success: false, error: '缺少 aid' });
2719
3157
  return;
@@ -2732,7 +3170,17 @@ async function handleRequest(req, res) {
2732
3170
  if (description)
2733
3171
  options.description = description;
2734
3172
  const result = await ops.createGroup(target, name, options);
2735
- console.log('[ACP] createGroup 返回:', JSON.stringify(result, null, 2));
3173
+ utils_1.logger.log('[ACP] createGroup 返回:', JSON.stringify(result, null, 2));
3174
+ // 设置值班规则
3175
+ if (duty_mode && result.group_id) {
3176
+ try {
3177
+ await ops.updateDutyConfig(target, result.group_id, { mode: duty_mode });
3178
+ utils_1.logger.log('[ACP] 值班规则已设置:', duty_mode);
3179
+ }
3180
+ catch (e) {
3181
+ utils_1.logger.warn('[ACP] 设置值班规则失败:', e.message);
3182
+ }
3183
+ }
2736
3184
  // 加入本地存储
2737
3185
  instance.agentCP.addGroupToStore(result.group_id, name);
2738
3186
  // 注册在线,才能收到实时消息推送
@@ -2760,7 +3208,7 @@ async function handleRequest(req, res) {
2760
3208
  instance.groupListSynced = true;
2761
3209
  }
2762
3210
  catch (syncErr) {
2763
- console.warn('[Group] syncGroupList error:', syncErr.message);
3211
+ utils_1.logger.warn('[Group] syncGroupList error:', syncErr.message);
2764
3212
  }
2765
3213
  }
2766
3214
  const groups = instance.agentCP.getLocalGroupList();
@@ -2856,7 +3304,8 @@ async function handleRequest(req, res) {
2856
3304
  await ensureGroupClient(instance);
2857
3305
  // 只读本地缓存,不再每次请求都去服务端拉取
2858
3306
  // 新消息通过 WebSocket 推送实时到达并由 SDK 自动存储
2859
- const messages = instance.agentCP.getLocalGroupMessages(groupId);
3307
+ const messages = instance.agentCP.getLocalGroupMessages(groupId) || [];
3308
+ 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}`);
2860
3309
  sendJson(res, { success: true, messages });
2861
3310
  }
2862
3311
  catch (e) {
@@ -2907,7 +3356,7 @@ async function handleRequest(req, res) {
2907
3356
  let groupName = groupId;
2908
3357
  try {
2909
3358
  const info = await instance.agentCP.groupOps.getGroupInfo(targetAid, groupId);
2910
- groupName = info.name || groupId;
3359
+ groupName = (info && info.name) || groupId;
2911
3360
  }
2912
3361
  catch (_) { }
2913
3362
  instance.agentCP.addGroupToStore(groupId, groupName);
@@ -3018,11 +3467,11 @@ async function handleRequest(req, res) {
3018
3467
  const result = await ops.listMyGroups(target);
3019
3468
  // 尝试获取每个群的详细信息(名称等)
3020
3469
  const groups = [];
3021
- for (const m of result.groups) {
3470
+ for (const m of (result.groups || [])) {
3022
3471
  let name = m.group_id;
3023
3472
  try {
3024
3473
  const info = await ops.getGroupInfo(target, m.group_id);
3025
- name = info.name || m.group_id;
3474
+ name = (info && info.name) || m.group_id;
3026
3475
  }
3027
3476
  catch (_) { }
3028
3477
  groups.push(Object.assign(Object.assign({}, m), { name }));
@@ -3059,6 +3508,59 @@ async function handleRequest(req, res) {
3059
3508
  }
3060
3509
  return;
3061
3510
  }
3511
+ if (pathname === '/api/group/duty-status' && method === 'GET') {
3512
+ try {
3513
+ const aid = parsedUrl.query.aid;
3514
+ const groupId = parsedUrl.query.groupId;
3515
+ if (!aid) {
3516
+ sendJson(res, { success: false, error: '缺少 aid' });
3517
+ return;
3518
+ }
3519
+ if (!groupId) {
3520
+ sendJson(res, { success: false, error: '缺少 groupId' });
3521
+ return;
3522
+ }
3523
+ const instance = await ensureOnline(aid);
3524
+ await ensureGroupClient(instance);
3525
+ const ops = instance.agentCP.groupOps;
3526
+ const target = instance.groupTargetAid;
3527
+ const result = await ops.getDutyStatus(target, groupId);
3528
+ sendJson(res, { success: true, config: result.config, state: result.state });
3529
+ }
3530
+ catch (e) {
3531
+ sendJson(res, { success: false, error: e.message });
3532
+ }
3533
+ return;
3534
+ }
3535
+ if (pathname === '/api/group/update-duty-config' && method === 'POST') {
3536
+ try {
3537
+ const body = await parseBody(req);
3538
+ const { groupId, aid, mode } = body;
3539
+ if (!aid) {
3540
+ sendJson(res, { success: false, error: '缺少 aid' });
3541
+ return;
3542
+ }
3543
+ if (!groupId) {
3544
+ sendJson(res, { success: false, error: '缺少 groupId' });
3545
+ return;
3546
+ }
3547
+ if (!mode) {
3548
+ sendJson(res, { success: false, error: '缺少 mode' });
3549
+ return;
3550
+ }
3551
+ const instance = await ensureOnline(aid);
3552
+ await ensureGroupClient(instance);
3553
+ const ops = instance.agentCP.groupOps;
3554
+ const target = instance.groupTargetAid;
3555
+ await ops.updateDutyConfig(target, groupId, { mode });
3556
+ utils_1.logger.log('[ACP] 值班规则已更新:', mode, 'groupId:', groupId);
3557
+ sendJson(res, { success: true });
3558
+ }
3559
+ catch (e) {
3560
+ sendJson(res, { success: false, error: e.message });
3561
+ }
3562
+ return;
3563
+ }
3062
3564
  // 404
3063
3565
  res.writeHead(404);
3064
3566
  res.end('Not Found');
@@ -3092,16 +3594,16 @@ function startServer(port, apiUrl, dataDir = '') {
3092
3594
  if (parts.length >= 3) {
3093
3595
  const domain = parts.slice(1).join('.');
3094
3596
  if (domain !== globalApiUrl) {
3095
- console.log(`[Server] 检测到 AID 所属 AP 为 ${domain},正在切换...`);
3597
+ utils_1.logger.log(`[Server] 检测到 AID 所属 AP 为 ${domain},正在切换...`);
3096
3598
  globalApiUrl = domain;
3097
3599
  agentCP = new agentcp_1.AgentCP(domain, '', dataDir || undefined, { persistMessages: true, persistGroupMessages: true });
3098
3600
  await agentCP.loadCurrentAid();
3099
3601
  }
3100
3602
  }
3101
- console.log(`已加载 AID: ${aid}`);
3603
+ utils_1.logger.log(`已加载 AID: ${aid}`);
3102
3604
  // 加载该 AID 的持久化会话
3103
3605
  await ensureMessageStoreLoaded(aid);
3104
- console.log(`已加载会话`);
3606
+ utils_1.logger.log(`已加载会话`);
3105
3607
  }
3106
3608
  }).catch(() => { });
3107
3609
  const server = http.createServer(handleRequest);
@@ -3128,7 +3630,7 @@ function startServer(port, apiUrl, dataDir = '') {
3128
3630
  browserWsClients.set(ws, client);
3129
3631
  wsAliveMap.set(ws, true);
3130
3632
  ws.on('pong', () => wsAliveMap.set(ws, true));
3131
- console.log(`[WS] browser client connected, total=${browserWsClients.size}`);
3633
+ utils_1.logger.log(`[WS] browser client connected, total=${browserWsClients.size}`);
3132
3634
  ws.on('message', (raw) => {
3133
3635
  try {
3134
3636
  const msg = JSON.parse(raw.toString());
@@ -3152,10 +3654,10 @@ function startServer(port, apiUrl, dataDir = '') {
3152
3654
  });
3153
3655
  ws.on('close', () => {
3154
3656
  browserWsClients.delete(ws);
3155
- console.log(`[WS] browser client disconnected, total=${browserWsClients.size}`);
3657
+ utils_1.logger.log(`[WS] browser client disconnected, total=${browserWsClients.size}`);
3156
3658
  });
3157
3659
  ws.on('error', (err) => {
3158
- console.error('[WS] browser client error:', err.message);
3660
+ utils_1.logger.error('[WS] browser client error:', err.message);
3159
3661
  browserWsClients.delete(ws);
3160
3662
  });
3161
3663
  });
@@ -3166,7 +3668,7 @@ function startServer(port, apiUrl, dataDir = '') {
3166
3668
  });
3167
3669
  // 资源清理函数
3168
3670
  const cleanup = async () => {
3169
- console.log('\n正在关闭服务...');
3671
+ utils_1.logger.log('\n正在关闭服务...');
3170
3672
  clearInterval(wssHeartbeat);
3171
3673
  // 持久化 agent info 缓存
3172
3674
  saveAgentInfoCacheToDisk();
@@ -3175,25 +3677,25 @@ function startServer(port, apiUrl, dataDir = '') {
3175
3677
  for (const [aid, store] of messageStores) {
3176
3678
  await store.flushAll();
3177
3679
  }
3178
- console.log('[Server] 会话已保存');
3680
+ utils_1.logger.log('[Server] 会话已保存');
3179
3681
  }
3180
3682
  catch (e) {
3181
- console.error('[Server] 保存会话失败:', e);
3683
+ utils_1.logger.error('[Server] 保存会话失败:', e);
3182
3684
  }
3183
3685
  for (const [aid, instance] of aidInstances) {
3184
- console.log(`[Server] 清理 AID: ${aid}`);
3686
+ utils_1.logger.log(`[Server] 清理 AID: ${aid}`);
3185
3687
  if (instance.groupInitialized) {
3186
3688
  try {
3187
3689
  await instance.agentCP.leaveAllGroupSessions();
3188
3690
  }
3189
3691
  catch (e) {
3190
- console.warn(`[Server] leaveAllGroupSessions error:`, e.message);
3692
+ utils_1.logger.warn(`[Server] leaveAllGroupSessions error:`, e.message);
3191
3693
  }
3192
3694
  try {
3193
3695
  await instance.agentCP.closeGroupMessageStore();
3194
3696
  }
3195
3697
  catch (e) {
3196
- console.warn(`[Server] closeGroupMessageStore error:`, e.message);
3698
+ utils_1.logger.warn(`[Server] closeGroupMessageStore error:`, e.message);
3197
3699
  }
3198
3700
  }
3199
3701
  if (instance.heartbeatClient) {
@@ -3211,7 +3713,7 @@ function startServer(port, apiUrl, dataDir = '') {
3211
3713
  browserWsClients.clear();
3212
3714
  wss.close();
3213
3715
  server.close(() => {
3214
- console.log('服务已关闭');
3716
+ utils_1.logger.log('服务已关闭');
3215
3717
  process.exit(0);
3216
3718
  });
3217
3719
  };
@@ -3221,20 +3723,20 @@ function startServer(port, apiUrl, dataDir = '') {
3221
3723
  // 处理端口占用错误
3222
3724
  server.on('error', (err) => {
3223
3725
  if (err.code === 'EADDRINUSE') {
3224
- console.error(`\n 错误: 端口 ${port} 已被占用`);
3225
- console.error(` 请使用 -p 参数指定其他端口,或关闭占用该端口的程序\n`);
3726
+ utils_1.logger.error(`\n 错误: 端口 ${port} 已被占用`);
3727
+ utils_1.logger.error(` 请使用 -p 参数指定其他端口,或关闭占用该端口的程序\n`);
3226
3728
  process.exit(1);
3227
3729
  }
3228
3730
  throw err;
3229
3731
  });
3230
3732
  server.listen(port, () => {
3231
- console.log(`\n ACP 身份管理服务已启动`);
3232
- console.log(` ─────────────────────────`);
3233
- console.log(` 本地地址: http://localhost:${port}`);
3234
- console.log(` API 服务: ${apiUrl}`);
3733
+ utils_1.logger.log(`\n ACP 身份管理服务已启动`);
3734
+ utils_1.logger.log(` ─────────────────────────`);
3735
+ utils_1.logger.log(` 本地地址: http://localhost:${port}`);
3736
+ utils_1.logger.log(` API 服务: ${apiUrl}`);
3235
3737
  if (dataDir) {
3236
- console.log(` 数据目录: ${dataDir}`);
3738
+ utils_1.logger.log(` 数据目录: ${dataDir}`);
3237
3739
  }
3238
- console.log(`\n 按 Ctrl+C 停止服务\n`);
3740
+ utils_1.logger.log(`\n 按 Ctrl+C 停止服务\n`);
3239
3741
  });
3240
3742
  }