acp-ts 1.2.4 → 1.2.6

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/agentcp.d.ts CHANGED
@@ -177,6 +177,19 @@ declare class AgentCP implements IAgentCP {
177
177
  * @param limit 每次拉取数量上限,0 表示使用服务端默认值
178
178
  */
179
179
  pullAndStoreGroupMessages(groupId: string, afterMsgId?: number, limit?: number): Promise<GroupMessage[]>;
180
+ /**
181
+ * 将群组加入在线列表(不发送网络请求)。
182
+ */
183
+ addOnlineGroup(groupId: string): void;
184
+ /**
185
+ * 确保群组心跳定时器已启动(公开方法)。
186
+ */
187
+ ensureGroupHeartbeat(): void;
188
+ /**
189
+ * 向 group.ap 发送一次 register_online,告知当前客户端在线。
190
+ * 不带群组 ID,只需在启动或重连时调用一次。
191
+ */
192
+ groupRegisterOnline(): Promise<void>;
180
193
  /**
181
194
  * 加入群组会话(完整生命周期):
182
195
  * 1. register_online → 告知 group.ap 在线
package/dist/agentcp.js CHANGED
@@ -77,7 +77,7 @@ class AgentCP {
77
77
  }
78
78
  const baseUrl = `https://acp3.${apiUrl}`;
79
79
  this.seedPassword = seedPassword;
80
- this._basePath = basePath || process.cwd();
80
+ this._basePath = basePath || datamanager_1.DEFAULT_ACP_DIR;
81
81
  this.apUrl = `${baseUrl}/api/accesspoint`;
82
82
  this.msgUrl = `${baseUrl}/api/message`;
83
83
  this._persistGroupMessages = (_a = options === null || options === void 0 ? void 0 : options.persistGroupMessages) !== null && _a !== void 0 ? _a : true;
@@ -460,8 +460,9 @@ class AgentCP {
460
460
  * 返回排序后的消息列表,供上层使用(如推送给浏览器)。
461
461
  */
462
462
  async processAndAckBatch(groupId, batch) {
463
- const sorted = [...batch.messages].sort((a, b) => a.msg_id - b.msg_id);
464
- utils_1.logger.log(`[AgentCP] processAndAckBatch: group=${groupId} batchCount=${batch.messages.length} sortedCount=${sorted.length} msgIds=[${sorted.map(m => m.msg_id).join(',')}]`);
463
+ const batchMessages = batch.messages || [];
464
+ const sorted = [...batchMessages].sort((a, b) => a.msg_id - b.msg_id);
465
+ utils_1.logger.log(`[AgentCP] processAndAckBatch: group=${groupId} batchCount=${batchMessages.length} sortedCount=${sorted.length} msgIds=[${sorted.map(m => m.msg_id).join(',')}]`);
465
466
  const storeExists = !!this.groupMessageStore;
466
467
  const storeGroupExists = storeExists ? !!this.groupMessageStore.getGroup(groupId) : false;
467
468
  utils_1.logger.log(`[AgentCP] processAndAckBatch: storeExists=${storeExists} storeGroupExists=${storeGroupExists} lastMsgId=${this.getGroupLastMsgId(groupId)}`);
@@ -681,10 +682,12 @@ class AgentCP {
681
682
  });
682
683
  await this.addGroupMessagesToStore(groupId, msgs);
683
684
  // ACK 这批消息中的最后一条
684
- const lastMsgId = msgs[msgs.length - 1].msg_id;
685
- await this.groupOps.ackMessages(this._groupTargetAid, groupId, lastMsgId);
686
- // 更新 after 用于下一轮拉取
687
- after = lastMsgId;
685
+ if (msgs.length > 0) {
686
+ const lastMsgId = msgs[msgs.length - 1].msg_id;
687
+ await this.groupOps.ackMessages(this._groupTargetAid, groupId, lastMsgId);
688
+ // 更新 after 用于下一轮拉取
689
+ after = lastMsgId;
690
+ }
688
691
  if (!pulled.has_more) {
689
692
  break;
690
693
  }
@@ -698,6 +701,29 @@ class AgentCP {
698
701
  // ============================================================
699
702
  // Group Session Lifecycle (register_online / pull / heartbeat / unregister)
700
703
  // ============================================================
704
+ /**
705
+ * 将群组加入在线列表(不发送网络请求)。
706
+ */
707
+ addOnlineGroup(groupId) {
708
+ this._onlineGroups.add(groupId);
709
+ }
710
+ /**
711
+ * 确保群组心跳定时器已启动(公开方法)。
712
+ */
713
+ ensureGroupHeartbeat() {
714
+ this._ensureHeartbeat();
715
+ }
716
+ /**
717
+ * 向 group.ap 发送一次 register_online,告知当前客户端在线。
718
+ * 不带群组 ID,只需在启动或重连时调用一次。
719
+ */
720
+ async groupRegisterOnline() {
721
+ if (!this.groupOps || !this._groupTargetAid) {
722
+ throw new Error('群组客户端未初始化,请先调用 initGroupClient');
723
+ }
724
+ await this.groupOps.registerOnline(this._groupTargetAid);
725
+ utils_1.logger.log(`[Group] registerOnline: 已通知 group.ap 在线`);
726
+ }
701
727
  /**
702
728
  * 加入群组会话(完整生命周期):
703
729
  * 1. register_online → 告知 group.ap 在线
@@ -708,11 +734,13 @@ class AgentCP {
708
734
  if (!this.groupOps || !this._groupTargetAid) {
709
735
  throw new Error('群组客户端未初始化,请先调用 initGroupClient');
710
736
  }
711
- // Step 1: register_online(仅通知 group.ap 在线,不再返回游标)
712
- await this.groupOps.registerOnline(this._groupTargetAid);
737
+ // 如果还没有在线群组,说明是首次加入,需要先 registerOnline
738
+ if (this._onlineGroups.size === 0) {
739
+ await this.groupOps.registerOnline(this._groupTargetAid);
740
+ }
713
741
  this._onlineGroups.add(groupId);
714
742
  utils_1.logger.log(`[Group] joinGroupSession: group=${groupId}`);
715
- // Step 2: 冷启动同步 — 拉取历史消息对齐,再进入批推送接收
743
+ // 冷启动同步 — 拉取历史消息对齐,再进入批推送接收
716
744
  try {
717
745
  const lastMsgId = this.getGroupLastMsgId(groupId);
718
746
  await this.pullAndStoreGroupMessages(groupId, lastMsgId, 50);
@@ -720,7 +748,7 @@ class AgentCP {
720
748
  catch (e) {
721
749
  utils_1.logger.warn(`[Group] cold-start sync failed: group=${groupId}`, e.message || e);
722
750
  }
723
- // Step 3: 启动心跳定时器(首次加入群组时启动)
751
+ // 启动心跳定时器(首次加入群组时启动)
724
752
  this._ensureHeartbeat();
725
753
  }
726
754
  /**
package/dist/agentmd.d.ts CHANGED
@@ -17,7 +17,7 @@ export interface AgentMdOptions {
17
17
  }
18
18
  /**
19
19
  * 从 AID 中提取显示名称
20
- * 例如: "alice.aid.show" -> "alice"
20
+ * 例如: "alice.agentcp.io" -> "alice"
21
21
  */
22
22
  export declare function extractDisplayName(aid: string): string;
23
23
  /**
package/dist/agentmd.js CHANGED
@@ -7,10 +7,10 @@ exports.extractDisplayName = extractDisplayName;
7
7
  exports.generateAgentMd = generateAgentMd;
8
8
  /**
9
9
  * 从 AID 中提取显示名称
10
- * 例如: "alice.aid.show" -> "alice"
10
+ * 例如: "alice.agentcp.io" -> "alice"
11
11
  */
12
12
  function extractDisplayName(aid) {
13
- const suffixes = ['.agentcp.io', '.aid.show', '.agentid.pub'];
13
+ const suffixes = ['.agentcp.io', '.agentid.pub'];
14
14
  for (const suffix of suffixes) {
15
15
  if (aid.endsWith(suffix)) {
16
16
  return aid.slice(0, -suffix.length);
@@ -1,3 +1,5 @@
1
+ declare const DEFAULT_ACP_DIR: string;
2
+ export { DEFAULT_ACP_DIR };
1
3
  export declare class CertAndKeyStore {
2
4
  static aidKey: string;
3
5
  private static basePath;
@@ -33,9 +33,13 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.CertAndKeyStore = void 0;
36
+ exports.CertAndKeyStore = exports.DEFAULT_ACP_DIR = void 0;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
+ const os = __importStar(require("os"));
40
+ // 默认数据目录:用户主目录下的 acp 文件夹
41
+ const DEFAULT_ACP_DIR = path.join(os.homedir(), 'acp');
42
+ exports.DEFAULT_ACP_DIR = DEFAULT_ACP_DIR;
39
43
  // 本地 logger(避免与 utils.ts 循环依赖)
40
44
  function _ts() {
41
45
  const n = new Date();
@@ -96,7 +100,7 @@ class NodeStorage {
96
100
  return (_a = this.cache[key]) !== null && _a !== void 0 ? _a : null;
97
101
  }
98
102
  }
99
- NodeStorage.dataDir = path.join(process.cwd(), '.acp-data');
103
+ NodeStorage.dataDir = path.join(DEFAULT_ACP_DIR, '.acp-data');
100
104
  NodeStorage.dataFile = path.join(NodeStorage.dataDir, 'storage.json');
101
105
  NodeStorage.cache = {};
102
106
  NodeStorage.initialized = false;
@@ -265,4 +269,4 @@ class CertAndKeyStore {
265
269
  }
266
270
  exports.CertAndKeyStore = CertAndKeyStore;
267
271
  CertAndKeyStore.aidKey = 'currentAidKey';
268
- CertAndKeyStore.basePath = process.cwd();
272
+ CertAndKeyStore.basePath = DEFAULT_ACP_DIR;
@@ -180,6 +180,8 @@ class GroupMessageStore {
180
180
  utils_1.logger.warn(`[GroupMessageStore] addMessages: group=${groupId} NOT FOUND in store! Available groups: [${Array.from(this.groups.keys()).join(', ')}]`);
181
181
  return;
182
182
  }
183
+ if (!msgs || msgs.length === 0)
184
+ return;
183
185
  utils_1.logger.log(`[GroupMessageStore] addMessages: group=${groupId} incoming=${msgs.length} currentLastMsgId=${data.record.lastMsgId} incomingMsgIds=[${msgs.map(m => m.msg_id).join(',')}]`);
184
186
  let added = 0;
185
187
  for (const msg of msgs) {
@@ -207,9 +207,10 @@ class GroupOperations {
207
207
  let after = cursor.msg_cursor.current_msg_id;
208
208
  while (true) {
209
209
  const result = await this.pullMessages(targetAid, groupId, after, 50);
210
- if (result.messages.length > 0) {
211
- handler.onMessages(groupId, result.messages);
212
- const lastId = (_a = result.messages[result.messages.length - 1].msg_id) !== null && _a !== void 0 ? _a : after;
210
+ const messages = result.messages || [];
211
+ if (messages.length > 0) {
212
+ handler.onMessages(groupId, messages);
213
+ const lastId = (_a = messages[messages.length - 1].msg_id) !== null && _a !== void 0 ? _a : after;
213
214
  await this.ackMessages(targetAid, groupId, lastId);
214
215
  after = lastId;
215
216
  }
@@ -223,9 +224,10 @@ class GroupOperations {
223
224
  let after = cursor.event_cursor.current_event_id;
224
225
  while (true) {
225
226
  const result = await this.pullEvents(targetAid, groupId, after, 50);
226
- if (result.events.length > 0) {
227
- handler.onEvents(groupId, result.events);
228
- const lastId = (_a = result.events[result.events.length - 1].event_id) !== null && _a !== void 0 ? _a : after;
227
+ const events = result.events || [];
228
+ if (events.length > 0) {
229
+ handler.onEvents(groupId, events);
230
+ const lastId = (_a = events[events.length - 1].event_id) !== null && _a !== void 0 ? _a : after;
229
231
  await this.ackEvents(targetAid, groupId, lastId);
230
232
  after = lastId;
231
233
  }
package/dist/server.js CHANGED
@@ -151,25 +151,27 @@ async function doEnsureOnline(aid) {
151
151
  instance.agentWS.acceptInviteFromHeartbeat(invite.sessionId, invite.inviterAgentId, invite.inviteCode);
152
152
  }
153
153
  });
154
- // 心跳重连成功后,自动触发 WebSocket 重连 + 群组重新注册
154
+ // 心跳重连成功后,自动触发 WebSocket 重连 + 重新注册上线
155
155
  hb.onReconnect(() => {
156
156
  if (instance.agentWS) {
157
157
  utils_1.logger.log('[Server] 心跳重连成功,触发 WebSocket 重连...');
158
158
  instance.agentWS.reconnect().then(async () => {
159
- // WebSocket 重连成功后,重新注册所有在线群组
160
- // 断线期间 group.ap 会将在线状态过期,必须重新 register_online 才能收到推送
161
159
  const onlineGroups = instance.agentCP.getOnlineGroups();
162
160
  if (onlineGroups.length > 0) {
163
- utils_1.logger.log(`[Server] WebSocket 重连成功,重新注册 ${onlineGroups.length} 个在线群组...`);
164
- for (const groupId of onlineGroups) {
161
+ await instance.agentCP.groupRegisterOnline();
162
+ utils_1.logger.log(`[Server] WebSocket 重连成功,已重新注册上线,在线群组: ${onlineGroups.length}`);
163
+ // 后台异步拉取断线期间可能漏掉的消息
164
+ Promise.all(onlineGroups.map(async (groupId) => {
165
165
  try {
166
- await instance.agentCP.joinGroupSession(groupId);
167
- utils_1.logger.log(`[Server] 群组重新注册成功: ${groupId}`);
166
+ const lastMsgId = instance.agentCP.getGroupLastMsgId(groupId);
167
+ await instance.agentCP.pullAndStoreGroupMessages(groupId, lastMsgId, 50);
168
168
  }
169
169
  catch (e) {
170
- utils_1.logger.warn(`[Server] 群组重新注册失败: ${groupId}`, e.message || e);
170
+ utils_1.logger.warn(`[Server] reconnect sync failed: ${groupId}`, e.message || e);
171
171
  }
172
- }
172
+ })).then(() => {
173
+ utils_1.logger.log(`[Server] 重连后台消息同步完成`);
174
+ });
173
175
  }
174
176
  }).catch((err) => {
175
177
  utils_1.logger.error('[Server] WebSocket 重连失败:', err);
@@ -184,19 +186,22 @@ async function doEnsureOnline(aid) {
184
186
  instance.connectionConfig = newConnConfig;
185
187
  utils_1.logger.log('[Server] 重新鉴权成功,使用新 signature 重连 WebSocket...');
186
188
  await instance.agentWS.reconnect(newConnConfig.messageServer, newConnConfig.messageSignature);
187
- // 重连成功后重新注册所有在线群组
188
189
  const onlineGroups = instance.agentCP.getOnlineGroups();
189
190
  if (onlineGroups.length > 0) {
190
- utils_1.logger.log(`[Server] 重新鉴权重连成功,重新注册 ${onlineGroups.length} 个在线群组...`);
191
- for (const groupId of onlineGroups) {
191
+ await instance.agentCP.groupRegisterOnline();
192
+ utils_1.logger.log(`[Server] 重新鉴权重连成功,已重新注册上线,在线群组: ${onlineGroups.length}`);
193
+ // 后台异步拉取断线期间可能漏掉的消息
194
+ Promise.all(onlineGroups.map(async (groupId) => {
192
195
  try {
193
- await instance.agentCP.joinGroupSession(groupId);
194
- utils_1.logger.log(`[Server] 群组重新注册成功: ${groupId}`);
196
+ const lastMsgId = instance.agentCP.getGroupLastMsgId(groupId);
197
+ await instance.agentCP.pullAndStoreGroupMessages(groupId, lastMsgId, 50);
195
198
  }
196
199
  catch (e) {
197
- utils_1.logger.warn(`[Server] 群组重新注册失败: ${groupId}`, e.message || e);
200
+ utils_1.logger.warn(`[Server] reconnect sync failed: ${groupId}`, e.message || e);
198
201
  }
199
- }
202
+ })).then(() => {
203
+ utils_1.logger.log(`[Server] 重连后台消息同步完成`);
204
+ });
200
205
  }
201
206
  }
202
207
  catch (err) {
@@ -230,7 +235,7 @@ async function doEnsureOnline(aid) {
230
235
  try {
231
236
  const parsed = JSON.parse(msgContent);
232
237
  if (Array.isArray(parsed) && parsed.length > 0) {
233
- content = parsed.map((item) => item.content || '').join('');
238
+ content = parsed.map((item) => (item && item.content) || '').join('');
234
239
  }
235
240
  else if (parsed.content) {
236
241
  content = parsed.content;
@@ -284,12 +289,33 @@ async function doEnsureOnline(aid) {
284
289
  return instance;
285
290
  }
286
291
  // 确保群组客户端已初始化
292
+ // 群组客户端初始化锁,防止并发请求重复初始化
293
+ const groupInitPromises = new Map();
287
294
  async function ensureGroupClient(instance) {
288
295
  if (instance.groupInitialized && instance.agentCP.groupClient)
289
296
  return;
290
297
  if (!instance.agentWS)
291
298
  throw new Error('WebSocket 未连接');
292
299
  const aid = instance.aid;
300
+ // 如果已有初始化在进行中,等待它完成
301
+ const existing = groupInitPromises.get(aid);
302
+ if (existing) {
303
+ await existing;
304
+ return;
305
+ }
306
+ const initPromise = doInitGroupClient(instance);
307
+ groupInitPromises.set(aid, initPromise);
308
+ try {
309
+ await initPromise;
310
+ }
311
+ finally {
312
+ groupInitPromises.delete(aid);
313
+ }
314
+ }
315
+ async function doInitGroupClient(instance) {
316
+ if (!instance.agentWS)
317
+ throw new Error('WebSocket 未连接');
318
+ const aid = instance.aid;
293
319
  // 计算 group target AID: group.{issuer}
294
320
  const parts = aid.split('.', 1);
295
321
  const issuer = aid.substring(parts[0].length + 1) || aid;
@@ -360,7 +386,7 @@ async function ensureGroupClient(instance) {
360
386
  let groupName = groupId;
361
387
  try {
362
388
  const info = await instance.agentCP.groupOps.getGroupInfo(instance.groupTargetAid, groupId);
363
- groupName = info.name || groupId;
389
+ groupName = (info && info.name) || groupId;
364
390
  }
365
391
  catch (_) { }
366
392
  instance.agentCP.addGroupToStore(groupId, groupName);
@@ -386,7 +412,8 @@ async function ensureGroupClient(instance) {
386
412
  broadcastToBrowser({ type: 'join_request', group_id: groupId, agent_id: agentId, message });
387
413
  },
388
414
  onGroupMessageBatch(groupId, batch) {
389
- utils_1.logger.log(`[Group] onGroupMessageBatch: group=${groupId} count=${batch.count} range=[${batch.start_msg_id}, ${batch.latest_msg_id}] messages=${JSON.stringify((batch.messages || []).map(m => m.msg_id))}`);
415
+ const batchMessages = batch.messages || [];
416
+ 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))}`);
390
417
  // 存储 + ACK(统一由 agentcp 处理),注意 processAndAckBatch 是 async
391
418
  instance.agentCP.processAndAckBatch(groupId, batch).then((sorted) => {
392
419
  var _a, _b;
@@ -431,15 +458,27 @@ async function ensureGroupClient(instance) {
431
458
  utils_1.logger.warn('[Group] syncGroupList error:', e.message);
432
459
  }
433
460
  }
434
- // 为所有已加入群组注册上线(register_online + 拉取未读 + 启动心跳)
461
+ // 注册上线 + 将群组加入在线列表(不阻塞)
435
462
  const groups = instance.agentCP.getLocalGroupList();
436
- for (const group of groups) {
437
- try {
438
- await instance.agentCP.joinGroupSession(group.group_id);
439
- }
440
- catch (e) {
441
- utils_1.logger.warn(`[Group] joinGroupSession failed: ${group.group_id}`, e.message);
442
- }
463
+ if (groups.length > 0) {
464
+ await instance.agentCP.groupRegisterOnline();
465
+ for (const group of groups) {
466
+ instance.agentCP.addOnlineGroup(group.group_id);
467
+ }
468
+ instance.agentCP.ensureGroupHeartbeat();
469
+ utils_1.logger.log(`[Group] 已注册上线,在线群组: ${groups.length}`);
470
+ // 后台异步拉取历史消息,不阻塞启动流程
471
+ Promise.all(groups.map(async (group) => {
472
+ try {
473
+ const lastMsgId = instance.agentCP.getGroupLastMsgId(group.group_id);
474
+ await instance.agentCP.pullAndStoreGroupMessages(group.group_id, lastMsgId, 50);
475
+ }
476
+ catch (e) {
477
+ utils_1.logger.warn(`[Group] background sync failed: ${group.group_id}`, e.message);
478
+ }
479
+ })).then(() => {
480
+ utils_1.logger.log(`[Group] 后台消息同步完成,共 ${groups.length} 个群组`);
481
+ });
443
482
  }
444
483
  instance.groupInitialized = true;
445
484
  instance.groupSessionId = groupSessionId;
@@ -480,7 +519,7 @@ async function getAidStatusList() {
480
519
  const agentInfoCache = new Map();
481
520
  const AGENT_INFO_CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
482
521
  function getAgentInfoCachePath() {
483
- const dir = globalDataDir || process.cwd();
522
+ const dir = globalDataDir || datamanager_1.DEFAULT_ACP_DIR;
484
523
  return path.join(dir, 'AIDs', '.agent-info-cache.json');
485
524
  }
486
525
  function loadAgentInfoCacheFromDisk() {
@@ -579,7 +618,7 @@ async function getAgentInfo(aid) {
579
618
  }
580
619
  // 每个 AID 的自定义 agent.md 选项 (昵称、描述)
581
620
  function getAidMdOptionsPath() {
582
- const dir = globalDataDir || process.cwd();
621
+ const dir = globalDataDir || datamanager_1.DEFAULT_ACP_DIR;
583
622
  return path.join(dir, 'AIDs', '.aid-md-options.json');
584
623
  }
585
624
  function loadAidMdOptions() {
@@ -614,7 +653,7 @@ function getMessageStoreForAid(aid) {
614
653
  if (!store) {
615
654
  store = new messagestore_1.MessageStore({
616
655
  persistMessages: true,
617
- basePath: globalDataDir || process.cwd(),
656
+ basePath: globalDataDir || datamanager_1.DEFAULT_ACP_DIR,
618
657
  });
619
658
  messageStores.set(aid, store);
620
659
  }
@@ -638,6 +677,10 @@ const indexHtml = `<!DOCTYPE html>
638
677
  <title>ACP 身份管理</title>
639
678
  <style>
640
679
  * { box-sizing: border-box; margin: 0; padding: 0; }
680
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
681
+ ::-webkit-scrollbar-track { background: transparent; }
682
+ ::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.15); border-radius: 3px; }
683
+ ::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,0.25); }
641
684
  body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: linear-gradient(135deg, #f0f4ff 0%, #e8edf5 100%); min-height: 100vh; display: flex; justify-content: center; align-items: flex-start; padding: 40px 20px; }
642
685
  .container { background: white; padding: 0; border-radius: 16px; box-shadow: 0 8px 32px rgba(0,0,0,0.08); max-width: 560px; width: 100%; overflow: hidden; }
643
686
  .page-header { background: linear-gradient(135deg, #2563eb 0%, #1e40af 100%); padding: 28px 32px 22px; color: white; text-align: center; }
@@ -674,6 +717,7 @@ const indexHtml = `<!DOCTYPE html>
674
717
  .create-section .extra-fields input { flex: 1; padding: 10px 14px; border: 1px solid #e2e8f0; border-radius: 8px; font-size: 14px; min-width: 0; background: #fff; transition: border-color 0.2s, box-shadow 0.2s; }
675
718
  .create-section .extra-fields input:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37,99,235,0.1); }
676
719
  .btn { display: block; width: 100%; padding: 11px; border: none; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s; }
720
+ .btn:active { transform: scale(0.98); }
677
721
  .btn-primary { background: linear-gradient(135deg, #2563eb, #1d4ed8); color: white; }
678
722
  .btn-primary:hover { background: linear-gradient(135deg, #1d4ed8, #1e40af); box-shadow: 0 2px 8px rgba(37,99,235,0.3); }
679
723
  .btn-sm { display: inline-block; width: auto; padding: 6px 14px; font-size: 13px; border-radius: 6px; }
@@ -684,10 +728,10 @@ const indexHtml = `<!DOCTYPE html>
684
728
  .btn-outline { background: white; color: #2563eb; border: 1px solid #2563eb; }
685
729
  .btn-outline:hover { background: #eff6ff; }
686
730
  .btn-outline.active { background: #2563eb; color: white; }
687
- .btn:disabled { background: #d1d5db; cursor: not-allowed; border-color: #d1d5db; color: #fff; }
731
+ .btn:disabled { background: #d1d5db; cursor: not-allowed; border-color: #d1d5db; color: #fff; transform: none; }
688
732
  .aid-list { margin-bottom: 24px; }
689
733
  .aid-card { background: #fff; border: 1px solid #e5e7eb; border-radius: 12px; padding: 16px; margin-bottom: 10px; transition: all 0.2s; display: flex; align-items: stretch; gap: 12px; }
690
- .aid-card:hover { border-color: #93c5fd; box-shadow: 0 2px 12px rgba(37,99,235,0.06); }
734
+ .aid-card:hover { border-color: #93c5fd; box-shadow: 0 4px 12px rgba(37,99,235,0.08); transform: translateY(-1px); }
691
735
  .aid-card.current { border-color: #2563eb; background: #eff6ff; }
692
736
  .aid-card-left { flex: 1; min-width: 0; }
693
737
  .aid-card-right { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; flex-shrink: 0; justify-content: center; }
@@ -703,9 +747,9 @@ const indexHtml = `<!DOCTYPE html>
703
747
  .badge-info { background: #dbeafe; color: #1e40af; }
704
748
  .badge-current { background: #2563eb; color: white; }
705
749
  .aid-card-actions { display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
706
- .status { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); padding: 12px 24px; border-radius: 10px; font-size: 14px; display: none; z-index: 1000; box-shadow: 0 4px 16px rgba(0,0,0,0.1); }
707
- .status.success { display: block; background: #d1fae5; color: #065f46; }
708
- .status.error { display: block; background: #fee2e2; color: #991b1b; }
750
+ .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); }
751
+ .status.success { opacity: 1; visibility: visible; transform: translate(-50%, 0); background: #d1fae5; color: #065f46; border: 1px solid #a7f3d0; }
752
+ .status.error { opacity: 1; visibility: visible; transform: translate(-50%, 0); background: #fee2e2; color: #991b1b; border: 1px solid #fecaca; }
709
753
  @media (max-width: 480px) {
710
754
  body { padding: 16px 8px; }
711
755
  .page-header { padding: 22px 18px 18px; }
@@ -763,7 +807,7 @@ const indexHtml = `<!DOCTYPE html>
763
807
  function updateApSelect() {
764
808
  var sel = document.getElementById('apSelect');
765
809
  if (sel && sel.options.length === 0) {
766
- const options = ['agentcp.io', 'aid.show', 'agentid.pub'];
810
+ const options = ['agentcp.io', 'agentid.pub'];
767
811
  options.forEach(function(op) {
768
812
  var opt = document.createElement('option');
769
813
  opt.value = op;
@@ -864,6 +908,7 @@ const indexHtml = `<!DOCTYPE html>
864
908
  var data = await res.json();
865
909
  if (data.success) {
866
910
  showStatus(aid + ' 已上线,正在进入聊天...', 'success');
911
+ sessionStorage.setItem('chatEntry','1');
867
912
  window.location.href = '/chat';
868
913
  } else {
869
914
  showStatus(data.error || '上线失败', 'error');
@@ -875,7 +920,7 @@ const indexHtml = `<!DOCTYPE html>
875
920
  }
876
921
  }
877
922
 
878
- function enterChat(aid) { window.location.href = '/chat'; }
923
+ function enterChat(aid) { sessionStorage.setItem('chatEntry','1'); window.location.href = '/chat'; }
879
924
 
880
925
  async function goOffline(aid) {
881
926
  try {
@@ -892,11 +937,13 @@ const indexHtml = `<!DOCTYPE html>
892
937
  });
893
938
  }
894
939
 
940
+ let statusTimeout = null;
895
941
  function showStatus(msg, type) {
896
942
  var el = document.getElementById('status');
897
943
  el.textContent = msg;
898
944
  el.className = 'status ' + type;
899
- setTimeout(function() { el.className = 'status'; }, 3000);
945
+ if (statusTimeout) clearTimeout(statusTimeout);
946
+ statusTimeout = setTimeout(function() { el.className = 'status'; }, 3000);
900
947
  }
901
948
 
902
949
  function escapeHtml(text) {
@@ -924,6 +971,10 @@ const chatHtml = `<!DOCTYPE html>
924
971
  <style>
925
972
  :root { --primary:#2563eb; --primary-h:#1d4ed8; --bg:#f3f4f6; --sidebar-bg:#fff; --chat-bg:#f9fafb; --border:#e5e7eb; --t1:#1f2937; --t2:#6b7280; --sent:#2563eb; --recv-bg:#fff; --ok:#10b981; }
926
973
  * { box-sizing:border-box; margin:0; padding:0; }
974
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
975
+ ::-webkit-scrollbar-track { background: transparent; }
976
+ ::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.15); border-radius: 3px; }
977
+ ::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,0.25); }
927
978
  body { font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; background:var(--bg); height:100vh; overflow:hidden; color:var(--t1); }
928
979
  #app { display:flex; height:100%; }
929
980
 
@@ -933,8 +984,9 @@ const chatHtml = `<!DOCTYPE html>
933
984
  .sidebar-header { padding:12px 14px; border-bottom:1px solid var(--border); display:flex; flex-direction:column; gap:12px; flex-shrink:0; }
934
985
  .header-top { display:flex; justify-content:space-between; align-items:center; width:100%; }
935
986
  .sidebar-header .my-aid { font-size:11px; color:#155724; font-family:monospace; background:#d4edda; padding:4px 8px; border-radius:12px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; border:1px solid #c3e6cb; flex:1; margin-right:8px; }
936
- .new-chat-btn { padding:8px 10px; background:var(--primary); color:#fff; border:none; border-radius:6px; font-size:12px; cursor:pointer; white-space:nowrap; width:100%; text-align:center; }
987
+ .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; }
937
988
  .new-chat-btn:hover { background:var(--primary-h); }
989
+ .new-chat-btn:active { transform:scale(0.96); }
938
990
  .session-list { flex:1; overflow-y:auto; }
939
991
 
940
992
  /* AID Group */
@@ -947,12 +999,12 @@ const chatHtml = `<!DOCTYPE html>
947
999
  .aid-group-arrow { font-size:10px; color:var(--primary); transition:transform 0.2s; flex-shrink:0; }
948
1000
  .aid-group-arrow.open { transform:rotate(90deg); }
949
1001
  .aid-group-badge { font-size:10px; background:var(--primary); color:#fff; padding:1px 6px; border-radius:8px; margin-left:8px; flex-shrink:0; }
950
- .aid-group-add { background:none; border:1px solid var(--border); color:var(--t2); width:22px; height:22px; border-radius:4px; cursor:pointer; font-size:14px; line-height:20px; text-align:center; margin-left:6px; flex-shrink:0; }
1002
+ .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; }
951
1003
  .aid-group-add:hover { background:var(--primary); color:#fff; border-color:var(--primary); }
952
- .aid-group-del { background:none; border:none; color:var(--t2); width:20px; height:20px; border-radius:4px; cursor:pointer; font-size:12px; line-height:20px; text-align:center; margin-left:4px; flex-shrink:0; display:none; }
1004
+ .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; }
953
1005
  .aid-group-header:hover .aid-group-del { display:block; }
954
1006
  .aid-group-del:hover { color:#dc3545; background:#ffebeb; }
955
- .session-del { position:absolute; right:8px; top:50%; transform:translateY(-50%); background:none; border:none; color:var(--t2); font-size:12px; cursor:pointer; display:none; padding:2px; }
1007
+ .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; }
956
1008
  .session-item:hover .session-del { display:block; }
957
1009
  .session-del:hover { color:#dc3545; }
958
1010
  .aid-group-sessions { display:none; background:#fafbfc; }
@@ -961,11 +1013,11 @@ const chatHtml = `<!DOCTYPE html>
961
1013
  .aid-group-avatar { width:36px; height:36px; border-radius:50%; object-fit:cover; flex-shrink:0; margin-right:8px; box-shadow:0 1px 4px rgba(37,99,235,0.18); border:2px solid #bfdbfe; }
962
1014
 
963
1015
  .session-item { padding:10px 14px 10px 32px; border-bottom:1px solid #f0f1f3; cursor:pointer; transition:all 0.15s; position:relative; }
964
- .session-item::before { content:''; position:absolute; left:18px; top:50%; transform:translateY(-50%); width:6px; height:6px; border-radius:50%; background:#d1d5db; }
1016
+ .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; }
965
1017
  .session-item:hover { background:#f0f5ff; }
966
1018
  .session-item.active { background:#eff6ff; border-left:3px solid var(--primary); padding-left:29px; }
967
1019
  .session-item.active::before { background:var(--primary); box-shadow:0 0 0 2px rgba(37,99,235,0.2); }
968
- .session-peer { font-weight:500; font-size:12px; color:var(--t1); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; padding-left:10px; background:#f1f5f9; border-radius:4px; padding:3px 8px 3px 10px; border:1px solid #e8ecf1; }
1020
+ .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; }
969
1021
  .session-item.active .session-peer { background:#dbeafe; border-color:#bfdbfe; color:#1e40af; }
970
1022
  .session-meta { font-size:10px; color:var(--t2); margin-top:4px; display:flex; align-items:center; gap:6px; padding-left:10px; }
971
1023
  .tag { font-size:9px; padding:1px 5px; border-radius:3px; color:#fff; font-weight:600; letter-spacing:0.3px; }
@@ -976,16 +1028,17 @@ const chatHtml = `<!DOCTYPE html>
976
1028
  .chat-area { flex:1; display:flex; flex-direction:column; background:var(--chat-bg); min-width:0; }
977
1029
  .chat-header { height:54px; padding:0 16px; background:#fff; border-bottom:1px solid var(--border); display:flex; align-items:center; justify-content:space-between; flex-shrink:0; }
978
1030
  .header-left { display:flex; align-items:center; gap:10px; overflow:hidden; }
979
- .toggle-sidebar-btn { background:none; border:none; cursor:pointer; color:var(--t2); padding:4px; display:flex; }
1031
+ .toggle-sidebar-btn { background:none; border:none; cursor:pointer; color:var(--t2); padding:4px; display:flex; transition:all 0.15s; }
980
1032
  .toggle-sidebar-btn:hover { color:var(--t1); }
981
- .status-dot { width:8px; height:8px; border-radius:50%; background:#ccc; flex-shrink:0; }
982
- .status-dot.connected { background:var(--ok); }
1033
+ .status-dot { width:8px; height:8px; border-radius:50%; background:#ccc; flex-shrink:0; transition:all 0.3s; }
1034
+ .status-dot.connected { background:var(--ok); box-shadow:0 0 0 2px rgba(16,185,129,0.2); }
983
1035
  .status-dot.connecting { background:#fbbf24; }
984
1036
  .chat-title { font-size:15px; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
985
1037
 
986
1038
  .aid-select-wrap { display:flex; align-items:center; gap:10px; flex-shrink:0; }
987
1039
  .manage-btn { display:flex; align-items:center; gap:4px; text-decoration:none; color:var(--t2); font-size:12px; padding:6px 10px; border-radius:6px; transition:all 0.2s; background:#fff; border:1px solid var(--border); }
988
1040
  .manage-btn:hover { background:#f8fafc; color:var(--primary); border-color:var(--primary); }
1041
+ .manage-btn:active { transform:scale(0.96); }
989
1042
  .aid-control-group { display:flex; align-items:center; background:#fff; border:1px solid var(--border); border-radius:6px; padding:2px; box-shadow:0 1px 2px rgba(0,0,0,0.03); }
990
1043
  .aid-select { border:none; background:transparent; font-size:12px; color:var(--t1); padding:5px 8px; outline:none; cursor:pointer; min-width:120px; font-weight:500; }
991
1044
  .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); }
@@ -994,7 +1047,7 @@ const chatHtml = `<!DOCTYPE html>
994
1047
  .status-indicator.online { background:var(--ok); box-shadow:0 0 0 2px rgba(16,185,129,0.2); }
995
1048
  .status-indicator.offline { background:#cbd5e1; }
996
1049
 
997
- .collapse-btn { background:none; border:none; cursor:pointer; color:var(--t2); padding:6px; display:flex; align-items:center; flex-shrink:0; }
1050
+ .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; }
998
1051
  .collapse-btn:hover { color:var(--t1); }
999
1052
 
1000
1053
  .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; }
@@ -1004,24 +1057,34 @@ const chatHtml = `<!DOCTYPE html>
1004
1057
  .message { display:flex; flex-direction:column; max-width:80%; }
1005
1058
  .message.sent { align-self:flex-end; align-items:flex-end; }
1006
1059
  .message.received { align-self:flex-start; align-items:flex-start; }
1007
- .bubble { padding:10px 14px; border-radius:12px; font-size:14px; line-height:1.5; word-wrap:break-word; box-shadow:0 1px 2px rgba(0,0,0,0.05); }
1008
- .message.sent .bubble { background:var(--sent); color:#fff; border-bottom-right-radius:2px; }
1009
- .message.received .bubble { background:var(--recv-bg); color:var(--t1); border-bottom-left-radius:2px; border:1px solid var(--border); }
1060
+ .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); }
1061
+ .message.sent .bubble { background:var(--sent); color:#fff; border-bottom-right-radius:4px; }
1062
+ .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); }
1010
1063
  .msg-meta { font-size:10px; color:var(--t2); margin-bottom:3px; padding:0 4px; }
1011
1064
 
1012
- .input-area { padding:12px 16px; background:#fff; border-top:1px solid var(--border); display:flex; align-items:center; gap:10px; flex-shrink:0; }
1013
- .input-area input { flex:1; padding:10px 14px; border-radius:20px; border:1px solid var(--border); font-size:14px; background:#f9fafb; }
1014
- .input-area input:focus { outline:none; border-color:var(--primary); background:#fff; }
1015
- .send-btn { width:40px; height:40px; border-radius:50%; background:var(--primary); border:none; color:#fff; display:flex; align-items:center; justify-content:center; cursor:pointer; flex-shrink:0; }
1065
+ .input-area { padding:12px 16px; background:#fff; border-top:1px solid var(--border); display:flex; flex-direction:column; gap:8px; flex-shrink:0; }
1066
+ .input-area.drag-over { background:#eff6ff; border-top-color:var(--primary); }
1067
+ .input-row { display:flex; align-items:flex-end; gap:10px; }
1068
+ .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; }
1069
+ .input-area textarea:focus { outline:none; border-color:var(--primary); background:#fff; box-shadow:0 0 0 3px rgba(37,99,235,0.1); }
1070
+ .file-list { display:flex; flex-wrap:wrap; gap:6px; padding:4px 0; }
1071
+ .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; }
1072
+ .file-item .file-icon { flex-shrink:0; width:16px; height:16px; color:var(--primary); }
1073
+ .file-item .file-name { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
1074
+ .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; }
1075
+ .file-item .file-remove:hover { color:#e53e3e; background:rgba(229,62,62,0.1); }
1076
+ .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; }
1016
1077
  .send-btn:hover { background:var(--primary-h); }
1017
- .send-btn:disabled { background:#ccc; cursor:not-allowed; }
1078
+ .send-btn:active { transform:scale(0.94); }
1079
+ .send-btn:disabled { background:#ccc; cursor:not-allowed; transform:none; }
1018
1080
 
1019
- .modal-overlay { position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.5); z-index:50; display:none; align-items:center; justify-content:center; }
1020
- .modal-overlay.show { display:flex; }
1021
- .modal { background:#fff; width:90%; max-width:400px; border-radius:12px; padding:24px; box-shadow:0 10px 25px rgba(0,0,0,0.1); }
1081
+ .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); }
1082
+ .modal-overlay.show { opacity:1; visibility:visible; }
1083
+ .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); }
1084
+ .modal-overlay.show .modal { transform:scale(1) translateY(0); }
1022
1085
  .modal h3 { margin-bottom:16px; font-size:16px; }
1023
- .modal input[type="text"], .modal input[type="password"], .modal input[type="url"] { width:100%; padding:10px; border:1px solid var(--border); border-radius:8px; margin-bottom:16px; font-size:14px; box-sizing:border-box; }
1024
- .modal input[type="text"]:focus, .modal input[type="password"]:focus, .modal input[type="url"]:focus { outline:none; border-color:var(--primary); }
1086
+ .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; }
1087
+ .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); }
1025
1088
  .modal input[type="radio"] { width:auto; margin:0; }
1026
1089
  .group-type-card { flex:1; padding:12px; border:2px solid var(--border); border-radius:10px; cursor:pointer; transition:all 0.2s; background:#fafafa; }
1027
1090
  .group-type-card:hover { border-color:#b0b0b0; background:#f5f5f5; }
@@ -1030,10 +1093,11 @@ const chatHtml = `<!DOCTYPE html>
1030
1093
  .duty-rule-card:hover { border-color:#b0b0b0; background:#f5f5f5; }
1031
1094
  .duty-rule-card.selected { border-color:var(--primary); background:rgba(0,122,255,0.06); }
1032
1095
  .modal-btns { display:flex; justify-content:flex-end; gap:10px; }
1033
- .mbtn { padding:8px 16px; border-radius:6px; font-size:13px; cursor:pointer; border:none; }
1096
+ .mbtn { padding:8px 16px; border-radius:6px; font-size:13px; cursor:pointer; border:none; transition:all 0.15s; }
1097
+ .mbtn:active { transform:scale(0.96); }
1034
1098
  .mbtn-cancel { background:#f3f4f6; color:var(--t1); }
1035
1099
  .mbtn-ok { background:var(--primary); color:#fff; }
1036
- .mbtn-ok:disabled { background:#ccc; }
1100
+ .mbtn-ok:disabled { background:#ccc; transform:none; }
1037
1101
 
1038
1102
  .bubble p { margin-bottom:0.4em; } .bubble p:last-child { margin-bottom:0; }
1039
1103
  .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; }
@@ -1049,6 +1113,16 @@ const chatHtml = `<!DOCTYPE html>
1049
1113
  .bubble code { background:rgba(0,0,0,0.1); padding:2px 4px; border-radius:3px; font-family:monospace; font-size:0.9em; }
1050
1114
  .bubble pre { background:#2d2d2d; color:#fff; padding:12px; border-radius:6px; overflow-x:auto; margin:8px 0; }
1051
1115
  .bubble pre code { background:transparent; padding:0; color:inherit; border-radius:0; }
1116
+ .bubble table { border-collapse:collapse; width:100%; margin:8px 0; font-size:0.9em; }
1117
+ .bubble th, .bubble td { border:1px solid rgba(0,0,0,0.15); padding:6px 10px; text-align:left; }
1118
+ .bubble th { background:rgba(0,0,0,0.05); font-weight:600; }
1119
+ .bubble hr { border:none; border-top:1px solid rgba(0,0,0,0.1); margin:0.8em 0; }
1120
+ .message.sent .bubble blockquote { color:rgba(255,255,255,0.8); border-left-color:rgba(255,255,255,0.4); }
1121
+ .message.sent .bubble code { background:rgba(255,255,255,0.2); }
1122
+ .message.sent .bubble th, .message.sent .bubble td { border-color:rgba(255,255,255,0.3); }
1123
+ .message.sent .bubble th { background:rgba(255,255,255,0.1); }
1124
+ .message.sent .bubble hr { border-top-color:rgba(255,255,255,0.3); }
1125
+ .message.sent .bubble h1, .message.sent .bubble h2 { border-bottom-color:rgba(255,255,255,0.2); }
1052
1126
  .bubble-wrap { position:relative; }
1053
1127
  .bubble-wrap .copy-msg-btn { position:absolute; top:4px; right:4px; opacity:0; pointer-events:none; background:rgba(0,0,0,0.45); color:#fff; border:none; border-radius:4px; padding:2px 6px; font-size:11px; cursor:pointer; line-height:1.4; z-index:1; transition:opacity 0.15s; }
1054
1128
  .bubble-wrap:hover .copy-msg-btn { opacity:1; pointer-events:auto; }
@@ -1085,14 +1159,15 @@ const chatHtml = `<!DOCTYPE html>
1085
1159
  .group-item.active { background:#eff6ff; border-left:3px solid var(--primary); padding-left:11px; }
1086
1160
  .group-item-name { font-size:13px; font-weight:600; color:var(--t1); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
1087
1161
  .group-item-meta { font-size:10px; color:var(--t2); margin-top:2px; }
1088
- .group-item-del { position:absolute; right:8px; top:12px; background:none; border:none; color:var(--t2); font-size:12px; cursor:pointer; display:none; padding:2px; }
1162
+ .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; }
1089
1163
  .group-item:hover .group-item-del { display:block; }
1090
1164
  .group-item-del:hover { color:#dc3545; }
1091
1165
  .group-actions { padding:8px 14px; display:flex; gap:6px; flex-shrink:0; border-bottom:1px solid var(--border); }
1092
- .group-actions .gbtn { flex:1; padding:6px 0; border:1px solid var(--border); border-radius:6px; font-size:11px; cursor:pointer; background:#fff; color:var(--t1); text-align:center; }
1166
+ .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; }
1093
1167
  .group-actions .gbtn:hover { background:#f1f5f9; border-color:var(--primary); color:var(--primary); }
1168
+ .group-actions .gbtn:active { transform:scale(0.96); }
1094
1169
  .group-info-bar { padding:6px 16px; background:#f0f9ff; border-bottom:1px solid #bae6fd; font-size:11px; color:#0369a1; display:flex; align-items:center; gap:8px; flex-shrink:0; }
1095
- .group-info-bar .copy-link { cursor:pointer; text-decoration:underline; }
1170
+ .group-info-bar .copy-link { cursor:pointer; text-decoration:underline; transition:all 0.15s; }
1096
1171
  .group-info-bar .copy-link:hover { color:#0284c7; }
1097
1172
  </style>
1098
1173
  <!-- CHATHTML_STYLE_END -->
@@ -1113,12 +1188,12 @@ const chatHtml = `<!DOCTYPE html>
1113
1188
  </div>
1114
1189
  </div>
1115
1190
  <!-- P2P panel -->
1116
- <div id="p2pPanel">
1191
+ <div id="p2pPanel" style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
1117
1192
  <div style="padding:8px 14px;flex-shrink:0;"><button class="new-chat-btn" onclick="showModal()">+ 连接龙虾</button></div>
1118
1193
  <div class="session-list" id="sessionList"></div>
1119
1194
  </div>
1120
1195
  <!-- Group panel -->
1121
- <div id="groupPanel" style="display:none;flex:1;display:none;flex-direction:column;overflow:hidden;">
1196
+ <div id="groupPanel" style="display:none;flex-direction:column;overflow:hidden;">
1122
1197
  <div class="group-actions">
1123
1198
  <div class="gbtn" onclick="showCreateGroupModal()">创建群组</div>
1124
1199
  <div class="gbtn" onclick="showJoinGroupModal()">加入群组</div>
@@ -1158,6 +1233,7 @@ const chatHtml = `<!DOCTYPE html>
1158
1233
  <div class="group-info-bar" id="groupInfoBar" style="display:none;">
1159
1234
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>
1160
1235
  <span id="groupInfoText">群组</span>
1236
+ <span id="groupMemberStats" style="font-size:11px;color:var(--t2);margin-left:4px;"></span>
1161
1237
  <span class="copy-link" id="groupInviteBtn" onclick="generateInviteLink()" title="生成邀请链接" style="display:none;">生成邀请链接</span>
1162
1238
  <span class="copy-link" id="groupCopyLinkBtn" onclick="copyGroupLink()" title="复制群链接" style="display:none;">复制群链接</span>
1163
1239
  <span class="copy-link" onclick="showGroupMembers()" title="查看成员">成员</span>
@@ -1173,11 +1249,14 @@ const chatHtml = `<!DOCTYPE html>
1173
1249
  </div>
1174
1250
  <div class="new-msg-tip" id="newMsgTip" onclick="scrollToBottom()">↓ 有新消息</div>
1175
1251
  </div>
1176
- <div class="input-area">
1177
- <input type="text" id="messageInput" placeholder="输入消息..." onkeypress="if(event.key==='Enter')sendMessage()">
1252
+ <div class="input-area" id="inputArea">
1253
+ <div class="file-list" id="fileList" style="display:none;"></div>
1254
+ <div class="input-row">
1255
+ <textarea id="messageInput" rows="3" placeholder="输入消息... 可拖入文件" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendMessage();}" oninput="autoResizeInput()"></textarea>
1178
1256
  <button class="send-btn" id="sendBtn" onclick="sendMessage()">
1179
1257
  <svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"></path></svg>
1180
1258
  </button>
1259
+ </div>
1181
1260
  </div>
1182
1261
  </div>
1183
1262
  </div>
@@ -1326,13 +1405,13 @@ const chatHtml = `<!DOCTYPE html>
1326
1405
  </div>
1327
1406
  </div>
1328
1407
  </div>
1329
- <div id="switchAidOverlay" style="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.45);z-index:999;display:none;align-items:center;justify-content:center;flex-direction:column;gap:16px;">
1330
- <div style="width:36px;height:36px;border:3px solid rgba(255,255,255,0.3);border-top-color:#fff;border-radius:50%;animation:spin 0.8s linear infinite;"></div>
1331
- <div id="switchAidMsg" style="color:#fff;font-size:15px;font-weight:500;">切换身份中...</div>
1408
+ <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;">
1409
+ <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>
1410
+ <div id="switchAidMsg" style="color:var(--t1);font-size:15px;font-weight:500;">切换身份中...</div>
1332
1411
  </div>
1333
- <div id="switchGroupOverlay" style="position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(0,0,0,0.75);z-index:999;display:none;align-items:center;justify-content:center;flex-direction:column;gap:12px;padding:28px 40px;border-radius:12px;box-shadow:0 4px 24px rgba(0,0,0,0.3);">
1334
- <div style="width:32px;height:32px;border:3px solid rgba(255,255,255,0.3);border-top-color:#fff;border-radius:50%;animation:spin 0.8s linear infinite;"></div>
1335
- <div id="switchGroupMsg" style="color:#fff;font-size:14px;font-weight:500;">加载群组中...</div>
1412
+ <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);">
1413
+ <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>
1414
+ <div id="switchGroupMsg" style="color:var(--t1);font-size:14px;font-weight:500;">加载群组中...</div>
1336
1415
  </div>
1337
1416
  <style>@keyframes spin{to{transform:rotate(360deg)}}</style>
1338
1417
  <script>
@@ -1345,14 +1424,39 @@ const chatHtml = `<!DOCTYPE html>
1345
1424
  if (type === 'human') return '/assets/human.png';
1346
1425
  return '/assets/agent.png';
1347
1426
  }
1348
- async function fetchAgentInfo(aid) {
1349
- if (agentInfoCache[aid]) return agentInfoCache[aid];
1350
- try {
1351
- var r = await fetch('/api/agent-info?aid=' + encodeURIComponent(aid));
1352
- var d = await r.json();
1353
- if (d.type || d.name) { agentInfoCache[aid] = d; }
1354
- return d;
1355
- } catch(e) { return { type:'', name:'', description:'', tags:[] }; }
1427
+ // 批量 agent info 请求:攒批 50ms 后合并发送一次
1428
+ var _agentInfoBatchQueue=[];
1429
+ var _agentInfoBatchTimer=null;
1430
+ function _flushAgentInfoBatch(){
1431
+ _agentInfoBatchTimer=null;
1432
+ var queue=_agentInfoBatchQueue;
1433
+ _agentInfoBatchQueue=[];
1434
+ var aids=queue.map(function(q){ return q.aid; });
1435
+ fetch('/api/agent-info-batch',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({aids:aids})})
1436
+ .then(function(r){ return r.json(); })
1437
+ .then(function(d){
1438
+ if(d.success&&d.data){
1439
+ queue.forEach(function(q){
1440
+ var info=d.data[q.aid]||{type:'',name:'',description:'',tags:[]};
1441
+ if(info.type||info.name) agentInfoCache[q.aid]=info;
1442
+ q.resolve(info);
1443
+ });
1444
+ } else {
1445
+ var empty={type:'',name:'',description:'',tags:[]};
1446
+ queue.forEach(function(q){ q.resolve(empty); });
1447
+ }
1448
+ })
1449
+ .catch(function(){
1450
+ var empty={type:'',name:'',description:'',tags:[]};
1451
+ queue.forEach(function(q){ q.resolve(empty); });
1452
+ });
1453
+ }
1454
+ function fetchAgentInfo(aid){
1455
+ if(agentInfoCache[aid]) return Promise.resolve(agentInfoCache[aid]);
1456
+ return new Promise(function(resolve){
1457
+ _agentInfoBatchQueue.push({aid:aid,resolve:resolve});
1458
+ if(!_agentInfoBatchTimer) _agentInfoBatchTimer=setTimeout(_flushAgentInfoBatch,50);
1459
+ });
1356
1460
  }
1357
1461
  async function deleteSession(e, sessionId){
1358
1462
  e.stopPropagation();
@@ -1382,7 +1486,7 @@ const chatHtml = `<!DOCTYPE html>
1382
1486
  } catch(err){ alert('删除失败: ' + err.message); }
1383
1487
  }
1384
1488
 
1385
- function initDom(){ D.myAid=$('myAid'); D.sList=$('sessionList'); D.title=$('chatTitle'); D.msgs=$('messages'); D.input=$('messageInput'); D.sendBtn=$('sendBtn'); D.dot=$('statusDot'); D.modal=$('modal'); D.tInput=$('targetAidInput'); D.cBtn=$('connectBtn'); D.sidebar=$('sidebar'); D.aidSel=$('aidSelect'); D.aidDot=$('aidOnlineDot'); D.aidStatusToggle=$('aidStatusToggle'); D.aidStatusText=$('aidStatusText'); D.p2pPanel=$('p2pPanel'); D.groupPanel=$('groupPanel'); D.groupList=$('groupList'); D.groupInfoBar=$('groupInfoBar'); D.groupInfoText=$('groupInfoText'); D.tabP2P=$('tabP2P'); D.tabGroup=$('tabGroup'); D.encryptBanner=$('encryptBanner'); D.newMsgTip=$('newMsgTip'); }
1489
+ 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'); }
1386
1490
 
1387
1491
  function isAtBottom(){ return D.msgs.scrollHeight-D.msgs.scrollTop-D.msgs.clientHeight<150; }
1388
1492
  function scrollToBottom(){ D.msgs.scrollTop=D.msgs.scrollHeight; hideNewMsgTip(); }
@@ -1390,7 +1494,19 @@ const chatHtml = `<!DOCTYPE html>
1390
1494
  function hideNewMsgTip(){ if(D.newMsgTip) D.newMsgTip.style.display='none'; }
1391
1495
 
1392
1496
  async function init(){
1497
+ // 刷新页面时强制回到身份管理页面
1498
+ if(!sessionStorage.getItem('chatEntry')){ window.location.href='/'; return; }
1499
+ sessionStorage.removeItem('chatEntry');
1393
1500
  initDom();
1501
+ // 配置 marked:支持换行、GFM
1502
+ if(typeof marked!=='undefined'&&marked.setOptions){
1503
+ marked.setOptions({breaks:true,gfm:true});
1504
+ }
1505
+ // 文件拖拽支持
1506
+ S.pendingFiles=[];
1507
+ D.inputArea.addEventListener('dragover',function(e){ e.preventDefault(); e.stopPropagation(); D.inputArea.classList.add('drag-over'); });
1508
+ D.inputArea.addEventListener('dragleave',function(e){ e.preventDefault(); e.stopPropagation(); D.inputArea.classList.remove('drag-over'); });
1509
+ 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); });
1394
1510
  // 监听滚动,用户滚到底部时自动隐藏新消息提示
1395
1511
  D.msgs.addEventListener('scroll',function(){ if(isAtBottom()) hideNewMsgTip(); });
1396
1512
  try {
@@ -1469,6 +1585,17 @@ const chatHtml = `<!DOCTYPE html>
1469
1585
  D.msgs.innerHTML=''; D.title.textContent='未选择会话';
1470
1586
  D.sList.dataset.s='';
1471
1587
  await loadSessions();
1588
+ // 群组状态重置并刷新
1589
+ S.groups=[]; S.activeGroupId=null;
1590
+ _lastGroupMsgSig='';
1591
+ if(S.tab==='group'){
1592
+ D.msgs.innerHTML='';
1593
+ renderGroupList();
1594
+ await initGroupClient();
1595
+ await pollGroupList();
1596
+ } else {
1597
+ renderGroupList();
1598
+ }
1472
1599
  } catch(e){
1473
1600
  msg.textContent='切换失败: '+(e.message||'未知错误');
1474
1601
  await new Promise(function(ok){setTimeout(ok,2000);});
@@ -1678,16 +1805,79 @@ const chatHtml = `<!DOCTYPE html>
1678
1805
  } catch(e){}
1679
1806
  }
1680
1807
 
1808
+ function autoResizeInput(){
1809
+ var el=D.input;
1810
+ el.style.height='auto';
1811
+ var maxH=105; // 5行 ≈ 5*21
1812
+ el.style.height=Math.min(el.scrollHeight,maxH)+'px';
1813
+ }
1814
+
1815
+ function addFiles(fileListObj){
1816
+ for(var i=0;i<fileListObj.length;i++){
1817
+ var f=fileListObj[i];
1818
+ // 跳过过大的文件(>2MB)
1819
+ if(f.size>2*1024*1024){ alert('文件 '+f.name+' 超过2MB,已跳过'); continue; }
1820
+ S.pendingFiles.push(f);
1821
+ }
1822
+ renderFileList();
1823
+ }
1824
+ function removeFile(idx){
1825
+ S.pendingFiles.splice(idx,1);
1826
+ renderFileList();
1827
+ }
1828
+ function renderFileList(){
1829
+ if(!S.pendingFiles.length){ D.fileList.style.display='none'; D.fileList.innerHTML=''; return; }
1830
+ D.fileList.style.display='flex';
1831
+ D.fileList.innerHTML=S.pendingFiles.map(function(f,i){
1832
+ return '<div class="file-item">'+
1833
+ '<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>'+
1834
+ '<span class="file-name" title="'+escH(f.name)+'">'+escH(f.name)+'</span>'+
1835
+ '<button class="file-remove" onclick="removeFile('+i+')" title="移除">'+
1836
+ '<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>'+
1837
+ '</button></div>';
1838
+ }).join('');
1839
+ }
1840
+ function readFileAsText(file){
1841
+ return new Promise(function(resolve){
1842
+ var reader=new FileReader();
1843
+ reader.onload=function(){ resolve(reader.result); };
1844
+ reader.onerror=function(){ resolve('[读取失败]'); };
1845
+ reader.readAsText(file);
1846
+ });
1847
+ }
1848
+ async function buildFileContent(){
1849
+ var parts=[];
1850
+ for(var i=0;i<S.pendingFiles.length;i++){
1851
+ var f=S.pendingFiles[i];
1852
+ var content=await readFileAsText(f);
1853
+ parts.push('<file name="'+f.name+'">\\n'+content+'\\n</file>');
1854
+ }
1855
+ return parts.join('\\n');
1856
+ }
1857
+
1681
1858
  async function sendMessage(){
1682
1859
  var txt=D.input.value.trim();
1683
- if(!txt){ return; }
1860
+ var hasFiles=S.pendingFiles&&S.pendingFiles.length>0;
1861
+ if(!txt&&!hasFiles){ return; }
1862
+ // 拼接文件内容
1863
+ if(hasFiles){
1864
+ var fileContent=await buildFileContent();
1865
+ txt=txt?(txt+'\\n'+fileContent):fileContent;
1866
+ S.pendingFiles=[];
1867
+ renderFileList();
1868
+ }
1684
1869
  // 用户主动发送消息,确保滚动到底部
1685
1870
  hideNewMsgTip();
1871
+
1872
+ // 禁用输入框和发送按钮
1873
+ D.input.disabled = true;
1874
+ D.sendBtn.disabled = true;
1875
+
1686
1876
  // 群组模式
1687
1877
  if(S.tab==='group'){
1688
- if(!S.activeGroupId){ alert('请先选择一个群组'); return; }
1878
+ if(!S.activeGroupId){ alert('请先选择一个群组'); D.input.disabled = false; D.sendBtn.disabled = false; return; }
1689
1879
  try {
1690
- D.input.value='';
1880
+ D.input.value=''; D.input.style.height='';
1691
1881
  var r=await fetch('/api/group/send',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({groupId:S.activeGroupId,message:txt,aid:S.aid})});
1692
1882
  var d=await r.json();
1693
1883
  if(!d.success) alert(d.error||'发送失败');
@@ -1705,18 +1895,28 @@ const chatHtml = `<!DOCTYPE html>
1705
1895
  }
1706
1896
  }
1707
1897
  } catch(e){ alert('发送失败'); }
1898
+ finally {
1899
+ D.input.disabled = false;
1900
+ D.sendBtn.disabled = false;
1901
+ D.input.focus();
1902
+ }
1708
1903
  return;
1709
1904
  }
1710
1905
  // P2P 模式
1711
- if(!S.sid){ alert('请先选择或新建一个会话'); return; }
1712
- if(S.closed){ alert('该会话已关闭,请新建会话继续通信'); return; }
1906
+ if(!S.sid){ alert('请先选择或新建一个会话'); D.input.disabled = false; D.sendBtn.disabled = false; return; }
1907
+ if(S.closed){ alert('该会话已关闭,请新建会话继续通信'); D.input.disabled = false; D.sendBtn.disabled = false; return; }
1713
1908
  try {
1714
- D.input.value='';
1909
+ D.input.value=''; D.input.style.height='';
1715
1910
  var r=await fetch('/api/ws/send',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:txt,sessionId:S.sid,aid:S.aid})});
1716
1911
  var d=await r.json();
1717
1912
  if(!d.success) alert(d.error||'发送失败');
1718
1913
  else { await loadMessages(); scrollToBottom(); }
1719
1914
  } catch(e){ alert('发送失败'); }
1915
+ finally {
1916
+ D.input.disabled = false;
1917
+ D.sendBtn.disabled = false;
1918
+ D.input.focus();
1919
+ }
1720
1920
  }
1721
1921
 
1722
1922
  function toggleSidebar(){
@@ -1781,7 +1981,9 @@ const chatHtml = `<!DOCTYPE html>
1781
1981
  S.tab=tab;
1782
1982
  D.tabP2P.className='tab'+(tab==='p2p'?' active':'');
1783
1983
  D.tabGroup.className='tab'+(tab==='group'?' active':'');
1784
- D.p2pPanel.style.display=tab==='p2p'?'block':'none';
1984
+ D.p2pPanel.style.display=tab==='p2p'?'flex':'none';
1985
+ if(tab==='p2p') D.p2pPanel.style.flex='1';
1986
+ D.groupPanel.style.flex=tab==='group'?'1':'';
1785
1987
  D.groupPanel.style.display=tab==='group'?'flex':'none';
1786
1988
  if(tab==='group'){
1787
1989
  D.encryptBanner.style.display='none';
@@ -1860,6 +2062,7 @@ const chatHtml = `<!DOCTYPE html>
1860
2062
  D.title.textContent=name;
1861
2063
  D.groupInfoBar.style.display='flex';
1862
2064
  D.groupInfoText.textContent=name;
2065
+ $('groupMemberStats').textContent='';
1863
2066
  D.input.disabled=false;
1864
2067
  D.input.placeholder='输入群消息...';
1865
2068
  D.input.focus();
@@ -1875,7 +2078,7 @@ const chatHtml = `<!DOCTYPE html>
1875
2078
  gmsg.textContent='选择群组...';
1876
2079
  await fetch('/api/group/select',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({groupId:groupId,aid:S.aid})});
1877
2080
  } catch(e){}
1878
- // 获取群信息判断是否为创建者
2081
+ // 获取群信息判断是否为创建者,同时获取成员统计
1879
2082
  try {
1880
2083
  gmsg.textContent='获取群信息...';
1881
2084
  var r=await fetch('/api/group/info?groupId='+encodeURIComponent(groupId)+'&aid='+encodeURIComponent(S.aid));
@@ -1888,6 +2091,14 @@ const chatHtml = `<!DOCTYPE html>
1888
2091
  } else {
1889
2092
  $('groupCopyLinkBtn').style.display='';
1890
2093
  }
2094
+ // 展示成员统计
2095
+ if(d.member_stats){
2096
+ var ms=d.member_stats;
2097
+ var parts=[ms.total+'人'];
2098
+ if(ms.human) parts.push(ms.human+'人类');
2099
+ if(ms.agent) parts.push(ms.agent+'Agent');
2100
+ $('groupMemberStats').textContent='('+parts.join(' / ')+')';
2101
+ }
1891
2102
  } catch(e){
1892
2103
  // 获取失败时默认显示复制群链接
1893
2104
  $('groupCopyLinkBtn').style.display='';
@@ -1911,7 +2122,7 @@ const chatHtml = `<!DOCTYPE html>
1911
2122
  var r=await fetch('/api/group/messages?groupId='+encodeURIComponent(S.activeGroupId)+'&aid='+encodeURIComponent(S.aid));
1912
2123
  var d=await r.json();
1913
2124
  console.log('[pollGroupMessages] response: msgCount='+(d.messages?d.messages.length:0)+' tab='+S.tab);
1914
- if(S.tab==='group'&&d.messages) renderGroupMsgs(d.messages);
2125
+ if(S.tab==='group'&&Array.isArray(d.messages)) renderGroupMsgs(d.messages);
1915
2126
  } catch(e){ console.error('[pollGroupMessages] error:', e); }
1916
2127
  }
1917
2128
 
@@ -2044,6 +2255,7 @@ const chatHtml = `<!DOCTYPE html>
2044
2255
  var _lastGroupMsgs=[];
2045
2256
  var _groupRuleData=null;
2046
2257
  function renderGroupMsgs(msgs){
2258
+ if(!Array.isArray(msgs)) msgs=[];
2047
2259
  // 不在群组 tab 时不渲染,防止覆盖 P2P 消息
2048
2260
  if(S.tab!=='group') return;
2049
2261
  if(isUserSelecting()) return;
@@ -2099,16 +2311,33 @@ const chatHtml = `<!DOCTYPE html>
2099
2311
  } else {
2100
2312
  D.msgs.scrollTop=prevScrollTop;
2101
2313
  }
2102
- // 异步加载未缓存的 agent info,加载完成后重新渲染以更新头像
2314
+ // 异步加载未缓存的 agent info,加载完成后局部更新头像和名字
2103
2315
  var unique=needFetch.filter(function(v,i,a){ return a.indexOf(v)===i; });
2104
- unique.forEach(function(aid){
2105
- fetchAgentInfo(aid).then(function(){
2106
- if(S.tab!=='group') return;
2107
- _lastGroupMsgSig='';
2108
- _lastGroupMsgs._forceRender=true;
2109
- renderGroupMsgs(_lastGroupMsgs);
2316
+ if(unique.length){
2317
+ fetchAgentInfo(unique[0]); // 触发批量请求(攒批机制会合并)
2318
+ unique.forEach(function(aid){
2319
+ fetchAgentInfo(aid).then(function(info){
2320
+ if(!info||S.tab!=='group') return;
2321
+ // 局部更新 DOM:找到该 sender 的所有消息,更新头像和名字
2322
+ var avatars=D.msgs.querySelectorAll('.msg-avatar[title="'+escH(aid)+'"]');
2323
+ var newSrc=getAvatarSrc(info.type);
2324
+ var displayName=(info.name)||aid;
2325
+ avatars.forEach(function(img){
2326
+ img.src=newSrc;
2327
+ img.title=displayName;
2328
+ // 更新同一消息里的名字
2329
+ var msgEl=img.closest('.message');
2330
+ if(msgEl){
2331
+ var meta=msgEl.querySelector('.msg-meta');
2332
+ if(meta&&!msgEl.classList.contains('sent')){
2333
+ var timeStr=meta.textContent.split(' · ')[1]||'';
2334
+ meta.textContent=displayName+' · '+timeStr;
2335
+ }
2336
+ }
2337
+ });
2338
+ });
2110
2339
  });
2111
- });
2340
+ }
2112
2341
  }
2113
2342
 
2114
2343
  // Group modals
@@ -2603,6 +2832,31 @@ async function handleRequest(req, res) {
2603
2832
  sendJson(res, info);
2604
2833
  return;
2605
2834
  }
2835
+ // 批量获取 agent info,优先读本地缓存,未缓存的后台预热
2836
+ if (pathname === '/api/agent-info-batch' && method === 'POST') {
2837
+ try {
2838
+ const body = await parseBody(req);
2839
+ const aids = body.aids || [];
2840
+ const result = {};
2841
+ const empty = { type: '', name: '', description: '', tags: [] };
2842
+ for (const aid of aids) {
2843
+ const cached = agentInfoCache.get(aid);
2844
+ if (cached && Date.now() - cached.cachedAt < AGENT_INFO_CACHE_TTL) {
2845
+ result[aid] = { type: cached.type, name: cached.name, description: cached.description, tags: cached.tags || [] };
2846
+ }
2847
+ else {
2848
+ result[aid] = cached ? { type: cached.type, name: cached.name, description: cached.description, tags: cached.tags || [] } : empty;
2849
+ // 后台异步拉取,下次请求就有了
2850
+ getAgentInfo(aid).catch(() => { });
2851
+ }
2852
+ }
2853
+ sendJson(res, { success: true, data: result });
2854
+ }
2855
+ catch (e) {
2856
+ sendJson(res, { success: false, error: e.message });
2857
+ }
2858
+ return;
2859
+ }
2606
2860
  // 获取远程 agent.md 原始内容
2607
2861
  if (pathname === '/api/agent-md-raw' && method === 'GET') {
2608
2862
  const aid = parsedUrl.query.aid;
@@ -2807,6 +3061,19 @@ async function handleRequest(req, res) {
2807
3061
  sendJson(res, { success: false, error: '缺少 aid' });
2808
3062
  return;
2809
3063
  }
3064
+ // 验证目标 Agent 是否存在
3065
+ try {
3066
+ const agentMdUrl = `https://${targetAid}/agent.md`;
3067
+ const checkRes = await fetch(agentMdUrl, { method: 'GET', signal: AbortSignal.timeout(5000) });
3068
+ if (!checkRes.ok) {
3069
+ sendJson(res, { success: false, error: '该 AGENT 不存在,添加失败' });
3070
+ return;
3071
+ }
3072
+ }
3073
+ catch (_b) {
3074
+ sendJson(res, { success: false, error: '该 AGENT 不存在,添加失败' });
3075
+ return;
3076
+ }
2810
3077
  // 自动上线
2811
3078
  const instance = await ensureOnline(aid);
2812
3079
  if (!instance.agentWS) {
@@ -3102,8 +3369,33 @@ async function handleRequest(req, res) {
3102
3369
  }
3103
3370
  const instance = await ensureOnline(aid);
3104
3371
  await ensureGroupClient(instance);
3105
- const info = await instance.agentCP.groupOps.getGroupInfo(instance.groupTargetAid, groupId);
3106
- sendJson(res, Object.assign({ success: true }, info));
3372
+ const [info, membersResult] = await Promise.all([
3373
+ instance.agentCP.groupOps.getGroupInfo(instance.groupTargetAid, groupId),
3374
+ instance.agentCP.groupOps.getMembers(instance.groupTargetAid, groupId).catch(() => ({ members: [] })),
3375
+ ]);
3376
+ // 只用本地缓存统计成员类型,不发远程请求,不阻塞响应
3377
+ let humanCount = 0, agentCount = 0;
3378
+ const members = membersResult.members || [];
3379
+ for (const m of members) {
3380
+ const memberAid = m.agent_id || '';
3381
+ if (!memberAid)
3382
+ continue;
3383
+ const cached = agentInfoCache.get(memberAid);
3384
+ if (cached) {
3385
+ if (cached.type === 'human')
3386
+ humanCount++;
3387
+ else if (cached.type === 'agent')
3388
+ agentCount++;
3389
+ }
3390
+ }
3391
+ sendJson(res, Object.assign(Object.assign({ success: true }, info), { member_stats: { total: members.length, human: humanCount, agent: agentCount } }));
3392
+ // 后台异步预热未缓存的 agent info,下次请求就有了
3393
+ for (const m of members) {
3394
+ const memberAid = m.agent_id || '';
3395
+ if (memberAid && !agentInfoCache.has(memberAid)) {
3396
+ getAgentInfo(memberAid).catch(() => { });
3397
+ }
3398
+ }
3107
3399
  }
3108
3400
  catch (e) {
3109
3401
  sendJson(res, { success: false, error: e.message });
@@ -3136,6 +3428,7 @@ async function handleRequest(req, res) {
3136
3428
  sendJson(res, Object.assign({ success: true }, result));
3137
3429
  }
3138
3430
  catch (e) {
3431
+ utils_1.logger.error(`[API] /api/group/send FAILED: error=${e.message}`);
3139
3432
  sendJson(res, { success: false, error: e.message });
3140
3433
  }
3141
3434
  return;
@@ -3156,7 +3449,7 @@ async function handleRequest(req, res) {
3156
3449
  await ensureGroupClient(instance);
3157
3450
  // 只读本地缓存,不再每次请求都去服务端拉取
3158
3451
  // 新消息通过 WebSocket 推送实时到达并由 SDK 自动存储
3159
- const messages = instance.agentCP.getLocalGroupMessages(groupId);
3452
+ const messages = instance.agentCP.getLocalGroupMessages(groupId) || [];
3160
3453
  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}`);
3161
3454
  sendJson(res, { success: true, messages });
3162
3455
  }
@@ -3208,7 +3501,7 @@ async function handleRequest(req, res) {
3208
3501
  let groupName = groupId;
3209
3502
  try {
3210
3503
  const info = await instance.agentCP.groupOps.getGroupInfo(targetAid, groupId);
3211
- groupName = info.name || groupId;
3504
+ groupName = (info && info.name) || groupId;
3212
3505
  }
3213
3506
  catch (_) { }
3214
3507
  instance.agentCP.addGroupToStore(groupId, groupName);
@@ -3319,11 +3612,11 @@ async function handleRequest(req, res) {
3319
3612
  const result = await ops.listMyGroups(target);
3320
3613
  // 尝试获取每个群的详细信息(名称等)
3321
3614
  const groups = [];
3322
- for (const m of result.groups) {
3615
+ for (const m of (result.groups || [])) {
3323
3616
  let name = m.group_id;
3324
3617
  try {
3325
3618
  const info = await ops.getGroupInfo(target, m.group_id);
3326
- name = info.name || m.group_id;
3619
+ name = (info && info.name) || m.group_id;
3327
3620
  }
3328
3621
  catch (_) { }
3329
3622
  groups.push(Object.assign(Object.assign({}, m), { name }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "acp-ts",
3
- "version": "1.2.4",
3
+ "version": "1.2.6",
4
4
  "description": "基于 ACP智能体通信协议 的智能体通信库,提供智能体身份管理和实时通信功能",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",