@ai_mind/dsh-a2a 0.0.0-stage → 1.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/client.js ADDED
@@ -0,0 +1,927 @@
1
+ /**
2
+ * @ai_mind/dsh-a2a — browser half (settings panel).
3
+ *
4
+ * Registers one Settings → Plugins tab that renders the live A2A Agent Card,
5
+ * its interface URLs, capabilities, optional runtime counters, an editable
6
+ * configuration form and copy-ready request snippets.
7
+ *
8
+ * The configuration form talks to `<mountPath>/panel/config`, which is
9
+ * authenticated by the Harness Web connection (not the A2A token) and writes
10
+ * through the Host config editor.
11
+ */
12
+ window.__ModuleLoader__.load({
13
+ id: '@ai_mind/dsh-a2a',
14
+ factory(require) {
15
+ const React = require('react')
16
+ const h = React.createElement
17
+
18
+ const NS = 'a2aPanel'
19
+ const CARD_PATH = '/.well-known/agent-card.json'
20
+
21
+ const zh = {
22
+ tab: 'A2A 服务端',
23
+ heading: 'A2A 服务端',
24
+ intro: '通过 Agent2Agent v1.0 协议把本机智能体开放给其他智能体。',
25
+ refresh: '刷新',
26
+ loading: '正在读取 Agent Card…',
27
+ loadError: '无法读取 Agent Card',
28
+ loadErrorHint: '请确认 a2a-server 插件已启用,并且 /a2a 路径没有被其他插件占用。',
29
+ disabled: '未检测到 A2A 服务端',
30
+ disabledHint: '在 设置 → 插件 中启用 @ai_mind/dsh-a2a,或确认 profile 的 cordis.patch.yml 中存在 a2a-server 行。',
31
+ interfaces: '接口地址',
32
+ jsonrpc: 'JSON-RPC',
33
+ rest: 'HTTP+JSON',
34
+ capabilities: '能力',
35
+ streaming: '流式 SSE',
36
+ push: '推送通知',
37
+ extended: '扩展 Agent Card',
38
+ auth: '鉴权',
39
+ authNone: '未启用(仅允许本机调用)',
40
+ authOn: '已启用',
41
+ version: '协议版本',
42
+ runtime: '运行状态',
43
+ serverStatus: '监听方式',
44
+ serverMode: '模式',
45
+ serverModeShared: '复用 DSH 端口',
46
+ serverModeStandalone: '独立端口',
47
+ serverAddress: '地址',
48
+ serverListening: '监听中',
49
+ serverListeningYes: '是',
50
+ serverListeningNo: '否',
51
+ serverError: '监听错误',
52
+ serverNotListening: '独立端口未监听成功:请检查端口是否被占用,或修改 server.port。',
53
+ runtimeDenied: '运行状态需要 A2A 凭据,当前页面无权读取;这不影响其他信息。',
54
+ tasks: '任务',
55
+ contexts: '活跃上下文',
56
+ concurrency: '并发占用',
57
+ uptime: '运行时长',
58
+ skills: '技能',
59
+ noSkills: '未声明技能',
60
+ quickstart: '快速请求',
61
+ copy: '复制',
62
+ copied: '已复制',
63
+ jsClient: 'JavaScript 客户端',
64
+ config: '配置',
65
+ configHint: '保存到 profile 的 cordis.patch.yml(a2a-server 行);插件会重载,进行中的 A2A 任务会被中断。',
66
+ configUnavailable: '当前部署未启用配置编辑(缺少 configEditor)。请直接编辑 profile 的 cordis.patch.yml。',
67
+ configUnauthorized: '配置读取需要已登录的 Harness Web 会话,请从 DSH GUI 打开本页。',
68
+ configLoadError: '读取配置失败',
69
+ save: '保存',
70
+ saving: '保存中…',
71
+ saved: '已保存,插件正在重载…',
72
+ saveError: '保存失败',
73
+ reload: '重新读取',
74
+ dirty: '有未保存的修改',
75
+ token: '访问令牌',
76
+ tokenHint: '写回 profile YAML;推荐改用 auth.tokenEnv 从环境变量读取。',
77
+ tokenPlaceholder: '留空保持不变',
78
+ clearToken: '清除已保存的内联令牌',
79
+ tokenConfigured: '已配置内联令牌',
80
+ tokenNotConfigured: '未配置内联令牌',
81
+ tokenEnvConfigured: '环境变量已配置:{name}',
82
+ openCard: '打开 Agent Card',
83
+ seconds: '秒',
84
+ minutes: '分',
85
+ hours: '小时',
86
+ unknown: '—',
87
+ groupCard: '身份与描述',
88
+ groupServer: '服务与能力',
89
+ groupAuth: '鉴权',
90
+ groupAgent: 'Agent 运行时',
91
+ groupLimits: '限额与保留',
92
+ fieldCardName: '名称',
93
+ fieldCardDescription: '描述',
94
+ fieldCardVersion: '版本',
95
+ fieldServerPublicUrl: '对外地址',
96
+ fieldServerHost: '监听地址',
97
+ fieldServerPort: '监听端口',
98
+ fieldServerMountPath: '挂载路径',
99
+ fieldServerStreaming: '流式 SSE',
100
+ fieldServerPushNotifications: '推送通知',
101
+ fieldServerExtendedCard: '扩展 Agent Card',
102
+ fieldAuthMode: '鉴权模式',
103
+ fieldAuthTokenEnv: '令牌环境变量',
104
+ fieldAuthAllowRemoteUnauthenticated: '允许未鉴权的远程调用',
105
+ fieldAgentCwd: '工作目录',
106
+ fieldAgentPreset: 'Agent 预设',
107
+ fieldAgentProvider: '模型提供方',
108
+ fieldAgentModel: '模型',
109
+ fieldAgentReasoningEffort: '推理强度',
110
+ fieldAgentSandboxMode: '沙箱模式',
111
+ fieldAgentApprovalPolicy: '审批策略',
112
+ fieldAgentMaxConcurrentTasks: '最大并发任务',
113
+ fieldAgentQueueTimeoutMs: '排队超时 (ms)',
114
+ fieldAgentTurnTimeoutMs: '单回合超时 (ms)',
115
+ fieldLimitsMaxRequestBytes: '请求体上限 (bytes)',
116
+ fieldLimitsMaxFileBytes: '单附件上限 (bytes)',
117
+ fieldTasksTtlMs: '任务保留 (ms)',
118
+ fieldTasksMaxTasks: '任务数量上限',
119
+ fieldPushAllowPrivateTargets: '允许私有 Webhook',
120
+ enumNone: '不鉴权',
121
+ enumBearer: 'Bearer',
122
+ enumApiKey: 'API Key',
123
+ enumInherit: '继承部署默认',
124
+ enumReadOnly: '只读',
125
+ enumWorkspaceWrite: '工作区可写',
126
+ enumDangerFullAccess: '完全访问',
127
+ enumAsk: '询问',
128
+ enumNever: '从不',
129
+ restartHint: '修改挂载路径后请刷新页面。',
130
+ }
131
+
132
+ const en = {
133
+ tab: 'A2A Server',
134
+ heading: 'A2A Server',
135
+ intro: 'Exposes this Harness agent to other agents over the Agent2Agent v1.0 protocol.',
136
+ refresh: 'Refresh',
137
+ loading: 'Reading the Agent Card…',
138
+ loadError: 'Cannot read the Agent Card',
139
+ loadErrorHint: 'Confirm the a2a-server plugin is enabled and the /a2a path is not owned by another plugin.',
140
+ disabled: 'No A2A server detected',
141
+ disabledHint: 'Enable @ai_mind/dsh-a2a under Settings → Plugins, or add an a2a-server row to the profile cordis.patch.yml.',
142
+ interfaces: 'Endpoints',
143
+ jsonrpc: 'JSON-RPC',
144
+ rest: 'HTTP+JSON',
145
+ capabilities: 'Capabilities',
146
+ streaming: 'Streaming SSE',
147
+ push: 'Push notifications',
148
+ extended: 'Extended Agent Card',
149
+ auth: 'Authentication',
150
+ authNone: 'None (loopback only)',
151
+ authOn: 'Required',
152
+ version: 'Protocol version',
153
+ runtime: 'Runtime',
154
+ serverStatus: 'Listener',
155
+ serverMode: 'Mode',
156
+ serverModeShared: 'Shared with DSH',
157
+ serverModeStandalone: 'Dedicated port',
158
+ serverAddress: 'Address',
159
+ serverListening: 'Listening',
160
+ serverListeningYes: 'yes',
161
+ serverListeningNo: 'no',
162
+ serverError: 'Listener error',
163
+ serverNotListening: 'The dedicated listener is not up. Check whether the port is taken or change server.port.',
164
+ runtimeDenied: 'Runtime counters require A2A credentials; the panel cannot read them. Everything else is unaffected.',
165
+ tasks: 'Tasks',
166
+ contexts: 'Active contexts',
167
+ concurrency: 'Concurrency',
168
+ uptime: 'Uptime',
169
+ skills: 'Skills',
170
+ noSkills: 'No skills declared',
171
+ quickstart: 'Quick request',
172
+ copy: 'Copy',
173
+ copied: 'Copied',
174
+ jsClient: 'JavaScript client',
175
+ config: 'Configuration',
176
+ configHint: 'Saved to the a2a-server row in the profile cordis.patch.yml; the plugin reloads and in-flight A2A tasks are interrupted.',
177
+ configUnavailable: 'Configuration editing is unavailable in this deployment (no config editor). Edit the profile cordis.patch.yml instead.',
178
+ configUnauthorized: 'Reading configuration requires an authenticated Harness Web session; open this page from the DSH GUI.',
179
+ configLoadError: 'Cannot read configuration',
180
+ save: 'Save',
181
+ saving: 'Saving…',
182
+ saved: 'Saved. The plugin is reloading…',
183
+ saveError: 'Save failed',
184
+ reload: 'Reload',
185
+ dirty: 'Unsaved changes',
186
+ token: 'Access token',
187
+ tokenHint: 'Stored in the profile YAML. Prefer auth.tokenEnv instead.',
188
+ tokenPlaceholder: 'Leave empty to keep',
189
+ clearToken: 'Clear the saved inline token',
190
+ tokenConfigured: 'Inline token configured',
191
+ tokenNotConfigured: 'No inline token',
192
+ tokenEnvConfigured: 'Environment variable: {name}',
193
+ openCard: 'Open Agent Card',
194
+ seconds: 's',
195
+ minutes: 'm',
196
+ hours: 'h',
197
+ unknown: '—',
198
+ groupCard: 'Identity',
199
+ groupServer: 'Service & capabilities',
200
+ groupAuth: 'Authentication',
201
+ groupAgent: 'Agent runtime',
202
+ groupLimits: 'Limits & retention',
203
+ fieldCardName: 'Name',
204
+ fieldCardDescription: 'Description',
205
+ fieldCardVersion: 'Version',
206
+ fieldServerPublicUrl: 'Public URL',
207
+ fieldServerHost: 'Listen host',
208
+ fieldServerPort: 'Listen port',
209
+ fieldServerMountPath: 'Mount path',
210
+ fieldServerStreaming: 'Streaming SSE',
211
+ fieldServerPushNotifications: 'Push notifications',
212
+ fieldServerExtendedCard: 'Extended Agent Card',
213
+ fieldAuthMode: 'Auth mode',
214
+ fieldAuthTokenEnv: 'Token environment variable',
215
+ fieldAuthAllowRemoteUnauthenticated: 'Allow unauthenticated remote callers',
216
+ fieldAgentCwd: 'Working directory',
217
+ fieldAgentPreset: 'Agent preset',
218
+ fieldAgentProvider: 'Model provider',
219
+ fieldAgentModel: 'Model',
220
+ fieldAgentReasoningEffort: 'Reasoning effort',
221
+ fieldAgentSandboxMode: 'Sandbox mode',
222
+ fieldAgentApprovalPolicy: 'Approval policy',
223
+ fieldAgentMaxConcurrentTasks: 'Max concurrent tasks',
224
+ fieldAgentQueueTimeoutMs: 'Queue timeout (ms)',
225
+ fieldAgentTurnTimeoutMs: 'Turn timeout (ms)',
226
+ fieldLimitsMaxRequestBytes: 'Request body limit (bytes)',
227
+ fieldLimitsMaxFileBytes: 'Attachment limit (bytes)',
228
+ fieldTasksTtlMs: 'Task retention (ms)',
229
+ fieldTasksMaxTasks: 'Max tasks',
230
+ fieldPushAllowPrivateTargets: 'Allow private webhooks',
231
+ enumNone: 'None',
232
+ enumBearer: 'Bearer',
233
+ enumApiKey: 'API Key',
234
+ enumInherit: 'Inherit deployment',
235
+ enumReadOnly: 'Read-only',
236
+ enumWorkspaceWrite: 'Workspace write',
237
+ enumDangerFullAccess: 'Full access',
238
+ enumAsk: 'Ask',
239
+ enumNever: 'Never',
240
+ restartHint: 'Refresh the page after changing the mount path.',
241
+ }
242
+
243
+ const CSS = `
244
+ .a2aui-page{display:flex;flex-direction:column;gap:16px;max-width:860px;color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px}
245
+ .a2aui-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px}
246
+ .a2aui-brand{display:flex;align-items:center;gap:14px;min-width:0}
247
+ .a2aui-logo{width:44px;height:44px;flex:none}
248
+ .a2aui-title{margin:0;font-size:18px;font-weight:600;line-height:26px}
249
+ .a2aui-desc{margin:2px 0 0;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px}
250
+ .a2aui-card{border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-lg);background:var(--dsw-alias-bg-layer-1);padding:14px 16px;display:flex;flex-direction:column;gap:10px;min-width:0}
251
+ .a2aui-section-title{margin:0;font-size:13px;font-weight:600;line-height:20px}
252
+ .a2aui-badges{display:flex;flex-wrap:wrap;gap:6px}
253
+ .a2aui-badge{border:.5px solid var(--dsw-alias-border-l3);border-radius:999px;padding:1px 9px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);white-space:nowrap}
254
+ .a2aui-badge[data-on=true]{border-color:color-mix(in srgb,var(--dsw-alias-state-success-primary) 45%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 10%,transparent);color:var(--dsw-alias-label-primary)}
255
+ .a2aui-badge[data-tone=warn]{border-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 45%,transparent);background:var(--dsw-alias-state-warn-tertiary);color:var(--dsw-alias-label-primary)}
256
+ .a2aui-row{display:flex;align-items:center;gap:10px;min-width:0;border-top:.5px solid var(--dsw-alias-border-l2);padding-top:10px}
257
+ .a2aui-row:first-of-type{border-top:0;padding-top:0}
258
+ .a2aui-row-label{flex:none;width:88px;color:var(--dsw-alias-label-secondary);font-size:12px}
259
+ .a2aui-code{flex:1;min-width:0;overflow-wrap:anywhere;font-family:var(--dsw-font-mono,ui-monospace,SFMono-Regular,Menlo,monospace);font-size:12px;color:var(--dsw-alias-label-primary)}
260
+ .a2aui-btn{flex:none;border:.5px solid var(--dsw-alias-border-l3);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:var(--dsw-radius-md);height:26px;padding:0 10px;font:inherit;font-size:12px;cursor:pointer}
261
+ .a2aui-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}
262
+ .a2aui-btn:disabled{opacity:.5;cursor:default}
263
+ .a2aui-btn[data-copied=true]{color:var(--dsw-alias-state-success-primary);border-color:color-mix(in srgb,var(--dsw-alias-state-success-primary) 45%,transparent)}
264
+ .a2aui-btnPrimary{background:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-label-primary-foreground)}
265
+ .a2aui-facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px}
266
+ .a2aui-fact{display:flex;flex-direction:column;gap:1px}
267
+ .a2aui-fact-key{color:var(--dsw-alias-label-tertiary);font-size:11.5px}
268
+ .a2aui-fact-value{font-size:15px;font-weight:600;line-height:22px;font-variant-numeric:tabular-nums}
269
+ .a2aui-skills{display:flex;flex-direction:column;gap:8px;margin:0;padding:0;list-style:none}
270
+ .a2aui-skill-name{font-weight:600}
271
+ .a2aui-skill-desc{color:var(--dsw-alias-label-secondary);margin:1px 0 0}
272
+ .a2aui-muted{color:var(--dsw-alias-label-tertiary);margin:0}
273
+ .a2aui-pre{margin:0;border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-2);padding:10px 12px;overflow:auto;max-height:260px;font-family:var(--dsw-font-mono,ui-monospace,SFMono-Regular,Menlo,monospace);font-size:11.5px;line-height:17px;white-space:pre;color:var(--dsw-alias-label-primary)}
274
+ .a2aui-details summary{cursor:pointer;color:var(--dsw-alias-label-secondary);font-size:12px;user-select:none}
275
+ .a2aui-foot{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
276
+ .a2aui-link{color:var(--dsw-alias-state-business-primary);text-decoration:none;font-size:12px}
277
+ .a2aui-link:hover{text-decoration:underline}
278
+ .a2aui-error{border:.5px solid color-mix(in srgb,var(--dsw-alias-state-error-primary) 40%,transparent);border-radius:var(--dsw-radius-lg);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 6%,transparent);padding:14px 16px;display:flex;flex-direction:column;gap:4px}
279
+ .a2aui-error-title{margin:0;font-weight:600}
280
+ .a2aui-form{display:flex;flex-direction:column;gap:14px}
281
+ .a2aui-group{display:flex;flex-direction:column;gap:8px}
282
+ .a2aui-group-title{margin:0;color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;line-height:18px}
283
+ .a2aui-field{display:grid;grid-template-columns:190px minmax(0,1fr);gap:10px;align-items:center;min-width:0}
284
+ .a2aui-field>label{color:var(--dsw-alias-label-secondary);font-size:12.5px;line-height:18px;overflow-wrap:anywhere}
285
+ .a2aui-input,.a2aui-select,.a2aui-textarea{box-sizing:border-box;width:100%;min-width:0;border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font:inherit;font-size:12.5px;padding:5px 9px;outline:none}
286
+ .a2aui-input:focus,.a2aui-select:focus,.a2aui-textarea:focus{border-color:var(--dsw-alias-state-business-primary);box-shadow:inset 0 0 0 .5px var(--dsw-alias-state-business-primary)}
287
+ .a2aui-textarea{resize:vertical;min-height:56px;line-height:18px}
288
+ .a2aui-check{display:inline-flex;align-items:center;gap:8px;color:var(--dsw-alias-label-primary);font-size:12.5px}
289
+ .a2aui-check input{width:15px;height:15px;accent-color:var(--dsw-alias-brand-primary)}
290
+ .a2aui-field-note{color:var(--dsw-alias-label-tertiary);font-size:11.5px;margin:0}
291
+ .a2aui-form-foot{display:flex;align-items:center;gap:10px;flex-wrap:wrap;border-top:.5px solid var(--dsw-alias-border-l2);padding-top:12px}
292
+ .a2aui-status{font-size:12px;color:var(--dsw-alias-label-secondary);margin:0}
293
+ .a2aui-status[data-tone=error]{color:var(--dsw-alias-state-error-primary)}
294
+ .a2aui-status[data-tone=ok]{color:var(--dsw-alias-state-success-primary)}
295
+ .a2aui-secret{display:flex;flex-direction:column;gap:6px}
296
+ `
297
+
298
+ function copyText(text) {
299
+ try {
300
+ if (navigator.clipboard && navigator.clipboard.writeText) {
301
+ return navigator.clipboard.writeText(text).then(
302
+ () => true,
303
+ () => fallbackCopy(text),
304
+ )
305
+ }
306
+ } catch {
307
+ /* fall through */
308
+ }
309
+ return Promise.resolve(fallbackCopy(text))
310
+ }
311
+
312
+ function fallbackCopy(text) {
313
+ try {
314
+ const area = document.createElement('textarea')
315
+ area.value = text
316
+ area.setAttribute('readonly', '')
317
+ area.style.position = 'fixed'
318
+ area.style.opacity = '0'
319
+ document.body.appendChild(area)
320
+ area.select()
321
+ const ok = document.execCommand('copy')
322
+ area.remove()
323
+ return ok
324
+ } catch {
325
+ return false
326
+ }
327
+ }
328
+
329
+ function formatUptime(seconds, t) {
330
+ if (typeof seconds !== 'number' || !Number.isFinite(seconds)) return t('unknown')
331
+ if (seconds < 60) return `${Math.round(seconds)}${t('seconds')}`
332
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}${t('minutes')} ${Math.round(seconds % 60)}${t('seconds')}`
333
+ return `${Math.floor(seconds / 3600)}${t('hours')} ${Math.floor((seconds % 3600) / 60)}${t('minutes')}`
334
+ }
335
+
336
+ const GROUP_LABELS = {
337
+ card: 'groupCard',
338
+ server: 'groupServer',
339
+ auth: 'groupAuth',
340
+ agent: 'groupAgent',
341
+ limits: 'groupLimits',
342
+ }
343
+
344
+ function humanize(path) {
345
+ const tail = path.split('.').pop() ?? path
346
+ return tail.replace(/([A-Z])/g, ' $1').replace(/^./, (value) => value.toUpperCase())
347
+ }
348
+
349
+ function fieldLabel(t, path) {
350
+ const key = `field${path
351
+ .split('.')
352
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
353
+ .join('')}`
354
+ const value = t(key)
355
+ return typeof value === 'string' && value !== key ? value : humanize(path)
356
+ }
357
+
358
+ function groupLabel(t, group) {
359
+ const key = GROUP_LABELS[group] ?? group
360
+ const value = t(key)
361
+ return typeof value === 'string' && value !== key ? value : group
362
+ }
363
+
364
+ function enumLabel(t, value) {
365
+ const key = `enum${value.replace(/[-_]/g, '').replace(/^./, (character) => character.toUpperCase())}`
366
+ const label = t(key)
367
+ return typeof label === 'string' && label !== key ? label : value
368
+ }
369
+
370
+ function draftFrom(panel) {
371
+ const values = {}
372
+ for (const field of panel && Array.isArray(panel.fields) ? panel.fields : []) values[field.path] = field.value
373
+ return values
374
+ }
375
+
376
+ function sameValue(field, value) {
377
+ if (field.type === 'integer') {
378
+ if (value === '' || value === null || value === undefined) return field.value === null
379
+ return Number(value) === Number(field.value)
380
+ }
381
+ if (field.type === 'boolean') return Boolean(value) === Boolean(field.value)
382
+ return String(value ?? '') === String(field.value ?? '')
383
+ }
384
+
385
+ function serializeValue(field, value) {
386
+ if (field.type === 'integer') {
387
+ if (value === '' || value === null || value === undefined) return null
388
+ return Number(value)
389
+ }
390
+ if (field.type === 'boolean') return Boolean(value)
391
+ if (field.nullable && String(value ?? '').trim() === '') return null
392
+ return value
393
+ }
394
+
395
+ function CopyButton({ text, label, copied, onCopy, id }) {
396
+ const done = copied === id
397
+ return h(
398
+ 'button',
399
+ {
400
+ type: 'button',
401
+ className: 'a2aui-btn',
402
+ 'data-copied': done ? 'true' : 'false',
403
+ onClick: () => onCopy(id, text),
404
+ },
405
+ done ? label.copied : label.copy,
406
+ )
407
+ }
408
+
409
+ function createTab(t) {
410
+ return function A2AServerPanel() {
411
+ const [state, setState] = React.useState({
412
+ status: 'loading',
413
+ card: null,
414
+ health: null,
415
+ denied: false,
416
+ error: null,
417
+ panel: null,
418
+ panelPath: null,
419
+ panelError: null,
420
+ })
421
+ const [draft, setDraft] = React.useState({})
422
+ const [token, setToken] = React.useState('')
423
+ const [clearToken, setClearToken] = React.useState(false)
424
+ const [saving, setSaving] = React.useState(false)
425
+ const [saved, setSaved] = React.useState(false)
426
+ const [saveError, setSaveError] = React.useState(null)
427
+ const [copied, setCopied] = React.useState(null)
428
+ const [reloadKey, setReloadKey] = React.useState(0)
429
+
430
+ React.useEffect(() => {
431
+ const controller = new AbortController()
432
+ setState((previous) => ({ ...previous, status: 'loading', error: null }))
433
+ setSaved(false)
434
+ setSaveError(null)
435
+ ;(async () => {
436
+ try {
437
+ const response = await fetch(CARD_PATH, { cache: 'no-store', signal: controller.signal })
438
+ if (!response.ok) {
439
+ setState({ status: response.status === 404 ? 'missing' : 'error', card: null, health: null, denied: false, error: `HTTP ${response.status}`, panel: null, panelPath: null, panelError: null })
440
+ return
441
+ }
442
+ const card = await response.json()
443
+ let health = null
444
+ let denied = false
445
+ let panel = null
446
+ let panelPath = null
447
+ let panelError = null
448
+ const jsonrpc = Array.isArray(card.supportedInterfaces)
449
+ ? card.supportedInterfaces.find((entry) => entry.protocolBinding === 'JSONRPC')
450
+ : undefined
451
+ if (jsonrpc && typeof jsonrpc.url === 'string') {
452
+ try {
453
+ const url = new URL(jsonrpc.url, window.location.origin)
454
+ const pathname = url.pathname.replace(/\/$/, '')
455
+ // The A2A port owns health; the panel stays on the GUI origin
456
+ // so the settings form reuses the browser's Harness session.
457
+ const endpointBase = `${url.origin}${pathname}`
458
+ const panelBase = `${window.location.origin}${pathname}`
459
+ // Prefer the same-origin panel: it carries the health snapshot
460
+ // and avoids a cross-origin read on the dedicated A2A port.
461
+ panelPath = `${panelBase}/panel/config`
462
+ try {
463
+ const panelResponse = await fetch(panelPath, { cache: 'no-store', signal: controller.signal, credentials: 'same-origin' })
464
+ if (panelResponse.ok) panel = await panelResponse.json()
465
+ else if (panelResponse.status === 401 || panelResponse.status === 403) panelError = 'unauthorized'
466
+ else if (panelResponse.status === 501) panelError = 'unavailable'
467
+ else panelError = 'error'
468
+ } catch {
469
+ panelError = 'error'
470
+ }
471
+ if (panel && panel.health) {
472
+ health = panel.health
473
+ } else {
474
+ try {
475
+ const healthResponse = await fetch(`${endpointBase}/health`, { cache: 'no-store', signal: controller.signal })
476
+ if (healthResponse.ok) health = await healthResponse.json()
477
+ else denied = healthResponse.status === 401 || healthResponse.status === 403
478
+ } catch {
479
+ /* standalone listener down: the panel status reports it */
480
+ }
481
+ }
482
+ } catch {
483
+ denied = true
484
+ }
485
+ }
486
+ setState({ status: 'ready', card, health, denied, error: null, panel, panelPath, panelError })
487
+ setDraft(draftFrom(panel))
488
+ } catch (error) {
489
+ if (error && error.name === 'AbortError') return
490
+ setState({ status: 'error', card: null, health: null, denied: false, error: String((error && error.message) || error), panel: null, panelPath: null, panelError: null })
491
+ }
492
+ })()
493
+ return () => controller.abort()
494
+ }, [reloadKey])
495
+
496
+ const writeClipboard = React.useCallback((id, text) => {
497
+ copyText(text).then((ok) => {
498
+ if (!ok) return
499
+ setCopied(id)
500
+ window.setTimeout(() => setCopied((current) => (current === id ? null : current)), 1600)
501
+ })
502
+ }, [])
503
+
504
+ const card = state.card
505
+ const health = state.health
506
+ const panel = state.panel
507
+ const jsonrpc = card && Array.isArray(card.supportedInterfaces)
508
+ ? card.supportedInterfaces.find((entry) => entry.protocolBinding === 'JSONRPC')
509
+ : undefined
510
+ const rest = card && Array.isArray(card.supportedInterfaces)
511
+ ? card.supportedInterfaces.find((entry) => entry.protocolBinding === 'HTTP+JSON')
512
+ : undefined
513
+ const endpoint = jsonrpc && jsonrpc.url ? jsonrpc.url : ''
514
+ const security = card && card.securitySchemes ? Object.keys(card.securitySchemes) : []
515
+ const authRequired = security.length > 0
516
+ const authLine = authRequired ? " \\\n -H 'authorization: Bearer $A2A_TOKEN'" : ''
517
+ const curl = endpoint
518
+ ? `curl -s ${endpoint} \\\n -H 'content-type: application/json' \\\n -H 'a2a-version: 1.0'${authLine} \\\n -d '{"jsonrpc":"2.0","id":1,"method":"SendMessage","params":{"message":{"messageId":"m-1","role":"ROLE_USER","parts":[{"text":"Hello"}]}}}'`
519
+ : ''
520
+ const js = endpoint
521
+ ? `import { ClientFactory, Role } from '@a2a-js/sdk/client'\n\nconst client = await new ClientFactory().createFromUrl('${new URL(endpoint, window.location.origin).origin}')\nconst result = await client.sendMessage({\n message: {\n messageId: crypto.randomUUID(),\n role: Role.ROLE_USER,\n parts: [{ content: { $case: 'text', value: 'Hello' } }],\n },\n})\nconsole.log(result)`
522
+ : ''
523
+
524
+ const badge = (label, on, key) => h('span', { key, className: 'a2aui-badge', 'data-on': on ? 'true' : 'false' }, label)
525
+
526
+ const header = h(
527
+ 'div',
528
+ { className: 'a2aui-head' },
529
+ h(
530
+ 'div',
531
+ { className: 'a2aui-brand' },
532
+ h(
533
+ 'svg',
534
+ { className: 'a2aui-logo', viewBox: '0 0 64 64', 'aria-hidden': true },
535
+ h('rect', { x: 2, y: 2, width: 60, height: 60, rx: 14, fill: '#2f6df6' }),
536
+ h('circle', { cx: 18, cy: 32, r: 7, fill: 'none', stroke: '#fff', strokeWidth: 3.5 }),
537
+ h('circle', { cx: 46, cy: 32, r: 7, fill: 'none', stroke: '#fff', strokeWidth: 3.5 }),
538
+ h('path', { d: 'M25.5 32h13', stroke: '#fff', strokeWidth: 3.5, strokeLinecap: 'round' }),
539
+ h('path', { d: 'M32 25.5l6.5 6.5-6.5 6.5', fill: 'none', stroke: '#fff', strokeWidth: 3.5, strokeLinecap: 'round', strokeLinejoin: 'round' }),
540
+ ),
541
+ h(
542
+ 'div',
543
+ null,
544
+ h('h3', { className: 'a2aui-title' }, card ? card.name : t('heading')),
545
+ h('p', { className: 'a2aui-desc' }, card ? card.description : t('intro')),
546
+ ),
547
+ ),
548
+ h('button', { type: 'button', className: 'a2aui-btn', onClick: () => setReloadKey((value) => value + 1) }, t('refresh')),
549
+ )
550
+
551
+ if (state.status === 'loading' && !card) {
552
+ return h('div', { className: 'a2aui-page' }, header, h('p', { className: 'a2aui-muted' }, t('loading')))
553
+ }
554
+
555
+ if (!card) {
556
+ const missing = state.status === 'missing'
557
+ return h(
558
+ 'div',
559
+ { className: 'a2aui-page' },
560
+ header,
561
+ h(
562
+ 'div',
563
+ { className: 'a2aui-error' },
564
+ h('p', { className: 'a2aui-error-title' }, missing ? t('disabled') : t('loadError')),
565
+ h('p', { className: 'a2aui-muted' }, missing ? t('disabledHint') : t('loadErrorHint')),
566
+ state.error ? h('p', { className: 'a2aui-muted' }, state.error) : null,
567
+ ),
568
+ )
569
+ }
570
+
571
+ const dirty = panel
572
+ ? panel.fields.some((field) => !sameValue(field, draft[field.path])) || token !== '' || clearToken
573
+ : false
574
+
575
+ const setField = (path, value) => {
576
+ setDraft((previous) => ({ ...previous, [path]: value }))
577
+ setSaved(false)
578
+ setSaveError(null)
579
+ }
580
+
581
+ const save = async () => {
582
+ if (!panel || !state.panelPath || saving) return
583
+ const values = {}
584
+ for (const field of panel.fields) {
585
+ if (sameValue(field, draft[field.path])) continue
586
+ values[field.path] = serializeValue(field, draft[field.path])
587
+ }
588
+ const body = { values }
589
+ if (clearToken) body.clearToken = true
590
+ else if (token !== '') body.token = token
591
+ setSaving(true)
592
+ setSaved(false)
593
+ setSaveError(null)
594
+ try {
595
+ const response = await fetch(state.panelPath, {
596
+ method: 'PUT',
597
+ credentials: 'same-origin',
598
+ headers: { 'content-type': 'application/json' },
599
+ body: JSON.stringify(body),
600
+ })
601
+ const payload = await response.json().catch(() => ({}))
602
+ if (!response.ok) throw new Error(payload.message || payload.error || `HTTP ${response.status}`)
603
+ setState((previous) => ({ ...previous, panel: payload, panelError: null }))
604
+ setDraft(draftFrom(payload))
605
+ setToken('')
606
+ setClearToken(false)
607
+ setSaved(true)
608
+ window.setTimeout(() => setReloadKey((value) => value + 1), 900)
609
+ } catch (error) {
610
+ setSaveError(String((error && error.message) || error))
611
+ } finally {
612
+ setSaving(false)
613
+ }
614
+ }
615
+
616
+ const renderControl = (field) => {
617
+ const value = draft[field.path]
618
+ const onChange = (event) => setField(field.path, event.target.value)
619
+ if (field.type === 'boolean') {
620
+ return h(
621
+ 'label',
622
+ { className: 'a2aui-check' },
623
+ h('input', { type: 'checkbox', checked: Boolean(value), onChange: (event) => setField(field.path, event.target.checked) }),
624
+ value ? 'true' : 'false',
625
+ )
626
+ }
627
+ if (field.type === 'enum') {
628
+ return h(
629
+ 'select',
630
+ { className: 'a2aui-select', value: value ?? '', onChange },
631
+ (field.options ?? []).map((option) => h('option', { key: option, value: option }, enumLabel(t, option))),
632
+ )
633
+ }
634
+ if (field.type === 'textarea') {
635
+ return h('textarea', { className: 'a2aui-textarea', rows: 2, value: value ?? '', onChange })
636
+ }
637
+ if (field.type === 'integer') {
638
+ return h('input', { className: 'a2aui-input', type: 'number', min: field.min, max: field.max, value: value ?? '', onChange })
639
+ }
640
+ return h('input', {
641
+ className: 'a2aui-input',
642
+ type: 'text',
643
+ value: value ?? '',
644
+ placeholder: field.placeholder || (field.nullable ? t('unknown') : ''),
645
+ onChange,
646
+ })
647
+ }
648
+
649
+ const groups = panel
650
+ ? [...new Set(panel.fields.map((field) => field.group))].map((group) => [group, panel.fields.filter((field) => field.group === group)])
651
+ : []
652
+
653
+ const secretBlock = panel
654
+ ? h(
655
+ 'div',
656
+ { className: 'a2aui-group' },
657
+ h('h5', { className: 'a2aui-group-title' }, t('token')),
658
+ h(
659
+ 'div',
660
+ { className: 'a2aui-secret' },
661
+ h('input', {
662
+ className: 'a2aui-input',
663
+ type: 'password',
664
+ autoComplete: 'new-password',
665
+ value: token,
666
+ placeholder: t('tokenPlaceholder'),
667
+ onChange: (event) => {
668
+ setToken(event.target.value)
669
+ setSaved(false)
670
+ setSaveError(null)
671
+ },
672
+ }),
673
+ h(
674
+ 'label',
675
+ { className: 'a2aui-check' },
676
+ h('input', {
677
+ type: 'checkbox',
678
+ checked: clearToken,
679
+ onChange: (event) => {
680
+ setClearToken(event.target.checked)
681
+ setSaved(false)
682
+ },
683
+ }),
684
+ t('clearToken'),
685
+ ),
686
+ h(
687
+ 'p',
688
+ { className: 'a2aui-field-note' },
689
+ panel.secrets && panel.secrets.inline ? t('tokenConfigured') : t('tokenNotConfigured'),
690
+ panel.secrets && panel.secrets.tokenEnv
691
+ ? ` · ${t('tokenEnvConfigured').replace('{name}', panel.secrets.tokenEnv)}`
692
+ : '',
693
+ ),
694
+ h('p', { className: 'a2aui-field-note' }, t('tokenHint')),
695
+ ),
696
+ )
697
+ : null
698
+
699
+ const configSection = h(
700
+ 'section',
701
+ { className: 'a2aui-card' },
702
+ h('h4', { className: 'a2aui-section-title' }, t('config')),
703
+ panel
704
+ ? h(
705
+ 'div',
706
+ { className: 'a2aui-form' },
707
+ ...groups.map(([group, fields]) =>
708
+ h(
709
+ 'div',
710
+ { className: 'a2aui-group', key: group },
711
+ h('h5', { className: 'a2aui-group-title' }, groupLabel(t, group)),
712
+ ...fields.map((field) =>
713
+ h(
714
+ 'div',
715
+ { className: 'a2aui-field', key: field.path },
716
+ h('label', { htmlFor: `a2aui-${field.path}` }, fieldLabel(t, field.path)),
717
+ h('div', null, renderControl(field)),
718
+ ),
719
+ ),
720
+ ),
721
+ ),
722
+ secretBlock,
723
+ h(
724
+ 'div',
725
+ { className: 'a2aui-form-foot' },
726
+ h(
727
+ 'button',
728
+ { type: 'button', className: 'a2aui-btn a2aui-btnPrimary', disabled: !dirty || saving, onClick: save },
729
+ saving ? t('saving') : t('save'),
730
+ ),
731
+ h('button', { type: 'button', className: 'a2aui-btn', onClick: () => setReloadKey((value) => value + 1) }, t('reload')),
732
+ dirty ? h('span', { className: 'a2aui-status' }, t('dirty')) : null,
733
+ saved ? h('span', { className: 'a2aui-status', 'data-tone': 'ok' }, t('saved')) : null,
734
+ saveError ? h('span', { className: 'a2aui-status', 'data-tone': 'error' }, `${t('saveError')}: ${saveError}`) : null,
735
+ h('span', { className: 'a2aui-status' }, t('configHint')),
736
+ h('span', { className: 'a2aui-status' }, t('restartHint')),
737
+ ),
738
+ )
739
+ : h(
740
+ 'p',
741
+ { className: 'a2aui-muted' },
742
+ state.panelError === 'unauthorized'
743
+ ? t('configUnauthorized')
744
+ : state.panelError === 'unavailable'
745
+ ? t('configUnavailable')
746
+ : `${t('configLoadError')}${state.panelPath ? `(${state.panelPath})` : ''}`,
747
+ ),
748
+ )
749
+
750
+ const statusSection = h(
751
+ 'section',
752
+ { className: 'a2aui-card' },
753
+ h('h4', { className: 'a2aui-section-title' }, t('capabilities')),
754
+ h(
755
+ 'div',
756
+ { className: 'a2aui-badges' },
757
+ badge(`${t('version')} ${jsonrpc ? jsonrpc.protocolVersion || '1.0' : '1.0'}`, true),
758
+ badge(t('streaming'), Boolean(card.capabilities && card.capabilities.streaming)),
759
+ badge(t('push'), Boolean(card.capabilities && card.capabilities.pushNotifications)),
760
+ badge(t('extended'), Boolean(card.capabilities && card.capabilities.extendedAgentCard)),
761
+ h(
762
+ 'span',
763
+ { className: 'a2aui-badge', 'data-tone': authRequired ? 'warn' : undefined },
764
+ `${t('auth')}: ${authRequired ? t('authOn') : t('authNone')}`,
765
+ ),
766
+ ),
767
+ )
768
+
769
+ const endpointSection = h(
770
+ 'section',
771
+ { className: 'a2aui-card' },
772
+ h('h4', { className: 'a2aui-section-title' }, t('interfaces')),
773
+ jsonrpc
774
+ ? h(
775
+ 'div',
776
+ { className: 'a2aui-row' },
777
+ h('span', { className: 'a2aui-row-label' }, t('jsonrpc')),
778
+ h('code', { className: 'a2aui-code' }, jsonrpc.url),
779
+ h(CopyButton, { id: 'jsonrpc', text: jsonrpc.url, label: { copy: t('copy'), copied: t('copied') }, copied, onCopy: writeClipboard }),
780
+ )
781
+ : null,
782
+ rest
783
+ ? h(
784
+ 'div',
785
+ { className: 'a2aui-row' },
786
+ h('span', { className: 'a2aui-row-label' }, t('rest')),
787
+ h('code', { className: 'a2aui-code' }, rest.url),
788
+ h(CopyButton, { id: 'rest', text: rest.url, label: { copy: t('copy'), copied: t('copied') }, copied, onCopy: writeClipboard }),
789
+ )
790
+ : null,
791
+ h(
792
+ 'div',
793
+ { className: 'a2aui-foot' },
794
+ h('a', { className: 'a2aui-link', href: CARD_PATH, target: '_blank', rel: 'noreferrer' }, t('openCard')),
795
+ ),
796
+ )
797
+
798
+ const serverInfo = (state.panel && state.panel.server) || (health && health.server) || null
799
+ const serverSection = serverInfo
800
+ ? h(
801
+ 'section',
802
+ { className: 'a2aui-card' },
803
+ h('h4', { className: 'a2aui-section-title' }, t('serverStatus')),
804
+ h(
805
+ 'div',
806
+ { className: 'a2aui-facts' },
807
+ h('div', { className: 'a2aui-fact' }, h('span', { className: 'a2aui-fact-key' }, t('serverMode')), h('span', { className: 'a2aui-fact-value' }, serverInfo.mode === 'standalone' ? t('serverModeStandalone') : t('serverModeShared'))),
808
+ serverInfo.mode === 'standalone'
809
+ ? h('div', { className: 'a2aui-fact' }, h('span', { className: 'a2aui-fact-key' }, t('serverAddress')), h('span', { className: 'a2aui-fact-value' }, `${serverInfo.host}:${serverInfo.port}`))
810
+ : null,
811
+ h('div', { className: 'a2aui-fact' }, h('span', { className: 'a2aui-fact-key' }, t('serverListening')), h('span', { className: 'a2aui-fact-value' }, serverInfo.listening ? t('serverListeningYes') : t('serverListeningNo'))),
812
+ ),
813
+ serverInfo.error
814
+ ? h('p', { className: 'a2aui-status', 'data-tone': 'error' }, `${t('serverError')}: ${serverInfo.error}`)
815
+ : null,
816
+ serverInfo.mode === 'standalone' && serverInfo.listening === false
817
+ ? h('p', { className: 'a2aui-status', 'data-tone': 'warn' }, t('serverNotListening'))
818
+ : null,
819
+ )
820
+ : null
821
+
822
+ const runtimeSection = health
823
+ ? h(
824
+ 'section',
825
+ { className: 'a2aui-card' },
826
+ h('h4', { className: 'a2aui-section-title' }, t('runtime')),
827
+ h(
828
+ 'div',
829
+ { className: 'a2aui-facts' },
830
+ h('div', { className: 'a2aui-fact' }, h('span', { className: 'a2aui-fact-key' }, t('tasks')), h('span', { className: 'a2aui-fact-value' }, String(health.tasks ? health.tasks.tasks : t('unknown')))),
831
+ h('div', { className: 'a2aui-fact' }, h('span', { className: 'a2aui-fact-key' }, t('contexts')), h('span', { className: 'a2aui-fact-value' }, String(health.agents ? health.agents.contexts : t('unknown')))),
832
+ h('div', { className: 'a2aui-fact' }, h('span', { className: 'a2aui-fact-key' }, t('concurrency')), h('span', { className: 'a2aui-fact-value' }, health.concurrency ? `${health.concurrency.active}/${health.concurrency.max}` : t('unknown'))),
833
+ h('div', { className: 'a2aui-fact' }, h('span', { className: 'a2aui-fact-key' }, t('uptime')), h('span', { className: 'a2aui-fact-value' }, formatUptime(health.uptimeSeconds, t))),
834
+ ),
835
+ )
836
+ : null
837
+
838
+ const deniedSection = state.denied
839
+ ? h('section', { className: 'a2aui-card' }, h('p', { className: 'a2aui-muted' }, t('runtimeDenied')))
840
+ : null
841
+
842
+ const skills = Array.isArray(card.skills) ? card.skills : []
843
+ const skillsSection = h(
844
+ 'section',
845
+ { className: 'a2aui-card' },
846
+ h('h4', { className: 'a2aui-section-title' }, t('skills')),
847
+ skills.length === 0
848
+ ? h('p', { className: 'a2aui-muted' }, t('noSkills'))
849
+ : h(
850
+ 'ul',
851
+ { className: 'a2aui-skills' },
852
+ skills.map((skill) =>
853
+ h(
854
+ 'li',
855
+ { key: skill.id },
856
+ h('div', { className: 'a2aui-skill-name' }, skill.name),
857
+ h('p', { className: 'a2aui-skill-desc' }, skill.description),
858
+ Array.isArray(skill.tags) && skill.tags.length > 0
859
+ ? h('div', { className: 'a2aui-badges' }, skill.tags.map((tag) => badge(tag, false, tag)))
860
+ : null,
861
+ ),
862
+ ),
863
+ ),
864
+ )
865
+
866
+ const quickstartSection = h(
867
+ 'section',
868
+ { className: 'a2aui-card' },
869
+ h('h4', { className: 'a2aui-section-title' }, t('quickstart')),
870
+ h('pre', { className: 'a2aui-pre' }, curl),
871
+ h(
872
+ 'div',
873
+ { className: 'a2aui-foot' },
874
+ h(CopyButton, { id: 'curl', text: curl, label: { copy: t('copy'), copied: t('copied') }, copied, onCopy: writeClipboard }),
875
+ h(
876
+ 'details',
877
+ { className: 'a2aui-details' },
878
+ h('summary', null, t('jsClient')),
879
+ h('pre', { className: 'a2aui-pre' }, js),
880
+ ),
881
+ h(CopyButton, { id: 'js', text: js, label: { copy: t('copy'), copied: t('copied') }, copied, onCopy: writeClipboard }),
882
+ ),
883
+ )
884
+
885
+ return h(
886
+ 'div',
887
+ { className: 'a2aui-page' },
888
+ header,
889
+ statusSection,
890
+ endpointSection,
891
+ serverSection,
892
+ runtimeSection,
893
+ deniedSection,
894
+ configSection,
895
+ skillsSection,
896
+ quickstartSection,
897
+ )
898
+ }
899
+ }
900
+
901
+ return {
902
+ inject: ['slots', 'locale'],
903
+ apply(ctx) {
904
+ const t = ctx.locale.bind(NS)
905
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'a2a-ui: dictionaries')
906
+ ctx.effect(() => {
907
+ const tag = document.createElement('style')
908
+ tag.dataset.plugin = '@ai_mind/dsh-a2a'
909
+ tag.textContent = CSS
910
+ document.head.appendChild(tag)
911
+ return () => tag.remove()
912
+ }, 'a2a-ui: styles')
913
+ ctx.slots.inject('settings.plugins.tab', () =>
914
+ ctx.slots.register(
915
+ {
916
+ name: 'settings.plugins.tab',
917
+ id: 'a2a-server',
918
+ order: 20,
919
+ label: () => t('tab'),
920
+ },
921
+ createTab(t),
922
+ ),
923
+ )
924
+ },
925
+ }
926
+ },
927
+ })