dsh-lh-data 0.1.0

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 (89) hide show
  1. package/README.md +332 -0
  2. package/cordis.patch.yml +7 -0
  3. package/lib/admin-contract.d.ts +274 -0
  4. package/lib/admin-http.d.ts +31 -0
  5. package/lib/admin-validate.d.ts +87 -0
  6. package/lib/admin.d.ts +60 -0
  7. package/lib/client.js +2969 -0
  8. package/lib/client.js.map +1 -0
  9. package/lib/datasource/columns.d.ts +13 -0
  10. package/lib/datasource/connection.d.ts +33 -0
  11. package/lib/datasource/connector/base.d.ts +21 -0
  12. package/lib/datasource/connector/mysql.d.ts +23 -0
  13. package/lib/datasource/connector/postgresql.d.ts +23 -0
  14. package/lib/datasource/crypto.d.ts +13 -0
  15. package/lib/datasource/driver.d.ts +39 -0
  16. package/lib/datasource/errors.d.ts +25 -0
  17. package/lib/datasource/importer.d.ts +38 -0
  18. package/lib/datasource/source-sql.d.ts +11 -0
  19. package/lib/datasource/source-store.d.ts +28 -0
  20. package/lib/datasource/types.d.ts +119 -0
  21. package/lib/db.d.ts +91 -0
  22. package/lib/http-common.d.ts +31 -0
  23. package/lib/http.d.ts +13 -0
  24. package/lib/index.d.ts +50 -0
  25. package/lib/index.js +5735 -0
  26. package/lib/parse.d.ts +41 -0
  27. package/lib/preview.d.ts +75 -0
  28. package/lib/render.d.ts +100 -0
  29. package/lib/scope-registry.d.ts +47 -0
  30. package/lib/scope.d.ts +56 -0
  31. package/lib/sql.d.ts +133 -0
  32. package/lib/store.d.ts +174 -0
  33. package/lib/table.d.ts +38 -0
  34. package/lib/tooling.d.ts +267 -0
  35. package/lib/tools/datasource.d.ts +16 -0
  36. package/lib/tools/import.d.ts +21 -0
  37. package/lib/tools/read.d.ts +77 -0
  38. package/lib/tools/registry.d.ts +12 -0
  39. package/lib/tools/write.d.ts +37 -0
  40. package/lib/view.d.ts +138 -0
  41. package/package.json +61 -0
  42. package/src/admin-contract.ts +351 -0
  43. package/src/admin-http.ts +254 -0
  44. package/src/admin-validate.ts +393 -0
  45. package/src/admin.ts +558 -0
  46. package/src/client/index.ts +403 -0
  47. package/src/client/settings/CreateForm.tsx +186 -0
  48. package/src/client/settings/DataSourceForm.tsx +278 -0
  49. package/src/client/settings/DataSourcesPanel.tsx +301 -0
  50. package/src/client/settings/DatasetEditor.tsx +245 -0
  51. package/src/client/settings/DatasetTable.tsx +110 -0
  52. package/src/client/settings/DatasetsPanel.tsx +207 -0
  53. package/src/client/settings/RowsPanel.tsx +196 -0
  54. package/src/client/settings/Section.tsx +41 -0
  55. package/src/client/settings/SourceTablesPanel.tsx +226 -0
  56. package/src/client/settings/api.ts +154 -0
  57. package/src/client/settings/styles.ts +125 -0
  58. package/src/datasource/columns.ts +56 -0
  59. package/src/datasource/connection.ts +124 -0
  60. package/src/datasource/connector/base.ts +54 -0
  61. package/src/datasource/connector/mysql.ts +187 -0
  62. package/src/datasource/connector/postgresql.ts +212 -0
  63. package/src/datasource/crypto.ts +61 -0
  64. package/src/datasource/driver.ts +108 -0
  65. package/src/datasource/errors.ts +69 -0
  66. package/src/datasource/importer.ts +262 -0
  67. package/src/datasource/index.ts +62 -0
  68. package/src/datasource/source-sql.ts +64 -0
  69. package/src/datasource/source-store.ts +152 -0
  70. package/src/datasource/types.ts +133 -0
  71. package/src/db.ts +277 -0
  72. package/src/http-common.ts +91 -0
  73. package/src/http.ts +130 -0
  74. package/src/index.ts +486 -0
  75. package/src/parse.ts +213 -0
  76. package/src/preview.ts +198 -0
  77. package/src/render.ts +294 -0
  78. package/src/scope-registry.ts +111 -0
  79. package/src/scope.ts +159 -0
  80. package/src/sql.ts +491 -0
  81. package/src/store.ts +412 -0
  82. package/src/table.ts +160 -0
  83. package/src/tooling.ts +551 -0
  84. package/src/tools/datasource.ts +378 -0
  85. package/src/tools/import.ts +282 -0
  86. package/src/tools/read.ts +536 -0
  87. package/src/tools/registry.ts +56 -0
  88. package/src/tools/write.ts +241 -0
  89. package/src/view.ts +371 -0
package/src/tooling.ts ADDED
@@ -0,0 +1,551 @@
1
+ /**
2
+ * 工具定义适配器 —— 设计文档 §2.2 的**方案 B**(零 dsh 运行时依赖)。
3
+ *
4
+ * 与 `dsh-lh-judge` 保持一致:运行时不 import 任何 dsh 框架包,手写
5
+ * `ToolDefinition` 字面量 + 自持参数校验。切换方案 A(`@deepseek-ai/dsh-tools`
6
+ * 的 `defineTool`)时只改本文件:其余模块只依赖这里导出的
7
+ * `ToolSpec` / `ValueSchemaSpec` / `ToolExec` 三个契约。
8
+ *
9
+ * 类型层刻意与 dsh-tools 的 `ParameterSchemaSpec` / `ValueSchemaSpec` 同构:
10
+ * - 参数根对象是隐式开放对象,必填用属性级 `required: true` 标注;
11
+ * - 显式 object 节点必须写 `additionalProperties: true | false`;
12
+ * - 编译产物是 dsh-tools `json-schema.ts` 的受支持子集(否则 `register()` 会抛
13
+ * `UNSUPPORTED_SCHEMA`)。
14
+ */
15
+
16
+ /** 插件标识:回注消息与后台任务的 `source.plugin` 字段。 */
17
+ export const PLUGIN_NAME = 'dsh-lh-data'
18
+
19
+ /**
20
+ * 视图分页排查用的临时后台日志(走 stdout),定位完成后删除本函数及其全部调用点。
21
+ */
22
+ export function debugLog(scope: string, payload: unknown): void {
23
+ console.log(`[${PLUGIN_NAME}:${scope}]`, JSON.stringify(payload))
24
+ }
25
+
26
+ // ── JSON 值与模型可见内容 ────────────────────────────────────────────────
27
+
28
+ export type JsonScalar = string | number | boolean | null
29
+ export type JsonValue = JsonScalar | JsonValue[] | { [key: string]: JsonValue }
30
+ export type JsonObject = { [key: string]: JsonValue }
31
+
32
+ /** 模型可见内容块(本插件只产出文本)。 */
33
+ export interface TextContentBlock {
34
+ type: 'text'
35
+ text: string
36
+ }
37
+
38
+ export type ContentBlock = TextContentBlock
39
+
40
+ // ── 作者侧 schema DSL ────────────────────────────────────────────────────
41
+
42
+ /** 所有节点共享的注解关键字。 */
43
+ export interface ValueSchemaAnnotations {
44
+ description?: string
45
+ title?: string
46
+ }
47
+
48
+ export interface StringValueSchemaSpec extends ValueSchemaAnnotations {
49
+ type: 'string'
50
+ enum?: readonly string[]
51
+ }
52
+
53
+ export interface NumberValueSchemaSpec extends ValueSchemaAnnotations {
54
+ type: 'number'
55
+ }
56
+
57
+ export interface IntegerValueSchemaSpec extends ValueSchemaAnnotations {
58
+ type: 'integer'
59
+ }
60
+
61
+ export interface BooleanValueSchemaSpec extends ValueSchemaAnnotations {
62
+ type: 'boolean'
63
+ }
64
+
65
+ export interface NullValueSchemaSpec extends ValueSchemaAnnotations {
66
+ type: 'null'
67
+ }
68
+
69
+ export interface ArrayValueSchemaSpec extends ValueSchemaAnnotations {
70
+ type: 'array'
71
+ items?: ValueSchemaSpec
72
+ }
73
+
74
+ /** 显式对象节点;开放性是必填字段,避免拿到 JSON Schema 的意外默认值。 */
75
+ export interface ObjectValueSchemaSpec extends ValueSchemaAnnotations {
76
+ type: 'object'
77
+ properties?: { [key: string]: ValueSchemaSpec }
78
+ additionalProperties: boolean
79
+ }
80
+
81
+ /** 作者侧「任意无损 JSON」节点;编译后是注解-only schema。 */
82
+ export interface JsonValueSchemaSpec extends ValueSchemaAnnotations {
83
+ type: 'json'
84
+ }
85
+
86
+ export interface OneOfValueSchemaSpec extends ValueSchemaAnnotations {
87
+ oneOf: readonly [ValueSchemaSpec, ValueSchemaSpec, ...ValueSchemaSpec[]]
88
+ }
89
+
90
+ export type ValueSchemaSpec =
91
+ | StringValueSchemaSpec
92
+ | NumberValueSchemaSpec
93
+ | IntegerValueSchemaSpec
94
+ | BooleanValueSchemaSpec
95
+ | NullValueSchemaSpec
96
+ | ArrayValueSchemaSpec
97
+ | ObjectValueSchemaSpec
98
+ | JsonValueSchemaSpec
99
+ | OneOfValueSchemaSpec
100
+
101
+ /** 隐式参数根对象的一个属性;`required: true` 表示必填。 */
102
+ export type ParameterPropertySpec = ValueSchemaSpec & { required?: true }
103
+
104
+ export type ParameterSchemaSpec = { [key: string]: ParameterPropertySpec }
105
+
106
+ // ── 类型推导 ────────────────────────────────────────────────────────────
107
+
108
+ /** 拍平交叉类型,便于 hover 阅读。 */
109
+ type Simplify<T> = { [K in keyof T]: T[K] } & {}
110
+
111
+ /** 标记为必填的属性名。 */
112
+ type RequiredKeys<S> = {
113
+ [K in keyof S]: S[K] extends { required: true } ? K : never
114
+ }[keyof S]
115
+
116
+ /** 显式对象节点的推导(含声明的开放性)。 */
117
+ type InferObject<S extends { additionalProperties: boolean }> =
118
+ S extends { properties: infer P }
119
+ ? S['additionalProperties'] extends true
120
+ ? { [K in keyof P]: InferValue<P[K]> } & JsonObject
121
+ : { [K in keyof P]: InferValue<P[K]> }
122
+ : S['additionalProperties'] extends true
123
+ ? JsonObject
124
+ : Record<string, never>
125
+
126
+ /** 一个节点接受的 TypeScript 值。 */
127
+ export type InferValue<S> =
128
+ S extends { type: 'string' }
129
+ ? S extends { enum: readonly (infer E)[] } ? E : string
130
+ : S extends { type: 'number' | 'integer' }
131
+ ? S extends { enum: readonly (infer E)[] } ? E : number
132
+ : S extends { type: 'boolean' } ? boolean
133
+ : S extends { type: 'null' } ? null
134
+ : S extends { type: 'array' }
135
+ ? S extends { items: infer I } ? InferValue<I>[] : JsonValue[]
136
+ : S extends { type: 'object'; additionalProperties: boolean } ? Simplify<InferObject<S>>
137
+ : S extends { type: 'json' } ? JsonValue
138
+ : S extends { oneOf: readonly (infer B)[] } ? InferValue<B>
139
+ : never
140
+
141
+ /** 一个隐式参数 map 推导出的 args 对象类型。 */
142
+ export type InferArgs<S extends ParameterSchemaSpec> = Simplify<
143
+ & { [K in RequiredKeys<S>]: InferValue<S[K]> }
144
+ & { [K in Exclude<keyof S, RequiredKeys<S>>]?: InferValue<S[K]> }
145
+ >
146
+
147
+ // ── 受支持 JSON Schema 子集 ─────────────────────────────────────────────
148
+
149
+ export type JsonSchemaScalar = string | number | boolean | null
150
+ export type JsonSchemaType = 'object' | 'array' | 'string' | 'number' | 'integer' | 'boolean' | 'null'
151
+
152
+ export interface JsonSchemaNode {
153
+ type?: JsonSchemaType
154
+ oneOf?: JsonSchemaNode[]
155
+ properties?: Record<string, JsonSchemaNode>
156
+ required?: string[]
157
+ additionalProperties?: boolean
158
+ items?: JsonSchemaNode
159
+ enum?: JsonSchemaScalar[]
160
+ description?: string
161
+ title?: string
162
+ }
163
+
164
+ const CONSTRAINT_KEYWORDS = ['type', 'oneOf', 'properties', 'required', 'additionalProperties', 'items', 'enum']
165
+ const ANNOTATION_KEYWORDS = ['description', 'title']
166
+ const SCHEMA_TYPES: readonly JsonSchemaType[] = ['object', 'array', 'string', 'number', 'integer', 'boolean', 'null']
167
+
168
+ /** 工具定义的作者错误(schema 写错):让插件加载即报错,而不是运行时才发现。 */
169
+ export class ToolSchemaError extends Error {
170
+ constructor(message: string) {
171
+ super(message)
172
+ this.name = 'ToolSchemaError'
173
+ }
174
+ }
175
+
176
+ /** 工具通用错误;`code` 便于门禁与日志归类。 */
177
+ export class ToolError extends Error {
178
+ readonly code: string
179
+
180
+ constructor(message: string, code = 'TOOL_ERROR') {
181
+ super(message)
182
+ this.name = 'ToolError'
183
+ this.code = code
184
+ }
185
+ }
186
+
187
+ /** 模型生成的参数不合法(对应 dsh-tools 的 `ToolArgsError` / `INVALID_ARGS`)。 */
188
+ export class ToolArgsError extends ToolError {
189
+ readonly violations: string[]
190
+
191
+ constructor(violations: string[]) {
192
+ super(`invalid arguments: ${violations.join('; ')}`, 'INVALID_ARGS')
193
+ this.name = 'ToolArgsError'
194
+ this.violations = violations
195
+ }
196
+ }
197
+
198
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
199
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return false
200
+ const proto: unknown = Object.getPrototypeOf(value)
201
+ return proto === null || proto === Object.prototype
202
+ }
203
+
204
+ function isJsonNumber(value: unknown): boolean {
205
+ return typeof value === 'number' && Number.isFinite(value) && !Object.is(value, -0)
206
+ }
207
+
208
+ /** 判断一个值是否为无损 JSON(用于 `json` 节点)。 */
209
+ export function isJsonValue(value: unknown, depth = 0): boolean {
210
+ if (value === null) return true
211
+ const kind = typeof value
212
+ if (kind === 'string' || kind === 'boolean') return true
213
+ if (kind === 'number') return isJsonNumber(value)
214
+ if (kind !== 'object' || depth > 32) return false
215
+ if (Array.isArray(value)) return value.every(entry => isJsonValue(entry, depth + 1))
216
+ if (!isPlainRecord(value)) return false
217
+ return Object.values(value).every(entry => isJsonValue(entry, depth + 1))
218
+ }
219
+
220
+ // ── 编译:作者 DSL → 受支持子集 ──────────────────────────────────────────
221
+
222
+ function authorError(message: string): never {
223
+ throw new ToolSchemaError(message)
224
+ }
225
+
226
+ function copyAnnotations(spec: Record<string, unknown>, node: JsonSchemaNode): void {
227
+ if (typeof spec.description === 'string') node.description = spec.description
228
+ if (typeof spec.title === 'string') node.title = spec.title
229
+ }
230
+
231
+ function assertAuthorKeys(spec: Record<string, unknown>, path: string, allowed: readonly string[]): void {
232
+ for (const key of Object.keys(spec)) {
233
+ if (!allowed.includes(key)) authorError(`${path}.${key} 不是受支持的关键字(${allowed.join('/')})`)
234
+ }
235
+ }
236
+
237
+ function compileNode(spec: unknown, path: string, allowRequired: boolean): JsonSchemaNode {
238
+ if (!isPlainRecord(spec)) authorError(`${path} must be a value schema object`)
239
+ const allowed = [...ANNOTATION_KEYWORDS, ...CONSTRAINT_KEYWORDS, ...(allowRequired ? ['required'] : []), 'const']
240
+ assertAuthorKeys(spec, path, allowed)
241
+
242
+ const node: JsonSchemaNode = {}
243
+ copyAnnotations(spec, node)
244
+
245
+ if (Object.hasOwn(spec, 'oneOf')) {
246
+ assertAuthorKeys(spec, path, [...ANNOTATION_KEYWORDS, 'oneOf', ...(allowRequired ? ['required'] : [])])
247
+ const branches = spec.oneOf
248
+ if (!Array.isArray(branches) || branches.length < 2) authorError(`${path}.oneOf 至少需要两个分支`)
249
+ node.oneOf = branches.map((branch, index) => compileNode(branch, `${path}.oneOf[${index}]`, false))
250
+ return node
251
+ }
252
+
253
+ const type = spec.type
254
+ switch (type) {
255
+ case 'json':
256
+ assertAuthorKeys(spec, path, [...ANNOTATION_KEYWORDS, 'type', ...(allowRequired ? ['required'] : [])])
257
+ break
258
+ case 'object': {
259
+ assertAuthorKeys(spec, path, [...ANNOTATION_KEYWORDS, 'type', 'properties', 'additionalProperties', ...(allowRequired ? ['required'] : [])])
260
+ if (typeof spec.additionalProperties !== 'boolean') authorError(`${path}.additionalProperties 必须显式声明 true 或 false`)
261
+ node.type = 'object'
262
+ node.additionalProperties = spec.additionalProperties
263
+ if (Object.hasOwn(spec, 'properties')) {
264
+ if (!isPlainRecord(spec.properties)) authorError(`${path}.properties 必须是 schema 对象`)
265
+ const properties: Record<string, JsonSchemaNode> = {}
266
+ for (const [key, child] of Object.entries(spec.properties)) {
267
+ properties[key] = compileNode(child, `${path}.properties.${key}`, false)
268
+ }
269
+ node.properties = properties
270
+ }
271
+ break
272
+ }
273
+ case 'array':
274
+ assertAuthorKeys(spec, path, [...ANNOTATION_KEYWORDS, 'type', 'items', ...(allowRequired ? ['required'] : [])])
275
+ node.type = 'array'
276
+ if (Object.hasOwn(spec, 'items')) node.items = compileNode(spec.items, `${path}.items`, false)
277
+ break
278
+ case 'string':
279
+ case 'number':
280
+ case 'integer':
281
+ case 'boolean':
282
+ case 'null': {
283
+ assertAuthorKeys(spec, path, [...ANNOTATION_KEYWORDS, 'type', 'enum', 'const', ...(allowRequired ? ['required'] : [])])
284
+ node.type = type
285
+ if (Object.hasOwn(spec, 'enum')) {
286
+ const allowed = spec.enum
287
+ if (!Array.isArray(allowed) || allowed.length === 0 || !allowed.every(entry => scalarMatches(type, entry))) {
288
+ authorError(`${path}.enum 必须是非空的 ${type} 数组`)
289
+ }
290
+ node.enum = allowed as JsonSchemaScalar[]
291
+ }
292
+ break
293
+ }
294
+ default:
295
+ authorError(`${path}.type 必须是 string/number/integer/boolean/null/array/object/json,或使用 oneOf`)
296
+ }
297
+ return node
298
+ }
299
+
300
+ function scalarMatches(type: Exclude<JsonSchemaType, 'object' | 'array'>, value: unknown): boolean {
301
+ switch (type) {
302
+ case 'string': return typeof value === 'string'
303
+ case 'number': return isJsonNumber(value)
304
+ case 'integer': return isJsonNumber(value) && Number.isInteger(value)
305
+ case 'boolean': return typeof value === 'boolean'
306
+ case 'null': return value === null
307
+ default: return false
308
+ }
309
+ }
310
+
311
+ /** 编译参数 map(隐式开放对象根)。 */
312
+ export function compileParameters(spec: ParameterSchemaSpec): JsonSchemaNode {
313
+ if (!isPlainRecord(spec)) authorError('parameters 必须是属性 schema 的对象')
314
+ const properties: Record<string, JsonSchemaNode> = {}
315
+ const required: string[] = []
316
+ for (const [key, child] of Object.entries(spec)) {
317
+ if (!isPlainRecord(child)) authorError(`parameters.${key} 必须是 value schema 对象`)
318
+ if (Object.hasOwn(child, 'required')) {
319
+ if (child.required !== true) authorError(`parameters.${key}.required 只能是 true`)
320
+ required.push(key)
321
+ }
322
+ properties[key] = compileNode(child, `parameters.${key}`, true)
323
+ }
324
+ return {
325
+ type: 'object',
326
+ properties,
327
+ ...(required.length > 0 ? { required } : {}),
328
+ }
329
+ }
330
+
331
+ /** 编译输出 schema(任意 JSON 根)。 */
332
+ export function compileOutputSchema(spec: ValueSchemaSpec): JsonSchemaNode {
333
+ return compileNode(spec, 'output.schema', false)
334
+ }
335
+
336
+ // ── 校验:受支持子集 → 违规列表 ──────────────────────────────────────────
337
+
338
+ function diagnosticPath(path: string): string {
339
+ return path === '' ? 'arguments' : path
340
+ }
341
+
342
+ function propertyPath(path: string, key: string): string {
343
+ return path === '' ? key : `${path}.${key}`
344
+ }
345
+
346
+ /** 校验一个值是否匹配已编译节点;返回空数组表示通过。 */
347
+ export function validateValue(node: JsonSchemaNode, value: unknown, path = ''): string[] {
348
+ if (node.oneOf !== undefined) {
349
+ const matched = node.oneOf.filter(branch => validateValue(branch, value, path).length === 0).length
350
+ return matched === 1 ? [] : [`"${diagnosticPath(path)}" 必须恰好匹配一个 oneOf 分支(命中 ${matched} 个)`]
351
+ }
352
+ const type = node.type
353
+ if (type === undefined) {
354
+ return isJsonValue(value) ? [] : [`"${diagnosticPath(path)}" 必须是无损 JSON 值`]
355
+ }
356
+ switch (type) {
357
+ case 'object': {
358
+ if (!isPlainRecord(value)) return [`"${diagnosticPath(path)}" 必须是对象`]
359
+ const properties = node.properties ?? {}
360
+ const violations: string[] = []
361
+ for (const key of node.required ?? []) {
362
+ if (!Object.hasOwn(value, key) || value[key] === undefined) {
363
+ violations.push(`missing required property "${propertyPath(path, key)}"`)
364
+ }
365
+ }
366
+ for (const [key, child] of Object.entries(properties)) {
367
+ if (!Object.hasOwn(value, key) || value[key] === undefined) continue
368
+ violations.push(...validateValue(child, value[key], propertyPath(path, key)))
369
+ }
370
+ if (node.additionalProperties === false) {
371
+ for (const key of Object.keys(value)) {
372
+ if (!Object.hasOwn(properties, key)) {
373
+ violations.push(`"${propertyPath(path, key)}" 不是已声明的属性(additionalProperties: false)`)
374
+ }
375
+ }
376
+ }
377
+ return violations
378
+ }
379
+ case 'array': {
380
+ if (!Array.isArray(value)) return [`"${diagnosticPath(path)}" 必须是数组`]
381
+ if (node.items === undefined) return []
382
+ const violations: string[] = []
383
+ value.forEach((entry, index) => {
384
+ violations.push(...validateValue(node.items as JsonSchemaNode, entry, `${path}[${index}]`))
385
+ })
386
+ return violations
387
+ }
388
+ case 'string':
389
+ if (typeof value !== 'string') return [`"${diagnosticPath(path)}" 必须是字符串`]
390
+ break
391
+ case 'number':
392
+ if (!isJsonNumber(value)) return [`"${diagnosticPath(path)}" 必须是有限数字`]
393
+ break
394
+ case 'integer':
395
+ if (!isJsonNumber(value) || !Number.isInteger(value)) return [`"${diagnosticPath(path)}" 必须是整数`]
396
+ break
397
+ case 'boolean':
398
+ if (typeof value !== 'boolean') return [`"${diagnosticPath(path)}" 必须是布尔值`]
399
+ break
400
+ case 'null':
401
+ if (value !== null) return [`"${diagnosticPath(path)}" 必须是 null`]
402
+ break
403
+ default:
404
+ return [`"${path}" 使用了未知类型`]
405
+ }
406
+ if (node.enum !== undefined && !node.enum.includes(value as JsonSchemaScalar)) {
407
+ return [`"${diagnosticPath(path)}" 必须是 ${JSON.stringify(node.enum)} 之一`]
408
+ }
409
+ return []
410
+ }
411
+
412
+ // ── 运行时契约(duck-typed) ────────────────────────────────────────────
413
+
414
+ /** 调用方 agent 的最小视图(duck-typed,不 import dsh 运行时)。 */
415
+ export interface AgentLike {
416
+ /**
417
+ * 真实 dsh:`Agent.session` 是 `Session`,工作目录挂在它的 `header.cwd`
418
+ * (`SessionHeader`);扁平的 `cwd` / `id` 只是 headless 与自测的兼容位。
419
+ */
420
+ session?: { header?: { cwd?: string; id?: string }; cwd?: string; id?: string }
421
+ inject?(message: unknown): void
422
+ }
423
+
424
+ /** `defineTool` 交给 execute 的上下文(`ToolRunContext` 的最小视图)。 */
425
+ export interface ToolExec {
426
+ readonly name: string
427
+ readonly arguments: unknown
428
+ readonly agent?: AgentLike
429
+ readonly signal: AbortSignal
430
+ deferContext?(message: unknown): void
431
+ }
432
+
433
+ /** UI 渲染意图:本插件只用 generic 卡片。 */
434
+ export interface GenericCallView {
435
+ card: 'generic'
436
+ title: string
437
+ kind?: 'read' | 'write' | 'search' | 'edit' | 'run'
438
+ content?: string
439
+ rawInput?: string
440
+ locations?: { path: string; line?: number }[]
441
+ }
442
+
443
+ export type ToolCallView = GenericCallView
444
+
445
+ /** 注册到 `ctx.tools` 的原始工具定义。 */
446
+ export interface ToolDefinition {
447
+ name: string
448
+ description: string
449
+ parameters: Record<string, unknown>
450
+ output: {
451
+ schema: Record<string, unknown>
452
+ render(args: unknown, value: unknown): ContentBlock[]
453
+ /** 可选:只给前端的展示元数据(模型不可见)。 */
454
+ presentationMeta?(args: unknown, value: unknown): JsonValue
455
+ }
456
+ execute(args: unknown, exec: ToolExec): Promise<unknown>
457
+ presentCall?(args: unknown): ToolCallView | undefined
458
+ }
459
+
460
+ /** `ctx.tools` 服务的最小视图。 */
461
+ export interface ToolRegistry {
462
+ register(definition: ToolDefinition): () => void
463
+ guard?(guard: (exec: ToolExec) => string | undefined): () => void
464
+ }
465
+
466
+ /** `tools/pre-execute` 的决策(与 dsh-tools 的 `PreToolDecision` 同构)。 */
467
+ export type PreToolDecision =
468
+ | { kind: 'allow' }
469
+ | { kind: 'deny'; reason: string }
470
+ | { kind: 'ask'; reason?: string }
471
+
472
+ export const ALLOW_DECISION: PreToolDecision = { kind: 'allow' }
473
+
474
+ // ── 工具定义构造器 ──────────────────────────────────────────────────────
475
+
476
+ export interface ToolOutputSpec<P extends ParameterSchemaSpec, V> {
477
+ /** 规范输出 schema;execute 的返回值会被强制校验。 */
478
+ schema: ValueSchemaSpec
479
+ /**
480
+ * 把规范值渲染成模型可见内容(纯函数,不做 I/O)。
481
+ * 调用处请显式标注 value 的类型(与 execute 的返回类型一致),
482
+ * 以保证 render 内部拿到精确类型。
483
+ */
484
+ render(args: InferArgs<P>, value: V): ContentBlock[]
485
+ /**
486
+ * 只给 UI 的展示元数据(模型不可见)。平台只对顶层调用计算一次,
487
+ * 因此它必须是 (args, value) 的**纯函数**:任何需要交给前端的东西都要随规范值携带。
488
+ */
489
+ presentationMeta?(args: InferArgs<P>, value: V): JsonValue
490
+ }
491
+
492
+ export interface ToolSpec<P extends ParameterSchemaSpec, V> {
493
+ name: string
494
+ /** 模型唯一可见的说明。 */
495
+ description: string
496
+ parameters: P
497
+ output: ToolOutputSpec<P, V>
498
+ /** pending 卡片;参数不合法时由适配器返回 undefined(渲染永不抛错)。 */
499
+ presentCall?(args: InferArgs<P>): ToolCallView | undefined
500
+ execute(args: InferArgs<P>, exec: ToolExec): Promise<V>
501
+ }
502
+
503
+ /**
504
+ * 把作者侧定义编译成注册表可接受的 `ToolDefinition`:
505
+ * - 编译期校验作者 schema(违反受支持子集直接抛错,fail-loud);
506
+ * - `execute` 前校验模型入参,违规抛 `ToolArgsError`(→ `INVALID_ARGS`);
507
+ * - `presentCall` 软校验,任何异常回落到通用卡片。
508
+ */
509
+ export function toolDef<const P extends ParameterSchemaSpec, V>(spec: ToolSpec<P, V>): ToolDefinition {
510
+ const parameters = compileParameters(spec.parameters)
511
+ const outputSchema = compileOutputSchema(spec.output.schema)
512
+ const validate = (args: unknown): string[] => validateValue(parameters, args, '')
513
+ const userRender = spec.output.render
514
+ const userMeta = spec.output.presentationMeta
515
+ const userExecute = spec.execute
516
+ const userPresentCall = spec.presentCall
517
+
518
+ const definition: ToolDefinition = {
519
+ name: spec.name,
520
+ description: spec.description,
521
+ parameters: parameters as unknown as Record<string, unknown>,
522
+ output: {
523
+ schema: outputSchema as unknown as Record<string, unknown>,
524
+ render(args: unknown, value: unknown): ContentBlock[] {
525
+ return userRender(args as InferArgs<P>, value as V)
526
+ },
527
+ ...userMeta === undefined ? {} : {
528
+ presentationMeta(args: unknown, value: unknown): JsonValue {
529
+ return userMeta(args as InferArgs<P>, value as V)
530
+ },
531
+ },
532
+ },
533
+ async execute(args: unknown, exec: ToolExec): Promise<unknown> {
534
+ const violations = validate(args)
535
+ if (violations.length > 0) throw new ToolArgsError(violations)
536
+ return userExecute(args as InferArgs<P>, exec)
537
+ },
538
+ }
539
+
540
+ if (userPresentCall) {
541
+ definition.presentCall = (args: unknown): ToolCallView | undefined => {
542
+ if (validate(args).length > 0) return undefined
543
+ try {
544
+ return userPresentCall(args as InferArgs<P>)
545
+ } catch {
546
+ return undefined
547
+ }
548
+ }
549
+ }
550
+ return definition
551
+ }