@furongjun1999/dsh-memory 0.3.0 → 0.3.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.
@@ -0,0 +1,305 @@
1
+ /**
2
+ * llm_adapter.ts —— 白箱 LLM 服务商(WhiteboxLlmAdapter)
3
+ *
4
+ * 设计哲学:白箱本身就是 LLM 模型(对外完全对齐 dsh-llm 协议),内部白箱化。
5
+ * - provider: 'lingshu-whitebox'(设置页「模型」可见,可被 agent 默认选用)
6
+ * - stream(): 调灵枢 wisdom_chat(白箱 CCG 条件路由/组合生成/自校验)
7
+ * - token 计数:输入/输出/缓存命中(白箱直答 = 全部 cacheRead,零推理成本)
8
+ * - 降级:白箱无把握(route=llm / 桥未就绪)→ 可选 fallback(后续接 deepseek)
9
+ *
10
+ * 结构仿官方 @deepseek-ai/dsh-llm-deepseek adapter。
11
+ */
12
+
13
+ import type { Context } from '@deepseek-ai/cordis'
14
+ import type { LingshuBridge } from './bridge.js'
15
+
16
+ // ---- dsh-llm 类型(运行时仅用结构;类型从包引入保持协议对齐)----
17
+ // 不 import 运行时符号,仅引用类型,避免强依赖 dsh-llm 未装时报错。
18
+ export interface LlmStreamChunk {
19
+ type: 'block-start' | 'text-delta' | 'reasoning-delta' | 'tool-call-delta' | 'block-end' | 'usage' | 'finish'
20
+ index?: number
21
+ blockType?: string
22
+ text?: string
23
+ id?: string
24
+ name?: string
25
+ argumentsDelta?: string
26
+ block?: { type: string; text?: string; [k: string]: unknown }
27
+ usage?: { inputTokens: number; outputTokens: number; cacheReadTokens?: number; cacheWriteTokens?: number }
28
+ reason?: string
29
+ replayState?: unknown
30
+ }
31
+
32
+ export interface LlmGenerateOptions {
33
+ provider: string
34
+ model: string
35
+ reasoningEffort?: string
36
+ messages: Array<{ role: string; content?: Array<{ type?: string; text?: string; arguments?: string; [k: string]: unknown }> }>
37
+ system?: string
38
+ tools?: Array<{ name: string; description: string; parameters: Record<string, unknown> }>
39
+ temperature?: number
40
+ maxTokens?: number
41
+ stop?: string[]
42
+ signal?: AbortSignal
43
+ sessionId?: string
44
+ purpose?: 'compaction' | 'session-title'
45
+ }
46
+
47
+ // ---- token 估算(确定性启发式:CJK 0.6/字符,其余 /4)----
48
+ export function estimateTokens(text: string | undefined | null): number {
49
+ if (!text) return 0
50
+ let cjk = 0
51
+ let other = 0
52
+ for (const ch of text) {
53
+ const code = ch.codePointAt(0)!
54
+ if ((code >= 0x4e00 && code <= 0x9fff) || (code >= 0x3040 && code <= 0x30ff)) cjk++
55
+ else other++
56
+ }
57
+ return Math.ceil(cjk * 0.6 + other / 4)
58
+ }
59
+
60
+ /** 从 GenerateOptions 汇总输入 token(system + messages 全部文本)。 */
61
+ export function countInputTokens(options: LlmGenerateOptions): number {
62
+ let total = 0
63
+ if (options.system) total += estimateTokens(options.system)
64
+ for (const msg of options.messages ?? []) {
65
+ for (const block of msg.content ?? []) {
66
+ if (typeof block?.text === 'string') total += estimateTokens(block.text)
67
+ if (typeof block?.arguments === 'string') total += estimateTokens(block.arguments)
68
+ }
69
+ }
70
+ return total
71
+ }
72
+
73
+ /** 把 DSH 会话消息折叠成白箱可读的对话文本。 */
74
+ export function serializeMessages(options: LlmGenerateOptions): string {
75
+ const parts: string[] = []
76
+ if (options.system) parts.push(`[系统] ${options.system}`)
77
+ for (const msg of options.messages ?? []) {
78
+ const text = (msg.content ?? [])
79
+ .filter((b) => b?.type === 'text' && typeof b.text === 'string')
80
+ .map((b) => b.text as string)
81
+ .join('')
82
+ if (!text) continue
83
+ const role = msg.role === 'assistant' ? '灵枢' : msg.role === 'user' ? '用户' : msg.role
84
+ parts.push(`[${role}] ${text}`)
85
+ }
86
+ return parts.join('\n')
87
+ }
88
+
89
+ /** 把一段完整文本包装成协议流(确定性整段生成 → 单次流)。 */
90
+ export async function* wrapTextStream(
91
+ text: string,
92
+ inputTokens: number,
93
+ outputTokens: number,
94
+ cacheReadTokens: number,
95
+ ): AsyncIterable<LlmStreamChunk> {
96
+ yield { type: 'block-start', index: 0, blockType: 'text' }
97
+ yield { type: 'text-delta', index: 0, text }
98
+ yield { type: 'block-end', index: 0, block: { type: 'text', text } }
99
+ yield {
100
+ type: 'usage',
101
+ usage: {
102
+ inputTokens,
103
+ outputTokens,
104
+ ...(cacheReadTokens > 0 ? { cacheReadTokens } : {}),
105
+ },
106
+ }
107
+ yield { type: 'finish', reason: 'stop' }
108
+ }
109
+
110
+ /**
111
+ * 白箱知识卡片格式检测:REVERSE_DAILY 直答是知识库内部格式——
112
+ * 「X是什么…,是『A』vs『B』的矛盾——X(是…(…(…)…)…真相:①…」
113
+ * 特征(任一强信号命中即判定):
114
+ * ① 卡片开头句式:内容含「,是『X』vs『Y』的矛盾——」(矛盾标记+破折号)
115
+ * ② 「真相:」结构化标记(卡片分段标题)+ 括号密集(>8)
116
+ * ③ 长文本(>300)+ 高括号密度(>20)+ 无自然语言标点分隔(顿号密集)
117
+ */
118
+ export function isCardFormat(text: string): boolean {
119
+ if (!text) return false
120
+
121
+ // ① 强信号:矛盾标记句式(「,是『A』vs『B』的矛盾——」)——卡片开头的
122
+ // 标志性结构,不受长度门槛限制(短卡片也判定)
123
+ if (/,是『[^』]{1,12}』vs『[^』]{1,12}』的矛盾[——\-]/.test(text)) return true
124
+
125
+ // 短文本(<150)无矛盾标记 → 正常文本
126
+ if (text.length < 150) return false
127
+
128
+ // ② 「真相:」+ 括号密集(卡片的分段结构)
129
+ if (text.includes('真相:')) {
130
+ const parens = (text.match(/(/g) ?? []).length
131
+ if (parens >= 8) return true
132
+ }
133
+
134
+ // ③ 超长 + 极高括号密度(卡片展开体)
135
+ const parens2 = (text.match(/(/g) ?? []).length
136
+ if (text.length > 300 && parens2 > 20) return true
137
+
138
+ return false
139
+ }
140
+
141
+ /** 白箱 adapter 依赖注入。 */
142
+ export interface WhiteboxAdapterDeps {
143
+ /** 灵枢桥(MCP stdio)——白箱引擎通道。 */
144
+ bridge?: LingshuBridge
145
+ /**
146
+ * 降级 LLM:白箱未命中(route=llm / 桥未就绪)时转发。由 installWhiteboxLlm
147
+ * 注入 DSH 的 llm 服务(走 fallbackProvider 路由),实现「白箱优先 + LLM 降级」。
148
+ */
149
+ fallback?: { stream(o: LlmGenerateOptions): AsyncIterable<LlmStreamChunk> }
150
+ }
151
+
152
+ /** 白箱 LLM adapter —— 注册为 DSH 的 provider。 */
153
+ export class WhiteboxLlmAdapter {
154
+ private readonly bridge?: LingshuBridge
155
+ private readonly fallback?: WhiteboxAdapterDeps['fallback']
156
+
157
+ constructor(deps: WhiteboxAdapterDeps = {}) {
158
+ this.bridge = deps.bridge
159
+ this.fallback = deps.fallback
160
+ }
161
+
162
+ providerInfo(provider: string): { id: string; name: string } {
163
+ return { id: provider, name: '白箱灵枢 (lingshu-whitebox)' }
164
+ }
165
+
166
+ providerRetryPolicy(): undefined {
167
+ return undefined
168
+ }
169
+
170
+ async listModels(): Promise<Array<{ provider: string; id: string; name: string }>> {
171
+ return [
172
+ { provider: 'lingshu-whitebox', id: 'whitebox-v1', name: '白箱引擎 v1 (CCG 条件路由)' },
173
+ { provider: 'lingshu-whitebox', id: 'whitebox-wisdom', name: '白箱智慧之书' },
174
+ ]
175
+ }
176
+
177
+ async resolveModel(provider: string, model: string): Promise<{ provider: string; id: string; name: string }> {
178
+ return { provider, id: model, name: model }
179
+ }
180
+
181
+ /**
182
+ * 核心:一次模型调用。白箱优先,无把握降级。
183
+ * @param options 完全装配的请求(必须 honor options.signal)
184
+ */
185
+ async *stream(options: LlmGenerateOptions): AsyncIterable<LlmStreamChunk> {
186
+ const inputTokens = countInputTokens(options)
187
+ const conversation = serializeMessages(options)
188
+ // 取最后一条用户文本作为白箱输入(完整对话太长时截尾)
189
+ const lastUserText = [...(options.messages ?? [])]
190
+ .reverse()
191
+ .flatMap((m) => m.content ?? [])
192
+ .filter((b) => b?.type === 'text')
193
+ .map((b) => b.text as string)
194
+ .join('')
195
+ .slice(-2000)
196
+
197
+ // ---- 白箱主路径:调灵枢 wisdom_chat ----
198
+ let whitebox: { text: string; route: string } | null = null
199
+ if (this.bridge?.isReady?.()) {
200
+ try {
201
+ const r = await this.bridge.callTool('wisdom_chat', {
202
+ message: lastUserText || conversation.slice(-4000),
203
+ session_id: (options.sessionId as string) ?? 'default',
204
+ }, options.signal)
205
+ const raw = (r?.content ?? []).map((c) => (c as { text?: string }).text ?? '').join('\n')
206
+ // 白箱返回结构:{"reply": "...", "route": "self|llm", "hits": [...], "honest": bool}
207
+ let text = raw
208
+ let route = 'self'
209
+ try {
210
+ const parsed = JSON.parse(raw) as { route?: string; reply?: unknown }
211
+ if (parsed.route) route = parsed.route
212
+ if (typeof parsed.reply === 'string') text = parsed.reply
213
+ else if (parsed.reply !== undefined) text = JSON.stringify(parsed.reply)
214
+ }
215
+ catch { /* 非 JSON = 纯文本回复 */ }
216
+ // 质量门槛(v0.4.1):REVERSE_DAILY 卡片直答是知识库内部格式——
217
+ // 「X是什么…是『A』vs『B』的矛盾——X(是…(…)」重复+括号嵌套,
218
+ // 直接当 LLM 输出喂给用户=「重复回答」。检测到卡片特征 → 视为
219
+ // 未命中,走降级 LLM(白箱不输出内部格式垃圾)。
220
+ if (route !== 'llm' && text && text.trim() && !isCardFormat(text)) {
221
+ whitebox = { text: text.trim(), route }
222
+ }
223
+ else if (text && isCardFormat(text)) {
224
+ console.warn('[whitebox-llm] 白箱返回知识卡片格式,降级 LLM(内部格式不直出)')
225
+ }
226
+ }
227
+ catch (err) {
228
+ // 白箱异常不阻塞——记日志后走降级
229
+ console.warn('[whitebox-llm] 白箱调用失败,降级:', String(err))
230
+ }
231
+ }
232
+
233
+ if (whitebox) {
234
+ const outputTokens = estimateTokens(whitebox.text)
235
+ // 白箱直答 = 全缓存命中(条件路由固化命中,零推理成本)
236
+ yield* wrapTextStream(whitebox.text, inputTokens, outputTokens, inputTokens)
237
+ return
238
+ }
239
+
240
+ // ---- 降级路径:白箱无把握 / 桥未就绪 ----
241
+ if (this.fallback) {
242
+ console.warn('[whitebox-llm] 白箱未命中,转降级 LLM')
243
+ yield* this.fallback.stream(options)
244
+ return
245
+ }
246
+ // 无降级可用:诚实声明(盲区 28:未来承诺不可检验——不假装回答)
247
+ const honest = '(白箱引擎未命中且降级 LLM 未配置——按白箱纪律,我诚实声明不知道,不编造答案。)'
248
+ yield* wrapTextStream(honest, inputTokens, estimateTokens(honest), 0)
249
+ }
250
+ }
251
+
252
+ /** provider 路由名(设置页「模型」下拉可见)。 */
253
+ export const WHITEBOX_PROVIDER = 'lingshu-whitebox'
254
+
255
+ /**
256
+ * 注册白箱 provider 到 DSH llm 服务(动态检测,不强依赖 llm 服务——
257
+ * 最小 host 无 llm 时跳过,插件其余功能不受影响)。
258
+ * @returns 注销函数;未注册返回 noop。
259
+ */
260
+ export function installWhiteboxLlm(ctx: Context, bridge: LingshuBridge | undefined): () => void {
261
+ // 动态检测 llm 服务(P1 模式:timer/webServer 同理)。
262
+ // 注意:不能直接读 ctx.llm——Cordis 未声明 inject 的属性访问会抛
263
+ // "cannot get property without inject";ctx.get('llm') 安全返回 undefined。
264
+ const llm = ctx.get('llm') as
265
+ | {
266
+ registerAdapter?: (p: string[], a: unknown) => { replace?: (p: string[]) => void }
267
+ registerConfigurableProviders?: (e: unknown[]) => void
268
+ stream?: (o: Record<string, unknown>) => AsyncIterable<LlmStreamChunk>
269
+ listProviders?: () => Array<{ id: string }>
270
+ }
271
+ | undefined
272
+ if (!llm?.registerAdapter) {
273
+ ctx.logger?.info?.('dsh-memory: llm 服务不可用,跳过白箱 provider 注册(最小 host)')
274
+ return () => {}
275
+ }
276
+
277
+ // 降级 LLM:白箱未命中时转发给 fallbackProvider 路由(默认 deepseek-official,
278
+ // 可在 settings.yaml 的 llm-whitebox.fallbackProvider 覆盖)。通过 llm.stream
279
+ // 重路由——保持 DSH 的全链路(重试/瀑布流/用量计量)。
280
+ const fallbackProvider = 'deepseek-official'
281
+ const fallback: WhiteboxAdapterDeps['fallback'] | undefined = llm.stream
282
+ ? {
283
+ stream(options) {
284
+ return llm.stream!({ ...options, provider: fallbackProvider })
285
+ },
286
+ }
287
+ : undefined
288
+
289
+ const adapter = new WhiteboxLlmAdapter({ bridge, fallback })
290
+ try {
291
+ llm.registerConfigurableProviders?.([{
292
+ provider: WHITEBOX_PROVIDER,
293
+ displayName: '白箱灵枢 (Whitebox)',
294
+ settingsNs: 'llm-whitebox',
295
+ settingsPath: [],
296
+ }])
297
+ const registration = llm.registerAdapter([WHITEBOX_PROVIDER], adapter)
298
+ ctx.logger?.info?.('dsh-memory: 白箱 LLM provider 已注册: %s(降级→%s)', WHITEBOX_PROVIDER, fallbackProvider)
299
+ return () => { try { registration?.replace?.([]) } catch { /* 忽略注销异常 */ } }
300
+ }
301
+ catch (err) {
302
+ ctx.logger?.warn?.('dsh-memory: 白箱 provider 注册失败: %s', String(err))
303
+ return () => {}
304
+ }
305
+ }
package/src/mutual.ts CHANGED
@@ -345,7 +345,7 @@ export function writeLastContact(opts: MutualOptions = DEFAULTS): void {
345
345
  // ---------------------------------------------------------------------------
346
346
 
347
347
  export function installMutualMaintenance(
348
- ctx: { logger: { info(m: string): void }; interval(fn: () => void, ms: number): () => void; effect(fn: () => () => void, name?: string): void },
348
+ ctx: { logger: { info(m: string): void }; effect(fn: () => () => void, name?: string): void },
349
349
  config: Partial<MutualOptions> = {},
350
350
  hooks: {
351
351
  verify?: (c: string) => Promise<{ judgment: string; best: string; d_norm: number; record_id: string }>
@@ -358,7 +358,10 @@ export function installMutualMaintenance(
358
358
  // P1 修复(GPT 审查):cordis-plugin-timer 签名是 interval(callback, delay)
359
359
  // ——此前 (ms, fn) 写反:数字被当 callback、函数被当 delay,回调永不执行,
360
360
  // 互维心跳/任务扫描实际不工作。
361
- const heartbeatDispose = ctx.interval(() => {
361
+ // P2 修复:不能用 ctx.interval——那是 cordis-plugin-timer 注入的快捷属性,
362
+ // 未声明 `timer` inject 时访问抛 "cannot get property without inject"。
363
+ // 改用 Node 原生 setInterval(互维不需要 Cordis effect 生命周期管理)。
364
+ const heartbeatTimer = setInterval(() => {
362
365
  try {
363
366
  writeHeartbeat(opts)
364
367
  // 守护 A:harness 不在 → 拉起
@@ -373,9 +376,10 @@ export function installMutualMaintenance(
373
376
  writeLastContact(opts)
374
377
  } catch (e) { /* 单次失败不中断 */ }
375
378
  }, opts.heartbeatMs)
379
+ heartbeatTimer.unref?.()
376
380
 
377
381
  // 2. 任务轮询(30s 周期,检查 A→B 任务)
378
- const taskDispose = ctx.interval(() => {
382
+ const taskTimer = setInterval(() => {
379
383
  try {
380
384
  const tasks = scanTasks(opts)
381
385
  for (const t of tasks) {
@@ -386,13 +390,14 @@ export function installMutualMaintenance(
386
390
  }
387
391
  } catch (e) { /* 单次失败不中断 */ }
388
392
  }, 30_000)
393
+ taskTimer.unref?.()
389
394
 
390
395
  // 3. effect 作用域清理
391
396
  ctx.effect(() => {
392
397
  return () => {
393
398
  try {
394
- heartbeatDispose()
395
- taskDispose()
399
+ clearInterval(heartbeatTimer)
400
+ clearInterval(taskTimer)
396
401
  } catch { /* 清理失败不阻塞 */ }
397
402
  }
398
403
  }, 'dsh-memory-mutual')
@@ -438,7 +438,9 @@ window.addEventListener('resize', function () { if (chat) chat.scrollTop = chat.
438
438
  /** 在 webServer 上挂载 /roleplay 前缀路由(页面 + API)。失败不影响插件主体。 */
439
439
  export async function installRoleplayWeb(ctx, bridge, config, disposers) {
440
440
  try {
441
- const webServer = ctx.webServer;
441
+ // 注意:不能直接读 ctx.webServer——Cordis 未声明 inject 的属性访问会抛
442
+ // "cannot get property without inject";ctx.get() 安全返回 undefined。
443
+ const webServer = ctx.get('webServer');
442
444
  if (!webServer) {
443
445
  ctx.logger.warn('dsh-memory: webServer 服务不可用,跳过角色扮演网页挂载');
444
446
  return;
package/src/tools.ts CHANGED
@@ -44,11 +44,11 @@ export const BRAIN_TOOLS = [
44
44
  // 学习与盲区
45
45
  'blindspots', 'learn', 'induce',
46
46
  // 知识飞轮
47
- 'distill', 'flywheel_report', 'transfer_test', 'calibrate',
47
+ 'distill', 'flywheel_metrics', 'transfer_test', 'calibrate',
48
48
  // 外部知识摄取
49
49
  'ingest_text', 'ingest_file', 'ingest_url', 'web_search',
50
50
  // 生命周期
51
- 'step', 'lifecycle_state',
51
+ 'lifecycle_step', 'lifecycle_state',
52
52
  // 长期记忆门(v1.15:重要性评估主动沉淀)
53
53
  'longterm_snapshot', 'promote_memories',
54
54
  // 服务
@@ -64,7 +64,7 @@ export type ToolSelection = 'core' | 'brain' | 'all' | string[]
64
64
  const READ_TOOLS = new Set([
65
65
  'recall', 'search', 'timeline', 'think', 'reason', 'predict_routes',
66
66
  'self_check', 'service_info', 'session_recall', 'gap_trend', 'transfer_test',
67
- 'cognition_report', 'self_reliability', 'emotional_bias', 'flywheel_report',
67
+ 'cognition_report', 'self_reliability', 'emotional_bias', 'flywheel_metrics',
68
68
  'distill', 'insight_report', 'prediction_stats', 'blindspots', 'pattern_separation',
69
69
  ])
70
70