@master0071/dsh-systemone 0.4.4 → 0.6.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/lib/auto.js CHANGED
@@ -25,22 +25,48 @@
25
25
  * 安全约束(任何一条不满足都直接放行,绝不阻塞会话):
26
26
  * - 默认关闭(config.autoDecide = false)
27
27
  * - 超时(autoTimeoutMs)与失败一律 fail-open
28
- * - 相同 state 走缓存(autoCacheTtlMs),并发相同 state 走 in-flight 去重
28
+ * - 相同 state 且决策参数未变时走缓存(autoCacheTtlMs),并发相同 state 走 in-flight 去重
29
29
  * - 只在"本轮有新用户消息"时触发,工具结果步骤不触发
30
- * - 跳过斜杠命令、跳过本插件自己注入的运行时上下文
30
+ * - 跳过斜杠命令、跳过本插件自己注入的决策消息(按 INJECTED_SOURCE 识别)
31
31
  */
32
32
  import { runScenarioDecision } from './tools.js'
33
- import { findScenario } from './scenarios.js'
33
+ import { MAX_CHOICE_OPTIONS, findScenario } from './scenarios.js'
34
+ import { recordUsage } from './usage.js'
34
35
  import { readBoolean, readNumber, readString } from './volatile.js'
35
36
 
36
- /** 宿主运行时上下文使用的 source.kind(见 dsh-agent-loop 的 SOURCE 常量)。 */
37
- const RUNTIME_CONTEXT_SOURCE = 'runtime-context'
37
+ /**
38
+ * 本插件注入消息的 source.kind —— **必须用自己的 kind,绝不能复用宿主的 `runtime-context`**。
39
+ *
40
+ * 宿主 `@deepseek-ai/dsh-agent-loop` 的 RuntimeContextProjection 只凭
41
+ * `message.source.kind === 'runtime-context'` 判定"这是我自己写的动态上下文快照"
42
+ * (runtime-context.ts 的 `isOwned()`),并据此维护 `retained`(上一条自有快照的
43
+ * 序号与文本),用来决定下一步要不要再追加一条:
44
+ *
45
+ * if (event.type === 'user/message' && isOwned(event.data)) this.retained = {...}
46
+ * if (this.retained?.text === snapshot) return // 未变则不追加
47
+ * return [{ message: ..., intent: { surfaceOp: 'append' } }]
48
+ *
49
+ * 复用该 kind 的后果(本插件 v0.5.0 及更早版本的缺陷):
50
+ * 1. 每一步注入都把宿主的 retained 覆写成决策文本,于是宿主发现
51
+ * retained.text !== 自己渲染的上下文,**再追加一条冗余快照**;
52
+ * 2. 会话恢复时宿主倒序扫描,会把本插件的最新决策消息误认成自己保留的快照;
53
+ * 3. 宿主还有 CLEARED 归一化逻辑,任何按该 kind 过滤/替换的宿主代码都会误伤。
54
+ *
55
+ * 宿主 llm/message.ts 把 MessageSourceMap 定义为 merge-extensible:
56
+ * "each producer declares its own `kind` in its own module; there is no shared
57
+ * catch-all `plugin` kind"。首方同类插件(time-context / tmux-context)同样各用
58
+ * 自己的 kind。本常量同时用于**写入**注入消息与**读取**时的自我排除,两者必须一致。
59
+ */
60
+ const INJECTED_SOURCE = 'systemone-decision'
38
61
 
39
62
  /** 自动决策结果缓存的最大条目数;达到后先清过期再按插入序淘汰最旧。 */
40
63
  const MAX_AUTO_CACHE = 200
41
64
 
42
- /** 当前请求注入 state 的最大字符数(历史逐条限 2000,这里限制本轮原文)。 */
43
- const MAX_CURRENT_CHARS = 8000
65
+ /** 当前请求注入 state 的最大字符数(历史逐条限 HISTORY_CHARS_PER_MESSAGE,这里限制本轮原文)。 */
66
+ export const MAX_CURRENT_CHARS = 8000
67
+
68
+ /** 历史消息**每条**进入 state 的最大字符数(index.js 的外发声明预算同源引用)。 */
69
+ export const HISTORY_CHARS_PER_MESSAGE = 2000
44
70
 
45
71
  /** 自动决策数字配置的兜底默认值(与 lib/index.js Config schema 的 default 保持同一份来源)。 */
46
72
  export const AUTO_DEFAULTS = {
@@ -52,6 +78,11 @@ export const AUTO_DEFAULTS = {
52
78
  autoMinChars: 4,
53
79
  }
54
80
 
81
+ /** fail-closed 档判定:closed 只对"本应得到决策却没有"的真失败生效(见各调用点)。 */
82
+ function isFailClosed(config) {
83
+ return readString(config, 'autoFailMode', 'open') === 'closed'
84
+ }
85
+
55
86
  /**
56
87
  * 安装自动决策。返回卸载函数。
57
88
  *
@@ -60,7 +91,7 @@ export const AUTO_DEFAULTS = {
60
91
  * @returns {() => void}
61
92
  */
62
93
  export function installAutoDecide(ctx, deps) {
63
- const { provider, config, scenarios, logger } = deps
94
+ const { provider, config, scenarios, logger, usage } = deps
64
95
  if (!readBoolean(config, 'autoDecide', false)) return () => {}
65
96
 
66
97
  const disposers = []
@@ -70,9 +101,9 @@ export function installAutoDecide(ctx, deps) {
70
101
  const perAgent = new Map()
71
102
  /** agentId → 上次注入的决策文本:同一决策只注入一次,避免每步重复污染历史。 */
72
103
  const lastInjected = new Map()
73
- /** stateHash → { at, result }:避免同一内容重复请求。 */
104
+ /** 决策 key(state + 决策指纹)→ { at, result }:避免同一内容在同一配置下重复请求。 */
74
105
  const cache = new Map()
75
- /** stateHash → Promise:并发相同 state 只发一次上游请求。 */
106
+ /** 决策 key → Promise:并发相同 state 且同配置时只发一次上游请求。 */
76
107
  const inFlight = new Map()
77
108
  /** 已告警过的无效固定场景 id:同一错误只提示一次,避免每步刷日志。 */
78
109
  const warnedScenarios = new Set()
@@ -208,6 +239,7 @@ export function installAutoDecide(ctx, deps) {
208
239
  signal: timeout.signal,
209
240
  log,
210
241
  warnedScenarios,
242
+ usage,
211
243
  }),
212
244
  timeoutMs,
213
245
  )
@@ -215,6 +247,12 @@ export function installAutoDecide(ctx, deps) {
215
247
  timeout.dispose()
216
248
  }
217
249
  } catch (error) {
250
+ // fail-closed 档:决策管线失败 = 本轮没有可用决策,按 closed 语义否决
251
+ // (宿主把 turn 记为 blocked)。open 档维持注入语义:记 warning 放行。
252
+ if (isFailClosed(config)) {
253
+ log('warn', `自动决策失败,fail-closed 拦截本轮:${messageOf(error)}`)
254
+ return { kind: 'reject' }
255
+ }
218
256
  log('warn', `自动决策失败,已放行:${messageOf(error)}`)
219
257
  return decision
220
258
  }
@@ -259,6 +297,7 @@ export function installAutoDecide(ctx, deps) {
259
297
  signal: timeout.signal,
260
298
  log,
261
299
  warnedScenarios,
300
+ usage,
262
301
  }),
263
302
  timeoutMs,
264
303
  )
@@ -300,11 +339,62 @@ export function installAutoDecide(ctx, deps) {
300
339
  /* ─── 决策流程 ─────────────────────────────────────────────────────────────── */
301
340
 
302
341
  /**
303
- * 计算决策(带并发去重)。相同 state 只发一次上游请求。
342
+ * 决策指纹:把**影响决策结果的全部配置**压成一个短字符串。
343
+ *
344
+ * 缓存与 in-flight 去重的 key 必须是 `hashString(state + 指纹)`,不能只哈希 state
345
+ * ——否则用户在配置页改了「固定场景」、切了 model/provider、或调了两个置信度门槛后,
346
+ * 最长 autoCacheTtlMs(默认 60 秒)内仍会继续注入**旧场景的旧结论**,与
347
+ * "保存即生效、无需重启"的承诺直接冲突。
348
+ *
349
+ * 指纹含 `scenariosRef`:场景库本身也是热更新的(customScenarios),而自动路由的
350
+ * 结果是一个场景 id;若期间删除/改名了该场景,旧的路由结论会指向已不存在的场景,
351
+ * 表现为"决策突然不注入了"。把场景库指纹纳入 key 让这种变更立即失效。
352
+ *
353
+ * 刻意**不含** `autoTimeoutMs` / `autoCacheTtlMs` / `autoMaxMessages`:
354
+ * - 前两个只影响等待与缓存寿命,不影响判定内容,纳入只会让改超时白白清空缓存;
355
+ * - autoMaxMessages 已通过 state 文本体现(改变条数就会改变 state)。
356
+ *
357
+ * provider 用实例的 `name`(它本身由 provider 配置派生),model / 门槛现读现取,
358
+ * 因此本函数必须**在每次决策时调用**,而不能在装配期算一次。
359
+ *
360
+ * @param {object} config - 插件配置(volatile 字段经 readString/readNumber 脱壳)
361
+ * @param {object} provider - 当前提供商实例
362
+ * @param {string} scenariosRef - 场景库指纹(见 scenariosFingerprint)
363
+ * @returns {string} 形如 `customer_service|unisound|u2-decision|0.6|0.35|11:customer_service,…`
364
+ */
365
+ function decisionFingerprint(config, provider, scenariosRef) {
366
+ return [
367
+ readString(config, 'autoScenario', ''),
368
+ readString(provider, 'name', ''),
369
+ readString(config, 'model', ''),
370
+ String(readNumber(config, 'autoMinConfidence', AUTO_DEFAULTS.autoMinConfidence)),
371
+ String(readNumber(config, 'autoRouteMinConfidence', AUTO_DEFAULTS.autoRouteMinConfidence)),
372
+ scenariosRef,
373
+ ].join('|')
374
+ }
375
+
376
+ /**
377
+ * 场景库指纹:场景总数 + 每个场景的 `id:title`。
378
+ *
379
+ * 自定义场景是热更新的,改一个别名/加一个场景都会让自动路由的候选集变化。
380
+ * 用这个便宜字符串(远小于任何一次上游请求)换取"配置一改、缓存立即失效"。
381
+ *
382
+ * @param {object[]} scenarios - 场景库(活引用)
383
+ * @returns {string}
384
+ */
385
+ function scenariosFingerprint(scenarios) {
386
+ const list = Array.isArray(scenarios) ? scenarios : []
387
+ const parts = list.map((s) => `${s?.id ?? ''}:${s?.title ?? ''}`)
388
+ return `${list.length}:${parts.join(',')}`
389
+ }
390
+
391
+ /**
392
+ * 计算决策(带并发去重)。相同 state 且决策参数未变时只发一次上游请求。
304
393
  */
305
- async function computeDecision({ agent, newText, provider, config, scenarios, cache, inFlight, signal, log, warnedScenarios }) {
394
+ async function computeDecision({ agent, newText, provider, config, scenarios, cache, inFlight, signal, log, warnedScenarios, usage }) {
306
395
  const state = captureState(agent, newText, readNumber(config, 'autoMaxMessages', AUTO_DEFAULTS.autoMaxMessages))
307
- const key = hashString(state)
396
+ // key = state 文本 + 决策指纹:state 决定"问什么",指纹决定"按什么配置问"
397
+ const key = hashString(`${state}\u0000${decisionFingerprint(config, provider, scenariosFingerprint(scenarios))}`)
308
398
  const existing = inFlight.get(key)
309
399
  if (existing) return existing
310
400
  const promise = decide({
@@ -319,6 +409,7 @@ async function computeDecision({ agent, newText, provider, config, scenarios, ca
319
409
  signal,
320
410
  log,
321
411
  warnedScenarios,
412
+ usage,
322
413
  }).finally(() => {
323
414
  if (inFlight.get(key) === promise) inFlight.delete(key)
324
415
  })
@@ -326,7 +417,7 @@ async function computeDecision({ agent, newText, provider, config, scenarios, ca
326
417
  return promise
327
418
  }
328
419
 
329
- async function decide({ agent, newText, state, key, provider, config, scenarios, cache, signal, log, warnedScenarios }) {
420
+ async function decide({ agent, newText, state, key, provider, config, scenarios, cache, signal, log, warnedScenarios, usage }) {
330
421
  const ttl = readNumber(config, 'autoCacheTtlMs', AUTO_DEFAULTS.autoCacheTtlMs) || 0
331
422
  if (ttl > 0) {
332
423
  const hit = cache.get(key)
@@ -334,7 +425,7 @@ async function decide({ agent, newText, state, key, provider, config, scenarios,
334
425
  }
335
426
 
336
427
  // 1) 选场景:固定绑定,或让模型做一次自动路由
337
- const routed = await resolveScenario({ state, provider, config, scenarios, signal, log, warnedScenarios })
428
+ const routed = await resolveScenario({ state, provider, config, scenarios, signal, log, warnedScenarios, usage })
338
429
  if (!routed) return null
339
430
 
340
431
  // 路由置信度不足:宁可不注入,也不要塞一个瞎猜的结论
@@ -354,8 +445,14 @@ async function decide({ agent, newText, state, key, provider, config, scenarios,
354
445
  state,
355
446
  exec: { signal },
356
447
  minConfidence: readNumber(config, 'autoMinConfidence', AUTO_DEFAULTS.autoMinConfidence),
448
+ usage,
357
449
  })
358
450
  if (!result?.ok) {
451
+ // 上游明确回答"决策失败":closed 档必须上抛,让 pre-step 的 catch 按语义拦截;
452
+ // open 档维持原行为:warn + 不注入(= 放行)。
453
+ if (isFailClosed(config)) {
454
+ throw new Error(`场景 ${routed.scenario.id} 执行失败:${result?.error || '未知错误'}`)
455
+ }
359
456
  log?.('warn', `场景 ${routed.scenario.id} 执行失败,已放行:${result?.error || '未知错误'}`)
360
457
  return null
361
458
  }
@@ -384,8 +481,36 @@ async function decide({ agent, newText, state, key, provider, config, scenarios,
384
481
  return injected
385
482
  }
386
483
 
387
- /** 选择场景:固定绑定优先,否则用一次 choice 问句自动路由。 */
388
- async function resolveScenario({ state, provider, config, scenarios, signal, log, warnedScenarios }) {
484
+ /**
485
+ * 自动路由分批题的问题 id 前缀。
486
+ *
487
+ * 必须与"场景自己的问题 id"不可能相撞:场景问题 id 由用户定义,`scenario_0`
488
+ * 这种名字(正好也是 practice 里常见的场景 id 形态)会与之冲突,导致消费方
489
+ * 无法区分"这是路由题"还是"这是场景决策题"。用带命名空间的前缀 + 双下划线。
490
+ */
491
+ const ROUTE_QUESTION_PREFIX = 'systemone_route__'
492
+
493
+ /**
494
+ * 把场景清单切成每个不超过 MAX_CHOICE_OPTIONS 个选项的批次。
495
+ *
496
+ * 上游决策模型对单道 choice 的选项数有硬限制,而自动路由原本把**全部**场景塞进
497
+ * 一道题——11 个内置 + 15 个自定义即越界,且越界的问题不经过任何校验,只会被上游
498
+ * 拒绝(表现为路由静默失效)。分批后每道题都在限制内。
499
+ *
500
+ * @param {object[]} scenarios - 场景库
501
+ * @returns {object[][]} 批次数组;场景数不超限时只有一个批次
502
+ */
503
+ function chunkScenarios(scenarios) {
504
+ const size = Math.max(1, MAX_CHOICE_OPTIONS)
505
+ const batches = []
506
+ for (let i = 0; i < scenarios.length; i += size) {
507
+ batches.push(scenarios.slice(i, i + size))
508
+ }
509
+ return batches
510
+ }
511
+
512
+ /** 选择场景:固定绑定优先,否则用一次分批 choice 问句自动路由。 */
513
+ async function resolveScenario({ state, provider, config, scenarios, signal, log, warnedScenarios, usage }) {
389
514
  const fixed = readString(config, 'autoScenario', '').trim()
390
515
  if (fixed) {
391
516
  const scenario = findScenario(scenarios, fixed)
@@ -402,22 +527,59 @@ async function resolveScenario({ state, provider, config, scenarios, signal, log
402
527
  if (scenarios.length === 0) return null
403
528
  if (scenarios.length === 1) return { scenario: scenarios[0], source: '唯一场景', confidence: null }
404
529
 
530
+ // 场景数超过单道 choice 的上限时拆成多道题,一次请求并行问出(题目之间相互独立)
531
+ const batches = chunkScenarios(scenarios)
532
+ const questions = {}
533
+ batches.forEach((batch, index) => {
534
+ questions[`${ROUTE_QUESTION_PREFIX}${index}`] = {
535
+ type: 'choice',
536
+ instructions: batches.length === 1
537
+ ? '这段内容最需要哪一类业务判断?'
538
+ : `这段内容最需要哪一类业务判断?(第 ${index + 1}/${batches.length} 组;若都不匹配,选组内语义最近的一项)`,
539
+ criteria: Object.fromEntries(batch.map((s) => [s.id, s.title])),
540
+ }
541
+ })
542
+
405
543
  const response = await provider.decide({
406
544
  state,
407
- questions: {
408
- scenario: {
409
- type: 'choice',
410
- instructions: '这段内容最需要哪一类业务判断?',
411
- criteria: Object.fromEntries(scenarios.map((s) => [s.id, s.title])),
412
- },
413
- },
545
+ questions,
414
546
  model: readString(config, 'model', ''),
415
547
  signal,
416
548
  })
417
- const answer = response?.answers?.scenario
418
- const scenario = answer?.choice ? findScenario(scenarios, answer.choice) : undefined
419
- if (!scenario) return null
420
- return { scenario, source: '自动路由', confidence: Number(answer?.confidence) }
549
+ // 记账:路由请求不经过 executeDecision,必须在这里单独记,否则自动路由的花销完全不可见。
550
+ // 注意别重复计:目标场景那次决策由 runScenarioDecision → executeDecision 统一记账。
551
+ recordUsage(usage, {
552
+ source: 'auto-route',
553
+ ok: true,
554
+ model: response?.model || readString(config, 'model', ''),
555
+ provider: provider.name,
556
+ usage: response?.usage,
557
+ latencyMs: response?.latency_ms,
558
+ })
559
+ // 每题各选出一个候选,取置信度最高的那题作为路由结果;
560
+ // 置信度不可用时按题目顺序(第 1 组优先),保持确定性。
561
+ const answers = response?.answers || {}
562
+ const candidates = []
563
+ batches.forEach((_batch, index) => {
564
+ const answer = answers[`${ROUTE_QUESTION_PREFIX}${index}`]
565
+ const scenario = answer?.choice ? findScenario(scenarios, answer.choice) : undefined
566
+ if (scenario) candidates.push({ scenario, confidence: Number(answer?.confidence) })
567
+ })
568
+ if (candidates.length === 0) {
569
+ // 路由题全部无法映射回场景 = 上游给了不可用的回答,与请求异常同级。
570
+ // closed 档上抛拦截本轮;open 档维持"静默不注入"。
571
+ if (isFailClosed(config)) {
572
+ throw new Error('自动路由没有产出任何可映射到场景的回答')
573
+ }
574
+ return null
575
+ }
576
+ candidates.sort((a, b) => {
577
+ const ac = Number.isFinite(a.confidence) ? a.confidence : -1
578
+ const bc = Number.isFinite(b.confidence) ? b.confidence : -1
579
+ return bc - ac
580
+ })
581
+ const best = candidates[0]
582
+ return { scenario: best.scenario, source: '自动路由', confidence: best.confidence }
421
583
  }
422
584
 
423
585
  /** 把决策结果渲染成注入文本。 */
@@ -441,7 +603,13 @@ function renderInjection(result, routed, config) {
441
603
  if (derived) lines.push(`派生:${derived}`)
442
604
 
443
605
  if (result.recommendation) lines.push(`建议:${result.recommendation}`)
444
- if (result.needs_human_review) lines.push('注意:本次判断置信度偏低,请人工确认后再执行。')
606
+ if (result.needs_human_review) {
607
+ // 带上判定理由:只写"置信度偏低"会让模型无法判断该不该采信这条结论
608
+ const reasons = Array.isArray(result.review_reasons) && result.review_reasons.length > 0
609
+ ? `(${result.review_reasons.slice(0, 2).join(';')})`
610
+ : ''
611
+ lines.push(`注意:本次判断的可能分布不够集中${reasons},请人工确认后再执行。`)
612
+ }
445
613
 
446
614
  return lines.join('\n')
447
615
  }
@@ -453,18 +621,28 @@ function questionLabel(scenario, qid) {
453
621
  /* ─── 上下文捕获 ───────────────────────────────────────────────────────────── */
454
622
 
455
623
  /**
456
- * 判断是否为"无业务含义"的输入,用于跳过自动决策。
624
+ * 判断是否应当跳过自动决策:**只有"短促"且"整体是寒暄"才跳过**。
625
+ *
626
+ * 语义(0.6.0 起修正):早期实现是"短于 autoMinChars 一律跳过"的**或**关系,
627
+ * 在中文里会静默误杀完整的业务请求——"退款""报错""超时""闪退"都只有 2 个字,
628
+ * 却都是真实请求,结果是开了自动决策却什么都不发生、且没有任何日志。
629
+ *
630
+ * 现在两条规则取**交集**:
631
+ * 1. `TRIVIAL_PATTERN` 是整体匹配(`^…$`),单独一条就足以识别寒暄;
632
+ * 2. `autoMinChars` 从"长度下限"变为**寒暄判定的长度上界**——超过这个长度
633
+ * 即使内容是"你好…"也交给模型判断(如「你好,我的订单一直没到账」)。
457
634
  *
458
- * 两条独立的跳过规则(命中任一即跳过):
459
- * 1. 长度阈值:短于 autoMinChars 的一律跳过(默认 4,"退款失败" 恰好 4 字不被误伤);
460
- * 2. 寒暄黑名单:整体就是一句寒暄/致谢的跳过,与长度无关。
635
+ * 注意:`autoMinChars: 0` 表示**不做长度限制**,即只按寒暄黑名单判定。
636
+ * 副作用:短业务请求现在会真的发起一次上游调用(有 token 与延迟成本)。
461
637
  */
462
638
  const TRIVIAL_PATTERN = /^(你好|您好|哈喽|在吗|hi|hello|hey|ok|okay|好的|好嘞|收到|明白|嗯+|谢谢|多谢|感谢|thanks|thank you|thx|再见|拜拜|bye|测试|test)[\s!!。.~~,,、??]*$/i
463
639
 
464
640
  function isTrivialInput(text, minChars) {
465
641
  const trimmed = text.trim()
466
- if (Number.isFinite(minChars) && minChars > 0 && trimmed.length < minChars) return true
467
- return TRIVIAL_PATTERN.test(trimmed)
642
+ // 只按寒暄黑名单判定(不做长度限制)
643
+ if (!Number.isFinite(minChars) || minChars <= 0) return TRIVIAL_PATTERN.test(trimmed)
644
+ // 短 + 整体是寒暄:两个条件同时成立才跳过
645
+ return trimmed.length < minChars && TRIVIAL_PATTERN.test(trimmed)
468
646
  }
469
647
 
470
648
  /**
@@ -493,9 +671,9 @@ function readHistory(agent, limit) {
493
671
  else if (event?.type === 'assistant/message') message = event.data?.message
494
672
  else continue
495
673
 
496
- if (event.type === 'user/message' && message?.source?.kind === RUNTIME_CONTEXT_SOURCE) continue
674
+ if (event.type === 'user/message' && message?.source?.kind === INJECTED_SOURCE) continue
497
675
  const text = textOfMessage(message)
498
- if (text) picked.unshift(`- ${event.type === 'user/message' ? '用户' : '助手'}:${truncate(text, 2000)}`)
676
+ if (text) picked.unshift(`- ${event.type === 'user/message' ? '用户' : '助手'}:${truncate(text, HISTORY_CHARS_PER_MESSAGE)}`)
499
677
  }
500
678
  return picked
501
679
  }
@@ -504,7 +682,7 @@ function extractUserText(messages) {
504
682
  const texts = []
505
683
  for (const message of messages || []) {
506
684
  if (message?.role !== 'user') continue
507
- if (message?.source?.kind === RUNTIME_CONTEXT_SOURCE) continue
685
+ if (message?.source?.kind === INJECTED_SOURCE) continue
508
686
  const text = textOfMessage(message)
509
687
  if (text) texts.push(text)
510
688
  }
@@ -523,12 +701,12 @@ function textOfMessage(message) {
523
701
  .trim()
524
702
  }
525
703
 
526
- /** 构造与宿主 runtime-context 同形的消息(UserMessage)。 */
704
+ /** 构造本插件自己的注入消息(UserMessage);kind 见 INJECTED_SOURCE 的说明。 */
527
705
  function buildContextMessage(text) {
528
706
  return {
529
707
  role: 'user',
530
708
  content: [{ type: 'text', text }],
531
- source: { kind: RUNTIME_CONTEXT_SOURCE },
709
+ source: { kind: INJECTED_SOURCE },
532
710
  }
533
711
  }
534
712