ec-web 0.0.6 → 0.0.8
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/fs-utils.js +89 -65
- package/dist/paths.js +2 -1
- package/dist/sources/msg.js +35 -26
- package/dist/sources/stats.js +1 -1
- package/dist/sources/triggers.js +27 -0
- package/dist/static/app.js +184 -8
- package/dist/static/index.html +1 -1
- package/dist/static/style.css +50 -0
- package/package.json +1 -1
package/dist/fs-utils.js
CHANGED
|
@@ -14,72 +14,104 @@ export function encodeSegment(s) {
|
|
|
14
14
|
export function decodeDirSegment(seg) {
|
|
15
15
|
return seg.replace(/%([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
16
16
|
}
|
|
17
|
+
function encodeChannelSessionComponent(value) {
|
|
18
|
+
const encoded = encodeURIComponent(value).replace(/\*/g, '%2A');
|
|
19
|
+
return encoded.endsWith('.') ? `${encoded.slice(0, -1)}%2E` : encoded;
|
|
20
|
+
}
|
|
21
|
+
function formatChannelSessionKeyParts(channelType, channelName, channelId) {
|
|
22
|
+
if (!channelType || !channelName || !channelId)
|
|
23
|
+
throw new Error('channel session key components must not be empty');
|
|
24
|
+
const key = [channelType, channelName, channelId].map(encodeChannelSessionComponent).join('#');
|
|
25
|
+
if (Buffer.byteLength(key, 'utf8') > 255)
|
|
26
|
+
throw new Error('channel session key exceeds the 255-byte filesystem limit');
|
|
27
|
+
return key;
|
|
28
|
+
}
|
|
29
|
+
export function formatChannelSessionKey(channelType, channelId, channelKey) {
|
|
30
|
+
const parts = channelKey?.split('#');
|
|
31
|
+
const channelName = parts?.length === 3 && parts[0] === channelType && parts[2]?.trim()
|
|
32
|
+
? parts[2].trim()
|
|
33
|
+
: 'main';
|
|
34
|
+
return formatChannelSessionKeyParts(channelType, channelName, channelId);
|
|
35
|
+
}
|
|
36
|
+
export function tryParseChannelSessionKey(key) {
|
|
37
|
+
const parts = key.split('#');
|
|
38
|
+
if (parts.length !== 3 || parts.some(part => !part))
|
|
39
|
+
return undefined;
|
|
40
|
+
try {
|
|
41
|
+
const parsed = {
|
|
42
|
+
channelType: decodeURIComponent(parts[0]),
|
|
43
|
+
channelName: decodeURIComponent(parts[1]),
|
|
44
|
+
channelId: decodeURIComponent(parts[2]),
|
|
45
|
+
};
|
|
46
|
+
if (!parsed.channelType || !parsed.channelName || !parsed.channelId)
|
|
47
|
+
return undefined;
|
|
48
|
+
return formatChannelSessionKeyParts(parsed.channelType, parsed.channelName, parsed.channelId) === key
|
|
49
|
+
? parsed
|
|
50
|
+
: undefined;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
17
56
|
/** 与 evolcore cross-platform.ts encodePath 一致(CC transcript 目录名) */
|
|
18
57
|
export function encodePath(p) {
|
|
19
58
|
const isWindows = process.platform === 'win32';
|
|
20
59
|
const norm = isWindows ? p.replace(/\\/g, '/') : p;
|
|
21
60
|
return encodeURIComponent(norm).replace(/%2F/gi, '-').replace(/%3A/gi, '-').replace(/^-/, '');
|
|
22
61
|
}
|
|
62
|
+
function hasSessionPayload(dirPath) {
|
|
63
|
+
try {
|
|
64
|
+
return fs.readdirSync(dirPath, { withFileTypes: true }).some(entry => (entry.isFile() && (entry.name === 'active.json'
|
|
65
|
+
|| entry.name === 'messages.jsonl'
|
|
66
|
+
|| (entry.name.startsWith('meta_') && entry.name.endsWith('.jsonl')))) || (entry.isDirectory() && entry.name === '_threads'));
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
23
72
|
export function scanChatDirs(sessionsDir) {
|
|
24
73
|
const results = [];
|
|
25
|
-
let
|
|
74
|
+
let agents;
|
|
26
75
|
try {
|
|
27
|
-
|
|
76
|
+
agents = fs.readdirSync(sessionsDir, { withFileTypes: true });
|
|
28
77
|
}
|
|
29
78
|
catch {
|
|
30
79
|
return results;
|
|
31
80
|
}
|
|
32
|
-
for (const
|
|
33
|
-
if (!
|
|
81
|
+
for (const agent of agents) {
|
|
82
|
+
if (!agent.isDirectory())
|
|
34
83
|
continue;
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
let
|
|
84
|
+
const selfAID = decodeDirSegment(agent.name);
|
|
85
|
+
const agentSessions = path.join(sessionsDir, agent.name, 'sessions');
|
|
86
|
+
let sessions;
|
|
38
87
|
try {
|
|
39
|
-
|
|
88
|
+
sessions = fs.readdirSync(agentSessions, { withFileTypes: true });
|
|
40
89
|
}
|
|
41
90
|
catch {
|
|
42
91
|
continue;
|
|
43
92
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
channelId: decodeDirSegment(peerEnc.name),
|
|
64
|
-
selfAID,
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
else {
|
|
70
|
-
for (const chanEnc of level2) {
|
|
71
|
-
if (!chanEnc.isDirectory())
|
|
72
|
-
continue;
|
|
73
|
-
results.push({
|
|
74
|
-
dirPath: path.join(ctDir, chanEnc.name),
|
|
75
|
-
channelType,
|
|
76
|
-
channelId: decodeDirSegment(chanEnc.name),
|
|
77
|
-
selfAID: '',
|
|
78
|
-
});
|
|
79
|
-
}
|
|
93
|
+
for (const session of sessions) {
|
|
94
|
+
if (!session.isDirectory())
|
|
95
|
+
continue;
|
|
96
|
+
const parsed = tryParseChannelSessionKey(session.name);
|
|
97
|
+
if (!parsed)
|
|
98
|
+
continue;
|
|
99
|
+
const dirPath = path.join(agentSessions, session.name);
|
|
100
|
+
if (!hasSessionPayload(dirPath))
|
|
101
|
+
continue;
|
|
102
|
+
results.push({
|
|
103
|
+
dirPath,
|
|
104
|
+
channelType: parsed.channelType,
|
|
105
|
+
channelKey: parsed.channelType === 'daemon'
|
|
106
|
+
? 'daemon'
|
|
107
|
+
: `${parsed.channelType}#${selfAID}#${parsed.channelName}`,
|
|
108
|
+
channelId: parsed.channelId,
|
|
109
|
+
channelSessionKey: session.name,
|
|
110
|
+
selfAID,
|
|
111
|
+
});
|
|
80
112
|
}
|
|
81
113
|
}
|
|
82
|
-
return results;
|
|
114
|
+
return results.sort((left, right) => left.dirPath < right.dirPath ? -1 : left.dirPath > right.dirPath ? 1 : 0);
|
|
83
115
|
}
|
|
84
116
|
export function readJsonFile(filePath) {
|
|
85
117
|
try {
|
|
@@ -123,34 +155,26 @@ export function isTransientProtocolMessage(entry) {
|
|
|
123
155
|
return transient(entry.payloadType) || transient(entry.msgType);
|
|
124
156
|
}
|
|
125
157
|
export function getSessionsAunDir() {
|
|
126
|
-
return
|
|
158
|
+
return resolvePaths().sessionsDir;
|
|
127
159
|
}
|
|
128
160
|
export function listLocalAids(aunDir) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
.
|
|
132
|
-
.map(e => decodeDirSegment(e.name));
|
|
133
|
-
}
|
|
134
|
-
catch {
|
|
135
|
-
return [];
|
|
136
|
-
}
|
|
161
|
+
return [...new Set(scanChatDirs(aunDir)
|
|
162
|
+
.filter(chat => chat.channelType === 'aun')
|
|
163
|
+
.map(chat => chat.selfAID))].sort();
|
|
137
164
|
}
|
|
138
165
|
export function listPeers(aunDir, localAid) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
catch {
|
|
146
|
-
return [];
|
|
147
|
-
}
|
|
166
|
+
return [...new Set(scanChatDirs(aunDir)
|
|
167
|
+
.filter(chat => chat.channelType === 'aun' && chat.selfAID === localAid)
|
|
168
|
+
.map(chat => chat.channelId))].sort();
|
|
169
|
+
}
|
|
170
|
+
function aunChatDir(aunDir, localAid, peerId) {
|
|
171
|
+
return path.join(aunDir, encodeSegment(localAid), 'sessions', formatChannelSessionKey('aun', peerId, `aun#${localAid}#main`));
|
|
148
172
|
}
|
|
149
173
|
export function readMessages(aunDir, localAid, peerId) {
|
|
150
|
-
return readAllJsonlLines(path.join(aunDir,
|
|
174
|
+
return readAllJsonlLines(path.join(aunChatDir(aunDir, localAid, peerId), 'messages.jsonl')).filter(m => !isTransientProtocolMessage(m));
|
|
151
175
|
}
|
|
152
176
|
function readPeerName(aunDir, localAid, peerId) {
|
|
153
|
-
return readJsonFile(path.join(aunDir,
|
|
177
|
+
return readJsonFile(path.join(aunChatDir(aunDir, localAid, peerId), 'active.json'))?.metadata?.peerName ?? null;
|
|
154
178
|
}
|
|
155
179
|
export function loadAidInfo(aunDir, aid) {
|
|
156
180
|
const peers = listPeers(aunDir, aid);
|
package/dist/paths.js
CHANGED
|
@@ -42,7 +42,8 @@ export function resolvePaths() {
|
|
|
42
42
|
const root = resolveRoot();
|
|
43
43
|
return {
|
|
44
44
|
root,
|
|
45
|
-
|
|
45
|
+
daemonConfig: path.join(root, 'daemon.json'),
|
|
46
|
+
sessionsDir: path.join(root, 'agents'),
|
|
46
47
|
instanceDir: path.join(root, 'data', 'instance'),
|
|
47
48
|
dataDir: path.join(root, 'data'),
|
|
48
49
|
logs: path.join(root, 'logs'),
|
package/dist/sources/msg.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 消息数据源:扫描
|
|
2
|
+
* 消息数据源:扫描 agents/<aid>/sessions 下所有渠道的 messages.jsonl。
|
|
3
3
|
*
|
|
4
4
|
* 左列按 agent 聚合;中列列出该 agent 下所有渠道的私聊/群聊会话。
|
|
5
5
|
* 兼容旧参数名 aid/peer:前端的 aid 实际上传 scope id。
|
|
@@ -9,7 +9,7 @@ import path from 'path';
|
|
|
9
9
|
import { ipcQuery } from '../ipc-client.js';
|
|
10
10
|
import { resolvePaths } from '../paths.js';
|
|
11
11
|
import { resolveParentDistModule, toFileUrl } from './parent-package.js';
|
|
12
|
-
import { isTransientProtocolMessage, readAllJsonlLines, readJsonFile, } from '../fs-utils.js';
|
|
12
|
+
import { decodeDirSegment, isTransientProtocolMessage, readAllJsonlLines, readJsonFile, scanChatDirs, tryParseChannelSessionKey, } from '../fs-utils.js';
|
|
13
13
|
let contextModulesPromise = null;
|
|
14
14
|
const sessionAdapterPromises = new Map();
|
|
15
15
|
let peerDisplayResolverPromise = null;
|
|
@@ -201,26 +201,10 @@ function getSessionAdapter(baseagent) {
|
|
|
201
201
|
function safeId(value) {
|
|
202
202
|
return Buffer.from(value, 'utf8').toString('base64url');
|
|
203
203
|
}
|
|
204
|
-
function findMessageFiles(root) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
try {
|
|
209
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
210
|
-
}
|
|
211
|
-
catch {
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
for (const entry of entries) {
|
|
215
|
-
const full = path.join(dir, entry.name);
|
|
216
|
-
if (entry.isDirectory())
|
|
217
|
-
visit(full);
|
|
218
|
-
else if (entry.isFile() && entry.name === 'messages.jsonl')
|
|
219
|
-
out.push(full);
|
|
220
|
-
}
|
|
221
|
-
};
|
|
222
|
-
visit(root);
|
|
223
|
-
return out;
|
|
204
|
+
export function findMessageFiles(root) {
|
|
205
|
+
return scanChatDirs(root)
|
|
206
|
+
.map(chat => path.join(chat.dirPath, 'messages.jsonl'))
|
|
207
|
+
.filter(file => fs.existsSync(file));
|
|
224
208
|
}
|
|
225
209
|
function fileSignature(file) {
|
|
226
210
|
try {
|
|
@@ -488,9 +472,6 @@ export async function getAgentSelfNames() {
|
|
|
488
472
|
agentDisplayNamesCache = { expiresAt: Date.now() + 5000, names };
|
|
489
473
|
return names;
|
|
490
474
|
}
|
|
491
|
-
function decodeDirSegment(seg) {
|
|
492
|
-
return seg.replace(/%([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
493
|
-
}
|
|
494
475
|
function parseChannelKey(value) {
|
|
495
476
|
if (typeof value !== 'string')
|
|
496
477
|
return null;
|
|
@@ -515,10 +496,38 @@ function parseLegacySessionKey(value, channelType) {
|
|
|
515
496
|
return null;
|
|
516
497
|
return { type: parts[0], selfAID: parts[1], name: parts[2] || 'main' };
|
|
517
498
|
}
|
|
518
|
-
function pathIdentity(sessionsDir, dirPath) {
|
|
499
|
+
export function pathIdentity(sessionsDir, dirPath) {
|
|
519
500
|
const relParts = path.relative(sessionsDir, dirPath).split(path.sep).filter(Boolean);
|
|
520
501
|
if (!relParts.length)
|
|
521
502
|
return {};
|
|
503
|
+
// Current layout: agents/<aid>/sessions/<channelSessionKey>/
|
|
504
|
+
if (relParts.length === 3 && relParts[1] === 'sessions') {
|
|
505
|
+
const selfAID = decodeDirSegment(relParts[0]);
|
|
506
|
+
const parsed = tryParseChannelSessionKey(relParts[2]);
|
|
507
|
+
if (parsed) {
|
|
508
|
+
return {
|
|
509
|
+
channelType: parsed.channelType,
|
|
510
|
+
channel: parsed.channelType === 'daemon'
|
|
511
|
+
? 'daemon'
|
|
512
|
+
: `${parsed.channelType}#${selfAID}#${parsed.channelName}`,
|
|
513
|
+
channelId: parsed.channelId,
|
|
514
|
+
selfAID,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
// Immediately preceding Agent layout, retained for inspecting backups.
|
|
519
|
+
if (relParts.length >= 4 && relParts[1] === 'sessions') {
|
|
520
|
+
const selfAID = decodeDirSegment(relParts[0]);
|
|
521
|
+
const channelKey = decodeDirSegment(relParts[2]);
|
|
522
|
+
const parsed = parseChannelKey(channelKey);
|
|
523
|
+
return {
|
|
524
|
+
channelType: parsed?.type || channelKey,
|
|
525
|
+
channel: channelKey,
|
|
526
|
+
channelId: decodeDirSegment(relParts[3]),
|
|
527
|
+
selfAID: parsed?.selfAID || selfAID,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
// Pre-migration path fallback for viewing a backup directory only.
|
|
522
531
|
const firstKey = parseChannelKey(decodeDirSegment(relParts[0]));
|
|
523
532
|
if (firstKey) {
|
|
524
533
|
return {
|
package/dist/sources/stats.js
CHANGED
|
@@ -447,7 +447,7 @@ function getPeerInfo(peerKey) {
|
|
|
447
447
|
if (target.startsWith('group.')) {
|
|
448
448
|
// 群聊:读取群信息
|
|
449
449
|
const { sessionsDir } = resolvePaths();
|
|
450
|
-
const groupDir = path.join(sessionsDir, '
|
|
450
|
+
const groupDir = path.join(sessionsDir, encodeSegment(agentAid), 'sessions', encodeSegment(`aun#${agentAid}#main`), encodeSegment(target));
|
|
451
451
|
const activeJsonPath = path.join(groupDir, 'active.json');
|
|
452
452
|
if (fs.existsSync(activeJsonPath)) {
|
|
453
453
|
const activeData = JSON.parse(fs.readFileSync(activeJsonPath, 'utf-8'));
|
package/dist/sources/triggers.js
CHANGED
|
@@ -24,6 +24,20 @@ function readTriggerControlToken(instanceDir) {
|
|
|
24
24
|
return undefined;
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* The system upgrade trigger belongs to the daemon/control AID, which is not
|
|
29
|
+
* returned by the regular self-agent menu. Read the process-level config so
|
|
30
|
+
* the Triggers view can include that scheduler owner in its agent selector.
|
|
31
|
+
*/
|
|
32
|
+
function readDaemonOwnerAid(p) {
|
|
33
|
+
try {
|
|
34
|
+
const raw = JSON.parse(fs.readFileSync(p.daemonConfig, 'utf8'));
|
|
35
|
+
return typeof raw?.aid === 'string' && raw.aid.trim() ? raw.aid.trim() : '__daemon__';
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
27
41
|
function normalizeAgent(item) {
|
|
28
42
|
const aid = String(item?.value || item?.aid || '').trim();
|
|
29
43
|
if (!aid)
|
|
@@ -102,6 +116,19 @@ async function buildSnapshot(params) {
|
|
|
102
116
|
const agentInfos = agents
|
|
103
117
|
.map((item) => normalizeAgent({ ...runtimeByAid.get(String(item?.value || item?.aid || '')), ...item }))
|
|
104
118
|
.filter((x) => !!x);
|
|
119
|
+
const controlAid = readDaemonOwnerAid(p);
|
|
120
|
+
if (controlAid) {
|
|
121
|
+
const daemonOwnerAid = agentInfos.some(agentInfo => agentInfo.aid === controlAid)
|
|
122
|
+
? `${controlAid}#daemon`
|
|
123
|
+
: controlAid;
|
|
124
|
+
if (!agentInfos.some(agentInfo => agentInfo.aid === daemonOwnerAid)) {
|
|
125
|
+
agentInfos.push({
|
|
126
|
+
aid: daemonOwnerAid,
|
|
127
|
+
label: `${daemonOwnerAid} (daemon)`,
|
|
128
|
+
channels: ['daemon'],
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
105
132
|
if (agent) {
|
|
106
133
|
const found = agentInfos.find((x) => x.aid === agent);
|
|
107
134
|
const triggers = await listTriggersForAgent(agent, found?.label || agent);
|
package/dist/static/app.js
CHANGED
|
@@ -139,7 +139,8 @@ const translations = {
|
|
|
139
139
|
'agents.daemonStopped': '⚠ Daemon 主进程未运行,仅显示最近活动记录',
|
|
140
140
|
'agents.empty.disabled': '暂无禁用 Agent',
|
|
141
141
|
'agents.empty.enabled': '暂无启用 Agent',
|
|
142
|
-
'agents.stats.gateway': 'Gateway',
|
|
142
|
+
'agents.stats.gateway': 'Daemon Gateway',
|
|
143
|
+
'agents.stats.openGatewayDrawer': '查看全部 AUN 网关',
|
|
143
144
|
'agents.stats.aids': 'AIDs',
|
|
144
145
|
'agents.stats.total': 'total',
|
|
145
146
|
'agents.stats.online': '在线',
|
|
@@ -148,6 +149,13 @@ const translations = {
|
|
|
148
149
|
'agents.stats.version': 'Version',
|
|
149
150
|
'agents.stats.pid': 'PID',
|
|
150
151
|
'agents.stats.uptime': 'Uptime',
|
|
152
|
+
'agents.gatewayDrawer.title': 'AUN 网关',
|
|
153
|
+
'agents.gatewayDrawer.control': 'Daemon 控制 AID',
|
|
154
|
+
'agents.gatewayDrawer.agent': 'Agent',
|
|
155
|
+
'agents.gatewayDrawer.unresolved': '等待发现网关',
|
|
156
|
+
'agents.gatewayDrawer.empty': '暂无 AUN 连接状态',
|
|
157
|
+
'agents.gatewayDrawer.reconnects': '重连 {count}',
|
|
158
|
+
'agents.gatewayDrawer.connectedFor': '已连接 {duration}',
|
|
151
159
|
|
|
152
160
|
// Agent table headers
|
|
153
161
|
'agents.th.agent': 'Agent',
|
|
@@ -1019,7 +1027,8 @@ const translations = {
|
|
|
1019
1027
|
'agents.daemonStopped': '⚠ Daemon process is not running, showing recent activity only',
|
|
1020
1028
|
'agents.empty.disabled': 'No disabled agents',
|
|
1021
1029
|
'agents.empty.enabled': 'No enabled agents',
|
|
1022
|
-
'agents.stats.gateway': 'Gateway',
|
|
1030
|
+
'agents.stats.gateway': 'Daemon Gateway',
|
|
1031
|
+
'agents.stats.openGatewayDrawer': 'View all AUN gateways',
|
|
1023
1032
|
'agents.stats.aids': 'AIDs',
|
|
1024
1033
|
'agents.stats.total': 'total',
|
|
1025
1034
|
'agents.stats.online': 'online',
|
|
@@ -1028,6 +1037,13 @@ const translations = {
|
|
|
1028
1037
|
'agents.stats.version': 'Version',
|
|
1029
1038
|
'agents.stats.pid': 'PID',
|
|
1030
1039
|
'agents.stats.uptime': 'Uptime',
|
|
1040
|
+
'agents.gatewayDrawer.title': 'AUN Gateways',
|
|
1041
|
+
'agents.gatewayDrawer.control': 'Daemon control AID',
|
|
1042
|
+
'agents.gatewayDrawer.agent': 'Agent',
|
|
1043
|
+
'agents.gatewayDrawer.unresolved': 'Gateway discovery pending',
|
|
1044
|
+
'agents.gatewayDrawer.empty': 'No AUN connection status available',
|
|
1045
|
+
'agents.gatewayDrawer.reconnects': '{count} reconnects',
|
|
1046
|
+
'agents.gatewayDrawer.connectedFor': 'connected {duration}',
|
|
1031
1047
|
|
|
1032
1048
|
// Agent table headers
|
|
1033
1049
|
'agents.th.agent': 'Agent',
|
|
@@ -2469,6 +2485,7 @@ function compareVer(a, b) {
|
|
|
2469
2485
|
// 逐 AID 异步操作状态:aid → 操作中的描述文字
|
|
2470
2486
|
const _agentOps = new Map(); // Map<aid, string>
|
|
2471
2487
|
let _agSubtab = 'enabled'; // 'enabled' | 'disabled'
|
|
2488
|
+
let _gatewayDrawer = null;
|
|
2472
2489
|
|
|
2473
2490
|
// 工作状态徽标:一旦收到过消息就不再回 connected。
|
|
2474
2491
|
// stopped → connected(仅首次连接无消息时) → idle(收到第一条后) → working → idle ...
|
|
@@ -2640,7 +2657,138 @@ function initMsgTipFloat() {
|
|
|
2640
2657
|
window.addEventListener('scroll', hideNow, true);
|
|
2641
2658
|
}
|
|
2642
2659
|
|
|
2643
|
-
|
|
2660
|
+
function gatewayConnectionStatus(entry) {
|
|
2661
|
+
const status = String(entry?.status || '').trim().toLowerCase();
|
|
2662
|
+
if (status) return status;
|
|
2663
|
+
return entry?.connected === true ? 'connected' : 'disconnected';
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
function gatewayConnectionStatusClass(entry) {
|
|
2667
|
+
const status = gatewayConnectionStatus(entry);
|
|
2668
|
+
if (status === 'connected') return 'on';
|
|
2669
|
+
if (status === 'connecting' || status === 'reconnecting') return 'idle';
|
|
2670
|
+
return 'off';
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2673
|
+
function gatewayConnectionStatusLabel(entry) {
|
|
2674
|
+
const status = gatewayConnectionStatus(entry);
|
|
2675
|
+
if (status === 'connected') return t('status.connected');
|
|
2676
|
+
if (status === 'connecting') return t('status.connecting');
|
|
2677
|
+
if (status === 'reconnecting') return t('status.reconnecting');
|
|
2678
|
+
if (status === 'disconnected') return t('status.disconnected');
|
|
2679
|
+
return status || t('common.unknown');
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
function gatewayConnectionUrl(entry) {
|
|
2683
|
+
return typeof entry?.gatewayUrl === 'string' ? entry.gatewayUrl.trim() : '';
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
function gatewayConnections(data, aids) {
|
|
2687
|
+
const connections = [];
|
|
2688
|
+
const control = data?.status?.controlAid;
|
|
2689
|
+
if (control?.aid) connections.push({ ...control, role: 'control' });
|
|
2690
|
+
for (const aid of Array.isArray(aids) ? aids : []) {
|
|
2691
|
+
if (aid?.aid) connections.push({ ...aid, role: 'agent' });
|
|
2692
|
+
}
|
|
2693
|
+
return connections;
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
function groupGatewayConnections(data, aids) {
|
|
2697
|
+
const groups = [];
|
|
2698
|
+
const byGateway = new Map();
|
|
2699
|
+
for (const connection of gatewayConnections(data, aids)) {
|
|
2700
|
+
const url = gatewayConnectionUrl(connection);
|
|
2701
|
+
const key = url || '__unresolved__';
|
|
2702
|
+
let group = byGateway.get(key);
|
|
2703
|
+
if (!group) {
|
|
2704
|
+
group = { url, connections: [] };
|
|
2705
|
+
byGateway.set(key, group);
|
|
2706
|
+
groups.push(group);
|
|
2707
|
+
}
|
|
2708
|
+
group.connections.push(connection);
|
|
2709
|
+
}
|
|
2710
|
+
return groups;
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
function gatewayConnectionHtml(connection) {
|
|
2714
|
+
const status = gatewayConnectionStatusLabel(connection);
|
|
2715
|
+
const meta = [status];
|
|
2716
|
+
if (gatewayConnectionStatus(connection) === 'connected' && connection.lastConnectedAt) {
|
|
2717
|
+
meta.push(tf('agents.gatewayDrawer.connectedFor', {
|
|
2718
|
+
duration: fmtDur((Date.now() - connection.lastConnectedAt) / 1000),
|
|
2719
|
+
}));
|
|
2720
|
+
}
|
|
2721
|
+
if (connection.reconnectCount) {
|
|
2722
|
+
meta.push(tf('agents.gatewayDrawer.reconnects', { count: connection.reconnectCount }));
|
|
2723
|
+
}
|
|
2724
|
+
const role = connection.role === 'control'
|
|
2725
|
+
? t('agents.gatewayDrawer.control')
|
|
2726
|
+
: t('agents.gatewayDrawer.agent');
|
|
2727
|
+
const error = connection.lastError
|
|
2728
|
+
? `<div class="gateway-connection-error">${esc(connection.lastError)}</div>`
|
|
2729
|
+
: '';
|
|
2730
|
+
return `<div class="gateway-connection">` +
|
|
2731
|
+
`<span class="dot ${gatewayConnectionStatusClass(connection)}" title="${esc(status)}"></span>` +
|
|
2732
|
+
'<div class="gateway-connection-main">' +
|
|
2733
|
+
`<div class="gateway-connection-id"><span class="gateway-connection-role">${esc(role)}</span><code>${esc(connection.aid)}</code></div>` +
|
|
2734
|
+
`<div class="gateway-connection-meta">${meta.map(esc).join(' · ')}</div>` +
|
|
2735
|
+
error +
|
|
2736
|
+
'</div></div>';
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
function gatewayDrawerContentHtml(data, aids) {
|
|
2740
|
+
const groups = groupGatewayConnections(data, aids);
|
|
2741
|
+
if (!groups.length) return `<div class="empty">${t('agents.gatewayDrawer.empty')}</div>`;
|
|
2742
|
+
return groups.map(group => {
|
|
2743
|
+
const connected = group.connections.filter(connection => gatewayConnectionStatus(connection) === 'connected').length;
|
|
2744
|
+
const url = group.url || t('agents.gatewayDrawer.unresolved');
|
|
2745
|
+
return '<section class="gateway-group">' +
|
|
2746
|
+
'<header class="gateway-group-head">' +
|
|
2747
|
+
`<code class="gateway-group-url" title="${esc(url)}">${esc(url)}</code>` +
|
|
2748
|
+
`<span class="gateway-group-count">${connected}/${group.connections.length} ${t('agents.stats.online')}</span>` +
|
|
2749
|
+
'</header>' +
|
|
2750
|
+
`<div class="gateway-connection-list">${group.connections.map(gatewayConnectionHtml).join('')}</div>` +
|
|
2751
|
+
'</section>';
|
|
2752
|
+
}).join('');
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
function closeGatewayDrawer() {
|
|
2756
|
+
const ctx = _gatewayDrawer;
|
|
2757
|
+
if (!ctx) return;
|
|
2758
|
+
document.removeEventListener('keydown', ctx.onKeydown);
|
|
2759
|
+
try { ctx.backdrop.remove(); } catch {}
|
|
2760
|
+
_gatewayDrawer = null;
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
function openGatewayDrawer(data, aids) {
|
|
2764
|
+
closeGatewayDrawer();
|
|
2765
|
+
const wrap = document.createElement('div');
|
|
2766
|
+
wrap.innerHTML = `<div class="ag-edit-backdrop gateway-drawer-backdrop" id="gateway-drawer-backdrop">` +
|
|
2767
|
+
`<aside class="ag-edit-drawer gateway-drawer" role="dialog" aria-modal="true" aria-labelledby="gateway-drawer-title">` +
|
|
2768
|
+
'<header class="ag-edit-head">' +
|
|
2769
|
+
`<div id="gateway-drawer-title" class="ag-edit-title">${t('agents.gatewayDrawer.title')}</div>` +
|
|
2770
|
+
`<button class="ag-edit-close" id="gateway-drawer-close" type="button" aria-label="${t('common.close')}">×</button>` +
|
|
2771
|
+
'</header>' +
|
|
2772
|
+
`<div class="gateway-drawer-body">${gatewayDrawerContentHtml(data, aids)}</div>` +
|
|
2773
|
+
'</aside></div>';
|
|
2774
|
+
const backdrop = wrap.firstChild;
|
|
2775
|
+
document.body.appendChild(backdrop);
|
|
2776
|
+
const ctx = {
|
|
2777
|
+
backdrop,
|
|
2778
|
+
onKeydown: e => { if (e.key === 'Escape') closeGatewayDrawer(); },
|
|
2779
|
+
};
|
|
2780
|
+
_gatewayDrawer = ctx;
|
|
2781
|
+
backdrop.addEventListener('click', e => { if (e.target === backdrop) closeGatewayDrawer(); });
|
|
2782
|
+
backdrop.querySelector('#gateway-drawer-close').addEventListener('click', closeGatewayDrawer);
|
|
2783
|
+
document.addEventListener('keydown', ctx.onKeydown);
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2786
|
+
function refreshGatewayDrawer(data, aids) {
|
|
2787
|
+
const body = _gatewayDrawer?.backdrop?.querySelector('.gateway-drawer-body');
|
|
2788
|
+
if (body) body.innerHTML = gatewayDrawerContentHtml(data, aids);
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
// 顶部统计条:Daemon Gateway / AIDs total·connected·offline / Messages / Version·PID·Uptime
|
|
2644
2792
|
function agentsStatsBar(data, aids, agentStats) {
|
|
2645
2793
|
const connected = aids.filter(a => (a.status || 'connected') === 'connected').length;
|
|
2646
2794
|
const offline = aids.length - connected;
|
|
@@ -2652,15 +2800,16 @@ function agentsStatsBar(data, aids, agentStats) {
|
|
|
2652
2800
|
errors += s.errors || 0;
|
|
2653
2801
|
interrupts += s.interrupts || 0;
|
|
2654
2802
|
}
|
|
2655
|
-
const
|
|
2656
|
-
const gw =
|
|
2803
|
+
const control = data?.status?.controlAid;
|
|
2804
|
+
const gw = gatewayConnectionUrl(control) || (control?.aid ? t('agents.gatewayDrawer.unresolved') : '—');
|
|
2657
2805
|
const st = data.status || {};
|
|
2658
2806
|
const pid = st.pid != null ? st.pid : '—';
|
|
2659
2807
|
const uptime = st.uptime != null ? fmtDur(st.uptime / 1000) : '—';
|
|
2660
2808
|
const ver = data.version || '—';
|
|
2661
2809
|
|
|
2662
2810
|
let h = '<div class="agents-stats">';
|
|
2663
|
-
h += `<span class="sg"><span class="sg-k">${t('agents.stats.gateway')}</span><span class="sg-gw">${gw}</span
|
|
2811
|
+
h += `<span class="sg"><span class="sg-k">${t('agents.stats.gateway')}</span><span class="dot ${gatewayConnectionStatusClass(control)}"></span><span class="sg-gw">${esc(gw)}</span>` +
|
|
2812
|
+
`<button id="gateway-drawer-open" class="sg-gateway-open" type="button" title="${t('agents.stats.openGatewayDrawer')}" aria-label="${t('agents.stats.openGatewayDrawer')}">›</button></span>`;
|
|
2664
2813
|
h += `<span class="sg"><span class="sg-k">${t('agents.stats.aids')}</span>${aids.length} ${t('agents.stats.total')} · <span class="num-on">${connected} ${t('agents.stats.online')}</span>` +
|
|
2665
2814
|
`${offline ? ` · <span class="num-off">${offline} ${t('agents.stats.offline')}</span>` : ''}</span>`;
|
|
2666
2815
|
h += `<span class="sg"><span class="sg-k">${t('agents.stats.messages')}</span>${t('monitor.th.received')} ${recv} · ${t('monitor.th.sent')} ${sent} · ${t('monitor.th.errors')} ${errors} · ${t('monitor.th.interrupts')} ${interrupts} · ${t('monitor.th.completed')} ${done}</span>`;
|
|
@@ -2724,6 +2873,7 @@ function renderAgents(data) {
|
|
|
2724
2873
|
|
|
2725
2874
|
const allAgents = data.agents || [];
|
|
2726
2875
|
const aids = data.aids || [];
|
|
2876
|
+
refreshGatewayDrawer(data, aids);
|
|
2727
2877
|
const statsByAid = {};
|
|
2728
2878
|
for (const s of (data.stats || [])) statsByAid[s.aid] = s;
|
|
2729
2879
|
const agentStatsByAid = {};
|
|
@@ -3677,6 +3827,9 @@ function ensureOpsOutsideClose() {
|
|
|
3677
3827
|
|
|
3678
3828
|
function bindAgentsEvents(el) {
|
|
3679
3829
|
el.querySelector('#agent-new-btn')?.addEventListener('click', agentOpNew);
|
|
3830
|
+
el.querySelector('#gateway-drawer-open')?.addEventListener('click', () => {
|
|
3831
|
+
openGatewayDrawer(state.agents, state.agents?.aids || []);
|
|
3832
|
+
});
|
|
3680
3833
|
ensureOpsOutsideClose();
|
|
3681
3834
|
// 子标签切换:仅切视图变量并重渲,不重新订阅
|
|
3682
3835
|
el.querySelectorAll('.ag-subtab').forEach(btn => {
|
|
@@ -5213,6 +5366,18 @@ const TRIGGER_CONCURRENCY_OPTIONS = [
|
|
|
5213
5366
|
['replace', () => t('triggers.concurrency.replace')],
|
|
5214
5367
|
['allow', () => t('triggers.concurrency.allow')],
|
|
5215
5368
|
];
|
|
5369
|
+
|
|
5370
|
+
function triggerCanonicalThread(thread) {
|
|
5371
|
+
return thread === 'per_run' || thread === 'by_run' ? 'by_run' : 'by_trigger';
|
|
5372
|
+
}
|
|
5373
|
+
|
|
5374
|
+
function triggerConcurrencyOptions(executionType, thread) {
|
|
5375
|
+
if (executionType === 'script') return TRIGGER_CONCURRENCY_OPTIONS;
|
|
5376
|
+
if (executionType === 'trigger_session' && triggerCanonicalThread(thread) === 'by_run') {
|
|
5377
|
+
return TRIGGER_CONCURRENCY_OPTIONS;
|
|
5378
|
+
}
|
|
5379
|
+
return TRIGGER_CONCURRENCY_OPTIONS.filter(([value]) => value !== 'allow');
|
|
5380
|
+
}
|
|
5216
5381
|
const TRIGGER_MISSED_POLICY_OPTIONS = [
|
|
5217
5382
|
['skip', () => t('triggers.missed.skip')],
|
|
5218
5383
|
['run_once', () => t('triggers.missed.run_once')],
|
|
@@ -5758,6 +5923,7 @@ function triggerOriginalEditState(t) {
|
|
|
5758
5923
|
name: String(definition?.name ?? t?.name ?? t?.label ?? ''),
|
|
5759
5924
|
enabled: triggerIsEnabled(t),
|
|
5760
5925
|
executionType,
|
|
5926
|
+
triggerThread: String(execution.thread ?? ''),
|
|
5761
5927
|
scriptPath: String(execution.script?.path ?? ''),
|
|
5762
5928
|
scriptRuntime: String(execution.script?.runtime ?? 'node'),
|
|
5763
5929
|
scriptTimeoutMs: Number(execution.script?.timeoutMs ?? 30_000),
|
|
@@ -6554,6 +6720,7 @@ function syncTriggerEditFields(ctx) {
|
|
|
6554
6720
|
const valueField = root.querySelector('#trig-edit-schedule-value-field');
|
|
6555
6721
|
const eventFields = root.querySelectorAll('.trig-event-field');
|
|
6556
6722
|
const executionType = root.querySelector('#trig-edit-type')?.value || ctx.original.executionType;
|
|
6723
|
+
const concurrencySelect = root.querySelector('#trig-edit-concurrency');
|
|
6557
6724
|
const deliverySwitch = root.querySelector('#trig-edit-delivery-enabled');
|
|
6558
6725
|
const deliverySwitchField = root.querySelector('.trig-delivery-switch');
|
|
6559
6726
|
if (executionType === 'target_session') {
|
|
@@ -6633,6 +6800,15 @@ function syncTriggerEditFields(ctx) {
|
|
|
6633
6800
|
if (scriptFeedback) scriptFeedback.hidden = executionType !== 'script' || !deliveryEnabled;
|
|
6634
6801
|
if (threadField) threadField.hidden = !showTarget || targetSession !== 'thread';
|
|
6635
6802
|
if (threadInput) threadInput.disabled = ctx.saving || !showTarget || targetSession !== 'thread';
|
|
6803
|
+
if (concurrencySelect) {
|
|
6804
|
+
const options = triggerConcurrencyOptions(executionType, ctx.original.triggerThread);
|
|
6805
|
+
const current = options.some(([value]) => value === concurrencySelect.value)
|
|
6806
|
+
? concurrencySelect.value
|
|
6807
|
+
: 'forbid';
|
|
6808
|
+
concurrencySelect.innerHTML = triggerOptionHtml(options, current, null);
|
|
6809
|
+
concurrencySelect.value = current;
|
|
6810
|
+
concurrencySelect.disabled = Boolean(ctx.saving);
|
|
6811
|
+
}
|
|
6636
6812
|
}
|
|
6637
6813
|
|
|
6638
6814
|
function readTriggerEditValues(root) {
|
|
@@ -6779,7 +6955,7 @@ function triggerDefinitionFromEdit(original, values, eventFilter, feedbackStrate
|
|
|
6779
6955
|
execution.prompt = values.prompt;
|
|
6780
6956
|
delete execution.script;
|
|
6781
6957
|
if (values.executionType === 'trigger_session') {
|
|
6782
|
-
if (previousExecutionType !== 'trigger_session' || !['per_run', 'by_trigger'].includes(execution.thread)) {
|
|
6958
|
+
if (previousExecutionType !== 'trigger_session' || !['per_run', 'by_run', 'by_trigger'].includes(execution.thread)) {
|
|
6783
6959
|
execution.thread = 'by_trigger';
|
|
6784
6960
|
}
|
|
6785
6961
|
} else {
|
|
@@ -7024,7 +7200,7 @@ function openTriggerEditDrawer(id, agent) {
|
|
|
7024
7200
|
`<div class="ag-edit-grid trig-edit-grid trig-limit-grid">` +
|
|
7025
7201
|
`<label class="ag-edit-field"><span>${t('triggers.field.maxRuns')}</span><input id="trig-edit-max-runs" type="number" min="1" step="1" value="${esc(original.maxRuns)}" placeholder="${t('common.noLimit')}"></label>` +
|
|
7026
7202
|
`<label class="ag-edit-field"><span>${t('triggers.field.maxDuration')}</span><input id="trig-edit-max-duration" type="text" autocomplete="off" spellcheck="false" value="${esc(original.maxDuration)}" placeholder="${t('triggers.placeholder.maxDuration')}"></label>` +
|
|
7027
|
-
`<label class="ag-edit-field"><span>${t('triggers.field.concurrency')}</span><select id="trig-edit-concurrency">${triggerOptionHtml(
|
|
7203
|
+
`<label class="ag-edit-field"><span>${t('triggers.field.concurrency')}</span><select id="trig-edit-concurrency">${triggerOptionHtml(triggerConcurrencyOptions(original.executionType, original.triggerThread), original.concurrency, null)}</select></label>` +
|
|
7028
7204
|
`<label class="ag-edit-field"><span>${t('triggers.field.retryAttempts')}</span><input id="trig-edit-retry-attempts" type="number" min="0" max="20" step="1" value="${esc(original.retryAttempts)}"></label>` +
|
|
7029
7205
|
`<label class="ag-edit-field"><span>${t('triggers.field.retryBackoff')}</span><input id="trig-edit-retry-backoff" type="text" autocomplete="off" spellcheck="false" value="${esc(original.retryBackoff)}" placeholder="${t('triggers.placeholder.retryBackoff')}"></label>` +
|
|
7030
7206
|
`<label class="ag-edit-field"><span>${t('triggers.field.missedPolicy')}</span><select id="trig-edit-missed-policy">${triggerOptionHtml(TRIGGER_MISSED_POLICY_OPTIONS, original.missedPolicy, null)}</select></label>` +
|
package/dist/static/index.html
CHANGED
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
|
|
49
49
|
</nav>
|
|
50
50
|
<span style="flex:1"></span>
|
|
51
|
-
2026-
|
|
51
|
+
2026-08-04 23:39
|
|
52
52
|
<span id="conn-status" class="conn-status" data-i18n="status.connecting">连接中…</span>
|
|
53
53
|
<button id="lang-btn" class="theme-btn" data-i18n-title="language.buttonTitle" title="切换语言">🌐</button>
|
|
54
54
|
<button id="theme-btn" class="theme-btn" data-i18n-title="theme.buttonTitle" title="主题">🌗</button>
|
package/dist/static/style.css
CHANGED
|
@@ -1011,11 +1011,61 @@ tr.ag-disabled .ag-name { color: var(--dim); }
|
|
|
1011
1011
|
.agents-stats .sg { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
|
|
1012
1012
|
.agents-stats .sg-k { color: var(--green); font-weight: 600; }
|
|
1013
1013
|
.agents-stats .sg-gw { font-family: ui-monospace, Consolas, monospace; color: var(--fg); }
|
|
1014
|
+
.agents-stats .dot { margin: 0; }
|
|
1015
|
+
.sg-gateway-open {
|
|
1016
|
+
width: 22px;
|
|
1017
|
+
height: 22px;
|
|
1018
|
+
padding: 0;
|
|
1019
|
+
border: 1px solid transparent;
|
|
1020
|
+
border-radius: 4px;
|
|
1021
|
+
background: transparent;
|
|
1022
|
+
color: var(--dim);
|
|
1023
|
+
cursor: pointer;
|
|
1024
|
+
font: 16px/1 ui-monospace, Consolas, monospace;
|
|
1025
|
+
}
|
|
1026
|
+
.sg-gateway-open:hover,
|
|
1027
|
+
.sg-gateway-open:focus-visible { color: var(--accent); border-color: var(--border); outline: none; }
|
|
1014
1028
|
.agents-stats .num-on { color: var(--green); font-weight: 600; }
|
|
1015
1029
|
.agents-stats .num-off { color: var(--red); font-weight: 600; }
|
|
1016
1030
|
.agents-stats .in { color: var(--green); }
|
|
1017
1031
|
.agents-stats .out { color: var(--blue); }
|
|
1018
1032
|
|
|
1033
|
+
/* AUN 网关状态抽屉 */
|
|
1034
|
+
.gateway-drawer { width: min(680px, 100vw); }
|
|
1035
|
+
.gateway-drawer-body { flex: 1; overflow-y: auto; padding: 0 18px 18px; }
|
|
1036
|
+
.gateway-group { padding: 18px 0; border-bottom: 1px solid var(--border); }
|
|
1037
|
+
.gateway-group:last-child { border-bottom: none; }
|
|
1038
|
+
.gateway-group-head {
|
|
1039
|
+
display: flex;
|
|
1040
|
+
align-items: flex-start;
|
|
1041
|
+
justify-content: space-between;
|
|
1042
|
+
gap: 12px;
|
|
1043
|
+
margin-bottom: 8px;
|
|
1044
|
+
}
|
|
1045
|
+
.gateway-group-url {
|
|
1046
|
+
min-width: 0;
|
|
1047
|
+
overflow-wrap: anywhere;
|
|
1048
|
+
color: var(--accent);
|
|
1049
|
+
font: 12px/1.45 ui-monospace, Consolas, monospace;
|
|
1050
|
+
}
|
|
1051
|
+
.gateway-group-count { flex-shrink: 0; color: var(--dim); font-size: 12px; font-variant-numeric: tabular-nums; }
|
|
1052
|
+
.gateway-connection-list { border-top: 1px solid var(--border); }
|
|
1053
|
+
.gateway-connection {
|
|
1054
|
+
display: flex;
|
|
1055
|
+
align-items: flex-start;
|
|
1056
|
+
gap: 9px;
|
|
1057
|
+
padding: 10px 2px;
|
|
1058
|
+
border-bottom: 1px solid var(--border);
|
|
1059
|
+
}
|
|
1060
|
+
.gateway-connection:last-child { border-bottom: none; }
|
|
1061
|
+
.gateway-connection .dot { margin-top: 4px; flex-shrink: 0; }
|
|
1062
|
+
.gateway-connection-main { min-width: 0; flex: 1; }
|
|
1063
|
+
.gateway-connection-id { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
|
|
1064
|
+
.gateway-connection-role { flex-shrink: 0; color: var(--dim); font-size: 11px; font-weight: 600; }
|
|
1065
|
+
.gateway-connection-id code { min-width: 0; overflow-wrap: anywhere; color: var(--fg); font: 12px/1.4 ui-monospace, Consolas, monospace; }
|
|
1066
|
+
.gateway-connection-meta { margin-top: 3px; color: var(--dim); font-size: 11px; }
|
|
1067
|
+
.gateway-connection-error { margin-top: 4px; color: var(--red); font-size: 11px; overflow-wrap: anywhere; }
|
|
1068
|
+
|
|
1019
1069
|
/* 两行一个 agent:主行 + 信息行 */
|
|
1020
1070
|
#view-agents tr.ag-main > td { border-bottom: none; padding-bottom: 3px; }
|
|
1021
1071
|
#view-agents tr.ag-sub > td { padding-top: 0; padding-bottom: 9px; color: var(--dim); }
|