@capyclaw/tool-host 0.1.0 → 0.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.
- package/dsh-suite.json +6 -4
- package/package.json +15 -2
- package/src/index.mjs +341 -341
- package/src/optimizers/E2E/346/265/213/350/257/225/346/212/245/345/221/212.md +0 -396
- package/src/optimizers/PPT/345/234/272/346/231/257/344/274/230/345/214/226/346/226/271/346/241/210.md +0 -607
- package/src/optimizers/PPT/345/256/236/346/227/266/346/270/262/346/237/223/346/226/271/346/241/210.md +0 -456
- package/src/optimizers/PPT/346/265/201/345/274/217/346/270/262/346/237/223-/347/224/237/344/272/247/347/272/247/344/274/230/345/214/226.md +0 -874
- package/src/optimizers/README.md +0 -374
- package/src/optimizers/batch-optimizer.mjs +0 -94
- package/src/optimizers/cache-optimizer.mjs +0 -217
- package/src/optimizers/concurrency-optimizer.mjs +0 -129
- package/src/optimizers/e2e-test.mjs +0 -674
- package/src/optimizers/examples.mjs +0 -243
- package/src/optimizers/index.mjs +0 -188
- package/src/optimizers/ppt-optimizer.mjs +0 -263
- package/src/optimizers/ppt-streaming-demo.mjs +0 -192
- package/src/optimizers/ppt-streaming-renderer.mjs +0 -332
- package/src/optimizers/production-streaming-demo.mjs +0 -267
- package/src/optimizers/production-streaming-renderer.mjs +0 -611
- package/src/optimizers/real-ppt-test.mjs +0 -397
- package/src/optimizers/real-world-benchmark.mjs +0 -470
- package/src/optimizers/retry-optimizer.mjs +0 -118
- package/src/optimizers//347/224/237/344/272/247/347/272/247/344/274/230/345/214/226/346/200/273/347/273/223.md +0 -110
- package/src/optimizers//347/234/237/345/256/236/346/200/247/350/203/275/346/217/220/345/215/207/351/252/214/350/257/201/346/212/245/345/221/212.md +0 -331
package/src/index.mjs
CHANGED
|
@@ -1,341 +1,341 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-tool-host —— 工具面标准件。
|
|
3
|
-
*
|
|
4
|
-
* 把“能力声明 → 注册表 → 工具 schema/实现 → 契约报告 → MCP 协议壳”收成一套:
|
|
5
|
-
* defineTool / defineCapability / createRegistry / createMcpHandler /
|
|
6
|
-
* createCapabilityReport / buildInstructions
|
|
7
|
-
*
|
|
8
|
-
* 功能插件只写业务能力,不再复制 kernel/registry/dispatch/validate/contract。
|
|
9
|
-
*/
|
|
10
|
-
import { createMcpHandler as createKitMcpHandler } from '@capyclaw/plugin-kit/service/mcp-http'
|
|
11
|
-
import { validateToolArguments } from '@capyclaw/plugin-kit/tools/validate'
|
|
12
|
-
import { createServiceApi } from '@capyclaw/plugin-kit/service/api'
|
|
13
|
-
|
|
14
|
-
const KINDS = new Set(['read', 'write', 'control'])
|
|
15
|
-
|
|
16
|
-
export function defineTool({
|
|
17
|
-
name,
|
|
18
|
-
description = '',
|
|
19
|
-
inputSchema = { type: 'object', properties: {}, additionalProperties: false },
|
|
20
|
-
kind = 'write',
|
|
21
|
-
since = '1.0.0',
|
|
22
|
-
handler,
|
|
23
|
-
} = {}) {
|
|
24
|
-
if (!name || typeof name !== 'string') throw new Error('defineTool 需要 name')
|
|
25
|
-
if (typeof handler !== 'function') throw new Error(`工具 ${name} 需要 handler`)
|
|
26
|
-
if (!KINDS.has(kind)) throw new Error(`工具 ${name} 的 kind 非法: ${kind}`)
|
|
27
|
-
return Object.freeze({ name, description, inputSchema, kind, since, handler })
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function defineCapability({
|
|
31
|
-
id,
|
|
32
|
-
title,
|
|
33
|
-
description = '',
|
|
34
|
-
tags = [],
|
|
35
|
-
requires = [],
|
|
36
|
-
dependsOn = [],
|
|
37
|
-
provides = {},
|
|
38
|
-
features = [],
|
|
39
|
-
tools = [],
|
|
40
|
-
} = {}) {
|
|
41
|
-
if (!id || typeof id !== 'string') throw new Error('defineCapability 需要 id')
|
|
42
|
-
if (!Array.isArray(tools) || !tools.length) throw new Error(`能力 ${id} 至少需要一个工具`)
|
|
43
|
-
return Object.freeze({
|
|
44
|
-
id,
|
|
45
|
-
title: title || id,
|
|
46
|
-
description,
|
|
47
|
-
tags: Object.freeze([...tags]),
|
|
48
|
-
requires: Object.freeze([...requires]),
|
|
49
|
-
dependsOn: Object.freeze([...dependsOn]),
|
|
50
|
-
provides: Object.freeze({ ...provides }),
|
|
51
|
-
features: Object.freeze([...features]),
|
|
52
|
-
tools: Object.freeze([...tools]),
|
|
53
|
-
})
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function validateCapabilities(capabilities) {
|
|
57
|
-
const ids = new Set()
|
|
58
|
-
const toolNames = new Set()
|
|
59
|
-
for (const cap of capabilities) {
|
|
60
|
-
if (ids.has(cap.id)) throw new Error(`重复的 capability id: ${cap.id}`)
|
|
61
|
-
ids.add(cap.id)
|
|
62
|
-
for (const tool of cap.tools) {
|
|
63
|
-
if (toolNames.has(tool.name)) throw new Error(`重复的工具名: ${tool.name}`)
|
|
64
|
-
toolNames.add(tool.name)
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
for (const cap of capabilities) {
|
|
68
|
-
for (const dep of cap.dependsOn) {
|
|
69
|
-
if (!ids.has(dep)) throw new Error(`能力 ${cap.id} 依赖不存在的能力: ${dep}`)
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function selectCapabilities(capabilities, enabled) {
|
|
75
|
-
if (!enabled || !enabled.length) return capabilities
|
|
76
|
-
const byId = new Map(capabilities.map((cap) => [cap.id, cap]))
|
|
77
|
-
for (const id of enabled) {
|
|
78
|
-
if (!byId.has(id)) throw new Error(`enabled 里有未知能力: ${id}`)
|
|
79
|
-
}
|
|
80
|
-
const ids = new Set(enabled)
|
|
81
|
-
let changed = true
|
|
82
|
-
while (changed) {
|
|
83
|
-
changed = false
|
|
84
|
-
for (const cap of capabilities) {
|
|
85
|
-
if (!ids.has(cap.id)) continue
|
|
86
|
-
for (const dep of cap.dependsOn) {
|
|
87
|
-
if (!ids.has(dep)) { ids.add(dep); changed = true }
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return capabilities.filter((cap) => ids.has(cap.id))
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* @param {Array} capabilities
|
|
96
|
-
* @param {{ enabled?: string[], ports?: object, report?: Function, reserved?: string[] }} options
|
|
97
|
-
*/
|
|
98
|
-
export function createRegistry(capabilities, { enabled = null, ports = null, report = null, reserved = [] } = {}) {
|
|
99
|
-
validateCapabilities(capabilities)
|
|
100
|
-
const selected = selectCapabilities(capabilities, enabled)
|
|
101
|
-
|
|
102
|
-
const kernelPorts = { ...(ports || {}) }
|
|
103
|
-
const reservedNames = new Set([...(reserved || []), 'contract'])
|
|
104
|
-
const providers = new Map()
|
|
105
|
-
for (const cap of selected) {
|
|
106
|
-
for (const [ns, api] of Object.entries(cap.provides || {})) {
|
|
107
|
-
if (reservedNames.has(ns)) throw new Error(`能力 ${cap.id} 不能覆盖内核端口: ${ns}`)
|
|
108
|
-
if (providers.has(ns)) throw new Error(`服务命名空间重复: ${ns}(${providers.get(ns).cap.id} / ${cap.id})`)
|
|
109
|
-
providers.set(ns, { cap, api })
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const available = new Set([...Object.keys(kernelPorts), ...reservedNames, ...providers.keys()])
|
|
114
|
-
for (const cap of selected) {
|
|
115
|
-
for (const req of cap.requires) {
|
|
116
|
-
if (!available.has(req)) throw new Error(`能力 ${cap.id} 需要未提供的端口/服务: ${req}`)
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const ctx = { ...kernelPorts }
|
|
121
|
-
for (const [ns, { api }] of providers) {
|
|
122
|
-
ctx[ns] = {}
|
|
123
|
-
for (const [method, fn] of Object.entries(api)) {
|
|
124
|
-
if (typeof fn !== 'function') throw new Error(`服务 ${ns}.${method} 必须是函数`)
|
|
125
|
-
ctx[ns][method] = (...args) => fn(ctx, ...args)
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const TOOLS = []
|
|
130
|
-
const TOOL_IMPL = {}
|
|
131
|
-
const TOOL_META = {}
|
|
132
|
-
const FEATURES = []
|
|
133
|
-
const CAPABILITIES = []
|
|
134
|
-
|
|
135
|
-
for (const cap of selected) {
|
|
136
|
-
for (const feature of cap.features) {
|
|
137
|
-
if (!FEATURES.includes(feature)) FEATURES.push(feature)
|
|
138
|
-
}
|
|
139
|
-
CAPABILITIES.push({
|
|
140
|
-
id: cap.id,
|
|
141
|
-
title: cap.title,
|
|
142
|
-
description: cap.description,
|
|
143
|
-
tags: [...cap.tags],
|
|
144
|
-
tools: cap.tools.map((tool) => tool.name),
|
|
145
|
-
})
|
|
146
|
-
for (const tool of cap.tools) {
|
|
147
|
-
TOOLS.push({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema })
|
|
148
|
-
TOOL_META[tool.name] = { kind: tool.kind, since: tool.since, capability: cap.id }
|
|
149
|
-
TOOL_IMPL[tool.name] = (args) => tool.handler(args || {}, ctx)
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const reportFn = report || createCapabilityReport
|
|
154
|
-
ctx.contract = {
|
|
155
|
-
report: (editorUrl = '', runtime = {}) => reportFn({
|
|
156
|
-
tools: TOOLS,
|
|
157
|
-
toolMeta: TOOL_META,
|
|
158
|
-
features: FEATURES,
|
|
159
|
-
capabilities: CAPABILITIES,
|
|
160
|
-
editorUrl,
|
|
161
|
-
runtime,
|
|
162
|
-
}),
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return { TOOLS, TOOL_IMPL, TOOL_META, FEATURES, CAPABILITIES, ctx }
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/** 与 MCP 工具面解耦:调用前统一校验。 */
|
|
169
|
-
export function createToolDispatcher({ tools, callTool }) {
|
|
170
|
-
const toolMap = new Map((tools || []).map((tool) => [tool.name, tool]))
|
|
171
|
-
return {
|
|
172
|
-
listTools() {
|
|
173
|
-
return { tools }
|
|
174
|
-
},
|
|
175
|
-
async callTool(name, args = {}) {
|
|
176
|
-
const tool = toolMap.get(name)
|
|
177
|
-
if (!tool) {
|
|
178
|
-
return { content: [{ type: 'text', text: `未知工具: ${String(name)}` }], isError: true }
|
|
179
|
-
}
|
|
180
|
-
const check = validateToolArguments(tool, args)
|
|
181
|
-
if (!check.ok) {
|
|
182
|
-
return { content: [{ type: 'text', text: '参数不合法:' + check.errors.join(';') }], isError: true }
|
|
183
|
-
}
|
|
184
|
-
const out = await callTool(name, args)
|
|
185
|
-
if (out && out.content) return out
|
|
186
|
-
return { content: [{ type: 'text', text: out?.text || 'ok' }] }
|
|
187
|
-
},
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* MCP Streamable HTTP 处理器。registry 可直接传 createRegistry 的返回值。
|
|
193
|
-
* 兼容两种调用:
|
|
194
|
-
* { registry } 或 { tools, toolImpl }
|
|
195
|
-
* { tools, callTool }(旧 relay 形态)
|
|
196
|
-
* @param {{ registry?: object, tools?: Array, toolImpl?: object, callTool?: Function, serverInfo?: object, instructions?: string }} options
|
|
197
|
-
*/
|
|
198
|
-
export function createMcpHandler({ registry, tools, toolImpl, callTool, serverInfo, instructions } = {}) {
|
|
199
|
-
const list = tools || registry?.TOOLS || []
|
|
200
|
-
const impl = toolImpl || registry?.TOOL_IMPL || null
|
|
201
|
-
if (!Array.isArray(list) || !list.length) throw new Error('createMcpHandler: 没有工具可暴露')
|
|
202
|
-
if (!impl && typeof callTool !== 'function') throw new Error('createMcpHandler: 需要 toolImpl 或 callTool')
|
|
203
|
-
return createKitMcpHandler({
|
|
204
|
-
tools: list,
|
|
205
|
-
callTool: (name, args, context) => {
|
|
206
|
-
if (typeof callTool === 'function' && !impl) return callTool(name, args, context)
|
|
207
|
-
const fn = impl[name]
|
|
208
|
-
if (!fn) throw new Error(`未知工具: ${name}`)
|
|
209
|
-
return fn(args, context)
|
|
210
|
-
},
|
|
211
|
-
serverInfo: serverInfo || { name: 'dsh-tool-host', version: '0.1.0' },
|
|
212
|
-
instructions,
|
|
213
|
-
})
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
export function createCapabilityReport({
|
|
217
|
-
tools = [],
|
|
218
|
-
toolMeta = {},
|
|
219
|
-
features = [],
|
|
220
|
-
capabilities = [],
|
|
221
|
-
contractVersion = '1.0.0',
|
|
222
|
-
packageVersion = '0.0.0',
|
|
223
|
-
editorUrl = '',
|
|
224
|
-
runtime = {},
|
|
225
|
-
} = {}) {
|
|
226
|
-
return {
|
|
227
|
-
contractVersion,
|
|
228
|
-
packageVersion,
|
|
229
|
-
editorUrl,
|
|
230
|
-
...runtime,
|
|
231
|
-
toolCount: tools.length,
|
|
232
|
-
tools: tools.map((tool) => ({
|
|
233
|
-
name: tool.name,
|
|
234
|
-
kind: toolMeta[tool.name]?.kind ?? 'write',
|
|
235
|
-
since: toolMeta[tool.name]?.since ?? '1.0.0',
|
|
236
|
-
capability: toolMeta[tool.name]?.capability ?? '',
|
|
237
|
-
})),
|
|
238
|
-
features,
|
|
239
|
-
capabilities: capabilities.map((cap) => ({
|
|
240
|
-
id: cap.id,
|
|
241
|
-
title: cap.title,
|
|
242
|
-
description: cap.description || '',
|
|
243
|
-
tags: cap.tags || [],
|
|
244
|
-
tools: cap.tools || [],
|
|
245
|
-
})),
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* 生成 MCP server instructions(≤32KB)。业务只提供数据,不再手写排版。
|
|
251
|
-
*/
|
|
252
|
-
export function buildInstructions({
|
|
253
|
-
title = '',
|
|
254
|
-
intro = '',
|
|
255
|
-
startup = [],
|
|
256
|
-
capabilities = [],
|
|
257
|
-
tools = [],
|
|
258
|
-
rules = [],
|
|
259
|
-
output = [],
|
|
260
|
-
} = {}) {
|
|
261
|
-
const rows = capabilities.length
|
|
262
|
-
? capabilities.map((cap) => `| ${cap.title} | ${cap.tools.map((name) => `\`${name}\``).join(', ')} | ${cap.description || ''} |`)
|
|
263
|
-
: [{
|
|
264
|
-
title: '工具',
|
|
265
|
-
tools: tools.map((tool) => tool.name),
|
|
266
|
-
description: '',
|
|
267
|
-
}].map((cap) => `| ${cap.title} | ${cap.tools.map((name) => `\`${name}\``).join(', ')} | |`)
|
|
268
|
-
|
|
269
|
-
const lines = []
|
|
270
|
-
if (title) lines.push(`# ${title}`, '')
|
|
271
|
-
if (intro) lines.push(intro, '')
|
|
272
|
-
if (startup.length) {
|
|
273
|
-
lines.push('## 启动检查')
|
|
274
|
-
startup.forEach((line, index) => lines.push(`${index + 1}. ${line}`))
|
|
275
|
-
lines.push('')
|
|
276
|
-
}
|
|
277
|
-
lines.push('## 能力面', '| 能力 | 工具 | 用途 |', '|---|---|---|', ...rows, '')
|
|
278
|
-
if (rules.length) {
|
|
279
|
-
lines.push('## 硬规则')
|
|
280
|
-
rules.forEach((line) => lines.push(`- ${line}`))
|
|
281
|
-
lines.push('')
|
|
282
|
-
}
|
|
283
|
-
if (output.length) {
|
|
284
|
-
lines.push('## 输出')
|
|
285
|
-
output.forEach((line) => lines.push(`- ${line}`))
|
|
286
|
-
}
|
|
287
|
-
return lines.join('\n')
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* 把能力注册表直接注册到 DSH 的 ctx.tools(同进程原生工具面)。
|
|
292
|
-
* 注册后不再需要单独的 loopback /mcp 端口和 @deepseek-ai/dsh-mcp-client row。
|
|
293
|
-
* @param {object} ctx DSH cordis context
|
|
294
|
-
* @param {object} registry createRegistry() 的返回值
|
|
295
|
-
* @param {{ namespace?: string }} options
|
|
296
|
-
* @returns {() => void} 反注册全部工具
|
|
297
|
-
*/
|
|
298
|
-
export function registerWithDsh(ctx, registry, { namespace = '' } = {}) {
|
|
299
|
-
if (!ctx?.tools?.register) throw new Error('registerWithDsh: 当前 DSH 没有 ctx.tools.register')
|
|
300
|
-
const disposers = []
|
|
301
|
-
for (const tool of registry?.TOOLS || []) {
|
|
302
|
-
const impl = registry.TOOL_IMPL?.[tool.name]
|
|
303
|
-
if (typeof impl !== 'function') throw new Error(`registerWithDsh: 工具缺少实现 ${tool.name}`)
|
|
304
|
-
const name = namespace ? `${namespace}_${tool.name}` : tool.name
|
|
305
|
-
disposers.push(ctx.tools.register({
|
|
306
|
-
name,
|
|
307
|
-
description: tool.description || '',
|
|
308
|
-
parameters: tool.inputSchema || { type: 'object', properties: {}, additionalProperties: false },
|
|
309
|
-
output: {
|
|
310
|
-
schema: { type: 'object', additionalProperties: true },
|
|
311
|
-
render: (args, value) => (value && Array.isArray(value.content)
|
|
312
|
-
? value.content
|
|
313
|
-
: [{ type: 'text', text: typeof value === 'string' ? value : JSON.stringify(value) }]),
|
|
314
|
-
},
|
|
315
|
-
execute: (args) => impl(args),
|
|
316
|
-
}))
|
|
317
|
-
}
|
|
318
|
-
return () => {
|
|
319
|
-
for (const dispose of disposers) {
|
|
320
|
-
try { dispose() } catch { /* 忽略重复释放 */ }
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
export function createToolHost(config = {}) {
|
|
326
|
-
return {
|
|
327
|
-
api: createServiceApi('toolHost', '1.0.0', ['dsl', 'registry', 'validate', 'contract', 'mcp', 'instructions', 'native-tools']),
|
|
328
|
-
version: '0.1.0',
|
|
329
|
-
defineTool,
|
|
330
|
-
defineCapability,
|
|
331
|
-
createRegistry: (capabilities, options = {}) => createRegistry(capabilities, { ...config, ...options }),
|
|
332
|
-
createMcpHandler,
|
|
333
|
-
createToolDispatcher,
|
|
334
|
-
createCapabilityReport,
|
|
335
|
-
buildInstructions,
|
|
336
|
-
registerWithDsh,
|
|
337
|
-
validateToolArguments,
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
export { validateToolArguments }
|
|
1
|
+
/**
|
|
2
|
+
* dsh-tool-host —— 工具面标准件。
|
|
3
|
+
*
|
|
4
|
+
* 把“能力声明 → 注册表 → 工具 schema/实现 → 契约报告 → MCP 协议壳”收成一套:
|
|
5
|
+
* defineTool / defineCapability / createRegistry / createMcpHandler /
|
|
6
|
+
* createCapabilityReport / buildInstructions
|
|
7
|
+
*
|
|
8
|
+
* 功能插件只写业务能力,不再复制 kernel/registry/dispatch/validate/contract。
|
|
9
|
+
*/
|
|
10
|
+
import { createMcpHandler as createKitMcpHandler } from '@capyclaw/plugin-kit/service/mcp-http'
|
|
11
|
+
import { validateToolArguments } from '@capyclaw/plugin-kit/tools/validate'
|
|
12
|
+
import { createServiceApi } from '@capyclaw/plugin-kit/service/api'
|
|
13
|
+
|
|
14
|
+
const KINDS = new Set(['read', 'write', 'control'])
|
|
15
|
+
|
|
16
|
+
export function defineTool({
|
|
17
|
+
name,
|
|
18
|
+
description = '',
|
|
19
|
+
inputSchema = { type: 'object', properties: {}, additionalProperties: false },
|
|
20
|
+
kind = 'write',
|
|
21
|
+
since = '1.0.0',
|
|
22
|
+
handler,
|
|
23
|
+
} = {}) {
|
|
24
|
+
if (!name || typeof name !== 'string') throw new Error('defineTool 需要 name')
|
|
25
|
+
if (typeof handler !== 'function') throw new Error(`工具 ${name} 需要 handler`)
|
|
26
|
+
if (!KINDS.has(kind)) throw new Error(`工具 ${name} 的 kind 非法: ${kind}`)
|
|
27
|
+
return Object.freeze({ name, description, inputSchema, kind, since, handler })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function defineCapability({
|
|
31
|
+
id,
|
|
32
|
+
title,
|
|
33
|
+
description = '',
|
|
34
|
+
tags = [],
|
|
35
|
+
requires = [],
|
|
36
|
+
dependsOn = [],
|
|
37
|
+
provides = {},
|
|
38
|
+
features = [],
|
|
39
|
+
tools = [],
|
|
40
|
+
} = {}) {
|
|
41
|
+
if (!id || typeof id !== 'string') throw new Error('defineCapability 需要 id')
|
|
42
|
+
if (!Array.isArray(tools) || !tools.length) throw new Error(`能力 ${id} 至少需要一个工具`)
|
|
43
|
+
return Object.freeze({
|
|
44
|
+
id,
|
|
45
|
+
title: title || id,
|
|
46
|
+
description,
|
|
47
|
+
tags: Object.freeze([...tags]),
|
|
48
|
+
requires: Object.freeze([...requires]),
|
|
49
|
+
dependsOn: Object.freeze([...dependsOn]),
|
|
50
|
+
provides: Object.freeze({ ...provides }),
|
|
51
|
+
features: Object.freeze([...features]),
|
|
52
|
+
tools: Object.freeze([...tools]),
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function validateCapabilities(capabilities) {
|
|
57
|
+
const ids = new Set()
|
|
58
|
+
const toolNames = new Set()
|
|
59
|
+
for (const cap of capabilities) {
|
|
60
|
+
if (ids.has(cap.id)) throw new Error(`重复的 capability id: ${cap.id}`)
|
|
61
|
+
ids.add(cap.id)
|
|
62
|
+
for (const tool of cap.tools) {
|
|
63
|
+
if (toolNames.has(tool.name)) throw new Error(`重复的工具名: ${tool.name}`)
|
|
64
|
+
toolNames.add(tool.name)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
for (const cap of capabilities) {
|
|
68
|
+
for (const dep of cap.dependsOn) {
|
|
69
|
+
if (!ids.has(dep)) throw new Error(`能力 ${cap.id} 依赖不存在的能力: ${dep}`)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function selectCapabilities(capabilities, enabled) {
|
|
75
|
+
if (!enabled || !enabled.length) return capabilities
|
|
76
|
+
const byId = new Map(capabilities.map((cap) => [cap.id, cap]))
|
|
77
|
+
for (const id of enabled) {
|
|
78
|
+
if (!byId.has(id)) throw new Error(`enabled 里有未知能力: ${id}`)
|
|
79
|
+
}
|
|
80
|
+
const ids = new Set(enabled)
|
|
81
|
+
let changed = true
|
|
82
|
+
while (changed) {
|
|
83
|
+
changed = false
|
|
84
|
+
for (const cap of capabilities) {
|
|
85
|
+
if (!ids.has(cap.id)) continue
|
|
86
|
+
for (const dep of cap.dependsOn) {
|
|
87
|
+
if (!ids.has(dep)) { ids.add(dep); changed = true }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return capabilities.filter((cap) => ids.has(cap.id))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @param {Array} capabilities
|
|
96
|
+
* @param {{ enabled?: string[], ports?: object, report?: Function, reserved?: string[] }} options
|
|
97
|
+
*/
|
|
98
|
+
export function createRegistry(capabilities, { enabled = null, ports = null, report = null, reserved = [] } = {}) {
|
|
99
|
+
validateCapabilities(capabilities)
|
|
100
|
+
const selected = selectCapabilities(capabilities, enabled)
|
|
101
|
+
|
|
102
|
+
const kernelPorts = { ...(ports || {}) }
|
|
103
|
+
const reservedNames = new Set([...(reserved || []), 'contract'])
|
|
104
|
+
const providers = new Map()
|
|
105
|
+
for (const cap of selected) {
|
|
106
|
+
for (const [ns, api] of Object.entries(cap.provides || {})) {
|
|
107
|
+
if (reservedNames.has(ns)) throw new Error(`能力 ${cap.id} 不能覆盖内核端口: ${ns}`)
|
|
108
|
+
if (providers.has(ns)) throw new Error(`服务命名空间重复: ${ns}(${providers.get(ns).cap.id} / ${cap.id})`)
|
|
109
|
+
providers.set(ns, { cap, api })
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const available = new Set([...Object.keys(kernelPorts), ...reservedNames, ...providers.keys()])
|
|
114
|
+
for (const cap of selected) {
|
|
115
|
+
for (const req of cap.requires) {
|
|
116
|
+
if (!available.has(req)) throw new Error(`能力 ${cap.id} 需要未提供的端口/服务: ${req}`)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const ctx = { ...kernelPorts }
|
|
121
|
+
for (const [ns, { api }] of providers) {
|
|
122
|
+
ctx[ns] = {}
|
|
123
|
+
for (const [method, fn] of Object.entries(api)) {
|
|
124
|
+
if (typeof fn !== 'function') throw new Error(`服务 ${ns}.${method} 必须是函数`)
|
|
125
|
+
ctx[ns][method] = (...args) => fn(ctx, ...args)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const TOOLS = []
|
|
130
|
+
const TOOL_IMPL = {}
|
|
131
|
+
const TOOL_META = {}
|
|
132
|
+
const FEATURES = []
|
|
133
|
+
const CAPABILITIES = []
|
|
134
|
+
|
|
135
|
+
for (const cap of selected) {
|
|
136
|
+
for (const feature of cap.features) {
|
|
137
|
+
if (!FEATURES.includes(feature)) FEATURES.push(feature)
|
|
138
|
+
}
|
|
139
|
+
CAPABILITIES.push({
|
|
140
|
+
id: cap.id,
|
|
141
|
+
title: cap.title,
|
|
142
|
+
description: cap.description,
|
|
143
|
+
tags: [...cap.tags],
|
|
144
|
+
tools: cap.tools.map((tool) => tool.name),
|
|
145
|
+
})
|
|
146
|
+
for (const tool of cap.tools) {
|
|
147
|
+
TOOLS.push({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema })
|
|
148
|
+
TOOL_META[tool.name] = { kind: tool.kind, since: tool.since, capability: cap.id }
|
|
149
|
+
TOOL_IMPL[tool.name] = (args) => tool.handler(args || {}, ctx)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const reportFn = report || createCapabilityReport
|
|
154
|
+
ctx.contract = {
|
|
155
|
+
report: (editorUrl = '', runtime = {}) => reportFn({
|
|
156
|
+
tools: TOOLS,
|
|
157
|
+
toolMeta: TOOL_META,
|
|
158
|
+
features: FEATURES,
|
|
159
|
+
capabilities: CAPABILITIES,
|
|
160
|
+
editorUrl,
|
|
161
|
+
runtime,
|
|
162
|
+
}),
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { TOOLS, TOOL_IMPL, TOOL_META, FEATURES, CAPABILITIES, ctx }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** 与 MCP 工具面解耦:调用前统一校验。 */
|
|
169
|
+
export function createToolDispatcher({ tools, callTool }) {
|
|
170
|
+
const toolMap = new Map((tools || []).map((tool) => [tool.name, tool]))
|
|
171
|
+
return {
|
|
172
|
+
listTools() {
|
|
173
|
+
return { tools }
|
|
174
|
+
},
|
|
175
|
+
async callTool(name, args = {}) {
|
|
176
|
+
const tool = toolMap.get(name)
|
|
177
|
+
if (!tool) {
|
|
178
|
+
return { content: [{ type: 'text', text: `未知工具: ${String(name)}` }], isError: true }
|
|
179
|
+
}
|
|
180
|
+
const check = validateToolArguments(tool, args)
|
|
181
|
+
if (!check.ok) {
|
|
182
|
+
return { content: [{ type: 'text', text: '参数不合法:' + check.errors.join(';') }], isError: true }
|
|
183
|
+
}
|
|
184
|
+
const out = await callTool(name, args)
|
|
185
|
+
if (out && out.content) return out
|
|
186
|
+
return { content: [{ type: 'text', text: out?.text || 'ok' }] }
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* MCP Streamable HTTP 处理器。registry 可直接传 createRegistry 的返回值。
|
|
193
|
+
* 兼容两种调用:
|
|
194
|
+
* { registry } 或 { tools, toolImpl }
|
|
195
|
+
* { tools, callTool }(旧 relay 形态)
|
|
196
|
+
* @param {{ registry?: object, tools?: Array, toolImpl?: object, callTool?: Function, serverInfo?: object, instructions?: string }} options
|
|
197
|
+
*/
|
|
198
|
+
export function createMcpHandler({ registry, tools, toolImpl, callTool, serverInfo, instructions } = {}) {
|
|
199
|
+
const list = tools || registry?.TOOLS || []
|
|
200
|
+
const impl = toolImpl || registry?.TOOL_IMPL || null
|
|
201
|
+
if (!Array.isArray(list) || !list.length) throw new Error('createMcpHandler: 没有工具可暴露')
|
|
202
|
+
if (!impl && typeof callTool !== 'function') throw new Error('createMcpHandler: 需要 toolImpl 或 callTool')
|
|
203
|
+
return createKitMcpHandler({
|
|
204
|
+
tools: list,
|
|
205
|
+
callTool: (name, args, context) => {
|
|
206
|
+
if (typeof callTool === 'function' && !impl) return callTool(name, args, context)
|
|
207
|
+
const fn = impl[name]
|
|
208
|
+
if (!fn) throw new Error(`未知工具: ${name}`)
|
|
209
|
+
return fn(args, context)
|
|
210
|
+
},
|
|
211
|
+
serverInfo: serverInfo || { name: 'dsh-tool-host', version: '0.1.0' },
|
|
212
|
+
instructions,
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function createCapabilityReport({
|
|
217
|
+
tools = [],
|
|
218
|
+
toolMeta = {},
|
|
219
|
+
features = [],
|
|
220
|
+
capabilities = [],
|
|
221
|
+
contractVersion = '1.0.0',
|
|
222
|
+
packageVersion = '0.0.0',
|
|
223
|
+
editorUrl = '',
|
|
224
|
+
runtime = {},
|
|
225
|
+
} = {}) {
|
|
226
|
+
return {
|
|
227
|
+
contractVersion,
|
|
228
|
+
packageVersion,
|
|
229
|
+
editorUrl,
|
|
230
|
+
...runtime,
|
|
231
|
+
toolCount: tools.length,
|
|
232
|
+
tools: tools.map((tool) => ({
|
|
233
|
+
name: tool.name,
|
|
234
|
+
kind: toolMeta[tool.name]?.kind ?? 'write',
|
|
235
|
+
since: toolMeta[tool.name]?.since ?? '1.0.0',
|
|
236
|
+
capability: toolMeta[tool.name]?.capability ?? '',
|
|
237
|
+
})),
|
|
238
|
+
features,
|
|
239
|
+
capabilities: capabilities.map((cap) => ({
|
|
240
|
+
id: cap.id,
|
|
241
|
+
title: cap.title,
|
|
242
|
+
description: cap.description || '',
|
|
243
|
+
tags: cap.tags || [],
|
|
244
|
+
tools: cap.tools || [],
|
|
245
|
+
})),
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* 生成 MCP server instructions(≤32KB)。业务只提供数据,不再手写排版。
|
|
251
|
+
*/
|
|
252
|
+
export function buildInstructions({
|
|
253
|
+
title = '',
|
|
254
|
+
intro = '',
|
|
255
|
+
startup = [],
|
|
256
|
+
capabilities = [],
|
|
257
|
+
tools = [],
|
|
258
|
+
rules = [],
|
|
259
|
+
output = [],
|
|
260
|
+
} = {}) {
|
|
261
|
+
const rows = capabilities.length
|
|
262
|
+
? capabilities.map((cap) => `| ${cap.title} | ${cap.tools.map((name) => `\`${name}\``).join(', ')} | ${cap.description || ''} |`)
|
|
263
|
+
: [{
|
|
264
|
+
title: '工具',
|
|
265
|
+
tools: tools.map((tool) => tool.name),
|
|
266
|
+
description: '',
|
|
267
|
+
}].map((cap) => `| ${cap.title} | ${cap.tools.map((name) => `\`${name}\``).join(', ')} | |`)
|
|
268
|
+
|
|
269
|
+
const lines = []
|
|
270
|
+
if (title) lines.push(`# ${title}`, '')
|
|
271
|
+
if (intro) lines.push(intro, '')
|
|
272
|
+
if (startup.length) {
|
|
273
|
+
lines.push('## 启动检查')
|
|
274
|
+
startup.forEach((line, index) => lines.push(`${index + 1}. ${line}`))
|
|
275
|
+
lines.push('')
|
|
276
|
+
}
|
|
277
|
+
lines.push('## 能力面', '| 能力 | 工具 | 用途 |', '|---|---|---|', ...rows, '')
|
|
278
|
+
if (rules.length) {
|
|
279
|
+
lines.push('## 硬规则')
|
|
280
|
+
rules.forEach((line) => lines.push(`- ${line}`))
|
|
281
|
+
lines.push('')
|
|
282
|
+
}
|
|
283
|
+
if (output.length) {
|
|
284
|
+
lines.push('## 输出')
|
|
285
|
+
output.forEach((line) => lines.push(`- ${line}`))
|
|
286
|
+
}
|
|
287
|
+
return lines.join('\n')
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* 把能力注册表直接注册到 DSH 的 ctx.tools(同进程原生工具面)。
|
|
292
|
+
* 注册后不再需要单独的 loopback /mcp 端口和 @deepseek-ai/dsh-mcp-client row。
|
|
293
|
+
* @param {object} ctx DSH cordis context
|
|
294
|
+
* @param {object} registry createRegistry() 的返回值
|
|
295
|
+
* @param {{ namespace?: string }} options
|
|
296
|
+
* @returns {() => void} 反注册全部工具
|
|
297
|
+
*/
|
|
298
|
+
export function registerWithDsh(ctx, registry, { namespace = '' } = {}) {
|
|
299
|
+
if (!ctx?.tools?.register) throw new Error('registerWithDsh: 当前 DSH 没有 ctx.tools.register')
|
|
300
|
+
const disposers = []
|
|
301
|
+
for (const tool of registry?.TOOLS || []) {
|
|
302
|
+
const impl = registry.TOOL_IMPL?.[tool.name]
|
|
303
|
+
if (typeof impl !== 'function') throw new Error(`registerWithDsh: 工具缺少实现 ${tool.name}`)
|
|
304
|
+
const name = namespace ? `${namespace}_${tool.name}` : tool.name
|
|
305
|
+
disposers.push(ctx.tools.register({
|
|
306
|
+
name,
|
|
307
|
+
description: tool.description || '',
|
|
308
|
+
parameters: tool.inputSchema || { type: 'object', properties: {}, additionalProperties: false },
|
|
309
|
+
output: {
|
|
310
|
+
schema: { type: 'object', additionalProperties: true },
|
|
311
|
+
render: (args, value) => (value && Array.isArray(value.content)
|
|
312
|
+
? value.content
|
|
313
|
+
: [{ type: 'text', text: typeof value === 'string' ? value : JSON.stringify(value) }]),
|
|
314
|
+
},
|
|
315
|
+
execute: (args) => impl(args),
|
|
316
|
+
}))
|
|
317
|
+
}
|
|
318
|
+
return () => {
|
|
319
|
+
for (const dispose of disposers) {
|
|
320
|
+
try { dispose() } catch { /* 忽略重复释放 */ }
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function createToolHost(config = {}) {
|
|
326
|
+
return {
|
|
327
|
+
api: createServiceApi('toolHost', '1.0.0', ['dsl', 'registry', 'validate', 'contract', 'mcp', 'instructions', 'native-tools']),
|
|
328
|
+
version: '0.1.0',
|
|
329
|
+
defineTool,
|
|
330
|
+
defineCapability,
|
|
331
|
+
createRegistry: (capabilities, options = {}) => createRegistry(capabilities, { ...config, ...options }),
|
|
332
|
+
createMcpHandler,
|
|
333
|
+
createToolDispatcher,
|
|
334
|
+
createCapabilityReport,
|
|
335
|
+
buildInstructions,
|
|
336
|
+
registerWithDsh,
|
|
337
|
+
validateToolArguments,
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export { validateToolArguments }
|