@changfenhuang/dsh-genui 0.9.3 → 0.9.7
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 +17 -0
- package/README.md +7 -18
- package/README.zh-CN.md +13 -18
- package/SKILL.md +4 -2
- package/lib/assets/three.js +1 -1
- package/lib/client.js +3 -3
- package/lib/index.js +273 -61
- package/lib/types/client/GenuiBlock.d.ts +5 -2
- package/lib/types/client/TemplateDrawer.d.ts +7 -0
- package/lib/types/client/achievement-store.d.ts +35 -0
- package/lib/types/client/achievement-toast.d.ts +2 -0
- package/lib/types/client/achievements.d.ts +48 -0
- package/lib/types/client/blocks/advanced.d.ts +1 -1
- package/lib/types/client/blocks/image.d.ts +6 -0
- package/lib/types/client/blocks/state.d.ts +2 -0
- package/lib/types/client/fence-render.d.ts +3 -0
- package/lib/types/client/gallery.d.ts +3 -5
- package/lib/types/client/guard.d.ts +10 -5
- package/lib/types/client/panel.d.ts +4 -1
- package/lib/types/client/spec.d.ts +8 -1
- package/lib/types/client/templates.d.ts +22 -0
- package/lib/types/plugin/chart-contract.d.ts +7 -0
- package/lib/types/plugin/index.d.ts +1 -1
- package/package.json +9 -8
- package/src/client/GenuiBlock.module.css +141 -10
- package/src/client/GenuiBlock.tsx +53 -25
- package/src/client/PlotBlock.tsx +25 -10
- package/src/client/TemplateDrawer.module.css +152 -0
- package/src/client/TemplateDrawer.tsx +137 -0
- package/src/client/achievement-store.ts +151 -0
- package/src/client/achievement-toast.tsx +68 -0
- package/src/client/achievements.ts +111 -0
- package/src/client/blocks/advanced.tsx +34 -25
- package/src/client/blocks/charts.tsx +4 -4
- package/src/client/blocks/image.tsx +25 -0
- package/src/client/blocks/render-node.tsx +3 -0
- package/src/client/blocks/state.ts +2 -0
- package/src/client/dom-fence.tsx +12 -5
- package/src/client/fence-render.tsx +35 -11
- package/src/client/gallery.ts +4 -5
- package/src/client/guard.ts +125 -29
- package/src/client/index.tsx +13 -0
- package/src/client/panel.tsx +109 -10
- package/src/client/safe-math.ts +29 -1
- package/src/client/scene3d-core.ts +5 -2
- package/src/client/spec.ts +9 -1
- package/src/client/templates.ts +280 -0
- package/src/plugin/chart-contract.ts +103 -0
- package/src/plugin/index.ts +48 -2
- package/src/plugin/tool.ts +25 -4
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { validateGenuiChartSemantics } from '../client/guard.ts'
|
|
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
|
+
/**
|
|
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.
|
|
74
|
+
*/
|
|
75
|
+
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
|
|
103
|
+
}
|
package/src/plugin/index.ts
CHANGED
|
@@ -13,9 +13,11 @@
|
|
|
13
13
|
import { Context } from '@deepseek-ai/cordis'
|
|
14
14
|
import type {} from '@deepseek-ai/dsh-system-prompt'
|
|
15
15
|
import type {} from '@deepseek-ai/dsh-tools'
|
|
16
|
+
import type { SkillProvider } from '@deepseek-ai/dsh-skill'
|
|
16
17
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
18
|
+
import { readFileSync } from 'node:fs'
|
|
17
19
|
import { readFile } from 'node:fs/promises'
|
|
18
|
-
import { join } from 'node:path'
|
|
20
|
+
import { basename, dirname, join, resolve } from 'node:path'
|
|
19
21
|
import { fileURLToPath } from 'node:url'
|
|
20
22
|
import { createRenderUiTool, createValidateDshUiTool } from './tool.ts'
|
|
21
23
|
|
|
@@ -98,7 +100,7 @@ The spec is a white-listed component tree rendered inline where the fence sits.
|
|
|
98
100
|
|
|
99
101
|
- 布局: text · row · col · grid · card · divider · spacer
|
|
100
102
|
- 展示: badge · stat · progress · list · table · keyvalue · avatar · audio · video · timeline · file-tree · breadcrumb · callout · steps · diff · json · code · copy
|
|
101
|
-
- 图表: chart
|
|
103
|
+
- 图表: chart {"type":"chart","kind":"bars|line|donut","data":[{"label":"...","value":n,"color":"#hex?"}],"series":[...]?}(series 仅 bars;未知扩展字段可通过校验,但原生 chart repair/render 会忽略) · echart (preset|option) · plot (函数图)
|
|
102
104
|
- 交互: button · input · textarea · select · checkbox · switch · slider · radio · submit · quiz · link · tabs · accordion
|
|
103
105
|
- 高级: mermaid (flowchart/sequence/class/gantt/pie/er/state/journey) · diagram (编辑级架构/流程图,27 种 kind) · scene3d (3D WebGL)
|
|
104
106
|
|
|
@@ -123,6 +125,46 @@ Rules:
|
|
|
123
125
|
// instead (see apply).
|
|
124
126
|
export const inject = ['systemPrompt']
|
|
125
127
|
|
|
128
|
+
const BUNDLED_SKILL_RANK = 600
|
|
129
|
+
const BUNDLED_SKILL_PROVIDER = 'dsh-genui'
|
|
130
|
+
const BUNDLED_SKILL_DESCRIPTION = 'GenUI 完整组件与字段规范,用于生成 dsh-ui 结构化交互界面。'
|
|
131
|
+
const BUNDLED_SKILL_INVOCATION = { modelInvocable: true, userInvocable: true } as const
|
|
132
|
+
|
|
133
|
+
/** Register through the provider path so source=bundled also gets bundled precedence. */
|
|
134
|
+
function bundledSkillProvider(): SkillProvider {
|
|
135
|
+
const moduleDirectory = dirname(fileURLToPath(new URL(import.meta.url)))
|
|
136
|
+
const path = basename(moduleDirectory) === 'plugin'
|
|
137
|
+
? resolve(moduleDirectory, '../../SKILL.md')
|
|
138
|
+
: resolve(moduleDirectory, '../SKILL.md')
|
|
139
|
+
const raw = readFileSync(path, 'utf8')
|
|
140
|
+
const end = raw.indexOf('\n---\n', 4)
|
|
141
|
+
if (!raw.startsWith('---\n') || end < 0) throw new Error('genui SKILL.md has invalid frontmatter')
|
|
142
|
+
return {
|
|
143
|
+
name: BUNDLED_SKILL_PROVIDER,
|
|
144
|
+
list: () => Promise.resolve([{
|
|
145
|
+
name: 'genui',
|
|
146
|
+
description: BUNDLED_SKILL_DESCRIPTION,
|
|
147
|
+
invocation: BUNDLED_SKILL_INVOCATION,
|
|
148
|
+
source: 'bundled',
|
|
149
|
+
provider: BUNDLED_SKILL_PROVIDER,
|
|
150
|
+
path,
|
|
151
|
+
resourceBase: { kind: 'directory', path: dirname(path) },
|
|
152
|
+
rank: BUNDLED_SKILL_RANK,
|
|
153
|
+
locator: path,
|
|
154
|
+
}]),
|
|
155
|
+
get: () => Promise.resolve({
|
|
156
|
+
name: 'genui',
|
|
157
|
+
description: BUNDLED_SKILL_DESCRIPTION,
|
|
158
|
+
invocation: BUNDLED_SKILL_INVOCATION,
|
|
159
|
+
source: 'bundled',
|
|
160
|
+
provider: BUNDLED_SKILL_PROVIDER,
|
|
161
|
+
path,
|
|
162
|
+
resourceBase: { kind: 'directory', path: dirname(path) },
|
|
163
|
+
content: raw.slice(end + 5),
|
|
164
|
+
}),
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
126
168
|
export function apply(ctx: Context): void {
|
|
127
169
|
ctx.systemPrompt.section({
|
|
128
170
|
name: 'genui:fence',
|
|
@@ -156,6 +198,10 @@ export function apply(ctx: Context): void {
|
|
|
156
198
|
if (name === 'tools') tryRegister(value as { register(tool: unknown): unknown })
|
|
157
199
|
})
|
|
158
200
|
|
|
201
|
+
ctx.inject(['skills'], (skillCtx) => {
|
|
202
|
+
skillCtx.skills.registerProvider(() => bundledSkillProvider())
|
|
203
|
+
})
|
|
204
|
+
|
|
159
205
|
// Lazy-engine asset route: same optional-probe pattern as the tools
|
|
160
206
|
// registry — the webserver service may bind after this plugin starts.
|
|
161
207
|
let assetsRegistered = false
|
package/src/plugin/tool.ts
CHANGED
|
@@ -22,8 +22,11 @@ import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
|
|
22
22
|
import type { JsonValue } from '@deepseek-ai/dsh-session'
|
|
23
23
|
import type { GenericCallView, GenericResultView, JsonSchemaNode, ToolDefinition } from '@deepseek-ai/dsh-tools'
|
|
24
24
|
import type { GenuiSpec } from '../client/spec.ts'
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
GENUI_LIMITS, countDeclaredGenuiNodes, countGenuiNodes, repairGenuiSpec,
|
|
27
|
+
} from '../client/guard.ts'
|
|
26
28
|
import { completeFenceJson } from '../shared/fence-repair.ts'
|
|
29
|
+
import { validateRenderableChartSemantics } from './chart-contract.ts'
|
|
27
30
|
|
|
28
31
|
/**
|
|
29
32
|
* Arguments schema: an open `spec` slot. The schema must NOT reject anything
|
|
@@ -172,7 +175,12 @@ export function createRenderUiTool(): ToolDefinition {
|
|
|
172
175
|
},
|
|
173
176
|
},
|
|
174
177
|
async execute(args: unknown): Promise<JsonValue> {
|
|
175
|
-
const
|
|
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)
|
|
176
184
|
if (spec === null) {
|
|
177
185
|
return 'render_ui:spec 无效 —— 根对象需要 "items" 数组(组件树白名单见系统提示词),请修正后重试。'
|
|
178
186
|
}
|
|
@@ -277,6 +285,12 @@ function bracketDiagnostic(raw: string): string {
|
|
|
277
285
|
const COMMON_CAUSES =
|
|
278
286
|
'常见原因:① 收尾括号错位/缺失({ 与 }、[ 与 ] 数量不相等)② 字符串值内用了半角引号 "(中文引语请用 “” 或 「」)③ 尾随逗号 ④ 字符串未闭合'
|
|
279
287
|
|
|
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
|
+
|
|
280
294
|
/** Build the validate_dsh_ui tool definition (registered alongside render_ui). */
|
|
281
295
|
export function createValidateDshUiTool(): ToolDefinition {
|
|
282
296
|
return {
|
|
@@ -304,11 +318,18 @@ export function createValidateDshUiTool(): ToolDefinition {
|
|
|
304
318
|
// FIXED JSON instead of asking it to re-author the fix — re-writing
|
|
305
319
|
// the whole fence by hand is where the next typo comes from.
|
|
306
320
|
const repaired = completeFenceJson(raw)
|
|
307
|
-
if (repaired !== null
|
|
308
|
-
|
|
321
|
+
if (repaired !== null) {
|
|
322
|
+
const repairedValue = JSON.parse(repaired.text) as unknown
|
|
323
|
+
const chartFailure = chartValidationFailure(repairedValue)
|
|
324
|
+
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\`\`\``
|
|
327
|
+
}
|
|
309
328
|
}
|
|
310
329
|
return `❌ dsh-ui 围栏 JSON 解析失败:${detail}。\n${bracketDiagnostic(raw)} 自动修复未能恢复(结构损坏),请按错误信息修正后重新调用本工具验证,通过后再发出围栏。\n${COMMON_CAUSES}`
|
|
311
330
|
}
|
|
331
|
+
const chartFailure = chartValidationFailure(parsed)
|
|
332
|
+
if (chartFailure !== undefined) return chartFailure
|
|
312
333
|
const spec = repairGenuiSpec(parsed)
|
|
313
334
|
if (spec === null) {
|
|
314
335
|
return '❌ 不是合法 GenUI spec:根对象需要 "items" 数组,且每个节点 type 必须在白名单内(见系统提示词)。请修正后重新验证。'
|