@changfenhuang/dsh-genui 0.11.3 → 0.11.4
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 +16 -0
- package/README.md +10 -3
- package/README.zh-CN.md +10 -3
- package/SKILL.md +16 -4
- package/lib/assets/standalone-runtime.js +7 -6
- package/lib/client.js +15 -17
- package/lib/index.js +956 -354
- package/lib/types/client/blocks/forms.d.ts +5 -6
- package/lib/types/client/blocks/state.d.ts +2 -11
- package/lib/types/client/spec.d.ts +8 -11
- package/lib/types/client/submission-registry.d.ts +51 -0
- package/lib/types/client/table-details.d.ts +20 -0
- package/lib/types/client/walk-spec.d.ts +3 -0
- package/lib/types/plugin/fence-feedback.d.ts +50 -11
- package/lib/types/plugin/index.d.ts +3 -3
- package/lib/types/shared/fence-repair.d.ts +18 -7
- package/package.json +1 -1
- package/src/client/GenuiBlock.module.css +65 -1
- package/src/client/GenuiBlock.tsx +7 -14
- package/src/client/blocks/charts.tsx +41 -8
- package/src/client/blocks/forms.tsx +26 -48
- package/src/client/blocks/render-node.tsx +1 -1
- package/src/client/blocks/state.ts +2 -12
- package/src/client/dom-fence.tsx +94 -21
- package/src/client/genui-runtime/diagnostics.ts +20 -0
- package/src/client/genui-runtime/normalize.ts +28 -9
- package/src/client/guard.ts +93 -151
- package/src/client/inline.ts +11 -4
- package/src/client/spec.ts +8 -11
- package/src/client/submission-registry.ts +150 -0
- package/src/client/table-details.ts +151 -0
- package/src/client/walk-spec.ts +43 -0
- package/src/plugin/fence-feedback.ts +372 -53
- package/src/plugin/index.ts +7 -6
- package/src/shared/fence-repair.ts +309 -128
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { GENUI_LIMITS } from './genui-runtime/index.ts'
|
|
2
|
+
|
|
3
|
+
/** 保存 repair 所需的原始 table 字段。 */
|
|
4
|
+
export interface TableDetailSource {
|
|
5
|
+
columns?: unknown
|
|
6
|
+
rows?: unknown
|
|
7
|
+
data?: unknown
|
|
8
|
+
types?: unknown
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** 保存 repair 后的 table 列和行。 */
|
|
12
|
+
export interface PreparedTableRows {
|
|
13
|
+
columns: string[]
|
|
14
|
+
rows: Array<Array<string | number>>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** 使用 table repair 相同的转换规则处理列和行。 */
|
|
18
|
+
export function prepareTableRows(table: TableDetailSource): PreparedTableRows | null {
|
|
19
|
+
const declaredColumns = table.columns
|
|
20
|
+
const columnsSpecified = Array.isArray(declaredColumns) && declaredColumns.length > 0
|
|
21
|
+
const rawColumns = columnsSpecified && Array.isArray(declaredColumns)
|
|
22
|
+
&& typeof declaredColumns[0] === 'object' && declaredColumns[0] !== null
|
|
23
|
+
? declaredColumns.map(columnHeaderText)
|
|
24
|
+
: declaredColumns
|
|
25
|
+
let columns = repairColumnValues(rawColumns)
|
|
26
|
+
let rawRows = table.rows !== undefined ? table.rows : table.data
|
|
27
|
+
|
|
28
|
+
if (Array.isArray(rawRows) && rawRows.length > 0
|
|
29
|
+
&& typeof rawRows[0] === 'object' && rawRows[0] !== null && !Array.isArray(rawRows[0])) {
|
|
30
|
+
const keys = Array.isArray(table.columns) && table.columns.length > 0
|
|
31
|
+
&& typeof table.columns[0] === 'object' && table.columns[0] !== null
|
|
32
|
+
? table.columns.map(columnKeyOf).filter((key): key is string => key !== undefined)
|
|
33
|
+
: Object.keys(rawRows[0] as Record<string, unknown>)
|
|
34
|
+
rawRows = rawRows.map(row => keys.map(key => cellText((row as Record<string, unknown>)[key])))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let derived: PreparedTableRows | null = null
|
|
38
|
+
if (!columnsSpecified && Array.isArray(rawRows) && rawRows.length > 0 && Array.isArray(rawRows[0])) {
|
|
39
|
+
const grid = repairRows(rawRows)
|
|
40
|
+
const candidate = grid.length === 0 ? null : deriveTableColumns(grid)
|
|
41
|
+
if (candidate !== null && repairRows(candidate.rows).length === candidate.rows.length) {
|
|
42
|
+
derived = candidate
|
|
43
|
+
columns = candidate.columns.map(column => column.slice(0, 128))
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (columns.length === 0 && !Array.isArray(table.columns)) return null
|
|
48
|
+
const rows = repairRows(derived === null ? rawRows : derived.rows)
|
|
49
|
+
if (!Array.isArray(rawRows)) return null
|
|
50
|
+
return { columns: columns.slice(0, GENUI_LIMITS.maxTableCols), rows }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 返回 repair 后可显示详情的 table 行。 */
|
|
54
|
+
export function tableRowsForDetails<Row = unknown>(table: TableDetailSource): Row[] {
|
|
55
|
+
return (prepareTableRows(table)?.rows ?? []) as Row[]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 按照 table renderer 的规则识别分组标题行。 */
|
|
59
|
+
export function isTableGroupHeaderRow(row: unknown, types: unknown): boolean {
|
|
60
|
+
if (!Array.isArray(row) || !Array.isArray(types) || types[0] !== 'group') return false
|
|
61
|
+
return String(row[0] ?? '').trim() !== ''
|
|
62
|
+
&& row.slice(1).every(cell => String(cell ?? '').trim() === '')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 判断详情索引对应的行是否可展开并显示。 */
|
|
66
|
+
export function isTableDetailReachable(table: TableDetailSource, rowIndex: number): boolean {
|
|
67
|
+
const rows = tableRowsForDetails(table)
|
|
68
|
+
return rowIndex >= 0
|
|
69
|
+
&& rowIndex < rows.length
|
|
70
|
+
&& !isTableGroupHeaderRow(rows[rowIndex], table.types)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 按照 guard 接受的 object alias 规则修复 table 列。 */
|
|
74
|
+
function repairColumnValues(value: unknown): string[] {
|
|
75
|
+
if (!Array.isArray(value)) return []
|
|
76
|
+
const columns: string[] = []
|
|
77
|
+
for (const item of value) {
|
|
78
|
+
if (columns.length >= GENUI_LIMITS.maxTableCols) break
|
|
79
|
+
if (typeof item === 'string') columns.push(item.slice(0, 128))
|
|
80
|
+
else if (item !== null && typeof item === 'object') {
|
|
81
|
+
const column = item as Record<string, unknown>
|
|
82
|
+
const label = typeof column.label === 'string' ? column.label
|
|
83
|
+
: typeof column.value === 'string' ? column.value
|
|
84
|
+
: typeof column.title === 'string' ? column.title
|
|
85
|
+
: JSON.stringify(item)
|
|
86
|
+
columns.push(label.slice(0, 128))
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return columns
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** 按照 table repair 的单元格规则裁剪并清理行。 */
|
|
93
|
+
function repairRows(value: unknown): Array<Array<string | number>> {
|
|
94
|
+
if (!Array.isArray(value)) return []
|
|
95
|
+
const rows: Array<Array<string | number>> = []
|
|
96
|
+
for (const row of value) {
|
|
97
|
+
if (rows.length >= GENUI_LIMITS.maxTableRows) break
|
|
98
|
+
if (!Array.isArray(row)) continue
|
|
99
|
+
const cells: Array<string | number> = []
|
|
100
|
+
for (const cell of row) {
|
|
101
|
+
if (cells.length >= GENUI_LIMITS.maxTableCols) break
|
|
102
|
+
if (typeof cell === 'string') cells.push(cell.slice(0, 256))
|
|
103
|
+
else if (typeof cell === 'number' && Number.isFinite(cell)) cells.push(cell)
|
|
104
|
+
}
|
|
105
|
+
if (cells.length > 0) rows.push(cells)
|
|
106
|
+
}
|
|
107
|
+
return rows
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 从有效的首行表头生成 table 列。 */
|
|
111
|
+
function deriveTableColumns(rows: Array<Array<string | number>>): PreparedTableRows | null {
|
|
112
|
+
const header = rows[0]
|
|
113
|
+
if (header === undefined || header.length === 0) return null
|
|
114
|
+
const body = rows.slice(1)
|
|
115
|
+
if (body.length === 0) {
|
|
116
|
+
const columns = header.map(cell => String(cell).trim())
|
|
117
|
+
return columns.every(column => column !== '') ? { columns, rows: [] } : null
|
|
118
|
+
}
|
|
119
|
+
if (body.every(row => row.length === header.length)) {
|
|
120
|
+
return { columns: header.map(cell => String(cell).trim()), rows: body }
|
|
121
|
+
}
|
|
122
|
+
return { columns: Array.from({ length: header.length }, (_unused, index) => `列${index + 1}`), rows }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** 读取 object row 映射到列时使用的 key。 */
|
|
126
|
+
function columnKeyOf(value: unknown): string | undefined {
|
|
127
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return undefined
|
|
128
|
+
const column = value as Record<string, unknown>
|
|
129
|
+
for (const key of ['key', 'dataIndex', 'title', 'label'] as const) {
|
|
130
|
+
if (typeof column[key] === 'string' && column[key] !== '') return column[key]
|
|
131
|
+
}
|
|
132
|
+
return undefined
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** 将 object row 中的值转换为 table 单元格,并保留列位置。 */
|
|
136
|
+
function cellText(value: unknown): string | number {
|
|
137
|
+
if (typeof value === 'string') return value
|
|
138
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
139
|
+
if (value === null || value === undefined) return ''
|
|
140
|
+
return JSON.stringify(value)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** 读取 object column 显示的标题。 */
|
|
144
|
+
function columnHeaderText(value: unknown): string {
|
|
145
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return String(value)
|
|
146
|
+
const column = value as Record<string, unknown>
|
|
147
|
+
for (const key of ['title', 'label', 'key', 'dataIndex'] as const) {
|
|
148
|
+
if (typeof column[key] === 'string' && column[key] !== '') return column[key]
|
|
149
|
+
}
|
|
150
|
+
return JSON.stringify(value)
|
|
151
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { GenuiNode, GenuiSpec } from './spec.ts'
|
|
2
|
+
import { GENUI_LIMITS } from './genui-runtime/index.ts'
|
|
3
|
+
import { isTableDetailReachable, tableRowsForDetails } from './table-details.ts'
|
|
4
|
+
|
|
5
|
+
/** 遍历完整 GenUI 组件树,并提供每个组件在 spec 中的路径。 */
|
|
6
|
+
export function walkGenuiNodes(spec: GenuiSpec, visitor: (node: GenuiNode, path: string) => void): void {
|
|
7
|
+
const walk = (nodes: GenuiNode[], path: string, depth: number): void => {
|
|
8
|
+
if (depth > GENUI_LIMITS.maxDepth) return
|
|
9
|
+
nodes.forEach((node, index) => visit(node, `${path}[${index}]`, depth))
|
|
10
|
+
}
|
|
11
|
+
const visit = (node: GenuiNode, at: string, depth: number): void => {
|
|
12
|
+
if (depth > GENUI_LIMITS.maxDepth) return
|
|
13
|
+
visitor(node, at)
|
|
14
|
+
if (depth >= GENUI_LIMITS.maxDepth) return
|
|
15
|
+
switch (node.type) {
|
|
16
|
+
case 'row':
|
|
17
|
+
case 'col':
|
|
18
|
+
case 'grid':
|
|
19
|
+
case 'card':
|
|
20
|
+
walk(node.items, `${at}.items`, depth + 1)
|
|
21
|
+
break
|
|
22
|
+
case 'tabs':
|
|
23
|
+
node.tabs.forEach((tab, tabIndex) => walk(tab.items, `${at}.tabs[${tabIndex}].items`, depth + 1))
|
|
24
|
+
break
|
|
25
|
+
case 'accordion':
|
|
26
|
+
node.items.forEach((item, itemIndex) => walk(item.items, `${at}.items[${itemIndex}].items`, depth + 1))
|
|
27
|
+
break
|
|
28
|
+
case 'list':
|
|
29
|
+
node.items.forEach((item, itemIndex) => {
|
|
30
|
+
if (item !== null && typeof item === 'object' && 'type' in item) visit(item, `${at}.items[${itemIndex}]`, depth + 1)
|
|
31
|
+
})
|
|
32
|
+
break
|
|
33
|
+
case 'table':
|
|
34
|
+
node.details?.slice(0, tableRowsForDetails(node).length).forEach((detail, rowIndex) => {
|
|
35
|
+
if (detail !== null && isTableDetailReachable(node, rowIndex)) {
|
|
36
|
+
walk(detail, `${at}.details[${rowIndex}]`, depth + 1)
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
break
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
walk(spec.items, 'items', 0)
|
|
43
|
+
}
|