@furongjun1999/dsh-memory 0.2.9 → 0.3.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/tools.d.ts CHANGED
@@ -17,7 +17,16 @@ export declare const CORE_TOOLS: readonly ["remember", "recall", "search", "time
17
17
  export declare const BRAIN_TOOLS: readonly ["remember", "recall", "search", "timeline", "session_note", "session_recall", "compact_context", "think", "relate", "reason", "predict_routes", "self_check", "gap_trend", "cognition", "cognition_report", "emotional_bias", "self_reliability", "action_log", "preflight", "recursive_reflect", "blindspots", "learn", "induce", "distill", "flywheel_report", "transfer_test", "calibrate", "ingest_text", "ingest_file", "ingest_url", "web_search", "step", "lifecycle_state", "longterm_snapshot", "promote_memories", "service_info"];
18
18
  /** tools 配置:'core' | 'brain' | 'all' | 显式名称数组。 */
19
19
  export type ToolSelection = 'core' | 'brain' | 'all' | string[];
20
- /** 按配置筛选工具名。 */
20
+ /** 按工具名判定并发安全(只读查询 true;写操作 false) */
21
+ export declare function isToolConcurrencySafe(name: string): boolean;
22
+ /**
23
+ * P1 完善(GPT 审查·tools:all 自动扩权):即使 selection='all' 也排除的宿主级
24
+ * 风险工具——此前后端新增工具即自动暴露给 Agent(动态扩权无 denylist)。
25
+ * 排除原则:宿主命令执行/权限终裁/外部设备/自主生命周期控制/角色卡写入。
26
+ * 显式名称数组(显式配置)不受此名单限制(配置者已明确选择)。
27
+ */
28
+ export declare const RISK_TOOLS: Set<string>;
29
+ /** 按配置筛选工具名('all' 时排除 RISK_TOOLS 宿主级危险工具)。 */
21
30
  export declare function selectTools(all: string[], selection: ToolSelection): string[];
22
31
  /** 把 MCP JSON Schema 的属性表转换为 ParameterSchemaSpec。 */
23
32
  export declare function schemaToParameters(inputSchema: Record<string, unknown> | undefined): ParameterSchemaSpec;
package/lib/tools.js CHANGED
@@ -49,10 +49,44 @@ export const BRAIN_TOOLS = [
49
49
  // 服务
50
50
  'service_info',
51
51
  ];
52
- /** 按配置筛选工具名。 */
52
+ /** P1 修复(GPT 审查):只读/无副作用工具才允许并发——写操作(记忆/关系/
53
+ * 生命周期/摄取/学习)标 false,防止 DSH 并行调用导致 SQLite 写入竞争、
54
+ * 状态顺序错乱、关系边重复等。 */
55
+ const READ_TOOLS = new Set([
56
+ 'recall', 'search', 'timeline', 'think', 'reason', 'predict_routes',
57
+ 'self_check', 'service_info', 'session_recall', 'gap_trend', 'transfer_test',
58
+ 'cognition_report', 'self_reliability', 'emotional_bias', 'flywheel_report',
59
+ 'distill', 'insight_report', 'prediction_stats', 'blindspots', 'pattern_separation',
60
+ ]);
61
+ /** 按工具名判定并发安全(只读查询 true;写操作 false) */
62
+ export function isToolConcurrencySafe(name) {
63
+ return READ_TOOLS.has(name);
64
+ }
65
+ /**
66
+ * P1 完善(GPT 审查·tools:all 自动扩权):即使 selection='all' 也排除的宿主级
67
+ * 风险工具——此前后端新增工具即自动暴露给 Agent(动态扩权无 denylist)。
68
+ * 排除原则:宿主命令执行/权限终裁/外部设备/自主生命周期控制/角色卡写入。
69
+ * 显式名称数组(显式配置)不受此名单限制(配置者已明确选择)。
70
+ */
71
+ export const RISK_TOOLS = new Set([
72
+ 'run_command', // 宿主命令执行(安全边界:不由 Agent 动态调用)
73
+ 'designer_decide', // 设计者裁决(fail-closed 权限,绝不由 Agent 调用)
74
+ 'device_call', // 外部设备统一调用(屏幕/进程/音频/浏览器)
75
+ 'see', // 视觉感知(身体工具)
76
+ 'world3d', // 时空 3D 重建(身体工具)
77
+ 'vprim', // 视觉原语(身体工具)
78
+ 'visual_check', // 视觉面检查(身体工具)
79
+ 'start_lifecycle', // 启动自主生命周期循环
80
+ 'stop_lifecycle', // 中断生命周期循环
81
+ 'web_ingest_search', // 外部搜索摄取(网络调用 + 写知识层)
82
+ 'role_create', // 角色卡创建(写角色数据)
83
+ 'role_import', // 角色导入(写角色数据)
84
+ 'role_block', // 角色扮演注入块组装
85
+ ]);
86
+ /** 按配置筛选工具名('all' 时排除 RISK_TOOLS 宿主级危险工具)。 */
53
87
  export function selectTools(all, selection) {
54
88
  if (selection === 'all')
55
- return all;
89
+ return all.filter((name) => !RISK_TOOLS.has(name));
56
90
  const allowed = new Set(selection === 'core' ? CORE_TOOLS
57
91
  : selection === 'brain' ? BRAIN_TOOLS
58
92
  : selection);
@@ -107,7 +141,11 @@ function toValueSpec(raw) {
107
141
  }
108
142
  case 'object': {
109
143
  const props = schemaToParameters(node);
110
- return { type: 'object', properties: props, additionalProperties: true, ...annotations };
144
+ // P1 修复(GPT 审查):尊重后端 additionalProperties 声明(false 保留),
145
+ // 不再无条件 true——此前后端写 false 也会被覆盖成 true,DSH 侧认为
146
+ // 参数合法但后端拒绝。
147
+ const additional = node['additionalProperties'] === false ? false : true;
148
+ return { type: 'object', properties: props, additionalProperties: additional, ...annotations };
111
149
  }
112
150
  default:
113
151
  return { type: 'json', ...annotations };
@@ -142,9 +180,16 @@ export async function registerLingshuTools(ctx, bridge, opts) {
142
180
  },
143
181
  },
144
182
  timeoutMs: 120_000,
145
- isConcurrencySafe: () => true,
146
- async execute(args) {
147
- const result = await bridge.callTool(tool.name, args);
183
+ // P1 修复(GPT 审查):按工具分类——只读查询可并发,写操作串行
184
+ isConcurrencySafe: () => isToolConcurrencySafe(tool.name),
185
+ // P1 修复(GPT 审查):接收 exec.signal(用户取消/上层超时)——
186
+ // 此前完全忽略取消,取消后写操作(remember/relate/ingest 等)仍可能产生副作用
187
+ async execute(args, exec) {
188
+ if (exec.signal.aborted)
189
+ throw new Error(`灵枢 ${tool.name} 已取消`);
190
+ const result = await bridge.callTool(tool.name, args, exec.signal);
191
+ if (exec.signal.aborted)
192
+ throw new Error(`灵枢 ${tool.name} 已取消`);
148
193
  if (result.isError) {
149
194
  throw new Error(extractText(result.content) || `灵枢 ${tool.name} 执行失败`);
150
195
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
- {
1
+ {
2
2
  "name": "@furongjun1999/dsh-memory",
3
- "version": "0.2.9",
3
+ "version": "0.3.0",
4
4
  "description": "灵枢(Lingshu·líng shū)DeepSeek Harness 插件:完整大脑——长期记忆/知识飞轮/自我认知/递归反思接入 DSH,对话自动沉淀进记忆库",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -13,6 +13,7 @@
13
13
  },
14
14
  "files": [
15
15
  "lib",
16
+ "src",
16
17
  "README.md",
17
18
  "cordis.yml.example",
18
19
  "cordis.patch.yml",
@@ -51,6 +52,7 @@
51
52
  "scripts": {
52
53
  "build": "tsc -p tsconfig.json",
53
54
  "test": "npm run build && node --import tsx --test \"test/*.test.ts\"",
55
+ "prepare": "npm run build",
54
56
  "prepublishOnly": "npm run build && npm test"
55
57
  },
56
58
  "repository": {
@@ -58,7 +60,7 @@
58
60
  "url": "https://github.com/FuRongJun-1999/dsh-memory.git"
59
61
  },
60
62
  "engines": {
61
- "node": ">=18"
63
+ "node": ">=22.19"
62
64
  },
63
65
  "dsh": {
64
66
  "bundle": {
package/src/bridge.ts ADDED
@@ -0,0 +1,296 @@
1
+ /**
2
+ * 灵枢 MCP stdio 桥:管理灵枢(AEIS)Python 子进程的生命周期,
3
+ * 通过逐行 JSON-RPC 完成握手、工具发现与调用。
4
+ *
5
+ * 与官方 @deepseek-ai/dsh-mcp-client 不同,本桥零运行时依赖(不引入
6
+ * @modelcontextprotocol/sdk),直接实现灵枢 server 使用的 2024-11-05 协议
7
+ * 子集——与灵枢库 D-005「核心零外部依赖」的工程哲学一致。
8
+ */
9
+
10
+ import { spawn, type ChildProcess } from 'node:child_process'
11
+ import { createInterface } from 'node:readline'
12
+ import { mkdirSync } from 'node:fs'
13
+ import { dirname } from 'node:path'
14
+
15
+ /** 灵枢 MCP server 暴露的原始工具(tools/list 结果项)。 */
16
+ export interface McpTool {
17
+ name: string
18
+ description: string
19
+ inputSchema?: Record<string, unknown>
20
+ }
21
+
22
+ /** 一次 tools/call 的标准化结果。 */
23
+ export interface McpCallResult {
24
+ content: Array<{ type: string; text?: string; [key: string]: unknown }>
25
+ isError: boolean
26
+ }
27
+
28
+ /** 启动灵枢子进程的配置。 */
29
+ export interface BridgeOptions {
30
+ /** Python 可执行文件(或 aeis-mcp console script),默认 python。 */
31
+ python: string
32
+ /** 传给 python 的参数,默认 ['-m', 'aeis.mcp.server']。 */
33
+ args: string[]
34
+ /** 追加到子进程的环境变量(AEIS_DB / AEIS_IDENTITY / 密钥等)。 */
35
+ env: Record<string, string>
36
+ /** 子进程工作目录。 */
37
+ cwd?: string
38
+ /** 单次工具调用超时(毫秒),默认 60s。 */
39
+ timeoutMs: number
40
+ /** 断线重连的最大间隔(毫秒),默认 30s。 */
41
+ maxRetryDelayMs: number
42
+ }
43
+
44
+ interface PendingCall {
45
+ resolve: (value: unknown) => void
46
+ reject: (reason: Error) => void
47
+ timer: NodeJS.Timeout
48
+ }
49
+
50
+ export class LingshuBridge {
51
+ private readonly options: BridgeOptions
52
+ private proc: ChildProcess | null = null
53
+ private rl: ReturnType<typeof createInterface> | null = null
54
+ private nextId = 1
55
+ private pending = new Map<number, PendingCall>()
56
+ private started = false
57
+ private disposed = false
58
+ private retryDelayMs = 1000
59
+ private retryTimer: NodeJS.Timeout | null = null
60
+ private bootQueue: Array<(ok: boolean) => void> = []
61
+ private readyState: 'pending' | 'ok' | 'failed' = 'pending'
62
+
63
+ constructor(options: BridgeOptions) {
64
+ this.options = options
65
+ }
66
+
67
+ /**
68
+ * 启动灵枢进程并完成握手(initialize → notifications/initialized)。
69
+ * 进程崩溃后自动指数退避重启,并重新握手。
70
+ */
71
+ start(): void {
72
+ if (this.started || this.disposed) return
73
+ this.started = true
74
+ this.spawnAndHandshake()
75
+ }
76
+
77
+ /** 拉取灵枢的全部工具清单(每次实时请求,不缓存)。 */
78
+ async listTools(): Promise<McpTool[]> {
79
+ const result = await this.request('tools/list', {})
80
+ const tools = result as { tools?: McpTool[] }
81
+ return tools.tools ?? []
82
+ }
83
+
84
+ /** 调用灵枢的一个工具,返回标准化 MCP 结果。 */
85
+ async callTool(name: string, args: Record<string, unknown>,
86
+ signal?: AbortSignal): Promise<McpCallResult> {
87
+ const result = await this.request('tools/call', { name, arguments: args }, signal)
88
+ return result as McpCallResult
89
+ }
90
+
91
+ /** 关闭进程并释放资源(写 stdin EOF 优雅退出,超时兜底 kill)。 */
92
+ dispose(): void {
93
+ this.disposed = true
94
+ if (this.retryTimer) clearTimeout(this.retryTimer)
95
+ const proc = this.proc
96
+ if (!proc || proc.exitCode !== null) return
97
+ try {
98
+ proc.stdin?.end()
99
+ } catch {
100
+ /* 已关闭则忽略 */
101
+ }
102
+ const killer = setTimeout(() => proc.kill(), 2000)
103
+ killer.unref()
104
+ }
105
+
106
+ /** 进程是否存活。 */
107
+ get alive(): boolean {
108
+ return this.proc !== null && this.proc.exitCode === null
109
+ }
110
+
111
+ /** 桥是否已握手就绪(工具注册用;防止轮询访问 private readyState)。 */
112
+ isReady(): boolean {
113
+ return this.readyState === 'ok'
114
+ }
115
+
116
+ /** 等待握手完成(用于 apply 阶段同步就绪)。 */
117
+ waitReady(): Promise<boolean> {
118
+ if (this.readyState === 'ok') return Promise.resolve(true)
119
+ if (this.readyState === 'failed') return Promise.resolve(false)
120
+ return new Promise((resolve) => this.bootQueue.push(resolve))
121
+ }
122
+
123
+ private spawnAndHandshake(): void {
124
+ if (this.disposed) return
125
+ const { python, args, env, cwd } = this.options
126
+ const childEnv = { ...process.env, ...env }
127
+ // 确保 DB 目录存在(灵枢 server 也会防御性创建,这里提前为可读错误)
128
+ const dbPath = env['AEIS_DB']
129
+ if (dbPath && dbPath !== ':memory:') {
130
+ try {
131
+ mkdirSync(dirname(dbPath), { recursive: true })
132
+ } catch {
133
+ /* 目录创建失败由灵枢侧兜底 */
134
+ }
135
+ }
136
+ const proc = spawn(python, args, {
137
+ env: childEnv,
138
+ cwd,
139
+ stdio: ['pipe', 'pipe', 'pipe'],
140
+ windowsHide: true,
141
+ })
142
+ this.proc = proc
143
+ this.rl = createInterface({ input: proc.stdout!, crlfDelay: Infinity })
144
+
145
+ proc.stderr?.on('data', (chunk: Buffer) => {
146
+ // stderr 透传日志(灵枢把日志写在 stderr,避免污染协议流)
147
+ const text = chunk.toString('utf8').trim()
148
+ if (text) console.error(`[lingshu-bridge] ${text}`)
149
+ })
150
+
151
+ this.rl.on('line', (line: string) => {
152
+ if (!line.trim()) return
153
+ let msg: Record<string, unknown>
154
+ try {
155
+ msg = JSON.parse(line)
156
+ } catch {
157
+ console.error(`[lingshu-bridge] 非 JSON 输出: ${line.slice(0, 200)}`)
158
+ return
159
+ }
160
+ if (typeof msg['id'] === 'number') {
161
+ this.settle(msg['id'] as number, msg)
162
+ }
163
+ })
164
+
165
+ proc.on('error', (err) => {
166
+ // 进程无法启动(python 不存在等)——响亮失败
167
+ if (!this.disposed) {
168
+ console.error(`[lingshu-bridge] 灵枢进程启动失败: ${err.message}`)
169
+ this.readyState = 'failed'
170
+ this.flushBootQueue(false)
171
+ this.scheduleRetry()
172
+ }
173
+ })
174
+
175
+ proc.on('exit', (code, signal) => {
176
+ console.error(`[lingshu-bridge] 灵枢进程退出 code=${code} signal=${signal}`)
177
+ this.rl?.close()
178
+ this.rl = null
179
+ this.rejectAll(new Error(`灵枢进程已退出(code=${code} signal=${signal ?? 'none'})`))
180
+ if (!this.disposed) {
181
+ // 有挂起请求的失败是异常的;仅启动失败的等待者得到 false
182
+ this.readyState = 'failed'
183
+ this.flushBootQueue(false)
184
+ this.scheduleRetry()
185
+ }
186
+ })
187
+
188
+ void this.handshake()
189
+ }
190
+
191
+ private async handshake(): Promise<void> {
192
+ try {
193
+ await this.request('initialize', {
194
+ protocolVersion: '2024-11-05',
195
+ capabilities: {},
196
+ clientInfo: { name: 'dsh-memory', version: '0.1.0' },
197
+ })
198
+ // 初始化通知:无 id、无响应
199
+ this.writeRaw({ jsonrpc: '2.0', method: 'notifications/initialized' })
200
+ this.retryDelayMs = 1000
201
+ this.readyState = 'ok'
202
+ this.flushBootQueue(true)
203
+ } catch (err) {
204
+ if (!this.disposed) {
205
+ console.error(`[lingshu-bridge] 握手失败: ${(err as Error).message}`)
206
+ this.readyState = 'failed'
207
+ this.flushBootQueue(false)
208
+ this.scheduleRetry()
209
+ }
210
+ }
211
+ }
212
+
213
+ private scheduleRetry(): void {
214
+ if (this.disposed || this.retryTimer) return
215
+ const delay = this.retryDelayMs
216
+ this.retryDelayMs = Math.min(this.retryDelayMs * 2, this.options.maxRetryDelayMs)
217
+ this.retryTimer = setTimeout(() => {
218
+ this.retryTimer = null
219
+ if (!this.disposed) this.spawnAndHandshake()
220
+ }, delay)
221
+ this.retryTimer.unref()
222
+ }
223
+
224
+ private writeRaw(msg: Record<string, unknown>): void {
225
+ const proc = this.proc
226
+ if (!proc || !proc.stdin?.writable) {
227
+ throw new Error('灵枢进程不可写')
228
+ }
229
+ proc.stdin.write(JSON.stringify(msg) + '\n')
230
+ }
231
+
232
+ private request(method: string, params: Record<string, unknown>,
233
+ signal?: AbortSignal): Promise<unknown> {
234
+ const id = this.nextId++
235
+ const timeout = this.options.timeoutMs
236
+ // P0 修复(GPT 审查):定时器直接调 settle——settle 内部会 delete + clearTimeout
237
+ // 并 reject。此前先 pending.delete(id) 再 settle(),settle 找不到 entry 直接
238
+ // return,Promise 永不 resolve/reject(超时逻辑完全失效,子进程挂死时插件永久挂住)。
239
+ const timer = setTimeout(() => {
240
+ this.settle(id, {
241
+ error: { code: -32000, message: `灵枢调用超时(${timeout}ms):${method}` },
242
+ })
243
+ }, timeout)
244
+ if (signal?.aborted) {
245
+ clearTimeout(timer)
246
+ return Promise.reject(new Error(`已取消:${method}`))
247
+ }
248
+ const onAbort = (): void => {
249
+ this.settle(id, { error: { code: -32800, message: `已取消:${method}` } })
250
+ }
251
+ signal?.addEventListener('abort', onAbort, { once: true })
252
+ const promise = new Promise<unknown>((resolve, reject) => {
253
+ this.pending.set(id, { resolve, reject, timer })
254
+ })
255
+ // settle 后移除 abort 监听,避免内存积累
256
+ promise.finally(() => {
257
+ signal?.removeEventListener('abort', onAbort)
258
+ }).catch(() => { /* finally 链的 catch 防未处理拒绝 */ })
259
+ try {
260
+ this.writeRaw({ jsonrpc: '2.0', id, method, params })
261
+ } catch (err) {
262
+ clearTimeout(timer)
263
+ this.pending.delete(id)
264
+ signal?.removeEventListener('abort', onAbort)
265
+ return Promise.reject(err)
266
+ }
267
+ return promise
268
+ }
269
+
270
+ private settle(id: number, msg: Record<string, unknown>): void {
271
+ const entry = this.pending.get(id)
272
+ if (!entry) return
273
+ this.pending.delete(id)
274
+ clearTimeout(entry.timer)
275
+ if (msg['error']) {
276
+ const err = msg['error'] as { message?: string; code?: number }
277
+ entry.reject(new Error(`灵枢错误 ${err.code ?? ''}: ${err.message ?? 'unknown'}`))
278
+ } else {
279
+ entry.resolve(msg['result'])
280
+ }
281
+ }
282
+
283
+ private rejectAll(reason: Error): void {
284
+ for (const [, entry] of this.pending) {
285
+ clearTimeout(entry.timer)
286
+ entry.reject(reason)
287
+ }
288
+ this.pending.clear()
289
+ }
290
+
291
+ private flushBootQueue(ok: boolean): void {
292
+ const queue = this.bootQueue
293
+ this.bootQueue = []
294
+ for (const cb of queue) cb(ok)
295
+ }
296
+ }
package/src/hooks.ts ADDED
@@ -0,0 +1,138 @@
1
+ /**
2
+ * 自动记忆钩子:把 DSH 的会话事件(经统一的 session/event 分发)沉淀进灵枢。
3
+ *
4
+ * 与 DSH 的 session-persistence 插件(保存会话日志)不同,这里是"语义沉淀":
5
+ * 用户消息写入灵枢知识层(带去重与重要性),agent 回复与工具结果可选开启。
6
+ * 只记忆真实用户消息(source.kind === 'user'),过滤插件注入的噪音。
7
+ *
8
+ * P1 完善(GPT 审查·自动记忆只写不读):新增 autoRecall——通过
9
+ * system-prompt/assemble 事件(waterfall,异步允许)在每次模型请求组装
10
+ * system prompt 时自动注入灵枢最近记忆(timeline),让记忆"自动可用"
11
+ * 而不只依赖 Agent 主动调用 recall/think 工具。失败静默(不影响请求)。
12
+ */
13
+
14
+ import '@deepseek-ai/dsh-session'
15
+ import '@deepseek-ai/dsh-system-prompt'
16
+ import type { Context } from '@deepseek-ai/cordis'
17
+ import type { SessionEvent } from '@deepseek-ai/dsh-session'
18
+ import type { ContentBlock } from '@deepseek-ai/dsh-llm'
19
+ import type { LingshuBridge } from './bridge.js'
20
+
21
+ /** 自动记忆开关。 */
22
+ export interface MemoryHooksOptions {
23
+ /** 用户消息 → remember(默认 true)。 */
24
+ userMessage: boolean
25
+ /** agent 回复 → remember(默认 false,防噪音)。 */
26
+ assistantMessage: boolean
27
+ /** 工具结果 → remember(默认 false,噪音大)。 */
28
+ toolResult: boolean
29
+ /** 写入记忆的重要性(0~1),默认 0.6。 */
30
+ importance: number
31
+ /** 自动召回注入:模型请求前自动注入灵枢最近记忆(默认 true,失败静默)。 */
32
+ autoRecall: boolean
33
+ /** 自动召回条数(默认 4)。 */
34
+ autoRecallLimit: number
35
+ /** 自动记忆脱敏:写入前过滤敏感信息(密钥/密码/令牌/身份证/手机号,默认 true)。 */
36
+ desensitize: boolean
37
+ }
38
+
39
+ /** 从 ContentBlock[] 提取纯文本。 */
40
+ function extractText(blocks: ContentBlock[]): string {
41
+ const parts: string[] = []
42
+ for (const block of blocks) {
43
+ if (block && typeof block === 'object' && block.type === 'text' && typeof block.text === 'string') {
44
+ parts.push(block.text)
45
+ }
46
+ }
47
+ return parts.join('\n').trim()
48
+ }
49
+
50
+ /**
51
+ * 敏感信息模式(GPT 审查·自动记忆脱敏):写入记忆库前过滤凭据/个人标识。
52
+ * 命中 → 替换为 [已过滤:类别](保留对话主体);过滤后只剩占位符/空白 → 整条跳过。
53
+ * 纯内容过滤,不涉及身份认证——开源场景下的隐私保护。
54
+ */
55
+ const SENSITIVE_PATTERNS: Array<{ re: RegExp; label: string }> = [
56
+ { re: /sk-[A-Za-z0-9_-]{8,}/g, label: 'API密钥' },
57
+ { re: /\b(?:api[_-]?key|apikey|access[_-]?token)\b\s*[:=]\s*[^\s,,。;;]+/gi, label: 'API密钥' },
58
+ { re: /\b(?:password|passwd|pwd)\b\s*[:=]\s*[^\s,,。;;]+/gi, label: '密码' },
59
+ { re: /Bearer\s+[A-Za-z0-9._~+/=-]{8,}/gi, label: '令牌' },
60
+ // 中文密码:值限定非中文连续串(凭据特征),避免误伤「密码是重要的安全概念」
61
+ { re: /密码\s*[::是]\s*[A-Za-z0-9_@#$%^&*!.-]{4,}/g, label: '密码' },
62
+ { re: /\b\d{17}[\dXx]\b/g, label: '身份证号' },
63
+ { re: /\b1[3-9]\d{9}\b/g, label: '手机号' },
64
+ ]
65
+
66
+ /** 脱敏:替换敏感片段;返回 null 表示整条都是敏感内容(应跳过写入)。 */
67
+ export function desensitize(text: string): string | null {
68
+ let out = text
69
+ for (const { re, label } of SENSITIVE_PATTERNS) {
70
+ out = out.replace(re, `[已过滤:${label}]`)
71
+ }
72
+ // 过滤后只剩占位符/空白 → 纯凭据消息,不写(或全部被替换)
73
+ const residue = out.replace(/\[已过滤:[^\]]+\]/g, '').trim()
74
+ if (!residue) return null
75
+ return out
76
+ }
77
+
78
+ /** 安装自动记忆钩子(effect 作用域内,随插件卸载自动移除)。 */
79
+ export function installMemoryHooks(ctx: Context, bridge: LingshuBridge, opts: MemoryHooksOptions): void {
80
+ const memorize = (tool: string, args: Record<string, unknown>): void => {
81
+ void bridge
82
+ .callTool(tool, args)
83
+ .catch((err: Error) => ctx.logger.warn(`dsh-memory: ${tool} 自动记忆失败: ${err.message}`))
84
+ }
85
+
86
+ // P1 完善(GPT 审查·自动记忆脱敏):写入前过滤敏感信息(默认开启)。
87
+ // 命中敏感模式 → 替换为 [已过滤:类别];纯凭据消息 → 跳过写入(不落库)。
88
+ const sanitize = (text: string): string | null => {
89
+ if (!opts.desensitize) return text
90
+ return desensitize(text)
91
+ }
92
+
93
+ // P1 完善(自动 recall 注入):每次模型请求组装 system prompt 时,注入灵枢最近记忆。
94
+ // 用 system-prompt/assemble 事件(waterfall)而非 llm/stream——后者请求 deep-frozen 不可改写。
95
+ if (opts.autoRecall) {
96
+ const recallLimit = Math.max(1, Math.min(10, opts.autoRecallLimit || 4))
97
+ ctx.on('system-prompt/assemble', async (assembly, _ctx, next) => {
98
+ try {
99
+ // 异步取最近记忆(失败静默——不阻塞模型请求)
100
+ const r = await bridge.callTool('timeline', { limit: recallLimit })
101
+ const text = extractText(r.content as unknown as ContentBlock[])
102
+ if (text) {
103
+ assembly.contexts.push({
104
+ name: 'lingshu:auto-recall',
105
+ text: `【灵枢最近记忆】\n${text.slice(0, 600)}`,
106
+ })
107
+ }
108
+ }
109
+ catch { /* 静默:召回失败不影响请求 */ }
110
+ return next()
111
+ })
112
+ }
113
+
114
+ ctx.on('session/event', (_session, event: SessionEvent) => {
115
+ if (event.type === 'user/message' && opts.userMessage) {
116
+ // 只记真实用户输入(kind='user'),跳过插件注入/系统上下文
117
+ if (event.data.source?.kind !== 'user') return
118
+ const text = extractText(event.data.content)
119
+ if (!text) return
120
+ const safe = sanitize(text) // 脱敏:纯凭据消息 → null → 跳过写入
121
+ if (safe === null) return
122
+ memorize('remember', { content: safe, importance: opts.importance, tags: ['dsh', 'user'] })
123
+ } else if (event.type === 'assistant/message' && opts.assistantMessage) {
124
+ const text = extractText(event.data.message.content)
125
+ if (!text) return
126
+ const safe = sanitize(text)
127
+ if (safe === null) return
128
+ memorize('remember', { content: safe, importance: opts.importance * 0.8, tags: ['dsh', 'assistant'] })
129
+ } else if (event.type === 'tool/result' && opts.toolResult) {
130
+ if (event.data.error) return
131
+ const text = extractText(event.data.message.content)
132
+ if (!text) return
133
+ const safe = sanitize(text)
134
+ if (safe === null) return
135
+ memorize('remember', { content: safe, importance: opts.importance * 0.6, tags: ['dsh', 'tool'] })
136
+ }
137
+ })
138
+ }