@ai_mind/dsh-a2a 1.1.4 → 1.2.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/README.md CHANGED
@@ -115,7 +115,7 @@
115
115
  面板分三部分:
116
116
 
117
117
  1. **概览**:实时 Agent Card、JSON-RPC / HTTP+JSON 地址(一键复制)、流式/推送/扩展卡片能力、鉴权状态、运行状态与可复制的 `curl` / 官方 JS 客户端示例。
118
- 2. **配置表单**:直接编辑名称、描述、对外地址、挂载路径、流式/推送/扩展卡片、鉴权模式与令牌环境变量、访问令牌(写入后不再回显)、Agent 预设/模型/工作目录/沙箱/审批/并发/超时,以及请求体与任务保留限额。
118
+ 2. **配置表单**:直接编辑名称、描述、对外地址、挂载路径、流式/推送/扩展卡片、鉴权模式与令牌环境变量、访问令牌(写入后不再回显)、Agent 预设/模型/工作目录/沙箱/审批/并发/超时,以及请求体与任务保留限额。预设、模型提供方、模型和推理强度会从当前 Host 的 `agentPresets` / `llm` 服务动态列出选项;留空即使用部署默认,无法列出的自定义值仍可手动输入。
119
119
  3. **保存**:写入 profile 的 `cordis.patch.yml`(`a2a-server` 行),校验通过后插件自动重载;修改挂载路径后刷新页面即可。
120
120
 
121
121
  配置接口 `GET/PUT <mountPath>/panel/config` 始终位于 DSH Web 的 origin(独立端口模式下也不在 A2A 端口上),使用 **Harness Web 连接凭据**(不是 A2A token)鉴权:只有能打开 Settings 的已登录浏览器可以读写,远程 A2A 客户端即使知道路径也会收到 401。令牌字段是只写字段,GET 只返回“是否已配置”。
package/client.js CHANGED
@@ -108,6 +108,9 @@ window.__ModuleLoader__.load({
108
108
  fieldAgentProvider: '模型提供方',
109
109
  fieldAgentModel: '模型',
110
110
  fieldAgentReasoningEffort: '推理强度',
111
+ optionDefault: '(使用默认)',
112
+ defaultModel: '默认模型(留空时)',
113
+ modelPairHint: '模型提供方和模型必须同时选择,或都留空使用默认。',
111
114
  fieldAgentSandboxMode: '沙箱模式',
112
115
  fieldAgentApprovalPolicy: '审批策略',
113
116
  fieldAgentMaxConcurrentTasks: '最大并发任务',
@@ -220,6 +223,9 @@ window.__ModuleLoader__.load({
220
223
  fieldAgentProvider: 'Model provider',
221
224
  fieldAgentModel: 'Model',
222
225
  fieldAgentReasoningEffort: 'Reasoning effort',
226
+ optionDefault: '(use default)',
227
+ defaultModel: 'Default model (when empty)',
228
+ modelPairHint: 'Choose both a provider and a model, or leave both empty for the default.',
223
229
  fieldAgentSandboxMode: 'Sandbox mode',
224
230
  fieldAgentApprovalPolicy: 'Approval policy',
225
231
  fieldAgentMaxConcurrentTasks: 'Max concurrent tasks',
@@ -369,6 +375,15 @@ window.__ModuleLoader__.load({
369
375
  return typeof label === 'string' && label !== key ? label : value
370
376
  }
371
377
 
378
+ function panelOptionsPath(panelPath, provider, model) {
379
+ const base = panelPath.replace(/\/config$/, '/options')
380
+ const params = new URLSearchParams()
381
+ if (provider) params.set('provider', provider)
382
+ if (model) params.set('model', model)
383
+ const query = params.toString()
384
+ return query ? `${base}?${query}` : base
385
+ }
386
+
372
387
  function draftFrom(panel) {
373
388
  const values = {}
374
389
  for (const field of panel && Array.isArray(panel.fields) ? panel.fields : []) values[field.path] = field.value
@@ -428,6 +443,37 @@ window.__ModuleLoader__.load({
428
443
  const [saveError, setSaveError] = React.useState(null)
429
444
  const [copied, setCopied] = React.useState(null)
430
445
  const [reloadKey, setReloadKey] = React.useState(0)
446
+ const [optionSets, setOptionSets] = React.useState({})
447
+ const optionsRequest = React.useRef(0)
448
+
449
+ const loadOptions = async (panelPath, provider, model, signal) => {
450
+ if (!panelPath) return
451
+ const requestId = ++optionsRequest.current
452
+ try {
453
+ const response = await fetch(panelOptionsPath(panelPath, provider, model), {
454
+ cache: 'no-store',
455
+ credentials: 'same-origin',
456
+ signal,
457
+ })
458
+ if (!response.ok) return
459
+ const data = await response.json()
460
+ if (requestId !== optionsRequest.current) return
461
+ setOptionSets(data)
462
+ if (provider && !model && Array.isArray(data.models) && data.models.length > 0) {
463
+ const preferred =
464
+ data.default && data.default.provider === provider && data.models.some((entry) => entry.value === data.default.model)
465
+ ? data.default.model
466
+ : data.models[0].value
467
+ setDraft((previous) => {
468
+ if (previous['agent.provider'] !== provider || previous['agent.model']) return previous
469
+ return { ...previous, 'agent.model': preferred, 'agent.reasoningEffort': '' }
470
+ })
471
+ void loadOptions(panelPath, provider, preferred)
472
+ }
473
+ } catch {
474
+ /* options are optional; the text inputs remain usable */
475
+ }
476
+ }
431
477
 
432
478
  React.useEffect(() => {
433
479
  const controller = new AbortController()
@@ -487,6 +533,10 @@ window.__ModuleLoader__.load({
487
533
  }
488
534
  setState({ status: 'ready', card, health, denied, error: null, panel, panelPath, panelError })
489
535
  setDraft(draftFrom(panel))
536
+ if (panel && panelPath) {
537
+ const fieldValue = (path) => panel.fields.find((field) => field.path === path)?.value
538
+ void loadOptions(panelPath, fieldValue('agent.provider'), fieldValue('agent.model'), controller.signal)
539
+ }
490
540
  } catch (error) {
491
541
  if (error && error.name === 'AbortError') return
492
542
  setState({ status: 'error', card: null, health: null, denied: false, error: String((error && error.message) || error), panel: null, panelPath: null, panelError: null })
@@ -573,11 +623,35 @@ window.__ModuleLoader__.load({
573
623
  const dirty = panel
574
624
  ? panel.fields.some((field) => !sameValue(field, draft[field.path])) || token !== '' || clearToken
575
625
  : false
626
+ const invalidModelPair = Boolean(draft['agent.provider']) !== Boolean(draft['agent.model'])
576
627
 
577
628
  const setField = (path, value) => {
578
- setDraft((previous) => ({ ...previous, [path]: value }))
629
+ setDraft((previous) => {
630
+ const next = { ...previous, [path]: value }
631
+ if (path === 'agent.provider') {
632
+ next['agent.model'] = ''
633
+ next['agent.reasoningEffort'] = ''
634
+ } else if (path === 'agent.model') {
635
+ if (value === '') next['agent.provider'] = ''
636
+ else if (!previous['agent.provider']) {
637
+ next['agent.provider'] = optionSets.provider ?? optionSets.default?.provider ?? ''
638
+ }
639
+ next['agent.reasoningEffort'] = ''
640
+ }
641
+ return next
642
+ })
579
643
  setSaved(false)
580
644
  setSaveError(null)
645
+ if (path === 'agent.provider') {
646
+ void loadOptions(state.panelPath, value ?? '', '')
647
+ } else if (path === 'agent.model') {
648
+ if (value === '') {
649
+ void loadOptions(state.panelPath, '', '')
650
+ } else {
651
+ const provider = draft['agent.provider'] || optionSets.provider || optionSets.default?.provider || ''
652
+ void loadOptions(state.panelPath, provider, value)
653
+ }
654
+ }
581
655
  }
582
656
 
583
657
  const save = async () => {
@@ -618,6 +692,26 @@ window.__ModuleLoader__.load({
618
692
  const renderControl = (field) => {
619
693
  const value = draft[field.path]
620
694
  const onChange = (event) => setField(field.path, event.target.value)
695
+ const dynamicOptions = field.optionsKey ? optionSets[field.optionsKey] : undefined
696
+ if (field.optionsKey && Array.isArray(dynamicOptions) && dynamicOptions.length > 0) {
697
+ const current = value == null ? '' : String(value)
698
+ const options =
699
+ !current || dynamicOptions.some((option) => String(option.value) === current)
700
+ ? dynamicOptions
701
+ : [{ value: current, label: `${current} (current)` }, ...dynamicOptions]
702
+ return h(
703
+ 'select',
704
+ { className: 'a2aui-select', value: current, onChange },
705
+ h('option', { value: '' }, t('optionDefault')),
706
+ options.map((option) =>
707
+ h(
708
+ 'option',
709
+ { key: option.value, value: option.value },
710
+ option.description ? `${option.label} — ${option.description}` : option.label,
711
+ ),
712
+ ),
713
+ )
714
+ }
621
715
  if (field.type === 'boolean') {
622
716
  return h(
623
717
  'label',
@@ -719,6 +813,14 @@ window.__ModuleLoader__.load({
719
813
  h('div', null, renderControl(field)),
720
814
  ),
721
815
  ),
816
+ group === 'agent' && optionSets.default
817
+ ? h(
818
+ 'p',
819
+ { className: 'a2aui-field-note' },
820
+ `${t('defaultModel')}: ${optionSets.default.provider ?? '?'} / ${optionSets.default.model ?? '?'}` +
821
+ (optionSets.default.reasoningEffort ? ` · ${optionSets.default.reasoningEffort}` : ''),
822
+ )
823
+ : null,
722
824
  )
723
825
  return group === 'auth' ? [node, secretBlock] : [node]
724
826
  }),
@@ -727,11 +829,14 @@ window.__ModuleLoader__.load({
727
829
  { className: 'a2aui-form-foot' },
728
830
  h(
729
831
  'button',
730
- { type: 'button', className: 'a2aui-btn a2aui-btnPrimary', disabled: !dirty || saving, onClick: save },
832
+ { type: 'button', className: 'a2aui-btn a2aui-btnPrimary', disabled: !dirty || saving || invalidModelPair, onClick: save },
731
833
  saving ? t('saving') : t('save'),
732
834
  ),
733
835
  h('button', { type: 'button', className: 'a2aui-btn', onClick: () => setReloadKey((value) => value + 1) }, t('reload')),
734
836
  dirty ? h('span', { className: 'a2aui-status' }, t('dirty')) : null,
837
+ invalidModelPair
838
+ ? h('span', { className: 'a2aui-status', 'data-tone': 'warn' }, t('modelPairHint'))
839
+ : null,
735
840
  saved ? h('span', { className: 'a2aui-status', 'data-tone': 'ok' }, t('saved')) : null,
736
841
  saveError ? h('span', { className: 'a2aui-status', 'data-tone': 'error' }, `${t('saveError')}: ${saveError}`) : null,
737
842
  h('span', { className: 'a2aui-status' }, t('configHint')),
package/lib/panel.js CHANGED
@@ -49,10 +49,10 @@ const FIELDS = [
49
49
  { path: 'auth.tokenEnv', type: 'string', group: 'auth', nullable: true, placeholder: 'A2A_AUTH_TOKEN' },
50
50
  { path: 'auth.allowRemoteUnauthenticated', type: 'boolean', group: 'auth' },
51
51
  { path: 'agent.cwd', type: 'string', group: 'agent', nullable: true, placeholder: '/srv/a2a-workspace' },
52
- { path: 'agent.preset', type: 'string', group: 'agent', nullable: true },
53
- { path: 'agent.provider', type: 'string', group: 'agent', nullable: true },
54
- { path: 'agent.model', type: 'string', group: 'agent', nullable: true },
55
- { path: 'agent.reasoningEffort', type: 'string', group: 'agent', nullable: true },
52
+ { path: 'agent.preset', type: 'string', group: 'agent', nullable: true, optionsKey: 'presets' },
53
+ { path: 'agent.provider', type: 'string', group: 'agent', nullable: true, optionsKey: 'providers' },
54
+ { path: 'agent.model', type: 'string', group: 'agent', nullable: true, optionsKey: 'models' },
55
+ { path: 'agent.reasoningEffort', type: 'string', group: 'agent', nullable: true, optionsKey: 'reasoning' },
56
56
  {
57
57
  path: 'agent.sandboxMode',
58
58
  type: 'enum',
@@ -175,6 +175,7 @@ function describe(config, entry, warnings) {
175
175
  ...(field.min === undefined ? {} : { min: field.min }),
176
176
  ...(field.max === undefined ? {} : { max: field.max }),
177
177
  ...(field.placeholder === undefined ? {} : { placeholder: field.placeholder }),
178
+ ...(field.optionsKey === undefined ? {} : { optionsKey: field.optionsKey }),
178
179
  value: value === undefined ? null : value,
179
180
  }
180
181
  })
@@ -202,6 +203,23 @@ function describe(config, entry, warnings) {
202
203
  * @param {() => object} [options.health] live health snapshot
203
204
  * @returns {import('express').Router}
204
205
  */
206
+ /** First value of an express query parameter. */
207
+ function firstQuery(value) {
208
+ if (Array.isArray(value)) return value[0]
209
+ return typeof value === 'string' ? value : undefined
210
+ }
211
+
212
+ /** Resolve an optional Host call without letting a slow model catalog block the panel. */
213
+ function withTimeout(promise, fallback, ms) {
214
+ return Promise.race([
215
+ Promise.resolve(promise).catch(() => fallback),
216
+ new Promise((resolve) => {
217
+ const timer = setTimeout(() => resolve(fallback), ms)
218
+ timer.unref?.()
219
+ }),
220
+ ])
221
+ }
222
+
205
223
  function safeHealth(health, logger) {
206
224
  try {
207
225
  return health()
@@ -287,7 +305,7 @@ export function createPanelRouter({ ctx, config, mountPath, logger, status, heal
287
305
  res.setHeader('Cache-Control', 'no-store')
288
306
  res.json({
289
307
  ok: true,
290
- plugin: { name: PLUGIN_NAME, version: '1.1.4' },
308
+ plugin: { name: PLUGIN_NAME, version: '1.2.0' },
291
309
  mountPath,
292
310
  ...payload,
293
311
  ...(typeof status === 'function' ? { server: status() } : {}),
@@ -295,6 +313,88 @@ export function createPanelRouter({ ctx, config, mountPath, logger, status, heal
295
313
  })
296
314
  })
297
315
 
316
+ router.get('/options', async (req, res) => {
317
+ if (authorize(req, res)) return
318
+ const llm = ctx.get?.('llm')
319
+ const presetsService = ctx.get?.('agentPresets')
320
+ const defaults = ctx.get?.('agentDefaultModel')
321
+ let selection = null
322
+ try {
323
+ selection = defaults?.currentSelection?.() ?? null
324
+ } catch (error) {
325
+ logger.warn(`[a2a] cannot read the default model selection: ${errorMessage(error)}`)
326
+ }
327
+ const requestedProvider = firstQuery(req.query.provider)?.trim() || null
328
+ const requestedModel = firstQuery(req.query.model)?.trim() || null
329
+ const provider = requestedProvider ?? selection?.provider ?? null
330
+ const model = requestedModel ?? (provider === selection?.provider ? selection?.model : null) ?? null
331
+
332
+ let presets = []
333
+ try {
334
+ const listed = await withTimeout(presetsService?.list?.(), [], 3000)
335
+ presets = (listed ?? []).map((preset) => ({
336
+ value: preset.id,
337
+ label: preset.name ?? preset.id,
338
+ description: preset.broken ? `unavailable: ${preset.broken}` : (preset.description ?? null),
339
+ }))
340
+ } catch (error) {
341
+ logger.warn(`[a2a] cannot list agent presets: ${errorMessage(error)}`)
342
+ }
343
+
344
+ let providers = []
345
+ try {
346
+ providers = (llm?.listProviders?.() ?? []).map((entry) => ({
347
+ value: entry.id,
348
+ label: entry.name ?? entry.id,
349
+ }))
350
+ } catch (error) {
351
+ logger.warn(`[a2a] cannot list model providers: ${errorMessage(error)}`)
352
+ }
353
+
354
+ let models = []
355
+ if (provider !== null && typeof llm?.listModels === 'function') {
356
+ try {
357
+ const listed = await withTimeout(llm.listModels(provider), [], 4000)
358
+ models = (listed ?? []).map((entry) => ({
359
+ value: entry.id,
360
+ label: entry.name && entry.name !== entry.id ? `${entry.name} (${entry.id})` : entry.id,
361
+ description: entry.description ?? null,
362
+ }))
363
+ } catch (error) {
364
+ logger.warn(`[a2a] cannot list models for ${provider}: ${errorMessage(error)}`)
365
+ }
366
+ }
367
+
368
+ let reasoning = []
369
+ let reasoningDefault = null
370
+ if (provider !== null && model !== null && typeof llm?.resolveModelInfo === 'function') {
371
+ try {
372
+ const info = await withTimeout(llm.resolveModelInfo(provider, model), null, 4000)
373
+ reasoning = (info?.reasoning?.efforts ?? []).map((effort) => ({
374
+ value: effort.id,
375
+ label: effort.name ?? effort.id,
376
+ description: effort.description ?? null,
377
+ }))
378
+ reasoningDefault = info?.reasoning?.defaultEffort ?? null
379
+ } catch (error) {
380
+ logger.warn(`[a2a] cannot resolve reasoning options for ${provider}/${model}: ${errorMessage(error)}`)
381
+ }
382
+ }
383
+
384
+ res.setHeader('Cache-Control', 'no-store')
385
+ res.json({
386
+ ok: true,
387
+ presets,
388
+ providers,
389
+ models,
390
+ reasoning,
391
+ reasoningDefault,
392
+ default: selection,
393
+ provider,
394
+ model,
395
+ })
396
+ })
397
+
298
398
  router.put('/config', json, async (req, res) => {
299
399
  if (authorize(req, res)) return
300
400
  const configEditor = editor()
@@ -384,7 +484,7 @@ export function createPanelRouter({ ctx, config, mountPath, logger, status, heal
384
484
  logger.info('[a2a] configuration updated from the settings panel')
385
485
  res.json({
386
486
  ok: true,
387
- plugin: { name: PLUGIN_NAME, version: '1.1.4' },
487
+ plugin: { name: PLUGIN_NAME, version: '1.2.0' },
388
488
  mountPath,
389
489
  ...describe(live, liveEntry, []),
390
490
  ...(typeof status === 'function' ? { server: status() } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai_mind/dsh-a2a",
3
- "version": "1.1.4",
3
+ "version": "1.2.0",
4
4
  "description": "Production A2A (Agent2Agent) v1.0 server for DeepSeek Harness agents: JSON-RPC + HTTP+JSON bindings, streaming, tasks, push notifications and signed Agent Cards.",
5
5
  "type": "module",
6
6
  "license": "MIT",