@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.
- package/README.md +139 -24
- package/lib/bridge.d.ts +3 -0
- package/lib/bridge.js +57 -3
- package/lib/index.js +32 -2
- package/lib/llm_adapter.d.ts +123 -0
- package/lib/llm_adapter.js +243 -0
- package/lib/mutual.d.ts +0 -1
- package/lib/mutual.js +9 -4
- package/lib/roleplay_web.js +3 -1
- package/lib/tools.d.ts +1 -1
- package/lib/tools.js +3 -3
- package/package.json +9 -2
- package/src/bridge.ts +61 -3
- package/src/index.ts +33 -2
- package/src/llm_adapter.ts +305 -0
- package/src/mutual.ts +10 -5
- package/src/roleplay_web.ts +3 -1
- package/src/tools.ts +3 -3
|
@@ -0,0 +1,243 @@
|
|
|
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
|
+
// ---- token 估算(确定性启发式:CJK 0.6/字符,其余 /4)----
|
|
13
|
+
export function estimateTokens(text) {
|
|
14
|
+
if (!text)
|
|
15
|
+
return 0;
|
|
16
|
+
let cjk = 0;
|
|
17
|
+
let other = 0;
|
|
18
|
+
for (const ch of text) {
|
|
19
|
+
const code = ch.codePointAt(0);
|
|
20
|
+
if ((code >= 0x4e00 && code <= 0x9fff) || (code >= 0x3040 && code <= 0x30ff))
|
|
21
|
+
cjk++;
|
|
22
|
+
else
|
|
23
|
+
other++;
|
|
24
|
+
}
|
|
25
|
+
return Math.ceil(cjk * 0.6 + other / 4);
|
|
26
|
+
}
|
|
27
|
+
/** 从 GenerateOptions 汇总输入 token(system + messages 全部文本)。 */
|
|
28
|
+
export function countInputTokens(options) {
|
|
29
|
+
let total = 0;
|
|
30
|
+
if (options.system)
|
|
31
|
+
total += estimateTokens(options.system);
|
|
32
|
+
for (const msg of options.messages ?? []) {
|
|
33
|
+
for (const block of msg.content ?? []) {
|
|
34
|
+
if (typeof block?.text === 'string')
|
|
35
|
+
total += estimateTokens(block.text);
|
|
36
|
+
if (typeof block?.arguments === 'string')
|
|
37
|
+
total += estimateTokens(block.arguments);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return total;
|
|
41
|
+
}
|
|
42
|
+
/** 把 DSH 会话消息折叠成白箱可读的对话文本。 */
|
|
43
|
+
export function serializeMessages(options) {
|
|
44
|
+
const parts = [];
|
|
45
|
+
if (options.system)
|
|
46
|
+
parts.push(`[系统] ${options.system}`);
|
|
47
|
+
for (const msg of options.messages ?? []) {
|
|
48
|
+
const text = (msg.content ?? [])
|
|
49
|
+
.filter((b) => b?.type === 'text' && typeof b.text === 'string')
|
|
50
|
+
.map((b) => b.text)
|
|
51
|
+
.join('');
|
|
52
|
+
if (!text)
|
|
53
|
+
continue;
|
|
54
|
+
const role = msg.role === 'assistant' ? '灵枢' : msg.role === 'user' ? '用户' : msg.role;
|
|
55
|
+
parts.push(`[${role}] ${text}`);
|
|
56
|
+
}
|
|
57
|
+
return parts.join('\n');
|
|
58
|
+
}
|
|
59
|
+
/** 把一段完整文本包装成协议流(确定性整段生成 → 单次流)。 */
|
|
60
|
+
export async function* wrapTextStream(text, inputTokens, outputTokens, cacheReadTokens) {
|
|
61
|
+
yield { type: 'block-start', index: 0, blockType: 'text' };
|
|
62
|
+
yield { type: 'text-delta', index: 0, text };
|
|
63
|
+
yield { type: 'block-end', index: 0, block: { type: 'text', text } };
|
|
64
|
+
yield {
|
|
65
|
+
type: 'usage',
|
|
66
|
+
usage: {
|
|
67
|
+
inputTokens,
|
|
68
|
+
outputTokens,
|
|
69
|
+
...(cacheReadTokens > 0 ? { cacheReadTokens } : {}),
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
yield { type: 'finish', reason: 'stop' };
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 白箱知识卡片格式检测:REVERSE_DAILY 直答是知识库内部格式——
|
|
76
|
+
* 「X是什么…,是『A』vs『B』的矛盾——X(是…(…(…)…)…真相:①…」
|
|
77
|
+
* 特征(任一强信号命中即判定):
|
|
78
|
+
* ① 卡片开头句式:内容含「,是『X』vs『Y』的矛盾——」(矛盾标记+破折号)
|
|
79
|
+
* ② 「真相:」结构化标记(卡片分段标题)+ 括号密集(>8)
|
|
80
|
+
* ③ 长文本(>300)+ 高括号密度(>20)+ 无自然语言标点分隔(顿号密集)
|
|
81
|
+
*/
|
|
82
|
+
export function isCardFormat(text) {
|
|
83
|
+
if (!text)
|
|
84
|
+
return false;
|
|
85
|
+
// ① 强信号:矛盾标记句式(「,是『A』vs『B』的矛盾——」)——卡片开头的
|
|
86
|
+
// 标志性结构,不受长度门槛限制(短卡片也判定)
|
|
87
|
+
if (/,是『[^』]{1,12}』vs『[^』]{1,12}』的矛盾[——\-]/.test(text))
|
|
88
|
+
return true;
|
|
89
|
+
// 短文本(<150)无矛盾标记 → 正常文本
|
|
90
|
+
if (text.length < 150)
|
|
91
|
+
return false;
|
|
92
|
+
// ② 「真相:」+ 括号密集(卡片的分段结构)
|
|
93
|
+
if (text.includes('真相:')) {
|
|
94
|
+
const parens = (text.match(/(/g) ?? []).length;
|
|
95
|
+
if (parens >= 8)
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
// ③ 超长 + 极高括号密度(卡片展开体)
|
|
99
|
+
const parens2 = (text.match(/(/g) ?? []).length;
|
|
100
|
+
if (text.length > 300 && parens2 > 20)
|
|
101
|
+
return true;
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
/** 白箱 LLM adapter —— 注册为 DSH 的 provider。 */
|
|
105
|
+
export class WhiteboxLlmAdapter {
|
|
106
|
+
bridge;
|
|
107
|
+
fallback;
|
|
108
|
+
constructor(deps = {}) {
|
|
109
|
+
this.bridge = deps.bridge;
|
|
110
|
+
this.fallback = deps.fallback;
|
|
111
|
+
}
|
|
112
|
+
providerInfo(provider) {
|
|
113
|
+
return { id: provider, name: '白箱灵枢 (lingshu-whitebox)' };
|
|
114
|
+
}
|
|
115
|
+
providerRetryPolicy() {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
async listModels() {
|
|
119
|
+
return [
|
|
120
|
+
{ provider: 'lingshu-whitebox', id: 'whitebox-v1', name: '白箱引擎 v1 (CCG 条件路由)' },
|
|
121
|
+
{ provider: 'lingshu-whitebox', id: 'whitebox-wisdom', name: '白箱智慧之书' },
|
|
122
|
+
];
|
|
123
|
+
}
|
|
124
|
+
async resolveModel(provider, model) {
|
|
125
|
+
return { provider, id: model, name: model };
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* 核心:一次模型调用。白箱优先,无把握降级。
|
|
129
|
+
* @param options 完全装配的请求(必须 honor options.signal)
|
|
130
|
+
*/
|
|
131
|
+
async *stream(options) {
|
|
132
|
+
const inputTokens = countInputTokens(options);
|
|
133
|
+
const conversation = serializeMessages(options);
|
|
134
|
+
// 取最后一条用户文本作为白箱输入(完整对话太长时截尾)
|
|
135
|
+
const lastUserText = [...(options.messages ?? [])]
|
|
136
|
+
.reverse()
|
|
137
|
+
.flatMap((m) => m.content ?? [])
|
|
138
|
+
.filter((b) => b?.type === 'text')
|
|
139
|
+
.map((b) => b.text)
|
|
140
|
+
.join('')
|
|
141
|
+
.slice(-2000);
|
|
142
|
+
// ---- 白箱主路径:调灵枢 wisdom_chat ----
|
|
143
|
+
let whitebox = null;
|
|
144
|
+
if (this.bridge?.isReady?.()) {
|
|
145
|
+
try {
|
|
146
|
+
const r = await this.bridge.callTool('wisdom_chat', {
|
|
147
|
+
message: lastUserText || conversation.slice(-4000),
|
|
148
|
+
session_id: options.sessionId ?? 'default',
|
|
149
|
+
}, options.signal);
|
|
150
|
+
const raw = (r?.content ?? []).map((c) => c.text ?? '').join('\n');
|
|
151
|
+
// 白箱返回结构:{"reply": "...", "route": "self|llm", "hits": [...], "honest": bool}
|
|
152
|
+
let text = raw;
|
|
153
|
+
let route = 'self';
|
|
154
|
+
try {
|
|
155
|
+
const parsed = JSON.parse(raw);
|
|
156
|
+
if (parsed.route)
|
|
157
|
+
route = parsed.route;
|
|
158
|
+
if (typeof parsed.reply === 'string')
|
|
159
|
+
text = parsed.reply;
|
|
160
|
+
else if (parsed.reply !== undefined)
|
|
161
|
+
text = JSON.stringify(parsed.reply);
|
|
162
|
+
}
|
|
163
|
+
catch { /* 非 JSON = 纯文本回复 */ }
|
|
164
|
+
// 质量门槛(v0.4.1):REVERSE_DAILY 卡片直答是知识库内部格式——
|
|
165
|
+
// 「X是什么…是『A』vs『B』的矛盾——X(是…(…)」重复+括号嵌套,
|
|
166
|
+
// 直接当 LLM 输出喂给用户=「重复回答」。检测到卡片特征 → 视为
|
|
167
|
+
// 未命中,走降级 LLM(白箱不输出内部格式垃圾)。
|
|
168
|
+
if (route !== 'llm' && text && text.trim() && !isCardFormat(text)) {
|
|
169
|
+
whitebox = { text: text.trim(), route };
|
|
170
|
+
}
|
|
171
|
+
else if (text && isCardFormat(text)) {
|
|
172
|
+
console.warn('[whitebox-llm] 白箱返回知识卡片格式,降级 LLM(内部格式不直出)');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
// 白箱异常不阻塞——记日志后走降级
|
|
177
|
+
console.warn('[whitebox-llm] 白箱调用失败,降级:', String(err));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (whitebox) {
|
|
181
|
+
const outputTokens = estimateTokens(whitebox.text);
|
|
182
|
+
// 白箱直答 = 全缓存命中(条件路由固化命中,零推理成本)
|
|
183
|
+
yield* wrapTextStream(whitebox.text, inputTokens, outputTokens, inputTokens);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
// ---- 降级路径:白箱无把握 / 桥未就绪 ----
|
|
187
|
+
if (this.fallback) {
|
|
188
|
+
console.warn('[whitebox-llm] 白箱未命中,转降级 LLM');
|
|
189
|
+
yield* this.fallback.stream(options);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
// 无降级可用:诚实声明(盲区 28:未来承诺不可检验——不假装回答)
|
|
193
|
+
const honest = '(白箱引擎未命中且降级 LLM 未配置——按白箱纪律,我诚实声明不知道,不编造答案。)';
|
|
194
|
+
yield* wrapTextStream(honest, inputTokens, estimateTokens(honest), 0);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/** provider 路由名(设置页「模型」下拉可见)。 */
|
|
198
|
+
export const WHITEBOX_PROVIDER = 'lingshu-whitebox';
|
|
199
|
+
/**
|
|
200
|
+
* 注册白箱 provider 到 DSH llm 服务(动态检测,不强依赖 llm 服务——
|
|
201
|
+
* 最小 host 无 llm 时跳过,插件其余功能不受影响)。
|
|
202
|
+
* @returns 注销函数;未注册返回 noop。
|
|
203
|
+
*/
|
|
204
|
+
export function installWhiteboxLlm(ctx, bridge) {
|
|
205
|
+
// 动态检测 llm 服务(P1 模式:timer/webServer 同理)。
|
|
206
|
+
// 注意:不能直接读 ctx.llm——Cordis 未声明 inject 的属性访问会抛
|
|
207
|
+
// "cannot get property without inject";ctx.get('llm') 安全返回 undefined。
|
|
208
|
+
const llm = ctx.get('llm');
|
|
209
|
+
if (!llm?.registerAdapter) {
|
|
210
|
+
ctx.logger?.info?.('dsh-memory: llm 服务不可用,跳过白箱 provider 注册(最小 host)');
|
|
211
|
+
return () => { };
|
|
212
|
+
}
|
|
213
|
+
// 降级 LLM:白箱未命中时转发给 fallbackProvider 路由(默认 deepseek-official,
|
|
214
|
+
// 可在 settings.yaml 的 llm-whitebox.fallbackProvider 覆盖)。通过 llm.stream
|
|
215
|
+
// 重路由——保持 DSH 的全链路(重试/瀑布流/用量计量)。
|
|
216
|
+
const fallbackProvider = 'deepseek-official';
|
|
217
|
+
const fallback = llm.stream
|
|
218
|
+
? {
|
|
219
|
+
stream(options) {
|
|
220
|
+
return llm.stream({ ...options, provider: fallbackProvider });
|
|
221
|
+
},
|
|
222
|
+
}
|
|
223
|
+
: undefined;
|
|
224
|
+
const adapter = new WhiteboxLlmAdapter({ bridge, fallback });
|
|
225
|
+
try {
|
|
226
|
+
llm.registerConfigurableProviders?.([{
|
|
227
|
+
provider: WHITEBOX_PROVIDER,
|
|
228
|
+
displayName: '白箱灵枢 (Whitebox)',
|
|
229
|
+
settingsNs: 'llm-whitebox',
|
|
230
|
+
settingsPath: [],
|
|
231
|
+
}]);
|
|
232
|
+
const registration = llm.registerAdapter([WHITEBOX_PROVIDER], adapter);
|
|
233
|
+
ctx.logger?.info?.('dsh-memory: 白箱 LLM provider 已注册: %s(降级→%s)', WHITEBOX_PROVIDER, fallbackProvider);
|
|
234
|
+
return () => { try {
|
|
235
|
+
registration?.replace?.([]);
|
|
236
|
+
}
|
|
237
|
+
catch { /* 忽略注销异常 */ } };
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
ctx.logger?.warn?.('dsh-memory: 白箱 provider 注册失败: %s', String(err));
|
|
241
|
+
return () => { };
|
|
242
|
+
}
|
|
243
|
+
}
|
package/lib/mutual.d.ts
CHANGED
|
@@ -97,7 +97,6 @@ export declare function installMutualMaintenance(ctx: {
|
|
|
97
97
|
logger: {
|
|
98
98
|
info(m: string): void;
|
|
99
99
|
};
|
|
100
|
-
interval(fn: () => void, ms: number): () => void;
|
|
101
100
|
effect(fn: () => () => void, name?: string): void;
|
|
102
101
|
}, config?: Partial<MutualOptions>, hooks?: {
|
|
103
102
|
verify?: (c: string) => Promise<{
|
package/lib/mutual.js
CHANGED
|
@@ -296,7 +296,10 @@ export function installMutualMaintenance(ctx, config = {}, hooks = {}) {
|
|
|
296
296
|
// P1 修复(GPT 审查):cordis-plugin-timer 签名是 interval(callback, delay)
|
|
297
297
|
// ——此前 (ms, fn) 写反:数字被当 callback、函数被当 delay,回调永不执行,
|
|
298
298
|
// 互维心跳/任务扫描实际不工作。
|
|
299
|
-
|
|
299
|
+
// P2 修复:不能用 ctx.interval——那是 cordis-plugin-timer 注入的快捷属性,
|
|
300
|
+
// 未声明 `timer` inject 时访问抛 "cannot get property without inject"。
|
|
301
|
+
// 改用 Node 原生 setInterval(互维不需要 Cordis effect 生命周期管理)。
|
|
302
|
+
const heartbeatTimer = setInterval(() => {
|
|
300
303
|
try {
|
|
301
304
|
writeHeartbeat(opts);
|
|
302
305
|
// 守护 A:harness 不在 → 拉起
|
|
@@ -315,8 +318,9 @@ export function installMutualMaintenance(ctx, config = {}, hooks = {}) {
|
|
|
315
318
|
}
|
|
316
319
|
catch (e) { /* 单次失败不中断 */ }
|
|
317
320
|
}, opts.heartbeatMs);
|
|
321
|
+
heartbeatTimer.unref?.();
|
|
318
322
|
// 2. 任务轮询(30s 周期,检查 A→B 任务)
|
|
319
|
-
const
|
|
323
|
+
const taskTimer = setInterval(() => {
|
|
320
324
|
try {
|
|
321
325
|
const tasks = scanTasks(opts);
|
|
322
326
|
for (const t of tasks) {
|
|
@@ -329,12 +333,13 @@ export function installMutualMaintenance(ctx, config = {}, hooks = {}) {
|
|
|
329
333
|
}
|
|
330
334
|
catch (e) { /* 单次失败不中断 */ }
|
|
331
335
|
}, 30_000);
|
|
336
|
+
taskTimer.unref?.();
|
|
332
337
|
// 3. effect 作用域清理
|
|
333
338
|
ctx.effect(() => {
|
|
334
339
|
return () => {
|
|
335
340
|
try {
|
|
336
|
-
|
|
337
|
-
|
|
341
|
+
clearInterval(heartbeatTimer);
|
|
342
|
+
clearInterval(taskTimer);
|
|
338
343
|
}
|
|
339
344
|
catch { /* 清理失败不阻塞 */ }
|
|
340
345
|
};
|
package/lib/roleplay_web.js
CHANGED
|
@@ -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
|
-
|
|
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/lib/tools.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export declare const CORE_TOOLS: readonly ["remember", "recall", "search", "time
|
|
|
14
14
|
* 保留灵枢全部心智能力(记忆/认知/推理/学习/飞轮/反思/长期记忆门),
|
|
15
15
|
* 排除身体/视觉类工具(body/device_call/see/world3d 等)。
|
|
16
16
|
*/
|
|
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", "
|
|
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_metrics", "transfer_test", "calibrate", "ingest_text", "ingest_file", "ingest_url", "web_search", "lifecycle_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) */
|
package/lib/tools.js
CHANGED
|
@@ -39,11 +39,11 @@ export const BRAIN_TOOLS = [
|
|
|
39
39
|
// 学习与盲区
|
|
40
40
|
'blindspots', 'learn', 'induce',
|
|
41
41
|
// 知识飞轮
|
|
42
|
-
'distill', '
|
|
42
|
+
'distill', 'flywheel_metrics', 'transfer_test', 'calibrate',
|
|
43
43
|
// 外部知识摄取
|
|
44
44
|
'ingest_text', 'ingest_file', 'ingest_url', 'web_search',
|
|
45
45
|
// 生命周期
|
|
46
|
-
'
|
|
46
|
+
'lifecycle_step', 'lifecycle_state',
|
|
47
47
|
// 长期记忆门(v1.15:重要性评估主动沉淀)
|
|
48
48
|
'longterm_snapshot', 'promote_memories',
|
|
49
49
|
// 服务
|
|
@@ -55,7 +55,7 @@ export const BRAIN_TOOLS = [
|
|
|
55
55
|
const READ_TOOLS = new Set([
|
|
56
56
|
'recall', 'search', 'timeline', 'think', 'reason', 'predict_routes',
|
|
57
57
|
'self_check', 'service_info', 'session_recall', 'gap_trend', 'transfer_test',
|
|
58
|
-
'cognition_report', 'self_reliability', 'emotional_bias', '
|
|
58
|
+
'cognition_report', 'self_reliability', 'emotional_bias', 'flywheel_metrics',
|
|
59
59
|
'distill', 'insight_report', 'prediction_stats', 'blindspots', 'pattern_separation',
|
|
60
60
|
]);
|
|
61
61
|
/** 按工具名判定并发安全(只读查询 true;写操作 false) */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
{
|
|
2
2
|
"name": "@furongjun1999/dsh-memory",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "灵枢(Lingshu·líng shū)DeepSeek Harness 插件:完整大脑——长期记忆/知识飞轮/自我认知/递归反思接入 DSH,对话自动沉淀进记忆库",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -35,12 +35,19 @@
|
|
|
35
35
|
"license": "MIT",
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
38
|
+
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
|
|
38
39
|
"@deepseek-ai/dsh-session": "^0.1.0-rc.6",
|
|
39
40
|
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
|
|
40
41
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
41
42
|
},
|
|
43
|
+
"peerDependenciesMeta": {
|
|
44
|
+
"@deepseek-ai/dsh-llm": {
|
|
45
|
+
"optional": true
|
|
46
|
+
}
|
|
47
|
+
},
|
|
42
48
|
"devDependencies": {
|
|
43
49
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
50
|
+
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
|
|
44
51
|
"@deepseek-ai/dsh-session": "^0.1.0-rc.6",
|
|
45
52
|
"@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
|
|
46
53
|
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
|
package/src/bridge.ts
CHANGED
|
@@ -9,8 +9,26 @@
|
|
|
9
9
|
|
|
10
10
|
import { spawn, type ChildProcess } from 'node:child_process'
|
|
11
11
|
import { createInterface } from 'node:readline'
|
|
12
|
-
import { mkdirSync } from 'node:fs'
|
|
13
|
-
import { dirname } from 'node:path'
|
|
12
|
+
import { mkdirSync, appendFileSync } from 'node:fs'
|
|
13
|
+
import { dirname, join } from 'node:path'
|
|
14
|
+
import { homedir } from 'node:os'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 调试探针:记录桥生命周期到独立文件(绕过 DSH 日志系统,便于定位启动问题)。
|
|
18
|
+
* 路径从用户家目录动态解析(issue #5)——此前硬编码作者本机绝对路径,
|
|
19
|
+
* 其他用户机器上目录不存在且无权限,探针静默失效,恰在排查启动问题时失明。
|
|
20
|
+
*/
|
|
21
|
+
const DEBUG_LOG = join(homedir(), '.dsh', 'logs', 'lingshu-bridge-debug.log')
|
|
22
|
+
let debugLogDirReady = false
|
|
23
|
+
function probe(msg: string): void {
|
|
24
|
+
try {
|
|
25
|
+
if (!debugLogDirReady) {
|
|
26
|
+
mkdirSync(dirname(DEBUG_LOG), { recursive: true })
|
|
27
|
+
debugLogDirReady = true
|
|
28
|
+
}
|
|
29
|
+
appendFileSync(DEBUG_LOG, `[${new Date().toISOString()}] ${msg}\n`)
|
|
30
|
+
} catch { /* 探针失败忽略 */ }
|
|
31
|
+
}
|
|
14
32
|
|
|
15
33
|
/** 灵枢 MCP server 暴露的原始工具(tools/list 结果项)。 */
|
|
16
34
|
export interface McpTool {
|
|
@@ -47,6 +65,14 @@ interface PendingCall {
|
|
|
47
65
|
timer: NodeJS.Timeout
|
|
48
66
|
}
|
|
49
67
|
|
|
68
|
+
/**
|
|
69
|
+
* 连续启动失败的最大重试次数(issue #6):指数退避到 maxRetryDelayMs 后
|
|
70
|
+
* 若无上限,python 环境损坏时会以 30s 间隔永久空转(假激活 + 资源浪费)。
|
|
71
|
+
* 达到上限即进入 failed 终态并停止调度;后续所有请求快速失败并给出
|
|
72
|
+
* 明确的修复指引。握手成功后计数归零——运行期偶发崩溃重启不受影响。
|
|
73
|
+
*/
|
|
74
|
+
const MAX_RETRIES = 8
|
|
75
|
+
|
|
50
76
|
export class LingshuBridge {
|
|
51
77
|
private readonly options: BridgeOptions
|
|
52
78
|
private proc: ChildProcess | null = null
|
|
@@ -56,6 +82,7 @@ export class LingshuBridge {
|
|
|
56
82
|
private started = false
|
|
57
83
|
private disposed = false
|
|
58
84
|
private retryDelayMs = 1000
|
|
85
|
+
private retries = 0
|
|
59
86
|
private retryTimer: NodeJS.Timeout | null = null
|
|
60
87
|
private bootQueue: Array<(ok: boolean) => void> = []
|
|
61
88
|
private readyState: 'pending' | 'ok' | 'failed' = 'pending'
|
|
@@ -113,6 +140,11 @@ export class LingshuBridge {
|
|
|
113
140
|
return this.readyState === 'ok'
|
|
114
141
|
}
|
|
115
142
|
|
|
143
|
+
/** 是否已达放弃终态(连续启动失败超上限,不再自动重启)。 */
|
|
144
|
+
get gaveUp(): boolean {
|
|
145
|
+
return this.retries >= MAX_RETRIES
|
|
146
|
+
}
|
|
147
|
+
|
|
116
148
|
/** 等待握手完成(用于 apply 阶段同步就绪)。 */
|
|
117
149
|
waitReady(): Promise<boolean> {
|
|
118
150
|
if (this.readyState === 'ok') return Promise.resolve(true)
|
|
@@ -166,6 +198,7 @@ export class LingshuBridge {
|
|
|
166
198
|
// 进程无法启动(python 不存在等)——响亮失败
|
|
167
199
|
if (!this.disposed) {
|
|
168
200
|
console.error(`[lingshu-bridge] 灵枢进程启动失败: ${err.message}`)
|
|
201
|
+
probe(`spawn error: ${String(err)}`)
|
|
169
202
|
this.readyState = 'failed'
|
|
170
203
|
this.flushBootQueue(false)
|
|
171
204
|
this.scheduleRetry()
|
|
@@ -173,6 +206,8 @@ export class LingshuBridge {
|
|
|
173
206
|
})
|
|
174
207
|
|
|
175
208
|
proc.on('exit', (code, signal) => {
|
|
209
|
+
// 探针:写独立文件记录退出(绕过 DSH 日志系统,便于定位)
|
|
210
|
+
probe(`exit code=${code} signal=${signal} disposed=${this.disposed} ready=${this.readyState} started=${this.started}`)
|
|
176
211
|
console.error(`[lingshu-bridge] 灵枢进程退出 code=${code} signal=${signal}`)
|
|
177
212
|
this.rl?.close()
|
|
178
213
|
this.rl = null
|
|
@@ -197,12 +232,16 @@ export class LingshuBridge {
|
|
|
197
232
|
})
|
|
198
233
|
// 初始化通知:无 id、无响应
|
|
199
234
|
this.writeRaw({ jsonrpc: '2.0', method: 'notifications/initialized' })
|
|
235
|
+
// 握手成功:重置退避与失败计数(issue #6)——运行期偶发崩溃重启
|
|
236
|
+
// 不累积启动失败;只有「从未握手成功的连续失败」才走向放弃终态
|
|
200
237
|
this.retryDelayMs = 1000
|
|
238
|
+
this.retries = 0
|
|
201
239
|
this.readyState = 'ok'
|
|
202
240
|
this.flushBootQueue(true)
|
|
203
241
|
} catch (err) {
|
|
204
242
|
if (!this.disposed) {
|
|
205
243
|
console.error(`[lingshu-bridge] 握手失败: ${(err as Error).message}`)
|
|
244
|
+
probe(`handshake failed: ${String(err)}`)
|
|
206
245
|
this.readyState = 'failed'
|
|
207
246
|
this.flushBootQueue(false)
|
|
208
247
|
this.scheduleRetry()
|
|
@@ -211,9 +250,22 @@ export class LingshuBridge {
|
|
|
211
250
|
}
|
|
212
251
|
|
|
213
252
|
private scheduleRetry(): void {
|
|
214
|
-
if (this.disposed || this.retryTimer) return
|
|
253
|
+
if (this.disposed || this.retryTimer || this.gaveUp) return
|
|
254
|
+
this.retries += 1
|
|
255
|
+
if (this.gaveUp) {
|
|
256
|
+
// issue #6:放弃机制——明确终态,停止后台空转;请求方收到清晰错误而非超时
|
|
257
|
+
this.readyState = 'failed'
|
|
258
|
+
this.flushBootQueue(false)
|
|
259
|
+
console.error(
|
|
260
|
+
`[lingshu-bridge] 连续启动失败 ${this.retries} 次,已停止自动重启(不再后台空转)。` +
|
|
261
|
+
'请检查 python 可执行文件与 aeis 安装(pip install aeis),修复后在 DSH 中重新启用 dsh-memory 插件。')
|
|
262
|
+
probe(`give up: ${this.retries} consecutive failures, entering failed terminal state`)
|
|
263
|
+
return
|
|
264
|
+
}
|
|
215
265
|
const delay = this.retryDelayMs
|
|
216
266
|
this.retryDelayMs = Math.min(this.retryDelayMs * 2, this.options.maxRetryDelayMs)
|
|
267
|
+
console.error(`[lingshu-bridge] 将进行第 ${this.retries}/${MAX_RETRIES} 次重试(${delay}ms 后)`)
|
|
268
|
+
probe(`schedule retry ${this.retries}/${MAX_RETRIES} in ${delay}ms`)
|
|
217
269
|
this.retryTimer = setTimeout(() => {
|
|
218
270
|
this.retryTimer = null
|
|
219
271
|
if (!this.disposed) this.spawnAndHandshake()
|
|
@@ -231,6 +283,12 @@ export class LingshuBridge {
|
|
|
231
283
|
|
|
232
284
|
private request(method: string, params: Record<string, unknown>,
|
|
233
285
|
signal?: AbortSignal): Promise<unknown> {
|
|
286
|
+
// issue #6:放弃终态下快速失败——之前进程不在且不再重启,请求只能白等超时
|
|
287
|
+
if (this.gaveUp) {
|
|
288
|
+
return Promise.reject(new Error(
|
|
289
|
+
'灵枢进程不可用:连续启动失败已达上限,已停止重试。' +
|
|
290
|
+
'请检查 python 可执行文件与 aeis 安装(pip install aeis),修复后重新启用 dsh-memory 插件。'))
|
|
291
|
+
}
|
|
234
292
|
const id = this.nextId++
|
|
235
293
|
const timeout = this.options.timeoutMs
|
|
236
294
|
// P0 修复(GPT 审查):定时器直接调 settle——settle 内部会 delete + clearTimeout
|
package/src/index.ts
CHANGED
|
@@ -19,11 +19,30 @@
|
|
|
19
19
|
|
|
20
20
|
import type { Context } from '@deepseek-ai/cordis'
|
|
21
21
|
import z from '@deepseek-ai/schemastery'
|
|
22
|
+
import { appendFileSync, mkdirSync } from 'node:fs'
|
|
23
|
+
import { dirname, join } from 'node:path'
|
|
24
|
+
import { homedir } from 'node:os'
|
|
22
25
|
import { LingshuBridge, type McpCallResult } from './bridge.js'
|
|
23
26
|
import { registerLingshuTools, type ToolSelection } from './tools.js'
|
|
24
27
|
import { installMemoryHooks, type MemoryHooksOptions } from './hooks.js'
|
|
25
28
|
import { installRoleplayWeb } from './roleplay_web.js'
|
|
26
29
|
|
|
30
|
+
/**
|
|
31
|
+
* 调试探针:记录 apply 失败到独立文件(绕过 DSH 日志系统)。
|
|
32
|
+
* 路径从用户家目录动态解析(issue #5,与 bridge.ts 同因同修)。
|
|
33
|
+
*/
|
|
34
|
+
const APPLY_ERROR_LOG = join(homedir(), '.dsh', 'logs', 'dsh-memory-apply-error.log')
|
|
35
|
+
let applyLogDirReady = false
|
|
36
|
+
function probeApplyError(err: unknown): void {
|
|
37
|
+
try {
|
|
38
|
+
if (!applyLogDirReady) {
|
|
39
|
+
mkdirSync(dirname(APPLY_ERROR_LOG), { recursive: true })
|
|
40
|
+
applyLogDirReady = true
|
|
41
|
+
}
|
|
42
|
+
appendFileSync(APPLY_ERROR_LOG, `[${new Date().toISOString()}] apply failed: ${String(err)}\n${(err as Error).stack ?? ''}\n`)
|
|
43
|
+
} catch { /* 探针失败忽略 */ }
|
|
44
|
+
}
|
|
45
|
+
|
|
27
46
|
export const name = 'dsh-memory'
|
|
28
47
|
|
|
29
48
|
/** P1 修复(GPT 审查):MCP 调用的标准结果在 content[].text,
|
|
@@ -173,11 +192,21 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
|
|
|
173
192
|
// 角色扮演网页(同源挂载 /roleplay,复用本插件 bridge)
|
|
174
193
|
await installRoleplayWeb(ctx, bridge, config, disposers)
|
|
175
194
|
|
|
195
|
+
// 白箱 LLM 服务商(v0.4 新能力):把灵枢注册为 DSH 的 provider,
|
|
196
|
+
// Web/QQ/飞书等所有会话可选「白箱灵枢」模型——白箱直答(零 LLM),
|
|
197
|
+
// 输入/输出/缓存命中 token 计数对齐 dsh-llm 协议。动态检测 llm 服务。
|
|
198
|
+
{
|
|
199
|
+
const { installWhiteboxLlm } = await import('./llm_adapter.js')
|
|
200
|
+
disposers.push(installWhiteboxLlm(ctx, bridge))
|
|
201
|
+
}
|
|
202
|
+
|
|
176
203
|
// 互维维护(v1.1):心跳写戳 + 守护 A + 任务验证双通道
|
|
177
204
|
if (config.mutual.enabled) {
|
|
178
205
|
// P1 修复(GPT 审查):timer 是可选服务——缺失时告警跳过互维,
|
|
179
|
-
// 不让插件因互维而阻塞(inject 已不再强声明 timer
|
|
180
|
-
|
|
206
|
+
// 不让插件因互维而阻塞(inject 已不再强声明 timer)。
|
|
207
|
+
// 注意:不能直接读 ctx.timer——Cordis 未声明 inject 的属性访问会抛
|
|
208
|
+
// "cannot get property without inject"(getter 严格);ctx.get() 安全。
|
|
209
|
+
const hasTimer = ctx.get('timer') !== undefined
|
|
181
210
|
if (!hasTimer) {
|
|
182
211
|
ctx.logger.warn('dsh-memory: timer 服务不可用,跳过互维维护(mutual.enabled=true 但无 timer)')
|
|
183
212
|
} else {
|
|
@@ -223,6 +252,8 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
|
|
|
223
252
|
}
|
|
224
253
|
}
|
|
225
254
|
} catch (err) {
|
|
255
|
+
// 探针:记录 apply 失败的具体错误(定位插件加载失败根因)
|
|
256
|
+
probeApplyError(err)
|
|
226
257
|
bridge.dispose()
|
|
227
258
|
throw err
|
|
228
259
|
}
|