@ryubyte/dsh-a2a 0.1.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/LICENSE +21 -0
- package/README.md +224 -0
- package/README.zh-CN.md +196 -0
- package/cordis.patch.yml +12 -0
- package/lib/client.js +574 -0
- package/lib/index.js +1896 -0
- package/lib/types/a2a-config.d.ts +107 -0
- package/lib/types/a2a-config.d.ts.map +1 -0
- package/lib/types/card.d.ts +40 -0
- package/lib/types/card.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +22 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client.d.ts +52 -0
- package/lib/types/client.d.ts.map +1 -0
- package/lib/types/dashboard.d.ts +144 -0
- package/lib/types/dashboard.d.ts.map +1 -0
- package/lib/types/errors.d.ts +24 -0
- package/lib/types/errors.d.ts.map +1 -0
- package/lib/types/index.d.ts +89 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/jsonrpc.d.ts +40 -0
- package/lib/types/jsonrpc.d.ts.map +1 -0
- package/lib/types/outbound.d.ts +45 -0
- package/lib/types/outbound.d.ts.map +1 -0
- package/lib/types/protocol.d.ts +330 -0
- package/lib/types/protocol.d.ts.map +1 -0
- package/lib/types/server.d.ts +142 -0
- package/lib/types/server.d.ts.map +1 -0
- package/package.json +89 -0
- package/src/a2a-config.ts +201 -0
- package/src/card.ts +143 -0
- package/src/client/index.ts +613 -0
- package/src/client.ts +141 -0
- package/src/dashboard.ts +350 -0
- package/src/errors.ts +39 -0
- package/src/index.ts +659 -0
- package/src/jsonrpc.ts +185 -0
- package/src/outbound.ts +186 -0
- package/src/protocol.ts +308 -0
- package/src/server.ts +467 -0
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-a2a — browser half.
|
|
3
|
+
*
|
|
4
|
+
* Contributes the "A2A 连接" settings section: a dashboard showing who is
|
|
5
|
+
* connected TO this DSH (inbound peers) and who this DSH is connected TO
|
|
6
|
+
* (outbound agents), with reconnect / close controls per connection.
|
|
7
|
+
*
|
|
8
|
+
* The data and the controls live behind the host's `/a2a/api` routes
|
|
9
|
+
* (served by the node half on the same webserver); the browser half is a
|
|
10
|
+
* thin read/write client with no protocol knowledge.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
|
|
14
|
+
import type {} from '@deepseek-ai/dsh-client-ui-settings/client';
|
|
15
|
+
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
16
|
+
import { createElement, useEffect, useState, type ReactElement } from 'react';
|
|
17
|
+
|
|
18
|
+
/** Services this plugin needs from the client runtime. */
|
|
19
|
+
export const inject = ['slots'];
|
|
20
|
+
|
|
21
|
+
export function apply(ctx: ClientContext): void {
|
|
22
|
+
ctx.effect(() => {
|
|
23
|
+
try {
|
|
24
|
+
const off = ctx.slots.register(
|
|
25
|
+
{
|
|
26
|
+
name: 'settings.section',
|
|
27
|
+
id: 'a2a',
|
|
28
|
+
order: 90,
|
|
29
|
+
label: () => 'A2A 连接',
|
|
30
|
+
inject: () => ({}),
|
|
31
|
+
},
|
|
32
|
+
DashboardSection,
|
|
33
|
+
);
|
|
34
|
+
return () => off();
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.error('[dsh-a2a] failed to register settings section:', err);
|
|
37
|
+
return () => {};
|
|
38
|
+
}
|
|
39
|
+
}, 'dsh-a2a: settings section');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── wire types (mirror the host dashboard.ts) ──────────────────────────────
|
|
43
|
+
|
|
44
|
+
interface InboundPeerApi {
|
|
45
|
+
id: string;
|
|
46
|
+
label: string;
|
|
47
|
+
source?: string;
|
|
48
|
+
firstSeen: string;
|
|
49
|
+
lastSeen: string;
|
|
50
|
+
taskCount: number;
|
|
51
|
+
activeTaskIds: string[];
|
|
52
|
+
streaming: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface OutboundAgentApi {
|
|
56
|
+
id: string;
|
|
57
|
+
name: string;
|
|
58
|
+
agentCardUrl: string;
|
|
59
|
+
agentName?: string;
|
|
60
|
+
skillCount: number;
|
|
61
|
+
toolCount: number;
|
|
62
|
+
state: 'connected' | 'disconnected' | 'reconnecting';
|
|
63
|
+
lastSeen: string;
|
|
64
|
+
/** Stable connection id the control actions operate on. */
|
|
65
|
+
connectionId?: string;
|
|
66
|
+
/** True when this connection comes from the profile config. */
|
|
67
|
+
configured?: boolean;
|
|
68
|
+
/** False when this agent is configured but disabled (no live connection). */
|
|
69
|
+
enabled?: boolean;
|
|
70
|
+
/** Skills advertised by the remote AgentCard. */
|
|
71
|
+
skills?: Array<{ id: string; name: string; description?: string; tags?: string[] }>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface SnapshotApi {
|
|
75
|
+
inbound: InboundPeerApi[];
|
|
76
|
+
outbound: OutboundAgentApi[];
|
|
77
|
+
at: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface ControlResultApi {
|
|
81
|
+
ok: boolean;
|
|
82
|
+
message: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function fetchSnapshot(): Promise<SnapshotApi> {
|
|
86
|
+
const res = await fetch('/a2a/api', { headers: { accept: 'application/json' } });
|
|
87
|
+
if (!res.ok) throw new Error(`GET /a2a/api → HTTP ${res.status}`);
|
|
88
|
+
return (await res.json()) as SnapshotApi;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function postControl(action: string, target: string, payload?: Record<string, unknown>): Promise<ControlResultApi> {
|
|
92
|
+
const res = await fetch('/a2a/api', {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers: { 'content-type': 'application/json' },
|
|
95
|
+
body: JSON.stringify({ action, target, ...payload }),
|
|
96
|
+
});
|
|
97
|
+
const body = (await res.json()) as ControlResultApi;
|
|
98
|
+
return body;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
type SectionProps = PropsRuntime<'settings.section'>;
|
|
102
|
+
|
|
103
|
+
/** The A2A connection dashboard settings section. */
|
|
104
|
+
export function DashboardSection(_props: SectionProps): ReactElement {
|
|
105
|
+
// ── state (all hooks at the top, fixed order; helper components below
|
|
106
|
+
// never call hooks — otherwise React #310 crashes the whole slot) ──────
|
|
107
|
+
const [snap, setSnap] = useState<SnapshotApi | null>(null);
|
|
108
|
+
const [error, setError] = useState<string | null>(null);
|
|
109
|
+
const [busy, setBusy] = useState<string | null>(null);
|
|
110
|
+
const [notice, setNotice] = useState<string | null>(null);
|
|
111
|
+
const [serve, setServe] = useState<ServeStatus | null>(null);
|
|
112
|
+
const [addUrl, setAddUrl] = useState('');
|
|
113
|
+
const [discovering, setDiscovering] = useState(false);
|
|
114
|
+
const [discovered, setDiscovered] = useState<DiscoveredCard | null>(null);
|
|
115
|
+
const [tab, setTab] = useState<'out' | 'in' | 'serve'>('out');
|
|
116
|
+
/** connection ids whose skill list is expanded in the outbound tab. */
|
|
117
|
+
const [expanded, setExpanded] = useState<Set<string>>(new Set());
|
|
118
|
+
|
|
119
|
+
interface ServeStatus {
|
|
120
|
+
enabled: boolean;
|
|
121
|
+
baseUrl?: string;
|
|
122
|
+
agentName?: string;
|
|
123
|
+
agentDescription?: string;
|
|
124
|
+
agentVersion?: string;
|
|
125
|
+
endpoint?: string;
|
|
126
|
+
agentCardUrl?: string;
|
|
127
|
+
skills?: Array<{ id: string; name: string; description?: string }>;
|
|
128
|
+
customExecutor?: boolean;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const refresh = async (): Promise<void> => {
|
|
132
|
+
try {
|
|
133
|
+
setError(null);
|
|
134
|
+
setSnap(await fetchSnapshot());
|
|
135
|
+
const sres = await fetch('/a2a/api', {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
headers: { 'content-type': 'application/json' },
|
|
138
|
+
body: JSON.stringify({ action: 'server-status' }),
|
|
139
|
+
});
|
|
140
|
+
const sbody = (await sres.json()) as ControlResultApi;
|
|
141
|
+
if (sbody.ok && sbody.message) setServe(JSON.parse(sbody.message) as ServeStatus);
|
|
142
|
+
else setServe(null);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// initial load + 3s polling (live view)
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
void refresh();
|
|
151
|
+
const timer = setInterval(() => void refresh(), 3000);
|
|
152
|
+
return () => clearInterval(timer);
|
|
153
|
+
}, []);
|
|
154
|
+
|
|
155
|
+
const act = async (action: string, target: string, payload?: Record<string, unknown>): Promise<void> => {
|
|
156
|
+
setBusy(`${action}:${target}`);
|
|
157
|
+
setNotice(null);
|
|
158
|
+
try {
|
|
159
|
+
const result = await postControl(action, target, payload);
|
|
160
|
+
setNotice(`${result.ok ? '✓' : '✗'} ${result.message}`);
|
|
161
|
+
} catch (err) {
|
|
162
|
+
setNotice(`✗ ${err instanceof Error ? err.message : String(err)}`);
|
|
163
|
+
} finally {
|
|
164
|
+
setBusy(null);
|
|
165
|
+
await refresh();
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const inbound = snap?.inbound ?? [];
|
|
170
|
+
const outbound = snap?.outbound ?? [];
|
|
171
|
+
|
|
172
|
+
interface DiscoveredCard {
|
|
173
|
+
name: string;
|
|
174
|
+
description?: string;
|
|
175
|
+
version?: string;
|
|
176
|
+
agentCardUrl: string;
|
|
177
|
+
endpoint?: string;
|
|
178
|
+
skills?: Array<{ id: string; name: string; description?: string }>;
|
|
179
|
+
capabilities?: Record<string, unknown>;
|
|
180
|
+
defaultInputModes?: string[];
|
|
181
|
+
defaultOutputModes?: string[];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const doDiscover = async (): Promise<void> => {
|
|
185
|
+
const u = addUrl.trim();
|
|
186
|
+
if (!u) return;
|
|
187
|
+
setDiscovering(true);
|
|
188
|
+
setDiscovered(null);
|
|
189
|
+
setNotice(null);
|
|
190
|
+
try {
|
|
191
|
+
const res = await fetch('/a2a/api', {
|
|
192
|
+
method: 'POST',
|
|
193
|
+
headers: { 'content-type': 'application/json' },
|
|
194
|
+
body: JSON.stringify({ action: 'discover-agent', agentCardUrl: u }),
|
|
195
|
+
});
|
|
196
|
+
const body = (await res.json()) as ControlResultApi;
|
|
197
|
+
if (!body.ok || !body.message) {
|
|
198
|
+
setNotice(`✗ ${body.message}`);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
setDiscovered(JSON.parse(body.message) as DiscoveredCard);
|
|
202
|
+
} catch (err) {
|
|
203
|
+
setNotice(`✗ ${err instanceof Error ? err.message : String(err)}`);
|
|
204
|
+
} finally {
|
|
205
|
+
setDiscovering(false);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const doConnect = async (): Promise<void> => {
|
|
210
|
+
if (!discovered) return;
|
|
211
|
+
const n = discovered.name.trim() || 'remote';
|
|
212
|
+
await act('add-agent', n, { agentCardUrl: discovered.agentCardUrl });
|
|
213
|
+
if (!busy) {
|
|
214
|
+
setAddUrl('');
|
|
215
|
+
setDiscovered(null);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const inputStyle = { padding: '4px 8px', fontSize: 13, borderRadius: 6, border: '1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.3))', background: 'var(--dsw-alias-fill-l1, rgba(127,127,127,.06))', color: 'var(--dsw-alias-label-primary, #222)' };
|
|
220
|
+
|
|
221
|
+
return createElement(
|
|
222
|
+
'div',
|
|
223
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: 20 } },
|
|
224
|
+
// ── intro ─────────────────────────────────────────────
|
|
225
|
+
createElement(
|
|
226
|
+
'div',
|
|
227
|
+
{ style: { color: 'var(--dsw-alias-label-secondary, #666)', fontSize: 13, lineHeight: 1.6 } },
|
|
228
|
+
'管理本 DSH 的 A2A Agent 连接:连接远程 Agent(出站),查看谁在调用本 DSH 的 A2A 服务(入站)。',
|
|
229
|
+
),
|
|
230
|
+
error
|
|
231
|
+
? createElement(
|
|
232
|
+
'div',
|
|
233
|
+
{ style: { color: 'var(--dsw-danger, #c62828)', fontSize: 13, background: 'var(--dsw-alias-fill-l2, rgba(127,127,127,.08))', borderRadius: 8, padding: '8px 12px' } },
|
|
234
|
+
`加载失败:${error}`,
|
|
235
|
+
)
|
|
236
|
+
: null,
|
|
237
|
+
notice
|
|
238
|
+
? createElement(
|
|
239
|
+
'div',
|
|
240
|
+
{ style: { color: 'var(--dsw-alias-label-primary, #222)', fontSize: 13, background: 'var(--dsw-alias-fill-l2, rgba(127,127,127,.08))', borderRadius: 8, padding: '8px 12px' } },
|
|
241
|
+
notice,
|
|
242
|
+
)
|
|
243
|
+
: null,
|
|
244
|
+
// ── tabs (underline style, same as the 插件 settings page) ─────────
|
|
245
|
+
tabRail([
|
|
246
|
+
tabBtn('出站 Agent', outbound.length, tab === 'out', () => setTab('out')),
|
|
247
|
+
tabBtn('入站连接', inbound.length, tab === 'in', () => setTab('in')),
|
|
248
|
+
serve ? tabBtn('A2A 服务', 0, tab === 'serve', () => setTab('serve')) : null,
|
|
249
|
+
]),
|
|
250
|
+
// ── outbound tab ──────────────────────────────────────
|
|
251
|
+
tab === 'out'
|
|
252
|
+
? createElement(
|
|
253
|
+
'div',
|
|
254
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: 16 } },
|
|
255
|
+
// Connected agents (cards)
|
|
256
|
+
createElement(
|
|
257
|
+
'div',
|
|
258
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: 10 } },
|
|
259
|
+
sec('已连接 Agent', outbound.length),
|
|
260
|
+
outbound.length === 0
|
|
261
|
+
? emptyRow('暂无已连接的 Agent。使用下方"添加 Agent"通过 Agent Card URL 连接远程 A2A Agent。')
|
|
262
|
+
: createElement(
|
|
263
|
+
'div',
|
|
264
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: 8 } },
|
|
265
|
+
...outbound.map((a) => agentCard(a, busy, act, expanded, setExpanded)),
|
|
266
|
+
),
|
|
267
|
+
),
|
|
268
|
+
// Add agent (discover → connect)
|
|
269
|
+
createElement(
|
|
270
|
+
'div',
|
|
271
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: 10, background: 'var(--dsw-alias-fill-l2, rgba(127,127,127,.05))', borderRadius: 10, padding: '14px 16px' } },
|
|
272
|
+
createElement('div', { style: { fontSize: 13, fontWeight: 600, color: 'var(--dsw-alias-label-primary, #222)' } }, '添加 Agent'),
|
|
273
|
+
createElement(
|
|
274
|
+
'div',
|
|
275
|
+
{ style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' } },
|
|
276
|
+
createElement('input', {
|
|
277
|
+
placeholder: 'Agent Card URL(https://…/.well-known/agent-card.json)',
|
|
278
|
+
value: addUrl,
|
|
279
|
+
onChange: (e: { target: { value: string } }) => {
|
|
280
|
+
setAddUrl(e.target.value);
|
|
281
|
+
setDiscovered(null);
|
|
282
|
+
},
|
|
283
|
+
style: { ...inputStyle, flex: 1, minWidth: 260 },
|
|
284
|
+
}),
|
|
285
|
+
actionBtn('导入', false, () => void doDiscover(), discovering || !addUrl.trim()),
|
|
286
|
+
),
|
|
287
|
+
discovering
|
|
288
|
+
? createElement('div', { style: { fontSize: 13, color: 'var(--dsw-alias-label-tertiary, #999)' } }, '正在读取 Agent Card…')
|
|
289
|
+
: discovered
|
|
290
|
+
? createElement(
|
|
291
|
+
'div',
|
|
292
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: 8, background: 'var(--dsw-alias-fill-l1, rgba(127,127,127,.06))', borderRadius: 8, padding: '12px 14px' } },
|
|
293
|
+
createElement(
|
|
294
|
+
'div',
|
|
295
|
+
{ style: { display: 'flex', alignItems: 'center', gap: 8 } },
|
|
296
|
+
createElement('span', { style: { fontSize: 15, fontWeight: 700, color: 'var(--dsw-alias-label-primary, #222)' } }, discovered.name),
|
|
297
|
+
discovered.version
|
|
298
|
+
? createElement('span', { style: { fontSize: 11.5, color: 'var(--dsw-alias-label-tertiary, #999)', fontFamily: 'monospace' } }, `v${discovered.version}`)
|
|
299
|
+
: null,
|
|
300
|
+
stateDot('connected'),
|
|
301
|
+
),
|
|
302
|
+
discovered.description
|
|
303
|
+
? createElement('div', { style: { fontSize: 12.5, color: 'var(--dsw-alias-label-secondary, #666)' } }, discovered.description)
|
|
304
|
+
: null,
|
|
305
|
+
(discovered.skills?.length ?? 0) > 0
|
|
306
|
+
? createElement(
|
|
307
|
+
'div',
|
|
308
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: 4 } },
|
|
309
|
+
createElement('div', { style: { fontSize: 12, fontWeight: 600, color: 'var(--dsw-alias-label-secondary, #666)' } }, 'Skills'),
|
|
310
|
+
...(discovered.skills ?? []).map((s) =>
|
|
311
|
+
createElement('div', { key: s.id, style: { fontSize: 12.5, color: 'var(--dsw-alias-label-primary, #222)' } }, `• ${s.name}${s.description ? ` — ${s.description}` : ''}`),
|
|
312
|
+
),
|
|
313
|
+
)
|
|
314
|
+
: null,
|
|
315
|
+
discovered.endpoint
|
|
316
|
+
? createElement('div', { style: { fontSize: 11.5, fontFamily: 'monospace', color: 'var(--dsw-alias-label-tertiary, #999)' } }, `端点 ${discovered.endpoint}`)
|
|
317
|
+
: null,
|
|
318
|
+
createElement(
|
|
319
|
+
'div',
|
|
320
|
+
{ style: { display: 'flex', gap: 8, marginTop: 2 } },
|
|
321
|
+
actionBtn('连接', true, () => void doConnect(), busy === 'add-agent'),
|
|
322
|
+
actionBtn('取消', false, () => { setDiscovered(null); setAddUrl(''); }, false),
|
|
323
|
+
),
|
|
324
|
+
)
|
|
325
|
+
: createElement(
|
|
326
|
+
'div',
|
|
327
|
+
{ style: { fontSize: 12, color: 'var(--dsw-alias-label-tertiary, #999)' } },
|
|
328
|
+
'输入远程 Agent 的 Agent Card URL 并点击"导入",将自动读取其名称、描述、技能与能力,确认后建立连接。',
|
|
329
|
+
),
|
|
330
|
+
),
|
|
331
|
+
)
|
|
332
|
+
: null,
|
|
333
|
+
// ── serve tab ─────────────────────────────────────────
|
|
334
|
+
tab === 'serve' && serve
|
|
335
|
+
? createElement(
|
|
336
|
+
'div',
|
|
337
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: 10, background: 'var(--dsw-alias-fill-l2, rgba(127,127,127,.05))', borderRadius: 10, padding: '14px 16px' } },
|
|
338
|
+
createElement(
|
|
339
|
+
'div',
|
|
340
|
+
{ style: { display: 'flex', alignItems: 'center', gap: 8 } },
|
|
341
|
+
sec('A2A 服务(本 DSH 作为 Agent 对外)', 0),
|
|
342
|
+
stateDot(serve.enabled ? 'connected' : 'disconnected'),
|
|
343
|
+
createElement(
|
|
344
|
+
'span',
|
|
345
|
+
{ style: { fontSize: 12.5, color: serve.enabled ? '#2e7d32' : '#9e9e9e' } },
|
|
346
|
+
serve.enabled ? '运行中' : '已下线',
|
|
347
|
+
),
|
|
348
|
+
createElement(
|
|
349
|
+
'span',
|
|
350
|
+
{ style: { marginLeft: 'auto' } },
|
|
351
|
+
serve.enabled
|
|
352
|
+
? actionBtn('下线', false, () => void act('server-disable', ''), busy === 'server-disable')
|
|
353
|
+
: actionBtn('上线', true, () => void act('server-enable', ''), busy === 'server-enable'),
|
|
354
|
+
),
|
|
355
|
+
),
|
|
356
|
+
createElement(
|
|
357
|
+
'div',
|
|
358
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: 6 } },
|
|
359
|
+
serve.agentName
|
|
360
|
+
? createElement(
|
|
361
|
+
'div',
|
|
362
|
+
{ style: { fontSize: 13, fontWeight: 600, color: 'var(--dsw-alias-label-primary, #222)' } },
|
|
363
|
+
serve.agentName,
|
|
364
|
+
serve.agentVersion ? createElement('span', { style: { fontSize: 11.5, color: 'var(--dsw-alias-label-tertiary, #999)', fontFamily: 'monospace', marginLeft: 8 } }, `v${serve.agentVersion}`) : null,
|
|
365
|
+
)
|
|
366
|
+
: null,
|
|
367
|
+
serve.agentDescription
|
|
368
|
+
? createElement('div', { style: { fontSize: 12.5, color: 'var(--dsw-alias-label-secondary, #666)' } }, serve.agentDescription)
|
|
369
|
+
: null,
|
|
370
|
+
serve.endpoint
|
|
371
|
+
? createElement('div', { style: { fontSize: 12, fontFamily: 'monospace', color: 'var(--dsw-alias-label-tertiary, #999)' } }, `端点 ${serve.endpoint}`)
|
|
372
|
+
: null,
|
|
373
|
+
serve.agentCardUrl
|
|
374
|
+
? createElement('div', { style: { fontSize: 12, fontFamily: 'monospace', color: 'var(--dsw-alias-label-tertiary, #999)' } }, `Agent Card ${serve.agentCardUrl}`)
|
|
375
|
+
: null,
|
|
376
|
+
(serve.skills?.length ?? 0) > 0
|
|
377
|
+
? createElement(
|
|
378
|
+
'div',
|
|
379
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: 4 } },
|
|
380
|
+
createElement('div', { style: { fontSize: 12, fontWeight: 600, color: 'var(--dsw-alias-label-secondary, #666)' } }, 'Skills'),
|
|
381
|
+
...(serve.skills ?? []).map((s) =>
|
|
382
|
+
createElement('div', { key: s.id, style: { fontSize: 12.5, color: 'var(--dsw-alias-label-primary, #222)' } }, `• ${s.name}${s.description ? ` — ${s.description}` : ''}`),
|
|
383
|
+
),
|
|
384
|
+
)
|
|
385
|
+
: null,
|
|
386
|
+
),
|
|
387
|
+
serve.enabled && !serve.customExecutor
|
|
388
|
+
? createElement(
|
|
389
|
+
'div',
|
|
390
|
+
{ style: { fontSize: 12, color: 'var(--dsw-warning, #b26a00)', background: 'rgba(178,106,0,.08)', borderRadius: 6, padding: '6px 10px' } },
|
|
391
|
+
'⚠ 默认 executor 会执行 shell 命令。生产环境请通过配置指定受限的 execute 实现。',
|
|
392
|
+
)
|
|
393
|
+
: null,
|
|
394
|
+
)
|
|
395
|
+
: null,
|
|
396
|
+
// ── inbound tab ────────────────────────────────────────
|
|
397
|
+
tab === 'in'
|
|
398
|
+
? createElement(
|
|
399
|
+
'div',
|
|
400
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: 10 } },
|
|
401
|
+
sec('入站连接(谁在连接本 DSH)', inbound.length),
|
|
402
|
+
inbound.length === 0
|
|
403
|
+
? emptyRow('暂无入站连接。其他 A2A 客户端调用本 DSH 的 JSON-RPC 端点后会显示在这里。')
|
|
404
|
+
: createElement('table', tblStyle, createElement('thead', null, headRow(['来源', '地址', '首次连接', '最近活动', '任务', '流', '操作'])), createElement('tbody', null, ...inbound.map((p) => inboundRow(p, busy, act)))),
|
|
405
|
+
)
|
|
406
|
+
: null,
|
|
407
|
+
createElement(
|
|
408
|
+
'div',
|
|
409
|
+
{ style: { color: 'var(--dsw-alias-label-tertiary, #999)', fontSize: 12 } },
|
|
410
|
+
`快照时间 ${snap ? new Date(snap.at).toLocaleTimeString() : '—'} · 每 3 秒自动刷新`,
|
|
411
|
+
),
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ── connected agent card (pure: hooks live in the parent section) ────────
|
|
416
|
+
function agentCard(
|
|
417
|
+
a: OutboundAgentApi,
|
|
418
|
+
busy: string | null,
|
|
419
|
+
act: (a: string, t: string, p?: Record<string, unknown>) => Promise<void>,
|
|
420
|
+
expanded: Set<string>,
|
|
421
|
+
setExpanded: (s: Set<string>) => void,
|
|
422
|
+
): ReactElement {
|
|
423
|
+
const target = a.connectionId ?? a.id;
|
|
424
|
+
const open = expanded.has(target);
|
|
425
|
+
const skills = a.skills ?? [];
|
|
426
|
+
const toggle = (): void => {
|
|
427
|
+
const next = new Set(expanded);
|
|
428
|
+
if (next.has(target)) next.delete(target);
|
|
429
|
+
else next.add(target);
|
|
430
|
+
setExpanded(next);
|
|
431
|
+
};
|
|
432
|
+
return createElement(
|
|
433
|
+
'div',
|
|
434
|
+
{ key: a.id, style: { display: 'flex', flexDirection: 'column', gap: 8, background: 'var(--dsw-alias-fill-l1, rgba(127,127,127,.05))', borderRadius: 10, padding: '12px 14px', opacity: a.enabled === false ? 0.72 : 1 } },
|
|
435
|
+
// header row — click toggles skill list
|
|
436
|
+
createElement(
|
|
437
|
+
'div',
|
|
438
|
+
{ style: { display: 'flex', alignItems: 'center', gap: 10, cursor: 'pointer', userSelect: 'none' as const, flexWrap: 'wrap' as const }, onClick: toggle },
|
|
439
|
+
createElement('span', { style: { fontSize: 11, color: 'var(--dsw-alias-label-tertiary, #999)', width: 14, display: 'inline-block', transition: 'transform .15s', transform: open ? 'rotate(90deg)' : 'none' } }, '▶'),
|
|
440
|
+
stateDot(a.enabled === false ? 'disabled' : a.state),
|
|
441
|
+
createElement('span', { style: { fontSize: 14, fontWeight: 600, color: 'var(--dsw-alias-label-primary, #222)' } }, a.agentName ?? a.name),
|
|
442
|
+
a.enabled === false
|
|
443
|
+
? createElement('span', { style: { fontSize: 11.5, ...sourceBadge('#b26a00', 'rgba(178,106,0,.1)', '已禁用') } }, '已禁用')
|
|
444
|
+
: createElement(
|
|
445
|
+
'span',
|
|
446
|
+
{ style: { fontSize: 11.5, ...(a.configured ? sourceBadge('#666', 'rgba(127,127,127,.12)', '配置') : sourceBadge('#4a7dff', 'rgba(74,125,255,.1)', '运行时')) } },
|
|
447
|
+
a.configured ? '配置' : '运行时',
|
|
448
|
+
),
|
|
449
|
+
createElement('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-tertiary, #999)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', minWidth: 0 } }, `${a.skillCount} 技能 · ${a.toolCount} 工具 · ${new Date(a.lastSeen).toLocaleTimeString()}`),
|
|
450
|
+
// ── three operations: reconnect/enable · disable · delete (never wrap) ──
|
|
451
|
+
createElement(
|
|
452
|
+
'div',
|
|
453
|
+
{ style: { display: 'flex', alignItems: 'center', gap: 4, flexShrink: 0, marginLeft: 'auto', paddingLeft: 10 } },
|
|
454
|
+
a.enabled === false
|
|
455
|
+
? actionBtn('启用', true, () => void act('enable-agent', target), busy === `enable-agent:${target}`)
|
|
456
|
+
: actionBtn('重连', true, () => void act('reconnect-agent', target), busy === `reconnect-agent:${target}`),
|
|
457
|
+
actionBtn('禁用', false, () => void act('disable-agent', target), busy === `disable-agent:${target}`),
|
|
458
|
+
actionBtn('删除', false, () => void act('remove-agent', target), busy === `remove-agent:${target}`),
|
|
459
|
+
),
|
|
460
|
+
),
|
|
461
|
+
createElement(
|
|
462
|
+
'div',
|
|
463
|
+
{ style: { fontSize: 12, fontFamily: 'monospace', color: 'var(--dsw-alias-label-tertiary, #999)', wordBreak: 'break-all' } },
|
|
464
|
+
a.agentCardUrl,
|
|
465
|
+
),
|
|
466
|
+
// expandable skills
|
|
467
|
+
open && skills.length > 0
|
|
468
|
+
? createElement(
|
|
469
|
+
'div',
|
|
470
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: 6, background: 'var(--dsw-alias-fill-l2, rgba(0,0,0,.04))', borderRadius: 8, padding: '10px 12px', marginTop: 2 } },
|
|
471
|
+
...skills.map((s) =>
|
|
472
|
+
createElement(
|
|
473
|
+
'div',
|
|
474
|
+
{ key: s.id, style: { display: 'flex', flexDirection: 'column', gap: 2 } },
|
|
475
|
+
createElement(
|
|
476
|
+
'div',
|
|
477
|
+
{ style: { display: 'flex', alignItems: 'baseline', gap: 6 } },
|
|
478
|
+
createElement('span', { style: { fontSize: 12.5, fontWeight: 600, color: 'var(--dsw-alias-label-primary, #222)', fontFamily: 'monospace' } }, s.name),
|
|
479
|
+
s.tags?.length
|
|
480
|
+
? createElement('span', { style: { fontSize: 11, color: 'var(--dsw-alias-label-tertiary, #999)' } }, s.tags.join(', '))
|
|
481
|
+
: null,
|
|
482
|
+
),
|
|
483
|
+
s.description
|
|
484
|
+
? createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary, #666)' } }, s.description)
|
|
485
|
+
: null,
|
|
486
|
+
),
|
|
487
|
+
),
|
|
488
|
+
)
|
|
489
|
+
: open
|
|
490
|
+
? createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-tertiary, #999)' } }, '(该 Agent 未声明技能)')
|
|
491
|
+
: null,
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function sourceBadge(color: string, bg: string, _label: string): Record<string, string | number> {
|
|
496
|
+
return { color, background: bg, borderRadius: 4, padding: '1px 6px' };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** A tab button matching the settings "插件" page's underline tab style. */
|
|
500
|
+
function tabBtn(label: string, count: number, active: boolean, onClick: () => void): ReactElement {
|
|
501
|
+
return createElement(
|
|
502
|
+
'button',
|
|
503
|
+
{
|
|
504
|
+
onClick,
|
|
505
|
+
'aria-selected': active,
|
|
506
|
+
role: 'tab',
|
|
507
|
+
'data-active': active,
|
|
508
|
+
style: {
|
|
509
|
+
padding: '7px 1px 9px',
|
|
510
|
+
fontSize: 13,
|
|
511
|
+
lineHeight: '20px',
|
|
512
|
+
fontWeight: 400,
|
|
513
|
+
color: active ? 'var(--dsw-alias-label-primary, #0f1115)' : 'var(--dsw-alias-label-tertiary, #81858c)',
|
|
514
|
+
background: 'transparent',
|
|
515
|
+
border: 'none',
|
|
516
|
+
cursor: 'pointer',
|
|
517
|
+
position: 'relative',
|
|
518
|
+
// underline via box-shadow inset (React inline styles can't do ::after)
|
|
519
|
+
boxShadow: active ? 'inset 0 -2px 0 var(--dsw-alias-label-primary, #0f1115)' : 'none',
|
|
520
|
+
borderRadius: '2px 2px 0 0',
|
|
521
|
+
transition: 'color .15s, box-shadow .15s',
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
label,
|
|
525
|
+
count > 0 ? createElement('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-tertiary, #999)', marginLeft: 6 } }, String(count)) : null,
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/** The tab strip container replicating `.pbvGtq_tabs` (border-bottom rail). */
|
|
530
|
+
function tabRail(children: Array<ReactElement | null>): ReactElement {
|
|
531
|
+
return createElement(
|
|
532
|
+
'div',
|
|
533
|
+
{ role: 'tablist', style: { display: 'flex', alignItems: 'flex-end', gap: 22, borderBottom: '1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.1))', marginTop: 2 } },
|
|
534
|
+
...children,
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function sec(title: string, count: number): ReactElement {
|
|
539
|
+
return createElement(
|
|
540
|
+
'h3',
|
|
541
|
+
{ style: { margin: 0, fontSize: 14, fontWeight: 600, color: 'var(--dsw-alias-label-primary, #222)', display: 'flex', alignItems: 'center', gap: 8 } },
|
|
542
|
+
title,
|
|
543
|
+
createElement('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-tertiary, #999)', background: 'var(--dsw-alias-fill-l2, rgba(127,127,127,.1))', borderRadius: 10, padding: '1px 8px' } }, String(count)),
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function emptyRow(text: string): ReactElement {
|
|
548
|
+
return createElement(
|
|
549
|
+
'div',
|
|
550
|
+
{ style: { color: 'var(--dsw-alias-label-tertiary, #999)', fontSize: 13, background: 'var(--dsw-alias-fill-l2, rgba(127,127,127,.06))', borderRadius: 8, padding: '12px 14px' } },
|
|
551
|
+
text,
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const tblStyle = { borderCollapse: 'collapse' as const, width: '100%', fontSize: 13 };
|
|
556
|
+
const thStyle = { textAlign: 'left' as const, padding: '6px 8px', borderBottom: '1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.18))', color: 'var(--dsw-alias-label-secondary, #666)', fontWeight: 600, whiteSpace: 'nowrap' as const };
|
|
557
|
+
const tdStyle = { padding: '6px 8px', borderBottom: '1px solid var(--dsw-alias-border-l1, rgba(127,127,127,.1))', color: 'var(--dsw-alias-label-primary, #222)', verticalAlign: 'top' as const, fontSize: 12.5 };
|
|
558
|
+
|
|
559
|
+
function headRow(cols: string[]): ReactElement {
|
|
560
|
+
return createElement('tr', null, ...cols.map((c) => createElement('th', { style: thStyle }, c)));
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const stateColor: Record<string, string> = {
|
|
564
|
+
connected: '#2e7d32',
|
|
565
|
+
disconnected: '#9e9e9e',
|
|
566
|
+
reconnecting: '#f57c00',
|
|
567
|
+
disabled: '#b26a00',
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
function stateDot(state: string): ReactElement {
|
|
571
|
+
return createElement('span', { style: { display: 'inline-block', width: 8, height: 8, borderRadius: 4, marginRight: 6, background: stateColor[state] ?? '#9e9e9e' } });
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function actionBtn(label: string, primary: boolean, onClick: () => void, disabled: boolean): ReactElement {
|
|
575
|
+
return createElement(
|
|
576
|
+
'button',
|
|
577
|
+
{
|
|
578
|
+
onClick,
|
|
579
|
+
disabled,
|
|
580
|
+
style: {
|
|
581
|
+
marginRight: 6,
|
|
582
|
+
padding: '3px 10px',
|
|
583
|
+
fontSize: 12,
|
|
584
|
+
lineHeight: '18px',
|
|
585
|
+
borderRadius: 6,
|
|
586
|
+
border: `1px solid ${primary ? 'var(--dsw-brand, #4a7dff)' : 'var(--dsw-alias-border-l2, rgba(127,127,127,.3))'}`,
|
|
587
|
+
background: primary ? 'var(--dsw-brand, #4a7dff)' : 'transparent',
|
|
588
|
+
color: primary ? '#fff' : 'var(--dsw-alias-label-primary, #222)',
|
|
589
|
+
cursor: disabled ? 'default' : 'pointer',
|
|
590
|
+
opacity: disabled ? 0.5 : 1,
|
|
591
|
+
},
|
|
592
|
+
},
|
|
593
|
+
label,
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function inboundRow(p: InboundPeerApi, busy: string | null, act: (a: string, t: string) => void): ReactElement {
|
|
598
|
+
return createElement(
|
|
599
|
+
'tr',
|
|
600
|
+
{ key: p.id },
|
|
601
|
+
createElement('td', { style: tdStyle }, p.label),
|
|
602
|
+
createElement('td', { style: { ...tdStyle, fontFamily: 'monospace', fontSize: 12 } }, p.source ?? '—'),
|
|
603
|
+
createElement('td', { style: tdStyle }, new Date(p.firstSeen).toLocaleTimeString()),
|
|
604
|
+
createElement('td', { style: tdStyle }, new Date(p.lastSeen).toLocaleTimeString()),
|
|
605
|
+
createElement('td', { style: tdStyle }, String(p.taskCount)),
|
|
606
|
+
createElement('td', { style: tdStyle }, p.streaming ? '● 流' : '—'),
|
|
607
|
+
createElement(
|
|
608
|
+
'td',
|
|
609
|
+
{ style: tdStyle },
|
|
610
|
+
actionBtn('关闭', false, () => void act('close-peer', p.id), busy === `close-peer:${p.id}`),
|
|
611
|
+
),
|
|
612
|
+
);
|
|
613
|
+
}
|