@furongjun1999/dsh-memory 0.4.0 → 0.4.2

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,163 @@
1
+ # 灵枢 AEIS MCP 工具总表(81 个 · 智能论 v3.4)
2
+
3
+ > 生成:2026-08-29 · 来源:aeis/mcp/server.py · 版本:aeis 0.5.0(智能论 v3.4)
4
+ > 规则:每个工具一条说明,不重复、不遗漏。
5
+
6
+ ---
7
+
8
+ ## 记忆(12)
9
+
10
+ | 工具 | 功能 |
11
+ |---|---|
12
+ | `remember` | 写入一条感知记忆(知识层,自动去重) |
13
+ | `recall` | 组合联想召回(内容0.5+重要性0.3+近因0.2) |
14
+ | `search` | 内容检索(LIKE 预筛 + 中文二元组 Jaccard 排序) |
15
+ | `timeline` | 记忆时间线(按时间倒序) |
16
+ | `longterm_snapshot` | 长期记忆写入(快照→重要性评估→分层写入) |
17
+ | `prefeed` | H1 海马体前馈(新奇检测→当场强化编码) |
18
+ | `promote_memories` | 情境层批量提升扫描(睡眠巩固) |
19
+ | `pattern_separation` | H3 海马体模式分离(相似节点分离边) |
20
+ | `reconstruct_scene` | H4 海马体情景重构(线索→条件空间复原) |
21
+ | `session_note` | 上下文外部化(会话要点写入灵枢) |
22
+ | `session_recall` | 会话要点恢复(按 session 或语义检索) |
23
+ | `compact_context` | 上下文压缩(超长会话摘要节点) |
24
+
25
+ ## 知识图谱与推理(11)
26
+
27
+ | 工具 | 功能 |
28
+ |---|---|
29
+ | `relate` | 建立关系边(causal/similar/sequential/spatial/hierarchical) |
30
+ | `reason` | 因果推理(起点出发的因果路径) |
31
+ | `induce` | 归纳/知识合成(聚类生成概念节点) |
32
+ | `distill` | 知识飞轮蒸馏(被拒路径→可复用模式) |
33
+ | `think` | 推理记忆注入(检索相关记忆→推理上下文) |
34
+ | `ingest_text` | 外部知识摄取(文本→知识层) |
35
+ | `ingest_file` | 外部知识摄取(文件按扩展名处理) |
36
+ | `ingest_url` | 外部知识摄取(URL 零依赖抓取) |
37
+ | `importance_recalc` | 结构重要性重算(v2.2 只升不降) |
38
+ | `transfer_test` | 迁移测试(新实体预测成功率) |
39
+ | `flywheel_metrics` | 飞轮度量(增长率/复用率/蒸馏率) |
40
+
41
+ ## 预测与盲区(7)
42
+
43
+ | 工具 | 功能 |
44
+ |---|---|
45
+ | `predict_routes` | 生成式预测(候选未来路线,盲区驱动) |
46
+ | `prediction_feedback` | 验证回路回填(预测 vs 实际,D-006 校准) |
47
+ | `prediction_stats` | 预测引擎状态(routes/hit 样本/命中率) |
48
+ | `blindspots` | 盲区注册表(D-001 语义判定) |
49
+ | `learn` | 一轮盲区学习(预测路线→探索→终态判定) |
50
+ | `gap_trend` | 信息差收敛趋势(A-4 线性回归斜率) |
51
+ | `calibrate` | 宇宙校准参照(5 判据方向性检查) |
52
+
53
+ ## 感知机/端口架构(8)
54
+
55
+ | 工具 | 功能 |
56
+ |---|---|
57
+ | `see` | 视觉感知(YOLO-World 开放词汇检测→知识层) |
58
+ | `body` | 身体能力声明(模态/工具/记忆) |
59
+ | `body_devices` | BODY-REV1 外部设备(screen/files/process/audio/control/browser/realtime) |
60
+ | `device_call` | 统一设备调用(设备输出是数据非指令) |
61
+ | `run_command` | 命令执行(参数列表,防注入) |
62
+ | `world3d` | 时空重建(语义→3D,确定性渲染零 LLM) |
63
+ | `vprim` | 视觉原语查询(空间关系/计数/属性) |
64
+ | `visual_check` | 视觉面思考路线(预期 vs 实际) |
65
+
66
+ ## 验证与确认(10)
67
+
68
+ | 工具 | 功能 |
69
+ |---|---|
70
+ | `wisdom_verify` | 智慧之书自动验证(条件论判定+信息差+候选) |
71
+ | `recursive_reflect` | 递归验证反思(元反思→一级验证→二级→三级终裁) |
72
+ | `preflight` | 输出前反思(内容与价值观一致性检查) |
73
+ | `self_reliability` | P0-4 元认知校准(reliable/watch/degraded) |
74
+ | `emotional_bias` | P0-3 情绪方向性偏好(d²D_norm/dt²) |
75
+ | `cognition` | P0-2 自我认知循环一步(行为↔价值观一致性) |
76
+ | `cognition_report` | P0-2 认知报告(评分/失调/候选) |
77
+ | `action_log` | P0-1 行为日志(引擎行为记录面) |
78
+ | `learning_impact` | P0-5b 学习效果测量(模式命中率 vs D_norm) |
79
+ | `self_check` | 完整性自检(孤儿边/表统计/integrity_ok) |
80
+
81
+ ## 生命周期(4)
82
+
83
+ | 工具 | 功能 |
84
+ |---|---|
85
+ | `lifecycle_step` | 生命周期一步(感知→好奇→缩小信息差→信任→协作→巩固→standby) |
86
+ | `lifecycle_state` | 生命周期状态(cycle/state) |
87
+ | `start_lifecycle` | 启动生命周期自发循环(后台线程) |
88
+ | `stop_lifecycle` | 中断生命周期自发循环 |
89
+
90
+ ## 智慧之书(6)
91
+
92
+ | 工具 | 功能 |
93
+ |---|---|
94
+ | `wisdom_analyze` | 外来知识分析(条件卡+候选+判定) |
95
+ | `wisdom_predict` | 生成式预测(白箱智能预测生成化) |
96
+ | `wisdom_trust_judge` | 信任上下文判定(内容×信任×关系) |
97
+ | `wisdom_compose` | 跨学科组合分析(Convergence Over Coverage) |
98
+ | `wisdom_respond` | 出招查询(条件→命中学科出招) |
99
+ | `wisdom_chat` | 信息分层对话(语义识别分流→LLM 兜底) |
100
+
101
+ ## 角色扮演(4)
102
+
103
+ | 工具 | 功能 |
104
+ |---|---|
105
+ | `roleplay_chat` | 角色扮演对话(扮演论 v3.3,白箱优先) |
106
+ | `role_create` | 创建角色(角色卡=条件空间声明起点) |
107
+ | `role_import` | 角色导入(memory/anchor/values 三接口) |
108
+ | `role_block` | 角色扮演注入块(锚点/价值观/条件空间) |
109
+
110
+ ## 洞察(4)
111
+
112
+ | 工具 | 功能 |
113
+ |---|---|
114
+ | `insight_record` | 洞察条件层:记录洞见事件(C1-C8 条件快照) |
115
+ | `insight_verify` | 洞察验证(V1/V2/V3 证据提交) |
116
+ | `insight_report` | 洞察 CER 报告(有效洞见率+显著性) |
117
+ | `insight_window` | 洞察窗口检测(C1≥0.6∧跨域∧低压力) |
118
+
119
+ ## 外部(5)
120
+
121
+ | 工具 | 功能 |
122
+ |---|---|
123
+ | `web_search` | 外部网络搜索(博查 API,不写入记忆) |
124
+ | `web_ingest_search` | 外部搜索摄取(博查→知识层) |
125
+ | `export` | 全库导出 JSON(灾备/迁移) |
126
+ | `service_info` | 服务信息(身份/版本/协议/工具数) |
127
+ | `designer_decide` | 设计者裁决(需 AEIS_DESIGNER_KEY) |
128
+
129
+ ## 世界模型 · 游戏服务器(10)
130
+
131
+ | 工具 | 功能 |
132
+ |---|---|
133
+ | `voxel_world` | 小型我的世界(里程碑2.1 · 4D 时空占用沙盒:build/spawn/simulate/trail/state) |
134
+ | `world_server` | AI 游戏世界服务器(里程碑2.2 · tick 多路并行/快照记忆/反馈/同步/错误回滚/预测验证) |
135
+ | `scene_simulator` | 场景级世界模拟器(里程碑2.3 · 场景/实体/自主行为玩家:wander/seek/avoid/flee/follow + 决策循环) |
136
+ | `spacetime_consistency` | 时空一致性验证(里程碑2.4 · 阶段2收官 · 持续运行:预测 vs 实际→滚动命中率/漂移检测/自洽判定) |
137
+ | `world_model` | 统一世界模型(里程碑3.1 · HERMES 式统一架构:理解/生成/验证共享同一骨干,生成先验注入理解,观测-only 模式推断) |
138
+ | `world_learner` | 自监督世界学习(里程碑3.2 · V-JEPA 式:观测序列无标注学转移函数,外部裁判评估认知缺口收紧) |
139
+ | `curiosity_explorer` | 好奇驱动探索(里程碑3.3 · 有限带宽主动观测:信息增益最大化,盯住信息瓶颈,策略对比优于随机/轮询) |
140
+ | `seven_layer_loop` | 七层闭环(里程碑3.4 · 阶段3收官:感知→记忆→理解→预测→验证→物理→决策完整自主循环,七层留痕可审计) |
141
+ | `world_generator` | 文字生图/文字生视频(里程碑4.3 · 世界模型生成器:文字→场景解析→世界实例化→3D 渲染;image 确定性 PNG / video GIF 多帧=世界演化+预测叠加) |
142
+ | `world_semantics` | 图像语义提取(里程碑4.5 · 感知方向:轮廓/形状/颜色/亮度→从外到内→图的信息定义;generate_roundtrip 生成-感知闭环自验证) |
143
+
144
+ ---
145
+
146
+ ## 引擎内部能力(未挂载 MCP · 8)
147
+
148
+ > 存在于引擎中,按安全边界刻意不暴露给外部 Agent 调用:
149
+
150
+ | 工具 | 功能 |
151
+ |---|---|
152
+ | `condition_space_operate` | 条件空间 7 操作(identify/declare/separate/compose/switch/reverse/loop) |
153
+ | `add_context` | 情境层记忆写入(短时会话,FIFO+1h 窗口,可衰减) |
154
+ | `code_test` | 结构化代码测试(隔离环境断言执行) |
155
+ | `compile_exec` | 中文协议编译器(认知图→代码图→编译执行) |
156
+ | `lingshu_sensor_report` | 信息差传感器(五维结构质量信号扫描) |
157
+ | `lingshu_vitality_report` | 维生状态(心跳/影响面/回滚能力/判定) |
158
+ | `lingshu_auto_snapshot` | 前置快照(sha256 指纹+修改意图) |
159
+ | `lingshu_rollback` | 一键回滚(快照指纹校验) |
160
+
161
+ ---
162
+
163
+ **总计:81 个 MCP 工具 + 8 个引擎内部能力**(不重复、不遗漏)
package/lib/bridge.d.ts CHANGED
@@ -49,6 +49,10 @@ export declare class LingshuBridge {
49
49
  private retryTimer;
50
50
  private bootQueue;
51
51
  private readyState;
52
+ /** issue #7:当前子进程启动时间(计算 uptime,区分秒退与长存后外部关闭) */
53
+ private procStartedAt;
54
+ /** issue #7:运行期反复退出的滑动窗口(时间戳列表)——防无限重启刷屏 */
55
+ private unexpectedExits;
52
56
  constructor(options: BridgeOptions);
53
57
  /**
54
58
  * 启动灵枢进程并完成握手(initialize → notifications/initialized)。
package/lib/bridge.js CHANGED
@@ -48,6 +48,10 @@ export class LingshuBridge {
48
48
  retryTimer = null;
49
49
  bootQueue = [];
50
50
  readyState = 'pending';
51
+ /** issue #7:当前子进程启动时间(计算 uptime,区分秒退与长存后外部关闭) */
52
+ procStartedAt = 0;
53
+ /** issue #7:运行期反复退出的滑动窗口(时间戳列表)——防无限重启刷屏 */
54
+ unexpectedExits = [];
51
55
  constructor(options) {
52
56
  this.options = options;
53
57
  }
@@ -112,6 +116,7 @@ export class LingshuBridge {
112
116
  spawnAndHandshake() {
113
117
  if (this.disposed)
114
118
  return;
119
+ this.procStartedAt = Date.now();
115
120
  const { python, args, env, cwd } = this.options;
116
121
  const childEnv = { ...process.env, ...env };
117
122
  // 确保 DB 目录存在(灵枢 server 也会防御性创建,这里提前为可读错误)
@@ -165,12 +170,37 @@ export class LingshuBridge {
165
170
  });
166
171
  proc.on('exit', (code, signal) => {
167
172
  // 探针:写独立文件记录退出(绕过 DSH 日志系统,便于定位)
168
- probe(`exit code=${code} signal=${signal} disposed=${this.disposed} ready=${this.readyState} started=${this.started}`);
169
- console.error(`[lingshu-bridge] 灵枢进程退出 code=${code} signal=${signal}`);
173
+ const uptimeS = this.procStartedAt
174
+ ? Math.round((Date.now() - this.procStartedAt) / 1000)
175
+ : -1;
176
+ probe(`exit code=${code} signal=${signal} disposed=${this.disposed} ready=${this.readyState} started=${this.started} uptime=${uptimeS}s`);
177
+ console.error(`[lingshu-bridge] 灵枢进程退出 code=${code} signal=${signal}(存活 ${uptimeS}s)`);
170
178
  this.rl?.close();
171
179
  this.rl = null;
172
180
  this.rejectAll(new Error(`灵枢进程已退出(code=${code} signal=${signal ?? 'none'})`));
173
181
  if (!this.disposed) {
182
+ // issue #7:长存后 code=0 退出 = stdin 被外部关闭(非崩溃)。
183
+ // 记录滑动窗口(10 分钟内 ≥3 次)→ 进入冷却,防「外部反复关闭 +
184
+ // 无限重启」刷屏;冷却结束自动恢复,不进入放弃终态。
185
+ this.unexpectedExits = this.unexpectedExits.filter((t) => Date.now() - t < 600_000);
186
+ this.unexpectedExits.push(Date.now());
187
+ if (uptimeS >= 0 && uptimeS < 5) {
188
+ console.error('[lingshu-bridge] 进程启动后 5 秒内即退出——请检查 python 可执行文件与 aeis 安装。');
189
+ }
190
+ else if (this.unexpectedExits.length >= 3) {
191
+ console.error(`[lingshu-bridge] 10 分钟内已意外退出 ${this.unexpectedExits.length} 次,` +
192
+ '冷却 5 分钟后自动恢复。若持续出现,请检查是否有多个插件/脚本'
193
+ + '同时管理灵枢进程(重复 spawn 会互相关闭对方子进程的 stdin)。');
194
+ probe(`cooldown: ${this.unexpectedExits.length} unexpected exits in 10min`);
195
+ this.retryTimer = setTimeout(() => {
196
+ this.retryTimer = null;
197
+ this.unexpectedExits = [];
198
+ if (!this.disposed)
199
+ this.spawnAndHandshake();
200
+ }, 300_000);
201
+ this.retryTimer.unref();
202
+ return;
203
+ }
174
204
  // 有挂起请求的失败是异常的;仅启动失败的等待者得到 false
175
205
  this.readyState = 'failed';
176
206
  this.flushBootQueue(false);
package/lib/hooks.js CHANGED
@@ -86,8 +86,12 @@ export function installMemoryHooks(ctx, bridge, opts) {
86
86
  ctx.on('session/event', (_session, event) => {
87
87
  if (event.type === 'user/message' && opts.userMessage) {
88
88
  // 只记真实用户输入(kind='user'),跳过插件注入/系统上下文
89
- if (event.data.source?.kind !== 'user')
89
+ if (event.data.source?.kind !== 'user') {
90
+ // T4 诊断(2026-08-30):dsh 端对话零写入排查——记录被滤事件的实际
91
+ // source.kind(若 dsh 新版改了 kind 值,此处日志可定位)
92
+ ctx.logger.info(`dsh-memory: user/message 事件被滤(source.kind=${event.data.source?.kind ?? 'undefined'})`);
90
93
  return;
94
+ }
91
95
  const text = extractText(event.data.content);
92
96
  if (!text)
93
97
  return;
@@ -95,6 +99,9 @@ export function installMemoryHooks(ctx, bridge, opts) {
95
99
  if (safe === null)
96
100
  return;
97
101
  memorize('remember', { content: safe, importance: opts.importance, tags: ['dsh', 'user'] });
102
+ // T4:用用户消息做一次语义召回——触发灵枢 _note_reuse 落库(复用观测),
103
+ // 召回结果同时预热灵枢检索缓存(timeline 只读不触发复用统计)
104
+ memorize('recall', { query: safe.slice(0, 200), limit: 3 });
98
105
  }
99
106
  else if (event.type === 'assistant/message' && opts.assistantMessage) {
100
107
  const text = extractText(event.data.message.content);
package/lib/index.js CHANGED
@@ -75,6 +75,10 @@ export const Config = z.object({
75
75
  toolCallTimeoutMs: z.number().default(60_000),
76
76
  maxRetryDelayMs: z.number().default(30_000),
77
77
  failOnStartupError: z.boolean().default(false),
78
+ /** 角色扮演入口按钮(issue #8):注入 dsh 首页右上角浮动入口;
79
+ * 关闭后不注入(/roleplay 页面仍可直接访问)。按钮本身支持拖动,
80
+ * 位置记忆在浏览器 localStorage。 */
81
+ roleplayEntryButton: z.boolean().default(true),
78
82
  /** 互维维护(v1.1):心跳 10min / 任务验证双通道。 */
79
83
  mutual: z
80
84
  .object({
@@ -839,13 +839,36 @@ export async function installRoleplayWeb(ctx, bridge, config, disposers) {
839
839
  };
840
840
  disposers.push(webServer.register({ kind: 'prefix', path: '/roleplay', handler }));
841
841
  // 在 GUI 首页注入浮动入口按钮(APP 内置页面内一键进入角色扮演,右上角)
842
- disposers.push(webServer.tapIndex((html) => {
843
- if (html.includes('href="/roleplay"'))
844
- return html;
845
- const btn = '<a href="/roleplay" style="position:fixed;top:12px;right:14px;z-index:999999;background:#7c8cff;color:#0b0d14;border-radius:999px;padding:9px 15px;font:600 13px/1 system-ui,sans-serif;text-decoration:none;box-shadow:0 4px 14px rgba(0,0,0,.45)">🎭 角色扮演</a>';
846
- return html.replace('</body>', `${btn}\n</body>`);
847
- }));
848
- ctx.logger.info(`dsh-memory: 角色扮演网页已挂载 /roleplay(data_dir=${roleDataDir})`);
842
+ // issue #8:按钮可隐藏(roleplayEntryButton=false 不注入)+ 可拖动
843
+ // (pointer 拖拽,位置记忆在 localStorage;移动 >6px 视为拖动,否则放行点击跳转)
844
+ if (config.roleplayEntryButton) {
845
+ disposers.push(webServer.tapIndex((html) => {
846
+ if (html.includes('href="/roleplay"'))
847
+ return html;
848
+ const btn = '<a id="dshm-rp" href="/roleplay" style="position:fixed;top:12px;right:14px;z-index:999999;background:#7c8cff;color:#0b0d14;border-radius:999px;padding:9px 15px;font:600 13px/1 system-ui,sans-serif;text-decoration:none;box-shadow:0 4px 14px rgba(0,0,0,.45);touch-action:none;user-select:none;cursor:grab">🎭 角色扮演</a>' +
849
+ '<script>(function(){var b=document.getElementById("dshm-rp");if(!b)return;' +
850
+ 'try{var p=JSON.parse(localStorage.getItem("dshm_rlp")||"null");' +
851
+ 'if(p&&p.l!=null){b.style.right="auto";b.style.left=p.l+"px";b.style.top=p.t+"px";}}catch(e){}' +
852
+ 'var sx=0,sy=0,sl=0,st=0,drag=false;' +
853
+ 'b.addEventListener("pointerdown",function(e){sx=e.clientX;sy=e.clientY;' +
854
+ 'var r=b.getBoundingClientRect();sl=r.left;st=r.top;drag=false;' +
855
+ 'try{b.setPointerCapture(e.pointerId);}catch(err){}});' +
856
+ 'b.addEventListener("pointermove",function(e){' +
857
+ 'if(e.buttons===0||!drag&&!sx&&!sy)return;' +
858
+ 'var dx=e.clientX-sx,dy=e.clientY-sy;' +
859
+ 'if(!drag&&Math.sqrt(dx*dx+dy*dy)>6){drag=true;b.style.right="auto";b.style.cursor="grabbing";}' +
860
+ 'if(drag){var L=Math.max(4,Math.min(window.innerWidth-b.offsetWidth-4,sl+dx)),' +
861
+ 'T=Math.max(4,Math.min(window.innerHeight-b.offsetHeight-4,st+dy));' +
862
+ 'b.style.left=L+"px";b.style.top=T+"px";e.preventDefault();}});' +
863
+ 'b.addEventListener("pointerup",function(e){' +
864
+ 'if(drag){e.preventDefault();' +
865
+ 'try{var r=b.getBoundingClientRect();' +
866
+ 'localStorage.setItem("dshm_rlp",JSON.stringify({l:r.left,t:r.top}));}catch(err){}}' +
867
+ 'drag=false;sx=0;sy=0;});})();</script>';
868
+ return html.replace('</body>', `${btn}\n</body>`);
869
+ }));
870
+ }
871
+ ctx.logger.info(`dsh-memory: 角色扮演网页已挂载 /roleplay(data_dir=${roleDataDir},入口按钮=${config.roleplayEntryButton ? '显示(可拖动)' : '隐藏'})`);
849
872
  }
850
873
  catch (err) {
851
874
  ctx.logger.warn(`dsh-memory: 角色扮演网页挂载失败(不影响其他功能): ${String(err && err.message || err)}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@furongjun1999/dsh-memory",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "灵枢(Lingshu·líng shū)DeepSeek Harness 插件:完整大脑——长期记忆/知识飞轮/自我认知/递归反思接入 DSH,对话自动沉淀进记忆库",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
package/src/bridge.ts CHANGED
@@ -86,6 +86,10 @@ export class LingshuBridge {
86
86
  private retryTimer: NodeJS.Timeout | null = null
87
87
  private bootQueue: Array<(ok: boolean) => void> = []
88
88
  private readyState: 'pending' | 'ok' | 'failed' = 'pending'
89
+ /** issue #7:当前子进程启动时间(计算 uptime,区分秒退与长存后外部关闭) */
90
+ private procStartedAt = 0
91
+ /** issue #7:运行期反复退出的滑动窗口(时间戳列表)——防无限重启刷屏 */
92
+ private unexpectedExits: number[] = []
89
93
 
90
94
  constructor(options: BridgeOptions) {
91
95
  this.options = options
@@ -154,6 +158,7 @@ export class LingshuBridge {
154
158
 
155
159
  private spawnAndHandshake(): void {
156
160
  if (this.disposed) return
161
+ this.procStartedAt = Date.now()
157
162
  const { python, args, env, cwd } = this.options
158
163
  const childEnv = { ...process.env, ...env }
159
164
  // 确保 DB 目录存在(灵枢 server 也会防御性创建,这里提前为可读错误)
@@ -207,12 +212,39 @@ export class LingshuBridge {
207
212
 
208
213
  proc.on('exit', (code, signal) => {
209
214
  // 探针:写独立文件记录退出(绕过 DSH 日志系统,便于定位)
210
- probe(`exit code=${code} signal=${signal} disposed=${this.disposed} ready=${this.readyState} started=${this.started}`)
211
- console.error(`[lingshu-bridge] 灵枢进程退出 code=${code} signal=${signal}`)
215
+ const uptimeS = this.procStartedAt
216
+ ? Math.round((Date.now() - this.procStartedAt) / 1000)
217
+ : -1
218
+ probe(`exit code=${code} signal=${signal} disposed=${this.disposed} ready=${this.readyState} started=${this.started} uptime=${uptimeS}s`)
219
+ console.error(`[lingshu-bridge] 灵枢进程退出 code=${code} signal=${signal}(存活 ${uptimeS}s)`)
212
220
  this.rl?.close()
213
221
  this.rl = null
214
222
  this.rejectAll(new Error(`灵枢进程已退出(code=${code} signal=${signal ?? 'none'})`))
215
223
  if (!this.disposed) {
224
+ // issue #7:长存后 code=0 退出 = stdin 被外部关闭(非崩溃)。
225
+ // 记录滑动窗口(10 分钟内 ≥3 次)→ 进入冷却,防「外部反复关闭 +
226
+ // 无限重启」刷屏;冷却结束自动恢复,不进入放弃终态。
227
+ this.unexpectedExits = this.unexpectedExits.filter(
228
+ (t) => Date.now() - t < 600_000,
229
+ )
230
+ this.unexpectedExits.push(Date.now())
231
+ if (uptimeS >= 0 && uptimeS < 5) {
232
+ console.error(
233
+ '[lingshu-bridge] 进程启动后 5 秒内即退出——请检查 python 可执行文件与 aeis 安装。')
234
+ } else if (this.unexpectedExits.length >= 3) {
235
+ console.error(
236
+ `[lingshu-bridge] 10 分钟内已意外退出 ${this.unexpectedExits.length} 次,` +
237
+ '冷却 5 分钟后自动恢复。若持续出现,请检查是否有多个插件/脚本'
238
+ + '同时管理灵枢进程(重复 spawn 会互相关闭对方子进程的 stdin)。')
239
+ probe(`cooldown: ${this.unexpectedExits.length} unexpected exits in 10min`)
240
+ this.retryTimer = setTimeout(() => {
241
+ this.retryTimer = null
242
+ this.unexpectedExits = []
243
+ if (!this.disposed) this.spawnAndHandshake()
244
+ }, 300_000)
245
+ this.retryTimer.unref()
246
+ return
247
+ }
216
248
  // 有挂起请求的失败是异常的;仅启动失败的等待者得到 false
217
249
  this.readyState = 'failed'
218
250
  this.flushBootQueue(false)
package/src/hooks.ts CHANGED
@@ -114,12 +114,20 @@ export function installMemoryHooks(ctx: Context, bridge: LingshuBridge, opts: Me
114
114
  ctx.on('session/event', (_session, event: SessionEvent) => {
115
115
  if (event.type === 'user/message' && opts.userMessage) {
116
116
  // 只记真实用户输入(kind='user'),跳过插件注入/系统上下文
117
- if (event.data.source?.kind !== 'user') return
117
+ if (event.data.source?.kind !== 'user') {
118
+ // T4 诊断(2026-08-30):dsh 端对话零写入排查——记录被滤事件的实际
119
+ // source.kind(若 dsh 新版改了 kind 值,此处日志可定位)
120
+ ctx.logger.info(`dsh-memory: user/message 事件被滤(source.kind=${event.data.source?.kind ?? 'undefined'})`)
121
+ return
122
+ }
118
123
  const text = extractText(event.data.content)
119
124
  if (!text) return
120
125
  const safe = sanitize(text) // 脱敏:纯凭据消息 → null → 跳过写入
121
126
  if (safe === null) return
122
127
  memorize('remember', { content: safe, importance: opts.importance, tags: ['dsh', 'user'] })
128
+ // T4:用用户消息做一次语义召回——触发灵枢 _note_reuse 落库(复用观测),
129
+ // 召回结果同时预热灵枢检索缓存(timeline 只读不触发复用统计)
130
+ memorize('recall', { query: safe.slice(0, 200), limit: 3 })
123
131
  } else if (event.type === 'assistant/message' && opts.assistantMessage) {
124
132
  const text = extractText(event.data.message.content)
125
133
  if (!text) return
package/src/index.ts CHANGED
@@ -114,6 +114,10 @@ export const Config: z<Config> = z.object({
114
114
  toolCallTimeoutMs: z.number().default(60_000),
115
115
  maxRetryDelayMs: z.number().default(30_000),
116
116
  failOnStartupError: z.boolean().default(false),
117
+ /** 角色扮演入口按钮(issue #8):注入 dsh 首页右上角浮动入口;
118
+ * 关闭后不注入(/roleplay 页面仍可直接访问)。按钮本身支持拖动,
119
+ * 位置记忆在浏览器 localStorage。 */
120
+ roleplayEntryButton: z.boolean().default(true),
117
121
  /** 互维维护(v1.1):心跳 10min / 任务验证双通道。 */
118
122
  mutual: z
119
123
  .object({
@@ -839,13 +839,36 @@ export async function installRoleplayWeb(ctx, bridge, config, disposers) {
839
839
  };
840
840
  disposers.push(webServer.register({ kind: 'prefix', path: '/roleplay', handler }));
841
841
  // 在 GUI 首页注入浮动入口按钮(APP 内置页面内一键进入角色扮演,右上角)
842
- disposers.push(webServer.tapIndex((html) => {
843
- if (html.includes('href="/roleplay"'))
844
- return html;
845
- const btn = '<a href="/roleplay" style="position:fixed;top:12px;right:14px;z-index:999999;background:#7c8cff;color:#0b0d14;border-radius:999px;padding:9px 15px;font:600 13px/1 system-ui,sans-serif;text-decoration:none;box-shadow:0 4px 14px rgba(0,0,0,.45)">🎭 角色扮演</a>';
846
- return html.replace('</body>', `${btn}\n</body>`);
847
- }));
848
- ctx.logger.info(`dsh-memory: 角色扮演网页已挂载 /roleplay(data_dir=${roleDataDir})`);
842
+ // issue #8:按钮可隐藏(roleplayEntryButton=false 不注入)+ 可拖动
843
+ // (pointer 拖拽,位置记忆在 localStorage;移动 >6px 视为拖动,否则放行点击跳转)
844
+ if (config.roleplayEntryButton) {
845
+ disposers.push(webServer.tapIndex((html) => {
846
+ if (html.includes('href="/roleplay"'))
847
+ return html;
848
+ const btn = '<a id="dshm-rp" href="/roleplay" style="position:fixed;top:12px;right:14px;z-index:999999;background:#7c8cff;color:#0b0d14;border-radius:999px;padding:9px 15px;font:600 13px/1 system-ui,sans-serif;text-decoration:none;box-shadow:0 4px 14px rgba(0,0,0,.45);touch-action:none;user-select:none;cursor:grab">🎭 角色扮演</a>' +
849
+ '<script>(function(){var b=document.getElementById("dshm-rp");if(!b)return;' +
850
+ 'try{var p=JSON.parse(localStorage.getItem("dshm_rlp")||"null");' +
851
+ 'if(p&&p.l!=null){b.style.right="auto";b.style.left=p.l+"px";b.style.top=p.t+"px";}}catch(e){}' +
852
+ 'var sx=0,sy=0,sl=0,st=0,drag=false;' +
853
+ 'b.addEventListener("pointerdown",function(e){sx=e.clientX;sy=e.clientY;' +
854
+ 'var r=b.getBoundingClientRect();sl=r.left;st=r.top;drag=false;' +
855
+ 'try{b.setPointerCapture(e.pointerId);}catch(err){}});' +
856
+ 'b.addEventListener("pointermove",function(e){' +
857
+ 'if(e.buttons===0||!drag&&!sx&&!sy)return;' +
858
+ 'var dx=e.clientX-sx,dy=e.clientY-sy;' +
859
+ 'if(!drag&&Math.sqrt(dx*dx+dy*dy)>6){drag=true;b.style.right="auto";b.style.cursor="grabbing";}' +
860
+ 'if(drag){var L=Math.max(4,Math.min(window.innerWidth-b.offsetWidth-4,sl+dx)),' +
861
+ 'T=Math.max(4,Math.min(window.innerHeight-b.offsetHeight-4,st+dy));' +
862
+ 'b.style.left=L+"px";b.style.top=T+"px";e.preventDefault();}});' +
863
+ 'b.addEventListener("pointerup",function(e){' +
864
+ 'if(drag){e.preventDefault();' +
865
+ 'try{var r=b.getBoundingClientRect();' +
866
+ 'localStorage.setItem("dshm_rlp",JSON.stringify({l:r.left,t:r.top}));}catch(err){}}' +
867
+ 'drag=false;sx=0;sy=0;});})();</script>';
868
+ return html.replace('</body>', `${btn}\n</body>`);
869
+ }));
870
+ }
871
+ ctx.logger.info(`dsh-memory: 角色扮演网页已挂载 /roleplay(data_dir=${roleDataDir},入口按钮=${config.roleplayEntryButton ? '显示(可拖动)' : '隐藏'})`);
849
872
  }
850
873
  catch (err) {
851
874
  ctx.logger.warn(`dsh-memory: 角色扮演网页挂载失败(不影响其他功能): ${String(err && err.message || err)}`);