cs-bots 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/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/index.js
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cs-bots 宿主半身 —— 多智能体协作平台。
|
|
3
|
+
*
|
|
4
|
+
* 设计纪律(由 test/ci-zero-dep.sh 机械强制):
|
|
5
|
+
* - **零第三方依赖**:不 import 任何非 `@deepseek-ai/*` 的包;`dependencies` 为空。
|
|
6
|
+
* - **kernel.js 单点**:与 DSH 宿主的耦合只允许出现在 `src/kernel.js`;
|
|
7
|
+
* 本文件及后续业务文件一律从 kernel.js 取宿主能力。
|
|
8
|
+
* - **缺服务只记日志、绝不阻塞启动**:所有服务惰性获取 + 降级。
|
|
9
|
+
*
|
|
10
|
+
* 当前阶段:P0–P5 全域服务 + 模型面工具。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createBoard } from './board.js'
|
|
14
|
+
import { createDispatcher } from './dispatcher.js'
|
|
15
|
+
import { createRoster } from './roster.js'
|
|
16
|
+
import { createRoom } from './room.js'
|
|
17
|
+
import { createMemory } from './memory.js'
|
|
18
|
+
import { installTools } from './tools.js'
|
|
19
|
+
import { setServices } from './registry.js'
|
|
20
|
+
import { service, stateDir as kernelStateDir } from './kernel.js'
|
|
21
|
+
import { installApi } from './api.js'
|
|
22
|
+
import { createLibrary } from './library.js'
|
|
23
|
+
import { createDirBrowser } from './dirbrowse.js'
|
|
24
|
+
import { loadSettings } from './settings.js'
|
|
25
|
+
import { probeHost, formatReport } from './compat.js'
|
|
26
|
+
|
|
27
|
+
export const name = 'cs-bots'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Bot 会话中必须从工具表移除的工具(K1)。
|
|
31
|
+
*
|
|
32
|
+
* 一手依据(ds-bot 已踩过此坑,`ds-bot/src/host/tools.js:708-713` 注释原文):
|
|
33
|
+
* "dsh's own question tool holds the turn open until the user answers, and every
|
|
34
|
+
* message from another Bot or a group round queues behind it."
|
|
35
|
+
* ⇒ 在多智能体结构里,**同步等用户的提问工具会堵住整条投递链**,是结构性反模式。
|
|
36
|
+
*/
|
|
37
|
+
const BLOCKING_QUESTION_TOOL = 'ask_user_question'
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 插件入口 —— **绝不抛错**。
|
|
41
|
+
*
|
|
42
|
+
* 为什么必须这样(真实教训):宿主在 `apply()` 抛错时**整个 DSH 起不来**,
|
|
43
|
+
* 用户只能「禁用插件才能打开」。所以这里做最后一道兜底:
|
|
44
|
+
* 任何漏网的异常都只记日志,**不向外抛**。
|
|
45
|
+
* 代价是"静默降级",但绝不让一个插件拖垮整个宿主。
|
|
46
|
+
*
|
|
47
|
+
* @param {object} ctx
|
|
48
|
+
* @param {object} [config]
|
|
49
|
+
*/
|
|
50
|
+
export function apply(ctx, config = {}) {
|
|
51
|
+
let log = () => {}
|
|
52
|
+
let warn = () => {}
|
|
53
|
+
try {
|
|
54
|
+
log = (...args) => ctx?.logger?.info?.(...args)
|
|
55
|
+
warn = (...args) => ctx?.logger?.warn?.(...args)
|
|
56
|
+
} catch {
|
|
57
|
+
/* 连 logger 都取不到 → 用空实现 */
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
applyInner(ctx, config, { log, warn })
|
|
62
|
+
} catch (error) {
|
|
63
|
+
// 最后一道防线:不让插件把宿主启动搞崩
|
|
64
|
+
try {
|
|
65
|
+
warn(`[cs-bots] 装配异常已被兜底(插件降级,DSH 可正常启动):${error?.message ?? error}`)
|
|
66
|
+
warn(`[cs-bots] 若需完整功能,请把这段错误发回开发者:${String(error?.stack ?? '').split('\n').slice(0, 6).join(' | ')}`)
|
|
67
|
+
} catch {
|
|
68
|
+
/* 日志也失败 → 完全静默,但宿主安全 */
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function applyInner(ctx, config, { log, warn }) {
|
|
74
|
+
log('[cs-bots] 装配中')
|
|
75
|
+
|
|
76
|
+
// ── 宿主兼容性探测(**先探测再装配**;探测本身不抛)──────────────────────
|
|
77
|
+
let compat
|
|
78
|
+
try {
|
|
79
|
+
compat = probeHost(ctx, { warn })
|
|
80
|
+
log(`[cs-bots] 宿主兼容性:${formatReport(compat)}`)
|
|
81
|
+
if (compat.notes?.length) for (const n of compat.notes) warn(`[cs-bots] ${n}`)
|
|
82
|
+
} catch (error) {
|
|
83
|
+
warn(`[cs-bots] 兼容性探测失败(继续装配):${error?.message ?? error}`)
|
|
84
|
+
compat = { dshVersion: undefined, node: 'unknown', available: {}, missing: [], notes: [] }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── L2 域服务:一律惰性装配、各自降级,**任一失败都不阻塞启动** ──────────────
|
|
88
|
+
// ⚠️ 两个域的构造签名不同(board 是 `(ctx, config)`,dispatcher 也是 `(ctx, config)`);
|
|
89
|
+
// 日志一律用**字符串拼接**而非 `%s` 占位(避免形态差异导致格式串未插值)。
|
|
90
|
+
const services = {}
|
|
91
|
+
services.compat = compat
|
|
92
|
+
|
|
93
|
+
// ── 设置:**先读,再拿去装配各域**(改设置才真的改变行为)────────────────
|
|
94
|
+
const settingsDir = config.stateDir ?? kernelStateDir()
|
|
95
|
+
const loaded = loadSettings(settingsDir)
|
|
96
|
+
const settings = loaded.values
|
|
97
|
+
if (loaded.diagnostics.badKeys.length) {
|
|
98
|
+
warn(`[cs-bots] 设置文件有 ${loaded.diagnostics.badKeys.length} 个无效项已忽略:${JSON.stringify(loaded.diagnostics.badKeys)}`)
|
|
99
|
+
}
|
|
100
|
+
services.settings = { ...settings }
|
|
101
|
+
services.settingsDiagnostics = loaded.diagnostics
|
|
102
|
+
log(`[cs-bots] 设置来源=${loaded.diagnostics.source}(${Object.keys(settings).length} 项)`)
|
|
103
|
+
|
|
104
|
+
// RosterService 先装配:K1 闸门需要它判定「哪些会话是 Bot」。
|
|
105
|
+
try {
|
|
106
|
+
services.roster = createRoster(ctx, { stateDir: config.stateDir, leaseMs: settings.defaultLeaseMs })
|
|
107
|
+
log(`[cs-bots] RosterService 就绪(stateDir=${services.roster.paths?.stateDir ?? '?'},durable=${services.roster.durable})`)
|
|
108
|
+
} catch (error) {
|
|
109
|
+
warn(`[cs-bots] RosterService 装配失败 → 降级(Bot 注册表不可用,K1 保守放行):${error?.message ?? error}`)
|
|
110
|
+
services.roster = undefined
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
services.board = createBoard(ctx, { stateDir: config.stateDir, leaseMs: settings.defaultLeaseMs })
|
|
115
|
+
log(`[cs-bots] BoardService 就绪(stateDir=${services.board.paths?.stateDir ?? '?'})`)
|
|
116
|
+
} catch (error) {
|
|
117
|
+
warn(`[cs-bots] BoardService 装配失败 → 降级(无任务板):${error?.message ?? error}`)
|
|
118
|
+
services.board = undefined
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
services.dispatcher = createDispatcher(ctx, { stateDir: config.stateDir, throttleMs: settings.throttleMs, maxPerTarget: settings.maxWakesPerTarget, queueTtlMs: settings.wakeTtlMs })
|
|
123
|
+
if (services.dispatcher.durable === false) {
|
|
124
|
+
warn(`[cs-bots] Dispatcher 状态目录不可写 → 唤醒记录不持久:${services.dispatcher.probeError ?? ''}`)
|
|
125
|
+
}
|
|
126
|
+
log(`[cs-bots] Dispatcher 就绪(durable=${services.dispatcher.durable})`)
|
|
127
|
+
} catch (error) {
|
|
128
|
+
warn(`[cs-bots] Dispatcher 装配失败 → 降级(不自动唤醒):${error?.message ?? error}`)
|
|
129
|
+
services.dispatcher = undefined
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
services.room = createRoom(ctx, { stateDir: config.stateDir })
|
|
134
|
+
log(`[cs-bots] RoomService 就绪(stateDir=${services.room.paths?.stateDir ?? '?'})`)
|
|
135
|
+
} catch (error) {
|
|
136
|
+
warn(`[cs-bots] RoomService 装配失败 → 降级(无房间/群聊):${error?.message ?? error}`)
|
|
137
|
+
services.room = undefined
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
services.memory = createMemory(ctx, { stateDir: config.stateDir, entryMax: settings.memoryEntryMax, splitOnOverflow: settings.memorySplitOnOverflow })
|
|
142
|
+
log(`[cs-bots] MemoryService 就绪(stateDir=${services.memory.paths?.stateDir ?? '?'})`)
|
|
143
|
+
} catch (error) {
|
|
144
|
+
warn(`[cs-bots] MemoryService 装配失败 → 降级(无分层记忆):${error?.message ?? error}`)
|
|
145
|
+
services.memory = undefined
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (settings.k1Enabled) {
|
|
149
|
+
installBlockingToolGate(ctx, { warn, roster: services.roster })
|
|
150
|
+
installBotToolTrimmer(ctx, { warn, roster: services.roster })
|
|
151
|
+
} else {
|
|
152
|
+
warn('[cs-bots] K1 已按设置关闭 → Bot 会话可用 ask_user_question(有堵投递链风险)')
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── 模型面:注册全部工具(**没有这一层,新机器上装完插件模型看不到任何能力**)─
|
|
156
|
+
// 必须在域服务就绪之后注册:工具执行时从 `ctx.csBots` 取服务。
|
|
157
|
+
// ⚠️ 不能 `ctx.csBots = services`:Cordis ctx 禁止任意属性(实测报
|
|
158
|
+
// `cannot set property "csBots" without provide`)→ 走 WeakMap 注册表。
|
|
159
|
+
setServices(ctx, services)
|
|
160
|
+
// ── 目录浏览(自建,弥补原生选择器在 Linux/沙箱里不可用)────────────────
|
|
161
|
+
try {
|
|
162
|
+
services.dirbrowse = createDirBrowser({
|
|
163
|
+
stateDir: config.stateDir ?? kernelStateDir(),
|
|
164
|
+
dshHome: process.env.DSH_HOME,
|
|
165
|
+
})
|
|
166
|
+
log('[cs-bots] dirbrowse 就绪')
|
|
167
|
+
} catch (error) {
|
|
168
|
+
warn(`[cs-bots] dirbrowse 装配失败(目录浏览不可用):${error?.message ?? error}`)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── 技能库 / 工作流库(全局可复用资产,Bot 与任务可关联)──────────────
|
|
172
|
+
try {
|
|
173
|
+
services.library = createLibrary(ctx, { stateDir: config.stateDir ?? kernelStateDir() })
|
|
174
|
+
log('[cs-bots] library 就绪')
|
|
175
|
+
} catch (error) {
|
|
176
|
+
warn(`[cs-bots] library 装配失败(技能库/工作流库不可用):${error?.message ?? error}`)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── 页面数据面:HTTP API(页面层靠它拿数据;没有它 UI 是空壳)──────────────
|
|
180
|
+
services.apiDispose = installApi(ctx, { warn })
|
|
181
|
+
|
|
182
|
+
const toolNames = installTools(ctx, { warn })
|
|
183
|
+
if (toolNames.length === 0) {
|
|
184
|
+
warn('[cs-bots] 未注册任何工具(tools 服务不可用?)→ 模型将看不到平台能力')
|
|
185
|
+
} else {
|
|
186
|
+
log(`[cs-bots] 已注册 ${toolNames.length} 个工具:${toolNames.join(', ')}`)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── 生命周期收尾:停止定时器,避免进程退出被挂起的 timer 拖住 ────────────────
|
|
190
|
+
// 用官方 ctx.effect(若可用)保证插件卸载时被调用;不可用则退化为不注册。
|
|
191
|
+
const dispose = () => {
|
|
192
|
+
try {
|
|
193
|
+
services.dispatcher?.dispose?.()
|
|
194
|
+
try { services.apiDispose?.() } catch { /* 已卸载 */ }
|
|
195
|
+
} catch (error) {
|
|
196
|
+
warn(`[cs-bots] dispatcher dispose 失败:${error?.message ?? error}`)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (typeof ctx.effect === 'function') {
|
|
200
|
+
try {
|
|
201
|
+
ctx.effect(() => dispose, 'cs-bots:dispose')
|
|
202
|
+
} catch {
|
|
203
|
+
/* 注册失败不阻塞 */
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
log('[cs-bots] 装配完成')
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 装 K1 闸门(全局 guard,双保险之一)。
|
|
212
|
+
*
|
|
213
|
+
* 为什么不在这里直接 restrict:`tools.guard` 是**同步闸门**,任何时刻都拦得住;
|
|
214
|
+
* 而 `restrict({deny})` 让工具**不出现在模型的工具表**里(省 token、模型不会误调),
|
|
215
|
+
* 需要在 `agent/created` 时按 Bot 身份施加 —— 那要等 RosterService(P5)判定
|
|
216
|
+
* 「哪些会话是 Bot」。当前阶段**保守判定**,不误伤人类会话与官方 teammate。
|
|
217
|
+
*/
|
|
218
|
+
function installBlockingToolGate(ctx, { warn, roster }) {
|
|
219
|
+
const tools = service(ctx, 'tools')
|
|
220
|
+
if (!tools || typeof tools.guard !== 'function') {
|
|
221
|
+
warn('[cs-bots] tools 服务不可用 → K1 闸门未装(降级,不阻塞启动)')
|
|
222
|
+
return undefined
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
return tools.guard((exec) => {
|
|
226
|
+
if (exec?.name !== BLOCKING_QUESTION_TOOL) return undefined
|
|
227
|
+
if (!isBotSession(exec?.agent, roster)) return undefined
|
|
228
|
+
return `Bot 会话不能使用 ${BLOCKING_QUESTION_TOOL}(会堵住整条多智能体投递链)。请在回复里直接提问。`
|
|
229
|
+
})
|
|
230
|
+
} catch (error) {
|
|
231
|
+
warn(`[cs-bots] 装 K1 闸门失败:${error?.message ?? error}`)
|
|
232
|
+
return undefined
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* 判定一个 agent 是否属于本平台的 Bot。
|
|
238
|
+
*
|
|
239
|
+
* P5 起接入 RosterService:按 **sessionId** 查注册表(口径 = 「该会话已被注册为 Bot 且未退役」)。
|
|
240
|
+
* 无 roster(降级)或查不到时**保守返回 false** —— 宁可漏拦,也不误伤人类会话与官方 teammate。
|
|
241
|
+
*
|
|
242
|
+
* @param {unknown} agent
|
|
243
|
+
* @param {{ list?: Function } | undefined} roster
|
|
244
|
+
* @returns {boolean}
|
|
245
|
+
*/
|
|
246
|
+
function isBotSession(agent, roster) {
|
|
247
|
+
const sid = agent?.id ?? agent?.session?.id
|
|
248
|
+
if (!sid || !roster || typeof roster.list !== 'function') return false
|
|
249
|
+
try {
|
|
250
|
+
return roster.list().some((bot) => bot?.sessionId === sid)
|
|
251
|
+
} catch {
|
|
252
|
+
return false
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* 把阻塞式提问工具从某个 Bot 会话的工具表中彻底移除(双保险之二)。
|
|
258
|
+
* 由 `agent/created` 监听器在确认身份后调用。
|
|
259
|
+
*
|
|
260
|
+
* @param {{ ctx?: { tools?: { restrict?: Function, schemas?: Function } } }} agent
|
|
261
|
+
*/
|
|
262
|
+
export function hideBlockingToolsFromBot(agent, { warn } = {}) {
|
|
263
|
+
if (!agent || typeof agent !== 'object') return
|
|
264
|
+
const agentTools = agent.ctx?.tools
|
|
265
|
+
if (typeof agentTools?.restrict !== 'function') return
|
|
266
|
+
try {
|
|
267
|
+
// restrict() 对「本 profile 不提供的工具名」会抛错 → 先与实际工具表求交集。
|
|
268
|
+
const served = typeof agentTools.schemas === 'function'
|
|
269
|
+
? new Set(agentTools.schemas(agent).map((schema) => schema?.name))
|
|
270
|
+
: undefined
|
|
271
|
+
const deny = served
|
|
272
|
+
? [BLOCKING_QUESTION_TOOL].filter((n) => served.has(n))
|
|
273
|
+
: [BLOCKING_QUESTION_TOOL]
|
|
274
|
+
if (deny.length === 0) return
|
|
275
|
+
agentTools.restrict({ deny })
|
|
276
|
+
} catch (error) {
|
|
277
|
+
warn?.(`[cs-bots] 移除 Bot 的 ${BLOCKING_QUESTION_TOOL} 失败:${error?.message ?? error}`)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* 装 `agent/created` 监听器(K1 双保险之二)。
|
|
283
|
+
*
|
|
284
|
+
* 官方语义原文(`ds-bot/src/host/tools.js:736` 注释):
|
|
285
|
+
* "Creation waits for this listener, so the Agent's first step already lacks the tool."
|
|
286
|
+
* ⇒ 在**第一个 step 之前**就把工具摘掉,模型既看不到也调不到(省 token)。
|
|
287
|
+
*
|
|
288
|
+
* 缺 `ctx.on` 或 roster 时**只 warn 不抛**(降级安全)。
|
|
289
|
+
*/
|
|
290
|
+
function installBotToolTrimmer(ctx, { warn, roster }) {
|
|
291
|
+
if (typeof ctx.on !== 'function') {
|
|
292
|
+
warn('[cs-bots] ctx.on 不可用 → K1 工具摘除未装(仅靠 guard 兜底)')
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
if (!roster) {
|
|
296
|
+
warn('[cs-bots] 无 RosterService → K1 工具摘除未装(仅靠 guard 兜底)')
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
ctx.on('agent/created', ({ agent }) => {
|
|
301
|
+
try {
|
|
302
|
+
if (!isBotSession(agent, roster)) return
|
|
303
|
+
hideBlockingToolsFromBot(agent, { warn })
|
|
304
|
+
} catch (error) {
|
|
305
|
+
warn(`[cs-bots] agent/created 处理失败:${error?.message ?? error}`)
|
|
306
|
+
}
|
|
307
|
+
})
|
|
308
|
+
} catch (error) {
|
|
309
|
+
warn(`[cs-bots] 注册 agent/created 失败:${error?.message ?? error}`)
|
|
310
|
+
}
|
|
311
|
+
}
|