dsh-plugin-prompt-tool 0.6.2 → 0.7.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 +11 -9
- package/engine/anchor-match.mjs +38 -12
- package/engine/anchor-turn.mjs +68 -0
- package/engine/classify-task.mjs +35 -0
- package/engine/code-presentation.mjs +103 -0
- package/engine/compositions/library/anchor-turn.yml +10 -0
- package/engine/compositions/library/bootstrap-filesystem.yml +1 -1
- package/engine/compositions/library/code-presentation.yml +8 -0
- package/engine/compositions/library/command-goal.yml +11 -0
- package/engine/compositions/library/compaction.yml +1 -1
- package/engine/compositions/library/context-gate.yml +1 -2
- package/engine/compositions/library/cot-drip.yml +11 -0
- package/engine/compositions/library/custom-bash.yml +1 -1
- package/engine/compositions/library/delegation.yml +11 -7
- package/engine/compositions/library/deliberation-gate.yml +11 -0
- package/engine/compositions/library/persistent-shell.yml +25 -2
- package/engine/compositions/library/persona.yml +6 -2
- package/engine/compositions/library/planning.yml +1 -1
- package/engine/compositions/library/prompt-config-engine.yml +1 -1
- package/engine/compositions/library/run-code-env.yml +1 -1
- package/engine/compositions/library/skill-filesystem.yml +1 -1
- package/engine/compositions/library/skill-search.yml +1 -1
- package/engine/compositions/library/str-replace-editor.yml +2 -2
- package/engine/compositions/library/tool-ask-user.yml +1 -1
- package/engine/compositions/library/tool-bash.yml +1 -1
- package/engine/compositions/library/tool-bootstrap.yml +1 -3
- package/engine/compositions/library/tool-config-engine.yml +9 -0
- package/engine/compositions/library/tool-filter.yml +3 -7
- package/engine/compositions/library/tool-fs-search.yml +1 -1
- package/engine/compositions/library/tool-fs.yml +1 -1
- package/engine/compositions/library/tool-goal.yml +1 -9
- package/engine/compositions/library/tool-jobs.yml +1 -1
- package/engine/compositions/library/tool-pwsh.yml +1 -1
- package/engine/compositions/library/tool-todo.yml +1 -1
- package/engine/compositions/library/tool-web.yml +2 -2
- package/engine/compositions/source/local/code-presentation.yml +8 -0
- package/engine/compositions/source/local/context-gate.yml +0 -1
- package/engine/compositions/source/local/str-replace-editor.yml +10 -0
- package/engine/compositions/source/local/tool-bootstrap.yml +1 -3
- package/engine/compositions/source/local/tool-config-engine.yml +9 -0
- package/engine/compositions/source/local/tool-filter.yml +8 -0
- package/engine/cot-drip.mjs +137 -0
- package/engine/custom-bash.mjs +73 -21
- package/engine/deliberation-gate.mjs +127 -0
- package/engine/layers.mjs +223 -198
- package/engine/run-code-env.mjs +8 -2
- package/engine/schema.mjs +1 -1
- package/engine/strategies.mjs +25 -14
- package/engine/tool-bootstrap.mjs +185 -59
- package/engine/tool-config-engine.mjs +390 -0
- package/lib/client.js +3840 -2311
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +185 -79
- package/lib/index.mjs +1000 -356
- package/lib/preset-core.d.mts +1 -1
- package/lib/preset-core.mjs +29 -3
- package/lib/{prompt-configs-BgFw_bFf.mjs → prompt-configs-BnQKRHVb.mjs} +211 -136
- package/lib/prompt-configs-CN0sJTUL.d.mts +193 -0
- package/package.json +121 -105
- package/preset/anchored/preset.yml +262 -308
- package/preset/creative/preset.yml +34 -32
- package/preset/custom/preset.yml +0 -1
- package/preset/liangshen/preset.yml +53 -50
- package/preset/minimal/preset.yml +18 -17
- package/preset/ptc/preset.yml +35 -33
- package/preset/standard/preset.yml +34 -32
- package/templates/10-pre-step.yml +18 -15
- package/templates/14-first-turn-anchor.yml +13 -13
- package/templates/15-guide-auto.yml +13 -13
- package/templates/16-custom-fallback.yml +19 -18
- package/templates/18-placeholder-env-facts.yml +13 -2
- package/templates/30-runtime-context.yml +3 -0
- package/templates/40-agent-request.yml +9 -9
- package/templates/60-tool-pipeline.yml +9 -8
- package/templates/tools/10-shell-command.yml +20 -0
- package/templates/tools/20-http-fetch.yml +24 -0
- package/templates/tools/30-fs-read.yml +20 -0
- package/templates/tools/40-delegate-character.yml +15 -0
- package/templates/tools/50-delegate-worldbook.yml +15 -0
- package/templates/tools/60-delegate-session-var.yml +15 -0
- package/templates/tools/70-delegate-worldbook-upsert.yml +36 -0
- package/lib/prompt-configs-B4vH09wx.d.mts +0 -100
- package/templates/11-merged-a.yml +0 -11
- package/templates/13-anchor.yml +0 -19
- package/templates/17-instruction-hint.yml +0 -23
- package/templates/19-placeholder-skill-catalog.yml +0 -19
- package/templates/31-runtime-context-placeholder.yml +0 -18
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tool-config-engine — 自定义工具引擎(配置驱动 → ctx.tools.register)。
|
|
3
|
+
*
|
|
4
|
+
* 工具定义从 configsDir 目录加载(writePreset 渲染 custom-tools/*.yml,
|
|
5
|
+
* 源 = preset.yml 顶层 customTools 段),每份一个工具定义:
|
|
6
|
+
* id / name / description / scope / timeoutMs / parameters(dsh-tools schema
|
|
7
|
+
* DSL 的 JSON 形态)/ output.schema / execute.{kind, ...}
|
|
8
|
+
*
|
|
9
|
+
* 执行器(kind):
|
|
10
|
+
* shell — 命令执行(execFile 无 shell 解析,env 白名单,cwd=会话工作区)
|
|
11
|
+
* http — fetch 请求(method/url/headers/body,{{args.x}} 插值)
|
|
12
|
+
* delegate — 委托已注册工具(ctx.tools.get + 透传参数,高级用法:
|
|
13
|
+
* 目标工具 execute 直接调用,不经 registry 校验管线)
|
|
14
|
+
* fs — 文件操作(read/write/append/list/delete,路径限定 cwd 内)
|
|
15
|
+
* ask-user — approval 通道询问用户(结果文本化)
|
|
16
|
+
*
|
|
17
|
+
* 安全:行 config.requireApproval = [kind...] 时该执行器先过 approval 门
|
|
18
|
+
* (无 approval 服务则拒绝);单条定义非法 warnOnce 跳过(不挂整行)。
|
|
19
|
+
* 工具注册经 ctx.effect,插件卸载自动撤销。
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { readFileSync, readdirSync, writeFileSync, appendFileSync, rmSync, statSync } from 'node:fs'
|
|
23
|
+
import { execFile } from 'node:child_process'
|
|
24
|
+
import { isAbsolute, join, resolve, sep } from 'node:path'
|
|
25
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
26
|
+
import { parse as parseYaml } from './vendor/yaml/index.js'
|
|
27
|
+
|
|
28
|
+
/** Cordis 插件名,供 loader 诊断使用。 */
|
|
29
|
+
export const name = 'tool-config-engine'
|
|
30
|
+
|
|
31
|
+
export const inject = ['tools']
|
|
32
|
+
|
|
33
|
+
/** 工具名规范(模型可见,schemas() 白名单要求)。 */
|
|
34
|
+
const TOOL_NAME_RE = /^[a-z][a-z0-9_]*$/
|
|
35
|
+
|
|
36
|
+
/** 合法参数节点类型(dsh-tools ValueSchemaSpec 的 type 枚举)。 */
|
|
37
|
+
const SCHEMA_TYPES = new Set(['string', 'number', 'integer', 'boolean', 'null', 'array', 'object', 'json', 'oneOf'])
|
|
38
|
+
|
|
39
|
+
/** shell 执行器的 env 白名单(无凭据形态;与 run-code-env 同基调)。 */
|
|
40
|
+
const ENV_ALLOWLIST = [
|
|
41
|
+
'PATH', 'PATHEXT', 'SystemRoot', 'COMSPEC', 'TEMP', 'TMP',
|
|
42
|
+
'USERPROFILE', 'HOME', 'DSH_HOME', 'DSH_WORKSPACE', 'DSH_SESSION_ID',
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
/** 插值 {{args.x.y}} → 参数深取值(缺失保留字面)。 */
|
|
46
|
+
function interpolateArgs(text, args) {
|
|
47
|
+
return String(text ?? '').replace(/\{\{args\.([A-Za-z0-9_.]+)\}\}/g, (_whole, path) => {
|
|
48
|
+
let value = args
|
|
49
|
+
for (const part of String(path).split('.')) {
|
|
50
|
+
if (value === null || typeof value !== 'object') return _whole
|
|
51
|
+
value = value[part]
|
|
52
|
+
}
|
|
53
|
+
return value === undefined || value === null ? _whole : String(value)
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 递归校验 schema 节点(宽松:type 枚举 + object 必须声明 additionalProperties + required 必须 true)。 */
|
|
58
|
+
function validateSchemaNode(node, path, seen) {
|
|
59
|
+
if (node === null || typeof node !== 'object' || Array.isArray(node)) {
|
|
60
|
+
throw new TypeError(`${path} must be an object`)
|
|
61
|
+
}
|
|
62
|
+
if (seen.has(node)) throw new TypeError(`${path} is circular`)
|
|
63
|
+
seen.add(node)
|
|
64
|
+
try {
|
|
65
|
+
if (typeof node.type !== 'string' || !SCHEMA_TYPES.has(node.type)) {
|
|
66
|
+
throw new TypeError(`${path}.type must be one of ${[...SCHEMA_TYPES].join('/')}`)
|
|
67
|
+
}
|
|
68
|
+
if (node.type === 'object' && typeof node.additionalProperties !== 'boolean') {
|
|
69
|
+
throw new TypeError(`${path}.additionalProperties must be explicitly true or false`)
|
|
70
|
+
}
|
|
71
|
+
if (node.required !== undefined && node.required !== true) {
|
|
72
|
+
throw new TypeError(`${path}.required must be true when present`)
|
|
73
|
+
}
|
|
74
|
+
if (node.type === 'object' && node.properties !== undefined) {
|
|
75
|
+
for (const [key, child] of Object.entries(node.properties)) {
|
|
76
|
+
validateSchemaNode(child, `${path}.properties.${key}`, seen)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (node.type === 'array' && node.items !== undefined) {
|
|
80
|
+
validateSchemaNode(node.items, `${path}.items`, seen)
|
|
81
|
+
}
|
|
82
|
+
if (node.type === 'oneOf' && Array.isArray(node.oneOf)) {
|
|
83
|
+
if (node.oneOf.length < 2) throw new TypeError(`${path}.oneOf needs at least two branches`)
|
|
84
|
+
for (const [index, branch] of node.oneOf.entries()) {
|
|
85
|
+
validateSchemaNode(branch, `${path}.oneOf[${index}]`, seen)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} finally {
|
|
89
|
+
seen.delete(node)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** 工具定义轻校验(fail loud → 调用方 warnOnce 跳过该条)。 */
|
|
94
|
+
function validateDefinition(def) {
|
|
95
|
+
if (def === null || typeof def !== 'object' || Array.isArray(def)) throw new TypeError('tool definition must be an object')
|
|
96
|
+
if (typeof def.id !== 'string' || def.id.length === 0) throw new TypeError('tool definition needs a non-empty string id')
|
|
97
|
+
if (typeof def.name !== 'string' || !TOOL_NAME_RE.test(def.name)) {
|
|
98
|
+
throw new TypeError(`tool name ${JSON.stringify(def.name)} must match ${TOOL_NAME_RE}`)
|
|
99
|
+
}
|
|
100
|
+
if (typeof def.description !== 'string' || def.description.trim().length === 0) {
|
|
101
|
+
throw new TypeError(`tool ${def.id}: description is required`)
|
|
102
|
+
}
|
|
103
|
+
if (def.parameters !== undefined) {
|
|
104
|
+
if (def.parameters === null || typeof def.parameters !== 'object' || Array.isArray(def.parameters)) {
|
|
105
|
+
throw new TypeError(`tool ${def.id}: parameters must be an object map`)
|
|
106
|
+
}
|
|
107
|
+
for (const [key, child] of Object.entries(def.parameters)) {
|
|
108
|
+
validateSchemaNode(child, `parameters.${key}`, new Set())
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const output = def.output
|
|
112
|
+
if (output === null || typeof output !== 'object' || Array.isArray(output)) {
|
|
113
|
+
throw new TypeError(`tool ${def.id}: output is required`)
|
|
114
|
+
}
|
|
115
|
+
validateSchemaNode(output.schema, 'output.schema', new Set())
|
|
116
|
+
const exec = def.execute
|
|
117
|
+
if (exec === null || typeof exec !== 'object' || Array.isArray(exec)) {
|
|
118
|
+
throw new TypeError(`tool ${def.id}: execute is required`)
|
|
119
|
+
}
|
|
120
|
+
if (typeof exec.kind !== 'string' || !['shell', 'http', 'delegate', 'fs', 'ask-user'].includes(exec.kind)) {
|
|
121
|
+
throw new TypeError(`tool ${def.id}: execute.kind must be shell/http/delegate/fs/ask-user`)
|
|
122
|
+
}
|
|
123
|
+
if (exec.kind === 'shell' && (typeof exec.command !== 'string' || exec.command.trim().length === 0)) {
|
|
124
|
+
throw new TypeError(`tool ${def.id}: execute.command is required for shell`)
|
|
125
|
+
}
|
|
126
|
+
if (exec.kind === 'http' && (typeof exec.url !== 'string' || exec.url.trim().length === 0)) {
|
|
127
|
+
throw new TypeError(`tool ${def.id}: execute.url is required for http`)
|
|
128
|
+
}
|
|
129
|
+
if (exec.kind === 'delegate' && (typeof exec.tool !== 'string' || exec.tool.length === 0)) {
|
|
130
|
+
throw new TypeError(`tool ${def.id}: execute.tool is required for delegate`)
|
|
131
|
+
}
|
|
132
|
+
if (exec.kind === 'fs' && (typeof exec.action !== 'string'
|
|
133
|
+
|| (!['read', 'write', 'append', 'list', 'delete'].includes(exec.action) && !exec.action.includes('{{args.')))) {
|
|
134
|
+
throw new TypeError(`tool ${def.id}: execute.action must be read/write/append/list/delete (or {{args.*}} template) for fs`)
|
|
135
|
+
}
|
|
136
|
+
if (def.scope !== undefined && !['main', 'subagent', 'both'].includes(def.scope)) {
|
|
137
|
+
throw new TypeError(`tool ${def.id}: scope must be main/subagent/both`)
|
|
138
|
+
}
|
|
139
|
+
if (def.enabled !== undefined && typeof def.enabled !== 'boolean') {
|
|
140
|
+
throw new TypeError(`tool ${def.id}: enabled must be a boolean`)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** approval 门:requireApproval 含该 kind 时先请求批准;无 approval 服务拒绝。 */
|
|
145
|
+
async function approvalGate(ctx, exec, kind, reason) {
|
|
146
|
+
const approval = ctx.get('approval')
|
|
147
|
+
if (approval === undefined || typeof approval.request !== 'function') {
|
|
148
|
+
return { ok: false, message: `tool ${exec.name} (${kind}) requires approval, but no approval channel is available` }
|
|
149
|
+
}
|
|
150
|
+
if (exec.agent === undefined) {
|
|
151
|
+
return { ok: false, message: `tool ${exec.name} (${kind}) requires approval, but the call has no agent` }
|
|
152
|
+
}
|
|
153
|
+
const outcome = await approval.request({
|
|
154
|
+
agent: exec.agent,
|
|
155
|
+
toolName: exec.name,
|
|
156
|
+
callId: exec.callId,
|
|
157
|
+
reason,
|
|
158
|
+
signal: exec.signal,
|
|
159
|
+
})
|
|
160
|
+
if (outcome === 'allowed-once') return { ok: true }
|
|
161
|
+
return { ok: false, message: `approval for tool ${exec.name} was ${outcome}` }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** cwd 内路径限定(防越界;resolve 后必须位于 cwd 之下)。 */
|
|
165
|
+
function withinCwd(cwd, target) {
|
|
166
|
+
const resolved = resolve(cwd, target)
|
|
167
|
+
const root = resolve(cwd)
|
|
168
|
+
if (resolved !== root && !resolved.startsWith(root + sep)) {
|
|
169
|
+
throw new Error(`path ${JSON.stringify(target)} escapes the workspace`)
|
|
170
|
+
}
|
|
171
|
+
return resolved
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** 执行器分发:返回成功值(registry 按 output.schema 校验)。 */
|
|
175
|
+
function createExecute(ctx, def, requireApproval) {
|
|
176
|
+
const exec = def.execute
|
|
177
|
+
return async (args, run) => {
|
|
178
|
+
if (requireApproval.includes(exec.kind)) {
|
|
179
|
+
const gate = await approvalGate(ctx, run, exec.kind, `custom tool ${def.name} (${exec.kind})`)
|
|
180
|
+
if (!gate.ok) return { ok: false, error: gate.message }
|
|
181
|
+
}
|
|
182
|
+
if (exec.kind === 'shell') {
|
|
183
|
+
const command = interpolateArgs(exec.command, args)
|
|
184
|
+
const cwd = run.agent?.session?.header?.cwd ?? process.cwd()
|
|
185
|
+
const shell = exec.shell ?? (process.platform === 'win32' ? 'pwsh' : 'sh')
|
|
186
|
+
// pwsh/powershell:重定向输出时强制 UTF-8(默认随控制台代码页,中文会乱码)。
|
|
187
|
+
const psPrefix = '[Console]::OutputEncoding=[System.Text.UTF8Encoding]::new();$OutputEncoding=[System.Text.UTF8Encoding]::new();'
|
|
188
|
+
const argv = shell === 'pwsh' ? ['-NoProfile', '-NonInteractive', '-Command', `${psPrefix}${command}`]
|
|
189
|
+
: shell === 'powershell' ? ['-NoProfile', '-NonInteractive', '-Command', `${psPrefix}${command}`]
|
|
190
|
+
: shell === 'cmd' ? ['/D', '/S', '/C', command]
|
|
191
|
+
: ['-c', command]
|
|
192
|
+
const env = { ...Object.fromEntries(ENV_ALLOWLIST.filter((key) => process.env[key] !== undefined).map((key) => [key, process.env[key]])), ...(exec.env ?? {}) }
|
|
193
|
+
const output = await new Promise((resolvePromise) => {
|
|
194
|
+
let killTimer
|
|
195
|
+
const child = execFile(shell, argv, { cwd, env, windowsHide: true }, (error, stdout, stderr) => {
|
|
196
|
+
clearTimeout(killTimer)
|
|
197
|
+
resolvePromise({
|
|
198
|
+
exitCode: error === null ? 0 : (typeof error.code === 'number' ? error.code : 1),
|
|
199
|
+
stdout: String(stdout ?? ''),
|
|
200
|
+
stderr: String(stderr ?? ''),
|
|
201
|
+
error: error === null ? undefined : String(error.message),
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
const timeout = def.timeoutMs
|
|
205
|
+
if (timeout !== undefined && Number.isSafeInteger(timeout) && timeout > 0) {
|
|
206
|
+
killTimer = setTimeout(() => child.kill(), timeout)
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
return output
|
|
210
|
+
}
|
|
211
|
+
if (exec.kind === 'http') {
|
|
212
|
+
const url = interpolateArgs(exec.url, args)
|
|
213
|
+
const headers = Object.fromEntries(Object.entries(exec.headers ?? {}).map(([key, value]) => [key, interpolateArgs(String(value), args)]))
|
|
214
|
+
const rawBody = exec.body
|
|
215
|
+
const body = rawBody === undefined ? undefined : JSON.stringify(JSON.parse(interpolateArgs(JSON.stringify(rawBody), args)))
|
|
216
|
+
const timeout = def.timeoutMs ?? 15000
|
|
217
|
+
const response = await fetch(url, {
|
|
218
|
+
method: String(exec.method ?? 'GET').toUpperCase(),
|
|
219
|
+
headers,
|
|
220
|
+
...(body !== undefined ? { body } : {}),
|
|
221
|
+
signal: AbortSignal.timeout(timeout),
|
|
222
|
+
})
|
|
223
|
+
const text = await response.text()
|
|
224
|
+
return { status: response.status, ok: response.ok, body: text }
|
|
225
|
+
}
|
|
226
|
+
if (exec.kind === 'delegate') {
|
|
227
|
+
const tools = ctx.get('tools')
|
|
228
|
+
const target = tools?.get?.(exec.tool)
|
|
229
|
+
if (target === undefined || typeof target.execute !== 'function') {
|
|
230
|
+
return { ok: false, error: `delegated tool ${exec.tool} is not registered` }
|
|
231
|
+
}
|
|
232
|
+
// 高级用法:透传参数直接调用目标 execute(不经 registry 校验管线)。
|
|
233
|
+
// args 映射:完整引用({{args.x}})透传原始类型(数组/数字/布尔不字符串化);
|
|
234
|
+
// 部分引用(前缀-{{args.x}})插值为字符串;非插值值原样。
|
|
235
|
+
const FULL_REF_RE = /^\{\{args\.([A-Za-z0-9_.]+)\}\}$/
|
|
236
|
+
const mapDelegateArg = (value) => {
|
|
237
|
+
if (Array.isArray(value)) return value.map(mapDelegateArg)
|
|
238
|
+
if (value !== null && typeof value === 'object') {
|
|
239
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, mapDelegateArg(child)]))
|
|
240
|
+
}
|
|
241
|
+
if (typeof value === 'string') {
|
|
242
|
+
const ref = FULL_REF_RE.exec(value)
|
|
243
|
+
if (ref !== null) {
|
|
244
|
+
let resolved = args
|
|
245
|
+
for (const part of ref[1].split('.')) {
|
|
246
|
+
if (resolved === null || typeof resolved !== 'object') {
|
|
247
|
+
resolved = undefined
|
|
248
|
+
break
|
|
249
|
+
}
|
|
250
|
+
resolved = resolved[part]
|
|
251
|
+
}
|
|
252
|
+
return resolved === undefined ? value : resolved
|
|
253
|
+
}
|
|
254
|
+
return interpolateArgs(value, args)
|
|
255
|
+
}
|
|
256
|
+
return value
|
|
257
|
+
}
|
|
258
|
+
const delegatedArgs = exec.args !== undefined && exec.args !== null && typeof exec.args === 'object'
|
|
259
|
+
? Object.fromEntries(Object.entries(exec.args).map(([key, value]) => [key, mapDelegateArg(value)]))
|
|
260
|
+
: args
|
|
261
|
+
const minimalExec = {
|
|
262
|
+
name: def.name,
|
|
263
|
+
callId: `delegate-${def.name}-${Date.now().toString(36)}`,
|
|
264
|
+
agent: run.agent,
|
|
265
|
+
parent: run.parent,
|
|
266
|
+
signal: run.signal,
|
|
267
|
+
arguments: delegatedArgs,
|
|
268
|
+
deferContext: () => {},
|
|
269
|
+
concludeTurn: () => {},
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
return { ok: true, value: await target.execute(delegatedArgs, minimalExec) }
|
|
273
|
+
} catch (error) {
|
|
274
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) }
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (exec.kind === 'fs') {
|
|
278
|
+
const cwd = run.agent?.session?.header?.cwd ?? process.cwd()
|
|
279
|
+
try {
|
|
280
|
+
const file = withinCwd(cwd, interpolateArgs(exec.path, args))
|
|
281
|
+
const action = interpolateArgs(exec.action, args)
|
|
282
|
+
if (action === 'read') {
|
|
283
|
+
const raw = readFileSync(file, 'utf8')
|
|
284
|
+
return { ok: true, content: raw }
|
|
285
|
+
}
|
|
286
|
+
if (action === 'write') {
|
|
287
|
+
writeFileSync(file, interpolateArgs(String(exec.content ?? ''), args), 'utf8')
|
|
288
|
+
return { ok: true, path: file }
|
|
289
|
+
}
|
|
290
|
+
if (action === 'append') {
|
|
291
|
+
appendFileSync(file, interpolateArgs(String(exec.content ?? ''), args), 'utf8')
|
|
292
|
+
return { ok: true, path: file }
|
|
293
|
+
}
|
|
294
|
+
if (action === 'list') {
|
|
295
|
+
const entries = readdirSync(file, { withFileTypes: true }).map((entry) => entry.name)
|
|
296
|
+
return { ok: true, entries }
|
|
297
|
+
}
|
|
298
|
+
if (action === 'delete') {
|
|
299
|
+
const info = statSync(file)
|
|
300
|
+
rmSync(file, { recursive: info.isDirectory(), force: true })
|
|
301
|
+
return { ok: true, path: file }
|
|
302
|
+
}
|
|
303
|
+
return { ok: false, error: `unknown fs action ${action}` }
|
|
304
|
+
} catch (error) {
|
|
305
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) }
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (exec.kind === 'ask-user') {
|
|
309
|
+
const approval = ctx.get('approval')
|
|
310
|
+
if (approval === undefined || typeof approval.request !== 'function') {
|
|
311
|
+
return { ok: false, error: 'no approval channel is available' }
|
|
312
|
+
}
|
|
313
|
+
if (run.agent === undefined) return { ok: false, error: 'no active agent for ask-user' }
|
|
314
|
+
const outcome = await approval.request({
|
|
315
|
+
agent: run.agent,
|
|
316
|
+
toolName: def.name,
|
|
317
|
+
callId: run.callId,
|
|
318
|
+
reason: String(exec.question ?? `${def.name}: confirm?`),
|
|
319
|
+
signal: run.signal,
|
|
320
|
+
})
|
|
321
|
+
return { ok: true, answer: outcome }
|
|
322
|
+
}
|
|
323
|
+
return { ok: false, error: `unknown execute kind ${exec.kind}` }
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** 单份工具定义文件 → 编译为 ToolDefinition 结构(不含 id)。 */
|
|
328
|
+
function compileTool(ctx, def, requireApproval) {
|
|
329
|
+
return {
|
|
330
|
+
name: def.name,
|
|
331
|
+
description: def.description,
|
|
332
|
+
...(def.parameters !== undefined && Object.keys(def.parameters).length > 0 ? { parameters: def.parameters } : {}),
|
|
333
|
+
output: {
|
|
334
|
+
schema: def.output.schema,
|
|
335
|
+
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
336
|
+
},
|
|
337
|
+
...(Number.isSafeInteger(def.timeoutMs) && def.timeoutMs > 0 ? { timeoutMs: def.timeoutMs } : {}),
|
|
338
|
+
execute: createExecute(ctx, def, requireApproval),
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** 工具定义文件加载(*.yml / *.json;与 prompt-config-engine configsDir 同构)。 */
|
|
343
|
+
function loadToolFiles(dirUrl) {
|
|
344
|
+
const resolved = isAbsolute(dirUrl)
|
|
345
|
+
? pathToFileURL(dirUrl).href
|
|
346
|
+
: dirUrl
|
|
347
|
+
const dir = new URL(resolved.endsWith('/') ? resolved : `${resolved}/`, import.meta.url)
|
|
348
|
+
const localDir = fileURLToPath(dir)
|
|
349
|
+
const files = readdirSync(localDir, { withFileTypes: true })
|
|
350
|
+
.filter((entry) => entry.isFile() && /\.(ya?ml|json)$/i.test(entry.name))
|
|
351
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
352
|
+
const out = []
|
|
353
|
+
for (const entry of files) {
|
|
354
|
+
const raw = readFileSync(join(localDir, entry.name), 'utf8')
|
|
355
|
+
const parsed = /\.json$/i.test(entry.name) ? JSON.parse(raw) : parseYaml(raw, { logLevel: 'silent' })
|
|
356
|
+
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) out.push(parsed)
|
|
357
|
+
}
|
|
358
|
+
return out
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** 插件入口:扫描 configsDir 注册全部自定义工具。 */
|
|
362
|
+
export function apply(ctx, config) {
|
|
363
|
+
const dirName = typeof config?.configsDir === 'string' && config.configsDir.length > 0
|
|
364
|
+
? config.configsDir
|
|
365
|
+
: './custom-tools'
|
|
366
|
+
const requireApproval = Array.isArray(config?.requireApproval)
|
|
367
|
+
? config.requireApproval.filter((kind) => typeof kind === 'string')
|
|
368
|
+
: []
|
|
369
|
+
let definitions = []
|
|
370
|
+
try {
|
|
371
|
+
definitions = loadToolFiles(dirName)
|
|
372
|
+
} catch (error) {
|
|
373
|
+
ctx.logger?.warn(`${name}: cannot load ${dirName}: ${error?.message ?? error}`)
|
|
374
|
+
return
|
|
375
|
+
}
|
|
376
|
+
for (const def of definitions) {
|
|
377
|
+
try {
|
|
378
|
+
validateDefinition(def)
|
|
379
|
+
if (def.enabled === false) {
|
|
380
|
+
ctx.logger?.info(`${name}: skipping disabled tool ${JSON.stringify(def.name)}`)
|
|
381
|
+
continue
|
|
382
|
+
}
|
|
383
|
+
const tool = compileTool(ctx, def, requireApproval)
|
|
384
|
+
ctx.effect(() => ctx.tools.register(tool), `${name}: ${def.name}`)
|
|
385
|
+
ctx.logger?.info(`${name}: registered custom tool ${def.name} (${def.execute.kind})`)
|
|
386
|
+
} catch (error) {
|
|
387
|
+
ctx.logger?.warn(`${name}: skipping tool ${JSON.stringify(def?.id ?? def?.name)}: ${error?.message ?? error}`)
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|