@a9i5k4/dsh-auto-memory 3.1.1 → 3.1.3

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 CHANGED
@@ -651,7 +651,7 @@ Full credits, including infrastructure sponsors: **[Contributors & Sponsors](htt
651
651
 
652
652
  Development resources for this project are partly provided by:
653
653
 
654
- - **[DSH API](https://api.dshapi.icu/)** — API relay station providing the model endpoints used for development, testing, and the semantic-engine research behind the M-series features. Thank you for keeping the lights on.
654
+ - **[DSH API](https://api.dshapi.icu/register?aff=HJU27P7JL39N)** — API relay station providing the model endpoints used for development, testing, and the semantic-engine research behind the M-series features. Thank you for keeping the lights on.
655
655
 
656
656
  Infrastructure and API-quota sponsors are listed on the **[Contributors & Sponsors](https://htmlpreview.github.io/?https://github.com/Aik358/dsh-auto-memory/blob/main/docs/CONTRIBUTORS.html)** page. If you would like to support the project, open an issue or join the QQ group.
657
657
 
package/README.zh-CN.md CHANGED
@@ -653,7 +653,7 @@ DeepSeek Harness (Node, 127.0.0.1:3080)
653
653
 
654
654
  本项目部分开发资源由以下方提供:
655
655
 
656
- - **[DSH API](https://api.dshapi.icu/)** — API 中转站,为本项目的开发、测试以及 M 系列语义引擎研究提供模型端点。感谢一路同行。
656
+ - **[DSH API](https://api.dshapi.icu/register?aff=HJU27P7JL39N)** — API 中转站,为本项目的开发、测试以及 M 系列语义引擎研究提供模型端点。感谢一路同行。
657
657
 
658
658
  基础设施与 API 额度赞助方列在 **[贡献者与赞助](https://htmlpreview.github.io/?https://github.com/Aik358/dsh-auto-memory/blob/main/docs/CONTRIBUTORS.html)** 页面。如希望支持本项目,欢迎提 issue 或加入 QQ 交流群。
659
659
 
@@ -395,7 +395,7 @@
395
395
  <div class="sponsor">
396
396
  <div class="mark">DSH</div>
397
397
  <div class="body">
398
- <h3><a href="https://api.dshapi.icu/" target="_blank" rel="noopener">DSH API · api.dshapi.icu</a></h3>
398
+ <h3><a href="https://api.dshapi.icu/register?aff=HJU27P7JL39N" target="_blank" rel="noopener">DSH API · api.dshapi.icu</a></h3>
399
399
  <p class="zh">API 中转站,为本项目的开发、回归测试,以及 M 系列语义引擎的选型与激活策略研究提供模型端点。没有这些额度,那些基准实验根本跑不起来。</p>
400
400
  <p class="en">An API relay station providing the model endpoints used for development, regression testing, and the M-series semantic-engine selection / activation research. Without that quota, those benchmark runs simply would not have happened.</p>
401
401
  <div class="tag"><span class="zh">API 额度赞助</span><span class="en">API quota sponsor</span></div>
@@ -442,7 +442,7 @@
442
442
  <a href="https://github.com/Aik358/dsh-auto-memory">README</a> ·
443
443
  <a href="https://github.com/Aik358/dsh-auto-memory/blob/main/docs/USER-GUIDE.zh-CN.md">用户文档</a> ·
444
444
  <a href="https://github.com/Aik358/dsh-auto-memory" target="_blank" rel="noopener">GitHub</a> ·
445
- <a href="https://api.dshapi.icu/" target="_blank" rel="noopener">DSH API</a>
445
+ <a href="https://api.dshapi.icu/register?aff=HJU27P7JL39N" target="_blank" rel="noopener">DSH API</a>
446
446
  </p>
447
447
  <p style="margin-top:10px;opacity:.6">dsh-auto-memory · BSD-3-Clause</p>
448
448
  </footer>
package/docs/HANDBOOK.md CHANGED
@@ -27,7 +27,7 @@
27
27
  | **BM25** | 经典词法相关性算法 |
28
28
  | **M8 / 记忆中枢** | 三层记忆存储(fact 事实/episodic 经历/procedure 技能)与编排器 memory-hub 的合称 |
29
29
  | **evidence(证据)** | 记忆被使用情况的记录,六类:`seen`(曝光)/`read`(读到原文)/`cite`(回复引用)/`reuse`(跨会话复用)/`success`(任务成功)/`correction`(用户纠正) |
30
- | **endpoint / 端点** | 后端 HTTP 接口,路径前缀统一为 `/api/dsh-auto-memory/`(pre 树为 `/api/dsh-auto-memory-pre/`),共 53 条 |
30
+ | **endpoint / 端点** | 后端 HTTP 接口,路径前缀统一为 `/api/dsh-auto-memory/`(pre 树为 `/api/dsh-auto-memory-pre/`),共 54 条 |
31
31
 
32
32
  ---
33
33
 
@@ -78,7 +78,7 @@ dsh web --no-open --port 0
78
78
  │ HTTP /api/dsh-auto-memory/*(pre 树为 /api/dsh-auto-memory-pre/*)
79
79
  ┌───────────────▼───────────────────────────────────────────┐
80
80
  │ 宿主侧(后端)lib/index.js + lib/*-pre.js │
81
- │ · 端点路由(53 个) │
81
+ │ · 端点路由(54 个) │
82
82
  │ · 工具(18 个,供模型调用) │
83
83
  │ · 存储:记忆文件、M8 三层 JSON、evidence 事件 JSONL │
84
84
  │ · 检索:C1 词法 / C2 语义 / C3 Python 语义 │
@@ -169,7 +169,7 @@ dsh web --no-open --port 0
169
169
 
170
170
  ## 5. 后端状态来源(如何确认前端操作已生效)
171
171
 
172
- ### 5.1 主要端点(`lib/index.js` 注册,共 **53** 条,前缀 `/api/dsh-auto-memory/`)
172
+ ### 5.1 主要端点(`lib/index.js` 注册,共 **54** 条,前缀 `/api/dsh-auto-memory/`)
173
173
 
174
174
  | 端点 | 用途 | 关键返回字段 |
175
175
  |---|---|---|
@@ -195,7 +195,7 @@ dsh web --no-open --port 0
195
195
  > node -e "const f=require('fs');const s=f.readFileSync('lib/index.js','utf8'),r=f.readFileSync('tools/release.mjs','utf8');const P=[...r.matchAll(/\['([^']+)',\s*'([^']+)'\]/g)].map(m=>[m[1],m[2]]);const c=x=>P.reduce((a,[b,d])=>a.split(b).join(d),x);const e=[...new Set([...s.matchAll(/'(\/api\/dsh-auto-memory[^']*)'/g)].map(m=>m[1]))];console.log('端点条数:',e.length);console.log('发布名前缀:',c(e[0]).split('/').slice(0,3).join('/')+'/')"
196
196
  > ```
197
197
  >
198
- > 在工程根执行,输出应为 `端点条数: 53` / `发布名前缀: /api/dsh-auto-memory/`。命令读的是**源码 + 发布转换表**,所以两条线的拼写都能对上。
198
+ > 在工程根执行,输出应为 `端点条数: 54` / `发布名前缀: /api/dsh-auto-memory/`。命令读的是**源码 + 发布转换表**,所以两条线的拼写都能对上。
199
199
 
200
200
  ### 5.2 日志
201
201
 
@@ -384,7 +384,7 @@ node tests/smoke/smoke-test-continue-chain-pre.mjs # 58
384
384
  | 工程根 | `D:\dsh-auto-memory` |
385
385
  | 宿主侧入口 | `lib/index.js` |
386
386
  | 浏览器侧入口 | `lib/client.js` |
387
- | 端点前缀 | `/api/dsh-auto-memory/`(53 条;pre 树 `/api/dsh-auto-memory-pre/`) |
387
+ | 端点前缀 | `/api/dsh-auto-memory/`(54 条;pre 树 `/api/dsh-auto-memory-pre/`) |
388
388
  | 工具 | 10 个,`memory_*_pre` |
389
389
  | 记忆根 | `C:\Users\JH Z\.dsh\memory` |
390
390
  | 证据事件 | `...\memory\evidence\events\YYYY-MM-DD.jsonl`(时间戳在 `event.ts`) |
package/lib/client.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * 3. settings.section — 自动记忆设置页(存储位置、注入、反思风格)
8
8
  * Data flows over /api/dsh-auto-memory/* (loopback-only host routes).
9
9
  */
10
- console.log('[dsh-auto-memory] client v3.1.1 fingerprint: migrate-pack-quiet-logs')
10
+ console.log('[dsh-auto-memory] client v3.1.3 fingerprint: session-name-guide-onboarding')
11
11
  window.__ModuleLoader__.load({
12
12
  id: '@a9i5k4/dsh-auto-memory',
13
13
  factory: (require) => {
@@ -234,7 +234,7 @@ window.__ModuleLoader__.load({
234
234
  statsSegChars: '按字符数(谁最占预算)',
235
235
  statsSegCount: '按出现次数',
236
236
  statsSince: '开始记录于',
237
- statsNever: '从未', logs: '日志', refineTab: '唤起回顾', notes: '笔记', reflections: '反思', connect: '接续', calendar: '日历', search: '检索', workspaces: '工作区', hubTab: '记忆中枢', storageTab: '存储管理', planTab: '白板', planTitle: '白板 · PLAN.md(项目全貌)', planEmpty: '(白板尚未建立——宿主会在该工作区第一次会话结束时自动落一版骨架,模型理解项目全貌后把它重写成真内容;也可在对话中让它用 memory_note(kind=plan) 写)', planVersions: '白板历史版本', planLedgers: '交接账本时间线', planDisabled: '交接白板未启用(设置 → 自动化)。', planWsUnbound: '当前会话未绑定工作区——白板与交接账本暂不可用(会话的工作区身份解析不出来;把会话开进项目文件夹或绑定工作区后自动恢复)。', planFileTitle: '查看',
237
+ statsNever: '从未', logs: '日志', refineTab: '唤起回顾', notes: '笔记', reflections: '反思', connect: '接续', calendar: '日历', search: '检索', workspaces: '工作区', hubTab: '记忆中枢', storageTab: '存储管理', planTab: '白板', planTitle: '白板 · PLAN.md(项目全貌)', planEmpty: '(白板尚未建立——宿主会在该工作区第一次会话结束时自动落一版骨架,模型理解项目全貌后把它重写成真内容;也可在对话中让它用 memory_note(kind=plan) 写)', planVersions: '白板历史版本', planLedgers: '交接账本时间线', planDisabled: '交接白板未启用(设置 → 长会话接续)。', planWsUnbound: '当前会话未绑定工作区——白板与交接账本暂不可用(会话的工作区身份解析不出来;把会话开进项目文件夹或绑定工作区后自动恢复)。', planFileTitle: '查看',
238
238
  hubSkills: '技能 (Procedural)', hubSkillsEmpty: '暂无已固化的技能。反复成功的流程会自动固化为技能并在相似场景召回。', hubFacts: '事实 (Semantic)', hubFactsEmpty: '暂无固化的事实。', hubConflicts: '待决冲突', hubEpisodic: '经历 (Episodic)', hubEpisodicEmpty: '暂无已巩固的经历。',
239
239
  storageScanHint: '语料健康 = 逐源比对索引(sidecar)与正文的 digest。手动改动记忆文件后索引会失配,该记忆会退出检索直到重建索引。',
240
240
  storageDeleteHint: '删除记忆 = 正文原子删除 + 在途唤起包清理 + 派生事实撤销(三联动)。已产生的 seen 证据不改写。',
@@ -281,7 +281,7 @@ window.__ModuleLoader__.load({
281
281
  pickedDir: '已选择目录:', rememberSave: '(点击「保存设置」后生效)', pickerUnavailable: '系统文件夹选择器不可用,改用内嵌浏览。',
282
282
  fDayBoundary: '日界(分钟)', fDayBoundaryHint: '从 0 点起算:凌晨在此之前的活儿归前一天。默认 450=早上 7:30 进入新一天;改 480=8:00;0=按午夜切日。',
283
283
  fVersion: '插件版本', checkUpdate: '检查更新', checking: '检查中…', upToDate: '(已是最新)', hasUpdate: '(有新版本)', versionError: '版本检查失败: ', versionCmdHint: '更新命令: cd ~/.dsh/profiles/web && pnpm up @a9i5k4/dsh-auto-memory,然后重启 dsh web 生效。',
284
- updateNow: '一键更新', updating: '更新中…', updateDone: '更新完成,请重启 dsh web 生效。', updateFailed: '更新失败: ', devLinkHint: '(当前为本地开发链接,请直接同步开发源码)', noProfileHint: '(未找到 dsh profile,无法自动更新)',
284
+ updateNow: '一键更新', updating: '更新中…', updateDone: '更新完成,请重启 dsh web 生效。', updateFailed: '更新失败: ', devLinkHint: '(当前为本地开发链接 link:,一键更新不适用。若上方显示「开发树 vX → 线上 vY」,说明 npm 上已有更新版,请同步开发源码后重新构建发布。)', noProfileHint: '(未找到 dsh profile,无法自动更新)',
285
285
  kvVersion: '插件版本', kvUnreadable: '(读不到)', kvPid: 'PID / 启动', kvRestart: 'Host 需重启?', kvRestartYes: '是(代码比进程新)', kvRestartNo: '否',
286
286
  kvHeartbeat: '轮询心跳', hbAliveAgo: '存活(上次 ', hbSecAgo: ' 秒前)', hbAlive: '存活', hbNone: '无心跳文件',
287
287
  kvQueue: '沉淀重试队列', kvCountSuffix: ' 条', kvToday: '今日沉淀', kvRecentPrefix: ' 条 / 最近 ', kvBusy: '正在沉淀中', kvYes: '是', kvNo: '否',
@@ -476,7 +476,7 @@ window.__ModuleLoader__.load({
476
476
  statsSegChars: 'By characters (who eats the budget)',
477
477
  statsSegCount: 'By occurrences',
478
478
  statsSince: 'Recording since',
479
- statsNever: 'never', logs: 'Logs', refineTab: 'Recall review', notes: 'Notes', reflections: 'Reflections', connect: 'Connect', calendar: 'Calendar', search: 'Search', workspaces: 'Workspaces', hubTab: 'Memory Hub', storageTab: 'Storage', planTab: 'Whiteboard', planTitle: 'Whiteboard · PLAN.md (project plan)', planEmpty: '(not created yet — the host seeds a skeleton board when a turn first ends in this workspace; the model rewrites it with real content once it grasps the picture, or ask it to write memory_note kind=plan)', planVersions: 'Plan versions', planLedgers: 'Handoff ledger timeline', planDisabled: 'Handoff whiteboard is disabled (Settings → Automation).', planWsUnbound: 'This session has no bound workspace — whiteboard and handoff ledgers are unavailable (the session workspace could not be resolved; open the session inside a project folder or bind a workspace to restore).', planFileTitle: 'View',
479
+ statsNever: 'never', logs: 'Logs', refineTab: 'Recall review', notes: 'Notes', reflections: 'Reflections', connect: 'Connect', calendar: 'Calendar', search: 'Search', workspaces: 'Workspaces', hubTab: 'Memory Hub', storageTab: 'Storage', planTab: 'Whiteboard', planTitle: 'Whiteboard · PLAN.md (project plan)', planEmpty: '(not created yet — the host seeds a skeleton board when a turn first ends in this workspace; the model rewrites it with real content once it grasps the picture, or ask it to write memory_note kind=plan)', planVersions: 'Plan versions', planLedgers: 'Handoff ledger timeline', planDisabled: 'Handoff whiteboard is disabled (Settings → Handoff).', planWsUnbound: 'This session has no bound workspace — whiteboard and handoff ledgers are unavailable (the session workspace could not be resolved; open the session inside a project folder or bind a workspace to restore).', planFileTitle: 'View',
480
480
  hubSkills: 'Skills (Procedural)', hubSkillsEmpty: 'No solidified skills yet. Repeatedly-successful workflows become skills and are recalled in similar contexts.', hubFacts: 'Facts (Semantic)', hubFactsEmpty: 'No solidified facts yet.', hubConflicts: 'Pending conflicts', hubEpisodic: 'Episodes (Episodic)', hubEpisodicEmpty: 'No consolidated episodes yet.',
481
481
  storageScanHint: 'Corpus health = compare each source\'s index (sidecar) against its body digest. After you hand-edit a memory file the index no longer matches, and that memory drops out of retrieval until the index is rebuilt.',
482
482
  storageDeleteHint: 'Delete = atomic body removal + in-flight activation purge + derived-fact revocation (cascading). Evidence already recorded (seen) is never rewritten.',
@@ -522,7 +522,7 @@ window.__ModuleLoader__.load({
522
522
  pickedDir: 'Selected folder:', rememberSave: '(click Save to apply)', pickerUnavailable: 'Native folder picker unavailable, falling back to in-app browser.',
523
523
  fDayBoundary: 'Day boundary (minutes)', fDayBoundaryHint: 'Minutes from midnight: work before it counts as the previous day. Default 450 = 7:30 AM starts the new day; 480 = 8:00; 0 = midnight cut.',
524
524
  fVersion: 'Plugin version', checkUpdate: 'Check for updates', checking: 'Checking…', upToDate: '(up to date)', hasUpdate: '(update available)', versionError: 'Version check failed: ', versionCmdHint: 'Update: cd ~/.dsh/profiles/web && pnpm up @a9i5k4/dsh-auto-memory, then restart dsh web.',
525
- updateNow: 'Update now', updating: 'Updating…', updateDone: 'Update finished, restart dsh web to apply.', updateFailed: 'Update failed: ', devLinkHint: '(local dev link, sync source instead)', noProfileHint: '(no dsh profile found, cannot auto-update)',
525
+ updateNow: 'Update now', updating: 'Updating…', updateDone: 'Update finished, restart dsh web to apply.', updateFailed: 'Update failed: ', devLinkHint: '(local dev link — one-click update does not apply. If "dev tree vX to registry vY" shows above, a newer version is on npm; sync the source and rebuild.)', noProfileHint: '(no dsh profile found, cannot auto-update)',
526
526
  kvVersion: 'Plugin version', kvUnreadable: '(unreadable)', kvPid: 'PID / Started', kvRestart: 'Host restart needed?', kvRestartYes: 'yes (code newer than process)', kvRestartNo: 'no',
527
527
  kvHeartbeat: 'Heartbeat', hbAliveAgo: 'alive (last ', hbSecAgo: ' s ago)', hbAlive: 'alive', hbNone: 'no heartbeat file',
528
528
  kvQueue: 'Consolidation retry queue', kvCountSuffix: ' item(s)', kvToday: 'Consolidated today', kvRecentPrefix: ' item(s) / latest ', kvBusy: 'Consolidating now', kvYes: 'yes', kvNo: 'no',
@@ -806,7 +806,30 @@ window.__ModuleLoader__.load({
806
806
 
807
807
  // ───────────────────────── 更新弹窗 / 首次指导 ─────────────────────────
808
808
  var CHANGELOG = {
809
- '3.1.1': { zh: [
809
+ '3.1.3': { zh: [
810
+ '★ 修复:宿主下次改会话文件名时,五条功能链会集体静默降级 —— 已从根上拆掉;会话日志路径不再用固定候选表,改按结构识别(兼容未来 v4 命名)。',
811
+ '★ 修复:迁移搬包的范围扩到日历(CALENDAR.md);日历跨工作区共享,导入按合并不覆盖,本机已完成的条目不被打回,同包重导不重复。',
812
+ '★ 修复:「周期记忆快照」开关此前是死开关 —— 界面能翻面、能落盘,但记忆照旧每轮注入。现在关掉它真的会停注入(只关这一条通路,记忆写入与检索不受影响)。',
813
+ '修复:引导里「思维链监听」原写「默认关」,真值是开 —— 方向反了,会误导隐私判断;已改正。',
814
+ '修复:引导里「技能固化」原说晋升审批在记忆中枢页(实际在唤起回顾页),且该开关并不控晋升;两处指引已纠正。',
815
+ '修复:白板空态与索引提示指向的分区名已废弃(自动化/自动记忆引擎);改为实际分区名(长会话接续/语义记忆总开关)。',
816
+ '修复:设置页补上「机械流程切片」控件 —— 该键功能本来是通的,但此前只有向导里能改。',
817
+ '新增:引导末页新增「贡献者与赞助」「DSH API 中转站」两个入口。',
818
+ '新增:关闭新手引导后弹出的更新说明,从此跟随当前版本(此前恒为 2.1.0)。',
819
+ '新增:云盘/远端同步目录探测(判断目录是否可用并给出人话诊断;只当普通目录,不依赖任何网盘专有接口)。',
820
+ ], en: [
821
+ '★ Fix: five feature chains would silently degrade the next time the host renamed its session-log file — root cause removed. Session paths are no longer a fixed candidate list; they are matched structurally, so future v4 names work.',
822
+ '★ Fix: pack-and-carry now includes your calendar (CALENDAR.md). The calendar is shared across workspaces, so import merges instead of overwriting: locally completed items are never reverted, and re-importing the same pack adds nothing.',
823
+ '★ Fix: the "periodic memory snapshot" switch used to be dead — it flipped and saved, but memories were injected every turn regardless. Turning it off now really stops injection (only that one path; memory writes and search are untouched).',
824
+ 'Fix: the tour said the reasoning observer defaults OFF — it defaults ON. That reversal could mislead privacy decisions; corrected.',
825
+ 'Fix: the tour said promotion review lives in the Memory Hub tab (it is the Recall review tab), and that switch does not control promotion at all; both pointers corrected.',
826
+ 'Fix: the whiteboard empty state and index hints pointed at renamed/removed settings sections; now they name the real ones.',
827
+ 'Fix: Settings now exposes the "mechanical flow slicing" control — the key worked, but previously only the tour could change it.',
828
+ 'New: two entry points on the tour’s last page — Contributors & Sponsors, and the DSH API relay.',
829
+ 'New: the release-notes card after the tour now follows the current version (it used to be pinned to 2.1.0).',
830
+ 'New: cloud/remote sync-directory probe (reports whether a directory is usable, with a plain-language hint; treats it as an ordinary folder and depends on no vendor API).',
831
+ ] },
832
+ '3.1.1': { zh: [
810
833
  '新增:记忆搬包 —— 把某个工作区的记忆整体搬到另一台机器。「存储管理」页签新增入口:导出本工作区得到 .dam-pack,到新机器导入。',
811
834
  '导入是三步向导:选包 → 预览差异(只读,不动任何文件) → 确认导入。预览会列出路径是否变化、新增/覆盖几个文件、内部引用要重写多少处。',
812
835
  '换机器会自动重写内部路径:包内指向旧路径/旧 slug 的引用(含正文里的路径字样)按 4 种写法逐一替换成新机器真实路径;路径相同则一个字都不改。',
@@ -1347,6 +1370,8 @@ window.__ModuleLoader__.load({
1347
1370
  pyStatus: ROUTE_PREFIX + '/python-setup/status',
1348
1371
  handoffState: ROUTE_PREFIX + '/handoff-state',
1349
1372
  kanbanBoard: ROUTE_PREFIX + '/kanban-board',
1373
+ // ★v3.1.2:卡片全文按需取(看板载荷不再内联 full)
1374
+ kanbanCard: ROUTE_PREFIX + '/kanban-card',
1350
1375
  handoffContinue: ROUTE_PREFIX + '/handoff-continue',
1351
1376
  handoffPermission: ROUTE_PREFIX + '/handoff-permission',
1352
1377
  autoContState: ROUTE_PREFIX + '/auto-continue-state',
@@ -1965,7 +1990,9 @@ window.__ModuleLoader__.load({
1965
1990
  * 正文 = 剩余行拼接(已清洗), 与标题不重复 —— 这正是「标题+正文分离」。
1966
1991
  */
1967
1992
  function wbShape(c) {
1968
- var rawFull = c.full || c.preview || ''
1993
+ // ★v3.1.2 性能:列表渲染(节点画布)用 preview 即可 —— 载荷已不内联 full,
1994
+ // 此处若仍读 c.full 会恒为空 ⇒ 标题/正文全丢。preview 是稳定来源。
1995
+ var rawFull = c.preview || c.full || ''
1969
1996
  var ls = wbCleanLines(rawFull)
1970
1997
  var head = ls.length ? ls[0] : ''
1971
1998
  var title = head ? (head.length > 60 ? head.slice(0, 57) + '…' : head) : (c.title || c.short || '')
@@ -2005,6 +2032,34 @@ window.__ModuleLoader__.load({
2005
2032
  return 'rgba(128,128,128,.75)'
2006
2033
  }
2007
2034
 
2035
+ /**
2036
+ * ★v3.1.2 性能:卡片全文**按需取**。
2037
+ * 看板载荷不再内联 full(1178 卡 × ≤6000 字符、且 lanes+matrix 双份 ⇒ 约 1.7 MB 纯冗余),
2038
+ * 取而代之的是 fullLen;点「展开全文」/打开抽屉时才按 id 向 /kanban-card 单取一次并缓存。
2039
+ * 失败/未取到时**回退 preview**(绝不显示空白)。
2040
+ */
2041
+ var wbFullCache = {} // id -> string(进程内缓存,避免反复取同一张)
2042
+ function useCardFull(card) {
2043
+ var p = useState(function () { return (card && wbFullCache[card.id]) || '' })
2044
+ var text = p[0], setText = p[1]
2045
+ useEffect(function () {
2046
+ var c = card || {}
2047
+ if (!c.id) return
2048
+ if (wbFullCache[c.id] !== undefined) { setText(wbFullCache[c.id]); return }
2049
+ // 载荷里若仍带 full(老版本/其它来源)直接用,不打请求
2050
+ if (typeof c.full === 'string') { wbFullCache[c.id] = c.full; setText(c.full); return }
2051
+ var alive = true
2052
+ apiGet(API.kanbanCard, { id: c.id, sessionId: currentSessionIdClient() }).then(function (r) {
2053
+ if (!alive) return
2054
+ var t = (r && r.ok && typeof r.full === 'string') ? r.full : ''
2055
+ wbFullCache[c.id] = t
2056
+ setText(t)
2057
+ }).catch(function () { if (alive) { wbFullCache[c.id] = ''; setText('') } })
2058
+ return function () { alive = false }
2059
+ }, [card && card.id])
2060
+ return text
2061
+ }
2062
+
2008
2063
  /** 单张卡片: 标题 + 判据徽章 + 预览/全文 + 小节徽章 + 标签 + 来源。可点击展开。 */
2009
2064
  function KanbanCard(props) {
2010
2065
  var c = props.card || {}
@@ -2012,8 +2067,10 @@ window.__ModuleLoader__.load({
2012
2067
  var expanded = !!props.expanded
2013
2068
  var onToggle = props.onToggle
2014
2069
  var tone = KANBAN_TONES[(c.badge && c.badge.tone) || 'unknown'] || KANBAN_TONES.unknown
2015
- var body = expanded ? (c.full || c.preview || '') : (c.preview || '')
2016
- var canExpand = (c.full || '').length > (c.preview || '').length
2070
+ // ★v3.1.2:全文按需取(展开时才请求;未取到回退 preview)
2071
+ var fullText = useCardFull(expanded ? c : null)
2072
+ var body = expanded ? (fullText || c.full || c.preview || '') : (c.preview || '')
2073
+ var canExpand = (typeof c.fullLen === 'number' ? c.fullLen : (c.full || '').length) > (c.preview || '').length
2017
2074
  return h('div', {
2018
2075
  'data-dam-kcard': '',
2019
2076
  title: c.source + (c.section ? ' §' + c.section : '') + (c.anchored ? ' · 有锚点' : ''),
@@ -2038,7 +2095,7 @@ window.__ModuleLoader__.load({
2038
2095
  maxHeight: expanded ? 'none' : (7.2 * zoom) + 'em', overflow: 'hidden',
2039
2096
  },
2040
2097
  }, body) : null,
2041
- (canExpand || (c.full || '').length > 300) ? h('button', {
2098
+ (canExpand || (typeof c.fullLen === 'number' ? c.fullLen : (c.full || '').length) > 300) ? h('button', {
2042
2099
  'data-dam-btn': '', onClick: function (e) { e.stopPropagation(); if (onToggle) onToggle(c.id) },
2043
2100
  style: { marginTop: '3px', padding: '1px 6px', fontSize: (10 * zoom) + 'px', opacity: .7 },
2044
2101
  }, expanded ? (locale === 'zh' ? '收起' : 'collapse') : (locale === 'zh' ? '展开全文' : 'expand')) : null,
@@ -2458,7 +2515,7 @@ window.__ModuleLoader__.load({
2458
2515
  ? h('div', { key: 'sg', style: { display: 'flex', gap: '4px', flexWrap: 'wrap', marginBottom: '8px' } },
2459
2516
  drawer.sectionTags.map(function (tg, i) { return h('span', { key: i, 'data-dam-chip': '', style: { fontSize: '10px', padding: '1px 5px', borderRadius: '3px', background: 'rgba(76,141,255,.18)' } }, tg) }))
2460
2517
  : null,
2461
- h('pre', { key: 'body', 'data-dam-kx-full': drawer.id, style: { whiteSpace: 'pre-wrap', wordBreak: 'break-word', fontFamily: 'inherit', margin: 0, fontSize: '12px' } }, drawer.full || drawer.preview || ''),
2518
+ h('pre', { key: 'body', 'data-dam-kx-full': drawer.id, style: { whiteSpace: 'pre-wrap', wordBreak: 'break-word', fontFamily: 'inherit', margin: 0, fontSize: '12px' } }, (useCardFull(drawer) || drawer.full || drawer.preview || '')),
2462
2519
  ].map(function (node, i) {
2463
2520
  // 头部固定、正文滚动: 把内容各自包一层; 正文那层给 flex:1 + overflow:auto
2464
2521
  if (i === 0) return h('div', { key: 'headwrap', style: { padding: '12px 14px 8px', flexShrink: 0 } }, node)
@@ -3813,7 +3870,9 @@ window.__ModuleLoader__.load({
3813
3870
  if (migBusy) return
3814
3871
  opts = opts || {}
3815
3872
  setMigBusy('export'); setMigErr(''); setMigNote('')
3816
- apiPost(API.migrateExport, { outPath: migOut || undefined }).then(function (j) {
3873
+ // ★v3.1.2 修:此前只发 outPath,宿主拿不到工作区 ⇒ 恒返回 missing-ws(用户实测)。
3874
+ // 本文件既有 currentWs()(跟随 GUI 切换工作区),导出必须显式带上。
3875
+ apiPost(API.migrateExport, { ws: currentWs() || undefined, outPath: migOut || undefined }).then(function (j) {
3817
3876
  setMigBusy('')
3818
3877
  if (!j || j.ok === false) { migFail(t('migExport'), j, null); return }
3819
3878
  var s = j.stats || {}
@@ -3830,7 +3889,7 @@ window.__ModuleLoader__.load({
3830
3889
  if (migBusy) return
3831
3890
  if (!migPack) { setMigErr(t('migPickPack')); return }
3832
3891
  setMigBusy('preview'); setMigErr(''); setMigNote(''); setMigPlan(null); setMigResult(null)
3833
- apiPost(API.migrateInspect, { packPath: migPack }).then(function (j) {
3892
+ apiPost(API.migrateInspect, { packPath: migPack, targetWs: currentWs() || undefined }).then(function (j) {
3834
3893
  setMigBusy('')
3835
3894
  if (!j || j.ok === false) { migFail(t('migPreview'), j, null); return }
3836
3895
  var p = j.plan || {}
@@ -3843,7 +3902,7 @@ window.__ModuleLoader__.load({
3843
3902
  if (migBusy) return
3844
3903
  if (!migPack) { setMigErr(t('migPickPack')); return }
3845
3904
  setMigBusy('apply'); setMigErr(''); setMigNote('')
3846
- apiPost(API.migrateImport, { packPath: migPack, onConflict: migConflict }).then(function (j) {
3905
+ apiPost(API.migrateImport, { packPath: migPack, targetWs: currentWs() || undefined, onConflict: migConflict }).then(function (j) {
3847
3906
  setMigBusy('')
3848
3907
  if (!j || j.ok === false) { migFail(t('migApply'), j, null); return }
3849
3908
  setMigResult(j)
@@ -3866,7 +3925,7 @@ window.__ModuleLoader__.load({
3866
3925
  .then(function (j) {
3867
3926
  var reason = (j && (j.reason || (j.ok === false ? 'rejected' : 'ok'))) || 'done'
3868
3927
  if (j && j.ok === false && j.reason === 'no-doc-store') {
3869
- reason = locale === 'zh' ? '记忆锚定索引未启用:请在 设置 → 自动记忆引擎 开启「记忆锚定索引」后重试;或忽略此提示(不影响记忆读写与检索)' : 'anchor index disabled: enable "Memory anchor index" under Settings → Semantic engine, or ignore (read/write/recall unaffected)'
3928
+ reason = locale === 'zh' ? '记忆锚定索引未启用:请在 设置 → 语义记忆总开关 开启「记忆锚定索引」后重试;或忽略此提示(不影响记忆读写与检索)' : 'anchor index disabled: enable "Memory anchor index" under Settings → Semantic engine, or ignore (read/write/recall unaffected)'
3870
3929
  }
3871
3930
  setMsg(action + ': ' + reason)
3872
3931
  if (onDone) onDone(j)
@@ -3882,7 +3941,7 @@ window.__ModuleLoader__.load({
3882
3941
  var rows = []
3883
3942
  if (data.indexEnabled === false) {
3884
3943
  rows.push(h('div', { 'data-dam-hint': '', style: { marginTop: '6px', color: 'var(--dsw-alias-state-warn-primary, #e8c584)' } },
3885
- locale === 'zh' ? '⚠ 记忆锚定索引未启用:语料健康不做 sidecar 比对(无 stale 可修)。如需「修复 stale / 删除记忆」,请到 设置 → 自动记忆引擎 开启「记忆锚定索引」。此提示不影响记忆读写与检索。' : '⚠ Memory anchor index disabled: corpus health performs no sidecar comparison (nothing stale). To use repair/delete, enable "Memory anchor index" under Settings → Semantic engine. Read/write/recall are unaffected.'))
3944
+ locale === 'zh' ? '⚠ 记忆锚定索引未启用:语料健康不做 sidecar 比对(无 stale 可修)。如需「修复 stale / 删除记忆」,请到 设置 → 语义记忆总开关 开启「记忆锚定索引」。此提示不影响记忆读写与检索。' : '⚠ Memory anchor index disabled: corpus health performs no sidecar comparison (nothing stale). To use repair/delete, enable "Memory anchor index" under Settings → Semantic engine. Read/write/recall are unaffected.'))
3886
3945
  }
3887
3946
  rows.push(h('div', { 'data-dam-hint': '', style: { marginTop: '8px' } }, t('storageScanHint')))
3888
3947
  rows.push(h(Card, { title: (locale === 'zh' ? '语料健康' : 'Corpus health') + ' (' + counts.ok + '/' + counts.total + ' ok)' },
@@ -5857,7 +5916,7 @@ function WorkspaceTab() {
5857
5916
  text: locale === 'zh' ? '词法检索(0GB)永远可用作保底;内置语义引擎(约 130MB 量化模型)显著提升召回;进阶 Python 引擎(BGE-M3,约 563MB)面向深度用户。以下为自动检测结果,可在此直接安装。另有一个与隐私相关的检索信号开关:'
5858
5917
  : 'Lexical search (0GB) is the always-on floor; the built-in engine (~130MB quantized model) boosts recall; the advanced Python engine (BGE-M3, ~563MB) is optional. Live detection below — install right here. One privacy-related retrieval switch:',
5859
5918
  toggles: [
5860
- { key: 'reasoningObserverEnabled', name: locale === 'zh' ? '思维链监听' : 'Reasoning observer', sub: locale === 'zh' ? '监听模型思维链分段作为检索信号(默认关;内容比可见输出更敏感,按需开)' : 'Watch model CoT segments as a retrieval signal (default off; more sensitive than visible output)', def: false, where: locale === 'zh' ? '自动记忆引擎' : 'Semantic engine' },
5919
+ { key: 'reasoningObserverEnabled', name: locale === 'zh' ? '思维链监听' : 'Reasoning observer', sub: locale === 'zh' ? '监听模型思维链分段作为检索信号(默认开;内容比可见输出更敏感,不需要可在此关掉)' : 'Watch model CoT segments as a retrieval signal (default ON; more sensitive than visible output — turn off here if unwanted)', def: false, where: locale === 'zh' ? '自动记忆引擎' : 'Semantic engine' },
5861
5920
  ] },
5862
5921
  { art: 'radar', core: '', kicker: locale === 'zh' ? '唤起与固化' : 'ACTIVATION',
5863
5922
  title: locale === 'zh' ? '该出手时才出手' : 'Interrupt only when it matters',
@@ -5868,7 +5927,7 @@ function WorkspaceTab() {
5868
5927
  { key: 'autoConsolidate', name: locale === 'zh' ? '记忆固化(自动沉淀)' : 'Memory consolidation', sub: locale === 'zh' ? '每轮对话结束自动把结论沉淀进每日日志与记忆店(推荐开)' : 'Consolidate conclusions into the log & stores each turn (recommended)', rec: true, where: locale === 'zh' ? '自动化' : 'Automation' },
5869
5928
  // ★2026-09-22 改键(设置页缺口审计 P0-2):旧键 procedurePromotionEnabled 因 DEFAULT_CONFIG
5870
5929
  // 恒供给布尔 procedureInjectEnabled 而**结构性不可达** ⇒ 该开关此前是摆设(会翻面、写盘成功、宿主读不到)。
5871
- { key: 'procedureInjectEnabled', name: locale === 'zh' ? '技能固化与注入' : 'Skill crystallization', sub: locale === 'zh' ? '重复流程固化为 checklist 自动附上;跨会话验证后晋升(「记忆中枢」页审批)' : 'Turn repeated flows into auto-attached checklists; promote after validation (Memory Hub tab)', where: locale === 'zh' ? '记忆中枢' : 'Memory Hub' },
5930
+ { key: 'procedureInjectEnabled', name: locale === 'zh' ? '技能固化与注入' : 'Skill crystallization', sub: locale === 'zh' ? '重复流程固化为 checklist 自动附上(本开关只管「附不附」;晋升审批在「唤起回顾」页,与本开关无关)' : 'Turn repeated flows into auto-attached checklists (this switch only controls attaching; promotion review lives in the Recall review tab and is unrelated)', where: locale === 'zh' ? '记忆中枢' : 'Memory Hub' },
5872
5931
  // ★T10(2026-09-20):机械 procedure 切片开关。
5873
5932
  // 背景:`crossFeed()` 会把 episode 意图**机械截断**成技能名与步骤(无模型介入),
5874
5933
  // 产出的观察行「既不像技能名也不像步骤」——用户报「审批里看不懂」的来源。
@@ -5957,8 +6016,9 @@ function WorkspaceTab() {
5957
6016
  try {
5958
6017
  apiGet(API.updateCheck).then(function (d) {
5959
6018
  var cur = (d && d.current) || '2.1.0'
5960
- // 只弹大更新(2.1.0);历次小更新已总结进大卡
5961
- var latestKey = '2.1.0'
6019
+ // ★v3.1.3:**动态取最新版本**(此前硬编码 '2.1.0' ⇒ 关掉向导永远弹 2.1.0 的老卡)。
6020
+ // 与 :~7280 那处已正确的写法同源;兜底仍是 '2.1.0'(CHANGELOG 为空也不崩)。
6021
+ var latestKey = Object.keys(CHANGELOG).reduce(function (a, b) { return cmpVersion(b, a) > 0 ? b : a }, '2.1.0')
5962
6022
  openDialog({ kind: 'update', versions: [{ version: latestKey, items: CHANGELOG[latestKey] || CHANGELOG['2.1.0'] }], currentVersion: cur })
5963
6023
  try { localStorage.setItem('dsh-auto-memory.seenVersion', cur) } catch (e5) {}
5964
6024
  }).catch(function () {
@@ -6058,6 +6118,16 @@ function WorkspaceTab() {
6058
6118
  return h('div', { key: k }, '· ', h('b', null, k), ' —— ' + whereGroups[k].join(locale === 'zh' ? ' / ' : ' / '))
6059
6119
  }),
6060
6120
  h('div', null, '· ', h('b', null, locale === 'zh' ? '面板页签' : 'Panel tabs'), locale === 'zh' ? ' —— 唤起回顾(决策打分)/ 存储管理(扫描修复)' : ' — Recall review / Storage tools'))) : null),
6121
+ // ★v3.1.3:末页赞助/贡献入口(用户要求「两个小按钮 + 简短注释,不破坏原有结构」)。
6122
+ // 作为 where 的**兄弟节点追加**;文案走 inline locale(与 TOUR_STEPS 全体现有写法一致),
6123
+ // 不新增 i18n 键 ⇒ 零 i18n 表改动。
6124
+ step.final ? h('div', { 'data-dam-tour-links': '' },
6125
+ h('a', { href: 'https://htmlpreview.github.io/?https://github.com/Aik358/dsh-auto-memory/blob/main/docs/CONTRIBUTORS.html', target: '_blank', rel: 'noopener noreferrer', 'data-dam-tour-link': '' },
6126
+ locale === 'zh' ? '贡献者与赞助' : 'Contributors & Sponsors'),
6127
+ h('a', { href: 'https://api.dshapi.icu/register?aff=HJU27P7JL39N', target: '_blank', rel: 'noopener noreferrer', 'data-dam-tour-link': '' },
6128
+ locale === 'zh' ? 'DSH API 中转站' : 'DSH API relay'),
6129
+ h('div', { 'data-dam-tour-links-note': '' },
6130
+ locale === 'zh' ? '感谢为本项目出力的贡献者与基础设施赞助方。' : 'Thanks to our contributors and infrastructure sponsors.')) : null,
6061
6131
  h('div', { 'data-dam-tour-dots': '' },
6062
6132
  TOUR_STEPS.map(function (s, si) {
6063
6133
  return h('button', { key: si, 'data-dam-tour-dot': '', 'data-on': String(si === tourStep),
@@ -6930,6 +7000,9 @@ function WorkspaceTab() {
6930
7000
  // 2026-09-22 补接线(设置页缺口审计 P0-1):procedureInjectEnabled 才是宿主真读的总闸
6931
7001
  // (procedure-switch.js 先判它);此前设置页只有旧键 ⇒ 用户改了也无效(灰控件)。
6932
7002
  field(t('fProcInject'), h('input', { type: 'checkbox', 'data-dam-key': 'procedureInjectEnabled', checked: cfg.procedureInjectEnabled !== false, onChange: function (e) { set('procedureInjectEnabled', e.target.checked) } }), t('fProcInjectHint')),
7003
+ // ★v3.1.3 补控件(引导审计 K-3):本键功能是通的(index.js 有 getter → memory-hub 消费),
7004
+ // 但此前**只有向导里有控件** ⇒ 用户在向导关掉后无处回改。与 procedureInjectEnabled 同节。
7005
+ field(locale === 'zh' ? '机械流程切片' : 'Mechanical flow slicing', h('input', { type: 'checkbox', 'data-dam-key': 'hubMechanicalProcedureFeedEnabled', checked: cfg.hubMechanicalProcedureFeedEnabled === true, onChange: function (e) { set('hubMechanicalProcedureFeedEnabled', e.target.checked) } }), locale === 'zh' ? '把情节意图机械截断成"技能"(无模型介入、可读性差)。关=只保留模型自己写的技能(推荐关)。' : 'Truncates episode intent into "skills" with no model involvement (poor readability). Off = only model-authored skills (recommended).'),
6933
7006
  field(t('fProcLevel'), h('select', { 'data-dam-select': '', value: cfg.procedureActiveLevel || 'checklist', onChange: function (e) { set('procedureActiveLevel', e.target.value) } },
6934
7007
  h('option', { value: 'checklist' }, locale === 'zh' ? 'checklist 完整步骤' : 'checklist (full steps)'),
6935
7008
  h('option', { value: 'excerpt' }, locale === 'zh' ? 'excerpt 摘要' : 'excerpt (summary)'),
@@ -7060,6 +7133,11 @@ function WorkspaceTab() {
7060
7133
  h('span', { style: { flex: 1 } }, verInfo ? (verInfo.current || '?') + (verInfo.latest ? ' → ' + verInfo.latest + (verInfo.upToDate ? ' ' + t('upToDate') : ' ' + t('hasUpdate')) : '') : (checkingUpdate ? t('checking') : '—')),
7061
7134
  h('button', { 'data-dam-btn': '', onClick: checkUpdate, disabled: checkingUpdate }, checkingUpdate ? t('checking') : t('checkUpdate')),
7062
7135
  (verInfo && verInfo.latest && !verInfo.upToDate && verInfo.installKind === 'registry') ? h('button', { 'data-dam-btn': '', onClick: doUpdate, disabled: upBusy, style: { marginLeft: '4px' } }, upBusy ? t('updating') : t('updateNow')) : null,
7136
+ // ★v3.1.2:dev-link(开发树挂载)无法「一键更新」,但也必须让用户看见线上是否有新版 ——
7137
+ // 此前此处恒为 null,用户既看不到按钮也看不到「有新版」,反馈「按钮消失且不知道为什么」。
7138
+ (verInfo && verInfo.installKind === 'dev-link' && verInfo.latest && verInfo.devVersion && verInfo.latest !== verInfo.devVersion)
7139
+ ? h('span', { 'data-dam-hint': '', style: { marginLeft: '6px' } }, (locale === 'zh' ? '开发树 v' : 'dev tree v') + verInfo.devVersion + ' → ' + (locale === 'zh' ? '线上 v' : 'registry v') + verInfo.latest + (locale === 'zh' ? ',请同步开发源码后重新构建' : ' — sync source and rebuild'))
7140
+ : null,
7063
7141
  upMsg ? h('span', { 'data-dam-hint': '' }, upMsg) : null),
7064
7142
  t('versionCmdHint') + (verInfo && verInfo.error ? ' ' + t('versionError') + verInfo.error : '') + (verInfo && verInfo.installKind === 'dev-link' ? ' ' + t('devLinkHint') : '') + (verInfo && !verInfo.installKind ? ' ' + t('noProfileHint') : '')),
7065
7143
  field(locale === 'zh' ? '交流群' : 'Community', h('div', { 'data-dam-row': '' },
package/lib/index.js CHANGED
@@ -22,7 +22,7 @@ import { createReadStream, existsSync, readdirSync, readFileSync, writeFileSync,
22
22
  import { exec as cpExec } from 'node:child_process'
23
23
  import { promisify } from 'node:util'
24
24
  const execP = promisify(cpExec)
25
- import { homedir } from 'node:os'
25
+ import { homedir, hostname as hostnameOf } from 'node:os'
26
26
  import { randomUUID } from 'node:crypto'
27
27
  import path from 'node:path'
28
28
  import { fileURLToPath } from 'node:url'
@@ -59,7 +59,7 @@ import { pickConsolidationTextPre } from './intent-clean.js'
59
59
  // 必须独占一行(本仓契约守卫以字面量断言既有 import 行,禁止为排版合并)。
60
60
  import { stripRuntimeIntentPre, looksRuntimeResiduePre } from './intent-clean-safe.js'
61
61
  import { createStorageManagerPre } from './storage-manage.js'
62
- import { workspaceSlugPre as migrateSlugPre, buildPackPre, validatePackPre, planImportPre, mergeSummaryRecordPre, MIGRATE_PACK_FORMAT_PRE } from './migrate-pack.js'
62
+ import { workspaceSlugPre as migrateSlugPre, buildPackPre, validatePackPre, planImportPre, mergeSummaryRecordPre, calendarMergePre, MIGRATE_PACK_FORMAT_PRE } from './migrate-pack.js'
63
63
  import { rankWorkspacesByMemoryRecencyPre } from './ws-overview-rank.js'
64
64
  import { scanPluginSubagentSessions, recycleSessions, PLUGIN_LABEL_PREFIX, decodeZstdFrames, decodeZstdFramesHead } from './subagent-gc.js'
65
65
  import { parseModelWindowsPre, pickWindowPre, findOfficialContextWindowPre, findSessionModelPre, scanPressureSignalsPre, reusableWindowCachePre, shouldArmAutoContinuePre } from './water-window.js'
@@ -254,6 +254,8 @@ export const API = {
254
254
  'semantic-deep-detect': '/api/dsh-auto-memory/semantic-deep-detect',
255
255
  'handoff-state': '/api/dsh-auto-memory/handoff-state',
256
256
  'kanban-board': '/api/dsh-auto-memory/kanban-board',
257
+ // ★v3.1.2:卡片全文按需取(看板载荷不再内联 full ⇒ 展开全文时单取一张)。
258
+ 'kanban-card': '/api/dsh-auto-memory/kanban-card',
257
259
  'handoff-continue': '/api/dsh-auto-memory/handoff-continue',
258
260
  'handoff-permission': '/api/dsh-auto-memory/handoff-permission',
259
261
  'auto-continue-state': '/api/dsh-auto-memory/auto-continue-state',
@@ -601,6 +603,8 @@ const DEFAULT_CONFIG = {
601
603
  */
602
604
  l0IndexEnabled: true,
603
605
  /** pre-step 软注入。 */
606
+ /** ★v3.1.3 审计标注:本键当前**无消费者**(全 lib/ 零读取点、设置页零控件)——
607
+ * 属保留字段(不删,避免历史配置反序列化告警)。若将来启用,请同时补设置页控件与守卫。 */
604
608
  softInjectionEnabled: false,
605
609
  /** M7.5:分支/子代理会话是否也纳入上下文观测(shadow 观测零注入风险;2026-08-26 裁定默认开:开源模型为主,思维链/分支是主要观测面)。 */
606
610
  contextBridgeObserveChildSessions: true,
@@ -2869,6 +2873,39 @@ class MemoryEngine {
2869
2873
  * (见 buildContinueCarry 头注释: 关时既不写、也不读) ⇒ 它管写入/取材; **看板是渲染**, 不该被它拦。
2870
2874
  * 保持者: boardMode 门(下方那条) —— 那是渲染形态开关, 且 legacy 须与旧行为逐字节相同(:3673 纪律)。
2871
2875
  */
2876
+ /**
2877
+ * ★v3.1.2:单卡全文(看板「展开全文」按需取)。
2878
+ * 为什么单独开:看板载荷不再内联 full(1178 卡 × ≤6000 字符 ≈ 0.85 MB,且 lanes+matrix 双份),
2879
+ * 前端默认只渲染 preview,全文仅点开时才需要 ⇒ 按 id 单取更省。
2880
+ * 复用与看板同一读盘口径(_collectWhiteboardDocsPre + buildSectionCardsPre),保证 id 可命中。
2881
+ */
2882
+ async kanbanCardBody(sessionId, cardId, opts = {}) {
2883
+ const want = String(cardId || '').trim()
2884
+ if (!want) return { ok: false, error: 'missing-card-id' }
2885
+ if (!this.configLoaded) { try { await this.loadConfig() } catch (e) {} }
2886
+ try {
2887
+ let p, wsBound = true
2888
+ if (sessionId) {
2889
+ p = await this.resolvePathsForSession(sessionId)
2890
+ wsBound = p.wsBound !== false
2891
+ } else {
2892
+ p = (this.state.handoffDir || this.state.planPath)
2893
+ ? { handoffDir: this.state.handoffDir || path.dirname(this.state.planPath), planPath: this.state.planPath, ws: this.state.ws, projectDir: this.state.projectDir }
2894
+ : await this.resolvePaths(undefined)
2895
+ }
2896
+ if (!wsBound) return { ok: false, error: 'unbound-ws' }
2897
+ const projectDir = p.projectDir || path.dirname(p.handoffDir)
2898
+ // 展开全文要能找到**任意**一张卡(含归档),故这里 includeArchive: true。
2899
+ const docs = await this._collectWhiteboardDocsPre(projectDir, { includeArchive: true })
2900
+ const cards = buildSectionCardsPre(this.wbWsKeyPre(projectDir), docs)
2901
+ const hit = cards.find((c) => String(c.id || '') === want)
2902
+ if (!hit) return { ok: false, error: 'card-not-found' }
2903
+ return { ok: true, id: want, full: String(hit.body || hit.preview || '').slice(0, 6000) }
2904
+ } catch (e) {
2905
+ return { ok: false, error: String(e && e.message ? e.message : e) }
2906
+ }
2907
+ }
2908
+
2872
2909
  async kanbanBoardData(sessionId, opts = {}) {
2873
2910
  if (!this.configLoaded) { try { await this.loadConfig() } catch (e) {} }
2874
2911
  if (!(String((this.config || {}).boardMode || '').trim().toLowerCase() === 'graph')) {
@@ -2890,7 +2927,8 @@ class MemoryEngine {
2890
2927
  // v1 用文件级条目 ⇒ 一篇账本压成一根卡(实测 92 文件仅 92 条, 且 sectionOfPre 无 break
2891
2928
  // 导致整篇归一到最后一个 ## 标题, 三条泳道恒空)。改为小节级后实测 92 → 417 条,
2892
2929
  // 目标/进行中/失败与弯路/进度 四条泳道全部有内容。
2893
- const docs = await this._collectWhiteboardDocsPre(projectDir)
2930
+ // ★v3.1.2 性能:看板默认不含 archive(用户拍板 C)——归档只做历史留档,不进看板投影。
2931
+ const docs = await this._collectWhiteboardDocsPre(projectDir, { includeArchive: false })
2894
2932
  const cards = buildSectionCardsPre(this.wbWsKeyPre(projectDir), docs)
2895
2933
  const index = await this._loadSidecarIndexPre(projectDir)
2896
2934
  const kb = buildKanbanPre(index, { now: new Date().toISOString(), cards, perLaneCap: Number(opts.perLaneCap) || 0 })
@@ -2909,7 +2947,10 @@ class MemoryEngine {
2909
2947
  * 读取白板/账本文档清单(看板与 sidecar 重建的**唯一读盘口径**)。
2910
2948
  * 返回 [{relPath, text, title, kind, mtime, ts}] —— 供小节切分与文件级重建共用。
2911
2949
  */
2912
- async _collectWhiteboardDocsPre(projectDir) {
2950
+ // ★v3.1.2 性能(用户拍板 C):归档默认不进**看板**投影。
2951
+ // 实测 handoff/ 活跃账本 157 个 / archive 99 个,每次 /kanban-board 全量读盘+切分;
2952
+ // 归档属于历史留档,看板默认只看 活跃账本 + PLAN(白板/账本本身仍全量可读,不受影响)。
2953
+ async _collectWhiteboardDocsPre(projectDir, opts = {}) {
2913
2954
  const sideDir = path.join(projectDir, 'handoff')
2914
2955
  const docs = []
2915
2956
  const planText = await this.readTextSafe(path.join(sideDir, 'PLAN.md'))
@@ -2924,7 +2965,7 @@ class MemoryEngine {
2924
2965
  kind: 'ledger', mtime: st ? Number(st.mtimeMs) : 0,
2925
2966
  })
2926
2967
  }
2927
- const archNames = await readdir(path.join(sideDir, 'archive')).catch(() => [])
2968
+ const archNames = (opts && opts.includeArchive) ? await readdir(path.join(sideDir, 'archive')).catch(() => []) : []
2928
2969
  for (const f of archNames.filter((x) => /\.md$/.test(x)).sort()) {
2929
2970
  const st = await stat(path.join(sideDir, 'archive', f)).catch(() => null)
2930
2971
  docs.push({
@@ -3187,10 +3228,58 @@ class MemoryEngine {
3187
3228
  return rec && Date.now() - (Number(rec.at) || 0) < 600000 ? rec : null
3188
3229
  }
3189
3230
 
3231
+ /**
3232
+ * ★v3.1.3:从**目录列出的文件名**里挑出会话日志文件(纯函数,便于单测)。
3233
+ *
3234
+ * 为什么不再用固定候选表:实盘同时存在 `session.jsonl.zstd` 与 `session.v3.jsonl.zstd` 两代,
3235
+ * 代码注释自述该名已随宿主改过一次、靠人工追加候选名跟上。若宿主改名(如 v4),
3236
+ * 固定表会**返回空**并让五条功能链同时静默降级。这里改成按**结构**识别:
3237
+ * · 前缀 `session.`(或 `session-`)
3238
+ * · 含 `.jsonl`(可带 `.zstd`)
3239
+ * · 排除明显的备份/调试产物(`.broken-backup-` / `.dup-backup-` / 以 `_` 开头)
3240
+ * 排序优先级:**版本号高者优先**(v4 > v3 > 无版本)> 非压缩优先于压缩 > mtime 新者优先。
3241
+ * 这样既兼容未来命名,又不会把备份文件误当主文件。
3242
+ *
3243
+ * @param {string[]} names 目录里的文件名
3244
+ * @param {Record<string, number>} mtimes name → mtimeMs(缺省视为 0)
3245
+ * @returns {string} 选中的文件名;无可选返回 ''
3246
+ */
3247
+ pickSessionFileNamePre(names, mtimes) {
3248
+ const list = Array.isArray(names) ? names : []
3249
+ const mt = mtimes && typeof mtimes === 'object' ? mtimes : {}
3250
+ const cands = []
3251
+ for (const n of list) {
3252
+ if (typeof n !== 'string' || !n) continue
3253
+ if (n.startsWith('_')) continue // _debug_child.jsonl 之类
3254
+ if (/backup|corrupt|\.tmp$|\.dam-tmp$/i.test(n)) continue
3255
+ if (!/^session[.-]/.test(n)) continue // session. / session-
3256
+ if (!/\.jsonl(\.zstd)?$/i.test(n)) continue // 只认 jsonl[.zstd]
3257
+ const vm = n.match(/session\.v(\d+)\./i)
3258
+ cands.push({ name: n, ver: vm ? Number(vm[1]) : 0, zstd: /\.zstd$/i.test(n) ? 1 : 0, m: Number(mt[n]) || 0 })
3259
+ }
3260
+ if (!cands.length) return ''
3261
+ cands.sort((a, b) => (b.ver - a.ver) || (a.zstd - b.zstd) || (b.m - a.m) || a.name.localeCompare(b.name))
3262
+ return cands[0].name
3263
+ }
3264
+
3190
3265
  /** 2.3.1:解析会话日志文件路径 —— DSH 0.1.5 起会话格式迁移到 `session.v3.jsonl.zstd`
3191
3266
  * (旧 `session.jsonl.zstd` 保留但停止更新);按 mtime 取最新存在的文件,兼容两代与未来命名。 */
3192
3267
  async resolveSessionFile(dir) {
3193
3268
  try {
3269
+ // ★v3.1.3:先扫目录(兼容任意 session.vN / 未来命名),目录读不到才回退固定表。
3270
+ let names = []
3271
+ try { names = await readdir(dir) } catch (_) { names = [] }
3272
+ let picked = ''
3273
+ if (names.length) {
3274
+ const mtimes = {}
3275
+ for (const n of names) {
3276
+ if (!/^session[.-].*\.jsonl(\.zstd)?$/i.test(n)) continue
3277
+ try { const st = await stat(path.join(dir, n)); if (st.isFile()) mtimes[n] = st.mtimeMs } catch (_) {}
3278
+ }
3279
+ picked = this.pickSessionFileNamePre(Object.keys(mtimes), mtimes)
3280
+ }
3281
+ if (picked) return path.join(dir, picked)
3282
+ // 回退(目录不可读时):旧的固定候选表,行为与历史一致
3194
3283
  const cands = ['session.v3.jsonl.zstd', 'session.jsonl.zstd', 'session.v3.jsonl', 'session.jsonl']
3195
3284
  let best = null
3196
3285
  for (const name of cands) {
@@ -3969,9 +4058,18 @@ class MemoryEngine {
3969
4058
  for (const ent of entries) {
3970
4059
  if (!ent.isDirectory()) continue
3971
4060
  if (!/^session-/.test(ent.name)) continue
4061
+ // ★v3.1.3:遍历该会话目录里**全部** session.* 文件(不再固定表),取最大 mtime。
3972
4062
  let ms = 0
3973
- for (const f of ['session.v3.jsonl.zstd', 'session.jsonl.zstd']) {
3974
- try { const st = statSync(path.join(wsPath, ent.name, f)); if (st.mtimeMs > ms) ms = st.mtimeMs } catch (_) {}
4063
+ try {
4064
+ for (const f of readdirSync(path.join(wsPath, ent.name))) {
4065
+ if (!/^session[.-].*\.jsonl(\.zstd)?$/i.test(f)) continue
4066
+ if (/backup|corrupt/i.test(f)) continue
4067
+ try { const st = statSync(path.join(wsPath, ent.name, f)); if (st.mtimeMs > ms) ms = st.mtimeMs } catch (_) {}
4068
+ }
4069
+ } catch (_) {
4070
+ for (const f of ['session.v3.jsonl.zstd', 'session.jsonl.zstd']) {
4071
+ try { const st = statSync(path.join(wsPath, ent.name, f)); if (st.mtimeMs > ms) ms = st.mtimeMs } catch (_) {}
4072
+ }
3975
4073
  }
3976
4074
  if (ms > bestMs) { bestMs = ms; best = ent.name }
3977
4075
  }
@@ -4679,7 +4777,10 @@ class MemoryEngine {
4679
4777
  /** 对比本地版本与 npm registry 最新版;结果缓存 12 小时(启动/设置页打开都走缓存,不重复查网)。 */
4680
4778
  async checkUpdate(force) {
4681
4779
  const cacheFile = path.join(dshHome(), 'memory', 'update-check.json')
4682
- const base = { current: '', installKind: 'unknown' }
4780
+ // ★v3.1.2:dev-link(profile 以 link: 挂载开发树)时,current 读的是**开发树自己的版本**,
4781
+ // 与 registry latest 恒等 ⇒ upToDate 永远 true,用户感知不到线上新版。故把开发树版本
4782
+ // 单独标成 devVersion,让前端能呈现「开发树 vX ↔ 线上 vY」而不是假装已是最新。
4783
+ const base = { current: '', installKind: 'unknown', devVersion: '' }
4683
4784
  try {
4684
4785
  const indexPath = fileURLToPath(import.meta.url)
4685
4786
  const pkgPath = path.join(path.dirname(indexPath), '..', 'package.json')
@@ -4688,6 +4789,7 @@ class MemoryEngine {
4688
4789
  } catch (e) {}
4689
4790
  const prof = await this.findProfileDir()
4690
4791
  if (prof) base.installKind = prof.installKind
4792
+ if (base.installKind === 'dev-link') base.devVersion = base.current
4691
4793
  if (!force) {
4692
4794
  try {
4693
4795
  const cached = await this.readTextSafe(cacheFile)
@@ -7006,11 +7108,19 @@ _wsSummaryFile() { return path.join(dshHome(), 'memory', 'workspaces-summary.jso
7006
7108
  }
7007
7109
  }
7008
7110
  await walk(dir, '')
7111
+ // ★v3.1.3:用户级文件(日历)也进包 —— 用户要求「把同步的范围扩大到日历」。
7112
+ // CALENDAR.md 在 userDir(跨工作区共享),故键用「相对 userDir 的路径」;
7113
+ // 导入侧按 merge 处理(见 _applyUserFilesPre / calendarMergePre),绝不整篇覆盖。
7114
+ const userFiles = {}
7115
+ try {
7116
+ const calRaw = await this.readTextSafe(path.join(this.userDirOf(), 'CALENDAR.md'))
7117
+ if (calRaw && String(calRaw).trim()) userFiles['CALENDAR.md'] = calRaw
7118
+ } catch (_) {}
7009
7119
  const built = buildPackPre({
7010
- ws, files,
7120
+ ws, files, userFiles,
7011
7121
  summaryRecord: await this._wsSummaryRecord(ws),
7012
7122
  pluginVersion: (this.manifest && this.manifest.version) || '',
7013
- sourceHost: os.hostname(),
7123
+ sourceHost: hostnameOf(),
7014
7124
  })
7015
7125
  if (!built.ok) return { ok: false, error: built.error, skipped }
7016
7126
  const json = JSON.stringify(built.pack)
@@ -7031,6 +7141,32 @@ _wsSummaryFile() { return path.join(dshHome(), 'memory', 'workspaces-summary.jso
7031
7141
  }
7032
7142
  }
7033
7143
 
7144
+ /**
7145
+ * ★v3.1.3:把包内的用户级文件落地(当前只有 CALENDAR.md)。**合并而非覆盖**。
7146
+ * 为什么单独一个方法:日历在用户级目录、跨工作区共享,落盘口径与 files 那支完全不同。
7147
+ * 返回 { applied:[], skipped:[] } 供调用方汇总 —— 绝不静默。
7148
+ */
7149
+ async _applyUserFilesPre(pack) {
7150
+ const applied = []
7151
+ const skipped = []
7152
+ const uf = (pack && pack.userFiles && typeof pack.userFiles === 'object') ? pack.userFiles : {}
7153
+ for (const rel of Object.keys(uf)) {
7154
+ if (rel !== 'CALENDAR.md') { skipped.push('user:' + rel + '(unsupported)'); continue }
7155
+ try {
7156
+ const target = path.join(this.userDirOf(), rel)
7157
+ const existing = await this.readTextSafe(target)
7158
+ const merged = calendarMergePre(existing, uf[rel])
7159
+ if (!merged.ok) { skipped.push('user:' + rel + '(merge-failed)'); continue }
7160
+ // 无新增且本机已有内容 ⇒ 不写盘(避免无谓 mtime 变动)
7161
+ if (merged.added === 0 && String(existing || '').trim()) { applied.push({ rel, added: 0, action: 'noop' }); continue }
7162
+ await this.writeFullRaw(target, merged.text)
7163
+ this.state.calendarText = merged.text; this.state.loadedAt = Date.now()
7164
+ applied.push({ rel, added: merged.added, action: 'merged' })
7165
+ } catch (e) { skipped.push('user:' + rel + '(error:' + String(e && e.message ? e.message : e) + ')') }
7166
+ }
7167
+ return { applied, skipped }
7168
+ }
7169
+
7034
7170
  /** 读包(自动识别 gzip:gzip 魔数 1f 8b)。★坏包在 validate 阶段即拒,不写任何文件。 */
7035
7171
  async _readPack(packPath) {
7036
7172
  const buf = await readFile(packPath).catch(() => null)
@@ -7071,7 +7207,7 @@ _wsSummaryFile() { return path.join(dshHome(), 'memory', 'workspaces-summary.jso
7071
7207
  }
7072
7208
  }
7073
7209
 
7074
- /** 导入:执行。★顺序 = 重算计划 → 备份 → 落盘 → 合并 summary(任一步失败即中止并回报)。 */
7210
+ /** 导入:执行。★顺序 = 重算计划 → 备份 → 落盘 → 合并 summary → 合并用户级文件(任一步失败即中止并回报)。 */
7075
7211
  async migrateImport(opts) {
7076
7212
  const o = opts || {}
7077
7213
  const r = await this._readPack(String(o.packPath || ''))
@@ -7121,6 +7257,12 @@ _wsSummaryFile() { return path.join(dshHome(), 'memory', 'workspaces-summary.jso
7121
7257
  }
7122
7258
  summaryAction = merged.action
7123
7259
  } catch (e) { summaryError = String(e && e.message || e) }
7260
+ // ★v3.1.3:用户级文件(日历)落地 —— **merge 不覆盖**,与 summary 同一条纪律。
7261
+ // 放在 summary 之后、refresh 之前:日历改动要进 state.calendarText(_applyUserFilesPre 内已同步),
7262
+ // 失败不阻断导入主流程(计入 userFilesError,绝不静默)。
7263
+ let userFilesResult = { applied: [], skipped: [] }
7264
+ let userFilesError = null
7265
+ try { userFilesResult = await this._applyUserFilesPre(r.pack) } catch (e) { userFilesError = String(e && e.message || e) }
7124
7266
  // 索引失效:语义索引按工作区缓存,路径/内容变了必须让它重建(S5:索引本身不随包走)
7125
7267
  try { this._semanticIndexStaleByWorkspace = this._semanticIndexStaleByWorkspace || new Set(); this._semanticIndexStaleByWorkspace.add(targetWs) } catch (_) {}
7126
7268
  try { if (this.state) this.state.loadedAt = 0; await this.refresh(undefined) } catch (_) {}
@@ -7131,6 +7273,7 @@ _wsSummaryFile() { return path.join(dshHome(), 'memory', 'workspaces-summary.jso
7131
7273
  rewrite: { totalHits: plan.rewrite.totalHits, fileCount: plan.rewrite.fileCount, files: plan.rewrite.files.slice(0, 50) },
7132
7274
  additions: plan.additions.length, overwrites: plan.overwrites.length,
7133
7275
  summaryAction, summaryError,
7276
+ userFiles: userFilesResult.applied, userFilesSkipped: userFilesResult.skipped, userFilesError,
7134
7277
  }
7135
7278
  }
7136
7279
  /** 工作区总览:每个工作区一个小总结 + 区内细分总结;结果缓存到用户级 ~/.dsh/memory/workspaces-summary.json(跨工作区全局)。 */
@@ -7565,6 +7708,44 @@ _wsSummaryFile() { return path.join(dshHome(), 'memory', 'workspaces-summary.jso
7565
7708
  return '已保存反思 ' + file
7566
7709
  }
7567
7710
 
7711
+ /**
7712
+ * ★v3.1.3:**同步目录(云盘/远端)探测** —— 用户要求「增加一个在云盘或者远端同步的接口,
7713
+ * 尝试看看能不能做好兼容性」。
7714
+ *
7715
+ * 定位:**不发明私有同步协议、不做后台自动同步**(延续用户既有拍板)。本方法只做两件事:
7716
+ * ① 判断这个目录现在能不能用(存在 / 可写);
7717
+ * ② 给出人话诊断,帮用户把「导出目录指向云盘」这一步走对。
7718
+ *
7719
+ * 兼容性做法:**只认为它是个普通目录** —— OneDrive / 坚果云 / Dropbox / iCloud / 网络盘 / U 盘 /
7720
+ * 本地路径一律同等对待:不嗅探客户端、不依赖专有 API、不假设路径形态、不要求特定盘符。
7721
+ *
7722
+ * @param {string} dir 待检测目录(空 ⇒ 用配置里的 syncDir)
7723
+ * @returns {{ok:boolean, dir:string, exists:boolean, writable:boolean, looksSynced:boolean, hint:string}}
7724
+ */
7725
+ async syncDirProbe(dir) {
7726
+ if (!this.configLoaded) { try { await this.loadConfig() } catch (e) {} }
7727
+ const want = String(dir || (this.config && this.config.syncDir) || '').trim()
7728
+ if (!want) {
7729
+ return { ok: false, dir: '', exists: false, writable: false, looksSynced: false,
7730
+ hint: '未配置同步目录。填写你的云盘本地目录(如 OneDrive/坚果云 的同步文件夹)即可把导出包自动带过去;也可留空走默认导出目录。' }
7731
+ }
7732
+ const expanded = this.expandUserPath(want) || want
7733
+ let exists = false, writable = false
7734
+ try { const st = await stat(expanded); exists = !!st.isDirectory() } catch (_) { exists = false }
7735
+ if (exists) {
7736
+ const probe = path.join(expanded, '.dam-sync-probe-' + Date.now())
7737
+ try { await writeFile(probe, 'ok'); await unlink(probe); writable = true } catch (_) { writable = false }
7738
+ }
7739
+ // 「像同步盘」只是**提示**,不作门槛:命中常见客户端名就给一句更贴心的说明。
7740
+ const looksSynced = /onedrive|dropbox|nutstore|坚果云|icloud|googledrive|google drive|baidunetdisk|百度网盘/i.test(expanded)
7741
+ let hint
7742
+ if (!exists) hint = '该目录不存在。请确认路径拼写;若是网络盘,先在文件管理器里连上再试。'
7743
+ else if (!writable) hint = '目录存在但**不可写**。若是云盘,可能还在首次同步或被占用;换个位置或稍后重试。'
7744
+ else if (looksSynced) hint = '目录可用,且看起来是同步盘。导出到这里的包会被云盘自动同步到另一台机器;到那边用「导入包」显式导入即可(靠包内校验和对齐,不做后台自动同步)。'
7745
+ else hint = '目录可用。若不是同步盘,这个包不会自动到另一台机器——请手动拷贝(网盘上传/邮件/U 盘均可)。'
7746
+ return { ok: exists && writable, dir: expanded, exists, writable, looksSynced, hint }
7747
+ }
7748
+
7568
7749
  // ---------- 日历/日程(用户级 CALENDAR.md) ----------
7569
7750
  /** 解析 CALENDAR.md 为条目数组。 */
7570
7751
  parseCalendar(text) {
@@ -10592,6 +10773,14 @@ engine._recallStats = createRecallStatsPre({
10592
10773
  try {
10593
10774
  const agent = context && context.agent
10594
10775
  if (!agent) return ''
10776
+ // ★v3.1.3(用户拍板「接线」):`injectEnabled` 此前是**死开关** —— 键白名单内有、
10777
+ // UI 能翻面能落盘,但全 lib/ 零读取点 ⇒ 用户关掉后记忆照旧每轮注入(界面骗人)。
10778
+ // 现在接入本回调最前面,作为**动态快照总闸**:关 = 真不注入。
10779
+ // ⚠️解耦(用户硬性要求「单一开关不得顺带改变其他功能」):只关这**一条**通路,不动
10780
+ // · 静态规则段 `renderMemoryStatic()`(systemPrompt.section,下方 :~10900,缓存锚 + 属另一功能)
10781
+ // · M6 tail surface(下方 :~10910)
10782
+ // · 记忆写入(memory_log / 自动沉淀)与检索(memory_recall)—— 关注入 ≠ 关记忆
10783
+ if (engine.config && engine.config.injectEnabled === false) return ''
10595
10784
  // M0/M1: 绑定到该 agent 的 runtime 再读取(state 是 per-session getter,避免落到 default runtime 读到空)
10596
10785
  return engine.withAgent(agent, () => {
10597
10786
  const st = engine.stateFor(agent)
@@ -11270,6 +11459,18 @@ engine._recallStats = createRecallStatsPre({
11270
11459
  } catch (e) { return writeJson(res, 500, { error: String(e && e.message ? e.message : e) }) }
11271
11460
  },
11272
11461
  },
11462
+ {
11463
+ kind: 'exact',
11464
+ path: API['kanban-card'],
11465
+ handler: async (req, res) => {
11466
+ // ★v3.1.2:单卡全文(GET 只读;loopback)。看板载荷不再内联 full,展开时按 id 单取。
11467
+ if (!isLoopbackRequest(req)) return writeJson(res, 403, { error: 'forbidden: loopback-only' })
11468
+ try {
11469
+ const url = new URL(req.url, 'http://127.0.0.1')
11470
+ return writeJson(res, 200, await engine.kanbanCardBody(url.searchParams.get('sessionId'), url.searchParams.get('id')))
11471
+ } catch (e) { return writeJson(res, 500, { error: String(e && e.message ? e.message : e) }) }
11472
+ },
11473
+ },
11273
11474
  {
11274
11475
  kind: 'exact',
11275
11476
  path: API['kanban-board'],
@@ -11832,7 +12033,12 @@ engine._recallStats = createRecallStatsPre({
11832
12033
  if ((req.method || 'POST') !== 'POST') return writeJson(res, 405, { error: 'method not allowed' })
11833
12034
  const body = await readJsonBody(req).catch(() => ({}))
11834
12035
  try {
11835
- const ws = String((body && body.ws) || (engine._lastAgent && engine._lastAgent.cwd) || '')
12036
+ // ★v3.1.2 修:此前回退段用的是 `_lastAgent.cwd`,而该字段不存在 ——
12037
+ // 全库权威口径是 session.header.cwd(见 :2324 / :5781 / :10628),本文件 :2799 注释亦明写
12038
+ // 「不再用不存在的 agent.cwd」⇒ 回退段恒空,前端不传 ws 即 missing-ws(用户实测)。
12039
+ const _la = engine._lastAgent
12040
+ const _lacwd = (_la && _la.session && _la.session.header && _la.session.header.cwd) || ''
12041
+ const ws = String((body && body.ws) || _lacwd || '')
11836
12042
  writeJson(res, 200, await engine.migrateExport({ ws, outPath: body && body.outPath, compress: !(body && body.compress === false) }))
11837
12043
  } catch (e) { writeJson(res, 500, { error: String(e && e.message ? e.message : e) }) }
11838
12044
  },
@@ -11922,8 +12128,22 @@ engine._recallStats = createRecallStatsPre({
11922
12128
  handler: async (req, res) => {
11923
12129
  if (!isLoopbackRequest(req)) return writeJson(res, 403, { error: 'forbidden: loopback-only' })
11924
12130
  if ((req.method || 'POST') !== 'POST') return writeJson(res, 405, { error: 'method not allowed' })
11925
- // 调用系统的文件夹选择器(native 后端,由 dsh 的 directory-picker-auto 按主机情况挂载);不可用则返回 native:false,前端回退内嵌浏览
12131
+ // 调用系统的文件夹选择器。★v3.1.2 修:此前只试 `ctx.directoryPicker.capability()` 一种形态,
12132
+ // 与 DSH 内核标准契约不符 ⇒ 恒返回 native:false(用户实测「选择器不可用,请手动填写路径」)。
12133
+ // 内核形态(@linxin666/dsh-web-all / dsh-client-ui-skin-center 两处同源):
12134
+ // const r = await ctx.remote.directoryPicker.pick(); if (!r.ok) throw ...; return r.value
12135
+ // 这里做**双路径兼容**:内核契约优先 → 旧 capability 形态回退 → 都不可用才 native:false。
11926
12136
  try {
12137
+ // 路径①:内核标准契约(远程/桌面形态下挂在 ctx.remote 上)
12138
+ let remotePicker = undefined
12139
+ try { remotePicker = ctx.remote && ctx.remote.directoryPicker } catch (e) {}
12140
+ if (remotePicker && typeof remotePicker.pick === 'function') {
12141
+ const r = await remotePicker.pick()
12142
+ if (r && r.ok === false) return writeJson(res, 200, { native: true, dir: null, error: String((r.error && r.error.message) || r.error || 'picker failed') })
12143
+ const v = r && (r.value !== undefined ? r.value : r)
12144
+ return writeJson(res, 200, { native: true, dir: (typeof v === 'string' ? v : (v && v.path) || null) })
12145
+ }
12146
+ // 路径②:本机既有形态(native 后端由 directory-picker-auto 按主机情况挂载)
11927
12147
  let picker = undefined
11928
12148
  try { picker = ctx.directoryPicker } catch (e) {}
11929
12149
  if (!picker || typeof picker.capability !== 'function') return writeJson(res, 200, { native: false })
@@ -11985,12 +12205,40 @@ engine._recallStats = createRecallStatsPre({
11985
12205
  if (!prof) return writeJson(res, 200, { ok: false, message: '未找到 dsh profile 目录(未安装插件或已删除)。' })
11986
12206
  if (prof.installKind === 'dev-link') return writeJson(res, 200, { ok: false, message: '当前为本地开发链接(link:)安装,不适用 npm 更新,请直接同步开发源码。' })
11987
12207
  if (prof.installKind !== 'registry') return writeJson(res, 200, { ok: false, message: '未检测到 registry 安装方式,无法自动更新。' })
11988
- const cmd = prof.usesPnpm ? 'pnpm up @a9i5k4/dsh-auto-memory' : 'npm install @a9i5k4/dsh-auto-memory@latest'
12208
+ // ★v3.1.2 修:pnpm 11 默认开启「1 天 Minimum Release Age」,对发布不足 24h 的版本**静默跳过**
12209
+ // (3.1.1 当天发布即中招)⇒ 旧命令 exit 0 但什么都没装,路由还报「成功」。
12210
+ // 改为:钉死目标版本(latest 也解析成具体版本号)+ 显式关闸门 + 装后复核(见下)。
12211
+ const target = String((await engine.checkUpdate(true)).latest || '').trim()
12212
+ const spec = '@a9i5k4/dsh-auto-memory' + (target ? '@' + target : '@latest')
12213
+ const cmd = prof.usesPnpm
12214
+ ? 'pnpm add ' + spec + ' --config.minimumReleaseAge=0'
12215
+ : 'npm install ' + spec + ' --min-release-age=0'
11989
12216
  const out = await execP(cmd, { cwd: prof.dir, timeout: 120000, windowsHide: true })
11990
12217
  const tail = ((out.stdout || '') + (out.stderr || '')).trim().slice(-1200)
12218
+ // ★v3.1.2:装后复核 —— 旧实现无条件报成功,供应链闸门静默跳过时用户被谎报「安装成功」。
12219
+ // 读 profile 里该包的真实 version;与目标不符即 ok:false(并把命令输出带回去供排查)。
12220
+ let landed = ''
12221
+ try {
12222
+ const pj = await this.readTextSafe(path.join(prof.dir, 'node_modules', '@a9i5k4', 'dsh-auto-memory', 'package.json'))
12223
+ if (pj) { try { landed = String(JSON.parse(pj).version || '') } catch (_) {} }
12224
+ } catch (_) {}
12225
+ const want = target || landed
12226
+ if (want && landed !== want) {
12227
+ return writeJson(res, 200, {
12228
+ ok: false,
12229
+ message: (landed ? ('安装后版本仍为 ' + landed + ',目标 ' + want) : '未检测到已安装的包版本')
12230
+ + '(若为 pnpm 11 供应链闸门所致,命令已带 --config.minimumReleaseAge=0,请把下方输出反馈给作者)',
12231
+ output: tail || '(无输出)',
12232
+ landedVersion: landed,
12233
+ targetVersion: want,
12234
+ installKind: prof.installKind,
12235
+ })
12236
+ }
11991
12237
  writeJson(res, 200, {
11992
12238
  ok: true,
11993
12239
  output: tail || '(无输出)',
12240
+ landedVersion: landed,
12241
+ targetVersion: want,
11994
12242
  restartHint: '更新完成,请重启 dsh web 生效。',
11995
12243
  installKind: prof.installKind,
11996
12244
  })
@@ -151,6 +151,17 @@ export function buildPackPre(opt = {}) {
151
151
  if (bytes > MIGRATE_PACK_MAX_BYTES_PRE) return { ok: false, error: 'too-large:' + bytes, warnings }
152
152
  // 体积提示(不阻断):超过 16 MB 时提醒用户包会很大
153
153
  if (bytes > 16 * 1024 * 1024) warnings.push('pack-large:' + Math.round(bytes / 1048576) + 'MB')
154
+ // ★v3.1.3:用户级文件(如 `CALENDAR.md`)—— 与 `files` 的语义区别:
155
+ // files 相对「工作区 slug 目录」,userFiles 相对「用户级 memory 目录」,且**跨工作区共享**。
156
+ // 老包无此字段 ⇒ 视为 {},行为逐字节不变(向后兼容)。
157
+ const userFiles = (opt.userFiles && typeof opt.userFiles === 'object') ? opt.userFiles : {}
158
+ let userBytes = 0
159
+ for (const n of Object.keys(userFiles)) {
160
+ if (n.includes('..') || n.startsWith('/') || n.startsWith('\\')) {
161
+ return { ok: false, error: 'unsafe-user-relative-path:' + n, warnings }
162
+ }
163
+ userBytes += Buffer.byteLength(String(userFiles[n]), 'utf8')
164
+ }
154
165
  const pack = {
155
166
  format: MIGRATE_PACK_FORMAT_PRE,
156
167
  createdAt: typeof opt.now === 'number' ? opt.now : Date.now(),
@@ -158,7 +169,8 @@ export function buildPackPre(opt = {}) {
158
169
  runtime: { pluginVersion: String(opt.pluginVersion || '') },
159
170
  summaryRecord: opt.summaryRecord && typeof opt.summaryRecord === 'object' ? opt.summaryRecord : null,
160
171
  files,
161
- stats: { fileCount: names.length, bytes },
172
+ userFiles,
173
+ stats: { fileCount: names.length, bytes, userFileCount: Object.keys(userFiles).length, userBytes },
162
174
  checksum: { algo: MIGRATE_PACK_CHECKSUM_ALGO_PRE, value: packChecksumPre(files) },
163
175
  }
164
176
  return { ok: true, pack, warnings }
@@ -183,6 +195,13 @@ export function validatePackPre(pack) {
183
195
  if (k.includes('..') || k.startsWith('/') || k.startsWith('\\')) { errors.push('unsafe-relative-path:' + k); break }
184
196
  }
185
197
  }
198
+ // ★v3.1.3:userFiles 与 files 受**同等**安全检查(路径穿越);老包无该字段直接通过。
199
+ if (!errors.length && pack.userFiles && typeof pack.userFiles === 'object') {
200
+ for (const k of Object.keys(pack.userFiles)) {
201
+ if (k.includes('..') || k.startsWith('/') || k.startsWith('\\')) { errors.push('unsafe-user-relative-path:' + k); break }
202
+ if (typeof pack.userFiles[k] !== 'string') { errors.push('non-text-user-file:' + k); break }
203
+ }
204
+ }
186
205
  if (!errors.length) {
187
206
  const want = pack.checksum && pack.checksum.value
188
207
  const got = packChecksumPre(pack.files)
@@ -331,6 +350,67 @@ export function mergeSummaryRecordPre(summary, record, targetWs) {
331
350
  }
332
351
 
333
352
  /** 去重(保持顺序,字面量比对)。 */
353
+ /**
354
+ * ★v3.1.3:日历条目**合并**(纯函数,不覆盖)。
355
+ *
356
+ * 为什么不是整篇覆盖:CALENDAR.md 在 `~/.dsh/memory/CALENDAR.md` —— **用户级、跨工作区共享**,
357
+ * 且两台机器都可能往里写过。整篇覆盖会丢掉本机独有的条目
358
+ * (与 `workspaces-summary.json` 必须 merge 是同一条纪律)。
359
+ *
360
+ * 判据:以 `date | time | title` 三元组为身份。
361
+ * - 身份冲突 ⇒ **保留本机已有的**(与导入冲突默认 keep 一致;不覆盖用户已改过的状态)。
362
+ * - 本机没有的 ⇒ 追加进来。
363
+ * - 输出按 日期 → 时间 排序,保证确定性(同输入必得同输出,便于断言)。
364
+ *
365
+ * @param {string} localText 本机 CALENDAR.md 原文(空 ⇒ 视为空日历)
366
+ * @param {string} incomingText 包内 CALENDAR.md 原文
367
+ * @returns {{ok:boolean, text:string, added:number, kept:number}}
368
+ */
369
+ export function calendarMergePre(localText, incomingText) {
370
+ const parse = (t) => {
371
+ const out = []
372
+ let curDate = ''
373
+ for (const raw of String(t || '').split(/\r?\n/)) {
374
+ const line = raw.trim()
375
+ if (!line) continue
376
+ const dm = line.match(/^## (\d{4}-\d{2}-\d{2})/)
377
+ if (dm) { curDate = dm[1]; continue }
378
+ const m = line.match(/^- \[([ xX])\] (\d{1,2}:\d{2}) \| ([^|]+?) \| (.+?)(?: \| (.*))?$/)
379
+ if (m) out.push({ date: curDate, done: m[1] !== ' ', time: m[2], quadrant: m[3].trim(), title: m[4].trim(), note: (m[5] || '').trim() })
380
+ }
381
+ return out
382
+ }
383
+ const keyOf = (en) => en.date + '|' + en.time + '|' + en.title
384
+ const local = parse(localText).filter((en) => en.date)
385
+ const incoming = parse(incomingText).filter((en) => en.date)
386
+ const seen = new Set(local.map(keyOf))
387
+ const merged = local.slice()
388
+ let added = 0
389
+ for (const en of incoming) {
390
+ const k = keyOf(en)
391
+ if (seen.has(k)) continue
392
+ seen.add(k)
393
+ merged.push(en)
394
+ added++
395
+ }
396
+ const byDate = new Map()
397
+ for (const en of merged) {
398
+ if (!byDate.has(en.date)) byDate.set(en.date, [])
399
+ byDate.get(en.date).push(en)
400
+ }
401
+ const lines = ['# 日历与日程 (CALENDAR)', '', '> 由 dsh-auto-memory 维护;AI 可从对话中提取 deadline/约定写入,用户也可在 GUI 操作。', '']
402
+ for (const date of Array.from(byDate.keys()).sort()) {
403
+ lines.push('## ' + date)
404
+ for (const en of byDate.get(date).sort((a, b) => String(a.time || '').localeCompare(String(b.time || '')))) {
405
+ const mark = en.done ? 'x' : ' '
406
+ const note = en.note ? ' | ' + en.note : ''
407
+ lines.push('- [' + mark + '] ' + (en.time || '--:--') + ' | ' + (en.quadrant || '未分类') + ' | ' + en.title + note)
408
+ }
409
+ lines.push('')
410
+ }
411
+ return { ok: true, text: lines.join('\n'), added, kept: local.length }
412
+ }
413
+
334
414
  export function dedupeStringsPre(arr) {
335
415
  const seen = new Set()
336
416
  const out = []
@@ -201,7 +201,11 @@ export async function scanPluginSubagentSessions(opts = {}) {
201
201
  // 按 mtime 取最新存在的文件,兼容两代命名。
202
202
  let file = ''
203
203
  let st = null
204
- for (const name of ['session.v3.jsonl.zstd', 'session.jsonl.zstd']) {
204
+ // ★v3.1.3:不再固定候选名 —— 扫目录取任意 session.*\.jsonl[.zstd](兼容未来 vN 命名)。
205
+ let names = []
206
+ try { names = readdirSync(dir) } catch (_) { names = [] }
207
+ const hits = names.filter((n) => /^session[.-].*\.jsonl(\.zstd)?$/i.test(n) && !/backup|corrupt/i.test(n))
208
+ for (const name of (hits.length ? hits : ['session.v3.jsonl.zstd', 'session.jsonl.zstd'])) {
205
209
  const cand = path.join(dir, name)
206
210
  try {
207
211
  const s = await stat(cand)
package/lib/wb-sidecar.js CHANGED
@@ -668,7 +668,10 @@ export function buildKanbanPre(index, opts = {}) {
668
668
  source: String(c.source || ''),
669
669
  section: String(c.section || ''),
670
670
  preview: body.replace(/\s+/g, ' ').trim().slice(0, 400),
671
- full: body.slice(0, 6000), // P12: 卡上可展开看全文
671
+ // ★v3.1.2 性能:full 不再内联进看板载荷(实测每卡≤6000 字符、1178 卡合计 0.85 MB,
672
+ // 且 lanes+matrix 双份 ⇒ 1.7 MB 纯冗余;前端默认只渲染 preview,full 仅「展开全文」需要)。
673
+ // 改为只带长度标示,正文由 kanban-card 路由按 id 单取。
674
+ fullLen: body.length,
672
675
  tags: t.tags,
673
676
  sectionTags: t.sectionTags,
674
677
  cues: (Array.isArray(c.cues) ? c.cues : []).slice(0, 3),
@@ -698,7 +701,8 @@ export function buildKanbanPre(index, opts = {}) {
698
701
  source: String(e.source || ''),
699
702
  section: String(e.section || ''),
700
703
  preview: String(e.preview || '').slice(0, 400),
701
- full: String(e.preview || '').slice(0, 6000),
704
+ // ★v3.1.2 性能:同 sectionCards 分支 —— 不内联 full,仅带长度(见上)。
705
+ fullLen: String(e.preview || '').length,
702
706
  tags: t.tags,
703
707
  sectionTags: t.sectionTags,
704
708
  cues: (Array.isArray(e.cues) ? e.cues : []).slice(0, 3),
@@ -842,7 +846,8 @@ export function buildKanbanMatrixPre(cards, opts = {}) {
842
846
  docTitle: String(c.docTitle || ''),
843
847
  source: String(c.source || ''),
844
848
  preview: body.replace(/\s+/g, ' ').trim().slice(0, 220),
845
- full: body.slice(0, 6000),
849
+ // ★v3.1.2 性能:矩阵卡同样不内联 full(同 lanes 口径)——带长度标示,点开时按 id 单取。
850
+ fullLen: body.length,
846
851
  bullets: Number(c.bullets) || 0,
847
852
  criteria: String(c.criteria || 'unknown'),
848
853
  badge: criteriaBadgePre(c.criteria),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@a9i5k4/dsh-auto-memory",
3
3
  "description": "Proactive associative memory for DSH: zero-prompt recall injected before the model speaks, three-layer auto-consolidation, skill crystallization, and Astra-style context management - handoff ledgers, PLAN whiteboard, water-level sensing. Local-first, model-agnostic, zero deps. 主动联想记忆+Astra 式上下文管理:自动唤回/自动沉淀/技能固化/交接账本与白板跨窗口续命/水位感知。",
4
- "version": "3.1.1",
4
+ "version": "3.1.3",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {