@changfenhuang/dsh-genui 0.9.8 → 0.9.9

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.
Files changed (53) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +13 -8
  3. package/README.zh-CN.md +13 -8
  4. package/SKILL.md +5 -5
  5. package/lib/client.js +3 -3
  6. package/lib/index.js +1273 -338
  7. package/lib/types/client/blocks/advanced.d.ts +1 -1
  8. package/lib/types/client/blocks/checkbox.d.ts +17 -0
  9. package/lib/types/client/blocks/forms.d.ts +3 -8
  10. package/lib/types/client/blocks/state.d.ts +18 -14
  11. package/lib/types/client/dom-fence.d.ts +1 -1
  12. package/lib/types/client/genui-runtime/diagnostics.d.ts +18 -0
  13. package/lib/types/client/genui-runtime/index.d.ts +1 -0
  14. package/lib/types/client/genui-runtime/limits.d.ts +54 -0
  15. package/lib/types/client/genui-runtime/normalize.d.ts +15 -0
  16. package/lib/types/client/genui-runtime/schema.d.ts +73 -0
  17. package/lib/types/client/genui-runtime/value-utils.d.ts +18 -0
  18. package/lib/types/client/guard.d.ts +60 -71
  19. package/lib/types/client/interaction-store.d.ts +7 -2
  20. package/lib/types/client/panel-command.d.ts +1 -1
  21. package/lib/types/client/primitive-labels.d.ts +11 -0
  22. package/lib/types/client/spec.d.ts +17 -17
  23. package/lib/types/plugin/chart-contract.d.ts +6 -4
  24. package/lib/types/plugin/index.d.ts +1 -4
  25. package/package.json +74 -13
  26. package/src/client/GenuiBlock.tsx +27 -11
  27. package/src/client/blocks/advanced.tsx +6 -5
  28. package/src/client/blocks/charts.tsx +1 -1
  29. package/src/client/blocks/checkbox.tsx +60 -0
  30. package/src/client/blocks/diagram/index.tsx +1 -1
  31. package/src/client/blocks/forms.tsx +51 -28
  32. package/src/client/blocks/render-node.tsx +3 -16
  33. package/src/client/blocks/state.ts +18 -14
  34. package/src/client/dom-fence.tsx +2 -1
  35. package/src/client/fence-render.tsx +26 -15
  36. package/src/client/genui-runtime/diagnostics.ts +143 -0
  37. package/src/client/genui-runtime/index.ts +1 -0
  38. package/src/client/genui-runtime/limits.ts +54 -0
  39. package/src/client/genui-runtime/normalize.ts +115 -0
  40. package/src/client/genui-runtime/schema.ts +291 -0
  41. package/src/client/genui-runtime/value-utils.ts +67 -0
  42. package/src/client/guard.ts +314 -169
  43. package/src/client/index.tsx +3 -2
  44. package/src/client/interaction-store.ts +7 -2
  45. package/src/client/panel-command.ts +2 -1
  46. package/src/client/panel.tsx +1 -1
  47. package/src/client/primitive-labels.ts +37 -0
  48. package/src/client/spec.ts +17 -17
  49. package/src/client/toolview.tsx +2 -1
  50. package/src/index.ts +3 -6
  51. package/src/plugin/chart-contract.ts +7 -97
  52. package/src/plugin/index.ts +2 -6
  53. package/src/plugin/tool.ts +60 -40
@@ -24,7 +24,7 @@
24
24
  * apply) queues a [genui-action] user message back to the model.
25
25
  */
26
26
  import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react'
27
- import type {} from '@deepseek-ai/dsh-client-runtime/client'
27
+ import type {} from '@deepseek-ai/dsh-client-ui-session/client'
28
28
  import { IconChevronDownOutline14, IconChevronUpOutline14 } from '@deepseek-ai/dsh-client-ui-primitives'
29
29
  import { GenuiActionContext, type GenuiActionHandler } from './action-context.ts'
30
30
  import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
@@ -0,0 +1,37 @@
1
+ /** Localized labels required by the rc.1 DSH UI primitives. */
2
+ import type { DiffBlockLabels, JsonTreeLabels } from '@deepseek-ai/dsh-client-ui-primitives'
3
+
4
+ /** Chrome labels for an inline GenUI diff block. */
5
+ export const DIFF_BLOCK_LABELS: DiffBlockLabels = {
6
+ copy: '复制',
7
+ copied: '复制成功',
8
+ collapseAria: '收起差异',
9
+ /** Accessible label for expanding a collapsed diff tail. */
10
+ expandAria(hidden) { return `展开其余 ${hidden} 行差异` },
11
+ collapse: '收起',
12
+ /** Visible label for expanding a collapsed diff tail. */
13
+ expand(hidden) { return `… 其余 ${hidden} 行` },
14
+ /** Localized file-count summary in the diff footer. */
15
+ files(count) { return `${count} 个文件` },
16
+ }
17
+
18
+ /** Copy labels for a code block rendered inside a GenUI fence or node. */
19
+ export const CODE_BLOCK_LABELS = {
20
+ copyLabel: '复制',
21
+ copiedLabel: '复制成功',
22
+ } as const
23
+
24
+ /** Chrome labels for an inline GenUI JSON tree. */
25
+ export const JSON_TREE_LABELS: JsonTreeLabels = {
26
+ copyValue: '复制值',
27
+ copyJson: '复制 JSON',
28
+ copyPath: '复制属性路径',
29
+ copyPrettyJson: '复制格式化 JSON',
30
+ copyCompactJson: '复制紧凑 JSON',
31
+ copied: '已复制',
32
+ copyFailed: '复制失败',
33
+ collapseNode: '收起 JSON 节点',
34
+ expandNode: '展开 JSON 节点',
35
+ /** Tooltip for a JSON tree copy action. */
36
+ copyButtonTitle(action) { return `${action};右键查看复制选项` },
37
+ }
@@ -104,7 +104,7 @@ export interface GenuiInput {
104
104
  label?: string
105
105
  placeholder?: string
106
106
  value?: string
107
- inputType?: 'text' | 'email' | 'password'
107
+ inputType?: 'text' | 'email' | 'password' | 'color'
108
108
  /** v2: when set, interaction sends this action back to the model. */
109
109
  action?: string
110
110
  /**
@@ -139,6 +139,12 @@ export interface GenuiCheckbox {
139
139
  checked?: boolean
140
140
  /** v2: when set, interaction sends this action back to the model. */
141
141
  action?: string
142
+ /**
143
+ * Aggregation group name. When set, toggles stay local and update the
144
+ * block-wide multi-answer registry instead of firing the per-click action;
145
+ * a sibling `submit` collects the selected labels as a string array.
146
+ */
147
+ group?: string
142
148
  }
143
149
 
144
150
  export interface GenuiLink {
@@ -396,23 +402,17 @@ export interface GenuiRadio {
396
402
  explanation?: string
397
403
  }
398
404
 
399
- /** Submit node: collects the answers of sibling `radio` groups in this block.
400
- * LOCAL-FIRST: when the questions carry `answer` data the click grades IN
401
- * PLACE — score, per-question right/wrong, explanations — with no model
402
- * round trip, and locks the questions until "重新作答" resets them. Only when
403
- * NO question carries answers does it fall back to firing ONE action
404
- * (`{type:'submit', answers, total, answered}`). */
405
+ /** Submit node: collects sibling `radio` and grouped `checkbox` answers in
406
+ * this block. LOCAL-FIRST grading still applies to radio-only question scopes;
407
+ * aggregation scopes can emit strings for radio groups and string arrays for
408
+ * checkbox groups in the same `answers` object. */
405
409
  export interface GenuiSubmit {
406
410
  type: 'submit'
407
411
  label: string
408
412
  /**
409
- * Optional action name. Local-first: when ANY question in scope carries
410
- * `answer` data the click grades IN PLACE with zero model round trip, so
411
- * no action is needed — the spec stays valid without one. Only when NO
412
- * question has local answers does the submit need an action to collect
413
- * `{type:'submit', answers, total, answered}`; without one the button
414
- * renders disabled (honest affordance). Also fired as the reset hook if
415
- * `resetAction` is absent.
413
+ * Optional action name. Local-first: when the in-scope questions can be
414
+ * graded entirely locally, no action is needed. Aggregation submits require
415
+ * an action and emit `{type:'submit', answers, total, answered}`.
416
416
  */
417
417
  action?: string
418
418
  /**
@@ -423,9 +423,9 @@ export interface GenuiSubmit {
423
423
  resetAction?: string
424
424
  /**
425
425
  * Optional explicit group list to wait for; when absent the submit enables
426
- * once at least one grouped radio has an answer. When present it stays
427
- * disabled until EVERY listed group has a recorded answer (the hint shows
428
- * the progress).
426
+ * once at least one grouped answer (or filled field) exists. When present
427
+ * it stays disabled until EVERY listed radio/checkbox group has a non-empty
428
+ * recorded answer (the hint shows the progress).
429
429
  */
430
430
  groups?: string[]
431
431
  }
@@ -11,7 +11,8 @@
11
11
  * render_ui calls update one surface instead of stacking tool-row cards.
12
12
  */
13
13
  import { useEffect, useMemo } from 'react'
14
- import type {} from '@deepseek-ai/dsh-client-runtime/client'
14
+ import type {} from '@deepseek-ai/dsh-client-ui-session/client'
15
+ import type {} from '@deepseek-ai/dsh-client-ui-chat/client'
15
16
  import type { ToolCallViewProps } from '@deepseek-ai/dsh-client-ui-tool/client'
16
17
  import { GenuiBlock } from './GenuiBlock.tsx'
17
18
  import { ErrorBoundary } from './ErrorBoundary.tsx'
package/src/index.ts CHANGED
@@ -2,11 +2,8 @@
2
2
  * Bundle source entry (plugin_check tool-bundle contract): the node-half
3
3
  * surface the host loader resolves through `package.json#main`.
4
4
  *
5
- * The `import type {}` line is NOT decorative: importing the dsh client
6
- * runtime's cordis Context augmentation BEFORE re-exporting the plugin keeps
7
- * TypeScript's global-augmentation ordering deterministic — without it,
8
- * TS 5.9 mis-resolves `ctx.sessions` in the client half when the plugin
9
- * module is first reached through this root entry.
10
5
  */
11
- import type {} from '@deepseek-ai/dsh-client-runtime/client'
6
+ // Load the owner package's client augmentation before the plugin re-export so
7
+ // TypeScript resolves client context members consistently from this root entry.
8
+ import type {} from '@deepseek-ai/dsh-api-session-controller/client'
12
9
  export * from './plugin/index'
@@ -1,103 +1,13 @@
1
1
  import { validateGenuiChartSemantics } from '../client/guard.ts'
2
2
 
3
- /** Plain object helper for raw model-authored specs. */
4
- function obj(value: unknown): Record<string, unknown> | undefined {
5
- return typeof value === 'object' && value !== null && !Array.isArray(value)
6
- ? value as Record<string, unknown>
7
- : undefined
8
- }
9
-
10
3
  /**
11
- * Visit chart nodes through the component-bearing containers understood by
12
- * the GenUI guard. This deliberately does not recursively inspect arbitrary
13
- * objects (for example `json.value` or `echart.option`), where a nested
14
- * `{type:"chart"}` is data rather than a GenUI node.
15
- */
16
- function visitChartNodes(
17
- value: unknown,
18
- visit: (node: Record<string, unknown>, at: string) => void,
19
- ): void {
20
- function walkNode(value: unknown, at: string): void {
21
- const node = obj(value)
22
- if (node === undefined || typeof node.type !== 'string') return
23
- if (node.type === 'chart') visit(node, at)
24
-
25
- if ((node.type === 'row' || node.type === 'col' || node.type === 'grid' || node.type === 'card') && Array.isArray(node.items)) {
26
- walk(node.items, `${at}.items`)
27
- return
28
- }
29
- if (node.type === 'tabs' && Array.isArray(node.tabs)) {
30
- for (let index = 0; index < node.tabs.length; index++) {
31
- const tab = obj(node.tabs[index])
32
- if (tab === undefined) continue
33
- const items = tab.items !== undefined ? tab.items : tab.content
34
- if (Array.isArray(items)) walk(items, `${at}.tabs[${index}].items`)
35
- else walkNode(items, `${at}.tabs[${index}].items`)
36
- }
37
- return
38
- }
39
- if (node.type === 'accordion' && Array.isArray(node.items)) {
40
- for (let index = 0; index < node.items.length; index++) {
41
- const item = obj(node.items[index])
42
- if (item === undefined) continue
43
- const items = item.items !== undefined ? item.items : item.content
44
- if (Array.isArray(items)) walk(items, `${at}.items[${index}].items`)
45
- else walkNode(items, `${at}.items[${index}].items`)
46
- }
47
- return
48
- }
49
- if (node.type === 'list' && Array.isArray(node.items)) {
50
- for (let index = 0; index < node.items.length; index++) {
51
- walkNode(node.items[index], `${at}.items[${index}]`)
52
- }
53
- }
54
- }
55
-
56
- function walk(list: unknown, path: string): void {
57
- if (!Array.isArray(list)) return
58
- for (let index = 0; index < list.length; index++) {
59
- walkNode(list[index], `${path}[${index}]`)
60
- }
61
- }
62
-
63
- const root = obj(value)
64
- if (root === undefined) return
65
- if (!Array.isArray(root.items) && root.type === 'chart') walkNode(root, 'spec')
66
- else walk(root.items, 'items')
67
- }
68
-
69
- /**
70
- * Validate the native chart contract all the way to something the renderer
71
- * can actually draw. The shared guard still owns field/type validation; this
72
- * layer adds renderer-specific invariants that would otherwise produce an
73
- * empty line/donut chart or a grouped-bars shell with no points.
4
+ * Validate chart nodes using the guard's shared native renderability contract.
5
+ * The plugin-facing API remains stable while the guard remains the single
6
+ * owner of chart field and renderer-semantic rules.
7
+ *
8
+ * @param value - Raw GenUI spec or bare chart node.
9
+ * @returns Chart errors in deterministic tree order.
74
10
  */
75
11
  export function validateRenderableChartSemantics(value: unknown): string[] {
76
- const errors = validateGenuiChartSemantics(value)
77
- visitChartNodes(value, (node, at) => {
78
- const kind = node.kind === undefined ? 'bars' : node.kind
79
-
80
- if (Array.isArray(node.data) && node.data.length === 0) {
81
- errors.push(`${at}.data must not be empty`)
82
- }
83
- if (Array.isArray(node.series)) {
84
- if (node.series.length === 0) errors.push(`${at}.series must not be empty`)
85
- for (let index = 0; index < node.series.length; index++) {
86
- const series = obj(node.series[index])
87
- if (series !== undefined && Array.isArray(series.data) && series.data.length === 0) {
88
- errors.push(`${at}.series[${index}].data must not be empty`)
89
- }
90
- }
91
- }
92
-
93
- if (kind === 'line' || kind === 'donut') {
94
- if (node.series !== undefined) {
95
- errors.push(`${at}.series is only supported for bars`)
96
- }
97
- if (node.data === undefined) {
98
- errors.push(`${at}.data is required for ${kind}`)
99
- }
100
- }
101
- })
102
- return errors
12
+ return validateGenuiChartSemantics(value)
103
13
  }
@@ -5,8 +5,7 @@
5
5
  * model the language exists, so a session without the plugin simply never
6
6
  * emits fences and nothing changes.
7
7
  *
8
- * The section is a convention section (order 100-199), placed after the bash
9
- * guidance so the model sees it among its output-format rules.
8
+ * The section uses the host's centrally allocated structured-output placement.
10
9
  * @module @changfenhuang/dsh-genui
11
10
  */
12
11
 
@@ -21,9 +20,6 @@ import { basename, dirname, join, resolve } from 'node:path'
21
20
  import { fileURLToPath } from 'node:url'
22
21
  import { createRenderUiTool, createValidateDshUiTool } from './tool.ts'
23
22
 
24
- /** Convention: tool guidance uses 100–199; bash's section is 104. */
25
- export const GENUI_SECTION_ORDER = 105
26
-
27
23
  /* ---------------- lazy engine asset route ---------------- */
28
24
 
29
25
  /**
@@ -168,7 +164,7 @@ function bundledSkillProvider(): SkillProvider {
168
164
  export function apply(ctx: Context): void {
169
165
  ctx.systemPrompt.section({
170
166
  name: 'genui:fence',
171
- order: GENUI_SECTION_ORDER,
167
+ order: ctx.systemPrompt.getSectionOrder('STRUCTURED_OUTPUT'),
172
168
  text: GENUI_SECTION_TEXT,
173
169
  })
174
170
  // The tools service is optional: hosts without tool access (or minimal
@@ -19,14 +19,13 @@
19
19
  */
20
20
 
21
21
  import type { ContentBlock } from '@deepseek-ai/dsh-llm'
22
- import type { JsonValue } from '@deepseek-ai/dsh-session'
22
+ import type { JsonValue } from '@deepseek-ai/dsh-util-values'
23
23
  import type { GenericCallView, GenericResultView, JsonSchemaNode, ToolDefinition } from '@deepseek-ai/dsh-tools'
24
- import type { GenuiSpec } from '../client/spec.ts'
25
24
  import {
26
- GENUI_LIMITS, countDeclaredGenuiNodes, countGenuiNodes, repairGenuiSpec,
25
+ isRenderableProcess, processGenuiSpec,
27
26
  } from '../client/guard.ts'
27
+ import type { GenuiProcessResult } from '../client/guard.ts'
28
28
  import { completeFenceJson } from '../shared/fence-repair.ts'
29
- import { validateRenderableChartSemantics } from './chart-contract.ts'
30
29
 
31
30
  /**
32
31
  * Arguments schema: an open `spec` slot. The schema must NOT reject anything
@@ -136,18 +135,43 @@ function parseSpecJson(raw: string, shape: string): unknown {
136
135
  }
137
136
  }
138
137
 
139
- /** Total node count of a repaired spec — the shared guard traversal
140
- * (`countGenuiNodes`) so the tool reports the SAME number the panel fold and
141
- * validation use. A local walker used to under-count specs whose content
142
- * lives inside tabs/accordion/file-tree (their children are not `.items`). */
143
- function countNodes(spec: GenuiSpec): number {
144
- return countGenuiNodes(spec, GENUI_LIMITS.maxNodes)
138
+ /** Process a raw tool value once for all render-time decisions. */
139
+ function processRenderableValue(value: unknown): GenuiProcessResult {
140
+ return processGenuiSpec(value)
141
+ }
142
+
143
+ /** Render process diagnostics as stable model-facing warning lines. */
144
+ function formatProcessWarnings(processed: GenuiProcessResult): string[] {
145
+ return processed.warnings.map(warning => {
146
+ if (warning.kind === 'alias' && warning.canonical !== undefined) {
147
+ const separator = warning.path.lastIndexOf('.')
148
+ const canonicalPath = `${separator < 0 ? '' : warning.path.slice(0, separator + 1)}${warning.canonical}`
149
+ return warning.message.includes('ignored')
150
+ ? `⚠️ 已忽略别名字段:${warning.path} → ${canonicalPath}(ignored because canonical field '${warning.canonical}' is present)`
151
+ : `⚠️ 已规范化字段:${warning.path} → ${canonicalPath}(normalized/adopted as '${warning.canonical}')`
152
+ }
153
+ return `⚠️ ${warning.message}`
154
+ })
155
+ }
156
+
157
+ /** Format chart-specific process errors while keeping other schema errors generic. */
158
+ function formatProcessFailure(processed: GenuiProcessResult): string | undefined {
159
+ const chartErrors = processed.errors.filter(error => /(?:variant is unsupported|kind must be bars, line, or donut|requires data or series|(?:data|series) is required for|(?:\.data|\.series)(?:\[\d+\])?(?:\.(?:data|label|value|color))? must|series is only supported for bars)/.test(error))
160
+ return chartErrors.length === 0 ? undefined : `❌ chart 字段验证失败:\n- ${chartErrors.join('\n- ')}`
161
+ }
162
+
163
+ /** Return the legacy validation text for a process that dropped native nodes. */
164
+ function droppedNodeFailure(processed: GenuiProcessResult): string | undefined {
165
+ if (!processed.errors.some(error => error.startsWith('repair dropped '))) return undefined
166
+ const dropped = processed.declaredNativeCount - processed.renderedNativeCount
167
+ return `❌ 验证未通过:检测到声明了 ${processed.declaredNativeCount} 个组件,但仅成功解析出 ${processed.renderedNativeCount} 个(有 ${dropped} 个组件因字段格式异常被丢弃)。常见原因:table 的 columns/rows 不是二维字符串数组、tabs 的 items/content 缺失、嵌套组件字段类型不符。请修正后重新验证。`
145
168
  }
146
169
 
147
170
  /** Tool-call title shared by the pending and completed presentations. */
148
171
  function cardTitle(args: unknown): string | undefined {
149
- const spec = repairGenuiSpec(specOf(args))
150
- return spec === null ? undefined : `渲染 UI:${spec.title ?? '未命名'}`
172
+ const processed = processRenderableValue(specOf(args))
173
+ if (!isRenderableProcess(processed) || processed.spec === null) return undefined
174
+ return `渲染 UI:${processed.spec.title ?? '未命名'}`
151
175
  }
152
176
 
153
177
  /**
@@ -171,21 +195,23 @@ export function createRenderUiTool(): ToolDefinition {
171
195
  // The browser toolview reads the repaired spec from result meta. The
172
196
  // spec is JSON-safe by construction (only string/number/boolean/array
173
197
  // fields after repair), so the widening cast is lossless.
174
- return repairGenuiSpec(specOf(args)) as unknown as JsonValue
198
+ const processed = processRenderableValue(specOf(args))
199
+ return (isRenderableProcess(processed) ? processed.spec : null) as unknown as JsonValue
175
200
  },
176
201
  },
177
202
  async execute(args: unknown): Promise<JsonValue> {
178
- const input = specOf(args)
179
- const chartErrors = validateRenderableChartSemantics(input)
180
- if (chartErrors.length > 0) {
181
- throw new Error('render_ui spec invalid: ' + chartErrors.join('; '))
182
- }
183
- const spec = repairGenuiSpec(input)
184
- if (spec === null) {
203
+ const processed = processRenderableValue(specOf(args))
204
+ if (processed.spec === null) {
185
205
  return 'render_ui:spec 无效 —— 根对象需要 "items" 数组(组件树白名单见系统提示词),请修正后重试。'
186
206
  }
207
+ if (!isRenderableProcess(processed)) {
208
+ throw new Error('render_ui spec invalid: ' + processed.errors.join('; '))
209
+ }
210
+ const spec = processed.spec
187
211
  const title = spec.title ?? '未命名'
188
- return `已渲染 UI「${title}」(${countNodes(spec)} 个组件)。用户现在可以看到这张卡片;组件带 action 时,用户交互会以 [genui-action] 消息发回给你,届时请重新渲染更新后的界面。`
212
+ const warnings = formatProcessWarnings(processed)
213
+ const warningText = warnings.length === 0 ? '' : `\n${warnings.join('\n')}`
214
+ return `已渲染 UI「${title}」(${processed.renderedCount} 个组件)。用户现在可以看到这张卡片;组件带 action 时,用户交互会以 [genui-action] 消息发回给你,届时请重新渲染更新后的界面。${warningText}`
189
215
  },
190
216
  presentCall(args: unknown): GenericCallView | undefined {
191
217
  const title = cardTitle(args)
@@ -285,12 +311,6 @@ function bracketDiagnostic(raw: string): string {
285
311
  const COMMON_CAUSES =
286
312
  '常见原因:① 收尾括号错位/缺失({ 与 }、[ 与 ] 数量不相等)② 字符串值内用了半角引号 "(中文引语请用 “” 或 「」)③ 尾随逗号 ④ 字符串未闭合'
287
313
 
288
- /** Return the model-facing chart field failure for one parsed fence body. */
289
- function chartValidationFailure(value: unknown): string | undefined {
290
- const errors = validateRenderableChartSemantics(value)
291
- return errors.length === 0 ? undefined : `❌ chart 字段验证失败:\n- ${errors.join('\n- ')}`
292
- }
293
-
294
314
  /** Build the validate_dsh_ui tool definition (registered alongside render_ui). */
295
315
  export function createValidateDshUiTool(): ToolDefinition {
296
316
  return {
@@ -320,26 +340,26 @@ export function createValidateDshUiTool(): ToolDefinition {
320
340
  const repaired = completeFenceJson(raw)
321
341
  if (repaired !== null) {
322
342
  const repairedValue = JSON.parse(repaired.text) as unknown
323
- const chartFailure = chartValidationFailure(repairedValue)
343
+ const processed = processRenderableValue(repairedValue)
344
+ const chartFailure = formatProcessFailure(processed)
324
345
  if (chartFailure !== undefined) return chartFailure
325
- if (repairGenuiSpec(repairedValue) !== null) {
326
- return `❌ dsh-ui 围栏 JSON 解析失败:${detail}。\n${bracketDiagnostic(raw)} 已自动修复 ${repaired.repairs} 处,下面是修复后的 JSON,直接作为围栏正文发出即可(无需再验证):\n\`\`\`\n${repaired.text}\n\`\`\``
346
+ if (processed.spec !== null && processed.errors.length === 0) {
347
+ const warnings = formatProcessWarnings(processed)
348
+ const warningText = warnings.length === 0 ? '' : `${warnings.join('\n')}\n`
349
+ return `❌ dsh-ui 围栏 JSON 解析失败:${detail}。\n${bracketDiagnostic(raw)}${warningText} 已自动修复 ${repaired.repairs} 处,下面是修复后的 JSON,直接作为围栏正文发出即可(无需再验证):\n\`\`\`\n${repaired.text}\n\`\`\``
327
350
  }
328
351
  }
329
352
  return `❌ dsh-ui 围栏 JSON 解析失败:${detail}。\n${bracketDiagnostic(raw)} 自动修复未能恢复(结构损坏),请按错误信息修正后重新调用本工具验证,通过后再发出围栏。\n${COMMON_CAUSES}`
330
353
  }
331
- const chartFailure = chartValidationFailure(parsed)
354
+ const processed = processRenderableValue(parsed)
355
+ const chartFailure = formatProcessFailure(processed)
332
356
  if (chartFailure !== undefined) return chartFailure
333
- const spec = repairGenuiSpec(parsed)
334
- if (spec === null) {
335
- return '❌ 不是合法 GenUI spec:根对象需要 "items" 数组,且每个节点 type 必须在白名单内(见系统提示词)。请修正后重新验证。'
336
- }
337
- const validCount = countNodes(spec)
338
- const declaredCount = countDeclaredGenuiNodes(parsed, GENUI_LIMITS.maxNodes + 1)
339
- if (declaredCount > validCount) {
340
- return `❌ 验证未通过:检测到声明了 ${declaredCount} 个组件,但仅成功解析出 ${validCount} 个(有 ${declaredCount - validCount} 个组件因字段格式异常被丢弃)。常见原因:table 的 columns/rows 不是二维字符串数组、tabs 的 items/content 缺失、嵌套组件字段类型不符。请修正后重新验证。`
357
+ if (processed.spec === null || processed.errors.length > 0) {
358
+ return droppedNodeFailure(processed)
359
+ ?? `❌ 不是合法 GenUI spec:${processed.errors.join(';') || '根对象需要 "items" 数组,且每个节点 type 必须在白名单内(见系统提示词)'}。请修正后重新验证。`
341
360
  }
342
- return `✅ dsh-ui spec 合法(${validCount} 个组件),可以发出围栏。`
361
+ const warnings = formatProcessWarnings(processed)
362
+ return [`✅ dsh-ui spec 合法(${processed.renderedCount} 个组件),可以发出围栏。`, ...warnings].join('\n')
343
363
  },
344
364
  presentCall(): GenericCallView | undefined {
345
365
  return { card: 'generic', title: '验证 dsh-ui 围栏', kind: 'other' }