cs-bots 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +164 -2
- package/cordis.patch.agent-team.yml +31 -0
- package/cordis.patch.yml +30 -0
- package/install.sh +218 -0
- package/package.json +57 -4
- package/src/api.js +706 -0
- package/src/board.js +1056 -0
- package/src/botprofile.js +331 -0
- package/src/client.js +2089 -0
- package/src/compat.js +196 -0
- package/src/dirbrowse.js +555 -0
- package/src/dispatcher.js +651 -0
- package/src/index.js +311 -0
- package/src/kernel.js +480 -0
- package/src/library.js +740 -0
- package/src/memory.js +1905 -0
- package/src/registry.js +34 -0
- package/src/room.js +1550 -0
- package/src/roster.js +1119 -0
- package/src/settings.js +107 -0
- package/src/store.js +243 -0
- package/src/tools.js +469 -0
package/src/kernel.js
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cs-bots kernel —— **全项目唯一允许 import `@deepseek-ai/*` 的文件**。
|
|
3
|
+
*
|
|
4
|
+
* 由 `test/ci-zero-dep.sh` 检查 1 机械强制:其余任何 `.js` 出现真实
|
|
5
|
+
* `@deepseek-ai/` 的 import/require 即判失败。
|
|
6
|
+
*
|
|
7
|
+
* 为什么单点隔离:DSH 与官方插件包**同版本号发布**(本机 `0.2.1-rc.2`),
|
|
8
|
+
* 宿主改版时只需改本文件;且「零依赖」这条纪律可被 CI 检验,而不是靠自觉。
|
|
9
|
+
*
|
|
10
|
+
* 本文件只暴露**窄接口**,不泄漏宿主类型;每个函数都对缺失服务降级:
|
|
11
|
+
* 缺服务 → 返回降级值(`undefined` / `false`)或抛结构化错误,**绝不阻塞插件装配**。
|
|
12
|
+
*
|
|
13
|
+
* 接口版本:v1(2026-11-01 冻结)。改动本文件即为一次接口变更,需同步更新
|
|
14
|
+
* `docs/kernel-interface.md` 与调用方。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// ── 唯一的宿主 import 面 ────────────────────────────────────────────────────
|
|
18
|
+
//
|
|
19
|
+
// ⚠️ 刻意**不用静态 import**,改惰性 + 多锚点解析。原因(2026-11-01 实测踩到):
|
|
20
|
+
// 官方包只存在于 **dsh 安装目录**(本机 `/usr/lib/node_modules/@deepseek-ai/dsh/node_modules/`),
|
|
21
|
+
// 而它的**兄弟包**(本插件在 profile 的 `node_modules` 里)**解析不到** `@deepseek-ai/dsh-llm`
|
|
22
|
+
// —— profile 的 `node_modules/@deepseek-ai/` 只有 `cosmokit` 与 `schemastery`。
|
|
23
|
+
// 静态 import 会让「宿主包位置一变,插件直接加载失败」,与「绝不阻塞启动」纪律冲突。
|
|
24
|
+
//
|
|
25
|
+
// 惰性的另一收益:**不构造消息的调用路径完全不依赖宿主包** —— 单元测试因此可离线跑。
|
|
26
|
+
import { createRequire } from 'node:module'
|
|
27
|
+
import { existsSync } from 'node:fs'
|
|
28
|
+
|
|
29
|
+
const HOST_LLM = '@deepseek-ai/dsh-llm'
|
|
30
|
+
|
|
31
|
+
/** @type {Function | null} */
|
|
32
|
+
let _createUserMessage = null
|
|
33
|
+
/** @type {Error | null} */
|
|
34
|
+
let _loadError = null
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 探测 dsh 安装锚点(按可靠性排序)。
|
|
38
|
+
* 环境变量 `CS_BOTS_DSH_ROOT` 可显式指定(测试与非常规安装用)。
|
|
39
|
+
* @returns {string[]}
|
|
40
|
+
*/
|
|
41
|
+
function dshAnchors() {
|
|
42
|
+
const anchors = []
|
|
43
|
+
if (process.env.CS_BOTS_DSH_ROOT) anchors.push(process.env.CS_BOTS_DSH_ROOT)
|
|
44
|
+
// 1) 本文件自身位置:装进 dsh 内时通常已经可解析
|
|
45
|
+
anchors.push(import.meta.url)
|
|
46
|
+
// 2) 常见全局安装布局(本机实测命中第 3 条)
|
|
47
|
+
anchors.push('/usr/lib/node_modules/@deepseek-ai/dsh/package.json')
|
|
48
|
+
anchors.push('/usr/local/lib/node_modules/@deepseek-ai/dsh/package.json')
|
|
49
|
+
// 3) 从 dsh 可执行文件反推(PATH 上的 dsh 多半在 <root>/bin/ 或 <root>/resources/...)
|
|
50
|
+
for (const dir of (process.env.PATH ?? '').split(':')) {
|
|
51
|
+
if (!dir) continue
|
|
52
|
+
anchors.push(`${dir}/../package.json`)
|
|
53
|
+
anchors.push(`${dir}/../node_modules/@deepseek-ai/dsh/package.json`)
|
|
54
|
+
anchors.push(`${dir}/../lib/node_modules/@deepseek-ai/dsh/package.json`)
|
|
55
|
+
}
|
|
56
|
+
// 4) 本机 dsh 默认 home 的邻居(供容器/云机兜底)
|
|
57
|
+
const home = process.env.HOME
|
|
58
|
+
if (home) anchors.push(`${home}/.dsh/../node_modules/@deepseek-ai/dsh/package.json`)
|
|
59
|
+
return anchors.filter((a) => typeof a === 'string' && a.length > 0)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 惰性加载宿主的 `createUserMessage`。
|
|
64
|
+
* @returns {Function}
|
|
65
|
+
* @throws {Error} 所有锚点都失败时(**带排查提示**,不静默)
|
|
66
|
+
*/
|
|
67
|
+
function loadCreateUserMessage() {
|
|
68
|
+
if (_createUserMessage) return _createUserMessage
|
|
69
|
+
if (_loadError) throw _loadError
|
|
70
|
+
|
|
71
|
+
const tried = []
|
|
72
|
+
for (const anchor of dshAnchors()) {
|
|
73
|
+
try {
|
|
74
|
+
// 文件锚点需先存在,createRequire 才不会因怪路径抛错
|
|
75
|
+
if (anchor.startsWith('/') && !existsSync(anchor)) continue
|
|
76
|
+
const req = createRequire(anchor)
|
|
77
|
+
const mod = req(HOST_LLM)
|
|
78
|
+
const fn = mod?.createUserMessage
|
|
79
|
+
if (typeof fn === 'function') {
|
|
80
|
+
_createUserMessage = fn
|
|
81
|
+
return fn
|
|
82
|
+
}
|
|
83
|
+
tried.push(`${anchor}: 已解析但无 createUserMessage 导出`)
|
|
84
|
+
} catch (error) {
|
|
85
|
+
tried.push(`${anchor}: ${error?.code ?? error?.message ?? error}`)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
_loadError = new Error(
|
|
89
|
+
`cs-bots kernel: 无法解析宿主包 ${HOST_LLM}。\n` +
|
|
90
|
+
` 已知锚点尝试:\n ${tried.join('\n ')}\n` +
|
|
91
|
+
` 对策:设 CS_BOTS_DSH_ROOT=<dsh 安装目录>,或用 \`dsh --profile <p>\` 启动(宿主会暴露解析锚)。`,
|
|
92
|
+
)
|
|
93
|
+
throw _loadError
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 惰性取服务 —— **可选依赖**的正确姿势。
|
|
98
|
+
*
|
|
99
|
+
* 三条通路按优先级尝试(2026-11-02 实测,这是**真 bug 的修复**):
|
|
100
|
+
* ① `ctx.reflect.get(key, false)` —— 官方注释原文:
|
|
101
|
+
* "Read a service from the store **without the inject requirement**."
|
|
102
|
+
* ⇒ 这是唯一**不声明 inject 也能读**的服务通道,正是「缺服务就降级」所需。
|
|
103
|
+
* ② `ctx.get(key)` —— 老/测试上下文形态;在真实 Cordis ctx 上会抛
|
|
104
|
+
* `cannot get property "X" without inject`(实测),故必须 try/catch。
|
|
105
|
+
* ③ `key in ctx` 探路 —— 更老宿主可能没有 ctx.reflect,靠 `in` 判存在再取。
|
|
106
|
+
* ④ `ctx[key]` —— 直接挂属性的测试替身;同样会抛,故 try/catch。
|
|
107
|
+
*
|
|
108
|
+
* ⚠️ 教训:不做 try/catch 会让插件在真实宿主里**加载失败**
|
|
109
|
+
* (`Error: cannot get property "webServer" without inject`),
|
|
110
|
+
* 而单元测试用普通对象反而全绿 —— 典型的「测了但没测真」。
|
|
111
|
+
*
|
|
112
|
+
* @param {object} ctx
|
|
113
|
+
* @param {string} key
|
|
114
|
+
* @returns {unknown} 服务实例,或 undefined(缺失/不可读)
|
|
115
|
+
*/
|
|
116
|
+
export function service(ctx, key) {
|
|
117
|
+
if (!ctx || typeof ctx !== 'object') return undefined
|
|
118
|
+
// ① 不要求 inject 的反射读取(首选)
|
|
119
|
+
try {
|
|
120
|
+
const viaReflect = ctx.reflect?.get?.(key, false)
|
|
121
|
+
if (viaReflect !== undefined) return viaReflect
|
|
122
|
+
} catch {
|
|
123
|
+
/* 反射层不可用 → 继续尝试其它通路 */
|
|
124
|
+
}
|
|
125
|
+
// ② ctx.get()
|
|
126
|
+
try {
|
|
127
|
+
const viaGet = ctx.get?.(key)
|
|
128
|
+
if (viaGet !== undefined) return viaGet
|
|
129
|
+
} catch {
|
|
130
|
+
/* 真实 ctx 未声明 inject 时会抛 → 忽略 */
|
|
131
|
+
}
|
|
132
|
+
// ③ `in` 探路 + 直接属性:更老宿主(如 rc1)可能没有 ctx.reflect,
|
|
133
|
+
// 用 `in` 先判存在再取值,可绕开代理 get 陷阱的抛错。
|
|
134
|
+
try {
|
|
135
|
+
if (key in ctx) return ctx[key]
|
|
136
|
+
} catch {
|
|
137
|
+
/* `in` 也被拒 → 继续 */
|
|
138
|
+
}
|
|
139
|
+
// ④ 直接属性(测试替身)
|
|
140
|
+
try {
|
|
141
|
+
return ctx[key]
|
|
142
|
+
} catch {
|
|
143
|
+
return undefined
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── 能力探测 ───────────────────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 探测宿主能力。**缺任一项都不抛错**,调用方按 `available` 决定降级行为。
|
|
151
|
+
* @param {object} ctx
|
|
152
|
+
* @returns {{
|
|
153
|
+
* agents: boolean, subagents: boolean, agentTeams: boolean,
|
|
154
|
+
* webServer: boolean, sandboxPolicy: boolean, sessionPersistence: boolean,
|
|
155
|
+
* missing: string[],
|
|
156
|
+
* }}
|
|
157
|
+
*/
|
|
158
|
+
export function capabilities(ctx) {
|
|
159
|
+
const keys = ['agents', 'subagents', 'agentTeams', 'webServer', 'sandboxPolicy', 'sessionPersistence']
|
|
160
|
+
const missing = []
|
|
161
|
+
const found = {}
|
|
162
|
+
for (const key of keys) {
|
|
163
|
+
const ok = Boolean(service(ctx, key))
|
|
164
|
+
found[key] = ok
|
|
165
|
+
if (!ok) missing.push(key)
|
|
166
|
+
}
|
|
167
|
+
return { ...found, missing }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ── Agent 注册表(live agent 访问)────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 取一个 live Agent 的句柄。
|
|
174
|
+
*
|
|
175
|
+
* 官方语义(`dsh-agent/lib/types/index.d.ts`):`ctx.agents` 是 `AgentRegistry`,
|
|
176
|
+
* 提供 `get(id)` / `list()`。Team 服务的所有方法都要 `caller: Agent`,
|
|
177
|
+
* 所以平台服务层必须先拿到 live Agent —— 本函数就是那条通路。
|
|
178
|
+
*
|
|
179
|
+
* @param {object} ctx
|
|
180
|
+
* @param {string} sessionId
|
|
181
|
+
* @returns {object | undefined} live Agent,或 undefined(服务缺失 / 会话不在)
|
|
182
|
+
*/
|
|
183
|
+
export function getAgent(ctx, sessionId) {
|
|
184
|
+
const agents = service(ctx, 'agents')
|
|
185
|
+
if (!agents || typeof agents.get !== 'function') return undefined
|
|
186
|
+
try {
|
|
187
|
+
return agents.get(sessionId) ?? undefined
|
|
188
|
+
} catch {
|
|
189
|
+
return undefined
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* 列出全部 live Agent。
|
|
195
|
+
* @param {object} ctx
|
|
196
|
+
* @returns {object[]} 空数组表示服务缺失或无 agent(**不区分**,调用方按需再探测)
|
|
197
|
+
*/
|
|
198
|
+
export function listAgents(ctx) {
|
|
199
|
+
const agents = service(ctx, 'agents')
|
|
200
|
+
if (!agents || typeof agents.list !== 'function') return []
|
|
201
|
+
try {
|
|
202
|
+
const list = agents.list()
|
|
203
|
+
return Array.isArray(list) ? list : []
|
|
204
|
+
} catch {
|
|
205
|
+
return []
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ── 投递(唤醒)─────────────────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 消息来源标记 —— 官方 `UserMessage.source` 需要它。
|
|
213
|
+
* 未经标识的消息在会话日志里无法追溯,故统一走这里。
|
|
214
|
+
*/
|
|
215
|
+
const SOURCE = { kind: 'plugin', name: 'cs-bots' }
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* 构造一条可投递的用户消息。**不投递**,只构造(便于幂等键先记账再发送)。
|
|
219
|
+
* 宿主包解析失败时抛错(调用方需知道失败原因,这里不降级)。
|
|
220
|
+
* @param {string} text
|
|
221
|
+
* @returns {object} 冻结的 UserMessage
|
|
222
|
+
*/
|
|
223
|
+
export function buildMessage(text) {
|
|
224
|
+
const createUserMessage = loadCreateUserMessage()
|
|
225
|
+
return createUserMessage({
|
|
226
|
+
content: [{ type: 'text', text: String(text ?? '') }],
|
|
227
|
+
source: SOURCE,
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** 宿主包是否可解析(诊断用,**不抛错**)。 */
|
|
232
|
+
export function hostLlmAvailable() {
|
|
233
|
+
try {
|
|
234
|
+
loadCreateUserMessage()
|
|
235
|
+
return true
|
|
236
|
+
} catch {
|
|
237
|
+
return false
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* 投递一条消息并唤醒目标会话(`followup` 语义:**新开一个回合**)。
|
|
243
|
+
*
|
|
244
|
+
* 官方语义原文(`dsh-agent/lib/types/runtime-types.d.ts:191-193`):
|
|
245
|
+
* "Queue an ordinary follow-up turn and wake the driver. The item becomes the
|
|
246
|
+
* sole ordinary message of its own turn."
|
|
247
|
+
* ⇒ 适合「有新活要干」的唤醒。
|
|
248
|
+
*
|
|
249
|
+
* @param {object} ctx
|
|
250
|
+
* @param {string} sessionId
|
|
251
|
+
* @param {string} text
|
|
252
|
+
* @returns {{ ok: true } | { ok: false, reason: string }}
|
|
253
|
+
*/
|
|
254
|
+
export function followup(ctx, sessionId, text) {
|
|
255
|
+
const agent = getAgent(ctx, sessionId)
|
|
256
|
+
if (!agent) return { ok: false, reason: 'agent-not-live' }
|
|
257
|
+
if (typeof agent.followup !== 'function') return { ok: false, reason: 'followup-unsupported' }
|
|
258
|
+
try {
|
|
259
|
+
agent.followup(buildMessage(text))
|
|
260
|
+
return { ok: true }
|
|
261
|
+
} catch (error) {
|
|
262
|
+
return { ok: false, reason: `followup-failed:${error?.message ?? error}` }
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* 把消息插入目标会话的**最近步骤边界**(`steer` 语义)。
|
|
268
|
+
*
|
|
269
|
+
* 官方语义原文(`:194-199`):"Submit steering for the nearest step. An idle driver
|
|
270
|
+
* starts a turn; a running driver consumes it at its next step boundary."
|
|
271
|
+
* ⇒ 适合「正在干活时插一句话」,不新开回合。
|
|
272
|
+
*
|
|
273
|
+
* @param {object} ctx
|
|
274
|
+
* @param {string} sessionId
|
|
275
|
+
* @param {string} text
|
|
276
|
+
* @returns {{ ok: true } | { ok: false, reason: string }}
|
|
277
|
+
*/
|
|
278
|
+
export function steer(ctx, sessionId, text) {
|
|
279
|
+
const agent = getAgent(ctx, sessionId)
|
|
280
|
+
if (!agent) return { ok: false, reason: 'agent-not-live' }
|
|
281
|
+
if (typeof agent.steer !== 'function') return { ok: false, reason: 'steer-unsupported' }
|
|
282
|
+
try {
|
|
283
|
+
agent.steer(buildMessage(text))
|
|
284
|
+
return { ok: true }
|
|
285
|
+
} catch (error) {
|
|
286
|
+
return { ok: false, reason: `steer-failed:${error?.message ?? error}` }
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ── 会话创建(**唯一能指定 cwd 的路径**)──────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* 创建一个 root Agent 会话,**可指定工作目录**。
|
|
294
|
+
*
|
|
295
|
+
* ⚠️ 这是本平台「共用工作目录」问题的**唯一解法**,务必不要用官方
|
|
296
|
+
* `spawnTeammate` 代替(已实测:`SpawnTeammateRequest` 无 `cwd` 字段,
|
|
297
|
+
* 且 teammate 强制继承父 cwd —— 见 `11-融合最终稿-v1.md` §0.1)。
|
|
298
|
+
*
|
|
299
|
+
* 官方语义(`dsh-agent/lib/types/index.d.ts:48-90`):`CreateAgentOptions.meta.cwd`
|
|
300
|
+
* 是 "validated absolute `cwd`",且 **"This is durable session data"**。
|
|
301
|
+
*
|
|
302
|
+
* @param {object} ctx
|
|
303
|
+
* @param {{ sessionId: string, cwd?: string, parentAgent?: object, agentPreset?: string,
|
|
304
|
+
* agentOptions?: object, signal?: AbortSignal }} options
|
|
305
|
+
* @returns {Promise<object>} AgentHandle
|
|
306
|
+
* @throws {Error} 服务缺失或创建失败(**这一条不降级**:调用方需要知道失败原因)
|
|
307
|
+
*/
|
|
308
|
+
export async function createSession(ctx, options) {
|
|
309
|
+
const agents = service(ctx, 'agents')
|
|
310
|
+
if (!agents || typeof agents.create !== 'function') {
|
|
311
|
+
throw new Error('cs-bots kernel: agents.create 不可用(宿主未提供 agents 服务)')
|
|
312
|
+
}
|
|
313
|
+
const { sessionId, cwd, parentAgent, agentPreset, agentOptions, signal } = options ?? {}
|
|
314
|
+
if (!sessionId) throw new Error('cs-bots kernel: createSession 需要 sessionId')
|
|
315
|
+
|
|
316
|
+
const meta = {}
|
|
317
|
+
if (cwd) meta.cwd = cwd
|
|
318
|
+
if (agentPreset) meta.agentPreset = agentPreset
|
|
319
|
+
if (parentAgent) meta.parentSession = parentAgent.id
|
|
320
|
+
|
|
321
|
+
return agents.create({
|
|
322
|
+
sessionId,
|
|
323
|
+
...(parentAgent ? { parentAgent } : {}),
|
|
324
|
+
...(Object.keys(meta).length ? { meta } : {}),
|
|
325
|
+
...(agentOptions ? { agentOptions } : {}),
|
|
326
|
+
...(signal ? { signal } : {}),
|
|
327
|
+
})
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* 在既有持久会话上恢复 Agent(重启后复活会话用)。
|
|
332
|
+
* @param {object} ctx
|
|
333
|
+
* @param {{ resumeSessionId: string, signal?: AbortSignal }} options
|
|
334
|
+
* @returns {Promise<object>} AgentHandle
|
|
335
|
+
*/
|
|
336
|
+
export async function resumeSession(ctx, options) {
|
|
337
|
+
const agents = service(ctx, 'agents')
|
|
338
|
+
if (!agents || typeof agents.resume !== 'function') {
|
|
339
|
+
throw new Error('cs-bots kernel: agents.resume 不可用')
|
|
340
|
+
}
|
|
341
|
+
const { resumeSessionId, signal } = options ?? {}
|
|
342
|
+
if (!resumeSessionId) throw new Error('cs-bots kernel: resumeSession 需要 resumeSessionId')
|
|
343
|
+
return agents.resume({ resumeSessionId, ...(signal ? { signal } : {}) })
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ── HTTP 路由注册 ───────────────────────────────────────────────────────────
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* 在宿主 webServer 上注册一条前缀路由(平台 API 面)。
|
|
350
|
+
* 缺 webServer 时返回 `undefined`(降级:平台无 HTTP 面,但服务照常工作)。
|
|
351
|
+
*
|
|
352
|
+
* @param {object} ctx
|
|
353
|
+
* @param {string} path 必须以 `/` 开头
|
|
354
|
+
* @param {(req: object, res: object) => void | Promise<void>} handler
|
|
355
|
+
* @returns {(() => void) | undefined} disposer
|
|
356
|
+
*/
|
|
357
|
+
export function registerRoute(ctx, path, handler) {
|
|
358
|
+
const webServer = service(ctx, 'webServer')
|
|
359
|
+
if (!webServer || typeof webServer.register !== 'function') return undefined
|
|
360
|
+
return webServer.register({ kind: 'prefix', path, handler })
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ── 工具注册(模型面)────────────────────────────────────────────────────────
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* 注册一条模型可见工具。
|
|
367
|
+
*
|
|
368
|
+
* 官方契约(`dsh-tools/lib/types/index.d.ts:115-146`):
|
|
369
|
+
* `ToolDefinition = ToolSchema + { output: { schema, render }, execute(args, exec) }`
|
|
370
|
+
* —— `output` 是**强制**的,且 `execute` 只返回**规范无损 JSON 值**(渲染交给 `render`)。
|
|
371
|
+
*
|
|
372
|
+
* 缺 tools 服务时返回 `undefined`(降级:模型看不到该工具,但域服务照常工作)。
|
|
373
|
+
*
|
|
374
|
+
* @param {object} ctx
|
|
375
|
+
* @param {{ name: string, description: string, parameters: object,
|
|
376
|
+
* outputSchema: object, render: Function, execute: Function }} spec
|
|
377
|
+
* @returns {(() => void) | undefined} disposer
|
|
378
|
+
*/
|
|
379
|
+
export function registerTool(ctx, spec) {
|
|
380
|
+
const tools = service(ctx, 'tools')
|
|
381
|
+
if (!tools || typeof tools.register !== 'function') return undefined
|
|
382
|
+
const { name, description, parameters, outputSchema, render, execute } = spec ?? {}
|
|
383
|
+
if (!name || typeof execute !== 'function') {
|
|
384
|
+
throw new Error('cs-bots kernel.registerTool: 需要 name 与 execute')
|
|
385
|
+
}
|
|
386
|
+
return tools.register({
|
|
387
|
+
name,
|
|
388
|
+
description: description ?? '',
|
|
389
|
+
parameters: parameters ?? { type: 'object', properties: {}, additionalProperties: false },
|
|
390
|
+
output: {
|
|
391
|
+
schema: outputSchema ?? { type: 'object' },
|
|
392
|
+
render:
|
|
393
|
+
typeof render === 'function'
|
|
394
|
+
? render
|
|
395
|
+
: (_args, value) => [{ type: 'text', text: JSON.stringify(value ?? null, null, 2) }],
|
|
396
|
+
},
|
|
397
|
+
execute,
|
|
398
|
+
})
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* 装一条全局工具执行闸门(K1 用)。缺 tools 服务时返回 `undefined`。
|
|
403
|
+
* @param {object} ctx
|
|
404
|
+
* @param {(exec: object) => string | undefined} guard 返回字符串=拒绝执行(作为理由)
|
|
405
|
+
* @returns {(() => void) | undefined}
|
|
406
|
+
*/
|
|
407
|
+
export function guardTool(ctx, guard) {
|
|
408
|
+
const tools = service(ctx, 'tools')
|
|
409
|
+
if (!tools || typeof tools.guard !== 'function') return undefined
|
|
410
|
+
return tools.guard(guard)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* 从某个 agent 的工具表中移除工具名(K1 双保险之二)。
|
|
415
|
+
* @param {object} agent
|
|
416
|
+
* @param {string[]} names
|
|
417
|
+
* @returns {boolean} 是否施加成功
|
|
418
|
+
*/
|
|
419
|
+
export function restrictTools(agent, names) {
|
|
420
|
+
const agentTools = agent?.ctx?.tools
|
|
421
|
+
if (typeof agentTools?.restrict !== 'function') return false
|
|
422
|
+
try {
|
|
423
|
+
// restrict() 对「本 profile 不提供的工具名」会抛错 → 先与实际工具表求交集
|
|
424
|
+
const served = typeof agentTools.schemas === 'function'
|
|
425
|
+
? new Set(agentTools.schemas(agent).map((schema) => schema?.name))
|
|
426
|
+
: undefined
|
|
427
|
+
const deny = served ? names.filter((n) => served.has(n)) : names
|
|
428
|
+
if (deny.length === 0) return false
|
|
429
|
+
agentTools.restrict({ deny })
|
|
430
|
+
return true
|
|
431
|
+
} catch {
|
|
432
|
+
return false
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// ── 状态目录 ────────────────────────────────────────────────────────────────
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* 平台状态目录:`$DSH_HOME/cs-bots/`。
|
|
440
|
+
* @returns {string}
|
|
441
|
+
*/
|
|
442
|
+
export function stateDir() {
|
|
443
|
+
const home = process.env.DSH_HOME || `${process.env.HOME ?? '.'}/.dsh`
|
|
444
|
+
return `${home}/cs-bots`
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* 列出可用 agent 预设(**缺服务返回空数组,不抛**)。
|
|
449
|
+
*
|
|
450
|
+
* 预设是什么:一组「模型 + 系统提示 + 技能 + 工具白名单」的声明式组合。
|
|
451
|
+
* 同一个 DSH 里可以有 dev-steward / 普通助手 / 记忆整理 等多个预设。
|
|
452
|
+
* Bot 建会话时把 preset 传进 `meta.agentPreset`,新会话就用那套人格与工具面。
|
|
453
|
+
*
|
|
454
|
+
* @param {object} ctx
|
|
455
|
+
* @returns {Promise<Array<{id:string,name?:string,description?:string,order?:number,broken?:string}>>}
|
|
456
|
+
*/
|
|
457
|
+
export async function listPresets(ctx) {
|
|
458
|
+
const registry = service(ctx, 'agentPresets')
|
|
459
|
+
if (!registry || typeof registry.list !== 'function') return []
|
|
460
|
+
try {
|
|
461
|
+
const rows = await registry.list()
|
|
462
|
+
return Array.isArray(rows) ? rows : []
|
|
463
|
+
} catch {
|
|
464
|
+
return []
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* 取默认预设 id(缺服务返回 undefined)。
|
|
470
|
+
* @param {object} ctx
|
|
471
|
+
* @returns {string | undefined}
|
|
472
|
+
*/
|
|
473
|
+
export function defaultPreset(ctx) {
|
|
474
|
+
const registry = service(ctx, 'agentPresets')
|
|
475
|
+
try {
|
|
476
|
+
return typeof registry?.defaultId === 'string' ? registry.defaultId : undefined
|
|
477
|
+
} catch {
|
|
478
|
+
return undefined
|
|
479
|
+
}
|
|
480
|
+
}
|