@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.
- package/CHANGELOG.md +13 -0
- package/README.md +13 -8
- package/README.zh-CN.md +13 -8
- package/SKILL.md +5 -5
- package/lib/client.js +3 -3
- package/lib/index.js +1273 -338
- package/lib/types/client/blocks/advanced.d.ts +1 -1
- package/lib/types/client/blocks/checkbox.d.ts +17 -0
- package/lib/types/client/blocks/forms.d.ts +3 -8
- package/lib/types/client/blocks/state.d.ts +18 -14
- package/lib/types/client/dom-fence.d.ts +1 -1
- package/lib/types/client/genui-runtime/diagnostics.d.ts +18 -0
- package/lib/types/client/genui-runtime/index.d.ts +1 -0
- package/lib/types/client/genui-runtime/limits.d.ts +54 -0
- package/lib/types/client/genui-runtime/normalize.d.ts +15 -0
- package/lib/types/client/genui-runtime/schema.d.ts +73 -0
- package/lib/types/client/genui-runtime/value-utils.d.ts +18 -0
- package/lib/types/client/guard.d.ts +60 -71
- package/lib/types/client/interaction-store.d.ts +7 -2
- package/lib/types/client/panel-command.d.ts +1 -1
- package/lib/types/client/primitive-labels.d.ts +11 -0
- package/lib/types/client/spec.d.ts +17 -17
- package/lib/types/plugin/chart-contract.d.ts +6 -4
- package/lib/types/plugin/index.d.ts +1 -4
- package/package.json +74 -13
- package/src/client/GenuiBlock.tsx +27 -11
- package/src/client/blocks/advanced.tsx +6 -5
- package/src/client/blocks/charts.tsx +1 -1
- package/src/client/blocks/checkbox.tsx +60 -0
- package/src/client/blocks/diagram/index.tsx +1 -1
- package/src/client/blocks/forms.tsx +51 -28
- package/src/client/blocks/render-node.tsx +3 -16
- package/src/client/blocks/state.ts +18 -14
- package/src/client/dom-fence.tsx +2 -1
- package/src/client/fence-render.tsx +26 -15
- package/src/client/genui-runtime/diagnostics.ts +143 -0
- package/src/client/genui-runtime/index.ts +1 -0
- package/src/client/genui-runtime/limits.ts +54 -0
- package/src/client/genui-runtime/normalize.ts +115 -0
- package/src/client/genui-runtime/schema.ts +291 -0
- package/src/client/genui-runtime/value-utils.ts +67 -0
- package/src/client/guard.ts +314 -169
- package/src/client/index.tsx +3 -2
- package/src/client/interaction-store.ts +7 -2
- package/src/client/panel-command.ts +2 -1
- package/src/client/panel.tsx +1 -1
- package/src/client/primitive-labels.ts +37 -0
- package/src/client/spec.ts +17 -17
- package/src/client/toolview.tsx +2 -1
- package/src/index.ts +3 -6
- package/src/plugin/chart-contract.ts +7 -97
- package/src/plugin/index.ts +2 -6
- package/src/plugin/tool.ts +60 -40
package/src/client/panel.tsx
CHANGED
|
@@ -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-
|
|
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
|
+
}
|
package/src/client/spec.ts
CHANGED
|
@@ -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
|
|
400
|
-
* LOCAL-FIRST
|
|
401
|
-
*
|
|
402
|
-
*
|
|
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
|
|
410
|
-
*
|
|
411
|
-
*
|
|
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
|
|
427
|
-
* disabled until EVERY listed group has a
|
|
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
|
}
|
package/src/client/toolview.tsx
CHANGED
|
@@ -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-
|
|
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
|
-
|
|
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
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/plugin/index.ts
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
package/src/plugin/tool.ts
CHANGED
|
@@ -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-
|
|
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
|
-
|
|
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
|
-
/**
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
150
|
-
|
|
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
|
-
|
|
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
|
|
179
|
-
|
|
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
|
-
|
|
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
|
|
343
|
+
const processed = processRenderableValue(repairedValue)
|
|
344
|
+
const chartFailure = formatProcessFailure(processed)
|
|
324
345
|
if (chartFailure !== undefined) return chartFailure
|
|
325
|
-
if (
|
|
326
|
-
|
|
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
|
|
354
|
+
const processed = processRenderableValue(parsed)
|
|
355
|
+
const chartFailure = formatProcessFailure(processed)
|
|
332
356
|
if (chartFailure !== undefined) return chartFailure
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
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' }
|