@capyclaw/tool-host 0.0.0-stage → 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/README.md +55 -2
- package/cordis.patch.yml +4 -0
- package/dsh-suite.json +29 -0
- package/lib/host.mjs +13 -0
- package/package.json +44 -4
- package/src/index.mjs +341 -0
package/README.md
CHANGED
|
@@ -1,3 +1,56 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-tool-host
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
DSH 公共工具面插件。把每个插件都会重复写的工具声明、注册表、参数校验、契约报告、MCP 协议壳和 instructions 模板收成一套。
|
|
4
|
+
|
|
5
|
+
## 使用
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
export const inject = ['toolHost']
|
|
9
|
+
|
|
10
|
+
const echo = ctx.toolHost.defineTool({
|
|
11
|
+
name: 'demo_echo',
|
|
12
|
+
description: '回显文本',
|
|
13
|
+
inputSchema: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'] },
|
|
14
|
+
handler: ({ text }) => ({ content: [{ type: 'text', text }] }),
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const capabilities = [
|
|
18
|
+
ctx.toolHost.defineCapability({ id: 'demo', title: 'Demo', tools: [echo] }),
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
const registry = ctx.toolHost.createRegistry(capabilities)
|
|
22
|
+
const mcp = ctx.toolHost.createMcpHandler({
|
|
23
|
+
registry,
|
|
24
|
+
instructions: ctx.toolHost.buildInstructions({ title: 'Demo', capabilities: registry.CAPABILITIES }),
|
|
25
|
+
})
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## 原生 DSH 工具面
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
export const inject = ['toolHost']
|
|
32
|
+
|
|
33
|
+
export function apply(ctx) {
|
|
34
|
+
const registry = ctx.toolHost.createRegistry(capabilities)
|
|
35
|
+
const dispose = ctx.toolHost.registerWithDsh(ctx, registry, { namespace: 'demo' })
|
|
36
|
+
return dispose
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
注册后工具直接出现在 `ctx.tools`,不需要 loopback `/mcp` 和 `dsh-mcp-client` row。
|
|
41
|
+
MCP handler 仍保留给跨进程/外部客户端。
|
|
42
|
+
|
|
43
|
+
## 提供什么
|
|
44
|
+
|
|
45
|
+
- `defineTool` / `defineCapability`:声明式能力 DSL
|
|
46
|
+
- `createRegistry`:校验 id/工具名/依赖/服务唯一性,派生 `TOOLS / TOOL_IMPL / TOOL_META / FEATURES / CAPABILITIES`
|
|
47
|
+
- `createToolDispatcher`:统一参数校验后分派
|
|
48
|
+
- `createMcpHandler`:MCP Streamable HTTP(initialize / ping / tools/list / tools/call)
|
|
49
|
+
- `createCapabilityReport`:契约报告
|
|
50
|
+
- `buildInstructions`:MCP server instructions 模板
|
|
51
|
+
|
|
52
|
+
## 边界
|
|
53
|
+
|
|
54
|
+
- 不绑定具体插件;PPT 只是第一个消费者
|
|
55
|
+
- 不负责 HTTP 监听;和 `dsh-service-host` 组合使用
|
|
56
|
+
- 不负责契约快照文件;由构建脚本调用 `registry.ctx.contract.report()` 生成
|
package/cordis.patch.yml
ADDED
package/dsh-suite.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../dsh-suite-kit/suite.schema.json",
|
|
3
|
+
"name": "dsh-tool-host-suite",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"title": "Tool Host",
|
|
6
|
+
"description": "通用工具面:能力 DSL、注册表、校验、契约、MCP 壳。",
|
|
7
|
+
"packages": {
|
|
8
|
+
"@capyclaw/tool-host": {
|
|
9
|
+
"role": "provider",
|
|
10
|
+
"version": "^0.1.1",
|
|
11
|
+
"path": ".",
|
|
12
|
+
"provides": {
|
|
13
|
+
"services": [
|
|
14
|
+
"toolHost"
|
|
15
|
+
],
|
|
16
|
+
"capabilities": {
|
|
17
|
+
"tools.registry": "1.0.0"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"rows": [
|
|
23
|
+
{
|
|
24
|
+
"id": "tool-host",
|
|
25
|
+
"name": "@capyclaw/tool-host"
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"checks": []
|
|
29
|
+
}
|
package/lib/host.mjs
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-tool-host 插件:向同进程插件提供 ctx.toolHost。
|
|
3
|
+
*/
|
|
4
|
+
import { createToolHost } from '../src/index.mjs'
|
|
5
|
+
|
|
6
|
+
export const name = 'tool-host'
|
|
7
|
+
|
|
8
|
+
export function apply(ctx, config = {}) {
|
|
9
|
+
const service = createToolHost(config)
|
|
10
|
+
const dispose = ctx.provide('toolHost', service)
|
|
11
|
+
console.log('[tool-host] ready')
|
|
12
|
+
return () => { if (typeof dispose === 'function') dispose() }
|
|
13
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capyclaw/tool-host",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "DSH 公共工具面插件:能力 DSL、工具注册表、参数校验、契约报告、MCP 协议壳与 instructions 模板,向插件提供 ctx.toolHost。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./lib/host.mjs",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./lib/host.mjs",
|
|
9
|
+
"./src": "./src/index.mjs",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=18",
|
|
14
|
+
"dsh": "^0.2.0-rc.2"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"dsh": {
|
|
20
|
+
"bundle": {
|
|
21
|
+
"patch": "./cordis.patch.yml"
|
|
22
|
+
},
|
|
23
|
+
"suite": "./dsh-suite.json",
|
|
24
|
+
"apiVersion": "1.0.0",
|
|
25
|
+
"category": "service",
|
|
26
|
+
"platform": "both"
|
|
27
|
+
},
|
|
28
|
+
"meta": {
|
|
29
|
+
"title": "公共服务工具面",
|
|
30
|
+
"description": "能力 DSL、工具注册表、参数校验、契约报告与 MCP 协议壳,向插件提供 ctx.toolHost"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"lib/",
|
|
34
|
+
"src/",
|
|
35
|
+
"cordis.patch.yml",
|
|
36
|
+
"dsh-suite.json",
|
|
37
|
+
"README.md"
|
|
38
|
+
],
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"scripts": {
|
|
41
|
+
"test": "node --test test/unit.test.mjs"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@capyclaw/plugin-kit": "^0.2.0"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/index.mjs
ADDED
|
@@ -0,0 +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 }
|