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