cs-bots 0.1.1 → 0.7.5
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/CHANGELOG.md +539 -0
- package/README.md +65 -10
- package/docs/01-/346/236/266/346/236/204/346/226/207/346/241/243.md +364 -0
- package/docs/02-/351/234/200/346/261/202/350/247/204/346/240/274/344/271/246.md +284 -0
- package/docs/03-/350/257/246/347/273/206/350/256/276/350/256/241.md +567 -0
- package/docs/04-/346/223/215/344/275/234/346/211/213/345/206/214.md +569 -0
- package/docs/ARCHITECTURE.md +277 -0
- package/docs/adr/ADR-001-/351/241/265/347/255/276/347/273/223/346/236/204/345/233/236/345/210/260/344/270/203/351/241/265.md +70 -0
- package/docs/steps/step-01-/345/216/237/346/236/266/346/236/204/345/244/215/345/216/237/344/270/216IA/350/257/212/346/226/255.md +80 -0
- package/docs/steps/step-02-/351/241/265/347/255/276/346/224/266/346/225/233/344/270/216/345/215/207/347/272/247/351/200/232/351/201/223.md +51 -0
- package/docs/steps/step-03-/346/236/266/346/236/204/350/265/204/346/226/231/346/210/220/346/226/207.md +16 -0
- package/docs/steps/step-04-/344/270/212/347/272/277/344/270/216/351/252/214/350/257/201.md +46 -0
- package/docs//344/272/244/344/273/230/350/257/264/346/230/216-v0.2.0.md +95 -0
- package/docs//346/223/215/344/275/234/346/211/213/345/206/214-v1.md +509 -0
- package/docs//350/277/201/347/247/273/346/226/271/346/241/210-v1.md +64 -0
- package/docs//351/225/277/346/234/237/350/256/260/345/277/206/346/226/271/346/241/210-v1.md +85 -0
- package/package.json +4 -2
- package/src/admission.js +365 -0
- package/src/api.js +4097 -420
- package/src/board.js +883 -14
- package/src/botprofile.js +190 -3
- package/src/client.js +6596 -1082
- package/src/cycle.js +516 -0
- package/src/delivery-watch.js +255 -0
- package/src/dirbrowse.js +20 -0
- package/src/discipline.js +1188 -0
- package/src/fence.js +142 -0
- package/src/index.js +387 -4
- package/src/kernel.js +636 -24
- package/src/library.js +1224 -11
- package/src/memory.js +617 -37
- package/src/metrics.js +339 -0
- package/src/prompts.js +1318 -0
- package/src/reviewer.js +709 -0
- package/src/room.js +655 -14
- package/src/roster.js +346 -26
- package/src/routing/complexity.js +320 -0
- package/src/routing/hints.js +707 -0
- package/src/scratch.js +477 -0
- package/src/settings.js +97 -0
- package/src/stage.js +263 -0
- package/src/store.js +133 -1
- package/src/subscription.js +571 -0
- package/src/suggest.js +1203 -0
- package/src/teams.js +527 -0
- package/src/todo.js +1301 -0
- package/src/tools.js +734 -10
- package/src/verify.js +356 -0
package/src/kernel.js
CHANGED
|
@@ -27,11 +27,14 @@ import { createRequire } from 'node:module'
|
|
|
27
27
|
import { existsSync } from 'node:fs'
|
|
28
28
|
|
|
29
29
|
const HOST_LLM = '@deepseek-ai/dsh-llm'
|
|
30
|
+
const HOST_AGENT = '@deepseek-ai/dsh-agent'
|
|
30
31
|
|
|
31
32
|
/** @type {Function | null} */
|
|
32
33
|
let _createUserMessage = null
|
|
33
34
|
/** @type {Error | null} */
|
|
34
35
|
let _loadError = null
|
|
36
|
+
/** 宿主模块缓存:`spec → module | Error`(同一个 spec 只解析一次,失败也缓存)。 */
|
|
37
|
+
const _hostModules = new Map()
|
|
35
38
|
|
|
36
39
|
/**
|
|
37
40
|
* 探测 dsh 安装锚点(按可靠性排序)。
|
|
@@ -60,13 +63,23 @@ function dshAnchors() {
|
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
+
* 惰性加载一个宿主包(**通用版**)。
|
|
67
|
+
*
|
|
68
|
+
* 为什么不用静态 import:官方包只存在于 dsh 安装目录(本机
|
|
69
|
+
* `/home/ubuntu/.npm-global/lib/node_modules/@deepseek-ai/dsh/node_modules/`),
|
|
70
|
+
* 而 plugin 的 profile `node_modules/@deepseek-ai/` 只有 cosmokit 与 schemastery
|
|
71
|
+
* ——**解析不到**任何 dsh 子包(2026-11-09 实测)。所以必须从锚点 `createRequire`。
|
|
72
|
+
*
|
|
73
|
+
* 结果按 spec 缓存:同一个 spec 只解析一次,**失败也缓存**(避免反复重试空转)。
|
|
74
|
+
*
|
|
75
|
+
* @param {string} spec 包名(如 `@deepseek-ai/dsh-agent`)
|
|
76
|
+
* @returns {unknown} 模块导出
|
|
77
|
+
* @throws {Error} 所有锚点都失败时(带排查提示,不静默)
|
|
66
78
|
*/
|
|
67
|
-
function
|
|
68
|
-
|
|
69
|
-
if (
|
|
79
|
+
export function loadHostModule(spec) {
|
|
80
|
+
const cached = _hostModules.get(spec)
|
|
81
|
+
if (cached instanceof Error) throw cached
|
|
82
|
+
if (cached !== undefined) return cached
|
|
70
83
|
|
|
71
84
|
const tried = []
|
|
72
85
|
for (const anchor of dshAnchors()) {
|
|
@@ -74,22 +87,45 @@ function loadCreateUserMessage() {
|
|
|
74
87
|
// 文件锚点需先存在,createRequire 才不会因怪路径抛错
|
|
75
88
|
if (anchor.startsWith('/') && !existsSync(anchor)) continue
|
|
76
89
|
const req = createRequire(anchor)
|
|
77
|
-
const mod = req(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return fn
|
|
90
|
+
const mod = req(spec)
|
|
91
|
+
if (mod !== undefined) {
|
|
92
|
+
_hostModules.set(spec, mod)
|
|
93
|
+
return mod
|
|
82
94
|
}
|
|
83
|
-
tried.push(`${anchor}:
|
|
95
|
+
tried.push(`${anchor}: 已解析但模块为空`)
|
|
84
96
|
} catch (error) {
|
|
85
97
|
tried.push(`${anchor}: ${error?.code ?? error?.message ?? error}`)
|
|
86
98
|
}
|
|
87
99
|
}
|
|
88
|
-
|
|
89
|
-
`cs-bots kernel: 无法解析宿主包 ${
|
|
100
|
+
const err = new Error(
|
|
101
|
+
`cs-bots kernel: 无法解析宿主包 ${spec}。\n` +
|
|
90
102
|
` 已知锚点尝试:\n ${tried.join('\n ')}\n` +
|
|
91
103
|
` 对策:设 CS_BOTS_DSH_ROOT=<dsh 安装目录>,或用 \`dsh --profile <p>\` 启动(宿主会暴露解析锚)。`,
|
|
92
104
|
)
|
|
105
|
+
_hostModules.set(spec, err)
|
|
106
|
+
throw err
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 惰性加载宿主的 `createUserMessage`。
|
|
111
|
+
* @returns {Function}
|
|
112
|
+
* @throws {Error} 所有锚点都失败时(**带排查提示**,不静默)
|
|
113
|
+
*/
|
|
114
|
+
function loadCreateUserMessage() {
|
|
115
|
+
if (_createUserMessage) return _createUserMessage
|
|
116
|
+
if (_loadError) throw _loadError
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const mod = /** @type {any} */ (loadHostModule(HOST_LLM))
|
|
120
|
+
const fn = mod?.createUserMessage
|
|
121
|
+
if (typeof fn === 'function') {
|
|
122
|
+
_createUserMessage = fn
|
|
123
|
+
return fn
|
|
124
|
+
}
|
|
125
|
+
} catch (error) {
|
|
126
|
+
_loadError = /** @type {Error} */ (error)
|
|
127
|
+
}
|
|
128
|
+
_loadError = _loadError ?? new Error(`cs-bots kernel: 已解析 ${HOST_LLM} 但没有 createUserMessage 导出`)
|
|
93
129
|
throw _loadError
|
|
94
130
|
}
|
|
95
131
|
|
|
@@ -209,10 +245,25 @@ export function listAgents(ctx) {
|
|
|
209
245
|
// ── 投递(唤醒)─────────────────────────────────────────────────────────────
|
|
210
246
|
|
|
211
247
|
/**
|
|
212
|
-
* 消息来源标记 —— 官方 `UserMessage.source`
|
|
213
|
-
*
|
|
248
|
+
* 消息来源标记 —— 官方 `UserMessage.source` 需要它。未经标识的消息在会话日志里
|
|
249
|
+
* 无法追溯,故统一走这里。
|
|
250
|
+
*
|
|
251
|
+
* ## ⚠️ 这里踩过一个坐实的坑(2026-11-09)
|
|
252
|
+
* 原来写的是 `{ kind: 'plugin', name: 'cs-bots' }`。看起来「很官方」,实际上
|
|
253
|
+
* **v4 格式明令拒收 `kind:'plugin'`**(`dsh-session-format-v3-to-v4/lib/index.js:126`):
|
|
254
|
+
*
|
|
255
|
+
* if (... source["kind"] === "plugin") throw new SessionFormatError(
|
|
256
|
+
* "format v4 message requires a producer-owned source kind")
|
|
257
|
+
*
|
|
258
|
+
* 'plugin' 是 **v3 时代的「包装写法」**,只允许出现在**历史回放**里由迁移层
|
|
259
|
+
* 改写成 `plugin:<name>`;**新建消息**必须直接给「生产者自己的 kind」。
|
|
260
|
+
* 后果:cs-bots 每次唤醒投递都被宿主在准入处拒收,而 `followup()` 早已返回
|
|
261
|
+
* ok —— 于是任务静默卡在「进行中」,页面看不到任何异常。
|
|
262
|
+
*
|
|
263
|
+
* 取值与迁移层的映射**逐字对齐**(`producerKind()`:非已发布同名生产者 →
|
|
264
|
+
* `plugin:<name>`),这样新建的消息与历史回放后的形状完全一致。
|
|
214
265
|
*/
|
|
215
|
-
const SOURCE = { kind: 'plugin
|
|
266
|
+
const SOURCE = { kind: 'plugin:cs-bots' }
|
|
216
267
|
|
|
217
268
|
/**
|
|
218
269
|
* 构造一条可投递的用户消息。**不投递**,只构造(便于幂等键先记账再发送)。
|
|
@@ -289,6 +340,116 @@ export function steer(ctx, sessionId, text) {
|
|
|
289
340
|
|
|
290
341
|
// ── 会话创建(**唯一能指定 cwd 的路径**)──────────────────────────────────────
|
|
291
342
|
|
|
343
|
+
/**
|
|
344
|
+
* 组装「Agent 级 setup 回调」—— 把 preset 的工具面挂上去。
|
|
345
|
+
*
|
|
346
|
+
* ## 为什么必须自己拼(2026-11-09 复核,源码级证据链)
|
|
347
|
+
*
|
|
348
|
+
* 官方 `agents.create(options)` 只透传 `AgentOptions`,而
|
|
349
|
+
* `AgentOptions`(`dsh-agent/lib/types/runtime-types.d.ts:27-36`)**没有
|
|
350
|
+
* tools/preset 字段**;preset 挂载完全发生在 `setup` 回调里:
|
|
351
|
+
* 1. `dsh-api-session-controller/lib/index.js:264-278` 的 `composeAgent(presetId)`
|
|
352
|
+
* 才是官方唯一的装配入口 —— 它返回
|
|
353
|
+
* `{ agentPreset, setup: async (agentCtx) => { installModelSelection(agentCtx); await presets.mount(agentCtx, resolvedId) } }`
|
|
354
|
+
* 2. `dsh-agent-loop/lib/index.js:1574-1576` 在 `initializeAgent` 里真正调用
|
|
355
|
+
* `setup?.(agent.ctx, agent)`;`:543` 证实 `agent.ctx` 带 scope(`mount` 的前置条件)
|
|
356
|
+
* 3. `dsh-agent-preset-registry/lib/index.js:702-711` 的 `mount()` → `retain` + `bind(ctx, generation)`
|
|
357
|
+
* **这一步才挂载工具面**
|
|
358
|
+
*
|
|
359
|
+
* cs-bots 走的 `agents.create({ sessionId, meta, agentOptions })` 这条窄接口
|
|
360
|
+
* **从不传 `setup`** ⇒ preset 从未被挂载 ⇒ Bot 只有 cs-bots 自己注册的
|
|
361
|
+
* `bot_*` 工具,**没有 read / exec / web_fetch 等任何宿主能力**。
|
|
362
|
+
* 这就是「Bot 说它没有文件读写能力」的根因(用户:「无法调用工具会失去很多功能」)。
|
|
363
|
+
*
|
|
364
|
+
* ## 降级策略(沿用本文件「绝不阻塞启动」纪律)
|
|
365
|
+
* - 缺 `agentPresets` 服务 → 退化成只装 model selection(官方同款降级,见 ①)
|
|
366
|
+
* - 缺宿主包 / `mount` 抛错 → warn + 返回 setup 的降级版,**不抛**
|
|
367
|
+
* (「没挂上工具面」仍然好过「会话建不起来」)
|
|
368
|
+
*
|
|
369
|
+
* @param {object} ctx
|
|
370
|
+
* @param {string | undefined} presetId 调用方显式给的 preset;不给则用宿主默认
|
|
371
|
+
* @param {(msg: string) => void} [warn] 告警出口(缺省静默)
|
|
372
|
+
* @returns {Promise<{ setup?: (agentCtx: object) => Promise<void>, agentPreset?: string }>}
|
|
373
|
+
*/
|
|
374
|
+
export async function composePresetSetup(ctx, presetId, warn = () => {}) {
|
|
375
|
+
/** @type {(agentCtx: object) => Promise<void>} */
|
|
376
|
+
let setup = async () => {}
|
|
377
|
+
let agentPreset
|
|
378
|
+
|
|
379
|
+
// ① 惰性取宿主的 installModelSelection(复用 loadHostModule 的锚点机制)
|
|
380
|
+
// 注意:不能静态 import —— profile 的 node_modules/@deepseek-ai/ 解析不到
|
|
381
|
+
// dsh 子包(实测 ERR_MODULE_NOT_FOUND),必须从锚点 createRequire。
|
|
382
|
+
let installModelSelection = null
|
|
383
|
+
try {
|
|
384
|
+
const mod = /** @type {any} */ (loadHostModule(HOST_AGENT))
|
|
385
|
+
installModelSelection = typeof mod?.installModelSelection === 'function' ? mod.installModelSelection : null
|
|
386
|
+
} catch (error) {
|
|
387
|
+
warn(`cs-bots: 无法解析 ${HOST_AGENT}(model-selection 兜底将缺失):${error?.message ?? error}`)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** 安全装 model selection:任何异常都只 warn,绝不向上抛(见下方 mount 同纪律)。 */
|
|
391
|
+
const safeInstall = async (agentCtx) => {
|
|
392
|
+
if (!installModelSelection) return
|
|
393
|
+
try {
|
|
394
|
+
installModelSelection(agentCtx)
|
|
395
|
+
} catch (error) {
|
|
396
|
+
warn(`cs-bots: installModelSelection 失败(不影响工具面挂载):${error?.message ?? error}`)
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const presets = service(ctx, 'agentPresets')
|
|
401
|
+
if (!presets || typeof presets.mount !== 'function') {
|
|
402
|
+
// 官方同款降级:只有 model selection,没有 preset 工具面
|
|
403
|
+
if (installModelSelection) setup = safeInstall
|
|
404
|
+
warn('cs-bots: agentPresets 服务不可用 —— Bot 将只挂 model selection,不会挂 preset 工具面')
|
|
405
|
+
return { setup: installModelSelection ? setup : undefined }
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ② resolve → 拿到真实 id(未给则用宿主默认;官方 `composeAgent` 同款)
|
|
409
|
+
let resolvedId
|
|
410
|
+
try {
|
|
411
|
+
const resolved = await presets.resolve(presetId)
|
|
412
|
+
resolvedId = /** @type {any} */ (resolved)?.id
|
|
413
|
+
} catch (error) {
|
|
414
|
+
// 请求的 preset 不可用(未知 / 已下架)⇒ 回落**宿主默认**。
|
|
415
|
+
// ⚠️ 回落目标必须是 `presets.defaultId`,不能是 `presetId` —— 那等于拿同一个
|
|
416
|
+
// 坏值再 resolve 一次,必然再失败(2026-11-09 测试 P4 揪出来的真 bug:
|
|
417
|
+
// 上一版写 `presetId ?? presets.defaultId`,presetId 非空所以永远不回落)。
|
|
418
|
+
const fallback = presets.defaultId
|
|
419
|
+
warn(
|
|
420
|
+
`cs-bots: preset ${JSON.stringify(presetId ?? null)} 不可用(${error?.message ?? error})`
|
|
421
|
+
+ `,回落到宿主默认 ${JSON.stringify(fallback ?? null)}`,
|
|
422
|
+
)
|
|
423
|
+
if (typeof fallback === 'string' && fallback) {
|
|
424
|
+
try {
|
|
425
|
+
const resolved = await presets.resolve(fallback)
|
|
426
|
+
resolvedId = /** @type {any} */ (resolved)?.id
|
|
427
|
+
} catch (err) {
|
|
428
|
+
warn(`cs-bots: 连默认 preset ${JSON.stringify(fallback)} 都 resolve 不了,跳过挂载:${err?.message ?? err}`)
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (typeof resolvedId !== 'string' || !resolvedId) {
|
|
433
|
+
warn('cs-bots: preset resolve 返回的 id 无效,跳过挂载')
|
|
434
|
+
return { setup: installModelSelection ? safeInstall : undefined }
|
|
435
|
+
}
|
|
436
|
+
agentPreset = resolvedId
|
|
437
|
+
|
|
438
|
+
// ③ 真正拼 setup:installModelSelection + presets.mount
|
|
439
|
+
// 两步必须**各自独立兜底**:installModelSelection 抛错(宿主版本不匹配、
|
|
440
|
+
// agentCtx 形状不符)时绝不允许连带跳过 mount —— 那等于把整个工具面也丢了。
|
|
441
|
+
// 纪律:会话必须建得起来,挂不上的部分如实 warn。
|
|
442
|
+
setup = async (agentCtx) => {
|
|
443
|
+
await safeInstall(agentCtx)
|
|
444
|
+
try {
|
|
445
|
+
await presets.mount(agentCtx, resolvedId)
|
|
446
|
+
} catch (error) {
|
|
447
|
+
warn(`cs-bots: preset ${resolvedId} mount 失败(会话仍可用,但工具面未挂上):${error?.message ?? error}`)
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return { setup, agentPreset }
|
|
451
|
+
}
|
|
452
|
+
|
|
292
453
|
/**
|
|
293
454
|
* 创建一个 root Agent 会话,**可指定工作目录**。
|
|
294
455
|
*
|
|
@@ -310,27 +471,132 @@ export async function createSession(ctx, options) {
|
|
|
310
471
|
if (!agents || typeof agents.create !== 'function') {
|
|
311
472
|
throw new Error('cs-bots kernel: agents.create 不可用(宿主未提供 agents 服务)')
|
|
312
473
|
}
|
|
313
|
-
const { sessionId, cwd, parentAgent, agentPreset, agentOptions, signal } = options ?? {}
|
|
474
|
+
const { sessionId, cwd, parentAgent, agentPreset, agentOptions, signal, warn } = options ?? {}
|
|
314
475
|
if (!sessionId) throw new Error('cs-bots kernel: createSession 需要 sessionId')
|
|
315
476
|
|
|
477
|
+
// ── preset 装配(2026-11-09 修复「Bot 没有任何工具」)──────────────────────
|
|
478
|
+
// 必须走 setup 回调,理由见 composePresetSetup 的注释(源码级证据链)。
|
|
479
|
+
// 先 resolve,再拿它回填 meta.agentPreset —— 这样落盘的 preset 与会话真正
|
|
480
|
+
// 挂载的 preset **是同一个**(官方 composeAgent 也是先 resolve 再写 meta)。
|
|
481
|
+
const composition = await composePresetSetup(ctx, agentPreset, warn)
|
|
482
|
+
|
|
316
483
|
const meta = {}
|
|
317
484
|
if (cwd) meta.cwd = cwd
|
|
318
|
-
|
|
485
|
+
const effectivePreset = composition.agentPreset ?? agentPreset
|
|
486
|
+
if (effectivePreset) meta.agentPreset = effectivePreset
|
|
319
487
|
if (parentAgent) meta.parentSession = parentAgent.id
|
|
320
488
|
|
|
321
|
-
|
|
489
|
+
// ── 模型选择(2026-11-09 修复「bot has no provider/model」)───────────────
|
|
490
|
+
// 官方 agent-loop 在每一步 preStep 都会 `this.model.current()`,
|
|
491
|
+
// 得到 undefined 就直接抛 `agent "X" has no provider/model`。
|
|
492
|
+
// 官方 Web UI 建会话时由 session-controller 记录一次 model selection;
|
|
493
|
+
// 我们走 agents.create 这条窄接口,**没有人替我们记录** ⇒ 必须自己带上。
|
|
494
|
+
// 取值优先级:调用方显式给 > 宿主的全局默认(agentDefaultModel.currentSelection())。
|
|
495
|
+
const route = agentOptions ?? defaultModelSelection(ctx)
|
|
496
|
+
|
|
497
|
+
const handle = await agents.create({
|
|
322
498
|
sessionId,
|
|
323
499
|
...(parentAgent ? { parentAgent } : {}),
|
|
324
500
|
...(Object.keys(meta).length ? { meta } : {}),
|
|
325
|
-
...(
|
|
501
|
+
...(route ? { agentOptions: route } : {}),
|
|
502
|
+
...(composition.setup ? { setup: composition.setup } : {}),
|
|
326
503
|
...(signal ? { signal } : {}),
|
|
327
504
|
})
|
|
505
|
+
|
|
506
|
+
// ── 挂进工作区(2026-11-09 修复「左侧栏未分组」)─────────────────────────
|
|
507
|
+
// 官方把会话归组进 Workspace(`workspaceRegistry`),attachSession 的
|
|
508
|
+
// 前置条件是「会话 header 的 cwd 与 workspace.path 完全相同」。
|
|
509
|
+
// 我们建会话时正好指定了 cwd,所以这里补上「建/复用 workspace + attach」,
|
|
510
|
+
// 否则会话永远落在「未分组」里(2026-11-09 现场:bot-6b654c19… 就是)。
|
|
511
|
+
|
|
512
|
+
// 注意顺序:attach 必须在会话创建之后(attach 要读会话 header 的 cwd)。
|
|
513
|
+
if (cwd) {
|
|
514
|
+
try {
|
|
515
|
+
await attachToWorkspace(ctx, cwd, pickSessionId(handle, sessionId))
|
|
516
|
+
} catch {
|
|
517
|
+
/* 工作区挂载是「锦上添花」:失败不影响会话可用(只是仍显示未分组) */
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return handle
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* 宿主全局默认模型(`agentDefaultModel.currentSelection()`)。
|
|
525
|
+
* 缺失/异常一律返回 undefined —— 不因拿不到默认模型就让建会话失败。
|
|
526
|
+
* @param {object} ctx
|
|
527
|
+
* @returns {{ provider: string, model: string, reasoningEffort?: string } | undefined}
|
|
528
|
+
*/
|
|
529
|
+
function defaultModelSelection(ctx) {
|
|
530
|
+
try {
|
|
531
|
+
const svc = service(ctx, 'agentDefaultModel')
|
|
532
|
+
if (!svc || typeof svc.currentSelection !== 'function') return undefined
|
|
533
|
+
const sel = svc.currentSelection()
|
|
534
|
+
if (!sel || typeof sel.provider !== 'string' || typeof sel.model !== 'string') return undefined
|
|
535
|
+
if (sel.provider === '' || sel.model === '') return undefined
|
|
536
|
+
return {
|
|
537
|
+
provider: sel.provider,
|
|
538
|
+
model: sel.model,
|
|
539
|
+
...(sel.reasoningEffort === undefined ? {} : { reasoningEffort: sel.reasoningEffort }),
|
|
540
|
+
}
|
|
541
|
+
} catch {
|
|
542
|
+
return undefined
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** 从 AgentHandle 里取 session id(宿主可能返回 { agent } / { sessionId } / 字符串)。 */
|
|
547
|
+
function pickSessionId(handle, fallback) {
|
|
548
|
+
if (typeof handle === 'string' && handle) return handle
|
|
549
|
+
const id = handle?.agent?.id ?? handle?.sessionId ?? handle?.id
|
|
550
|
+
return typeof id === 'string' && id ? id : fallback
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* 把会话挂到「路径 = 该会话 cwd」的工作区(不存在则先建)。
|
|
555
|
+
*
|
|
556
|
+
* 官方契约(`dsh-workspace/lib/index.js`):
|
|
557
|
+
* - `workspaceRegistry.create(path)` 建(同路径已存在时会复用,返回 created=false)
|
|
558
|
+
* - `workspace.attachSession(sessionId)` 挂,前提是会话 cwd === workspace.path
|
|
559
|
+
* - 两个方法都可能不存在(老宿主)⇒ 全程 try/catch,静默降级
|
|
560
|
+
*
|
|
561
|
+
* @param {object} ctx
|
|
562
|
+
* @param {string} cwd 已存在的绝对目录
|
|
563
|
+
* @param {string} sessionId
|
|
564
|
+
* @returns {Promise<boolean>} 是否挂上
|
|
565
|
+
*/
|
|
566
|
+
export async function attachToWorkspace(ctx, cwd, sessionId) {
|
|
567
|
+
const registry = service(ctx, 'workspaceRegistry')
|
|
568
|
+
if (!registry || typeof sessionId !== 'string' || !sessionId) return false
|
|
569
|
+
let workspace
|
|
570
|
+
try {
|
|
571
|
+
if (typeof registry.resolveByPath === 'function') {
|
|
572
|
+
workspace = await registry.resolveByPath(cwd)
|
|
573
|
+
}
|
|
574
|
+
} catch {
|
|
575
|
+
workspace = undefined
|
|
576
|
+
}
|
|
577
|
+
if (!workspace) {
|
|
578
|
+
if (typeof registry.create !== 'function') return false
|
|
579
|
+
const out = await registry.create(cwd) // 官方可能返回 { workspace, created } 或直接返回 workspace
|
|
580
|
+
workspace = out?.workspace ?? out
|
|
581
|
+
}
|
|
582
|
+
if (!workspace || typeof workspace.attachSession !== 'function') return false
|
|
583
|
+
await workspace.attachSession(sessionId)
|
|
584
|
+
return true
|
|
328
585
|
}
|
|
329
586
|
|
|
330
587
|
/**
|
|
331
588
|
* 在既有持久会话上恢复 Agent(重启后复活会话用)。
|
|
589
|
+
*
|
|
590
|
+
* ⚠️ resume 也必须传 `setup`(2026-11-09 同源修复)。
|
|
591
|
+
* 官方 resume 路径同样装配 preset:`dsh-api-session-controller/lib/index.js:364-370`
|
|
592
|
+
* 传的是 `setup: this.reuseSetup(composition)`,其中 `reuseSetup`(382-397)
|
|
593
|
+
* 除了 installModelSelection + mount,还会在 preset 变化时 append
|
|
594
|
+
* `agent-preset/selected` 事件。**不传 setup 的 resume 出来的 Agent 同样没有工具面**
|
|
595
|
+
* —— 存量 Bot 光靠「复活」不会自动长出 read/exec,必须在这里补挂。
|
|
596
|
+
*
|
|
332
597
|
* @param {object} ctx
|
|
333
|
-
* @param {{ resumeSessionId: string, signal?: AbortSignal
|
|
598
|
+
* @param {{ resumeSessionId: string, signal?: AbortSignal, agentPreset?: string,
|
|
599
|
+
* agentOptions?: object, warn?: (msg: string) => void }} options
|
|
334
600
|
* @returns {Promise<object>} AgentHandle
|
|
335
601
|
*/
|
|
336
602
|
export async function resumeSession(ctx, options) {
|
|
@@ -338,9 +604,22 @@ export async function resumeSession(ctx, options) {
|
|
|
338
604
|
if (!agents || typeof agents.resume !== 'function') {
|
|
339
605
|
throw new Error('cs-bots kernel: agents.resume 不可用')
|
|
340
606
|
}
|
|
341
|
-
const { resumeSessionId, signal } = options ?? {}
|
|
607
|
+
const { resumeSessionId, signal, agentOptions, agentPreset, warn } = options ?? {}
|
|
342
608
|
if (!resumeSessionId) throw new Error('cs-bots kernel: resumeSession 需要 resumeSessionId')
|
|
343
|
-
|
|
609
|
+
// 2026-11-09 同源修复:**存量会话**从来没有记录过模型选择(当时走的是不带
|
|
610
|
+
// agentOptions 的创建路径),复活后依然 `model.current() === undefined`,
|
|
611
|
+
// 下一次唤醒照样炸 `has no provider/model`。所以复活也要给兜底路由:
|
|
612
|
+
// 调用方显式给 > 会话自己已记录的 > 宿主全局默认(由宿主负责,我们只在
|
|
613
|
+
// 前两者都没有时才传,避免把用户后来改过的会话级选择覆盖回默认值)。
|
|
614
|
+
const route = agentOptions ?? defaultModelSelection(ctx)
|
|
615
|
+
// preset 装配:与 createSession 同源(见 composePresetSetup 的注释)。
|
|
616
|
+
const composition = await composePresetSetup(ctx, agentPreset, warn)
|
|
617
|
+
return agents.resume({
|
|
618
|
+
resumeSessionId,
|
|
619
|
+
...(route ? { agentOptions: route } : {}),
|
|
620
|
+
...(composition.setup ? { setup: composition.setup } : {}),
|
|
621
|
+
...(signal ? { signal } : {}),
|
|
622
|
+
})
|
|
344
623
|
}
|
|
345
624
|
|
|
346
625
|
// ── HTTP 路由注册 ───────────────────────────────────────────────────────────
|
|
@@ -433,6 +712,100 @@ export function restrictTools(agent, names) {
|
|
|
433
712
|
}
|
|
434
713
|
}
|
|
435
714
|
|
|
715
|
+
// ── LLM 请求拦截(复杂度路由唯一需要的宿主触点)──────────────────────────────
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* 注册一条 `llm/stream` 瀑布拦截(**接口 v1,2026-11-04 新增**)。
|
|
719
|
+
*
|
|
720
|
+
* 官方语义一手依据:
|
|
721
|
+
* - 调用点:`dsh-llm/lib/index.js:2397`
|
|
722
|
+
* `this.ctx.waterfall(this, 'llm/stream', options, () => this.adapterStream(...))`
|
|
723
|
+
* - 签名:`'llm/stream'(this: LlmRuntime, options: GenerateOptions,
|
|
724
|
+
* next: () => AsyncIterable<StreamChunk>): AsyncIterable<StreamChunk>`
|
|
725
|
+
* - 官方插件用法样板:`dsh-session-checkpoint-policy/lib/index.js:61`
|
|
726
|
+
* `ctx.on('llm/stream', (options, next) => { ... return next() })`
|
|
727
|
+
*
|
|
728
|
+
* ⚠️ `options` 是**冻结的**(`dsh-agent-loop/lib/index.js:1286` 处 `Object.freeze`):
|
|
729
|
+
* 处理函数只能读、不能写;想让下游知道点什么,必须走旁路通道
|
|
730
|
+
* (本平台用 `globalThis[Symbol.for('cs-myfree.routing.hints.v1')]`)。
|
|
731
|
+
*
|
|
732
|
+
* ⚠️ handler 必须**同步返回 `next()`**(那是 AsyncIterable,不是 Promise),
|
|
733
|
+
* 所以本函数不做任何 await,也不包 async。
|
|
734
|
+
*
|
|
735
|
+
* 缺 `ctx.on`(测试替身/更老宿主)时返回 `false`,调用方按降级处理,**不抛**。
|
|
736
|
+
*
|
|
737
|
+
* @param {object} ctx
|
|
738
|
+
* @param {(options: object, next: Function) => any} handler
|
|
739
|
+
* @returns {boolean} 是否注册成功
|
|
740
|
+
*/
|
|
741
|
+
export function onLlmStream(ctx, handler) {
|
|
742
|
+
if (!ctx || typeof ctx.on !== 'function') return false
|
|
743
|
+
if (typeof handler !== 'function') return false
|
|
744
|
+
try {
|
|
745
|
+
ctx.on('llm/stream', handler)
|
|
746
|
+
return true
|
|
747
|
+
} catch {
|
|
748
|
+
return false
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// ── 会话元数据(谱系)──────────────────────────────────────────────────────
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* 读一个会话的**谱系元数据**(接口 v1,2026-11-04 新增)。
|
|
756
|
+
*
|
|
757
|
+
* 为什么需要(授权泄漏修复):官方 `spawn_teammate` 生成的子会话**不在 cs-bots 的
|
|
758
|
+
* roster 注册表里**,但宿主 session header 带谱系字段。没有这条信息,平台只能把
|
|
759
|
+
* 「不是 Bot 的会话」当成主对话 ⇒ 官方子智能体反而拿到 `paid_allowed`(越权付费)。
|
|
760
|
+
*
|
|
761
|
+
* 一手依据(宿主源码,已核验):
|
|
762
|
+
* - `dsh-session/lib/index.js:1340` `Session.header`(公开属性,deep-frozen);
|
|
763
|
+
* - `dsh-session/lib/index.js:1823-1827` header 由 `meta` 组装,含
|
|
764
|
+
* `parentSession` / `origin`(子代理为 `'subagent'`)/ `delegationDepth` / `agentPreset`;
|
|
765
|
+
* - `dsh-session/lib/index.js:1980` `sessions.get(id)` ⇒ Session。
|
|
766
|
+
*
|
|
767
|
+
* 纪律:
|
|
768
|
+
* - **只取白名单四个字段**并转成纯数据(绝不把宿主对象/header 引用漏给业务层);
|
|
769
|
+
* - 服务缺失、无此会话、任何抛错 ⇒ `null`(调用方据此 fail-closed,**本函数绝不抛**);
|
|
770
|
+
* - 返回 `{}` 表示「会话存在但没有任何谱系字段」= 可确认是顶层会话(主对话)。
|
|
771
|
+
*
|
|
772
|
+
* @param {object} ctx
|
|
773
|
+
* @param {string} sessionId
|
|
774
|
+
* @returns {{ origin?: string, parentSession?: string, delegationDepth?: number,
|
|
775
|
+
* agentPreset?: string } | null}
|
|
776
|
+
*/
|
|
777
|
+
export function sessionMeta(ctx, sessionId) {
|
|
778
|
+
if (typeof sessionId !== 'string' || sessionId === '') return null
|
|
779
|
+
const sessions = service(ctx, 'sessions')
|
|
780
|
+
if (!sessions || typeof sessions.get !== 'function') return null
|
|
781
|
+
let header
|
|
782
|
+
try {
|
|
783
|
+
header = sessions.get(sessionId)?.header
|
|
784
|
+
} catch {
|
|
785
|
+
return null
|
|
786
|
+
}
|
|
787
|
+
if (!header || typeof header !== 'object') return null
|
|
788
|
+
|
|
789
|
+
/** @type {{ origin?: string, parentSession?: string, delegationDepth?: number, agentPreset?: string }} */
|
|
790
|
+
const meta = {}
|
|
791
|
+
try {
|
|
792
|
+
if (typeof header.origin === 'string' && header.origin !== '') meta.origin = header.origin
|
|
793
|
+
if (typeof header.parentSession === 'string' && header.parentSession !== '') {
|
|
794
|
+
meta.parentSession = header.parentSession
|
|
795
|
+
}
|
|
796
|
+
if (typeof header.delegationDepth === 'number' && Number.isFinite(header.delegationDepth)) {
|
|
797
|
+
meta.delegationDepth = header.delegationDepth
|
|
798
|
+
}
|
|
799
|
+
if (typeof header.agentPreset === 'string' && header.agentPreset !== '') {
|
|
800
|
+
meta.agentPreset = header.agentPreset
|
|
801
|
+
}
|
|
802
|
+
} catch {
|
|
803
|
+
// 代理陷阱/只读失败等 ⇒ 视为拿不到元数据(调用方 fail-closed)
|
|
804
|
+
return null
|
|
805
|
+
}
|
|
806
|
+
return meta
|
|
807
|
+
}
|
|
808
|
+
|
|
436
809
|
// ── 状态目录 ────────────────────────────────────────────────────────────────
|
|
437
810
|
|
|
438
811
|
/**
|
|
@@ -478,3 +851,242 @@ export function defaultPreset(ctx) {
|
|
|
478
851
|
return undefined
|
|
479
852
|
}
|
|
480
853
|
}
|
|
854
|
+
|
|
855
|
+
// ── 会话内容读取(需求2:任务详情里直接看对话,不必跳走)──────────────────────
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* 读一个会话的历史事件,转成「人类可读的对话片段」。
|
|
859
|
+
*
|
|
860
|
+
* 为什么需要它(2026-11-08 用户反馈):
|
|
861
|
+
* 「任务栏页面指派 bot 后,无法追踪任务进度,无法看到产出物,
|
|
862
|
+
* 没有跳转对话也没有对话内容查看」
|
|
863
|
+
* ⇒ 跳转会话(openSession)只能把人带到会话界面,**看不到这个任务相关的对话**;
|
|
864
|
+
* 人想看「Bot 到底干了什么」,必须能在任务详情里直接读。
|
|
865
|
+
*
|
|
866
|
+
* 数据来源:官方 `ctx.sessionPersistence`(durable 事件日志,**真相源**)。
|
|
867
|
+
* 为什么不用 live Agent 的内存:进程重启后 live 注册表是空的,但磁盘上的会话仍在——
|
|
868
|
+
* 用持久化层读才能保证「重启后照样能看历史」。
|
|
869
|
+
*
|
|
870
|
+
* @param {object} ctx
|
|
871
|
+
* @param {string} sessionId
|
|
872
|
+
* @param {{ limit?: number, offset?: number }} [options]
|
|
873
|
+
* - `limit`:最多返回多少条**消息片段**(默认 40;取**末尾**的,因为人关心最近发生了什么)
|
|
874
|
+
* @returns {Promise<{
|
|
875
|
+
* ok: boolean, sessionId?: string, messages?: object[], total?: number,
|
|
876
|
+
* reason?: string, available?: boolean,
|
|
877
|
+
* }>}
|
|
878
|
+
*/
|
|
879
|
+
export async function readSessionMessages(ctx, sessionId, options = {}) {
|
|
880
|
+
if (typeof sessionId !== 'string' || sessionId === '') {
|
|
881
|
+
return { ok: false, reason: 'missing-session-id', available: true }
|
|
882
|
+
}
|
|
883
|
+
const persistence = service(ctx, 'sessionPersistence')
|
|
884
|
+
if (!persistence || typeof persistence.open !== 'function') {
|
|
885
|
+
return {
|
|
886
|
+
ok: false,
|
|
887
|
+
available: false,
|
|
888
|
+
reason: 'sessionPersistence 不可用(宿主未装配持久化层)——无法读取会话历史',
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
let handle
|
|
893
|
+
try {
|
|
894
|
+
// access: 'read' —— 只读打开,绝不抢所有权(会话可能正在被 live agent 使用)
|
|
895
|
+
handle = await persistence.open(sessionId, 'read')
|
|
896
|
+
} catch (error) {
|
|
897
|
+
return {
|
|
898
|
+
ok: false,
|
|
899
|
+
available: true,
|
|
900
|
+
reason: `打开会话失败:${error?.message ?? error}`,
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
try {
|
|
905
|
+
const result = await handle.read(0)
|
|
906
|
+
const events = Array.isArray(result?.events) ? result.events : []
|
|
907
|
+
const messages = eventsToMessages(events)
|
|
908
|
+
const limit = Number.isFinite(options.limit) && options.limit > 0 ? Math.floor(options.limit) : 40
|
|
909
|
+
// 取末尾 limit 条:人关心的是「最近在干什么」,不是开场白
|
|
910
|
+
const tail = messages.length > limit ? messages.slice(-limit) : messages
|
|
911
|
+
return {
|
|
912
|
+
ok: true,
|
|
913
|
+
sessionId,
|
|
914
|
+
messages: tail,
|
|
915
|
+
total: messages.length,
|
|
916
|
+
truncated: messages.length > limit,
|
|
917
|
+
}
|
|
918
|
+
} catch (error) {
|
|
919
|
+
return { ok: false, available: true, reason: `读取会话事件失败:${error?.message ?? error}` }
|
|
920
|
+
} finally {
|
|
921
|
+
try {
|
|
922
|
+
await handle.close?.()
|
|
923
|
+
} catch {
|
|
924
|
+
/* 关闭失败不影响已读到的数据 */
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* 会话的「水位」:最新事件序号 + 最新事件时间 + 是否已有用户消息。
|
|
931
|
+
*
|
|
932
|
+
* ## 为什么要它(2026-11-09「投递成功但执行者不动」)
|
|
933
|
+
* `dispatcher` 只记 `sent`(= 我们把消息交给了宿主),这不等于「执行者真的开始了
|
|
934
|
+
* 一个回合」。现场就出现过:wake 记 `sent`,而会话 `turns: 0 / steps: 0`、
|
|
935
|
+
* 事件文件还停在几天前 —— 任务卡在「进行中」,页面上看不到任何异常。
|
|
936
|
+
*
|
|
937
|
+
* 要判「它到底动没动」,最可靠、成本最低的依据就是**会话事件水位**:
|
|
938
|
+
* 投递后一段时间内,如果 seq 没有增长、也没有新的 `user/message`,
|
|
939
|
+
* 那这条输入就是没被消费。这就是 superagent `inputsRead` 那一层的最小实现。
|
|
940
|
+
*
|
|
941
|
+
* 只读打开(`access:'read'`),绝不抢会话所有权。
|
|
942
|
+
*
|
|
943
|
+
* @param {object} ctx
|
|
944
|
+
* @param {string} sessionId
|
|
945
|
+
* @returns {Promise<{ ok: boolean, available?: boolean, seq?: number, lastAt?: number,
|
|
946
|
+
* userMessages?: number, reason?: string }>}
|
|
947
|
+
*/
|
|
948
|
+
export async function readSessionWatermark(ctx, sessionId) {
|
|
949
|
+
if (typeof sessionId !== 'string' || sessionId === '') {
|
|
950
|
+
return { ok: false, reason: 'missing-session-id', available: true }
|
|
951
|
+
}
|
|
952
|
+
const persistence = service(ctx, 'sessionPersistence')
|
|
953
|
+
if (!persistence || typeof persistence.open !== 'function') {
|
|
954
|
+
return { ok: false, available: false, reason: 'sessionPersistence 不可用' }
|
|
955
|
+
}
|
|
956
|
+
let handle
|
|
957
|
+
try {
|
|
958
|
+
handle = await persistence.open(sessionId, 'read')
|
|
959
|
+
} catch (error) {
|
|
960
|
+
return { ok: false, available: true, reason: `打开会话失败:${error?.message ?? error}` }
|
|
961
|
+
}
|
|
962
|
+
try {
|
|
963
|
+
const result = await handle.read(0)
|
|
964
|
+
const events = Array.isArray(result?.events) ? result.events : []
|
|
965
|
+
let seq = -1
|
|
966
|
+
let lastAt
|
|
967
|
+
let userMessages = 0
|
|
968
|
+
for (const ev of events) {
|
|
969
|
+
if (!ev || typeof ev !== 'object') continue
|
|
970
|
+
if (Number.isFinite(ev.seq) && ev.seq > seq) seq = ev.seq
|
|
971
|
+
if (Number.isFinite(ev.time)) lastAt = ev.time
|
|
972
|
+
if (ev.type === 'user/message') userMessages += 1
|
|
973
|
+
}
|
|
974
|
+
return { ok: true, available: true, seq, lastAt, userMessages }
|
|
975
|
+
} catch (error) {
|
|
976
|
+
return { ok: false, available: true, reason: `读取会话事件失败:${error?.message ?? error}` }
|
|
977
|
+
} finally {
|
|
978
|
+
try { await handle.close?.() } catch { /* 关闭失败不影响已读到的数据 */ }
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/**
|
|
983
|
+
* 把官方 SessionEvent 转成人类可读的消息片段。
|
|
984
|
+
*
|
|
985
|
+
* 只保留**有对话价值**的事件类型:用户消息、助手文本、工具调用(摘要)、工具结果(摘要)。
|
|
986
|
+
* 其余事件(会话元数据、内存写入等)一律跳过——它们对「看 Bot 干了什么」没有帮助,
|
|
987
|
+
* 塞进来只会让人淹没在噪声里。
|
|
988
|
+
*
|
|
989
|
+
* 每条输出统一形状:`{ role, kind, text, at?, toolName? }`
|
|
990
|
+
* - role: 'user' | 'assistant' | 'tool'
|
|
991
|
+
* - kind: 'text' | 'tool-call' | 'tool-result' | 'reasoning'
|
|
992
|
+
*
|
|
993
|
+
* @param {object[]} events
|
|
994
|
+
* @returns {object[]}
|
|
995
|
+
*/
|
|
996
|
+
export function eventsToMessages(events) {
|
|
997
|
+
const out = []
|
|
998
|
+
for (const ev of events) {
|
|
999
|
+
if (!ev || typeof ev !== 'object') continue
|
|
1000
|
+
// 官方 SessionEvent 的形状是 `{ type, seq, time, data }` —— 内容在 `data` 里。
|
|
1001
|
+
// 早期实现只读 `ev.content`,结果对**任何真实会话**都产出空数组(需求2「看不到对话内容」
|
|
1002
|
+
// 的真因)。这里同时兼容两种形状,避免新旧数据各有盲区。
|
|
1003
|
+
const type = ev.type
|
|
1004
|
+
const at = typeof ev.time === 'number' ? ev.time : undefined
|
|
1005
|
+
const data = (ev.data && typeof ev.data === 'object') ? ev.data : {}
|
|
1006
|
+
|
|
1007
|
+
const pushBlocks = (role, blocks) => {
|
|
1008
|
+
if (!Array.isArray(blocks)) return
|
|
1009
|
+
for (const b of blocks) {
|
|
1010
|
+
if (!b || typeof b !== 'object') continue
|
|
1011
|
+
if (b.type === 'text' && typeof b.text === 'string' && b.text.trim()) {
|
|
1012
|
+
out.push({ role, kind: 'text', text: b.text, at })
|
|
1013
|
+
} else if (b.type === 'reasoning' && typeof b.text === 'string' && b.text.trim()) {
|
|
1014
|
+
out.push({ role: 'assistant', kind: 'reasoning', text: b.text.slice(0, 2000), at })
|
|
1015
|
+
} else if (b.type === 'tool-call' || b.type === 'toolCall') {
|
|
1016
|
+
out.push({ role: 'assistant', kind: 'tool-call', toolName: String(b.name ?? b.toolName ?? '?'), text: summarizeInput(b.input ?? b.arguments), at })
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// 用户消息(含 system/developer 注入的「用户侧」文本,人看任务上下文需要)
|
|
1022
|
+
if (type === 'user/message' || type === 'user-message') {
|
|
1023
|
+
pushBlocks('user', data.content ?? ev.content ?? data.message?.content ?? ev.message?.content)
|
|
1024
|
+
continue
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// 助手消息:内容在 data.message.content
|
|
1028
|
+
if (type === 'assistant/message' || type === 'assistant-message') {
|
|
1029
|
+
const msg = data.message ?? ev.message ?? {}
|
|
1030
|
+
pushBlocks('assistant', msg.content ?? data.content ?? ev.content)
|
|
1031
|
+
continue
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// 工具调用:data.name + data.arguments(顶层事件,不嵌在 assistant 消息里)
|
|
1035
|
+
if (type === 'tool/call' || type === 'tool-call' || type === 'toolCall') {
|
|
1036
|
+
const name = data.name ?? data.toolName ?? ev.name
|
|
1037
|
+
const summary = summarizeInput(data.arguments ?? data.input ?? ev.arguments)
|
|
1038
|
+
// 既没名字也没入参 ⇒ 对「看 Bot 干了什么」毫无信息量,跳过而不是画一个空壳
|
|
1039
|
+
if (!name && !summary) continue
|
|
1040
|
+
out.push({
|
|
1041
|
+
role: 'assistant',
|
|
1042
|
+
kind: 'tool-call',
|
|
1043
|
+
toolName: String(name ?? '?'),
|
|
1044
|
+
text: summary,
|
|
1045
|
+
at,
|
|
1046
|
+
})
|
|
1047
|
+
continue
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// 工具结果:data.message.content
|
|
1051
|
+
if (type === 'tool/result' || type === 'tool-result' || type === 'toolResult') {
|
|
1052
|
+
const msg = data.message ?? ev.message ?? {}
|
|
1053
|
+
const text = blocksToText(msg.content ?? data.content ?? ev.content ?? ev.result)
|
|
1054
|
+
const name = msg.toolName ?? data.name ?? ev.toolName
|
|
1055
|
+
if (text) {
|
|
1056
|
+
out.push({
|
|
1057
|
+
role: 'tool',
|
|
1058
|
+
kind: 'tool-result',
|
|
1059
|
+
...(typeof name === 'string' && name ? { toolName: name } : {}),
|
|
1060
|
+
text: text.length > 1500 ? `${text.slice(0, 1500)}\u2026` : text,
|
|
1061
|
+
at,
|
|
1062
|
+
})
|
|
1063
|
+
}
|
|
1064
|
+
continue
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
return out
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
/** 工具入参压成一行摘要(对象转 JSON,超长截断)。 */
|
|
1071
|
+
function summarizeInput(input) {
|
|
1072
|
+
if (input === undefined || input === null) return ''
|
|
1073
|
+
let summary = ''
|
|
1074
|
+
try {
|
|
1075
|
+
summary = typeof input === 'string' ? input : JSON.stringify(input)
|
|
1076
|
+
} catch {
|
|
1077
|
+
summary = ''
|
|
1078
|
+
}
|
|
1079
|
+
return summary && summary.length > 500 ? `${summary.slice(0, 500)}\u2026` : summary
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
/** 把 content blocks 拍平成一段文本(只取 text 类型块)。 */
|
|
1083
|
+
function blocksToText(content) {
|
|
1084
|
+
if (typeof content === 'string') return content.trim()
|
|
1085
|
+
if (!Array.isArray(content)) return ''
|
|
1086
|
+
const parts = []
|
|
1087
|
+
for (const b of content) {
|
|
1088
|
+
if (typeof b === 'string') parts.push(b)
|
|
1089
|
+
else if (b && typeof b === 'object' && typeof b.text === 'string') parts.push(b.text)
|
|
1090
|
+
}
|
|
1091
|
+
return parts.join('\n').trim()
|
|
1092
|
+
}
|