@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.
Files changed (50) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +7 -18
  3. package/README.zh-CN.md +13 -18
  4. package/SKILL.md +4 -2
  5. package/lib/assets/three.js +1 -1
  6. package/lib/client.js +3 -3
  7. package/lib/index.js +273 -61
  8. package/lib/types/client/GenuiBlock.d.ts +5 -2
  9. package/lib/types/client/TemplateDrawer.d.ts +7 -0
  10. package/lib/types/client/achievement-store.d.ts +35 -0
  11. package/lib/types/client/achievement-toast.d.ts +2 -0
  12. package/lib/types/client/achievements.d.ts +48 -0
  13. package/lib/types/client/blocks/advanced.d.ts +1 -1
  14. package/lib/types/client/blocks/image.d.ts +6 -0
  15. package/lib/types/client/blocks/state.d.ts +2 -0
  16. package/lib/types/client/fence-render.d.ts +3 -0
  17. package/lib/types/client/gallery.d.ts +3 -5
  18. package/lib/types/client/guard.d.ts +10 -5
  19. package/lib/types/client/panel.d.ts +4 -1
  20. package/lib/types/client/spec.d.ts +8 -1
  21. package/lib/types/client/templates.d.ts +22 -0
  22. package/lib/types/plugin/chart-contract.d.ts +7 -0
  23. package/lib/types/plugin/index.d.ts +1 -1
  24. package/package.json +9 -8
  25. package/src/client/GenuiBlock.module.css +141 -10
  26. package/src/client/GenuiBlock.tsx +53 -25
  27. package/src/client/PlotBlock.tsx +25 -10
  28. package/src/client/TemplateDrawer.module.css +152 -0
  29. package/src/client/TemplateDrawer.tsx +137 -0
  30. package/src/client/achievement-store.ts +151 -0
  31. package/src/client/achievement-toast.tsx +68 -0
  32. package/src/client/achievements.ts +111 -0
  33. package/src/client/blocks/advanced.tsx +34 -25
  34. package/src/client/blocks/charts.tsx +4 -4
  35. package/src/client/blocks/image.tsx +25 -0
  36. package/src/client/blocks/render-node.tsx +3 -0
  37. package/src/client/blocks/state.ts +2 -0
  38. package/src/client/dom-fence.tsx +12 -5
  39. package/src/client/fence-render.tsx +35 -11
  40. package/src/client/gallery.ts +4 -5
  41. package/src/client/guard.ts +125 -29
  42. package/src/client/index.tsx +13 -0
  43. package/src/client/panel.tsx +109 -10
  44. package/src/client/safe-math.ts +29 -1
  45. package/src/client/scene3d-core.ts +5 -2
  46. package/src/client/spec.ts +9 -1
  47. package/src/client/templates.ts +280 -0
  48. package/src/plugin/chart-contract.ts +103 -0
  49. package/src/plugin/index.ts +48 -2
  50. 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
+ }
@@ -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 (bars|line|donut) · echart (preset|option) · plot (函数图)
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
@@ -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 { GENUI_LIMITS, countDeclaredGenuiNodes, countGenuiNodes, repairGenuiSpec } from '../client/guard.ts'
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 spec = repairGenuiSpec(specOf(args))
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 && repairGenuiSpec(JSON.parse(repaired.text) as unknown) !== null) {
308
- return `❌ dsh-ui 围栏 JSON 解析失败:${detail}。\n${bracketDiagnostic(raw)} 已自动修复 ${repaired.repairs} 处,下面是修复后的 JSON,直接作为围栏正文发出即可(无需再验证):\n\`\`\`\n${repaired.text}\n\`\`\``
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 必须在白名单内(见系统提示词)。请修正后重新验证。'