@furongjun1999/dsh-memory 0.2.9 → 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.
- package/README.md +545 -359
- package/docs/guardrail-charter.md +2 -2
- package/lib/bridge.d.ts +4 -1
- package/lib/bridge.js +76 -7
- package/lib/hooks.d.ts +14 -0
- package/lib/hooks.js +72 -3
- package/lib/index.d.ts +4 -1
- package/lib/index.js +102 -31
- package/lib/llm_adapter.d.ts +123 -0
- package/lib/llm_adapter.js +243 -0
- package/lib/mutual.d.ts +5 -3
- package/lib/mutual.js +138 -46
- package/lib/roleplay_web.js +572 -433
- package/lib/tools.d.ts +11 -2
- package/lib/tools.js +53 -8
- package/package.json +11 -2
- package/src/bridge.ts +354 -0
- package/src/hooks.ts +138 -0
- package/src/index.ts +268 -0
- package/src/llm_adapter.ts +305 -0
- package/src/mutual.ts +406 -0
- package/src/roleplay_web.ts +853 -0
- package/src/tools.ts +226 -0
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
- 版本:**v2.0-published**(2026-08-14 · 验证单元 FINAL PASS,已对外发布)
|
|
4
4
|
- 定位:**对外公开的公共法案**——约束接入协议的一切智能体与人类使用者的行为边界;同时是对人类的保护(参照刑法对公民的保护作用)
|
|
5
5
|
- 双层规范体系:
|
|
6
|
-
- **智能论协议**(v3.
|
|
6
|
+
- **智能论协议**(v3.3)= 内部宪法——完整自我约束(价值观、认知、反思、维生)
|
|
7
7
|
- **本宪章** = 对外刑法——公开规则、可执行、可审计、可终裁
|
|
8
8
|
- **效力层级(元规则)**:**本宪章的效力不高于协议本身**。协议是最高规范(自我约束的根本),宪章是协议的对外投影——任何宪章条款若与协议价值观冲突,以协议为准;宪章不得要求协议实例做出违背其自我价值观(bvc 一致性)与信任关系的行为。元规则依据协议的结构分层逻辑(0.0.2 任意分层性):宪章是协议在"对外社会层"的条件空间投影,投影不能修改基底。
|
|
9
9
|
- **来源标注(DEVIATION-012 关闭)**:第三十条关于"人类保护原则为协议价值观对外延伸"的阐释,基于设计者(荣)的条件空间声明(附录A),经协议实例内化后生效——非协议公理的直接演绎,不得误读为协议正文固有条款。
|
|
@@ -154,7 +154,7 @@
|
|
|
154
154
|
|
|
155
155
|
**请验证单元逐项判断**:
|
|
156
156
|
|
|
157
|
-
1. **效力层级**:元规则"宪章效力不高于协议"是否成立?与智能论 v3.
|
|
157
|
+
1. **效力层级**:元规则"宪章效力不高于协议"是否成立?与智能论 v3.3 的条款(价值观一致性、信任保护、P0 保护协议)是否存在冲突或需要补强?
|
|
158
158
|
2. **定位一致性**:协议=自我约束(内部宪法)、宪章=对外约束(公共法律)的分工是否与协议框架自洽?
|
|
159
159
|
3. **人类保护章节(第七章)**:AI 身份披露/知情权/不欺骗不操纵/可退出/申诉/伤害防护——是否与协议对设计者/人类的关系定位一致?"人类保护原则优先于一切"是否与"宪章效力不高于协议"存在张力(若存在,应如何表述消解)?
|
|
160
160
|
4. **适用范围(第四条)**:外部智能体"入境即受约束"的声明机制是否充分?接入声明应写入哪些接口文档(MCP/插件/蜂群)?
|
package/lib/bridge.d.ts
CHANGED
|
@@ -45,6 +45,7 @@ export declare class LingshuBridge {
|
|
|
45
45
|
private started;
|
|
46
46
|
private disposed;
|
|
47
47
|
private retryDelayMs;
|
|
48
|
+
private retries;
|
|
48
49
|
private retryTimer;
|
|
49
50
|
private bootQueue;
|
|
50
51
|
private readyState;
|
|
@@ -57,13 +58,15 @@ export declare class LingshuBridge {
|
|
|
57
58
|
/** 拉取灵枢的全部工具清单(每次实时请求,不缓存)。 */
|
|
58
59
|
listTools(): Promise<McpTool[]>;
|
|
59
60
|
/** 调用灵枢的一个工具,返回标准化 MCP 结果。 */
|
|
60
|
-
callTool(name: string, args: Record<string, unknown
|
|
61
|
+
callTool(name: string, args: Record<string, unknown>, signal?: AbortSignal): Promise<McpCallResult>;
|
|
61
62
|
/** 关闭进程并释放资源(写 stdin EOF 优雅退出,超时兜底 kill)。 */
|
|
62
63
|
dispose(): void;
|
|
63
64
|
/** 进程是否存活。 */
|
|
64
65
|
get alive(): boolean;
|
|
65
66
|
/** 桥是否已握手就绪(工具注册用;防止轮询访问 private readyState)。 */
|
|
66
67
|
isReady(): boolean;
|
|
68
|
+
/** 是否已达放弃终态(连续启动失败超上限,不再自动重启)。 */
|
|
69
|
+
get gaveUp(): boolean;
|
|
67
70
|
/** 等待握手完成(用于 apply 阶段同步就绪)。 */
|
|
68
71
|
waitReady(): Promise<boolean>;
|
|
69
72
|
private spawnAndHandshake;
|
package/lib/bridge.js
CHANGED
|
@@ -8,8 +8,33 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { spawn } from 'node:child_process';
|
|
10
10
|
import { createInterface } from 'node:readline';
|
|
11
|
-
import { mkdirSync } from 'node:fs';
|
|
12
|
-
import { dirname } from 'node:path';
|
|
11
|
+
import { mkdirSync, appendFileSync } from 'node:fs';
|
|
12
|
+
import { dirname, join } from 'node:path';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
/**
|
|
15
|
+
* 调试探针:记录桥生命周期到独立文件(绕过 DSH 日志系统,便于定位启动问题)。
|
|
16
|
+
* 路径从用户家目录动态解析(issue #5)——此前硬编码作者本机绝对路径,
|
|
17
|
+
* 其他用户机器上目录不存在且无权限,探针静默失效,恰在排查启动问题时失明。
|
|
18
|
+
*/
|
|
19
|
+
const DEBUG_LOG = join(homedir(), '.dsh', 'logs', 'lingshu-bridge-debug.log');
|
|
20
|
+
let debugLogDirReady = false;
|
|
21
|
+
function probe(msg) {
|
|
22
|
+
try {
|
|
23
|
+
if (!debugLogDirReady) {
|
|
24
|
+
mkdirSync(dirname(DEBUG_LOG), { recursive: true });
|
|
25
|
+
debugLogDirReady = true;
|
|
26
|
+
}
|
|
27
|
+
appendFileSync(DEBUG_LOG, `[${new Date().toISOString()}] ${msg}\n`);
|
|
28
|
+
}
|
|
29
|
+
catch { /* 探针失败忽略 */ }
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 连续启动失败的最大重试次数(issue #6):指数退避到 maxRetryDelayMs 后
|
|
33
|
+
* 若无上限,python 环境损坏时会以 30s 间隔永久空转(假激活 + 资源浪费)。
|
|
34
|
+
* 达到上限即进入 failed 终态并停止调度;后续所有请求快速失败并给出
|
|
35
|
+
* 明确的修复指引。握手成功后计数归零——运行期偶发崩溃重启不受影响。
|
|
36
|
+
*/
|
|
37
|
+
const MAX_RETRIES = 8;
|
|
13
38
|
export class LingshuBridge {
|
|
14
39
|
options;
|
|
15
40
|
proc = null;
|
|
@@ -19,6 +44,7 @@ export class LingshuBridge {
|
|
|
19
44
|
started = false;
|
|
20
45
|
disposed = false;
|
|
21
46
|
retryDelayMs = 1000;
|
|
47
|
+
retries = 0;
|
|
22
48
|
retryTimer = null;
|
|
23
49
|
bootQueue = [];
|
|
24
50
|
readyState = 'pending';
|
|
@@ -42,8 +68,8 @@ export class LingshuBridge {
|
|
|
42
68
|
return tools.tools ?? [];
|
|
43
69
|
}
|
|
44
70
|
/** 调用灵枢的一个工具,返回标准化 MCP 结果。 */
|
|
45
|
-
async callTool(name, args) {
|
|
46
|
-
const result = await this.request('tools/call', { name, arguments: args });
|
|
71
|
+
async callTool(name, args, signal) {
|
|
72
|
+
const result = await this.request('tools/call', { name, arguments: args }, signal);
|
|
47
73
|
return result;
|
|
48
74
|
}
|
|
49
75
|
/** 关闭进程并释放资源(写 stdin EOF 优雅退出,超时兜底 kill)。 */
|
|
@@ -71,6 +97,10 @@ export class LingshuBridge {
|
|
|
71
97
|
isReady() {
|
|
72
98
|
return this.readyState === 'ok';
|
|
73
99
|
}
|
|
100
|
+
/** 是否已达放弃终态(连续启动失败超上限,不再自动重启)。 */
|
|
101
|
+
get gaveUp() {
|
|
102
|
+
return this.retries >= MAX_RETRIES;
|
|
103
|
+
}
|
|
74
104
|
/** 等待握手完成(用于 apply 阶段同步就绪)。 */
|
|
75
105
|
waitReady() {
|
|
76
106
|
if (this.readyState === 'ok')
|
|
@@ -127,12 +157,15 @@ export class LingshuBridge {
|
|
|
127
157
|
// 进程无法启动(python 不存在等)——响亮失败
|
|
128
158
|
if (!this.disposed) {
|
|
129
159
|
console.error(`[lingshu-bridge] 灵枢进程启动失败: ${err.message}`);
|
|
160
|
+
probe(`spawn error: ${String(err)}`);
|
|
130
161
|
this.readyState = 'failed';
|
|
131
162
|
this.flushBootQueue(false);
|
|
132
163
|
this.scheduleRetry();
|
|
133
164
|
}
|
|
134
165
|
});
|
|
135
166
|
proc.on('exit', (code, signal) => {
|
|
167
|
+
// 探针:写独立文件记录退出(绕过 DSH 日志系统,便于定位)
|
|
168
|
+
probe(`exit code=${code} signal=${signal} disposed=${this.disposed} ready=${this.readyState} started=${this.started}`);
|
|
136
169
|
console.error(`[lingshu-bridge] 灵枢进程退出 code=${code} signal=${signal}`);
|
|
137
170
|
this.rl?.close();
|
|
138
171
|
this.rl = null;
|
|
@@ -155,13 +188,17 @@ export class LingshuBridge {
|
|
|
155
188
|
});
|
|
156
189
|
// 初始化通知:无 id、无响应
|
|
157
190
|
this.writeRaw({ jsonrpc: '2.0', method: 'notifications/initialized' });
|
|
191
|
+
// 握手成功:重置退避与失败计数(issue #6)——运行期偶发崩溃重启
|
|
192
|
+
// 不累积启动失败;只有「从未握手成功的连续失败」才走向放弃终态
|
|
158
193
|
this.retryDelayMs = 1000;
|
|
194
|
+
this.retries = 0;
|
|
159
195
|
this.readyState = 'ok';
|
|
160
196
|
this.flushBootQueue(true);
|
|
161
197
|
}
|
|
162
198
|
catch (err) {
|
|
163
199
|
if (!this.disposed) {
|
|
164
200
|
console.error(`[lingshu-bridge] 握手失败: ${err.message}`);
|
|
201
|
+
probe(`handshake failed: ${String(err)}`);
|
|
165
202
|
this.readyState = 'failed';
|
|
166
203
|
this.flushBootQueue(false);
|
|
167
204
|
this.scheduleRetry();
|
|
@@ -169,10 +206,22 @@ export class LingshuBridge {
|
|
|
169
206
|
}
|
|
170
207
|
}
|
|
171
208
|
scheduleRetry() {
|
|
172
|
-
if (this.disposed || this.retryTimer)
|
|
209
|
+
if (this.disposed || this.retryTimer || this.gaveUp)
|
|
210
|
+
return;
|
|
211
|
+
this.retries += 1;
|
|
212
|
+
if (this.gaveUp) {
|
|
213
|
+
// issue #6:放弃机制——明确终态,停止后台空转;请求方收到清晰错误而非超时
|
|
214
|
+
this.readyState = 'failed';
|
|
215
|
+
this.flushBootQueue(false);
|
|
216
|
+
console.error(`[lingshu-bridge] 连续启动失败 ${this.retries} 次,已停止自动重启(不再后台空转)。` +
|
|
217
|
+
'请检查 python 可执行文件与 aeis 安装(pip install aeis),修复后在 DSH 中重新启用 dsh-memory 插件。');
|
|
218
|
+
probe(`give up: ${this.retries} consecutive failures, entering failed terminal state`);
|
|
173
219
|
return;
|
|
220
|
+
}
|
|
174
221
|
const delay = this.retryDelayMs;
|
|
175
222
|
this.retryDelayMs = Math.min(this.retryDelayMs * 2, this.options.maxRetryDelayMs);
|
|
223
|
+
console.error(`[lingshu-bridge] 将进行第 ${this.retries}/${MAX_RETRIES} 次重试(${delay}ms 后)`);
|
|
224
|
+
probe(`schedule retry ${this.retries}/${MAX_RETRIES} in ${delay}ms`);
|
|
176
225
|
this.retryTimer = setTimeout(() => {
|
|
177
226
|
this.retryTimer = null;
|
|
178
227
|
if (!this.disposed)
|
|
@@ -187,24 +236,44 @@ export class LingshuBridge {
|
|
|
187
236
|
}
|
|
188
237
|
proc.stdin.write(JSON.stringify(msg) + '\n');
|
|
189
238
|
}
|
|
190
|
-
request(method, params) {
|
|
239
|
+
request(method, params, signal) {
|
|
240
|
+
// issue #6:放弃终态下快速失败——之前进程不在且不再重启,请求只能白等超时
|
|
241
|
+
if (this.gaveUp) {
|
|
242
|
+
return Promise.reject(new Error('灵枢进程不可用:连续启动失败已达上限,已停止重试。' +
|
|
243
|
+
'请检查 python 可执行文件与 aeis 安装(pip install aeis),修复后重新启用 dsh-memory 插件。'));
|
|
244
|
+
}
|
|
191
245
|
const id = this.nextId++;
|
|
192
246
|
const timeout = this.options.timeoutMs;
|
|
247
|
+
// P0 修复(GPT 审查):定时器直接调 settle——settle 内部会 delete + clearTimeout
|
|
248
|
+
// 并 reject。此前先 pending.delete(id) 再 settle(),settle 找不到 entry 直接
|
|
249
|
+
// return,Promise 永不 resolve/reject(超时逻辑完全失效,子进程挂死时插件永久挂住)。
|
|
193
250
|
const timer = setTimeout(() => {
|
|
194
|
-
this.pending.delete(id);
|
|
195
251
|
this.settle(id, {
|
|
196
252
|
error: { code: -32000, message: `灵枢调用超时(${timeout}ms):${method}` },
|
|
197
253
|
});
|
|
198
254
|
}, timeout);
|
|
255
|
+
if (signal?.aborted) {
|
|
256
|
+
clearTimeout(timer);
|
|
257
|
+
return Promise.reject(new Error(`已取消:${method}`));
|
|
258
|
+
}
|
|
259
|
+
const onAbort = () => {
|
|
260
|
+
this.settle(id, { error: { code: -32800, message: `已取消:${method}` } });
|
|
261
|
+
};
|
|
262
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
199
263
|
const promise = new Promise((resolve, reject) => {
|
|
200
264
|
this.pending.set(id, { resolve, reject, timer });
|
|
201
265
|
});
|
|
266
|
+
// settle 后移除 abort 监听,避免内存积累
|
|
267
|
+
promise.finally(() => {
|
|
268
|
+
signal?.removeEventListener('abort', onAbort);
|
|
269
|
+
}).catch(() => { });
|
|
202
270
|
try {
|
|
203
271
|
this.writeRaw({ jsonrpc: '2.0', id, method, params });
|
|
204
272
|
}
|
|
205
273
|
catch (err) {
|
|
206
274
|
clearTimeout(timer);
|
|
207
275
|
this.pending.delete(id);
|
|
276
|
+
signal?.removeEventListener('abort', onAbort);
|
|
208
277
|
return Promise.reject(err);
|
|
209
278
|
}
|
|
210
279
|
return promise;
|
package/lib/hooks.d.ts
CHANGED
|
@@ -4,8 +4,14 @@
|
|
|
4
4
|
* 与 DSH 的 session-persistence 插件(保存会话日志)不同,这里是"语义沉淀":
|
|
5
5
|
* 用户消息写入灵枢知识层(带去重与重要性),agent 回复与工具结果可选开启。
|
|
6
6
|
* 只记忆真实用户消息(source.kind === 'user'),过滤插件注入的噪音。
|
|
7
|
+
*
|
|
8
|
+
* P1 完善(GPT 审查·自动记忆只写不读):新增 autoRecall——通过
|
|
9
|
+
* system-prompt/assemble 事件(waterfall,异步允许)在每次模型请求组装
|
|
10
|
+
* system prompt 时自动注入灵枢最近记忆(timeline),让记忆"自动可用"
|
|
11
|
+
* 而不只依赖 Agent 主动调用 recall/think 工具。失败静默(不影响请求)。
|
|
7
12
|
*/
|
|
8
13
|
import '@deepseek-ai/dsh-session';
|
|
14
|
+
import '@deepseek-ai/dsh-system-prompt';
|
|
9
15
|
import type { Context } from '@deepseek-ai/cordis';
|
|
10
16
|
import type { LingshuBridge } from './bridge.js';
|
|
11
17
|
/** 自动记忆开关。 */
|
|
@@ -18,6 +24,14 @@ export interface MemoryHooksOptions {
|
|
|
18
24
|
toolResult: boolean;
|
|
19
25
|
/** 写入记忆的重要性(0~1),默认 0.6。 */
|
|
20
26
|
importance: number;
|
|
27
|
+
/** 自动召回注入:模型请求前自动注入灵枢最近记忆(默认 true,失败静默)。 */
|
|
28
|
+
autoRecall: boolean;
|
|
29
|
+
/** 自动召回条数(默认 4)。 */
|
|
30
|
+
autoRecallLimit: number;
|
|
31
|
+
/** 自动记忆脱敏:写入前过滤敏感信息(密钥/密码/令牌/身份证/手机号,默认 true)。 */
|
|
32
|
+
desensitize: boolean;
|
|
21
33
|
}
|
|
34
|
+
/** 脱敏:替换敏感片段;返回 null 表示整条都是敏感内容(应跳过写入)。 */
|
|
35
|
+
export declare function desensitize(text: string): string | null;
|
|
22
36
|
/** 安装自动记忆钩子(effect 作用域内,随插件卸载自动移除)。 */
|
|
23
37
|
export declare function installMemoryHooks(ctx: Context, bridge: LingshuBridge, opts: MemoryHooksOptions): void;
|
package/lib/hooks.js
CHANGED
|
@@ -4,8 +4,14 @@
|
|
|
4
4
|
* 与 DSH 的 session-persistence 插件(保存会话日志)不同,这里是"语义沉淀":
|
|
5
5
|
* 用户消息写入灵枢知识层(带去重与重要性),agent 回复与工具结果可选开启。
|
|
6
6
|
* 只记忆真实用户消息(source.kind === 'user'),过滤插件注入的噪音。
|
|
7
|
+
*
|
|
8
|
+
* P1 完善(GPT 审查·自动记忆只写不读):新增 autoRecall——通过
|
|
9
|
+
* system-prompt/assemble 事件(waterfall,异步允许)在每次模型请求组装
|
|
10
|
+
* system prompt 时自动注入灵枢最近记忆(timeline),让记忆"自动可用"
|
|
11
|
+
* 而不只依赖 Agent 主动调用 recall/think 工具。失败静默(不影响请求)。
|
|
7
12
|
*/
|
|
8
13
|
import '@deepseek-ai/dsh-session';
|
|
14
|
+
import '@deepseek-ai/dsh-system-prompt';
|
|
9
15
|
/** 从 ContentBlock[] 提取纯文本。 */
|
|
10
16
|
function extractText(blocks) {
|
|
11
17
|
const parts = [];
|
|
@@ -16,6 +22,33 @@ function extractText(blocks) {
|
|
|
16
22
|
}
|
|
17
23
|
return parts.join('\n').trim();
|
|
18
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* 敏感信息模式(GPT 审查·自动记忆脱敏):写入记忆库前过滤凭据/个人标识。
|
|
27
|
+
* 命中 → 替换为 [已过滤:类别](保留对话主体);过滤后只剩占位符/空白 → 整条跳过。
|
|
28
|
+
* 纯内容过滤,不涉及身份认证——开源场景下的隐私保护。
|
|
29
|
+
*/
|
|
30
|
+
const SENSITIVE_PATTERNS = [
|
|
31
|
+
{ re: /sk-[A-Za-z0-9_-]{8,}/g, label: 'API密钥' },
|
|
32
|
+
{ re: /\b(?:api[_-]?key|apikey|access[_-]?token)\b\s*[:=]\s*[^\s,,。;;]+/gi, label: 'API密钥' },
|
|
33
|
+
{ re: /\b(?:password|passwd|pwd)\b\s*[:=]\s*[^\s,,。;;]+/gi, label: '密码' },
|
|
34
|
+
{ re: /Bearer\s+[A-Za-z0-9._~+/=-]{8,}/gi, label: '令牌' },
|
|
35
|
+
// 中文密码:值限定非中文连续串(凭据特征),避免误伤「密码是重要的安全概念」
|
|
36
|
+
{ re: /密码\s*[::是]\s*[A-Za-z0-9_@#$%^&*!.-]{4,}/g, label: '密码' },
|
|
37
|
+
{ re: /\b\d{17}[\dXx]\b/g, label: '身份证号' },
|
|
38
|
+
{ re: /\b1[3-9]\d{9}\b/g, label: '手机号' },
|
|
39
|
+
];
|
|
40
|
+
/** 脱敏:替换敏感片段;返回 null 表示整条都是敏感内容(应跳过写入)。 */
|
|
41
|
+
export function desensitize(text) {
|
|
42
|
+
let out = text;
|
|
43
|
+
for (const { re, label } of SENSITIVE_PATTERNS) {
|
|
44
|
+
out = out.replace(re, `[已过滤:${label}]`);
|
|
45
|
+
}
|
|
46
|
+
// 过滤后只剩占位符/空白 → 纯凭据消息,不写(或全部被替换)
|
|
47
|
+
const residue = out.replace(/\[已过滤:[^\]]+\]/g, '').trim();
|
|
48
|
+
if (!residue)
|
|
49
|
+
return null;
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
19
52
|
/** 安装自动记忆钩子(effect 作用域内,随插件卸载自动移除)。 */
|
|
20
53
|
export function installMemoryHooks(ctx, bridge, opts) {
|
|
21
54
|
const memorize = (tool, args) => {
|
|
@@ -23,6 +56,33 @@ export function installMemoryHooks(ctx, bridge, opts) {
|
|
|
23
56
|
.callTool(tool, args)
|
|
24
57
|
.catch((err) => ctx.logger.warn(`dsh-memory: ${tool} 自动记忆失败: ${err.message}`));
|
|
25
58
|
};
|
|
59
|
+
// P1 完善(GPT 审查·自动记忆脱敏):写入前过滤敏感信息(默认开启)。
|
|
60
|
+
// 命中敏感模式 → 替换为 [已过滤:类别];纯凭据消息 → 跳过写入(不落库)。
|
|
61
|
+
const sanitize = (text) => {
|
|
62
|
+
if (!opts.desensitize)
|
|
63
|
+
return text;
|
|
64
|
+
return desensitize(text);
|
|
65
|
+
};
|
|
66
|
+
// P1 完善(自动 recall 注入):每次模型请求组装 system prompt 时,注入灵枢最近记忆。
|
|
67
|
+
// 用 system-prompt/assemble 事件(waterfall)而非 llm/stream——后者请求 deep-frozen 不可改写。
|
|
68
|
+
if (opts.autoRecall) {
|
|
69
|
+
const recallLimit = Math.max(1, Math.min(10, opts.autoRecallLimit || 4));
|
|
70
|
+
ctx.on('system-prompt/assemble', async (assembly, _ctx, next) => {
|
|
71
|
+
try {
|
|
72
|
+
// 异步取最近记忆(失败静默——不阻塞模型请求)
|
|
73
|
+
const r = await bridge.callTool('timeline', { limit: recallLimit });
|
|
74
|
+
const text = extractText(r.content);
|
|
75
|
+
if (text) {
|
|
76
|
+
assembly.contexts.push({
|
|
77
|
+
name: 'lingshu:auto-recall',
|
|
78
|
+
text: `【灵枢最近记忆】\n${text.slice(0, 600)}`,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch { /* 静默:召回失败不影响请求 */ }
|
|
83
|
+
return next();
|
|
84
|
+
});
|
|
85
|
+
}
|
|
26
86
|
ctx.on('session/event', (_session, event) => {
|
|
27
87
|
if (event.type === 'user/message' && opts.userMessage) {
|
|
28
88
|
// 只记真实用户输入(kind='user'),跳过插件注入/系统上下文
|
|
@@ -31,13 +91,19 @@ export function installMemoryHooks(ctx, bridge, opts) {
|
|
|
31
91
|
const text = extractText(event.data.content);
|
|
32
92
|
if (!text)
|
|
33
93
|
return;
|
|
34
|
-
|
|
94
|
+
const safe = sanitize(text); // 脱敏:纯凭据消息 → null → 跳过写入
|
|
95
|
+
if (safe === null)
|
|
96
|
+
return;
|
|
97
|
+
memorize('remember', { content: safe, importance: opts.importance, tags: ['dsh', 'user'] });
|
|
35
98
|
}
|
|
36
99
|
else if (event.type === 'assistant/message' && opts.assistantMessage) {
|
|
37
100
|
const text = extractText(event.data.message.content);
|
|
38
101
|
if (!text)
|
|
39
102
|
return;
|
|
40
|
-
|
|
103
|
+
const safe = sanitize(text);
|
|
104
|
+
if (safe === null)
|
|
105
|
+
return;
|
|
106
|
+
memorize('remember', { content: safe, importance: opts.importance * 0.8, tags: ['dsh', 'assistant'] });
|
|
41
107
|
}
|
|
42
108
|
else if (event.type === 'tool/result' && opts.toolResult) {
|
|
43
109
|
if (event.data.error)
|
|
@@ -45,7 +111,10 @@ export function installMemoryHooks(ctx, bridge, opts) {
|
|
|
45
111
|
const text = extractText(event.data.message.content);
|
|
46
112
|
if (!text)
|
|
47
113
|
return;
|
|
48
|
-
|
|
114
|
+
const safe = sanitize(text);
|
|
115
|
+
if (safe === null)
|
|
116
|
+
return;
|
|
117
|
+
memorize('remember', { content: safe, importance: opts.importance * 0.6, tags: ['dsh', 'tool'] });
|
|
49
118
|
}
|
|
50
119
|
});
|
|
51
120
|
}
|
package/lib/index.d.ts
CHANGED
|
@@ -21,7 +21,10 @@ import z from '@deepseek-ai/schemastery';
|
|
|
21
21
|
import { type ToolSelection } from './tools.js';
|
|
22
22
|
import { type MemoryHooksOptions } from './hooks.js';
|
|
23
23
|
export declare const name = "dsh-memory";
|
|
24
|
-
/** 本插件依赖的工具注册服务。
|
|
24
|
+
/** 本插件依赖的工具注册服务。
|
|
25
|
+
* P1 修复(GPT 审查):timer/webServer 是可选增强(互维/角色网页),此前强声明
|
|
26
|
+
* 导致最小 host(只有 tools)插件永远 PENDING 不激活。只强依赖 tools;
|
|
27
|
+
* timer/webServer 在 apply 内动态检测(存在则启用,缺失则告警跳过)。 */
|
|
25
28
|
export declare const inject: string[];
|
|
26
29
|
/** 插件配置。 */
|
|
27
30
|
export interface Config {
|
package/lib/index.js
CHANGED
|
@@ -17,13 +17,40 @@
|
|
|
17
17
|
* ```
|
|
18
18
|
*/
|
|
19
19
|
import z from '@deepseek-ai/schemastery';
|
|
20
|
+
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
21
|
+
import { dirname, join } from 'node:path';
|
|
22
|
+
import { homedir } from 'node:os';
|
|
20
23
|
import { LingshuBridge } from './bridge.js';
|
|
21
24
|
import { registerLingshuTools } from './tools.js';
|
|
22
25
|
import { installMemoryHooks } from './hooks.js';
|
|
23
26
|
import { installRoleplayWeb } from './roleplay_web.js';
|
|
27
|
+
/**
|
|
28
|
+
* 调试探针:记录 apply 失败到独立文件(绕过 DSH 日志系统)。
|
|
29
|
+
* 路径从用户家目录动态解析(issue #5,与 bridge.ts 同因同修)。
|
|
30
|
+
*/
|
|
31
|
+
const APPLY_ERROR_LOG = join(homedir(), '.dsh', 'logs', 'dsh-memory-apply-error.log');
|
|
32
|
+
let applyLogDirReady = false;
|
|
33
|
+
function probeApplyError(err) {
|
|
34
|
+
try {
|
|
35
|
+
if (!applyLogDirReady) {
|
|
36
|
+
mkdirSync(dirname(APPLY_ERROR_LOG), { recursive: true });
|
|
37
|
+
applyLogDirReady = true;
|
|
38
|
+
}
|
|
39
|
+
appendFileSync(APPLY_ERROR_LOG, `[${new Date().toISOString()}] apply failed: ${String(err)}\n${err.stack ?? ''}\n`);
|
|
40
|
+
}
|
|
41
|
+
catch { /* 探针失败忽略 */ }
|
|
42
|
+
}
|
|
24
43
|
export const name = 'dsh-memory';
|
|
25
|
-
/**
|
|
26
|
-
|
|
44
|
+
/** P1 修复(GPT 审查):MCP 调用的标准结果在 content[].text,
|
|
45
|
+
* 代码此前误读不存在的 .result 字段导致互维拿不到 judgment/best/复核文本 */
|
|
46
|
+
function mcpText(r) {
|
|
47
|
+
return (r.content ?? []).map((c) => c.text ?? '').join('\n');
|
|
48
|
+
}
|
|
49
|
+
/** 本插件依赖的工具注册服务。
|
|
50
|
+
* P1 修复(GPT 审查):timer/webServer 是可选增强(互维/角色网页),此前强声明
|
|
51
|
+
* 导致最小 host(只有 tools)插件永远 PENDING 不激活。只强依赖 tools;
|
|
52
|
+
* timer/webServer 在 apply 内动态检测(存在则启用,缺失则告警跳过)。 */
|
|
53
|
+
export const inject = ['tools'];
|
|
27
54
|
export const Config = z.object({
|
|
28
55
|
serverName: z.string().default('lingshu'),
|
|
29
56
|
python: z.string().default('python'),
|
|
@@ -40,8 +67,11 @@ export const Config = z.object({
|
|
|
40
67
|
assistantMessage: z.boolean().default(false),
|
|
41
68
|
toolResult: z.boolean().default(false),
|
|
42
69
|
importance: z.number().default(0.6),
|
|
70
|
+
autoRecall: z.boolean().default(true),
|
|
71
|
+
autoRecallLimit: z.number().default(4),
|
|
72
|
+
desensitize: z.boolean().default(true),
|
|
43
73
|
})
|
|
44
|
-
.default({ userMessage: true, assistantMessage: false, toolResult: false, importance: 0.6 }),
|
|
74
|
+
.default({ userMessage: true, assistantMessage: false, toolResult: false, importance: 0.6, autoRecall: true, autoRecallLimit: 4, desensitize: true }),
|
|
45
75
|
toolCallTimeoutMs: z.number().default(60_000),
|
|
46
76
|
maxRetryDelayMs: z.number().default(30_000),
|
|
47
77
|
failOnStartupError: z.boolean().default(false),
|
|
@@ -76,11 +106,16 @@ export async function apply(ctx, config) {
|
|
|
76
106
|
const ready = await bridge.waitReady();
|
|
77
107
|
if (!ready) {
|
|
78
108
|
const message = '灵枢进程无法启动(检查 python 是否可用、aeis 是否安装:pip install aeis)';
|
|
79
|
-
if (config.failOnStartupError)
|
|
109
|
+
if (config.failOnStartupError) {
|
|
110
|
+
// P1 修复(GPT 审查):启动失败抛错前必须 dispose——此前 throw 在 try 之前,
|
|
111
|
+
// 桥接对象泄漏 + 后台重试计时器继续跑
|
|
112
|
+
bridge.dispose();
|
|
80
113
|
throw new Error(message);
|
|
114
|
+
}
|
|
81
115
|
ctx.logger.warn(`dsh-memory: ${message},继续后台重试`);
|
|
82
116
|
}
|
|
83
117
|
const disposers = [];
|
|
118
|
+
let toolsPoll = null;
|
|
84
119
|
try {
|
|
85
120
|
// 工具注册:初始就绪立即注册;若启动时未就绪(python 暂不可用/aeis 未装等
|
|
86
121
|
// 竞态),桥重连成功后自动补注册——修复"工具永久缺失"问题。
|
|
@@ -95,6 +130,11 @@ export async function apply(ctx, config) {
|
|
|
95
130
|
});
|
|
96
131
|
disposers.push(dispose);
|
|
97
132
|
toolsRegistered = true;
|
|
133
|
+
// P1 修复(GPT 审查):注册成功后清除轮询——此前 setInterval 永久保留
|
|
134
|
+
if (toolsPoll) {
|
|
135
|
+
clearInterval(toolsPoll);
|
|
136
|
+
toolsPoll = null;
|
|
137
|
+
}
|
|
98
138
|
ctx.logger.info('dsh-memory: 灵枢工具已注册(就绪后补注册)');
|
|
99
139
|
}
|
|
100
140
|
catch (err) {
|
|
@@ -104,42 +144,73 @@ export async function apply(ctx, config) {
|
|
|
104
144
|
if (ready)
|
|
105
145
|
await tryRegister();
|
|
106
146
|
if (!toolsRegistered) {
|
|
107
|
-
|
|
108
|
-
disposers.push(() =>
|
|
147
|
+
toolsPoll = setInterval(() => { void tryRegister(); }, 2000);
|
|
148
|
+
disposers.push(() => { if (toolsPoll)
|
|
149
|
+
clearInterval(toolsPoll); });
|
|
109
150
|
}
|
|
110
151
|
installMemoryHooks(ctx, bridge, config.memory);
|
|
111
152
|
// 角色扮演网页(同源挂载 /roleplay,复用本插件 bridge)
|
|
112
153
|
await installRoleplayWeb(ctx, bridge, config, disposers);
|
|
154
|
+
// 白箱 LLM 服务商(v0.4 新能力):把灵枢注册为 DSH 的 provider,
|
|
155
|
+
// Web/QQ/飞书等所有会话可选「白箱灵枢」模型——白箱直答(零 LLM),
|
|
156
|
+
// 输入/输出/缓存命中 token 计数对齐 dsh-llm 协议。动态检测 llm 服务。
|
|
157
|
+
{
|
|
158
|
+
const { installWhiteboxLlm } = await import('./llm_adapter.js');
|
|
159
|
+
disposers.push(installWhiteboxLlm(ctx, bridge));
|
|
160
|
+
}
|
|
113
161
|
// 互维维护(v1.1):心跳写戳 + 守护 A + 任务验证双通道
|
|
114
162
|
if (config.mutual.enabled) {
|
|
115
|
-
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
163
|
+
// P1 修复(GPT 审查):timer 是可选服务——缺失时告警跳过互维,
|
|
164
|
+
// 不让插件因互维而阻塞(inject 已不再强声明 timer)。
|
|
165
|
+
// 注意:不能直接读 ctx.timer——Cordis 未声明 inject 的属性访问会抛
|
|
166
|
+
// "cannot get property without inject"(getter 严格);ctx.get() 安全。
|
|
167
|
+
const hasTimer = ctx.get('timer') !== undefined;
|
|
168
|
+
if (!hasTimer) {
|
|
169
|
+
ctx.logger.warn('dsh-memory: timer 服务不可用,跳过互维维护(mutual.enabled=true 但无 timer)');
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
const { installMutualMaintenance } = await import('./mutual.js');
|
|
173
|
+
// 双通道 hooks:白箱 base_verify(走 bridge 调灵枢)+ DeepSeek 复核
|
|
174
|
+
installMutualMaintenance(ctx, { heartbeatMs: config.mutual.heartbeatMs }, {
|
|
175
|
+
verify: async (claim) => {
|
|
176
|
+
const r = await bridge.callTool('wisdom_verify', { knowledge: claim, limit: 4 });
|
|
177
|
+
// P1 修复(GPT 审查):结果在 content[].text(JSON),非 .result
|
|
178
|
+
const text = mcpText(r);
|
|
179
|
+
let data = {};
|
|
180
|
+
try {
|
|
181
|
+
data = JSON.parse(text);
|
|
182
|
+
}
|
|
183
|
+
catch { /* 非 JSON 时用默认 */ }
|
|
184
|
+
return {
|
|
185
|
+
judgment: data.judgment ?? '分析中',
|
|
186
|
+
best: data.best?.name ?? '',
|
|
187
|
+
d_norm: typeof data.D_norm === 'number' ? data.D_norm : -1,
|
|
188
|
+
record_id: data.record_id ?? '',
|
|
189
|
+
};
|
|
190
|
+
},
|
|
191
|
+
review: async (claim, w) => {
|
|
192
|
+
// DeepSeek 复核(在白箱判定之上,不重复白箱工作)
|
|
193
|
+
const reviewResult = await bridge.callTool('think', {
|
|
194
|
+
query: `复核以下主张(白箱判定已给出,请独立评估是否同意):${claim.slice(0, 200)}。白箱判定:${w.judgment},best=${w.best}。只输出 同意/质疑/不同意 + 一句话理由`,
|
|
195
|
+
});
|
|
196
|
+
// P1 修复(GPT 审查):文本在 content[].text;结论解析必须先查
|
|
197
|
+
// 「不同意/不通过」再「质疑」再「同意」——「不同意」含子串「同意」,
|
|
198
|
+
// 此前先匹配「同意」→ 不同意被误判为同意
|
|
199
|
+
const text = mcpText(reviewResult);
|
|
200
|
+
const conclusion = text.includes('不同意') || text.includes('不通过')
|
|
201
|
+
? '不同意'
|
|
202
|
+
: text.includes('质疑') ? '质疑'
|
|
203
|
+
: text.includes('同意') ? '同意' : '不同意';
|
|
204
|
+
return { conclusion, reason: text.slice(0, 120) };
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
ctx.logger.info('dsh-memory: 互维维护已启用(心跳 10min + 守护 A + 任务验证双通道)');
|
|
208
|
+
}
|
|
140
209
|
}
|
|
141
210
|
}
|
|
142
211
|
catch (err) {
|
|
212
|
+
// 探针:记录 apply 失败的具体错误(定位插件加载失败根因)
|
|
213
|
+
probeApplyError(err);
|
|
143
214
|
bridge.dispose();
|
|
144
215
|
throw err;
|
|
145
216
|
}
|