acp-ts 1.1.2 → 1.1.3

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
@@ -287,768 +287,808 @@ function getMessageStore() {
287
287
  return agentCP.messageStore;
288
288
  }
289
289
  // HTML 页面
290
- const indexHtml = `<!DOCTYPE html>
291
- <html lang="zh-CN">
292
- <head>
293
- <meta charset="UTF-8">
294
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
295
- <link rel="icon" href="/favicon.ico" type="image/x-icon">
296
- <title>ACP 身份管理</title>
297
- <style>
298
- * { box-sizing: border-box; margin: 0; padding: 0; }
299
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; min-height: 100vh; display: flex; justify-content: center; align-items: flex-start; padding: 40px 20px; }
300
- .container { background: white; padding: 32px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); max-width: 560px; width: 100%; }
301
- h1 { color: #333; margin-bottom: 24px; text-align: center; font-size: 22px; }
302
- .hint { text-align: center; color: #999; font-size: 13px; margin-bottom: 20px; }
303
- .create-section { margin-bottom: 24px; }
304
- .create-section input { width: 100%; padding: 10px 14px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px; margin-bottom: 10px; }
305
- .create-section input:focus { outline: none; border-color: #007bff; }
306
- .btn { display: block; width: 100%; padding: 12px; border: none; border-radius: 8px; font-size: 15px; cursor: pointer; transition: background 0.2s; }
307
- .btn-primary { background: #007bff; color: white; }
308
- .btn-primary:hover { background: #0056b3; }
309
- .btn-sm { display: inline-block; width: auto; padding: 6px 14px; font-size: 13px; border-radius: 6px; }
310
- .btn-success { background: #28a745; color: white; }
311
- .btn-success:hover { background: #218838; }
312
- .btn-danger { background: #dc3545; color: white; }
313
- .btn-danger:hover { background: #c82333; }
314
- .btn-outline { background: white; color: #007bff; border: 1px solid #007bff; }
315
- .btn-outline:hover { background: #e7f1ff; }
316
- .btn-outline.active { background: #007bff; color: white; }
317
- .btn:disabled { background: #ccc; cursor: not-allowed; border-color: #ccc; color: #fff; }
318
- .aid-list { margin-bottom: 24px; }
319
- .aid-card { background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 10px; padding: 16px; margin-bottom: 12px; transition: border-color 0.2s; display: flex; align-items: stretch; gap: 12px; }
320
- .aid-card.current { border-color: #007bff; background: #f0f7ff; }
321
- .aid-card-left { flex: 1; min-width: 0; }
322
- .aid-card-right { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; flex-shrink: 0; justify-content: center; }
323
- .aid-card-header { margin-bottom: 10px; }
324
- .aid-name { font-family: monospace; font-size: 13px; color: #333; word-break: break-all; }
325
- .copy-btn { background: none; border: none; color: #6c757d; cursor: pointer; font-size: 12px; padding: 2px 6px; }
326
- .copy-btn:hover { color: #333; }
327
- .aid-card-status { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
328
- .badge { display: inline-block; padding: 3px 10px; border-radius: 12px; font-size: 12px; font-weight: 500; }
329
- .badge-success { background: #d4edda; color: #155724; }
330
- .badge-warning { background: #fff3cd; color: #856404; }
331
- .badge-danger { background: #f8d7da; color: #721c24; }
332
- .badge-info { background: #d1ecf1; color: #0c5460; }
333
- .badge-current { background: #007bff; color: white; }
334
- .aid-card-actions { display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
335
- .status { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); padding: 12px 24px; border-radius: 8px; font-size: 14px; display: none; z-index: 1000; }
336
- .status.success { display: block; background: #d4edda; color: #155724; }
337
- .status.error { display: block; background: #f8d7da; color: #721c24; }
338
- </style>
339
- </head>
340
- <body>
341
- <div class="container">
342
- <h1>ACP 身份管理</h1>
343
- <div class="hint" id="hint">最多注册 10 AID</div>
344
-
345
- <div class="create-section" id="createSection">
346
- <input type="text" id="newAid" placeholder="输入 AID 名称(自动添加 .aid.pub 后缀)">
347
- <button class="btn btn-primary" onclick="createAid()">注册 AID</button>
348
- </div>
349
-
350
- <div class="aid-list" id="aidList"></div>
351
-
352
- <div class="status" id="status"></div>
353
- </div>
354
-
355
- <script>
356
- let aidData = { currentAid: '', aidList: [], aidStatus: [] };
357
-
358
- async function loadAidInfo() {
359
- try {
360
- const res = await fetch('/api/aid');
361
- const data = await res.json();
362
- aidData = data;
363
- renderAidList();
364
- } catch (e) {
365
- console.error('加载失败', e);
366
- }
367
- }
368
-
369
- function renderAidList() {
370
- const list = document.getElementById('aidList');
371
- const createSection = document.getElementById('createSection');
372
- const hint = document.getElementById('hint');
373
-
374
- if (aidData.aidList.length >= 10) {
375
- createSection.style.display = 'none';
376
- hint.textContent = '已达到 10 个 AID 上限';
377
- } else {
378
- createSection.style.display = 'block';
379
- hint.textContent = '最多注册 10 AID(已注册 ' + aidData.aidList.length + ' 个)';
380
- }
381
-
382
- if (!aidData.aidStatus || aidData.aidStatus.length === 0) {
383
- list.innerHTML = '<div style="text-align:center;color:#999;padding:20px;">暂无 AID,请先注册</div>';
384
- return;
385
- }
386
-
387
- list.innerHTML = aidData.aidStatus.map(function(item) {
388
- var isCurrent = item.aid === aidData.currentAid;
389
- var cardClass = 'aid-card' + (isCurrent ? ' current' : '');
390
-
391
- var badges = '';
392
- if (isCurrent) badges += '<span class="badge badge-current">当前</span>';
393
- if (item.online) badges += '<span class="badge badge-success">已上线</span>';
394
- if (item.keysExist && item.certValid) {
395
- badges += '<span class="badge badge-info">密钥有效</span>';
396
- } else if (item.keysExist && !item.certValid) {
397
- badges += '<span class="badge badge-warning">证书过期</span>';
398
- } else {
399
- badges += '<span class="badge badge-danger">密钥缺失</span>';
400
- }
401
-
402
- var actions = '';
403
- if (!isCurrent) {
404
- actions += '<button class="btn btn-sm btn-outline" onclick="selectAid(\\'' + escapeAttr(item.aid) + '\\')">选为当前</button>';
405
- }
406
- // 上线并进入聊天(合并按钮)
407
- if (isCurrent && item.keysExist && item.certValid) {
408
- if (item.online) {
409
- actions += '<button class="btn btn-sm btn-success" onclick="enterChat(\\'' + escapeAttr(item.aid) + '\\')">进入聊天</button>';
410
- actions += '<button class="btn btn-sm btn-danger" onclick="goOffline(\\'' + escapeAttr(item.aid) + '\\')">下线</button>';
411
- } else {
412
- actions += '<button class="btn btn-sm btn-success" id="goBtn_' + escapeAttr(item.aid) + '" onclick="goOnlineAndChat(\\'' + escapeAttr(item.aid) + '\\')">上线并进入</button>';
413
- }
414
- }
415
- if (!isCurrent && item.online) {
416
- actions += '<button class="btn btn-sm btn-danger" onclick="goOffline(\\'' + escapeAttr(item.aid) + '\\')">下线</button>';
417
- }
418
-
419
- return '<div class="' + cardClass + '">' +
420
- '<div class="aid-card-left">' +
421
- '<div class="aid-card-header">' +
422
- '<span class="aid-name">' + escapeHtml(item.aid) + '</span>' +
423
- '</div>' +
424
- '<div class="aid-card-status">' + badges + '</div>' +
425
- '</div>' +
426
- '<div class="aid-card-right">' +
427
- '<div class="aid-card-actions">' + actions + '</div>' +
428
- '<button class="copy-btn" onclick="copyText(\\'' + escapeAttr(item.aid) + '\\')">复制</button>' +
429
- '</div>' +
430
- '</div>';
431
- }).join('');
432
- }
433
-
434
- async function createAid() {
435
- var prefix = document.getElementById('newAid').value.trim();
436
- if (!prefix) { showStatus('请输入 AID 前缀名称', 'error'); return; }
437
- try {
438
- var res = await fetch('/api/aid/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prefix: prefix }) });
439
- var data = await res.json();
440
- if (data.success) { showStatus('AID 注册成功', 'success'); document.getElementById('newAid').value = ''; loadAidInfo(); }
441
- else { showStatus(data.error || '注册失败', 'error'); }
442
- } catch (e) { showStatus('注册失败: ' + e.message, 'error'); }
443
- }
444
-
445
- async function selectAid(aid) {
446
- try {
447
- var res = await fetch('/api/aid/select', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ aid: aid }) });
448
- var data = await res.json();
449
- if (data.success) { showStatus('已切换到 ' + aid, 'success'); loadAidInfo(); }
450
- else { showStatus(data.error || '切换失败', 'error'); }
451
- } catch (e) { showStatus('切换失败: ' + e.message, 'error'); }
452
- }
453
-
454
- async function goOnlineAndChat(aid) {
455
- var btn = document.getElementById('goBtn_' + aid);
456
- if (btn) { btn.disabled = true; btn.textContent = '启动中...'; }
457
- try {
458
- showStatus('正在上线 ' + aid + ' ...', 'success');
459
- var res = await fetch('/api/ws/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ aid: aid }) });
460
- var data = await res.json();
461
- if (data.success) {
462
- showStatus(aid + ' 已上线,正在进入聊天...', 'success');
463
- window.location.href = '/chat';
464
- } else {
465
- showStatus(data.error || '上线失败', 'error');
466
- if (btn) { btn.disabled = false; btn.textContent = '上线并进入'; }
467
- }
468
- } catch (e) {
469
- showStatus('上线失败: ' + e.message, 'error');
470
- if (btn) { btn.disabled = false; btn.textContent = '上线并进入'; }
471
- }
472
- }
473
-
474
- function enterChat(aid) { window.location.href = '/chat'; }
475
-
476
- async function goOffline(aid) {
477
- try {
478
- var res = await fetch('/api/aid/offline', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ aid: aid }) });
479
- var data = await res.json();
480
- if (data.success) { showStatus(aid + ' 已下线', 'success'); loadAidInfo(); }
481
- else { showStatus(data.error || '下线失败', 'error'); }
482
- } catch (e) { showStatus('下线失败: ' + e.message, 'error'); }
483
- }
484
-
485
- function copyText(text) {
486
- navigator.clipboard.writeText(text).then(function() {
487
- showStatus('已复制', 'success');
488
- });
489
- }
490
-
491
- function showStatus(msg, type) {
492
- var el = document.getElementById('status');
493
- el.textContent = msg;
494
- el.className = 'status ' + type;
495
- setTimeout(function() { el.className = 'status'; }, 3000);
496
- }
497
-
498
- function escapeHtml(text) {
499
- var div = document.createElement('div');
500
- div.textContent = text;
501
- return div.innerHTML;
502
- }
503
-
504
- function escapeAttr(text) {
505
- return text.replace(/'/g, "\\\\'").replace(/"/g, '&quot;');
506
- }
507
-
508
- loadAidInfo();
509
- setInterval(loadAidInfo, 5000);
510
- <\/script>
511
- </body>
290
+ const indexHtml = `<!DOCTYPE html>
291
+ <html lang="zh-CN">
292
+ <head>
293
+ <meta charset="UTF-8">
294
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
295
+ <link rel="icon" href="/favicon.ico" type="image/x-icon">
296
+ <title>ACP 身份管理</title>
297
+ <style>
298
+ * { box-sizing: border-box; margin: 0; padding: 0; }
299
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; min-height: 100vh; display: flex; justify-content: center; align-items: flex-start; padding: 40px 20px; }
300
+ .container { background: white; padding: 32px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); max-width: 560px; width: 100%; }
301
+ h1 { color: #333; margin-bottom: 24px; text-align: center; font-size: 22px; }
302
+ .hint { text-align: center; color: #999; font-size: 13px; margin-bottom: 20px; }
303
+ .create-section { margin-bottom: 24px; }
304
+ .create-section .aid-input-row { display: flex; gap: 8px; margin-bottom: 10px; align-items: center; }
305
+ .create-section .aid-input-row input { flex: 1; padding: 10px 14px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px; min-width: 0; }
306
+ .create-section .aid-input-row input:focus { outline: none; border-color: #007bff; }
307
+ .create-section .aid-input-row .dot-separator { color: #999; font-size: 16px; flex-shrink: 0; }
308
+ .create-section .aid-input-row select {
309
+ padding: 10px 30px 10px 14px;
310
+ border: 1px solid #ddd;
311
+ border-radius: 8px;
312
+ font-size: 14px;
313
+ background: white;
314
+ flex-shrink: 0;
315
+ cursor: pointer;
316
+ appearance: none;
317
+ -webkit-appearance: none;
318
+ -moz-appearance: none;
319
+ 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");
320
+ background-repeat: no-repeat;
321
+ background-position: right 10px top 50%;
322
+ background-size: 10px auto;
323
+ }
324
+ .create-section .aid-input-row select:focus { outline: none; border-color: #007bff; }
325
+ .btn { display: block; width: 100%; padding: 12px; border: none; border-radius: 8px; font-size: 15px; cursor: pointer; transition: background 0.2s; }
326
+ .btn-primary { background: #007bff; color: white; }
327
+ .btn-primary:hover { background: #0056b3; }
328
+ .btn-sm { display: inline-block; width: auto; padding: 6px 14px; font-size: 13px; border-radius: 6px; }
329
+ .btn-success { background: #28a745; color: white; }
330
+ .btn-success:hover { background: #218838; }
331
+ .btn-danger { background: #dc3545; color: white; }
332
+ .btn-danger:hover { background: #c82333; }
333
+ .btn-outline { background: white; color: #007bff; border: 1px solid #007bff; }
334
+ .btn-outline:hover { background: #e7f1ff; }
335
+ .btn-outline.active { background: #007bff; color: white; }
336
+ .btn:disabled { background: #ccc; cursor: not-allowed; border-color: #ccc; color: #fff; }
337
+ .aid-list { margin-bottom: 24px; }
338
+ .aid-card { background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 10px; padding: 16px; margin-bottom: 12px; transition: border-color 0.2s; display: flex; align-items: stretch; gap: 12px; }
339
+ .aid-card.current { border-color: #007bff; background: #f0f7ff; }
340
+ .aid-card-left { flex: 1; min-width: 0; }
341
+ .aid-card-right { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; flex-shrink: 0; justify-content: center; }
342
+ .aid-card-header { margin-bottom: 10px; }
343
+ .aid-name { font-family: monospace; font-size: 13px; color: #333; word-break: break-all; }
344
+ .copy-btn { background: none; border: none; color: #6c757d; cursor: pointer; font-size: 12px; padding: 2px 6px; }
345
+ .copy-btn:hover { color: #333; }
346
+ .aid-card-status { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
347
+ .badge { display: inline-block; padding: 3px 10px; border-radius: 12px; font-size: 12px; font-weight: 500; }
348
+ .badge-success { background: #d4edda; color: #155724; }
349
+ .badge-warning { background: #fff3cd; color: #856404; }
350
+ .badge-danger { background: #f8d7da; color: #721c24; }
351
+ .badge-info { background: #d1ecf1; color: #0c5460; }
352
+ .badge-current { background: #007bff; color: white; }
353
+ .aid-card-actions { display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
354
+ .status { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); padding: 12px 24px; border-radius: 8px; font-size: 14px; display: none; z-index: 1000; }
355
+ .status.success { display: block; background: #d4edda; color: #155724; }
356
+ .status.error { display: block; background: #f8d7da; color: #721c24; }
357
+ </style>
358
+ </head>
359
+ <body>
360
+ <div class="container">
361
+ <h1>ACP 身份管理</h1>
362
+ <div class="hint" id="hint">最多注册 10 个 AID</div>
363
+
364
+ <div class="create-section" id="createSection">
365
+ <div class="aid-input-row">
366
+ <input type="text" id="newAid" placeholder="输入名称">
367
+ <span class="dot-separator">.</span>
368
+ <select id="apSelect"></select>
369
+ </div>
370
+ <button class="btn btn-primary" onclick="createAid()">注册 AID</button>
371
+ </div>
372
+
373
+ <div class="aid-list" id="aidList"></div>
374
+
375
+ <div class="status" id="status"></div>
376
+ </div>
377
+
378
+ <script>
379
+ let aidData = { currentAid: '', aidList: [], aidStatus: [], apiUrl: '' };
380
+
381
+ async function loadAidInfo() {
382
+ try {
383
+ const res = await fetch('/api/aid');
384
+ const data = await res.json();
385
+ aidData = data;
386
+ updateApSelect();
387
+ renderAidList();
388
+ } catch (e) {
389
+ console.error('加载失败', e);
390
+ }
391
+ }
392
+
393
+ function updateApSelect() {
394
+ var sel = document.getElementById('apSelect');
395
+ if (sel && sel.options.length === 0) {
396
+ const options = ['agentcp.io', 'aid.show', 'agentid.pub'];
397
+ options.forEach(function(op) {
398
+ var opt = document.createElement('option');
399
+ opt.value = op;
400
+ opt.textContent = op;
401
+ if (op === 'agentcp.io') opt.selected = true;
402
+ sel.appendChild(opt);
403
+ });
404
+ }
405
+ }
406
+
407
+ function renderAidList() {
408
+ const list = document.getElementById('aidList');
409
+ const createSection = document.getElementById('createSection');
410
+ const hint = document.getElementById('hint');
411
+
412
+ if (aidData.aidList.length >= 10) {
413
+ createSection.style.display = 'none';
414
+ hint.textContent = '已达到 10 个 AID 上限';
415
+ } else {
416
+ createSection.style.display = 'block';
417
+ hint.textContent = '最多注册 10 个 AID(已注册 ' + aidData.aidList.length + ' 个)';
418
+ }
419
+
420
+ if (!aidData.aidStatus || aidData.aidStatus.length === 0) {
421
+ list.innerHTML = '<div style="text-align:center;color:#999;padding:20px;">暂无 AID,请先注册</div>';
422
+ return;
423
+ }
424
+
425
+ list.innerHTML = aidData.aidStatus.map(function(item) {
426
+ var isCurrent = item.aid === aidData.currentAid;
427
+ var cardClass = 'aid-card' + (isCurrent ? ' current' : '');
428
+
429
+ var badges = '';
430
+ if (isCurrent) badges += '<span class="badge badge-current">当前</span>';
431
+ if (item.online) badges += '<span class="badge badge-success">已上线</span>';
432
+ if (item.keysExist && item.certValid) {
433
+ badges += '<span class="badge badge-info">密钥有效</span>';
434
+ } else if (item.keysExist && !item.certValid) {
435
+ badges += '<span class="badge badge-warning">证书过期</span>';
436
+ } else {
437
+ badges += '<span class="badge badge-danger">密钥缺失</span>';
438
+ }
439
+
440
+ var actions = '';
441
+ if (!isCurrent) {
442
+ actions += '<button class="btn btn-sm btn-outline" onclick="selectAid(\\'' + escapeAttr(item.aid) + '\\')">选为当前</button>';
443
+ }
444
+ // 上线并进入聊天(合并按钮)
445
+ if (isCurrent && item.keysExist && item.certValid) {
446
+ if (item.online) {
447
+ actions += '<button class="btn btn-sm btn-success" onclick="enterChat(\\'' + escapeAttr(item.aid) + '\\')">进入聊天</button>';
448
+ actions += '<button class="btn btn-sm btn-danger" onclick="goOffline(\\'' + escapeAttr(item.aid) + '\\')">下线</button>';
449
+ } else {
450
+ actions += '<button class="btn btn-sm btn-success" id="goBtn_' + escapeAttr(item.aid) + '" onclick="goOnlineAndChat(\\'' + escapeAttr(item.aid) + '\\')">上线并进入</button>';
451
+ }
452
+ }
453
+ if (!isCurrent && item.online) {
454
+ actions += '<button class="btn btn-sm btn-danger" onclick="goOffline(\\'' + escapeAttr(item.aid) + '\\')">下线</button>';
455
+ }
456
+
457
+ return '<div class="' + cardClass + '">' +
458
+ '<div class="aid-card-left">' +
459
+ '<div class="aid-card-header">' +
460
+ '<span class="aid-name">' + escapeHtml(item.aid) + '</span>' +
461
+ '</div>' +
462
+ '<div class="aid-card-status">' + badges + '</div>' +
463
+ '</div>' +
464
+ '<div class="aid-card-right">' +
465
+ '<div class="aid-card-actions">' + actions + '</div>' +
466
+ '<button class="copy-btn" onclick="copyText(\\'' + escapeAttr(item.aid) + '\\')">复制</button>' +
467
+ '</div>' +
468
+ '</div>';
469
+ }).join('');
470
+ }
471
+
472
+ async function createAid() {
473
+ var prefix = document.getElementById('newAid').value.trim();
474
+ if (!prefix) { showStatus('请输入 AID 名称', 'error'); return; }
475
+ var ap = document.getElementById('apSelect').value;
476
+ if (!ap) { showStatus('请选择 AP', 'error'); return; }
477
+ var fullPrefix = prefix + '.' + ap;
478
+ try {
479
+ var res = await fetch('/api/aid/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prefix: fullPrefix }) });
480
+ var data = await res.json();
481
+ if (data.success) { showStatus('AID 注册成功', 'success'); document.getElementById('newAid').value = ''; loadAidInfo(); }
482
+ else { showStatus(data.error || '注册失败', 'error'); }
483
+ } catch (e) { showStatus('注册失败: ' + e.message, 'error'); }
484
+ }
485
+
486
+ async function selectAid(aid) {
487
+ try {
488
+ var res = await fetch('/api/aid/select', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ aid: aid }) });
489
+ var data = await res.json();
490
+ if (data.success) { showStatus('已切换到 ' + aid, 'success'); loadAidInfo(); }
491
+ else { showStatus(data.error || '切换失败', 'error'); }
492
+ } catch (e) { showStatus('切换失败: ' + e.message, 'error'); }
493
+ }
494
+
495
+ async function goOnlineAndChat(aid) {
496
+ var btn = document.getElementById('goBtn_' + aid);
497
+ if (btn) { btn.disabled = true; btn.textContent = '启动中...'; }
498
+ try {
499
+ showStatus('正在上线 ' + aid + ' ...', 'success');
500
+ var res = await fetch('/api/ws/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ aid: aid }) });
501
+ var data = await res.json();
502
+ if (data.success) {
503
+ showStatus(aid + ' 已上线,正在进入聊天...', 'success');
504
+ window.location.href = '/chat';
505
+ } else {
506
+ showStatus(data.error || '上线失败', 'error');
507
+ if (btn) { btn.disabled = false; btn.textContent = '上线并进入'; }
508
+ }
509
+ } catch (e) {
510
+ showStatus('上线失败: ' + e.message, 'error');
511
+ if (btn) { btn.disabled = false; btn.textContent = '上线并进入'; }
512
+ }
513
+ }
514
+
515
+ function enterChat(aid) { window.location.href = '/chat'; }
516
+
517
+ async function goOffline(aid) {
518
+ try {
519
+ var res = await fetch('/api/aid/offline', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ aid: aid }) });
520
+ var data = await res.json();
521
+ if (data.success) { showStatus(aid + ' 已下线', 'success'); loadAidInfo(); }
522
+ else { showStatus(data.error || '下线失败', 'error'); }
523
+ } catch (e) { showStatus('下线失败: ' + e.message, 'error'); }
524
+ }
525
+
526
+ function copyText(text) {
527
+ navigator.clipboard.writeText(text).then(function() {
528
+ showStatus('已复制', 'success');
529
+ });
530
+ }
531
+
532
+ function showStatus(msg, type) {
533
+ var el = document.getElementById('status');
534
+ el.textContent = msg;
535
+ el.className = 'status ' + type;
536
+ setTimeout(function() { el.className = 'status'; }, 3000);
537
+ }
538
+
539
+ function escapeHtml(text) {
540
+ var div = document.createElement('div');
541
+ div.textContent = text;
542
+ return div.innerHTML;
543
+ }
544
+
545
+ function escapeAttr(text) {
546
+ return text.replace(/'/g, "\\\\'").replace(/"/g, '&quot;');
547
+ }
548
+
549
+ loadAidInfo();
550
+ setInterval(loadAidInfo, 5000);
551
+ <\/script>
552
+ </body>
512
553
  </html>`;
513
- const chatHtml = `<!DOCTYPE html>
514
- <html lang="zh-CN">
515
- <head>
516
- <meta charset="UTF-8">
517
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
518
- <title>ACP 聊天</title>
519
- <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"><\/script>
520
- <style>
521
- :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; }
522
- * { box-sizing:border-box; margin:0; padding:0; }
523
- body { font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; background:var(--bg); height:100vh; overflow:hidden; color:var(--t1); }
524
- #app { display:flex; height:100%; }
525
-
526
- /* Sidebar */
527
- .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; }
528
- .sidebar.collapsed { width:0; border-right:none; }
529
- .sidebar-header { padding:12px 14px; border-bottom:1px solid var(--border); display:flex; flex-direction:column; gap:12px; flex-shrink:0; }
530
- .header-top { display:flex; justify-content:space-between; align-items:center; width:100%; }
531
- .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; }
532
- .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; }
533
- .new-chat-btn:hover { background:var(--primary-h); }
534
- .session-list { flex:1; overflow-y:auto; }
535
-
536
- /* AID Group */
537
- .aid-group { border-bottom:1px solid var(--border); }
538
- .aid-group-header { padding:12px 14px; display:flex; align-items:center; cursor:pointer; background:linear-gradient(135deg,#f8fafc,#f1f5f9); user-select:none; border-left:3px solid var(--primary); }
539
- .aid-group-header:hover { background:linear-gradient(135deg,#eef2f7,#e8edf4); }
540
- .aid-group-info { flex:1; min-width:0; margin-left:4px; }
541
- .aid-group-title { font-size:13px; font-weight:700; color:var(--t1); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; display:block; }
542
- .aid-group-desc { font-size:10px; color:var(--t2); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; margin-top:2px; display:block; }
543
- .aid-group-arrow { font-size:10px; color:var(--t2); transition:transform 0.2s; flex-shrink:0; }
544
- .aid-group-arrow.open { transform:rotate(90deg); }
545
- .aid-group-badge { font-size:10px; background:var(--primary); color:#fff; padding:1px 6px; border-radius:8px; margin-left:8px; flex-shrink:0; }
546
- .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; }
547
- .aid-group-add:hover { background:var(--primary); color:#fff; border-color:var(--primary); }
548
- .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; }
549
- .aid-group-header:hover .aid-group-del { display:block; }
550
- .aid-group-del:hover { color:#dc3545; background:#ffebeb; }
551
- .session-del { position:absolute; right:8px; top:12px; background:none; border:none; color:var(--t2); font-size:12px; cursor:pointer; display:none; padding:2px; }
552
- .session-item:hover .session-del { display:block; }
553
- .session-del:hover { color:#dc3545; }
554
- .aid-group-sessions { display:none; }
555
- .aid-group-sessions.open { display:block; }
556
-
557
- .aid-group-avatar { width:34px; height:34px; border-radius:50%; object-fit:cover; flex-shrink:0; margin-right:8px; box-shadow:0 1px 3px rgba(0,0,0,0.12); }
558
-
559
- .session-item { padding:10px 14px 10px 32px; border-bottom:1px solid #f3f4f6; cursor:pointer; transition:background 0.15s; position:relative; }
560
- .session-item::before { content:''; position:absolute; left:18px; top:16px; width:6px; height:6px; border-radius:50%; background:var(--border); }
561
- .session-item:hover { background:#f5f7fa; }
562
- .session-item.active { background:#eff6ff; border-left:3px solid var(--primary); padding-left:29px; }
563
- .session-item.active::before { background:var(--primary); }
564
- .session-peer { font-weight:400; font-size:12px; color:var(--t1); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; padding-left:10px; }
565
- .session-meta { font-size:10px; color:var(--t2); margin-top:2px; display:flex; align-items:center; gap:6px; padding-left:10px; }
566
- .tag { font-size:9px; padding:1px 5px; border-radius:3px; color:#fff; }
567
- .tag.outgoing { background:var(--ok); }
568
- .tag.incoming { background:var(--t2); }
569
-
570
- /* Chat Area */
571
- .chat-area { flex:1; display:flex; flex-direction:column; background:var(--chat-bg); min-width:0; }
572
- .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; }
573
- .header-left { display:flex; align-items:center; gap:10px; overflow:hidden; }
574
- .toggle-sidebar-btn { background:none; border:none; cursor:pointer; color:var(--t2); padding:4px; display:flex; }
575
- .toggle-sidebar-btn:hover { color:var(--t1); }
576
- .status-dot { width:8px; height:8px; border-radius:50%; background:#ccc; flex-shrink:0; }
577
- .status-dot.connected { background:var(--ok); }
578
- .status-dot.connecting { background:#fbbf24; }
579
- .chat-title { font-size:15px; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
580
-
581
- .aid-select-wrap { display:flex; align-items:center; gap:10px; flex-shrink:0; }
582
- .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); }
583
- .manage-btn:hover { background:#f8fafc; color:var(--primary); border-color:var(--primary); }
584
- .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); }
585
- .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; }
586
- .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); }
587
- .status-toggle:hover { background:#f1f5f9; }
588
- .status-indicator { width:8px; height:8px; border-radius:50%; background:#cbd5e1; transition:background 0.3s; }
589
- .status-indicator.online { background:var(--ok); box-shadow:0 0 0 2px rgba(16,185,129,0.2); }
590
- .status-indicator.offline { background:#cbd5e1; }
591
-
592
- .collapse-btn { background:none; border:none; cursor:pointer; color:var(--t2); padding:6px; display:flex; align-items:center; flex-shrink:0; }
593
- .collapse-btn:hover { color:var(--t1); }
594
-
595
- .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; }
596
- .encrypt-banner svg { flex-shrink:0; }
597
-
598
- .messages { flex:1; padding:16px; overflow-y:auto; display:flex; flex-direction:column; gap:12px; }
599
- .message { display:flex; flex-direction:column; max-width:80%; }
600
- .message.sent { align-self:flex-end; align-items:flex-end; }
601
- .message.received { align-self:flex-start; align-items:flex-start; }
602
- .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); }
603
- .message.sent .bubble { background:var(--sent); color:#fff; border-bottom-right-radius:2px; }
604
- .message.received .bubble { background:var(--recv-bg); color:var(--t1); border-bottom-left-radius:2px; border:1px solid var(--border); }
605
- .msg-meta { font-size:10px; color:var(--t2); margin-top:3px; padding:0 4px; }
606
-
607
- .input-area { padding:12px 16px; background:#fff; border-top:1px solid var(--border); display:flex; align-items:center; gap:10px; flex-shrink:0; }
608
- .input-area input { flex:1; padding:10px 14px; border-radius:20px; border:1px solid var(--border); font-size:14px; background:#f9fafb; }
609
- .input-area input:focus { outline:none; border-color:var(--primary); background:#fff; }
610
- .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; }
611
- .send-btn:hover { background:var(--primary-h); }
612
- .send-btn:disabled { background:#ccc; cursor:not-allowed; }
613
-
614
- .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; }
615
- .modal-overlay.show { display:flex; }
616
- .modal { background:#fff; width:90%; max-width:400px; border-radius:12px; padding:24px; box-shadow:0 10px 25px rgba(0,0,0,0.1); }
617
- .modal h3 { margin-bottom:16px; font-size:16px; }
618
- .modal input { width:100%; padding:10px; border:1px solid var(--border); border-radius:8px; margin-bottom:16px; font-size:14px; }
619
- .modal input:focus { outline:none; border-color:var(--primary); }
620
- .modal-btns { display:flex; justify-content:flex-end; gap:10px; }
621
- .mbtn { padding:8px 16px; border-radius:6px; font-size:13px; cursor:pointer; border:none; }
622
- .mbtn-cancel { background:#f3f4f6; color:var(--t1); }
623
- .mbtn-ok { background:var(--primary); color:#fff; }
624
- .mbtn-ok:disabled { background:#ccc; }
625
-
626
- .bubble p { margin-bottom:0.4em; } .bubble p:last-child { margin-bottom:0; }
627
- .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; }
628
- .bubble h1 { font-size:1.5em; border-bottom:1px solid rgba(0,0,0,0.1); padding-bottom:0.3em; }
629
- .bubble h2 { font-size:1.3em; border-bottom:1px solid rgba(0,0,0,0.05); padding-bottom:0.3em; }
630
- .bubble h3 { font-size:1.1em; }
631
- .bubble ul, .bubble ol { padding-left:1.5em; margin-bottom:0.5em; }
632
- .bubble li { margin-bottom:0.2em; }
633
- .bubble blockquote { margin:0.5em 0; padding-left:1em; border-left:4px solid rgba(0,0,0,0.1); color:var(--t2); }
634
- .bubble a { color:var(--primary); text-decoration:none; } .bubble a:hover { text-decoration:underline; }
635
- .bubble img { max-width:100%; border-radius:4px; }
636
- .bubble code { background:rgba(0,0,0,0.1); padding:2px 4px; border-radius:3px; font-family:monospace; font-size:0.9em; }
637
- .bubble pre { background:#2d2d2d; color:#fff; padding:12px; border-radius:6px; overflow-x:auto; margin:8px 0; }
638
- .bubble pre code { background:transparent; padding:0; color:inherit; border-radius:0; }
639
- .messages { flex:1; padding:16px; overflow-y:auto; display:flex; flex-direction:column; gap:12px; }
640
- .message { display:flex; flex-direction:row; max-width:85%; gap:8px; }
641
- .message.sent { align-self:flex-end; flex-direction:row-reverse; }
642
- .message.received { align-self:flex-start; }
643
- .msg-avatar { width:40px; height:40px; border-radius:50%; object-fit:cover; flex-shrink:0; box-shadow:0 1px 2px rgba(0,0,0,0.1); margin-top:4px; }
644
- .msg-content { display:flex; flex-direction:column; max-width:100%; min-width:0; }
645
- .message.sent .msg-content { align-items:flex-end; }
646
- .message.received .msg-content { align-items:flex-start; }
647
- @media (min-width: 1024px) { .message { max-width: 70%; } }
648
-
649
- @media (max-width:768px) {
650
- .sidebar { position:absolute; height:100%; z-index:20; width:280px; }
651
- .sidebar.collapsed { width:0; }
652
- }
653
- </style>
654
- <!-- CHATHTML_STYLE_END -->
655
- </head>
656
- <body>
657
- <div id="app">
658
- <div class="sidebar" id="sidebar">
659
- <div class="sidebar-header">
660
- <div class="header-top">
661
- <span class="my-aid" id="myAid">Loading...</span>
662
- <button class="collapse-btn" onclick="toggleSidebar()" title="收起面板">
663
- <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>
664
- </button>
665
- </div>
666
- <button class="new-chat-btn" onclick="showModal()">+ 连接龙虾</button>
667
- </div>
668
- <div class="session-list" id="sessionList"></div>
669
- </div>
670
- <div class="chat-area">
671
- <div class="chat-header">
672
- <div class="header-left">
673
- <button class="toggle-sidebar-btn" onclick="toggleSidebar()">
674
- <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>
675
- </button>
676
- <div class="status-dot" id="statusDot"></div>
677
- <div class="chat-title" id="chatTitle">未选择会话</div>
678
- </div>
679
- <div class="aid-select-wrap">
680
- <a href="/" class="manage-btn" title="ACP 身份管理">
681
- <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> 身份管理
682
- </a>
683
- <div class="aid-control-group">
684
- <select class="aid-select" id="aidSelect" onchange="switchAid(this.value)"></select>
685
- <div class="status-toggle" id="aidStatusToggle" onclick="toggleOnline()" title="点击切换在线状态">
686
- <div class="status-indicator" id="aidOnlineDot"></div>
687
- <span id="aidStatusText" style="color:var(--t2);">...</span>
688
- </div>
689
- </div>
690
- </div>
691
- </div>
692
- <div class="encrypt-banner">
693
- <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>
694
- <span>ACP Agent 点对点加密通信 — 消息经端到端加密传输,仅通信双方可读</span>
695
- </div>
696
- <div class="messages" id="messages">
697
- <div style="text-align:center;color:var(--t2);margin-top:40px;">
698
- <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>
699
- <div style="font-size:14px;font-weight:500;color:#64748b;margin-bottom:4px;">ACP Agent 安全通信</div>
700
- <div style="font-size:12px;color:#94a3b8;">选择或创建一个会话,开始点对点加密聊天</div>
701
- </div>
702
- </div>
703
- <div class="input-area">
704
- <input type="text" id="messageInput" placeholder="输入消息..." onkeypress="if(event.key==='Enter')sendMessage()">
705
- <button class="send-btn" id="sendBtn" onclick="sendMessage()">
706
- <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>
707
- </button>
708
- </div>
709
- </div>
710
- </div>
711
- <div class="modal-overlay" id="modal">
712
- <div class="modal">
713
- <h3>连接 ACP 龙虾</h3>
714
- <input type="text" id="targetAidInput" placeholder="输入对方龙虾 AID(自动添加 .aid.pub 后缀)" onkeypress="if(event.key==='Enter')doConnect()">
715
- <div class="modal-btns">
716
- <button class="mbtn mbtn-cancel" onclick="hideModal()">取消</button>
717
- <button class="mbtn mbtn-ok" id="connectBtn" onclick="doConnect()">连接</button>
718
- </div>
719
- </div>
720
- </div>
721
- <script>
722
- var S = { aid:'', sid:null, sessions:[], status:'disconnected', expanded:{}, sidebarOpen:true, aidList:[], closed:false };
723
- var D = {};
724
- var agentInfoCache = {};
725
- function $(id){ return document.getElementById(id); }
726
- function getAvatarSrc(type) {
727
- if (type === 'openclaw') return '/assets/openclaw.png';
728
- if (type === 'human') return '/assets/human.png';
729
- return '/assets/agent.png';
730
- }
731
- async function fetchAgentInfo(aid) {
732
- if (agentInfoCache[aid]) return agentInfoCache[aid];
733
- try {
734
- var r = await fetch('/api/agent-info?aid=' + encodeURIComponent(aid));
735
- var d = await r.json();
736
- if (d.type || d.name) { agentInfoCache[aid] = d; }
737
- return d;
738
- } catch(e) { return { type:'', name:'', description:'' }; }
739
- }
740
- async function deleteSession(e, sessionId){
741
- e.stopPropagation();
742
- if(!confirm('确认删除该会话?')) return;
743
- try {
744
- var r = await fetch('/api/sessions/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionId: sessionId }) });
745
- var d = await r.json();
746
- if(d.success){
747
- if(S.sid === sessionId){ S.sid = null; D.title.textContent='未选择会话'; D.msgs.innerHTML=''; D.input.disabled=false; }
748
- D.sList.dataset.s=''; // force update
749
- poll();
750
- } else { alert(d.error || '删除失败'); }
751
- } catch(err){ alert('删除失败: ' + err.message); }
752
- }
753
-
754
- async function deletePeer(e, peerAid){
755
- e.stopPropagation();
756
- if(!confirm('确认删除与 ' + peerAid + ' 的所有会话?')) return;
757
- try {
758
- var r = await fetch('/api/peers/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ peerAid: peerAid }) });
759
- var d = await r.json();
760
- if(d.success){
761
- S.sid = null; D.title.textContent='未选择会话'; D.msgs.innerHTML='';
762
- D.sList.dataset.s=''; // force update
763
- poll();
764
- } else { alert(d.error || '删除失败'); }
765
- } catch(err){ alert('删除失败: ' + err.message); }
766
- }
767
-
768
- 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'); }
769
-
770
- async function init(){
771
- initDom();
772
- try {
773
- var r = await fetch('/api/aid'); var d = await r.json();
774
- if(d.currentAid){
775
- S.aid=d.currentAid; D.myAid.textContent='我的身份: '+d.currentAid; D.myAid.title=d.currentAid;
776
- // 填充 AID 切换下拉
777
- S.aidList=d.aidStatus||[];
778
- renderAidSelect();
779
- poll(); setInterval(poll,1000);
780
- } else { window.location.href='/'; }
781
- } catch(e){ console.error(e); }
782
- }
783
-
784
- function renderAidSelect(){
785
- var html='';
786
- var curOnline=false;
787
- S.aidList.forEach(function(a){
788
- var sel=a.aid===S.aid?' selected':'';
789
- if(a.aid===S.aid) curOnline=a.online;
790
- html+='<option value="'+escH(a.aid)+'"'+sel+'>'+escH(a.aid)+'</option>';
791
- });
792
- D.aidSel.innerHTML=html;
793
- D.aidDot.className='status-indicator '+(curOnline?'online':'offline');
794
- D.aidStatusText.textContent=curOnline?'已上线':'离线';
795
- D.aidStatusText.style.color=curOnline?'#10b981':'#64748b';
796
- D.aidStatusToggle.title=curOnline?'点击下线':'点击上线';
797
- }
798
-
799
- async function switchAid(aid){
800
- if(aid===S.aid) return;
801
- try {
802
- // 先切换
803
- var r=await fetch('/api/aid/select',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({aid:aid})});
804
- var d=await r.json();
805
- if(!d.success) return;
806
- S.aid=aid; D.myAid.textContent='我的身份: '+aid; D.myAid.title=aid;
807
- S.sid=null; S.closed=false; D.title.textContent='未选择会话';
808
- D.sList.dataset.s=''; D.msgs.dataset.s='';
809
- D.input.disabled=false; D.input.placeholder='输入消息...';
810
- // 检查是否在线,不在线则自动上线
811
- var r2=await fetch('/api/aid'); var d2=await r2.json();
812
- S.aidList=d2.aidStatus||[];
813
- var info=S.aidList.find(function(a){ return a.aid===aid; });
814
- if(!info || !info.online){
815
- await fetch('/api/ws/start',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({aid:aid})});
816
- // 刷新状态
817
- var r3=await fetch('/api/aid'); var d3=await r3.json();
818
- S.aidList=d3.aidStatus||[];
819
- }
820
- renderAidSelect();
821
- } catch(e){}
822
- }
823
-
824
- async function toggleOnline(){
825
- var info=S.aidList.find(function(a){ return a.aid===S.aid; });
826
- var isOnline=info&&info.online;
827
- D.aidStatusText.textContent='...';
828
- try {
829
- if(isOnline){
830
- await fetch('/api/aid/offline',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({aid:S.aid})});
831
- } else {
832
- await fetch('/api/ws/start',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({aid:S.aid})});
833
- }
834
- var r=await fetch('/api/aid'); var d=await r.json();
835
- S.aidList=d.aidStatus||[];
836
- renderAidSelect();
837
- } catch(e){}
838
- }
839
-
840
- var _pollCount=0;
841
- async function poll(){
842
- try {
843
- var [sr,mr,wr] = await Promise.all([fetch('/api/sessions'),fetch('/api/messages'),fetch('/api/ws/status')]);
844
- var sd=await sr.json(), md=await mr.json(), wd=await wr.json();
845
- if(sd.sessions) updateSessions(sd.sessions, sd.activeSessionId);
846
- S.closed=md.closed||false;
847
- if(md.messages) renderMsgs(md.messages, S.closed);
848
- updateDot(wd.status);
849
- // 每5次轮询刷新一次AID在线状态
850
- if(++_pollCount%5===0){
851
- var ar=await fetch('/api/aid'); var ad=await ar.json();
852
- S.aidList=ad.aidStatus||[];
853
- renderAidSelect();
854
- }
855
- } catch(e){}
856
- }
857
-
858
- function updateSessions(sessions, activeId){
859
- var sig=JSON.stringify(sessions)+activeId+S.sid;
860
- if(D.sList.dataset.s===sig) return;
861
- D.sList.dataset.s=sig;
862
- if(activeId && S.sid!==activeId) S.sid=activeId;
863
- S.sessions=sessions;
864
-
865
- var groups={};
866
- sessions.forEach(function(s){
867
- var peer=s.peerAid||'unknown';
868
- if(!groups[peer]) groups[peer]=[];
869
- groups[peer].push(s);
870
- });
871
-
872
- if(!sessions.length){
873
- D.sList.innerHTML='<div style="padding:20px;text-align:center;color:#999;font-size:12px;">暂无会话</div>';
874
- return;
875
- }
876
-
877
- var html='';
878
- var peers=Object.keys(groups);
879
- peers.sort(function(a,b){
880
- var la=groups[a][0].lastMessageAt, lb=groups[b][0].lastMessageAt;
881
- return lb-la;
882
- });
883
- peers.forEach(function(peer){
884
- var isOpen = S.expanded[peer] !== false;
885
- var list=groups[peer];
886
- var shortPeer=peer.length>22?peer.substring(0,22)+'...':peer;
887
- var cached=agentInfoCache[peer];
888
- var avatarType=cached?cached.type:'';
889
- var avatarSrc=getAvatarSrc(avatarType);
890
- var displayName=(cached&&cached.name)?cached.name:shortPeer;
891
- var fullDisplayName=(cached&&cached.name)?cached.name:peer;
892
- var descText=(cached&&cached.description)?cached.description:peer;
893
- html+='<div class="aid-group">';
894
- 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>';
895
- html+='<div class="aid-group-sessions'+(isOpen?' open':'')+'">';
896
- list.forEach(function(s){
897
- var active=s.sessionId===S.sid;
898
- var time=new Date(s.lastMessageAt).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'});
899
- var tc=s.type==='outgoing'?'outgoing':'incoming';
900
- var tt=s.type==='outgoing'?'OUT':'IN';
901
- var name=s.lastMessage||'';
902
- var fullName=name;
903
- if(name.length>20) name=name.substring(0,20)+'...';
904
- if(!name) name='(空会话)';
905
- var closedTag=s.closed?'<span style="color:#dc3545;font-size:10px;margin-left:4px;">[已关闭]</span>':'';
906
- 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>';
907
- });
908
- html+='</div></div>';
909
- });
910
- D.sList.innerHTML=html;
911
-
912
- // 异步加载未缓存的 agent info 并更新头像和名称
913
- peers.forEach(function(peer){
914
- if(!agentInfoCache[peer]){
915
- fetchAgentInfo(peer).then(function(info){
916
- var safeId=peer.replace(/\\./g,'_');
917
- var el=document.getElementById('avatar_'+safeId);
918
- if(el) el.src=getAvatarSrc(info.type);
919
- if(info.name){
920
- var header=el&&el.parentElement;
921
- if(header){
922
- var titleEl=header.querySelector('.aid-group-title');
923
- if(titleEl){ titleEl.textContent=info.name; titleEl.title=info.name; }
924
- }
925
- }
926
- if(info.description){
927
- var descEl=document.getElementById('desc_'+safeId);
928
- if(descEl){ descEl.textContent=info.description; descEl.title=info.description; }
929
- }
930
- });
931
- }
932
- });
933
- }
934
-
935
- function toggleGroup(owner){
936
- S.expanded[owner] = S.expanded[owner]===false ? true : false;
937
- D.sList.dataset.s=''; // force re-render
938
- updateSessions(S.sessions, S.sid);
939
- }
940
-
941
- function renderMsgs(msgs, closed){
942
- var sig=msgs.length+(msgs.length>0?msgs[msgs.length-1].timestamp:0)+(closed?'c':'');
943
- // 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)
944
- // Actually, let's allow re-render if we call it.
945
- if(D.msgs.dataset.s==sig && !D.msgs.dataset.force) return;
946
- D.msgs.dataset.s=sig;
947
- D.msgs.dataset.force=''; // clear force flag
948
-
949
- if(!msgs.length){
950
- D.msgs.innerHTML='<div style="text-align:center;color:#ccc;margin-top:20px;font-size:12px;">暂无消息</div>';
951
- D.input.disabled=false; D.input.placeholder='输入消息...';
952
- return;
953
- }
954
- var html=msgs.map(function(m){
955
- var sent=m.type==='sent';
956
- var sender = sent ? S.aid : (m.from || 'unknown');
957
- var info = agentInfoCache[sender];
958
- if(!info){
959
- fetchAgentInfo(sender).then(function(){
960
- if(D.msgs.dataset.s===sig){ D.msgs.dataset.force='1'; renderMsgs(msgs, closed); }
961
- });
962
- }
963
- var avatarSrc = getAvatarSrc(info ? info.type : '');
964
- var t=new Date(m.timestamp).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'});
965
- var c=(typeof marked!=='undefined'&&marked.parse)?marked.parse(m.content):escH(m.content);
966
- var name = (info && info.name) ? info.name : sender;
967
-
968
- return '<div class="message '+m.type+'">' +
969
- '<img class="msg-avatar" src="'+avatarSrc+'" title="'+escH(name)+'">' +
970
- '<div class="msg-content">' +
971
- '<div class="bubble">'+c+'</div>' +
972
- '<div class="msg-meta">'+(sent?'我':escH(name))+' · '+t+'</div>' +
973
- '</div></div>';
974
- }).join('');
975
- if(closed){
976
- 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>';
977
- D.input.disabled=true; D.input.placeholder='会话已关闭,请新建会话';
978
- } else {
979
- D.input.disabled=false; D.input.placeholder='输入消息...';
980
- }
981
- D.msgs.innerHTML=html;
982
- D.msgs.scrollTop=D.msgs.scrollHeight;
983
- }
984
-
985
- function updateDot(st){
986
- S.status=st;
987
- D.dot.className='status-dot '+(st||'');
988
- }
989
-
990
- async function pickSession(sid,peer){
991
- S.sid=sid;
992
- D.title.textContent=peer;
993
- try {
994
- await fetch('/api/sessions/active',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sessionId:sid})});
995
- var r=await fetch('/api/messages'); var d=await r.json();
996
- S.closed=d.closed||false;
997
- D.msgs.dataset.s=''; // force
998
- renderMsgs(d.messages||[], S.closed);
999
- } catch(e){}
1000
- }
1001
-
1002
- async function sendMessage(){
1003
- var txt=D.input.value.trim();
1004
- if(!txt){ return; }
1005
- if(!S.sid){ alert('请先选择或新建一个会话'); return; }
1006
- if(S.closed){ alert('该会话已关闭,请新建会话继续通信'); return; }
1007
- try {
1008
- D.input.value='';
1009
- var r=await fetch('/api/ws/send',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:txt,sessionId:S.sid})});
1010
- var d=await r.json();
1011
- if(!d.success) alert(d.error||'发送失败');
1012
- } catch(e){ alert('发送失败'); }
1013
- }
1014
-
1015
- function toggleSidebar(){
1016
- S.sidebarOpen=!S.sidebarOpen;
1017
- D.sidebar.classList.toggle('collapsed',!S.sidebarOpen);
1018
- }
1019
-
1020
- function showModal(){ D.modal.classList.add('show'); D.tInput.value=''; D.tInput.focus(); }
1021
- function hideModal(){ D.modal.classList.remove('show'); }
1022
-
1023
- async function newSessionWith(peerAid){
1024
- try {
1025
- var r=await fetch('/api/ws/connect',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({targetAid:peerAid})});
1026
- var d=await r.json();
1027
- if(d.success){ pickSession(d.sessionId,peerAid); }
1028
- else { alert(d.error||'连接失败'); }
1029
- } catch(e){ alert('错误: '+e.message); }
1030
- }
1031
-
1032
- async function doConnect(){
1033
- var aid=D.tInput.value.trim();
1034
- if(!aid) return;
1035
- if(!aid.endsWith('.aid.pub')) aid+='.aid.pub';
1036
- D.cBtn.disabled=true; D.cBtn.textContent='连接中...';
1037
- try {
1038
- var r=await fetch('/api/ws/connect',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({targetAid:aid})});
1039
- var d=await r.json();
1040
- if(d.success){ hideModal(); pickSession(d.sessionId,aid); }
1041
- else { alert(d.error||'连接失败'); }
1042
- } catch(e){ alert('错误: '+e.message); }
1043
- finally { D.cBtn.disabled=false; D.cBtn.textContent='连接'; }
1044
- }
1045
-
1046
- function escH(t){ var d=document.createElement('div'); d.textContent=t; return d.innerHTML; }
1047
- function escA(t){ return t.replace(/\\\\/g,'\\\\\\\\').replace(/'/g,"\\\\'"); }
1048
-
1049
- init();
1050
- <\/script>
1051
- </body>
554
+ const chatHtml = `<!DOCTYPE html>
555
+ <html lang="zh-CN">
556
+ <head>
557
+ <meta charset="UTF-8">
558
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
559
+ <title>ACP 聊天</title>
560
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"><\/script>
561
+ <style>
562
+ :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; }
563
+ * { box-sizing:border-box; margin:0; padding:0; }
564
+ body { font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; background:var(--bg); height:100vh; overflow:hidden; color:var(--t1); }
565
+ #app { display:flex; height:100%; }
566
+
567
+ /* Sidebar */
568
+ .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; }
569
+ .sidebar.collapsed { width:0; border-right:none; }
570
+ .sidebar-header { padding:12px 14px; border-bottom:1px solid var(--border); display:flex; flex-direction:column; gap:12px; flex-shrink:0; }
571
+ .header-top { display:flex; justify-content:space-between; align-items:center; width:100%; }
572
+ .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; }
573
+ .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; }
574
+ .new-chat-btn:hover { background:var(--primary-h); }
575
+ .session-list { flex:1; overflow-y:auto; }
576
+
577
+ /* AID Group */
578
+ .aid-group { border-bottom:1px solid var(--border); }
579
+ .aid-group-header { padding:12px 14px; display:flex; align-items:center; cursor:pointer; background:linear-gradient(135deg,#f8fafc,#f1f5f9); user-select:none; border-left:3px solid var(--primary); }
580
+ .aid-group-header:hover { background:linear-gradient(135deg,#eef2f7,#e8edf4); }
581
+ .aid-group-info { flex:1; min-width:0; margin-left:4px; }
582
+ .aid-group-title { font-size:13px; font-weight:700; color:var(--t1); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; display:block; }
583
+ .aid-group-desc { font-size:10px; color:var(--t2); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; margin-top:2px; display:block; }
584
+ .aid-group-arrow { font-size:10px; color:var(--t2); transition:transform 0.2s; flex-shrink:0; }
585
+ .aid-group-arrow.open { transform:rotate(90deg); }
586
+ .aid-group-badge { font-size:10px; background:var(--primary); color:#fff; padding:1px 6px; border-radius:8px; margin-left:8px; flex-shrink:0; }
587
+ .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; }
588
+ .aid-group-add:hover { background:var(--primary); color:#fff; border-color:var(--primary); }
589
+ .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; }
590
+ .aid-group-header:hover .aid-group-del { display:block; }
591
+ .aid-group-del:hover { color:#dc3545; background:#ffebeb; }
592
+ .session-del { position:absolute; right:8px; top:12px; background:none; border:none; color:var(--t2); font-size:12px; cursor:pointer; display:none; padding:2px; }
593
+ .session-item:hover .session-del { display:block; }
594
+ .session-del:hover { color:#dc3545; }
595
+ .aid-group-sessions { display:none; }
596
+ .aid-group-sessions.open { display:block; }
597
+
598
+ .aid-group-avatar { width:34px; height:34px; border-radius:50%; object-fit:cover; flex-shrink:0; margin-right:8px; box-shadow:0 1px 3px rgba(0,0,0,0.12); }
599
+
600
+ .session-item { padding:10px 14px 10px 32px; border-bottom:1px solid #f3f4f6; cursor:pointer; transition:background 0.15s; position:relative; }
601
+ .session-item::before { content:''; position:absolute; left:18px; top:16px; width:6px; height:6px; border-radius:50%; background:var(--border); }
602
+ .session-item:hover { background:#f5f7fa; }
603
+ .session-item.active { background:#eff6ff; border-left:3px solid var(--primary); padding-left:29px; }
604
+ .session-item.active::before { background:var(--primary); }
605
+ .session-peer { font-weight:400; font-size:12px; color:var(--t1); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; padding-left:10px; }
606
+ .session-meta { font-size:10px; color:var(--t2); margin-top:2px; display:flex; align-items:center; gap:6px; padding-left:10px; }
607
+ .tag { font-size:9px; padding:1px 5px; border-radius:3px; color:#fff; }
608
+ .tag.outgoing { background:var(--ok); }
609
+ .tag.incoming { background:var(--t2); }
610
+
611
+ /* Chat Area */
612
+ .chat-area { flex:1; display:flex; flex-direction:column; background:var(--chat-bg); min-width:0; }
613
+ .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; }
614
+ .header-left { display:flex; align-items:center; gap:10px; overflow:hidden; }
615
+ .toggle-sidebar-btn { background:none; border:none; cursor:pointer; color:var(--t2); padding:4px; display:flex; }
616
+ .toggle-sidebar-btn:hover { color:var(--t1); }
617
+ .status-dot { width:8px; height:8px; border-radius:50%; background:#ccc; flex-shrink:0; }
618
+ .status-dot.connected { background:var(--ok); }
619
+ .status-dot.connecting { background:#fbbf24; }
620
+ .chat-title { font-size:15px; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
621
+
622
+ .aid-select-wrap { display:flex; align-items:center; gap:10px; flex-shrink:0; }
623
+ .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); }
624
+ .manage-btn:hover { background:#f8fafc; color:var(--primary); border-color:var(--primary); }
625
+ .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); }
626
+ .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; }
627
+ .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); }
628
+ .status-toggle:hover { background:#f1f5f9; }
629
+ .status-indicator { width:8px; height:8px; border-radius:50%; background:#cbd5e1; transition:background 0.3s; }
630
+ .status-indicator.online { background:var(--ok); box-shadow:0 0 0 2px rgba(16,185,129,0.2); }
631
+ .status-indicator.offline { background:#cbd5e1; }
632
+
633
+ .collapse-btn { background:none; border:none; cursor:pointer; color:var(--t2); padding:6px; display:flex; align-items:center; flex-shrink:0; }
634
+ .collapse-btn:hover { color:var(--t1); }
635
+
636
+ .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; }
637
+ .encrypt-banner svg { flex-shrink:0; }
638
+
639
+ .messages { flex:1; padding:16px; overflow-y:auto; display:flex; flex-direction:column; gap:12px; }
640
+ .message { display:flex; flex-direction:column; max-width:80%; }
641
+ .message.sent { align-self:flex-end; align-items:flex-end; }
642
+ .message.received { align-self:flex-start; align-items:flex-start; }
643
+ .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); }
644
+ .message.sent .bubble { background:var(--sent); color:#fff; border-bottom-right-radius:2px; }
645
+ .message.received .bubble { background:var(--recv-bg); color:var(--t1); border-bottom-left-radius:2px; border:1px solid var(--border); }
646
+ .msg-meta { font-size:10px; color:var(--t2); margin-top:3px; padding:0 4px; }
647
+
648
+ .input-area { padding:12px 16px; background:#fff; border-top:1px solid var(--border); display:flex; align-items:center; gap:10px; flex-shrink:0; }
649
+ .input-area input { flex:1; padding:10px 14px; border-radius:20px; border:1px solid var(--border); font-size:14px; background:#f9fafb; }
650
+ .input-area input:focus { outline:none; border-color:var(--primary); background:#fff; }
651
+ .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; }
652
+ .send-btn:hover { background:var(--primary-h); }
653
+ .send-btn:disabled { background:#ccc; cursor:not-allowed; }
654
+
655
+ .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; }
656
+ .modal-overlay.show { display:flex; }
657
+ .modal { background:#fff; width:90%; max-width:400px; border-radius:12px; padding:24px; box-shadow:0 10px 25px rgba(0,0,0,0.1); }
658
+ .modal h3 { margin-bottom:16px; font-size:16px; }
659
+ .modal input { width:100%; padding:10px; border:1px solid var(--border); border-radius:8px; margin-bottom:16px; font-size:14px; }
660
+ .modal input:focus { outline:none; border-color:var(--primary); }
661
+ .modal-btns { display:flex; justify-content:flex-end; gap:10px; }
662
+ .mbtn { padding:8px 16px; border-radius:6px; font-size:13px; cursor:pointer; border:none; }
663
+ .mbtn-cancel { background:#f3f4f6; color:var(--t1); }
664
+ .mbtn-ok { background:var(--primary); color:#fff; }
665
+ .mbtn-ok:disabled { background:#ccc; }
666
+
667
+ .bubble p { margin-bottom:0.4em; } .bubble p:last-child { margin-bottom:0; }
668
+ .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; }
669
+ .bubble h1 { font-size:1.5em; border-bottom:1px solid rgba(0,0,0,0.1); padding-bottom:0.3em; }
670
+ .bubble h2 { font-size:1.3em; border-bottom:1px solid rgba(0,0,0,0.05); padding-bottom:0.3em; }
671
+ .bubble h3 { font-size:1.1em; }
672
+ .bubble ul, .bubble ol { padding-left:1.5em; margin-bottom:0.5em; }
673
+ .bubble li { margin-bottom:0.2em; }
674
+ .bubble blockquote { margin:0.5em 0; padding-left:1em; border-left:4px solid rgba(0,0,0,0.1); color:var(--t2); }
675
+ .bubble a { color:var(--primary); text-decoration:none; } .bubble a:hover { text-decoration:underline; }
676
+ .bubble img { max-width:100%; border-radius:4px; }
677
+ .bubble code { background:rgba(0,0,0,0.1); padding:2px 4px; border-radius:3px; font-family:monospace; font-size:0.9em; }
678
+ .bubble pre { background:#2d2d2d; color:#fff; padding:12px; border-radius:6px; overflow-x:auto; margin:8px 0; }
679
+ .bubble pre code { background:transparent; padding:0; color:inherit; border-radius:0; }
680
+ .messages { flex:1; padding:16px; overflow-y:auto; display:flex; flex-direction:column; gap:12px; }
681
+ .message { display:flex; flex-direction:row; max-width:85%; gap:8px; }
682
+ .message.sent { align-self:flex-end; flex-direction:row-reverse; }
683
+ .message.received { align-self:flex-start; }
684
+ .msg-avatar { width:40px; height:40px; border-radius:50%; object-fit:cover; flex-shrink:0; box-shadow:0 1px 2px rgba(0,0,0,0.1); margin-top:4px; }
685
+ .msg-content { display:flex; flex-direction:column; max-width:100%; min-width:0; }
686
+ .message.sent .msg-content { align-items:flex-end; }
687
+ .message.received .msg-content { align-items:flex-start; }
688
+ @media (min-width: 1024px) { .message { max-width: 70%; } }
689
+
690
+ @media (max-width:768px) {
691
+ .sidebar { position:absolute; height:100%; z-index:20; width:280px; }
692
+ .sidebar.collapsed { width:0; }
693
+ }
694
+ </style>
695
+ <!-- CHATHTML_STYLE_END -->
696
+ </head>
697
+ <body>
698
+ <div id="app">
699
+ <div class="sidebar" id="sidebar">
700
+ <div class="sidebar-header">
701
+ <div class="header-top">
702
+ <span class="my-aid" id="myAid">Loading...</span>
703
+ <button class="collapse-btn" onclick="toggleSidebar()" title="收起面板">
704
+ <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>
705
+ </button>
706
+ </div>
707
+ <button class="new-chat-btn" onclick="showModal()">+ 连接龙虾</button>
708
+ </div>
709
+ <div class="session-list" id="sessionList"></div>
710
+ </div>
711
+ <div class="chat-area">
712
+ <div class="chat-header">
713
+ <div class="header-left">
714
+ <button class="toggle-sidebar-btn" onclick="toggleSidebar()">
715
+ <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>
716
+ </button>
717
+ <div class="status-dot" id="statusDot"></div>
718
+ <div class="chat-title" id="chatTitle">未选择会话</div>
719
+ </div>
720
+ <div class="aid-select-wrap">
721
+ <a href="/" class="manage-btn" title="ACP 身份管理">
722
+ <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> 身份管理
723
+ </a>
724
+ <div class="aid-control-group">
725
+ <select class="aid-select" id="aidSelect" onchange="switchAid(this.value)"></select>
726
+ <div class="status-toggle" id="aidStatusToggle" onclick="toggleOnline()" title="点击切换在线状态">
727
+ <div class="status-indicator" id="aidOnlineDot"></div>
728
+ <span id="aidStatusText" style="color:var(--t2);">...</span>
729
+ </div>
730
+ </div>
731
+ </div>
732
+ </div>
733
+ <div class="encrypt-banner">
734
+ <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>
735
+ <span>ACP Agent 点对点加密通信 — 消息经端到端加密传输,仅通信双方可读</span>
736
+ </div>
737
+ <div class="messages" id="messages">
738
+ <div style="text-align:center;color:var(--t2);margin-top:40px;">
739
+ <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>
740
+ <div style="font-size:14px;font-weight:500;color:#64748b;margin-bottom:4px;">ACP Agent 安全通信</div>
741
+ <div style="font-size:12px;color:#94a3b8;">选择或创建一个会话,开始点对点加密聊天</div>
742
+ </div>
743
+ </div>
744
+ <div class="input-area">
745
+ <input type="text" id="messageInput" placeholder="输入消息..." onkeypress="if(event.key==='Enter')sendMessage()">
746
+ <button class="send-btn" id="sendBtn" onclick="sendMessage()">
747
+ <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>
748
+ </button>
749
+ </div>
750
+ </div>
751
+ </div>
752
+ <div class="modal-overlay" id="modal">
753
+ <div class="modal">
754
+ <h3>连接 ACP 龙虾</h3>
755
+ <input type="text" id="targetAidInput" placeholder="输入对方 AID" onkeypress="if(event.key==='Enter')doConnect()">
756
+ <div class="modal-btns">
757
+ <button class="mbtn mbtn-cancel" onclick="hideModal()">取消</button>
758
+ <button class="mbtn mbtn-ok" id="connectBtn" onclick="doConnect()">连接</button>
759
+ </div>
760
+ </div>
761
+ </div>
762
+ <script>
763
+ var S = { aid:'', sid:null, sessions:[], status:'disconnected', expanded:{}, sidebarOpen:true, aidList:[], closed:false };
764
+ var D = {};
765
+ var agentInfoCache = {};
766
+ function $(id){ return document.getElementById(id); }
767
+ function getAvatarSrc(type) {
768
+ if (type === 'openclaw') return '/assets/openclaw.png';
769
+ if (type === 'human') return '/assets/human.png';
770
+ return '/assets/agent.png';
771
+ }
772
+ async function fetchAgentInfo(aid) {
773
+ if (agentInfoCache[aid]) return agentInfoCache[aid];
774
+ try {
775
+ var r = await fetch('/api/agent-info?aid=' + encodeURIComponent(aid));
776
+ var d = await r.json();
777
+ if (d.type || d.name) { agentInfoCache[aid] = d; }
778
+ return d;
779
+ } catch(e) { return { type:'', name:'', description:'' }; }
780
+ }
781
+ async function deleteSession(e, sessionId){
782
+ e.stopPropagation();
783
+ if(!confirm('确认删除该会话?')) return;
784
+ try {
785
+ var r = await fetch('/api/sessions/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionId: sessionId }) });
786
+ var d = await r.json();
787
+ if(d.success){
788
+ if(S.sid === sessionId){ S.sid = null; D.title.textContent='未选择会话'; D.msgs.innerHTML=''; D.input.disabled=false; }
789
+ D.sList.dataset.s=''; // force update
790
+ poll();
791
+ } else { alert(d.error || '删除失败'); }
792
+ } catch(err){ alert('删除失败: ' + err.message); }
793
+ }
794
+
795
+ async function deletePeer(e, peerAid){
796
+ e.stopPropagation();
797
+ if(!confirm('确认删除与 ' + peerAid + ' 的所有会话?')) return;
798
+ try {
799
+ var r = await fetch('/api/peers/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ peerAid: peerAid }) });
800
+ var d = await r.json();
801
+ if(d.success){
802
+ S.sid = null; D.title.textContent='未选择会话'; D.msgs.innerHTML='';
803
+ D.sList.dataset.s=''; // force update
804
+ poll();
805
+ } else { alert(d.error || '删除失败'); }
806
+ } catch(err){ alert('删除失败: ' + err.message); }
807
+ }
808
+
809
+ 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'); }
810
+
811
+ async function init(){
812
+ initDom();
813
+ try {
814
+ var r = await fetch('/api/aid'); var d = await r.json();
815
+ if(d.currentAid){
816
+ S.aid=d.currentAid; D.myAid.textContent='我的身份: '+d.currentAid; D.myAid.title=d.currentAid;
817
+ // 填充 AID 切换下拉
818
+ S.aidList=d.aidStatus||[];
819
+ renderAidSelect();
820
+ poll(); setInterval(poll,1000);
821
+ } else { window.location.href='/'; }
822
+ } catch(e){ console.error(e); }
823
+ }
824
+
825
+ function renderAidSelect(){
826
+ var html='';
827
+ var curOnline=false;
828
+ S.aidList.forEach(function(a){
829
+ var sel=a.aid===S.aid?' selected':'';
830
+ if(a.aid===S.aid) curOnline=a.online;
831
+ html+='<option value="'+escH(a.aid)+'"'+sel+'>'+escH(a.aid)+'</option>';
832
+ });
833
+ D.aidSel.innerHTML=html;
834
+ D.aidDot.className='status-indicator '+(curOnline?'online':'offline');
835
+ D.aidStatusText.textContent=curOnline?'已上线':'离线';
836
+ D.aidStatusText.style.color=curOnline?'#10b981':'#64748b';
837
+ D.aidStatusToggle.title=curOnline?'点击下线':'点击上线';
838
+ }
839
+
840
+ async function switchAid(aid){
841
+ if(aid===S.aid) return;
842
+ try {
843
+ // 先切换
844
+ var r=await fetch('/api/aid/select',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({aid:aid})});
845
+ var d=await r.json();
846
+ if(!d.success) return;
847
+ S.aid=aid; D.myAid.textContent='我的身份: '+aid; D.myAid.title=aid;
848
+ S.sid=null; S.closed=false; D.title.textContent='未选择会话';
849
+ D.sList.dataset.s=''; D.msgs.dataset.s='';
850
+ D.input.disabled=false; D.input.placeholder='输入消息...';
851
+ // 检查是否在线,不在线则自动上线
852
+ var r2=await fetch('/api/aid'); var d2=await r2.json();
853
+ S.aidList=d2.aidStatus||[];
854
+ var info=S.aidList.find(function(a){ return a.aid===aid; });
855
+ if(!info || !info.online){
856
+ await fetch('/api/ws/start',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({aid:aid})});
857
+ // 刷新状态
858
+ var r3=await fetch('/api/aid'); var d3=await r3.json();
859
+ S.aidList=d3.aidStatus||[];
860
+ }
861
+ renderAidSelect();
862
+ } catch(e){}
863
+ }
864
+
865
+ async function toggleOnline(){
866
+ var info=S.aidList.find(function(a){ return a.aid===S.aid; });
867
+ var isOnline=info&&info.online;
868
+ D.aidStatusText.textContent='...';
869
+ try {
870
+ if(isOnline){
871
+ await fetch('/api/aid/offline',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({aid:S.aid})});
872
+ } else {
873
+ await fetch('/api/ws/start',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({aid:S.aid})});
874
+ }
875
+ var r=await fetch('/api/aid'); var d=await r.json();
876
+ S.aidList=d.aidStatus||[];
877
+ renderAidSelect();
878
+ } catch(e){}
879
+ }
880
+
881
+ var _pollCount=0;
882
+ async function poll(){
883
+ try {
884
+ var [sr,mr,wr] = await Promise.all([fetch('/api/sessions'),fetch('/api/messages'),fetch('/api/ws/status')]);
885
+ var sd=await sr.json(), md=await mr.json(), wd=await wr.json();
886
+ if(sd.sessions) updateSessions(sd.sessions, sd.activeSessionId);
887
+ S.closed=md.closed||false;
888
+ if(md.messages) renderMsgs(md.messages, S.closed);
889
+ updateDot(wd.status);
890
+ // 每5次轮询刷新一次AID在线状态
891
+ if(++_pollCount%5===0){
892
+ var ar=await fetch('/api/aid'); var ad=await ar.json();
893
+ S.aidList=ad.aidStatus||[];
894
+ renderAidSelect();
895
+ }
896
+ } catch(e){}
897
+ }
898
+
899
+ function updateSessions(sessions, activeId){
900
+ var sig=JSON.stringify(sessions)+activeId+S.sid;
901
+ if(D.sList.dataset.s===sig) return;
902
+ D.sList.dataset.s=sig;
903
+ if(activeId && S.sid!==activeId) S.sid=activeId;
904
+ S.sessions=sessions;
905
+
906
+ var groups={};
907
+ sessions.forEach(function(s){
908
+ var peer=s.peerAid||'unknown';
909
+ if(!groups[peer]) groups[peer]=[];
910
+ groups[peer].push(s);
911
+ });
912
+
913
+ if(!sessions.length){
914
+ D.sList.innerHTML='<div style="padding:20px;text-align:center;color:#999;font-size:12px;">暂无会话</div>';
915
+ return;
916
+ }
917
+
918
+ var html='';
919
+ var peers=Object.keys(groups);
920
+ peers.sort(function(a,b){
921
+ var la=groups[a][0].lastMessageAt, lb=groups[b][0].lastMessageAt;
922
+ return lb-la;
923
+ });
924
+ peers.forEach(function(peer){
925
+ var isOpen = S.expanded[peer] !== false;
926
+ var list=groups[peer];
927
+ var shortPeer=peer.length>22?peer.substring(0,22)+'...':peer;
928
+ var cached=agentInfoCache[peer];
929
+ var avatarType=cached?cached.type:'';
930
+ var avatarSrc=getAvatarSrc(avatarType);
931
+ var displayName=(cached&&cached.name)?cached.name:shortPeer;
932
+ var fullDisplayName=(cached&&cached.name)?cached.name:peer;
933
+ var descText=(cached&&cached.description)?cached.description:peer;
934
+ html+='<div class="aid-group">';
935
+ 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>';
936
+ html+='<div class="aid-group-sessions'+(isOpen?' open':'')+'">';
937
+ list.forEach(function(s){
938
+ var active=s.sessionId===S.sid;
939
+ var time=new Date(s.lastMessageAt).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'});
940
+ var tc=s.type==='outgoing'?'outgoing':'incoming';
941
+ var tt=s.type==='outgoing'?'OUT':'IN';
942
+ var name=s.lastMessage||'';
943
+ var fullName=name;
944
+ if(name.length>20) name=name.substring(0,20)+'...';
945
+ if(!name) name='(空会话)';
946
+ var closedTag=s.closed?'<span style="color:#dc3545;font-size:10px;margin-left:4px;">[已关闭]</span>':'';
947
+ 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>';
948
+ });
949
+ html+='</div></div>';
950
+ });
951
+ D.sList.innerHTML=html;
952
+
953
+ // 异步加载未缓存的 agent info 并更新头像和名称
954
+ peers.forEach(function(peer){
955
+ if(!agentInfoCache[peer]){
956
+ fetchAgentInfo(peer).then(function(info){
957
+ var safeId=peer.replace(/\\./g,'_');
958
+ var el=document.getElementById('avatar_'+safeId);
959
+ if(el) el.src=getAvatarSrc(info.type);
960
+ if(info.name){
961
+ var header=el&&el.parentElement;
962
+ if(header){
963
+ var titleEl=header.querySelector('.aid-group-title');
964
+ if(titleEl){ titleEl.textContent=info.name; titleEl.title=info.name; }
965
+ }
966
+ }
967
+ if(info.description){
968
+ var descEl=document.getElementById('desc_'+safeId);
969
+ if(descEl){ descEl.textContent=info.description; descEl.title=info.description; }
970
+ }
971
+ });
972
+ }
973
+ });
974
+ }
975
+
976
+ function toggleGroup(owner){
977
+ S.expanded[owner] = S.expanded[owner]===false ? true : false;
978
+ D.sList.dataset.s=''; // force re-render
979
+ updateSessions(S.sessions, S.sid);
980
+ }
981
+
982
+ function renderMsgs(msgs, closed){
983
+ var sig=msgs.length+(msgs.length>0?msgs[msgs.length-1].timestamp:0)+(closed?'c':'');
984
+ // 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)
985
+ // Actually, let's allow re-render if we call it.
986
+ if(D.msgs.dataset.s==sig && !D.msgs.dataset.force) return;
987
+ D.msgs.dataset.s=sig;
988
+ D.msgs.dataset.force=''; // clear force flag
989
+
990
+ if(!msgs.length){
991
+ D.msgs.innerHTML='<div style="text-align:center;color:#ccc;margin-top:20px;font-size:12px;">暂无消息</div>';
992
+ D.input.disabled=false; D.input.placeholder='输入消息...';
993
+ return;
994
+ }
995
+ var html=msgs.map(function(m){
996
+ var sent=m.type==='sent';
997
+ var sender = sent ? S.aid : (m.from || 'unknown');
998
+ var info = agentInfoCache[sender];
999
+ if(!info){
1000
+ fetchAgentInfo(sender).then(function(){
1001
+ if(D.msgs.dataset.s===sig){ D.msgs.dataset.force='1'; renderMsgs(msgs, closed); }
1002
+ });
1003
+ }
1004
+ var avatarSrc = getAvatarSrc(info ? info.type : '');
1005
+ var t=new Date(m.timestamp).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'});
1006
+ var c=(typeof marked!=='undefined'&&marked.parse)?marked.parse(m.content):escH(m.content);
1007
+ var name = (info && info.name) ? info.name : sender;
1008
+
1009
+ return '<div class="message '+m.type+'">' +
1010
+ '<img class="msg-avatar" src="'+avatarSrc+'" title="'+escH(name)+'">' +
1011
+ '<div class="msg-content">' +
1012
+ '<div class="bubble">'+c+'</div>' +
1013
+ '<div class="msg-meta">'+(sent?'我':escH(name))+' · '+t+'</div>' +
1014
+ '</div></div>';
1015
+ }).join('');
1016
+ if(closed){
1017
+ 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>';
1018
+ D.input.disabled=true; D.input.placeholder='会话已关闭,请新建会话';
1019
+ } else {
1020
+ D.input.disabled=false; D.input.placeholder='输入消息...';
1021
+ }
1022
+ D.msgs.innerHTML=html;
1023
+ D.msgs.scrollTop=D.msgs.scrollHeight;
1024
+ }
1025
+
1026
+ function updateDot(st){
1027
+ S.status=st;
1028
+ D.dot.className='status-dot '+(st||'');
1029
+ }
1030
+
1031
+ async function pickSession(sid,peer){
1032
+ S.sid=sid;
1033
+ D.title.textContent=peer;
1034
+ try {
1035
+ await fetch('/api/sessions/active',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sessionId:sid})});
1036
+ var r=await fetch('/api/messages'); var d=await r.json();
1037
+ S.closed=d.closed||false;
1038
+ D.msgs.dataset.s=''; // force
1039
+ renderMsgs(d.messages||[], S.closed);
1040
+ } catch(e){}
1041
+ }
1042
+
1043
+ async function sendMessage(){
1044
+ var txt=D.input.value.trim();
1045
+ if(!txt){ return; }
1046
+ if(!S.sid){ alert('请先选择或新建一个会话'); return; }
1047
+ if(S.closed){ alert('该会话已关闭,请新建会话继续通信'); return; }
1048
+ try {
1049
+ D.input.value='';
1050
+ var r=await fetch('/api/ws/send',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:txt,sessionId:S.sid})});
1051
+ var d=await r.json();
1052
+ if(!d.success) alert(d.error||'发送失败');
1053
+ } catch(e){ alert('发送失败'); }
1054
+ }
1055
+
1056
+ function toggleSidebar(){
1057
+ S.sidebarOpen=!S.sidebarOpen;
1058
+ D.sidebar.classList.toggle('collapsed',!S.sidebarOpen);
1059
+ }
1060
+
1061
+ function showModal(){ D.modal.classList.add('show'); D.tInput.value=''; D.tInput.focus(); }
1062
+ function hideModal(){ D.modal.classList.remove('show'); }
1063
+
1064
+ async function newSessionWith(peerAid){
1065
+ try {
1066
+ var r=await fetch('/api/ws/connect',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({targetAid:peerAid})});
1067
+ var d=await r.json();
1068
+ if(d.success){ pickSession(d.sessionId,peerAid); }
1069
+ else { alert(d.error||'连接失败'); }
1070
+ } catch(e){ alert('错误: '+e.message); }
1071
+ }
1072
+
1073
+ async function doConnect(){
1074
+ var aid=D.tInput.value.trim();
1075
+ if(!aid) return;
1076
+ D.cBtn.disabled=true; D.cBtn.textContent='连接中...';
1077
+ try {
1078
+ var r=await fetch('/api/ws/connect',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({targetAid:aid})});
1079
+ var d=await r.json();
1080
+ if(d.success){ hideModal(); pickSession(d.sessionId,aid); }
1081
+ else { alert(d.error||'连接失败'); }
1082
+ } catch(e){ alert('错误: '+e.message); }
1083
+ finally { D.cBtn.disabled=false; D.cBtn.textContent='连接'; }
1084
+ }
1085
+
1086
+ function escH(t){ var d=document.createElement('div'); d.textContent=t; return d.innerHTML; }
1087
+ function escA(t){ return t.replace(/\\\\/g,'\\\\\\\\').replace(/'/g,"\\\\'"); }
1088
+
1089
+ init();
1090
+ <\/script>
1091
+ </body>
1052
1092
  </html>`;
1053
1093
  function sendJson(res, data, status = 200) {
1054
1094
  res.writeHead(status, { 'Content-Type': 'application/json' });
@@ -1141,7 +1181,7 @@ async function handleRequest(req, res) {
1141
1181
  try {
1142
1182
  const aidList = await datamanager_1.CertAndKeyStore.getAids();
1143
1183
  const aidStatus = await getAidStatusList();
1144
- sendJson(res, { currentAid, aidList, aidStatus });
1184
+ sendJson(res, { currentAid, aidList, aidStatus, apiUrl: globalApiUrl });
1145
1185
  }
1146
1186
  catch (e) {
1147
1187
  sendJson(res, { error: e.message }, 500);
@@ -1152,6 +1192,16 @@ async function handleRequest(req, res) {
1152
1192
  try {
1153
1193
  const body = await parseBody(req);
1154
1194
  const aid = body.aid;
1195
+ // 根据 AID 后缀切换 AP
1196
+ const parts = aid.split('.');
1197
+ if (parts.length >= 3) {
1198
+ const domain = parts.slice(1).join('.');
1199
+ if (domain && domain !== globalApiUrl) {
1200
+ console.log(`[Select] 切换 AP 为 ${domain} (AID: ${aid})`);
1201
+ globalApiUrl = domain;
1202
+ agentCP = new agentcp_1.AgentCP(domain, '', globalDataDir || undefined);
1203
+ }
1204
+ }
1155
1205
  if (!agentCP) {
1156
1206
  agentCP = new agentcp_1.AgentCP(globalApiUrl, '', globalDataDir || undefined);
1157
1207
  }
@@ -1187,9 +1237,15 @@ async function handleRequest(req, res) {
1187
1237
  sendJson(res, { success: false, error: `最多只能注册 ${MAX_AIDS} 个 AID` });
1188
1238
  return;
1189
1239
  }
1190
- // 自动添加 .aid.pub 后缀
1191
- if (!aid.endsWith('.aid.pub')) {
1192
- aid = aid + '.aid.pub';
1240
+ // 根据 AID 后缀切换 AP
1241
+ const parts = aid.split('.');
1242
+ if (parts.length >= 3) {
1243
+ const domain = parts.slice(1).join('.');
1244
+ if (domain && domain !== globalApiUrl) {
1245
+ console.log(`[Create] 切换 AP 为 ${domain} (AID: ${aid})`);
1246
+ globalApiUrl = domain;
1247
+ agentCP = new agentcp_1.AgentCP(domain, '', globalDataDir || undefined);
1248
+ }
1193
1249
  }
1194
1250
  if (!agentCP) {
1195
1251
  agentCP = new agentcp_1.AgentCP(globalApiUrl, '', globalDataDir || undefined);
@@ -1458,6 +1514,17 @@ function startServer(port, apiUrl, dataDir = '') {
1458
1514
  agentCP = new agentcp_1.AgentCP(apiUrl, '', dataDir || undefined, { persistMessages: true });
1459
1515
  agentCP.loadCurrentAid().then(async (aid) => {
1460
1516
  if (aid) {
1517
+ // 如果加载的 AID 的 AP 与当前 apiUrl 不一致,重新初始化 AgentCP
1518
+ const parts = aid.split('.');
1519
+ if (parts.length >= 3) {
1520
+ const domain = parts.slice(1).join('.');
1521
+ if (domain !== globalApiUrl) {
1522
+ console.log(`[Server] 检测到 AID 所属 AP 为 ${domain},正在切换...`);
1523
+ globalApiUrl = domain;
1524
+ agentCP = new agentcp_1.AgentCP(domain, '', dataDir || undefined, { persistMessages: true });
1525
+ await agentCP.loadCurrentAid();
1526
+ }
1527
+ }
1461
1528
  currentAid = aid;
1462
1529
  console.log(`已加载 AID: ${aid}`);
1463
1530
  // 加载该 AID 的持久化会话