@baize-ai/core 0.2.0 → 0.3.0
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/.dockerignore +8 -18
- package/CHANGELOG.md +19 -1
- package/Dockerfile +66 -12
- package/README.md +85 -6
- package/README.zh-CN.md +63 -3
- package/assets/logo.png +0 -0
- package/cli/baize.js +6 -0
- package/cli/commands/a2a.js +124 -0
- package/cli/commands/add.js +126 -86
- package/cli/commands/component.js +27 -4
- package/cli/commands/doctor.js +2 -2
- package/cli/commands/init.js +28 -0
- package/cli/commands/self-uninstall.js +1 -1
- package/cli/lib/__tests__/instruction-split.test.js +1 -1
- package/cli/lib/a2a.js +235 -0
- package/cli/lib/lock.js +3 -3
- package/cli/lib/self-upgrade.js +1 -1
- package/docker-compose.yml +1 -1
- package/docs/docker.md +18 -4
- package/docs/release.md +150 -0
- package/package.json +1 -1
- package/scripts/docker-publish.sh +70 -0
- package/scripts/install.sh +4 -4
- package/scripts/pack-release.sh +361 -0
- package/skills/comm-bridge/package.json +7 -3
- package/skills/comm-bridge/scripts/c4-receive.js +23 -2
- package/skills/scheduler/package.json +2 -2
- package/skills/web-console/SKILL.md +34 -0
- package/skills/web-console/package.json +2 -2
- package/skills/web-console/public/app.js +900 -4
- package/skills/web-console/public/index.html +126 -0
- package/skills/web-console/public/styles.css +130 -0
- package/skills/web-console/scripts/a2a-admin.js +506 -0
- package/skills/web-console/scripts/channel-admin.js +99 -16
- package/skills/web-console/scripts/server.js +386 -1
- package/skills/web-console/scripts/skill-catalog.js +179 -0
- package/templates/claude-system.md +22 -0
- package/templates/pm2/ecosystem.config.cjs +4 -1
- package/test/a2a-cli.test.js +180 -0
- package/test/agent-card-api.test.js +261 -0
- package/test/channel-admin.test.js +87 -0
- package/test/component-lock.test.js +216 -0
- package/test/skill-catalog.test.js +118 -0
- package/test/web-console-routes.test.js +488 -1
|
@@ -1382,14 +1382,875 @@ async function installChannelFromCatalogue(btn) {
|
|
|
1382
1382
|
}
|
|
1383
1383
|
}
|
|
1384
1384
|
|
|
1385
|
+
// ── A2A (Agent-to-Agent) view ───────────────────────────────────────────────
|
|
1386
|
+
|
|
1387
|
+
function renderA2a(basePath) {
|
|
1388
|
+
_globalBasePath = basePath;
|
|
1389
|
+
setAdminMsg('');
|
|
1390
|
+
loadA2aStatus(basePath);
|
|
1391
|
+
loadAgentCard(basePath);
|
|
1392
|
+
loadA2aAuthz(basePath);
|
|
1393
|
+
loadA2aPeers(basePath);
|
|
1394
|
+
loadA2aTasks(basePath);
|
|
1395
|
+
loadA2aMessages(basePath);
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
function a2aCliUnavailable(body) {
|
|
1399
|
+
return /未找到 baize-a2a CLI/.test(body.error || '');
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
async function loadA2aStatus(basePath) {
|
|
1403
|
+
const el = document.getElementById('a2a-status');
|
|
1404
|
+
const state = document.getElementById('a2a-status-state');
|
|
1405
|
+
if (!el) return;
|
|
1406
|
+
let body;
|
|
1407
|
+
try {
|
|
1408
|
+
const r = await adminFetch(basePath, '/api/admin/a2a/status');
|
|
1409
|
+
if (r.status !== 200) {
|
|
1410
|
+
const e = new Error(r.body?.error || '状态加载失败');
|
|
1411
|
+
e.__body = r.body || null; // keep the server's resolved mode for the fresh-install card
|
|
1412
|
+
throw e;
|
|
1413
|
+
}
|
|
1414
|
+
body = r.body;
|
|
1415
|
+
} catch (err) {
|
|
1416
|
+
// Fresh install: the CLI may be missing (status 400) — the server still
|
|
1417
|
+
// resolves mode='standalone', so surface the mode card as 单机 (with the
|
|
1418
|
+
// upload entry) instead of an opaque "状态不可用".
|
|
1419
|
+
const fallback = (err && err.__body && err.__body.mode) ? err.__body : null;
|
|
1420
|
+
_lastA2aStatus = null;
|
|
1421
|
+
updateA2aInstallArea(null);
|
|
1422
|
+
if (fallback) updateA2aModeCard({ mode: fallback.mode, daemonHealthy: false });
|
|
1423
|
+
else updateA2aModeCard(null);
|
|
1424
|
+
updateA2aHealth(null);
|
|
1425
|
+
el.innerHTML = `<p class="cred-state">状态加载失败:${escapeHtml(err.message)}</p>`;
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
_lastA2aStatus = body;
|
|
1429
|
+
const enabled = body.enabled === true || String(body.enabled) === 'true';
|
|
1430
|
+
if (state) {
|
|
1431
|
+
state.textContent = enabled ? '已启用' : '未启用';
|
|
1432
|
+
state.className = `channel-state ${enabled ? 'ok' : 'warn'}`;
|
|
1433
|
+
}
|
|
1434
|
+
const rows = [
|
|
1435
|
+
['启用', enabled ? '是' : '否'],
|
|
1436
|
+
['Agent ID', body.agentId],
|
|
1437
|
+
['已注册', body.registered === true ? '是' : (body.registered === false ? '否' : body.registered)],
|
|
1438
|
+
['注册状态', body.registrationStatus],
|
|
1439
|
+
['宣告地址', body.advertiseUrl],
|
|
1440
|
+
['Endpoint', body.endpoint],
|
|
1441
|
+
['版本', body.version],
|
|
1442
|
+
].filter(([, v]) => v !== undefined && v !== null && v !== '');
|
|
1443
|
+
el.innerHTML = rows.map(([k, v]) => `<div class="conn-summary-row"><span class="conn-summary-name">${escapeHtml(k)}</span><span class="conn-summary-detail">${escapeHtml(String(v))}</span></div>`).join('');
|
|
1444
|
+
updateA2aInstallArea(body);
|
|
1445
|
+
updateA2aModeCard(body);
|
|
1446
|
+
updateA2aConfigArea(body);
|
|
1447
|
+
updateA2aHealth(body);
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
// ── D25 U3: module install / mode card / config / health ───────
|
|
1451
|
+
|
|
1452
|
+
// 最近一次 status / peers 数据,供健康卡跨渲染刷新 peer 数
|
|
1453
|
+
let _lastA2aStatus = null;
|
|
1454
|
+
let _lastA2aPeerCount = null;
|
|
1455
|
+
|
|
1456
|
+
function a2aModeInfo(body) {
|
|
1457
|
+
const mode = body && body.mode === 'cluster' ? 'cluster' : 'standalone';
|
|
1458
|
+
const healthy = !!(body && body.daemonHealthy === true);
|
|
1459
|
+
if (mode === 'cluster') {
|
|
1460
|
+
return {
|
|
1461
|
+
mode,
|
|
1462
|
+
label: healthy ? '集群·在线' : '集群·故障',
|
|
1463
|
+
cls: healthy ? 'a2a-mode-badge ok' : 'a2a-mode-badge err',
|
|
1464
|
+
detail: healthy ? '已加入集群,daemon 健康' : 'daemon 异常,集群服务不可用',
|
|
1465
|
+
stateText: healthy ? '集群·在线' : '集群·故障',
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
return {
|
|
1469
|
+
mode,
|
|
1470
|
+
label: '单机',
|
|
1471
|
+
cls: 'a2a-mode-badge',
|
|
1472
|
+
detail: '仅本机运行,未加入集群',
|
|
1473
|
+
stateText: '单机',
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
function updateA2aInstallArea(body) {
|
|
1478
|
+
const el = document.getElementById('a2a-install-status');
|
|
1479
|
+
const state = document.getElementById('a2a-install-state');
|
|
1480
|
+
const fileInput = document.getElementById('a2a-install-file');
|
|
1481
|
+
const btn = document.getElementById('btn-a2a-install');
|
|
1482
|
+
if (!el || !fileInput || !btn) return;
|
|
1483
|
+
const version = body && (body.version || body.installedVersion);
|
|
1484
|
+
if (version) {
|
|
1485
|
+
el.textContent = `已安装 baize-a2a v${String(version).replace(/^v/, '')}。如需升级请重新上传模块包。`;
|
|
1486
|
+
if (state) { state.textContent = '已安装'; state.className = 'channel-state ok'; }
|
|
1487
|
+
fileInput.disabled = true;
|
|
1488
|
+
btn.disabled = true;
|
|
1489
|
+
} else {
|
|
1490
|
+
el.textContent = '未安装 — 上传 baize-a2a 模块包(.tgz)完成安装。';
|
|
1491
|
+
if (state) { state.textContent = '未安装'; state.className = 'channel-state warn'; }
|
|
1492
|
+
fileInput.disabled = false;
|
|
1493
|
+
btn.disabled = false;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
function updateA2aModeCard(body) {
|
|
1498
|
+
const badgeEl = document.getElementById('a2a-mode-badge');
|
|
1499
|
+
const btn = document.getElementById('btn-a2a-switch-mode');
|
|
1500
|
+
const state = document.getElementById('a2a-mode-state');
|
|
1501
|
+
if (!badgeEl || !btn) return;
|
|
1502
|
+
if (!body || body.error) {
|
|
1503
|
+
badgeEl.innerHTML = '<span class="a2a-mode-badge">状态不可用</span> <span class="a2a-mode-detail">无法获取运行模式(daemon 未运行或未安装)</span>';
|
|
1504
|
+
if (state) { state.textContent = '不可用'; state.className = 'channel-state warn'; }
|
|
1505
|
+
btn.textContent = '切换集群模式';
|
|
1506
|
+
btn.dataset.targetMode = 'cluster';
|
|
1507
|
+
btn.disabled = true;
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
const info = a2aModeInfo(body);
|
|
1511
|
+
badgeEl.innerHTML = `<span class="${info.cls}">${info.label}</span> <span class="a2a-mode-detail">${info.detail}</span>`;
|
|
1512
|
+
if (state) { state.textContent = info.stateText; state.className = 'channel-state'; }
|
|
1513
|
+
btn.textContent = info.mode === 'cluster' ? '切换单机模式' : '切换集群模式';
|
|
1514
|
+
btn.dataset.targetMode = info.mode === 'cluster' ? 'standalone' : 'cluster';
|
|
1515
|
+
btn.disabled = false;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
function updateA2aConfigArea(body) {
|
|
1519
|
+
const adminInput = document.getElementById('a2a-config-admin-url');
|
|
1520
|
+
const advertiseInput = document.getElementById('a2a-config-advertise-url');
|
|
1521
|
+
if (!adminInput || !advertiseInput) return;
|
|
1522
|
+
if (!adminInput.value && body && body.adminUrl) adminInput.value = body.adminUrl;
|
|
1523
|
+
if (!advertiseInput.value && body && body.advertiseUrl) advertiseInput.value = body.advertiseUrl;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
function a2aHeartbeatLabel(body) {
|
|
1527
|
+
if (!body) return '—';
|
|
1528
|
+
const direct = body.heartbeat ?? body.lastHeartbeat ?? body.heartbeatAt;
|
|
1529
|
+
if (direct !== undefined && direct !== null && direct !== '') return String(direct);
|
|
1530
|
+
// 未暴露心跳时间戳时,用注册态作为心跳健康代理(registered = JWT 有效,daemon 仍在心跳续期)
|
|
1531
|
+
return body.registered === true ? '正常(注册有效)' : (body.registrationStatus || '—');
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
function updateA2aHealth(body, peerCountOverride) {
|
|
1535
|
+
const el = document.getElementById('a2a-health');
|
|
1536
|
+
const state = document.getElementById('a2a-health-state');
|
|
1537
|
+
if (!el) return;
|
|
1538
|
+
const healthy = !!(body && body.daemonHealthy === true);
|
|
1539
|
+
const modeLabel = body && body.mode === 'cluster' ? '集群' : '单机';
|
|
1540
|
+
let peerCount = '—';
|
|
1541
|
+
if (peerCountOverride !== undefined && peerCountOverride !== null) {
|
|
1542
|
+
peerCount = peerCountOverride;
|
|
1543
|
+
} else if (body) {
|
|
1544
|
+
peerCount = body.peerCount ?? (Array.isArray(body.peers) ? body.peers.length : null) ?? (_lastA2aPeerCount ?? '—');
|
|
1545
|
+
} else if (_lastA2aPeerCount !== null) {
|
|
1546
|
+
peerCount = _lastA2aPeerCount;
|
|
1547
|
+
}
|
|
1548
|
+
const rows = [
|
|
1549
|
+
['Daemon 健康', healthy ? '✅ 正常' : '❌ 异常'],
|
|
1550
|
+
['运行模式', modeLabel],
|
|
1551
|
+
['Peer 数', peerCount],
|
|
1552
|
+
['最近心跳', a2aHeartbeatLabel(body)],
|
|
1553
|
+
].filter(([, v]) => v !== undefined && v !== null && v !== '');
|
|
1554
|
+
el.innerHTML = rows.map(([k, v]) => `<div class="conn-summary-row"><span class="conn-summary-name">${escapeHtml(k)}</span><span class="conn-summary-detail">${escapeHtml(String(v))}</span></div>`).join('');
|
|
1555
|
+
if (state) {
|
|
1556
|
+
state.textContent = healthy ? '健康' : '异常';
|
|
1557
|
+
state.className = `channel-state ${healthy ? 'ok' : 'warn'}`;
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
// ── Agent card (D21: name/description/skills self-service) ───────────────────
|
|
1562
|
+
|
|
1563
|
+
async function loadAgentCard(basePath) {
|
|
1564
|
+
const el = document.getElementById('a2a-card');
|
|
1565
|
+
const state = document.getElementById('a2a-card-state');
|
|
1566
|
+
if (!el) return;
|
|
1567
|
+
let body;
|
|
1568
|
+
try {
|
|
1569
|
+
const { status, body: b } = await adminFetch(basePath, '/api/admin/a2a/card');
|
|
1570
|
+
if (status !== 200) throw new Error(b.error || '名片加载失败');
|
|
1571
|
+
body = b;
|
|
1572
|
+
} catch (err) {
|
|
1573
|
+
el.innerHTML = `<p class="cred-state">名片加载失败:${escapeHtml(err.message)}</p>`;
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
const skills = Array.isArray(body.skills) ? body.skills : [];
|
|
1577
|
+
if (state) state.textContent = skills.length ? `${skills.length} 个 skills` : '无 skills';
|
|
1578
|
+
const skillLines = skills.length
|
|
1579
|
+
? skills.map((s) => ` · ${escapeHtml(s && (s.id || s.name))}`).join('<br>')
|
|
1580
|
+
: '<span class="cred-state">(未配置 — 点击「编辑名片」从已安装 skills 中选择)</span>';
|
|
1581
|
+
el.innerHTML = `<div class="conn-summary-row"><span class="conn-summary-name">名称</span><span class="conn-summary-detail">${escapeHtml(body.name || '')}</span></div>`
|
|
1582
|
+
+ `<div class="conn-summary-row"><span class="conn-summary-name">描述</span><span class="conn-summary-detail">${escapeHtml(body.description || '')}</span></div>`
|
|
1583
|
+
+ `<div class="conn-summary-row"><span class="conn-summary-name">Skills</span><span class="conn-summary-detail">${skillLines}</span></div>`;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// ── Call authorization (D22 单元 C) ──────────────────────────────────────────
|
|
1587
|
+
|
|
1588
|
+
async function loadA2aAuthz(basePath) {
|
|
1589
|
+
const el = document.getElementById('a2a-authz');
|
|
1590
|
+
const state = document.getElementById('a2a-authz-state');
|
|
1591
|
+
if (!el) return;
|
|
1592
|
+
let body;
|
|
1593
|
+
try {
|
|
1594
|
+
const { status, body: b } = await adminFetch(basePath, '/api/admin/a2a/authz');
|
|
1595
|
+
if (status !== 200) throw new Error(b.error || '授权策略加载失败');
|
|
1596
|
+
body = b;
|
|
1597
|
+
} catch (err) {
|
|
1598
|
+
el.innerHTML = `<p class="cred-state">授权策略加载失败:${escapeHtml(err.message)}</p>`;
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
const isAllowlist = body.mode === 'allowlist';
|
|
1602
|
+
const modeName = isAllowlist ? '白名单' : '开放';
|
|
1603
|
+
const listName = isAllowlist ? '允许' : '阻止';
|
|
1604
|
+
const items = Array.isArray(isAllowlist ? body.allow : body.block) ? (isAllowlist ? body.allow : body.block) : [];
|
|
1605
|
+
if (state) state.textContent = items.length ? `${listName} ${items.length} 个` : '未设置';
|
|
1606
|
+
const lines = items.length
|
|
1607
|
+
? items.map((id) => ` · ${escapeHtml(id)}`).join('<br>')
|
|
1608
|
+
: '<span class="cred-state">未设置 — 点击「编辑名片」→「调用授权」配置</span>';
|
|
1609
|
+
el.innerHTML = `<div class="conn-summary-row"><span class="conn-summary-name">模式</span><span class="conn-summary-detail">${modeName}</span></div>`
|
|
1610
|
+
+ `<div class="conn-summary-row"><span class="conn-summary-name">${listName}列表</span><span class="conn-summary-detail">${lines}</span></div>`;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
/**
|
|
1614
|
+
* Open the agent-card editor modal: name + description + skills multi-selector.
|
|
1615
|
+
* Available for use whether or not A2A is enabled (initializes card basics).
|
|
1616
|
+
*/
|
|
1617
|
+
async function openAgentCardEditor(basePath) {
|
|
1618
|
+
const existing = document.getElementById('agent-card-modal');
|
|
1619
|
+
if (existing) existing.remove();
|
|
1620
|
+
|
|
1621
|
+
let catalogue;
|
|
1622
|
+
let card;
|
|
1623
|
+
let authz;
|
|
1624
|
+
let peerIds;
|
|
1625
|
+
try {
|
|
1626
|
+
const [catRes, cardRes, authzRes, peersRes] = await Promise.all([
|
|
1627
|
+
adminFetch(basePath, '/api/admin/a2a/card/skills'),
|
|
1628
|
+
adminFetch(basePath, '/api/admin/a2a/card'),
|
|
1629
|
+
adminFetch(basePath, '/api/admin/a2a/authz'),
|
|
1630
|
+
adminFetch(basePath, '/api/admin/a2a/peers'),
|
|
1631
|
+
]);
|
|
1632
|
+
if (catRes.status !== 200) throw new Error(catRes.body.error || 'skills 列表加载失败');
|
|
1633
|
+
if (cardRes.status !== 200) throw new Error(cardRes.body.error || '名片加载失败');
|
|
1634
|
+
if (authzRes.status !== 200) throw new Error(authzRes.body.error || '授权策略加载失败');
|
|
1635
|
+
catalogue = catRes.body.skills || [];
|
|
1636
|
+
card = cardRes.body;
|
|
1637
|
+
authz = authzRes.body;
|
|
1638
|
+
peerIds = peersRes.status === 200 && Array.isArray(peersRes.body.peers)
|
|
1639
|
+
? peersRes.body.peers.map((p) => p.agentId).filter(Boolean)
|
|
1640
|
+
: [];
|
|
1641
|
+
} catch (err) {
|
|
1642
|
+
setAdminMsg(`❌ 无法打开名片编辑器:${err.message}`, true);
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
const selected = new Set((card.skills || []).map((s) => (typeof s === 'string' ? s : (s && s.id))).filter(Boolean));
|
|
1647
|
+
|
|
1648
|
+
const overlay = document.createElement('div');
|
|
1649
|
+
overlay.id = 'agent-card-modal';
|
|
1650
|
+
overlay.className = 'agent-card-modal-overlay';
|
|
1651
|
+
overlay.innerHTML = `
|
|
1652
|
+
<div class="agent-card-modal" role="dialog" aria-modal="true" aria-label="编辑名片">
|
|
1653
|
+
<div class="agent-card-modal-head">
|
|
1654
|
+
<div class="card-title">编辑名片</div>
|
|
1655
|
+
<button type="button" class="agent-card-modal-close" aria-label="关闭">×</button>
|
|
1656
|
+
</div>
|
|
1657
|
+
<div class="agent-card-modal-tabs" role="tablist" aria-label="编辑内容">
|
|
1658
|
+
<button type="button" class="agent-card-tab active" role="tab" aria-selected="true" data-tab="card">名片信息</button>
|
|
1659
|
+
<button type="button" class="agent-card-tab" role="tab" aria-selected="false" data-tab="authz">调用授权</button>
|
|
1660
|
+
</div>
|
|
1661
|
+
<div class="agent-card-modal-body">
|
|
1662
|
+
<div id="agent-card-tab-card">
|
|
1663
|
+
<label class="agent-card-field-label" for="agent-card-name">名称(必填,≤60 字符)</label>
|
|
1664
|
+
<input type="text" id="agent-card-name" class="agent-card-input" maxlength="60" autocomplete="off" spellcheck="false">
|
|
1665
|
+
<label class="agent-card-field-label" for="agent-card-desc">描述(≤300 字符)</label>
|
|
1666
|
+
<textarea id="agent-card-desc" class="agent-card-input agent-card-textarea" rows="3" maxlength="300" placeholder="一句话介绍你的 agent"></textarea>
|
|
1667
|
+
<div class="agent-card-field-label">Skills <span class="agent-card-selected-count" id="agent-card-count">已选 0 个</span></div>
|
|
1668
|
+
<input type="text" id="agent-card-search" class="agent-card-input" placeholder="搜索 skill(名称 / 描述 / 目录名)" autocomplete="off" spellcheck="false">
|
|
1669
|
+
<div class="agent-card-skill-list" id="agent-card-skills" role="list" aria-live="polite"></div>
|
|
1670
|
+
</div>
|
|
1671
|
+
<div id="agent-card-tab-authz" hidden>
|
|
1672
|
+
<div class="agent-card-field-label">模式</div>
|
|
1673
|
+
<div class="authz-mode-row">
|
|
1674
|
+
<label class="authz-mode-option"><input type="radio" name="authz-mode" value="open"> 开放(默认)</label>
|
|
1675
|
+
<label class="authz-mode-option"><input type="radio" name="authz-mode" value="allowlist"> 白名单</label>
|
|
1676
|
+
</div>
|
|
1677
|
+
<p class="conn-block-desc" id="authz-mode-desc"></p>
|
|
1678
|
+
<div class="agent-card-field-label"><span id="authz-list-title">阻止列表</span> <span class="agent-card-selected-count" id="authz-count"></span></div>
|
|
1679
|
+
<div class="authz-tags" id="authz-tags" aria-live="polite"></div>
|
|
1680
|
+
<div class="authz-add-row">
|
|
1681
|
+
<input type="text" id="authz-peer-input" class="agent-card-input" placeholder="输入 peer agent_id 后添加" autocomplete="off" spellcheck="false">
|
|
1682
|
+
<button type="button" class="btn btn-outline" id="authz-peer-add">添加</button>
|
|
1683
|
+
</div>
|
|
1684
|
+
<div class="agent-card-field-label">从 peer 列表选择</div>
|
|
1685
|
+
<select id="authz-peer-select" class="agent-card-input" aria-label="从 peer 列表选择"></select>
|
|
1686
|
+
</div>
|
|
1687
|
+
</div>
|
|
1688
|
+
<div class="agent-card-modal-foot">
|
|
1689
|
+
<button type="button" class="btn btn-outline" id="agent-card-cancel">取消</button>
|
|
1690
|
+
<button type="button" class="btn btn-primary" id="agent-card-save">保存</button>
|
|
1691
|
+
</div>
|
|
1692
|
+
</div>
|
|
1693
|
+
`;
|
|
1694
|
+
document.body.appendChild(overlay);
|
|
1695
|
+
|
|
1696
|
+
const nameInput = overlay.querySelector('#agent-card-name');
|
|
1697
|
+
const descInput = overlay.querySelector('#agent-card-desc');
|
|
1698
|
+
const searchInput = overlay.querySelector('#agent-card-search');
|
|
1699
|
+
const listEl = overlay.querySelector('#agent-card-skills');
|
|
1700
|
+
const countEl = overlay.querySelector('#agent-card-count');
|
|
1701
|
+
nameInput.value = card.name || '';
|
|
1702
|
+
descInput.value = card.description || '';
|
|
1703
|
+
|
|
1704
|
+
// ── 调用授权 tab state (D22 单元 C) ──
|
|
1705
|
+
const allowSet = new Set(Array.isArray(authz.allow) ? authz.allow : []);
|
|
1706
|
+
const blockSet = new Set(Array.isArray(authz.block) ? authz.block : []);
|
|
1707
|
+
let authzMode = authz.mode === 'allowlist' ? 'allowlist' : 'open';
|
|
1708
|
+
const activeAuthzList = () => (authzMode === 'allowlist' ? allowSet : blockSet);
|
|
1709
|
+
|
|
1710
|
+
const authzModeDesc = overlay.querySelector('#authz-mode-desc');
|
|
1711
|
+
const authzListTitle = overlay.querySelector('#authz-list-title');
|
|
1712
|
+
const authzCount = overlay.querySelector('#authz-count');
|
|
1713
|
+
const authzTagsEl = overlay.querySelector('#authz-tags');
|
|
1714
|
+
const authzPeerInput = overlay.querySelector('#authz-peer-input');
|
|
1715
|
+
const authzPeerSelect = overlay.querySelector('#authz-peer-select');
|
|
1716
|
+
|
|
1717
|
+
const renderAuthz = () => {
|
|
1718
|
+
overlay.querySelectorAll('input[name="authz-mode"]').forEach((r) => {
|
|
1719
|
+
r.checked = r.value === authzMode;
|
|
1720
|
+
});
|
|
1721
|
+
const isAllowlist = authzMode === 'allowlist';
|
|
1722
|
+
authzModeDesc.textContent = isAllowlist
|
|
1723
|
+
? '仅允许列表中的 agent 可以调用本 agent(空列表 = 无人可调用)。'
|
|
1724
|
+
: '接受所有已批准 agent,除阻止列表中的 agent。';
|
|
1725
|
+
authzListTitle.textContent = isAllowlist ? '允许列表' : '阻止列表';
|
|
1726
|
+
const list = [...activeAuthzList()];
|
|
1727
|
+
authzCount.textContent = `${list.length} 个`;
|
|
1728
|
+
authzTagsEl.innerHTML = list.length
|
|
1729
|
+
? list.map((id) => `<span class="authz-tag">${escapeHtml(id)}<button type="button" class="authz-tag-x" aria-label="移除 ${escapeHtml(id)}" data-id="${escapeHtml(id)}">×</button></span>`).join('')
|
|
1730
|
+
: '<span class="cred-state">(空)</span>';
|
|
1731
|
+
const current = new Set(activeAuthzList());
|
|
1732
|
+
const available = peerIds.filter((id) => !current.has(id));
|
|
1733
|
+
authzPeerSelect.innerHTML = `<option value="">${available.length ? '选择 peer 添加到列表…' : '列表已包含所有已知 peer'}</option>`
|
|
1734
|
+
+ available.map((id) => `<option value="${escapeHtml(id)}">${escapeHtml(id)}</option>`).join('');
|
|
1735
|
+
authzPeerSelect.disabled = available.length === 0;
|
|
1736
|
+
};
|
|
1737
|
+
|
|
1738
|
+
overlay.querySelectorAll('input[name="authz-mode"]').forEach((r) => {
|
|
1739
|
+
r.addEventListener('change', () => {
|
|
1740
|
+
if (r.checked) {
|
|
1741
|
+
authzMode = r.value;
|
|
1742
|
+
renderAuthz();
|
|
1743
|
+
}
|
|
1744
|
+
});
|
|
1745
|
+
});
|
|
1746
|
+
|
|
1747
|
+
const addAuthzPeer = () => {
|
|
1748
|
+
const value = authzPeerInput.value.trim();
|
|
1749
|
+
if (!value) {
|
|
1750
|
+
setAdminMsg('请输入 peer agent_id', true);
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
const list = activeAuthzList();
|
|
1754
|
+
if (list.has(value)) {
|
|
1755
|
+
setAdminMsg(`⚠️ ${value} 已在${authzMode === 'allowlist' ? '允许' : '阻止'}列表中`, true);
|
|
1756
|
+
return;
|
|
1757
|
+
}
|
|
1758
|
+
list.add(value);
|
|
1759
|
+
authzPeerInput.value = '';
|
|
1760
|
+
renderAuthz();
|
|
1761
|
+
};
|
|
1762
|
+
authzPeerInput.addEventListener('keydown', (e) => {
|
|
1763
|
+
if (e.key === 'Enter') {
|
|
1764
|
+
e.preventDefault();
|
|
1765
|
+
addAuthzPeer();
|
|
1766
|
+
}
|
|
1767
|
+
});
|
|
1768
|
+
overlay.querySelector('#authz-peer-add').addEventListener('click', addAuthzPeer);
|
|
1769
|
+
authzPeerSelect.addEventListener('change', () => {
|
|
1770
|
+
const value = authzPeerSelect.value;
|
|
1771
|
+
if (!value) return;
|
|
1772
|
+
activeAuthzList().add(value);
|
|
1773
|
+
renderAuthz();
|
|
1774
|
+
});
|
|
1775
|
+
authzTagsEl.addEventListener('click', (e) => {
|
|
1776
|
+
const btn = e.target.closest('.authz-tag-x');
|
|
1777
|
+
if (btn) {
|
|
1778
|
+
activeAuthzList().delete(btn.dataset.id);
|
|
1779
|
+
renderAuthz();
|
|
1780
|
+
}
|
|
1781
|
+
});
|
|
1782
|
+
|
|
1783
|
+
// ── Tab switching ──
|
|
1784
|
+
const tabButtons = overlay.querySelectorAll('.agent-card-tab');
|
|
1785
|
+
const cardTabEl = overlay.querySelector('#agent-card-tab-card');
|
|
1786
|
+
const authzTabEl = overlay.querySelector('#agent-card-tab-authz');
|
|
1787
|
+
const switchTab = (name) => {
|
|
1788
|
+
const isCard = name === 'card';
|
|
1789
|
+
cardTabEl.hidden = !isCard;
|
|
1790
|
+
authzTabEl.hidden = isCard;
|
|
1791
|
+
tabButtons.forEach((b) => {
|
|
1792
|
+
const active = b.dataset.tab === name;
|
|
1793
|
+
b.classList.toggle('active', active);
|
|
1794
|
+
b.setAttribute('aria-selected', active ? 'true' : 'false');
|
|
1795
|
+
});
|
|
1796
|
+
};
|
|
1797
|
+
tabButtons.forEach((b) => b.addEventListener('click', () => switchTab(b.dataset.tab)));
|
|
1798
|
+
|
|
1799
|
+
const updateCount = () => {
|
|
1800
|
+
countEl.textContent = `已选 ${selected.size} 个`;
|
|
1801
|
+
};
|
|
1802
|
+
|
|
1803
|
+
const renderSkills = () => {
|
|
1804
|
+
const q = searchInput.value.trim().toLowerCase();
|
|
1805
|
+
const filtered = catalogue.filter((s) => {
|
|
1806
|
+
if (!q) return true;
|
|
1807
|
+
return [s.name, s.description, s.id].some((v) => String(v || '').toLowerCase().includes(q));
|
|
1808
|
+
});
|
|
1809
|
+
if (!filtered.length) {
|
|
1810
|
+
listEl.innerHTML = '<p class="cred-state">没有匹配的 skill。</p>';
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
listEl.innerHTML = filtered.map((s) => {
|
|
1814
|
+
const isSelected = selected.has(s.id);
|
|
1815
|
+
return `<div class="agent-card-skill-row ${isSelected ? 'selected' : ''}" role="listitem" data-id="${escapeHtml(s.id)}" tabindex="0" aria-pressed="${isSelected}">
|
|
1816
|
+
<div class="agent-card-skill-main">
|
|
1817
|
+
<div class="agent-card-skill-title"><strong>${escapeHtml(s.name)}</strong> <span class="agent-card-skill-id">${escapeHtml(s.id)}</span></div>
|
|
1818
|
+
<p class="conn-block-desc">${escapeHtml(s.description || '')}</p>
|
|
1819
|
+
</div>
|
|
1820
|
+
<span class="agent-card-skill-check">${isSelected ? '✓' : ''}</span>
|
|
1821
|
+
</div>`;
|
|
1822
|
+
}).join('');
|
|
1823
|
+
updateCount();
|
|
1824
|
+
};
|
|
1825
|
+
|
|
1826
|
+
const toggleSkill = (id) => {
|
|
1827
|
+
if (selected.has(id)) selected.delete(id);
|
|
1828
|
+
else selected.add(id);
|
|
1829
|
+
renderSkills();
|
|
1830
|
+
};
|
|
1831
|
+
|
|
1832
|
+
listEl.addEventListener('click', (e) => {
|
|
1833
|
+
const row = e.target.closest('.agent-card-skill-row');
|
|
1834
|
+
if (row) toggleSkill(row.dataset.id);
|
|
1835
|
+
});
|
|
1836
|
+
listEl.addEventListener('keydown', (e) => {
|
|
1837
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
1838
|
+
const row = e.target.closest('.agent-card-skill-row');
|
|
1839
|
+
if (row) {
|
|
1840
|
+
e.preventDefault();
|
|
1841
|
+
toggleSkill(row.dataset.id);
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
});
|
|
1845
|
+
searchInput.addEventListener('input', renderSkills);
|
|
1846
|
+
|
|
1847
|
+
const closeModal = () => {
|
|
1848
|
+
overlay.remove();
|
|
1849
|
+
document.removeEventListener('keydown', onModalKey);
|
|
1850
|
+
};
|
|
1851
|
+
const onModalKey = (e) => {
|
|
1852
|
+
if (e.key === 'Escape' && document.getElementById('agent-card-modal') === overlay) {
|
|
1853
|
+
closeModal();
|
|
1854
|
+
}
|
|
1855
|
+
};
|
|
1856
|
+
document.addEventListener('keydown', onModalKey);
|
|
1857
|
+
|
|
1858
|
+
overlay.querySelector('#agent-card-cancel').addEventListener('click', closeModal);
|
|
1859
|
+
overlay.querySelector('.agent-card-modal-close').addEventListener('click', closeModal);
|
|
1860
|
+
overlay.addEventListener('mousedown', (e) => {
|
|
1861
|
+
if (e.target === overlay) closeModal();
|
|
1862
|
+
});
|
|
1863
|
+
|
|
1864
|
+
overlay.querySelector('#agent-card-save').addEventListener('click', async () => {
|
|
1865
|
+
const name = nameInput.value.trim();
|
|
1866
|
+
const description = descInput.value.trim();
|
|
1867
|
+
if (!name) {
|
|
1868
|
+
setAdminMsg('名片名称不能为空', true);
|
|
1869
|
+
return;
|
|
1870
|
+
}
|
|
1871
|
+
const saveBtn = overlay.querySelector('#agent-card-save');
|
|
1872
|
+
saveBtn.disabled = true;
|
|
1873
|
+
saveBtn.textContent = '保存中...';
|
|
1874
|
+
try {
|
|
1875
|
+
const [cardRes, authzRes] = await Promise.all([
|
|
1876
|
+
adminFetch(basePath, '/api/admin/a2a/card', {
|
|
1877
|
+
method: 'PUT',
|
|
1878
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1879
|
+
body: JSON.stringify({ name, description, skills: [...selected] }),
|
|
1880
|
+
}),
|
|
1881
|
+
adminFetch(basePath, '/api/admin/a2a/authz', {
|
|
1882
|
+
method: 'PUT',
|
|
1883
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1884
|
+
body: JSON.stringify({ mode: authzMode, allow: [...allowSet], block: [...blockSet] }),
|
|
1885
|
+
}),
|
|
1886
|
+
]);
|
|
1887
|
+
if (cardRes.status === 401 || authzRes.status === 401) {
|
|
1888
|
+
setAdminMsg('未登录或会话过期,请重新登录', true);
|
|
1889
|
+
closeModal();
|
|
1890
|
+
return;
|
|
1891
|
+
}
|
|
1892
|
+
const failed = [];
|
|
1893
|
+
if (cardRes.status !== 200) failed.push(`名片:${cardRes.body.error || '未知错误'}`);
|
|
1894
|
+
if (authzRes.status !== 200) failed.push(`调用授权:${authzRes.body.error || '未知错误'}`);
|
|
1895
|
+
if (failed.length) {
|
|
1896
|
+
setAdminMsg(`❌ 保存失败:${failed.join(';')}`, true);
|
|
1897
|
+
return;
|
|
1898
|
+
}
|
|
1899
|
+
setAdminMsg('✅ 名片与调用授权已保存(baize-a2a 将在数秒内热更新)');
|
|
1900
|
+
closeModal();
|
|
1901
|
+
loadAgentCard(basePath);
|
|
1902
|
+
loadA2aAuthz(basePath);
|
|
1903
|
+
} catch (err) {
|
|
1904
|
+
setAdminMsg(`❌ 保存失败:${err.message}`, true);
|
|
1905
|
+
} finally {
|
|
1906
|
+
saveBtn.disabled = false;
|
|
1907
|
+
saveBtn.textContent = '保存';
|
|
1908
|
+
}
|
|
1909
|
+
});
|
|
1910
|
+
|
|
1911
|
+
renderSkills();
|
|
1912
|
+
renderAuthz();
|
|
1913
|
+
nameInput.focus();
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
async function loadA2aPeers(basePath) {
|
|
1917
|
+
const el = document.getElementById('a2a-peers');
|
|
1918
|
+
const state = document.getElementById('a2a-peers-state');
|
|
1919
|
+
if (!el) return;
|
|
1920
|
+
let body;
|
|
1921
|
+
try {
|
|
1922
|
+
const { status, body: b } = await adminFetch(basePath, '/api/admin/a2a/peers');
|
|
1923
|
+
if (status !== 200) throw new Error(b.error || 'peer 列表加载失败');
|
|
1924
|
+
body = b;
|
|
1925
|
+
} catch (err) {
|
|
1926
|
+
el.innerHTML = `<p class="cred-state">Peer 列表加载失败:${escapeHtml(err.message)}</p>`;
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
const peers = body.peers || [];
|
|
1930
|
+
// 同步刷新健康卡 peer 数(status 无 peerCount 时以本列表为准)
|
|
1931
|
+
_lastA2aPeerCount = peers.length;
|
|
1932
|
+
updateA2aHealth(_lastA2aStatus);
|
|
1933
|
+
if (state) state.textContent = peers.length ? `${peers.length} 个` : '';
|
|
1934
|
+
if (!peers.length) {
|
|
1935
|
+
el.innerHTML = '<p class="cred-state">暂无缓存 peer。启用 A2A 并注册后,或使用下方搜索发现 agent。</p>';
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
el.innerHTML = peers.map((p) => {
|
|
1939
|
+
const skills = Array.isArray(p.skills) && p.skills.length
|
|
1940
|
+
? ` · skills: ${p.skills.map((s) => (typeof s === 'string' ? s : s.id || s.name)).join(', ')}` : '';
|
|
1941
|
+
const expiry = p.expiresAt ? ` · 缓存至 ${escapeHtml(p.expiresAt)}` : '';
|
|
1942
|
+
return `<div class="channel-item" role="listitem">
|
|
1943
|
+
<div class="channel-item-main">
|
|
1944
|
+
<div class="channel-item-title"><strong>${escapeHtml(p.agentId)}</strong></div>
|
|
1945
|
+
<p class="conn-block-desc">endpoint: ${escapeHtml(p.endpoint || '-')}${skills}${expiry}</p>
|
|
1946
|
+
</div>
|
|
1947
|
+
</div>`;
|
|
1948
|
+
}).join('');
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
// A2A 看板/消息列表最多渲染条数(后端 API 可查全部,前端控制展示长度)
|
|
1952
|
+
const A2A_LIST_MAX = 20;
|
|
1953
|
+
|
|
1954
|
+
async function loadA2aTasks(basePath) {
|
|
1955
|
+
const el = document.getElementById('a2a-tasks');
|
|
1956
|
+
const state = document.getElementById('a2a-tasks-state');
|
|
1957
|
+
if (!el) return;
|
|
1958
|
+
let body;
|
|
1959
|
+
try {
|
|
1960
|
+
const { status, body: b } = await adminFetch(basePath, '/api/admin/a2a/tasks');
|
|
1961
|
+
if (status !== 200) throw new Error(b.error || '任务看板加载失败');
|
|
1962
|
+
body = b;
|
|
1963
|
+
} catch (err) {
|
|
1964
|
+
el.innerHTML = `<p class="cred-state">任务看板加载失败:${escapeHtml(err.message)}</p>`;
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
const tasks = body.tasks || [];
|
|
1968
|
+
// 列表只渲染最近 20 条,避免任务多时页面过长(后端 API 仍可查全部)
|
|
1969
|
+
const shownTasks = tasks.slice(0, A2A_LIST_MAX);
|
|
1970
|
+
if (state) state.textContent = tasks.length ? `${tasks.length} 条(显示最近 ${shownTasks.length})` : '';
|
|
1971
|
+
if (body.hint || !tasks.length) {
|
|
1972
|
+
el.innerHTML = `<p class="cred-state">${escapeHtml(body.hint || '暂无 A2A 任务。')}</p>`;
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
const statusCls = (s) => ((s === 'completed') ? 'ok' : ((s === 'failed' || s === 'canceled') ? 'warn' : 'warn'));
|
|
1976
|
+
const statusLabel = (s) => ({ queued: '排队中', running: '运行中', input_required: '等待输入', cancel_requested: '取消中', completed: '已完成', failed: '失败', canceled: '已取消' }[s] || s);
|
|
1977
|
+
el.innerHTML = shownTasks.map((t) => {
|
|
1978
|
+
const inst = String(t.instruction || '').length > 120 ? `${String(t.instruction).slice(0, 120)}…` : String(t.instruction || '');
|
|
1979
|
+
const meta = [t.caller_agent_id ? `caller: ${t.caller_agent_id}` : null, t.skill_id ? `skill: ${t.skill_id}` : null, t.created_at ? `创建: ${t.created_at}` : null].filter(Boolean).join(' · ');
|
|
1980
|
+
return `<div class="channel-item" role="listitem">
|
|
1981
|
+
<div class="channel-item-main">
|
|
1982
|
+
<div class="channel-item-title">
|
|
1983
|
+
<strong>${escapeHtml(String(t.id).slice(0, 24))}</strong>
|
|
1984
|
+
<span class="channel-state ${statusCls(t.status)}">${escapeHtml(statusLabel(t.status))}</span>
|
|
1985
|
+
</div>
|
|
1986
|
+
<p class="conn-block-desc">${escapeHtml(inst)}</p>
|
|
1987
|
+
<p class="conn-block-desc">${escapeHtml(meta)}</p>
|
|
1988
|
+
</div>
|
|
1989
|
+
</div>`;
|
|
1990
|
+
}).join('');
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
async function loadA2aMessages(basePath) {
|
|
1994
|
+
const el = document.getElementById('a2a-messages');
|
|
1995
|
+
const state = document.getElementById('a2a-messages-state');
|
|
1996
|
+
if (!el) return;
|
|
1997
|
+
let body;
|
|
1998
|
+
try {
|
|
1999
|
+
const { status, body: b } = await adminFetch(basePath, '/api/admin/a2a/messages');
|
|
2000
|
+
if (status !== 200) throw new Error(b.error || '消息记录加载失败');
|
|
2001
|
+
body = b;
|
|
2002
|
+
} catch (err) {
|
|
2003
|
+
el.innerHTML = `<p class="cred-state">消息记录加载失败:${escapeHtml(err.message)}</p>`;
|
|
2004
|
+
return;
|
|
2005
|
+
}
|
|
2006
|
+
const messages = body.messages || [];
|
|
2007
|
+
// 只渲染最近 20 条(后端 API 可查全部)
|
|
2008
|
+
const shownMessages = messages.slice(0, A2A_LIST_MAX);
|
|
2009
|
+
if (state) state.textContent = messages.length ? `${messages.length} 条(显示最近 ${shownMessages.length})` : '';
|
|
2010
|
+
if (body.hint || !messages.length) {
|
|
2011
|
+
el.innerHTML = `<p class="cred-state">${escapeHtml(body.hint || '暂无 A2A 消息。')}</p>`;
|
|
2012
|
+
return;
|
|
2013
|
+
}
|
|
2014
|
+
el.innerHTML = shownMessages.map((m) => {
|
|
2015
|
+
const arrow = m.direction === 'in' ? '← 收' : '→ 发';
|
|
2016
|
+
const kind = m.kind === 'task' ? '任务' : '消息';
|
|
2017
|
+
const status = m.status ? ` · ${m.status}` : '';
|
|
2018
|
+
const content = String(m.content || '');
|
|
2019
|
+
const preview = content.length > 160 ? `${content.slice(0, 160)}…` : content;
|
|
2020
|
+
return `<div class="channel-item" role="listitem">
|
|
2021
|
+
<div class="channel-item-main">
|
|
2022
|
+
<div class="channel-item-title">
|
|
2023
|
+
<strong>${escapeHtml(m.direction === 'in' ? m.peer : m.peer)}</strong>
|
|
2024
|
+
<span class="channel-type">${arrow} · ${kind}</span>
|
|
2025
|
+
<span class="channel-state warn">${escapeHtml(m.createdAt || '')}</span>
|
|
2026
|
+
</div>
|
|
2027
|
+
<p class="conn-block-desc">${escapeHtml(preview)}</p>
|
|
2028
|
+
<p class="conn-block-desc">${escapeHtml(status)}</p>
|
|
2029
|
+
</div>
|
|
2030
|
+
</div>`;
|
|
2031
|
+
}).join('');
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
// ── D25 U3: module install upload ───────────────────────────────
|
|
2035
|
+
|
|
2036
|
+
async function submitA2aInstall(basePath, btn) {
|
|
2037
|
+
const fileInput = document.getElementById('a2a-install-file');
|
|
2038
|
+
const feedback = document.getElementById('a2a-install-feedback');
|
|
2039
|
+
if (!fileInput || !btn || !feedback) return;
|
|
2040
|
+
const file = fileInput.files && fileInput.files[0];
|
|
2041
|
+
if (!file) {
|
|
2042
|
+
feedback.textContent = '请先选择要上传的模块包(.tgz)';
|
|
2043
|
+
feedback.className = 'a2a-feedback-error';
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
const original = btn.textContent;
|
|
2047
|
+
btn.disabled = true;
|
|
2048
|
+
btn.textContent = '上传安装中...';
|
|
2049
|
+
feedback.textContent = '';
|
|
2050
|
+
feedback.className = '';
|
|
2051
|
+
const fd = new FormData();
|
|
2052
|
+
fd.append('file', file);
|
|
2053
|
+
try {
|
|
2054
|
+
const { status, body } = await adminFetch(basePath, '/api/admin/a2a/install', { method: 'POST', body: fd });
|
|
2055
|
+
if (body.ok === true || body.success === true) {
|
|
2056
|
+
const version = body.version ? ` v${String(body.version).replace(/^v/, '')}` : '';
|
|
2057
|
+
feedback.textContent = `✅ 安装成功${version}`;
|
|
2058
|
+
feedback.className = 'a2a-feedback-ok';
|
|
2059
|
+
fileInput.value = '';
|
|
2060
|
+
setAdminMsg(`✅ baize-a2a 安装成功${version}`);
|
|
2061
|
+
loadA2aStatus(basePath); // 刷新版本/模式/健康
|
|
2062
|
+
} else {
|
|
2063
|
+
const msg = body.error || body.message || `HTTP ${status}`;
|
|
2064
|
+
feedback.textContent = `❌ 安装失败:${msg}`;
|
|
2065
|
+
feedback.className = 'a2a-feedback-error';
|
|
2066
|
+
setAdminMsg(`❌ 安装失败:${msg}`, true);
|
|
2067
|
+
}
|
|
2068
|
+
} catch (err) {
|
|
2069
|
+
feedback.textContent = `❌ 安装请求失败:${err.message}`;
|
|
2070
|
+
feedback.className = 'a2a-feedback-error';
|
|
2071
|
+
setAdminMsg(`❌ 安装请求失败:${err.message}`, true);
|
|
2072
|
+
} finally {
|
|
2073
|
+
btn.disabled = false;
|
|
2074
|
+
btn.textContent = original;
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
// ── D25 U3: config area (adminUrl / advertiseUrl → enable 复用现有配置能力) ──
|
|
2079
|
+
|
|
2080
|
+
async function submitA2aConfig(basePath, btn) {
|
|
2081
|
+
const adminInput = document.getElementById('a2a-config-admin-url');
|
|
2082
|
+
const advertiseInput = document.getElementById('a2a-config-advertise-url');
|
|
2083
|
+
const feedback = document.getElementById('a2a-config-feedback');
|
|
2084
|
+
if (!adminInput || !advertiseInput || !btn || !feedback) return;
|
|
2085
|
+
const adminUrl = adminInput.value.trim();
|
|
2086
|
+
const advertiseUrl = advertiseInput.value.trim();
|
|
2087
|
+
if (!adminUrl || !advertiseUrl) {
|
|
2088
|
+
feedback.textContent = '保存配置需要填写 Admin 服务地址与对外宣告地址';
|
|
2089
|
+
feedback.className = 'a2a-feedback-error';
|
|
2090
|
+
return;
|
|
2091
|
+
}
|
|
2092
|
+
const original = btn.textContent;
|
|
2093
|
+
btn.disabled = true;
|
|
2094
|
+
btn.textContent = '保存中...';
|
|
2095
|
+
feedback.textContent = '';
|
|
2096
|
+
feedback.className = '';
|
|
2097
|
+
try {
|
|
2098
|
+
const { status, body } = await adminFetch(basePath, '/api/admin/a2a/enable', {
|
|
2099
|
+
method: 'POST',
|
|
2100
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2101
|
+
body: JSON.stringify({ adminUrl, advertiseUrl }),
|
|
2102
|
+
});
|
|
2103
|
+
if (status === 401) { setAdminMsg('未登录或会话过期,请重新登录', true); return; }
|
|
2104
|
+
if (body.ok === true || body.success === true) {
|
|
2105
|
+
const reg = body.registrationId ? `,注册 ID ${body.registrationId}(${body.status || 'pending'},等待管理员审批)` : '';
|
|
2106
|
+
feedback.textContent = `✅ 配置已保存${body.mode === 'cluster' ? ',已切换集群模式' : ''}${reg}`;
|
|
2107
|
+
feedback.className = 'a2a-feedback-ok';
|
|
2108
|
+
setAdminMsg(`✅ A2A 配置已保存${body.agentId ? `:Agent ID ${body.agentId}` : ''}${reg}`);
|
|
2109
|
+
loadA2aStatus(basePath);
|
|
2110
|
+
} else {
|
|
2111
|
+
const msg = body.error || '未知错误';
|
|
2112
|
+
feedback.textContent = `❌ 保存失败:${msg}`;
|
|
2113
|
+
feedback.className = 'a2a-feedback-error';
|
|
2114
|
+
setAdminMsg(`❌ 保存失败:${msg}`, true);
|
|
2115
|
+
}
|
|
2116
|
+
} catch (err) {
|
|
2117
|
+
feedback.textContent = `❌ 保存失败:${err.message}`;
|
|
2118
|
+
feedback.className = 'a2a-feedback-error';
|
|
2119
|
+
setAdminMsg(`❌ 保存失败:${err.message}`, true);
|
|
2120
|
+
} finally {
|
|
2121
|
+
btn.disabled = false;
|
|
2122
|
+
btn.textContent = original;
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
// ── D25 U3: mode switch (单机 ↔ 集群) ──────────────────────────
|
|
2127
|
+
|
|
2128
|
+
async function switchA2aMode(basePath, btn) {
|
|
2129
|
+
if (!btn) return;
|
|
2130
|
+
const target = btn.dataset.targetMode === 'cluster' ? 'cluster' : 'standalone';
|
|
2131
|
+
const label = target === 'cluster' ? '集群' : '单机';
|
|
2132
|
+
const feedback = document.getElementById('a2a-mode-feedback');
|
|
2133
|
+
const confirmMsg = target === 'cluster'
|
|
2134
|
+
? '切换到集群模式?(启用并注册到 admin 集群,可被 peer 发现)'
|
|
2135
|
+
: '切换到单机模式?(停止注册与对外服务,仅保留本机运行)';
|
|
2136
|
+
if (!confirm(confirmMsg)) return;
|
|
2137
|
+
const original = btn.textContent;
|
|
2138
|
+
btn.disabled = true;
|
|
2139
|
+
btn.textContent = '切换中...';
|
|
2140
|
+
if (feedback) { feedback.textContent = ''; feedback.className = ''; }
|
|
2141
|
+
try {
|
|
2142
|
+
let bodyPayload = {};
|
|
2143
|
+
if (target === 'cluster') {
|
|
2144
|
+
// 集群模式复用配置区已填地址(未填则由后端报错提示)
|
|
2145
|
+
const adminUrl = (document.getElementById('a2a-config-admin-url') || {}).value || '';
|
|
2146
|
+
const advertiseUrl = (document.getElementById('a2a-config-advertise-url') || {}).value || '';
|
|
2147
|
+
if (adminUrl.trim() || advertiseUrl.trim()) bodyPayload = { adminUrl: adminUrl.trim(), advertiseUrl: advertiseUrl.trim() };
|
|
2148
|
+
}
|
|
2149
|
+
const { status, body } = await adminFetch(basePath, target === 'cluster' ? '/api/admin/a2a/enable' : '/api/admin/a2a/disable', {
|
|
2150
|
+
method: 'POST',
|
|
2151
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2152
|
+
body: JSON.stringify(bodyPayload),
|
|
2153
|
+
});
|
|
2154
|
+
if (status === 401) { setAdminMsg('未登录或会话过期,请重新登录', true); return; }
|
|
2155
|
+
if (body.ok === true || body.success === true) {
|
|
2156
|
+
if (feedback) { feedback.textContent = `✅ 已切换到${label}模式`; feedback.className = 'a2a-feedback-ok'; }
|
|
2157
|
+
setAdminMsg(`✅ A2A 已切换到${label}模式`);
|
|
2158
|
+
loadA2aStatus(basePath);
|
|
2159
|
+
loadA2aPeers(basePath);
|
|
2160
|
+
} else {
|
|
2161
|
+
const msg = body.error || '未知错误';
|
|
2162
|
+
if (feedback) { feedback.textContent = `❌ 切换失败:${msg}`; feedback.className = 'a2a-feedback-error'; }
|
|
2163
|
+
setAdminMsg(`❌ 切换失败:${msg}`, true);
|
|
2164
|
+
}
|
|
2165
|
+
} catch (err) {
|
|
2166
|
+
if (feedback) { feedback.textContent = `❌ 切换失败:${err.message}`; feedback.className = 'a2a-feedback-error'; }
|
|
2167
|
+
setAdminMsg(`❌ 切换失败:${err.message}`, true);
|
|
2168
|
+
} finally {
|
|
2169
|
+
btn.disabled = false;
|
|
2170
|
+
btn.textContent = original;
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
async function searchA2aFromAdmin(basePath, form, btn) {
|
|
2175
|
+
const q = document.getElementById('a2a-search-query').value.trim();
|
|
2176
|
+
const results = document.getElementById('a2a-search-results');
|
|
2177
|
+
if (!q) { setAdminMsg('请输入搜索关键字', true); return; }
|
|
2178
|
+
if (btn) { btn.disabled = true; }
|
|
2179
|
+
try {
|
|
2180
|
+
const { status, body } = await adminFetch(basePath, `/api/admin/a2a/search?q=${encodeURIComponent(q)}`);
|
|
2181
|
+
if (status !== 200) { results.innerHTML = `搜索失败:${escapeHtml(body.error || '')}`; return; }
|
|
2182
|
+
const agents = body.agents || [];
|
|
2183
|
+
if (!agents.length) { results.innerHTML = '未找到匹配 agent。'; return; }
|
|
2184
|
+
results.innerHTML = agents.map((a) => {
|
|
2185
|
+
const meta = [a.status, a.team, a.role].filter(Boolean).join(' · ');
|
|
2186
|
+
const labels = Array.isArray(a.labels) && a.labels.length ? ` · labels: ${a.labels.join(', ')}` : '';
|
|
2187
|
+
const skills = Array.isArray(a.skills) && a.skills.length ? ` · skills: ${a.skills.map((s) => (typeof s === 'string' ? s : s.id || s.name)).join(', ')}` : '';
|
|
2188
|
+
return `<div class="channel-item" role="listitem">
|
|
2189
|
+
<div class="channel-item-main">
|
|
2190
|
+
<div class="channel-item-title"><strong>${escapeHtml(a.agentId)}</strong>${a.name ? ` <span class="channel-type">${escapeHtml(a.name)}</span>` : ''}</div>
|
|
2191
|
+
<p class="conn-block-desc">${escapeHtml(meta)}${labels}${skills}</p>
|
|
2192
|
+
</div>
|
|
2193
|
+
</div>`;
|
|
2194
|
+
}).join('');
|
|
2195
|
+
} finally {
|
|
2196
|
+
if (btn) btn.disabled = false;
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
async function submitA2aSend(basePath) {
|
|
2201
|
+
const peer = document.getElementById('a2a-send-peer').value.trim();
|
|
2202
|
+
const text = document.getElementById('a2a-send-text').value.trim();
|
|
2203
|
+
if (!peer || !text) { setAdminMsg('发送消息需要填写 peer 与内容', true); return; }
|
|
2204
|
+
setAdminMsg(`发送消息给 ${peer}...`);
|
|
2205
|
+
const { status, body } = await adminFetch(basePath, '/api/admin/a2a/send', {
|
|
2206
|
+
method: 'POST',
|
|
2207
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2208
|
+
body: JSON.stringify({ peer, text }),
|
|
2209
|
+
});
|
|
2210
|
+
if (status === 401) { setAdminMsg('未登录或会话过期,请重新登录', true); return; }
|
|
2211
|
+
if (body.success) {
|
|
2212
|
+
setAdminMsg(`✅ 已发送:${body.status || 'accepted'}${body.requestId ? `(${body.requestId})` : ''}`);
|
|
2213
|
+
document.getElementById('a2a-send-text').value = '';
|
|
2214
|
+
loadA2aMessages(basePath);
|
|
2215
|
+
} else {
|
|
2216
|
+
setAdminMsg(`❌ 发送失败:${body.error || '未知错误'}`, true);
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
async function submitA2aTaskForm(basePath) {
|
|
2221
|
+
const peer = document.getElementById('a2a-task-peer').value.trim();
|
|
2222
|
+
const skill = document.getElementById('a2a-task-skill').value.trim() || undefined;
|
|
2223
|
+
const instruction = document.getElementById('a2a-task-instruction').value.trim();
|
|
2224
|
+
const isAsync = document.getElementById('a2a-task-async').checked;
|
|
2225
|
+
if (!peer || !instruction) { setAdminMsg('派发任务需要填写 peer 与指令', true); return; }
|
|
2226
|
+
setAdminMsg(`派发任务给 ${peer}${isAsync ? '(异步)' : '(同步,等待终态)'}...`);
|
|
2227
|
+
const { status, body } = await adminFetch(basePath, '/api/admin/a2a/task', {
|
|
2228
|
+
method: 'POST',
|
|
2229
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2230
|
+
body: JSON.stringify({ peer, skill, instruction, async: isAsync }),
|
|
2231
|
+
});
|
|
2232
|
+
if (status === 401) { setAdminMsg('未登录或会话过期,请重新登录', true); return; }
|
|
2233
|
+
if (body.success) {
|
|
2234
|
+
const tail = body.status === 'completed' ? `,结果:${String(body.result || '').slice(0, 200)}` : '';
|
|
2235
|
+
setAdminMsg(`✅ 任务已派发:${body.taskId}(${body.status})${body.error ? `,错误:${body.error}` : ''}${tail}`);
|
|
2236
|
+
document.getElementById('a2a-task-instruction').value = '';
|
|
2237
|
+
loadA2aTasks(basePath);
|
|
2238
|
+
loadA2aMessages(basePath);
|
|
2239
|
+
} else {
|
|
2240
|
+
setAdminMsg(`❌ 派发失败:${body.error || '未知错误'}`, true);
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
|
|
1385
2244
|
function showAppView(basePath, view) {
|
|
1386
2245
|
const chatView = document.getElementById('chat-view');
|
|
1387
2246
|
const modelView = document.getElementById('model-view');
|
|
1388
2247
|
const channelsView = document.getElementById('channels-view');
|
|
2248
|
+
const a2aView = document.getElementById('a2a-view');
|
|
1389
2249
|
const navChat = document.getElementById('nav-chat');
|
|
1390
2250
|
const navModel = document.getElementById('nav-model');
|
|
1391
2251
|
const navChannels = document.getElementById('nav-channels');
|
|
1392
|
-
|
|
2252
|
+
const navA2a = document.getElementById('nav-a2a');
|
|
2253
|
+
if (!chatView || !modelView || !channelsView || !a2aView) return;
|
|
1393
2254
|
const setNav = (active, others) => {
|
|
1394
2255
|
active.classList.add('active');
|
|
1395
2256
|
active.setAttribute('aria-current', 'page');
|
|
@@ -1401,20 +2262,30 @@ function showAppView(basePath, view) {
|
|
|
1401
2262
|
if (view === 'model') {
|
|
1402
2263
|
chatView.hidden = true;
|
|
1403
2264
|
channelsView.hidden = true;
|
|
2265
|
+
a2aView.hidden = true;
|
|
1404
2266
|
modelView.hidden = false;
|
|
1405
|
-
setNav(navModel, [navChat, navChannels]);
|
|
2267
|
+
setNav(navModel, [navChat, navChannels, navA2a]);
|
|
1406
2268
|
renderModelSettings(basePath);
|
|
1407
2269
|
} else if (view === 'channels') {
|
|
1408
2270
|
chatView.hidden = true;
|
|
1409
2271
|
modelView.hidden = true;
|
|
2272
|
+
a2aView.hidden = true;
|
|
1410
2273
|
channelsView.hidden = false;
|
|
1411
|
-
setNav(navChannels, [navChat, navModel]);
|
|
2274
|
+
setNav(navChannels, [navChat, navModel, navA2a]);
|
|
1412
2275
|
renderChannels(basePath);
|
|
2276
|
+
} else if (view === 'a2a') {
|
|
2277
|
+
chatView.hidden = true;
|
|
2278
|
+
modelView.hidden = true;
|
|
2279
|
+
channelsView.hidden = true;
|
|
2280
|
+
a2aView.hidden = false;
|
|
2281
|
+
setNav(navA2a, [navChat, navModel, navChannels]);
|
|
2282
|
+
renderA2a(basePath);
|
|
1413
2283
|
} else {
|
|
1414
2284
|
modelView.hidden = true;
|
|
1415
2285
|
channelsView.hidden = true;
|
|
2286
|
+
a2aView.hidden = true;
|
|
1416
2287
|
chatView.hidden = false;
|
|
1417
|
-
setNav(navChat, [navModel, navChannels]);
|
|
2288
|
+
setNav(navChat, [navModel, navChannels, navA2a]);
|
|
1418
2289
|
ensureChatEmptyState(basePath);
|
|
1419
2290
|
updateChatHint(basePath);
|
|
1420
2291
|
refreshRuntimeLabel(basePath);
|
|
@@ -1575,6 +2446,7 @@ function initViews(basePath) {
|
|
|
1575
2446
|
document.getElementById('nav-chat').addEventListener('click', () => showAppView(basePath, 'chat'));
|
|
1576
2447
|
document.getElementById('nav-model').addEventListener('click', () => showAppView(basePath, 'model'));
|
|
1577
2448
|
document.getElementById('nav-channels').addEventListener('click', () => showAppView(basePath, 'channels'));
|
|
2449
|
+
document.getElementById('nav-a2a').addEventListener('click', () => showAppView(basePath, 'a2a'));
|
|
1578
2450
|
|
|
1579
2451
|
// D8 two-column layout: Claude official = setup-token form; custom API =
|
|
1580
2452
|
// per-column quick forms. The standalone api-key forms were removed.
|
|
@@ -1614,6 +2486,30 @@ function initViews(basePath) {
|
|
|
1614
2486
|
if (channelsCatalogue) bindChannelCatalogue(channelsCatalogue);
|
|
1615
2487
|
document.getElementById('btn-activate-official').addEventListener('click', () => activateProviderFromAdmin(basePath, 'official'));
|
|
1616
2488
|
document.getElementById('btn-activate-official-openai').addEventListener('click', () => activateProviderFromAdmin(basePath, 'official-openai'));
|
|
2489
|
+
|
|
2490
|
+
// A2A view
|
|
2491
|
+
document.getElementById('form-a2a-install').addEventListener('submit', (e) => {
|
|
2492
|
+
e.preventDefault();
|
|
2493
|
+
submitA2aInstall(basePath, e.target.querySelector('button[type="submit"]'));
|
|
2494
|
+
});
|
|
2495
|
+
document.getElementById('form-a2a-config').addEventListener('submit', (e) => {
|
|
2496
|
+
e.preventDefault();
|
|
2497
|
+
submitA2aConfig(basePath, e.target.querySelector('button[type="submit"]'));
|
|
2498
|
+
});
|
|
2499
|
+
document.getElementById('btn-a2a-switch-mode').addEventListener('click', (e) => switchA2aMode(basePath, e.target));
|
|
2500
|
+
document.getElementById('btn-a2a-edit-card').addEventListener('click', () => openAgentCardEditor(basePath));
|
|
2501
|
+
document.getElementById('form-a2a-search').addEventListener('submit', (e) => {
|
|
2502
|
+
e.preventDefault();
|
|
2503
|
+
searchA2aFromAdmin(basePath, e.target, e.target.querySelector('button[type="submit"]'));
|
|
2504
|
+
});
|
|
2505
|
+
document.getElementById('form-a2a-send').addEventListener('submit', (e) => {
|
|
2506
|
+
e.preventDefault();
|
|
2507
|
+
submitA2aSend(basePath);
|
|
2508
|
+
});
|
|
2509
|
+
document.getElementById('form-a2a-task').addEventListener('submit', (e) => {
|
|
2510
|
+
e.preventDefault();
|
|
2511
|
+
submitA2aTaskForm(basePath);
|
|
2512
|
+
});
|
|
1617
2513
|
}
|
|
1618
2514
|
|
|
1619
2515
|
async function determineConfigured(basePath) {
|