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.
- package/README.md +332 -0
- package/cordis.patch.yml +7 -0
- package/lib/admin-contract.d.ts +274 -0
- package/lib/admin-http.d.ts +31 -0
- package/lib/admin-validate.d.ts +87 -0
- package/lib/admin.d.ts +60 -0
- package/lib/client.js +2969 -0
- package/lib/client.js.map +1 -0
- package/lib/datasource/columns.d.ts +13 -0
- package/lib/datasource/connection.d.ts +33 -0
- package/lib/datasource/connector/base.d.ts +21 -0
- package/lib/datasource/connector/mysql.d.ts +23 -0
- package/lib/datasource/connector/postgresql.d.ts +23 -0
- package/lib/datasource/crypto.d.ts +13 -0
- package/lib/datasource/driver.d.ts +39 -0
- package/lib/datasource/errors.d.ts +25 -0
- package/lib/datasource/importer.d.ts +38 -0
- package/lib/datasource/source-sql.d.ts +11 -0
- package/lib/datasource/source-store.d.ts +28 -0
- package/lib/datasource/types.d.ts +119 -0
- package/lib/db.d.ts +91 -0
- package/lib/http-common.d.ts +31 -0
- package/lib/http.d.ts +13 -0
- package/lib/index.d.ts +50 -0
- package/lib/index.js +5735 -0
- package/lib/parse.d.ts +41 -0
- package/lib/preview.d.ts +75 -0
- package/lib/render.d.ts +100 -0
- package/lib/scope-registry.d.ts +47 -0
- package/lib/scope.d.ts +56 -0
- package/lib/sql.d.ts +133 -0
- package/lib/store.d.ts +174 -0
- package/lib/table.d.ts +38 -0
- package/lib/tooling.d.ts +267 -0
- package/lib/tools/datasource.d.ts +16 -0
- package/lib/tools/import.d.ts +21 -0
- package/lib/tools/read.d.ts +77 -0
- package/lib/tools/registry.d.ts +12 -0
- package/lib/tools/write.d.ts +37 -0
- package/lib/view.d.ts +138 -0
- package/package.json +61 -0
- package/src/admin-contract.ts +351 -0
- package/src/admin-http.ts +254 -0
- package/src/admin-validate.ts +393 -0
- package/src/admin.ts +558 -0
- package/src/client/index.ts +403 -0
- package/src/client/settings/CreateForm.tsx +186 -0
- package/src/client/settings/DataSourceForm.tsx +278 -0
- package/src/client/settings/DataSourcesPanel.tsx +301 -0
- package/src/client/settings/DatasetEditor.tsx +245 -0
- package/src/client/settings/DatasetTable.tsx +110 -0
- package/src/client/settings/DatasetsPanel.tsx +207 -0
- package/src/client/settings/RowsPanel.tsx +196 -0
- package/src/client/settings/Section.tsx +41 -0
- package/src/client/settings/SourceTablesPanel.tsx +226 -0
- package/src/client/settings/api.ts +154 -0
- package/src/client/settings/styles.ts +125 -0
- package/src/datasource/columns.ts +56 -0
- package/src/datasource/connection.ts +124 -0
- package/src/datasource/connector/base.ts +54 -0
- package/src/datasource/connector/mysql.ts +187 -0
- package/src/datasource/connector/postgresql.ts +212 -0
- package/src/datasource/crypto.ts +61 -0
- package/src/datasource/driver.ts +108 -0
- package/src/datasource/errors.ts +69 -0
- package/src/datasource/importer.ts +262 -0
- package/src/datasource/index.ts +62 -0
- package/src/datasource/source-sql.ts +64 -0
- package/src/datasource/source-store.ts +152 -0
- package/src/datasource/types.ts +133 -0
- package/src/db.ts +277 -0
- package/src/http-common.ts +91 -0
- package/src/http.ts +130 -0
- package/src/index.ts +486 -0
- package/src/parse.ts +213 -0
- package/src/preview.ts +198 -0
- package/src/render.ts +294 -0
- package/src/scope-registry.ts +111 -0
- package/src/scope.ts +159 -0
- package/src/sql.ts +491 -0
- package/src/store.ts +412 -0
- package/src/table.ts +160 -0
- package/src/tooling.ts +551 -0
- package/src/tools/datasource.ts +378 -0
- package/src/tools/import.ts +282 -0
- package/src/tools/read.ts +536 -0
- package/src/tools/registry.ts +56 -0
- package/src/tools/write.ts +241 -0
- 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
|
+
}
|