@yuhere/js-jsonrpc2 0.0.0-stage → 2.1.1

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 (174) hide show
  1. package/README.md +277 -2
  2. package/SKILL.md +237 -0
  3. package/dist/cli/cli.d.ts +3 -0
  4. package/dist/cli/cli.d.ts.map +1 -0
  5. package/dist/cli/cli.js +132 -0
  6. package/dist/cli/cli.js.map +1 -0
  7. package/dist/cli/demo-methods.d.ts +10 -0
  8. package/dist/cli/demo-methods.d.ts.map +1 -0
  9. package/dist/cli/demo-methods.js +477 -0
  10. package/dist/cli/demo-methods.js.map +1 -0
  11. package/dist/cli/express.d.ts +2 -0
  12. package/dist/cli/express.d.ts.map +1 -0
  13. package/dist/cli/express.js +42 -0
  14. package/dist/cli/express.js.map +1 -0
  15. package/dist/cli/stdio.d.ts +2 -0
  16. package/dist/cli/stdio.d.ts.map +1 -0
  17. package/dist/cli/stdio.js +5 -0
  18. package/dist/cli/stdio.js.map +1 -0
  19. package/dist/core/Repository.d.ts +24 -0
  20. package/dist/core/Repository.d.ts.map +1 -0
  21. package/dist/core/Repository.js +153 -0
  22. package/dist/core/Repository.js.map +1 -0
  23. package/dist/core/convert.d.ts +4 -0
  24. package/dist/core/convert.d.ts.map +1 -0
  25. package/dist/core/convert.js +29 -0
  26. package/dist/core/convert.js.map +1 -0
  27. package/dist/core/descriptor.d.ts +27 -0
  28. package/dist/core/descriptor.d.ts.map +1 -0
  29. package/dist/core/descriptor.js +83 -0
  30. package/dist/core/descriptor.js.map +1 -0
  31. package/dist/core/index.d.ts +4 -0
  32. package/dist/core/index.d.ts.map +1 -0
  33. package/dist/core/index.js +3 -0
  34. package/dist/core/index.js.map +1 -0
  35. package/dist/core/permission.d.ts +4 -0
  36. package/dist/core/permission.d.ts.map +1 -0
  37. package/dist/core/permission.js +18 -0
  38. package/dist/core/permission.js.map +1 -0
  39. package/dist/core/spec-check.d.ts +3 -0
  40. package/dist/core/spec-check.d.ts.map +1 -0
  41. package/dist/core/spec-check.js +11 -0
  42. package/dist/core/spec-check.js.map +1 -0
  43. package/dist/core/system-methods.d.ts +3 -0
  44. package/dist/core/system-methods.d.ts.map +1 -0
  45. package/dist/core/system-methods.js +76 -0
  46. package/dist/core/system-methods.js.map +1 -0
  47. package/dist/errors/RpcError.d.ts +9 -0
  48. package/dist/errors/RpcError.d.ts.map +1 -0
  49. package/dist/errors/RpcError.js +23 -0
  50. package/dist/errors/RpcError.js.map +1 -0
  51. package/dist/errors/codes.d.ts +7 -0
  52. package/dist/errors/codes.d.ts.map +1 -0
  53. package/dist/errors/codes.js +7 -0
  54. package/dist/errors/codes.js.map +1 -0
  55. package/dist/errors/index.d.ts +3 -0
  56. package/dist/errors/index.d.ts.map +1 -0
  57. package/dist/errors/index.js +3 -0
  58. package/dist/errors/index.js.map +1 -0
  59. package/dist/index.d.ts +18 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +17 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/schema/index.d.ts +4 -0
  64. package/dist/schema/index.d.ts.map +1 -0
  65. package/dist/schema/index.js +4 -0
  66. package/dist/schema/index.js.map +1 -0
  67. package/dist/schema/json/index.d.ts +19 -0
  68. package/dist/schema/json/index.d.ts.map +1 -0
  69. package/dist/schema/json/index.js +6 -0
  70. package/dist/schema/json/index.js.map +1 -0
  71. package/dist/schema/json/marker.d.ts +23 -0
  72. package/dist/schema/json/marker.d.ts.map +1 -0
  73. package/dist/schema/json/marker.js +27 -0
  74. package/dist/schema/json/marker.js.map +1 -0
  75. package/dist/schema/json/primitives.d.ts +202 -0
  76. package/dist/schema/json/primitives.d.ts.map +1 -0
  77. package/dist/schema/json/primitives.js +598 -0
  78. package/dist/schema/json/primitives.js.map +1 -0
  79. package/dist/schema/primitives.d.ts +25 -0
  80. package/dist/schema/primitives.d.ts.map +1 -0
  81. package/dist/schema/primitives.js +153 -0
  82. package/dist/schema/primitives.js.map +1 -0
  83. package/dist/schema/rpc.d.ts +21 -0
  84. package/dist/schema/rpc.d.ts.map +1 -0
  85. package/dist/schema/rpc.js +17 -0
  86. package/dist/schema/rpc.js.map +1 -0
  87. package/dist/schema/z/protocol.d.ts +9 -0
  88. package/dist/schema/z/protocol.d.ts.map +1 -0
  89. package/dist/schema/z/protocol.js +9 -0
  90. package/dist/schema/z/protocol.js.map +1 -0
  91. package/dist/schema/z/register.d.ts +9 -0
  92. package/dist/schema/z/register.d.ts.map +1 -0
  93. package/dist/schema/z/register.js +21 -0
  94. package/dist/schema/z/register.js.map +1 -0
  95. package/dist/stream/index.d.ts +3 -0
  96. package/dist/stream/index.d.ts.map +1 -0
  97. package/dist/stream/index.js +2 -0
  98. package/dist/stream/index.js.map +1 -0
  99. package/dist/stream/stringify-stream.d.ts +15 -0
  100. package/dist/stream/stringify-stream.d.ts.map +1 -0
  101. package/dist/stream/stringify-stream.js +322 -0
  102. package/dist/stream/stringify-stream.js.map +1 -0
  103. package/dist/stream/stringify.d.ts +19 -0
  104. package/dist/stream/stringify.d.ts.map +1 -0
  105. package/dist/stream/stringify.js +217 -0
  106. package/dist/stream/stringify.js.map +1 -0
  107. package/dist/transport/express/body.d.ts +9 -0
  108. package/dist/transport/express/body.d.ts.map +1 -0
  109. package/dist/transport/express/body.js +28 -0
  110. package/dist/transport/express/body.js.map +1 -0
  111. package/dist/transport/express/index.d.ts +3 -0
  112. package/dist/transport/express/index.d.ts.map +1 -0
  113. package/dist/transport/express/index.js +2 -0
  114. package/dist/transport/express/index.js.map +1 -0
  115. package/dist/transport/express/middleware.d.ts +12 -0
  116. package/dist/transport/express/middleware.d.ts.map +1 -0
  117. package/dist/transport/express/middleware.js +73 -0
  118. package/dist/transport/express/middleware.js.map +1 -0
  119. package/dist/transport/express/respond.d.ts +4 -0
  120. package/dist/transport/express/respond.d.ts.map +1 -0
  121. package/dist/transport/express/respond.js +125 -0
  122. package/dist/transport/express/respond.js.map +1 -0
  123. package/dist/transport/stdio/index.d.ts +2 -0
  124. package/dist/transport/stdio/index.d.ts.map +1 -0
  125. package/dist/transport/stdio/index.js +2 -0
  126. package/dist/transport/stdio/index.js.map +1 -0
  127. package/dist/transport/stdio/middleware.d.ts +34 -0
  128. package/dist/transport/stdio/middleware.d.ts.map +1 -0
  129. package/dist/transport/stdio/middleware.js +274 -0
  130. package/dist/transport/stdio/middleware.js.map +1 -0
  131. package/dist/transport/ws/index.d.ts +3 -0
  132. package/dist/transport/ws/index.d.ts.map +1 -0
  133. package/dist/transport/ws/index.js +3 -0
  134. package/dist/transport/ws/index.js.map +1 -0
  135. package/dist/transport/ws/middleware.d.ts +25 -0
  136. package/dist/transport/ws/middleware.d.ts.map +1 -0
  137. package/dist/transport/ws/middleware.js +209 -0
  138. package/dist/transport/ws/middleware.js.map +1 -0
  139. package/dist/transport/ws/standalone.d.ts +22 -0
  140. package/dist/transport/ws/standalone.d.ts.map +1 -0
  141. package/dist/transport/ws/standalone.js +39 -0
  142. package/dist/transport/ws/standalone.js.map +1 -0
  143. package/dist/types.d.ts +75 -0
  144. package/dist/types.d.ts.map +1 -0
  145. package/dist/types.js +2 -0
  146. package/dist/types.js.map +1 -0
  147. package/htdoc/assets/index-CQr-lnlD.js +9286 -0
  148. package/htdoc/assets/index-CQr-lnlD.js.map +1 -0
  149. package/htdoc/assets/index-hcP2-9jw.css +729 -0
  150. package/htdoc/index.html +13 -0
  151. package/package.json +89 -3
  152. package/webui/index.html +12 -0
  153. package/webui/src/App.tsx +56 -0
  154. package/webui/src/api/httpTransport.ts +148 -0
  155. package/webui/src/api/paramForm.ts +79 -0
  156. package/webui/src/api/rpc.ts +57 -0
  157. package/webui/src/api/schema.ts +12 -0
  158. package/webui/src/api/types.ts +16 -0
  159. package/webui/src/api/wsTransport.ts +305 -0
  160. package/webui/src/components/MethodDetail.tsx +43 -0
  161. package/webui/src/components/MethodTree.tsx +118 -0
  162. package/webui/src/components/SchemaForm.tsx +696 -0
  163. package/webui/src/components/SchemaView.tsx +151 -0
  164. package/webui/src/components/TestPanel.tsx +279 -0
  165. package/webui/src/components/Toolbar.tsx +70 -0
  166. package/webui/src/hooks/useMethodDetail.ts +61 -0
  167. package/webui/src/hooks/useMethods.ts +27 -0
  168. package/webui/src/main.tsx +13 -0
  169. package/webui/src/styles/geek.css +729 -0
  170. package/webui/src/theme/ThemeProvider.tsx +79 -0
  171. package/webui/src/vite-env.d.ts +1 -0
  172. package/webui/tsconfig.json +34 -0
  173. package/webui/tsconfig.node.json +13 -0
  174. package/webui/vite.config.ts +15 -0
@@ -0,0 +1,696 @@
1
+ import { useState } from 'react';
2
+ import type { SchemaDescription } from '../api/schema';
3
+ import { isSchema, sampleFromSchema } from '../api/paramForm';
4
+
5
+ // ─────────────────────────────────────────────────────────────
6
+ // 视觉常量
7
+ // ─────────────────────────────────────────────────────────────
8
+
9
+ const KIND_COLORS: Record<string, string> = {
10
+ string: 'var(--syn-string)',
11
+ number: 'var(--syn-number)',
12
+ boolean: 'var(--syn-boolean)',
13
+ literal: 'var(--syn-literal)',
14
+ array: 'var(--syn-container)',
15
+ tuple: 'var(--syn-container)',
16
+ object: 'var(--syn-object)',
17
+ record: 'var(--syn-object)',
18
+ union: 'var(--syn-union)',
19
+ enum: 'var(--syn-union)',
20
+ nullable: 'var(--syn-muted)',
21
+ unknown: 'var(--syn-muted)',
22
+ };
23
+
24
+ function colorOf(kind: string): string {
25
+ return KIND_COLORS[kind] ?? 'var(--fg)';
26
+ }
27
+
28
+ function formatConstraints(c?: Record<string, unknown>): string {
29
+ if (!c) return '';
30
+ return Object.entries(c)
31
+ .filter(([, v]) => v !== undefined)
32
+ .map(([k, v]) => `${k}=${JSON.stringify(v)}`)
33
+ .join(', ');
34
+ }
35
+
36
+ // ─────────────────────────────────────────────────────────────
37
+ // 顶层:一组参数
38
+ // ─────────────────────────────────────────────────────────────
39
+
40
+ export interface SchemaFormProps {
41
+ /** signature[1..],每个元素对应一个客户端参数 */
42
+ params: SchemaDescription[];
43
+ /** 与 params 等长的值数组 */
44
+ values: unknown[];
45
+ onChange: (index: number, value: unknown) => void;
46
+ disabled?: boolean;
47
+ }
48
+
49
+ export function SchemaForm({ params, values, onChange, disabled }: SchemaFormProps) {
50
+ if (params.length === 0) {
51
+ return <p className="hint">此方法无需参数。</p>;
52
+ }
53
+ return (
54
+ <div className="schema-form">
55
+ {params.map((schema, i) => (
56
+ <SchemaField
57
+ key={i}
58
+ schema={schema}
59
+ value={values[i]}
60
+ onChange={(v) => onChange(i, v)}
61
+ label={`参数 [${i}]`}
62
+ depth={0}
63
+ disabled={disabled}
64
+ />
65
+ ))}
66
+ </div>
67
+ );
68
+ }
69
+
70
+ // ─────────────────────────────────────────────────────────────
71
+ // 通用节点
72
+ // ─────────────────────────────────────────────────────────────
73
+
74
+ interface SchemaFieldProps {
75
+ schema: SchemaDescription;
76
+ value: unknown;
77
+ onChange: (v: unknown) => void;
78
+ label?: string;
79
+ depth?: number;
80
+ defaultOpen?: boolean;
81
+ disabled?: boolean;
82
+ }
83
+
84
+ function SchemaField({
85
+ schema,
86
+ value,
87
+ onChange,
88
+ label,
89
+ depth = 0,
90
+ defaultOpen = true,
91
+ disabled,
92
+ }: SchemaFieldProps) {
93
+ const [open, setOpen] = useState(defaultOpen);
94
+ const kind = schema.kind;
95
+
96
+ // ── nullable 包装:标题 + 控制按钮同行,下面递归或显示 null ──
97
+ if (kind === 'nullable') {
98
+ const inner =
99
+ schema.children && isSchema(schema.children.inner)
100
+ ? schema.children.inner
101
+ : undefined;
102
+ if (!inner) {
103
+ return (
104
+ <UnknownLeaf
105
+ schema={schema}
106
+ value={value}
107
+ onChange={onChange}
108
+ label={label}
109
+ depth={depth}
110
+ disabled={disabled}
111
+ />
112
+ );
113
+ }
114
+
115
+ const isNull = value === null || value === undefined;
116
+ return (
117
+ <div className="sf-node sf-row" style={{ paddingLeft: depth * 14 }}>
118
+ <div className="sf-head sf-head-inline">
119
+ {label !== undefined && <span className="sf-label">{label}</span>}
120
+ <span className="sf-kind" style={{ color: colorOf('nullable') }}>
121
+ nullable
122
+ </span>
123
+ <span className="sf-nullable">| null</span>
124
+ {!isNull && (
125
+ <button
126
+ type="button"
127
+ className="tiny"
128
+ disabled={disabled}
129
+ onClick={() => onChange(null)}
130
+ >
131
+ 置 null
132
+ </button>
133
+ )}
134
+ {isNull && (
135
+ <button
136
+ type="button"
137
+ className="tiny"
138
+ disabled={disabled}
139
+ onClick={() => onChange(sampleFromSchema(inner))}
140
+ >
141
+ 填值
142
+ </button>
143
+ )}
144
+ </div>
145
+ <div className="sf-input-cell">
146
+ {isNull ? (
147
+ <span className="sf-null-indicator">null</span>
148
+ ) : (
149
+ <SchemaField
150
+ schema={inner}
151
+ value={value}
152
+ onChange={onChange}
153
+ depth={0}
154
+ disabled={disabled}
155
+ defaultOpen
156
+ />
157
+ )}
158
+ </div>
159
+ </div>
160
+ );
161
+ }
162
+
163
+ // ── 标量叶子:标题与输入框同一行 ──
164
+ if (isScalarKind(kind)) {
165
+ return (
166
+ <div className="sf-node sf-row" style={{ paddingLeft: depth * 14 }}>
167
+ <div className="sf-head sf-head-inline">
168
+ {label !== undefined && <span className="sf-label">{label}</span>}
169
+ <span className="sf-kind" style={{ color: colorOf(kind) }}>
170
+ {kind}
171
+ </span>
172
+ {schema.description && (
173
+ <span className="sf-desc">— {schema.description}</span>
174
+ )}
175
+ <Constraints schema={schema} />
176
+ </div>
177
+ <div className="sf-input-cell">
178
+ <ScalarInput
179
+ schema={schema}
180
+ value={value}
181
+ onChange={onChange}
182
+ disabled={disabled}
183
+ />
184
+ </div>
185
+ </div>
186
+ );
187
+ }
188
+
189
+ // ── union:标题 + 分支选择同行 ──
190
+ if (kind === 'union') {
191
+ const options = Array.isArray(schema.children?.options)
192
+ ? (schema.children!.options as unknown[]).filter(isSchema)
193
+ : [];
194
+ return (
195
+ <div className="sf-node sf-row" style={{ paddingLeft: depth * 14 }}>
196
+ <div className="sf-head sf-head-inline">
197
+ {label !== undefined && <span className="sf-label">{label}</span>}
198
+ <span className="sf-kind" style={{ color: colorOf('union') }}>
199
+ union
200
+ </span>
201
+ {schema.description && (
202
+ <span className="sf-desc">— {schema.description}</span>
203
+ )}
204
+ </div>
205
+ <div className="sf-input-cell">
206
+ <UnionField
207
+ options={options}
208
+ value={value}
209
+ onChange={onChange}
210
+ depth={0}
211
+ disabled={disabled}
212
+ />
213
+ </div>
214
+ </div>
215
+ );
216
+ }
217
+
218
+ // ── 容器:标题行(展开/收起)+ 展开的子内容 ──
219
+ if (
220
+ kind === 'array' ||
221
+ kind === 'tuple' ||
222
+ kind === 'object' ||
223
+ kind === 'record'
224
+ ) {
225
+ return (
226
+ <div className="sf-node" style={{ paddingLeft: depth * 14 }}>
227
+ <div
228
+ className="sf-head sf-head-clickable"
229
+ onClick={() => setOpen((o) => !o)}
230
+ >
231
+ <span className="sf-expander">{open ? '▾' : '▸'}</span>
232
+ {label !== undefined && <span className="sf-label">{label}</span>}
233
+ <span className="sf-kind" style={{ color: colorOf(kind) }}>
234
+ {kind}
235
+ </span>
236
+ {schema.description && (
237
+ <span className="sf-desc">— {schema.description}</span>
238
+ )}
239
+ <Constraints schema={schema} />
240
+ </div>
241
+ {open && (
242
+ <div className="sf-children">
243
+ <ContainerChildren
244
+ schema={schema}
245
+ value={value}
246
+ onChange={onChange}
247
+ depth={depth + 1}
248
+ disabled={disabled}
249
+ />
250
+ </div>
251
+ )}
252
+ </div>
253
+ );
254
+ }
255
+
256
+ // ── unknown / 其他兜底 ──
257
+ return (
258
+ <UnknownLeaf
259
+ schema={schema}
260
+ value={value}
261
+ onChange={onChange}
262
+ label={label}
263
+ depth={depth}
264
+ disabled={disabled}
265
+ />
266
+ );
267
+ }
268
+
269
+ // ─────────────────────────────────────────────────────────────
270
+ // 标量控件
271
+ // ─────────────────────────────────────────────────────────────
272
+
273
+ function isScalarKind(kind: string): boolean {
274
+ return (
275
+ kind === 'string' ||
276
+ kind === 'number' ||
277
+ kind === 'boolean' ||
278
+ kind === 'enum' ||
279
+ kind === 'literal'
280
+ );
281
+ }
282
+
283
+ function ScalarInput({
284
+ schema,
285
+ value,
286
+ onChange,
287
+ disabled,
288
+ }: {
289
+ schema: SchemaDescription;
290
+ value: unknown;
291
+ onChange: (v: unknown) => void;
292
+ disabled?: boolean;
293
+ }) {
294
+ switch (schema.kind) {
295
+ case 'boolean':
296
+ return (
297
+ <label className="sf-scalar">
298
+ <input
299
+ type="checkbox"
300
+ checked={Boolean(value)}
301
+ disabled={disabled}
302
+ onChange={(e) => onChange(e.target.checked)}
303
+ />
304
+ <span>{value ? 'true' : 'false'}</span>
305
+ </label>
306
+ );
307
+ case 'number':
308
+ return (
309
+ <input
310
+ className="sf-input"
311
+ type="number"
312
+ disabled={disabled}
313
+ value={value == null ? '' : String(value)}
314
+ onChange={(e) =>
315
+ onChange(e.target.value === '' ? null : Number(e.target.value))
316
+ }
317
+ />
318
+ );
319
+ case 'enum': {
320
+ const values = Array.isArray(schema.constraints?.values)
321
+ ? (schema.constraints!.values as string[])
322
+ : [];
323
+ return (
324
+ <select
325
+ className="sf-input"
326
+ disabled={disabled}
327
+ value={value == null ? '' : String(value)}
328
+ onChange={(e) => onChange(e.target.value)}
329
+ >
330
+ {values.map((v) => (
331
+ <option key={v} value={v}>
332
+ {v}
333
+ </option>
334
+ ))}
335
+ </select>
336
+ );
337
+ }
338
+ case 'literal':
339
+ return (
340
+ <div className="sf-scalar">
341
+ <code className="literal-value">
342
+ {JSON.stringify(schema.constraints?.value ?? null)}
343
+ </code>
344
+ <span className="hint">(固定值)</span>
345
+ </div>
346
+ );
347
+ default:
348
+ // string
349
+ return (
350
+ <input
351
+ className="sf-input"
352
+ type="text"
353
+ disabled={disabled}
354
+ value={value == null ? '' : String(value)}
355
+ onChange={(e) => onChange(e.target.value)}
356
+ />
357
+ );
358
+ }
359
+ }
360
+
361
+ // ─────────────────────────────────────────────────────────────
362
+ // union 分支选择
363
+ // ─────────────────────────────────────────────────────────────
364
+
365
+ function UnionField({
366
+ options,
367
+ value,
368
+ onChange,
369
+ depth,
370
+ disabled,
371
+ }: {
372
+ options: SchemaDescription[];
373
+ value: unknown;
374
+ onChange: (v: unknown) => void;
375
+ depth: number;
376
+ disabled?: boolean;
377
+ }) {
378
+ const [selected, setSelected] = useState(0);
379
+ const active = options[selected];
380
+ if (!active) {
381
+ return <p className="hint">(union 无分支)</p>;
382
+ }
383
+ return (
384
+ <div style={{ paddingLeft: depth * 14 }}>
385
+ <div className="sf-union-picker">
386
+ <select
387
+ className="sf-input"
388
+ disabled={disabled}
389
+ value={selected}
390
+ onChange={(e) => {
391
+ const i = Number(e.target.value);
392
+ setSelected(i);
393
+ const next = options[i];
394
+ if (next) onChange(sampleFromSchema(next));
395
+ }}
396
+ >
397
+ {options.map((opt, i) => (
398
+ <option key={i} value={i}>
399
+ option[{i}] — {opt.kind}
400
+ </option>
401
+ ))}
402
+ </select>
403
+ </div>
404
+ <SchemaField
405
+ schema={active}
406
+ value={value}
407
+ onChange={onChange}
408
+ depth={depth + 1}
409
+ disabled={disabled}
410
+ defaultOpen
411
+ />
412
+ </div>
413
+ );
414
+ }
415
+
416
+ // ─────────────────────────────────────────────────────────────
417
+ // 容器子节点
418
+ // ─────────────────────────────────────────────────────────────
419
+
420
+ function ContainerChildren({
421
+ schema,
422
+ value,
423
+ onChange,
424
+ depth,
425
+ disabled,
426
+ }: {
427
+ schema: SchemaDescription;
428
+ value: unknown;
429
+ onChange: (v: unknown) => void;
430
+ depth: number;
431
+ disabled?: boolean;
432
+ }) {
433
+ const c = schema.children ?? {};
434
+
435
+ // array
436
+ if (schema.kind === 'array') {
437
+ const itemSchema = isSchema(c.items) ? c.items : undefined;
438
+ if (!itemSchema) return <p className="hint">(数组无 item schema)</p>;
439
+ const arr = Array.isArray(value) ? value : [];
440
+ return (
441
+ <div>
442
+ {arr.map((item, i) => (
443
+ <div key={i} className="sf-array-item">
444
+ <SchemaField
445
+ schema={itemSchema}
446
+ value={item}
447
+ onChange={(v) => {
448
+ const next = arr.slice();
449
+ next[i] = v;
450
+ onChange(next);
451
+ }}
452
+ label={`[${i}]`}
453
+ depth={depth}
454
+ disabled={disabled}
455
+ defaultOpen={false}
456
+ />
457
+ <button
458
+ type="button"
459
+ className="tiny"
460
+ disabled={disabled}
461
+ onClick={() => onChange(arr.filter((_, j) => j !== i))}
462
+ >
463
+ 删除
464
+ </button>
465
+ </div>
466
+ ))}
467
+ <button
468
+ type="button"
469
+ className="tiny"
470
+ disabled={disabled}
471
+ onClick={() => onChange([...arr, sampleFromSchema(itemSchema)])}
472
+ >
473
+ + 添加项
474
+ </button>
475
+ </div>
476
+ );
477
+ }
478
+
479
+ // tuple
480
+ if (schema.kind === 'tuple') {
481
+ const items = Array.isArray(c.items)
482
+ ? (c.items as unknown[]).filter(isSchema)
483
+ : [];
484
+ const arr = Array.isArray(value) ? value : [];
485
+ return (
486
+ <div>
487
+ {items.map((itemSchema, i) => (
488
+ <SchemaField
489
+ key={i}
490
+ schema={itemSchema}
491
+ value={arr[i]}
492
+ onChange={(v) => {
493
+ const next = arr.slice();
494
+ next[i] = v;
495
+ onChange(next);
496
+ }}
497
+ label={`[${i}]`}
498
+ depth={depth}
499
+ disabled={disabled}
500
+ defaultOpen={false}
501
+ />
502
+ ))}
503
+ </div>
504
+ );
505
+ }
506
+
507
+ // object
508
+ if (schema.kind === 'object') {
509
+ const props = (c.properties ?? {}) as Record<string, unknown>;
510
+ const obj =
511
+ value && typeof value === 'object' && !Array.isArray(value)
512
+ ? (value as Record<string, unknown>)
513
+ : {};
514
+ const strict = Boolean(schema.constraints?.strict);
515
+ return (
516
+ <div>
517
+ {Object.entries(props).map(([key, prop]) => {
518
+ if (!isSchema(prop)) return null;
519
+ return (
520
+ <SchemaField
521
+ key={key}
522
+ schema={prop}
523
+ value={obj[key]}
524
+ onChange={(v) => {
525
+ const next = { ...obj };
526
+ if (v === undefined) delete next[key];
527
+ else next[key] = v;
528
+ onChange(next);
529
+ }}
530
+ label={key}
531
+ depth={depth}
532
+ disabled={disabled}
533
+ defaultOpen={false}
534
+ />
535
+ );
536
+ })}
537
+ {!strict && (
538
+ <p
539
+ className="hint"
540
+ style={{ paddingLeft: depth * 14, margin: '4px 0' }}
541
+ >
542
+ (非 strict 模式,允许额外字段)
543
+ </p>
544
+ )}
545
+ </div>
546
+ );
547
+ }
548
+
549
+ // record
550
+ if (schema.kind === 'record') {
551
+ const valueSchema = isSchema(c.values) ? c.values : undefined;
552
+ if (!valueSchema) return <p className="hint">(record 无 value schema)</p>;
553
+ const obj =
554
+ value && typeof value === 'object' && !Array.isArray(value)
555
+ ? (value as Record<string, unknown>)
556
+ : {};
557
+ return (
558
+ <div>
559
+ {Object.entries(obj).map(([key, v]) => (
560
+ <div key={key} className="sf-record-item">
561
+ <SchemaField
562
+ schema={valueSchema}
563
+ value={v}
564
+ onChange={(nv) => onChange({ ...obj, [key]: nv })}
565
+ label={key}
566
+ depth={depth}
567
+ disabled={disabled}
568
+ defaultOpen={false}
569
+ />
570
+ <button
571
+ type="button"
572
+ className="tiny"
573
+ disabled={disabled}
574
+ onClick={() => {
575
+ const next = { ...obj };
576
+ delete next[key];
577
+ onChange(next);
578
+ }}
579
+ >
580
+ 删除
581
+ </button>
582
+ </div>
583
+ ))}
584
+ <AddRecordKey
585
+ onAdd={(key, v) => onChange({ ...obj, [key]: v })}
586
+ sample={() => sampleFromSchema(valueSchema)}
587
+ disabled={disabled}
588
+ />
589
+ </div>
590
+ );
591
+ }
592
+
593
+ return null;
594
+ }
595
+
596
+ function AddRecordKey({
597
+ onAdd,
598
+ sample,
599
+ disabled,
600
+ }: {
601
+ onAdd: (key: string, value: unknown) => void;
602
+ sample: () => unknown;
603
+ disabled?: boolean;
604
+ }) {
605
+ const [key, setKey] = useState('');
606
+ return (
607
+ <div className="sf-record-add">
608
+ <input
609
+ className="sf-input"
610
+ placeholder="新键名"
611
+ value={key}
612
+ onChange={(e) => setKey(e.target.value)}
613
+ disabled={disabled}
614
+ />
615
+ <button
616
+ type="button"
617
+ className="tiny"
618
+ disabled={disabled || !key}
619
+ onClick={() => {
620
+ onAdd(key, sample());
621
+ setKey('');
622
+ }}
623
+ >
624
+ + 添加键
625
+ </button>
626
+ </div>
627
+ );
628
+ }
629
+
630
+ // ─────────────────────────────────────────────────────────────
631
+ // unknown:JSON textarea(标题 + textarea 同一行)
632
+ // ─────────────────────────────────────────────────────────────
633
+
634
+ function UnknownLeaf({
635
+ schema,
636
+ value,
637
+ onChange,
638
+ label,
639
+ depth,
640
+ disabled,
641
+ }: {
642
+ schema: SchemaDescription;
643
+ value: unknown;
644
+ onChange: (v: unknown) => void;
645
+ label?: string;
646
+ depth: number;
647
+ disabled?: boolean;
648
+ }) {
649
+ const [text, setText] = useState(() => JSON.stringify(value ?? null, null, 2));
650
+ const [error, setError] = useState<string | null>(null);
651
+
652
+ const commit = (next: string) => {
653
+ setText(next);
654
+ try {
655
+ const parsed = JSON.parse(next);
656
+ setError(null);
657
+ onChange(parsed);
658
+ } catch (e) {
659
+ setError(e instanceof Error ? e.message : String(e));
660
+ }
661
+ };
662
+
663
+ return (
664
+ <div className="sf-node sf-row" style={{ paddingLeft: depth * 14 }}>
665
+ <div className="sf-head sf-head-inline">
666
+ {label !== undefined && <span className="sf-label">{label}</span>}
667
+ <span className="sf-kind" style={{ color: colorOf(schema.kind) }}>
668
+ {schema.kind}
669
+ </span>
670
+ {schema.description && (
671
+ <span className="sf-desc">— {schema.description}</span>
672
+ )}
673
+ </div>
674
+ <div className="sf-input-cell">
675
+ <textarea
676
+ className="sf-textarea"
677
+ rows={3}
678
+ spellCheck={false}
679
+ value={text}
680
+ disabled={disabled}
681
+ onChange={(e) => commit(e.target.value)}
682
+ />
683
+ {error && <span className="error small">JSON 错误:{error}</span>}
684
+ </div>
685
+ </div>
686
+ );
687
+ }
688
+
689
+ // ─────────────────────────────────────────────────────────────
690
+ // 小工具
691
+ // ─────────────────────────────────────────────────────────────
692
+
693
+ function Constraints({ schema }: { schema: SchemaDescription }) {
694
+ const text = formatConstraints(schema.constraints);
695
+ return text ? <span className="sf-constraints">{`{ ${text} }`}</span> : null;
696
+ }