dsh-session-index-lau 2.4.1 → 2.5.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 CHANGED
@@ -1,13 +1,13 @@
1
- # dsh-session-index-lau
1
+ # dsh-session-index
2
2
 
3
3
  > DSH(DeepSeek Harness)+ WorkBuddy 的**全会话索引与跨会话记忆** MCP 插件。
4
4
  > 拉取仓库 → 双击安装 → 之后全自动。零第三方依赖,源会话文件全程只读。
5
5
 
6
6
  | 项目 | 值 |
7
7
  |---|---|
8
- | 版本 | **v2.4.1** |
8
+ | 版本 | **v2.5.1** |
9
9
  | 运行要求 | Node **≥ 22.15**(需要 `zlib.zstdDecompressSync`) |
10
- | MCP 能力 | **13 个工具** / 4 个提示词 / 8 个资源 |
10
+ | MCP 能力 | **14 个工具** / 4 个提示词 / 8 个资源 |
11
11
  | 第三方依赖 | **0**(只用 Node 标准库) |
12
12
  | 完整手册 | [`操作使用手册.html`](./操作使用手册.html)(单页 14 章,导航 / 搜索 / 一键复制) |
13
13
 
@@ -41,8 +41,8 @@ DSH 的会话存在 `~/.dsh/sessions/` 下,是**上千个独立 zstd 帧拼接
41
41
 
42
42
  | 行 id | 包 | 作用 |
43
43
  |---|---|---|
44
- | `session-index-host` | `dsh-session-index-lau` | 宿主半身(`lib/index.mjs`):用 `import.meta.url` 解析**真实安装目录**,把 `server.mjs` 绝对路径写进 `~/.dsh/session-index/server-path.txt` |
45
- | `mcp-session-index` | `@deepseek-ai/dsh-mcp-client` | MCP stdio 客户端:13 工具 / 4 提示词 / 8 资源 |
44
+ | `session-index-host` | `dsh-session-index` | 宿主半身(`lib/index.mjs`):用 `import.meta.url` 解析**真实安装目录**,把 `server.mjs` 绝对路径写进 `~/.dsh/session-index/server-path.txt` |
45
+ | `mcp-session-index` | `@deepseek-ai/dsh-mcp-client` | MCP stdio 客户端:14 工具 / 4 提示词 / 8 资源 |
46
46
 
47
47
  那行配置里的 `command`、`args`、`DSH_HOME` 三处都是 `!!js` 表达式,**在 DSH 加载配置时求值**(不是安装时),
48
48
  按「显式环境变量 → 宿主记下的路径文件 → profile 标准安装位置」依次探测。
@@ -176,6 +176,7 @@ node install.mjs --doctor
176
176
  | `session_index_refresh` | 手动增量刷新 |
177
177
  | `session_index_package` | 重新生成分发包 |
178
178
  | `session_index_vectors` | 可选的向量索引管理 |
179
+ | `session_index_stage` | 会话整理成 Markdown → 思源桥「待入库」(两步确认,绝不自动写库) |
179
180
 
180
181
  另提供提示词 `session-context` / `resume-topic` / `catch-up` / `quickstart`,
181
182
  资源 `session-index://guide`、`://plugin.json`、`://status`、`://brief`、`://memory` 等。
@@ -186,6 +187,133 @@ node install.mjs --doctor
186
187
 
187
188
  ---
188
189
 
190
+ ## 会话整理 → 思源桥「待入库」
191
+
192
+ `session_index_stage` 把索引里的会话卡、结论、检索片段整理成一篇 Markdown,**投递到思源桥的待入库队列**。
193
+ 它只做「投递」这一步,**绝不调用 `/api/stage/commit`**——写进思源笔记库永远由你在思源桥界面点确认。
194
+
195
+ ### 为什么不用再做一个插件
196
+
197
+ DSH 的插件之间**不共享会话数据**:会话本体只有一份磁盘文件,各插件各自解析、各自维护私有目录。
198
+ 但会话卡与结论已经在 `session-index` 手里,而思源桥是独立的 HTTP 服务(默认 `127.0.0.1:5599`),
199
+ 所以一次 HTTP 调用就能对接,不需要 DSH 内核级的插件通信,更不需要第三个插件重新解一遍 zstd。
200
+
201
+ ### 三步用法
202
+
203
+ ```text
204
+ 1) 只读探测(可选)
205
+ session_index_stage { "status": true }
206
+ → 返回思源桥内核版本、队列篇数、自动接入目录
207
+
208
+ 2) 预览(默认,不产生任何写入)
209
+ session_index_stage { "query": "插件", "template": "brief", "limit": 5 }
210
+ → 返回 title / chars / preview / token / target / confirmWith
211
+
212
+ 3) 用户看过预览、点头后,把 confirmWith 原样回传
213
+ session_index_stage { ...confirmWith }
214
+ → 投递到「待入库」;返回 id 与落点
215
+ ```
216
+
217
+ 第 2 步**永远只是预览**。第 3 步的 `confirmToken` 绑定了「标题 + 落点 + 正文」,
218
+ 参数被改动或索引刚更新导致内容变化时令牌失配,会直接拒绝投递并要求重新预览——不会出现「预览 A、投递 B」。
219
+
220
+ ### 主要参数
221
+
222
+ | 参数 | 默认 | 说明 |
223
+ |---|---|---|
224
+ | `status` | false | 只读探测服务与队列,不做任何写入 |
225
+ | `query` | 空 | 主题词,按主题聚合会话 |
226
+ | `sessionIds` | 空 | 精确指定会话 id(支持前缀),不传则取最近/相关会话 |
227
+ | `app` / `workspace` / `since` / `until` | all / 空 | 范围过滤 |
228
+ | `template` | `brief` | `brief` 速览 / `decisions` 只要结论 / `detail` 附检索片段 |
229
+ | `title` | 自动生成 | 自定义文档标题(确认时回传预览给出的值) |
230
+ | `hpath` / `notebook` | `/会话札记/思源` / `思源桥` | 落点;**写入前仍可在思源桥界面改** |
231
+ | `note` / `limit` / `maxChars` | 自动 / 8 / 12000 | 备注、会话数、篇幅上限 |
232
+ | `confirmToken` | 空 | 预览返回的令牌;带上且匹配才真正投递 |
233
+
234
+ ### 环境变量
235
+
236
+ | 变量 | 默认 | 说明 |
237
+ |---|---|---|
238
+ | `SIYUAN_BRIDGE_PORT` / `SIYUAN_BRIDGE_URL` | `5599` / `http://127.0.0.1:5599` | 服务地址;**只允许回环地址**,不接受任意内网 URL |
239
+ | `SESSION_INDEX_STAGE` | `1` | 设 `0` 彻底禁用该工具 |
240
+ | `SESSION_INDEX_STAGE_TIMEOUT_MS` | `5000` | 单次请求超时(1000–60000) |
241
+ | `SESSION_INDEX_STAGE_HPATH` / `_NOTEBOOK` | `/会话札记/思源` / `思源桥` | 默认落点 |
242
+
243
+ ### 安全边界
244
+
245
+ - 只调用 `/api/status`、`/api/stage`(GET/POST)、`/api/autostage`,**不调用 commit**
246
+ - 投递前按「标题 + 字数」查队列,已有同稿则不重复入队
247
+ - 服务不可达时返回结构化错误(`BRIDGE_UNREACHABLE`),不抛堆栈、不影响其它 13 个工具
248
+ - 文档硬上限 60000 字;Markdown 里明确标注「未写入笔记库」
249
+
250
+ > 实测提醒:思源桥 `/api/autostage` 返回的实际 `session-docs` 目录可能与 `config.json` 里的
251
+ > `dataDir` 不一致(服务走 `SIYUAN_BRIDGE_DATA` 推导)。本工具走 HTTP 接口,不受这个差异影响。
252
+
253
+ ### 推送已有文档(手册 / README)
254
+
255
+ `session_index_stage` 整理的是**索引里的会话**;如果要把仓库里现成的文档送进待入库,
256
+ 用独立脚本 `tools/push-doc.mjs`(不需要重启 DSH、不依赖 MCP 服务):
257
+
258
+ ```bash
259
+ node tools/push-doc.mjs # 只读预览《操作使用手册.html》
260
+ node tools/push-doc.mjs --send # 投递到待入库(绝不写库)
261
+ node tools/push-doc.mjs --file README.md --dry # 推别的文件
262
+ node tools/push-doc.mjs --hpath "/会话札记/运维" --send # 指定落点
263
+ ```
264
+
265
+ **批量:WorkBuddy 所有会话生成的文档**
266
+
267
+ ```bash
268
+ node tools/push-doc.mjs --from-workbuddy # 只列清单(默认,零写入)
269
+ node tools/push-doc.mjs --from-workbuddy --send # 全部投递到待入库
270
+ node tools/push-doc.mjs --from-workbuddy --no-exclude # 连临时/备份目录一起
271
+ node tools/push-doc.mjs --from-workbuddy --include-txt # 连 .txt 一起
272
+ node tools/push-doc.mjs --from-workbuddy --source PresentFiles --limit 10 --send
273
+ ```
274
+
275
+ 数据源是 `~/.workbuddy/artifact-index/<会话id>.json` 里 `contentType: document` 的条目
276
+ (`WORKBUDDY_ARTIFACT_INDEX` 可改)。默认会**排除** `node_modules/dist/.git`、`.verify-*`、
277
+ `DSH-probe`、旧开发副本 `dsh-session-index`、备份与 UI 源码,并按**内容哈希 + 路径**去重,
278
+ 避免同一份 README 被推十几遍。每篇标题带相对路径、备注带来源会话,落点统一可在界面改。
279
+
280
+ 实测(2026-11-14):70 条文档命中 → 排除 26 条临时/依赖 → 文件已不存在 4 →
281
+ 路径或内容重复 18 → **唯一 22 篇全部投递成功**,队列 23 篇(含此前单推的手册),
282
+ 重复执行 22 篇全部被同名去重拦截。可用 `--limit`(默认 60)与 `--since` 收窄。
283
+
284
+ - **默认只预览**,加 `--send` 才入队;预览写到 `dist/push-doc-preview.md`
285
+ - HTML 会先裁掉侧栏导航、复制按钮、脚本、样式与统计卡,再用思源桥自带的
286
+ `/api/paste/html`(只读)转成 Markdown,不会把界面文字塞进笔记
287
+ - 队列里已有同名稿时**默认不重复投递**;手册更新后要再投一份需显式 `--force`
288
+ - 同样**只入队、不 commit**,写入仍由你在思源桥界面确认
289
+
290
+ ### 插件怎么更新(DSH 插件页有没有「更新」按钮)
291
+
292
+ **有,但只对特定安装来源显示**,这是 DSH 插件页的规则(从 DSH 内置文档确认):
293
+
294
+ | 安装来源 | 更新方式 |
295
+ |---|---|
296
+ | 注册表(npm)安装的第三方组合包 | 详情页有「版本」区块与**更新**按钮,可查新版本并升级 |
297
+ | Git 仓库安装的组合包(`update: 'git'`) | 通过「发现」页 / 详情页的**更新到**升级 |
298
+ | 官方按需插件 | 有显式**更新**(版本或开发链接来源不同时) |
299
+ | **本地路径 / `link:` / 压缩包安装的** | **不显示版本区块**,文档写明「通过卸载后重新安装升级」 |
300
+
301
+ 本仓库插件在本机是 **`link:` 挂载**(`node_modules/dsh-session-index` 是指向仓库目录的
302
+ junction),所以插件页**不会有更新按钮**——但也不需要:
303
+
304
+ ```bash
305
+ git -C <仓库目录> pull # 代码就在 junction 指向的目录里,pull 完即更新
306
+ # 然后完全退出 DSH 再打开(MCP 子进程在启动时加载)
307
+ ```
308
+
309
+ 不需要卸载、不需要重新安装、不需要发布 npm 包。若你以后改成从插件页输入
310
+ `github:Lau-Jin/DSH` 安装,才会变成 Git 来源、出现「更新」入口,但那样就不再跟随本地
311
+ 开发目录,且每次更新要走一遍 GitHub 下载(本机实测 80–100 秒)。
312
+
313
+ > 发布给别人用只需要 `git push`;本机生效只需要 `git pull` + 重启 DSH。
314
+
315
+ ---
316
+
189
317
  ## 更新 / 校验 / 回滚
190
318
 
191
319
  ```bash
@@ -202,7 +330,7 @@ node install.mjs --verify # 6 项状态校验
202
330
  node install.mjs --dry-run # 预览改动,不写文件
203
331
  node install.mjs --uninstall # 移除配置块与自动装的 Skill(先备份)
204
332
  node install.mjs --reset-env # 用默认值覆盖 env 自定义
205
- node smoke.mjs # 66 项端到端自测
333
+ node smoke.mjs # 80 项端到端自测
206
334
  ```
207
335
 
208
336
  ---
@@ -213,8 +341,8 @@ node smoke.mjs # 66 项端到端自测
213
341
 
214
342
  | 文件 | 内容 | 用途 |
215
343
  |---|---|---|
216
- | `dist/dsh-session-index-lau-v2.4.1.zip` | 全部源码 + 清单 + 文档 + 手册 + 脚本 + `MANIFEST.json` + `INSTALL.md` | 完整分发,解压后双击 `install.cmd` |
217
- | `dist/session-index-skill-v2.4.1.zip` | 根 `SKILL.md` + 说明 | 直接导入 DSH「MCP 与技能」 |
344
+ | `dist/dsh-session-index-v2.5.1.zip` | 全部源码 + 清单 + 文档 + 手册 + 脚本 + `MANIFEST.json` + `INSTALL.md` | 完整分发,解压后双击 `install.cmd` |
345
+ | `dist/session-index-skill-v2.5.1.zip` | 根 `SKILL.md` + 说明 | 直接导入 DSH「MCP 与技能」 |
218
346
 
219
347
  包内**排除**:索引分片 `*.jsonl.gz`、`meta/cards/notes/vectors.json`、备份与临时文件、已有 zip。
220
348
  已验证包内无任何会话数据与密钥,可安全分发。也可随时 `node pack.mjs` 重打,或调 MCP 工具 `session_index_package`。
@@ -225,7 +353,7 @@ node smoke.mjs # 66 项端到端自测
225
353
 
226
354
  全部通过配置块的 `env` 设置。改哪里取决于安装方式:
227
355
 
228
- - **插件页安装**:改插件包内的 `cordis.patch.yml`(`<profile>/node_modules/dsh-session-index-lau/cordis.patch.yml`),改完重启 DSH;
356
+ - **插件页安装**:改插件包内的 `cordis.patch.yml`(`<profile>/node_modules/dsh-session-index/cordis.patch.yml`),改完重启 DSH;
229
357
  - **clone + 脚本**:改 `~/.dsh/profiles/desktop/cordis.patch.yml` 里 `>>> session-index` 标记块内的 `env`,重跑 `node install.mjs` 会保留你写的值。
230
358
 
231
359
  | 变量 | 默认 | 说明 |
@@ -247,13 +375,13 @@ node smoke.mjs # 66 项端到端自测
247
375
  ## 目录结构
248
376
 
249
377
  ```text
250
- dsh-session-index-lau/ # 仓库根 = 插件根
251
- ├── server.mjs # MCP stdio server(13 工具 / prompts / resources)
378
+ dsh-session-index/ # 仓库根 = 插件根
379
+ ├── server.mjs # MCP stdio server(14 工具 / prompts / resources)
252
380
  ├── cordis.patch.yml # bundle patch:挂宿主行 + MCP 行(含 !!js 自解析路径)
253
381
  ├── build-index.mjs # 索引构建(多帧 zstd → 可检索记录 + 会话卡)
254
382
  ├── install.mjs # 安装 / 校验 / 卸载 / 自动打包
255
383
  ├── pack.mjs # 打包 CLI(零依赖 ZIP writer)
256
- ├── smoke.mjs # 66 项端到端自测
384
+ ├── smoke.mjs # 80 项端到端自测
257
385
  ├── install.cmd / install.sh # 一键安装(双击 / 执行)
258
386
  ├── update.cmd / update.sh # 更新(git pull + 重装)
259
387
  ├── icon.svg # 插件页图标(package.json 的 icon 字段指向它)
@@ -272,8 +400,10 @@ dsh-session-index-lau/ # 仓库根 = 插件根
272
400
  │ ├── notes.mjs # 长期记忆库
273
401
  │ ├── auto.mjs # 自动建索引 / 定时刷新
274
402
  │ ├── watch.mjs # 文件事件监听
275
- │ └── pack.mjs # ZIP writer
403
+ │ ├── pack.mjs # ZIP writer
404
+ │ └── stage.mjs # 会话整理 → 思源桥待入库(渲染 + HTTP 投递)
276
405
  ├── skill/SKILL.md # 自动接入用技能
406
+ ├── tools/push-doc.mjs # 推送任意 md/html 到思源桥待入库(默认只读预览)
277
407
  └── dist/ # 自动生成的分发包(git 忽略)
278
408
  ```
279
409
 
@@ -292,7 +422,7 @@ dsh-session-index-lau/ # 仓库根 = 插件根
292
422
 
293
423
  ## 卸载
294
424
 
295
- - **插件页安装的**:插件页里禁用或卸载 `dsh-session-index-lau`,重启 DSH 即可(MCP 行随 bundle 一起消失)。
425
+ - **插件页安装的**:插件页里禁用或卸载 `dsh-session-index`,重启 DSH 即可(MCP 行随 bundle 一起消失)。
296
426
  - **clone + 脚本安装的**:`node install.mjs --uninstall`,移除配置块与自动安装的 Skill(先备份)。
297
427
 
298
428
  两种情况下索引目录 `~/.dsh/session-index/` 都保留(不影响原始会话),需要时手动删除。
@@ -303,6 +433,8 @@ dsh-session-index-lau/ # 仓库根 = 插件根
303
433
 
304
434
  | 版本 | 变更 |
305
435
  |---|---|
436
+ | **v2.5.1** | `tools/push-doc.mjs` 增加 **`--from-workbuddy` 批量模式**:扫描 WorkBuddy 全部会话产出的文档,按内容哈希去重、排除临时/依赖目录、逐篇查重后一次性投递到思源桥待入库;默认只读预览,`--send` 才入队,`--no-exclude` / `--include-txt` / `--source` / `--limit` / `--since` / `--force` 可控。修掉按 `chars` 判重不稳的问题(思源桥会规整正文)。README 补「插件怎么更新」章节。 |
437
+ | **v2.5.0** | 新增第 14 个工具 `session_index_stage`:**会话整理文档 → 思源桥「待入库」队列**。两步确认(预览返回令牌,原样回传 `confirmWith` 才投递)、按标题+字数去重、支持主题/指定会话/三种模板/落点配置,**绝不自动写入思源笔记库**。读取状态与队列可传 `status: true`。 |
306
438
  | **v2.4.1** | 新增 `install.mjs --doctor` **环境体检**(7 项,只读),覆盖插件页安装失败的四个真实成因:git URL 重写镜像、pnpm `fetch-timeout` 超上限、远端大 tarball、lockfile 残留 `git+ssh`。README 增加「插件页装不上」排障章节。 |
307
439
  | **v2.4.0** | 支持 **DSH 插件页直接安装**:新增 `dsh.bundle.patch` 声明与 `cordis.patch.yml`(含 `!!js` 自解析表达式),新增宿主半身 `lib/index.mjs`。安装路径由运行时探测,不再依赖手工配置;`install.mjs` 自动识别 bundle 模式,避免与插件页安装重复注册。仓库转公开。 |
308
440
  | v2.3.0 | 仓库收敛为单插件,仓库根即插件根;新增 `install.cmd/sh`、`update.cmd/sh`;重装保留手工改过的 env。 |
package/install.cmd CHANGED
@@ -3,13 +3,13 @@ rem Pure ASCII output on purpose: cmd.exe mis-parses UTF-8 .cmd files
3
3
  rem (multibyte chars break the parser), so keep every echo line ASCII.
4
4
  setlocal EnableExtensions EnableDelayedExpansion
5
5
  chcp 65001 >nul 2>nul
6
- title dsh-session-index-lau installer
6
+ title dsh-session-index installer
7
7
 
8
8
  set "PLUGIN_DIR=%~dp0"
9
9
  if "%PLUGIN_DIR:~-1%"=="\" set "PLUGIN_DIR=%PLUGIN_DIR:~0,-1%"
10
10
 
11
11
  echo ============================================================
12
- echo dsh-session-index-lau installer
12
+ echo dsh-session-index installer
13
13
  echo Plugin dir: %PLUGIN_DIR%
14
14
  echo ============================================================
15
15
  echo.
package/install.sh CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # dsh-session-index-lau 一键安装(macOS / Linux / Git Bash)
2
+ # dsh-session-index 一键安装(macOS / Linux / Git Bash)
3
3
  # 用法: ./install.sh 安装
4
4
  # ./install.sh --verify 校验
5
5
  # ./install.sh --uninstall 回滚
@@ -9,7 +9,7 @@ PLUGIN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
9
  cd "$PLUGIN_DIR" || exit 1
10
10
 
11
11
  echo "============================================================"
12
- echo " dsh-session-index-lau 一键安装"
12
+ echo " dsh-session-index 一键安装"
13
13
  echo " 插件目录: $PLUGIN_DIR"
14
14
  echo "============================================================"
15
15
  echo
@@ -54,7 +54,26 @@ export function loadCards(indexDir) {
54
54
  function atomicWrite(filePath, data) {
55
55
  const tmp = `${filePath}.tmp-${process.pid}`;
56
56
  fs.writeFileSync(tmp, data);
57
- fs.renameSync(tmp, filePath);
57
+ // Windows 上目标文件被另一进程短暂占用时 rename 会抛 EPERM/EACCES。
58
+ // DSH 的 MCP 服务与冒烟/手动构建可能同时写同一份索引,做短退避重试。
59
+ const delays = [0, 40, 120, 300];
60
+ let last;
61
+ for (const delay of delays) {
62
+ if (delay) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delay);
63
+ try {
64
+ fs.renameSync(tmp, filePath);
65
+ return;
66
+ } catch (err) {
67
+ last = err;
68
+ if (!['EPERM', 'EACCES', 'EBUSY'].includes(err.code)) break;
69
+ }
70
+ }
71
+ try {
72
+ fs.unlinkSync(tmp);
73
+ } catch {
74
+ // 清理失败不影响错误上报
75
+ }
76
+ throw last;
58
77
  }
59
78
 
60
79
  export function saveMeta(indexDir, meta) {
package/lib/stage.mjs ADDED
@@ -0,0 +1,231 @@
1
+ /**
2
+ * 会话整理文档 → 思源桥「待入库」队列(服务层 + 仓储层)。
3
+ *
4
+ * 边界(刻意为之):
5
+ * - 本模块**不读索引、不碰会话文件**,源数据由 server.mjs 控制器组装后传入;
6
+ * - 只调用 /api/status、/api/stage(GET/POST),**绝不调用 /api/stage/commit**;
7
+ * 写进思源笔记库这一步永远留给人在思源桥界面确认;
8
+ * - 只允许回环地址,避免工具被误用来打任意内网服务。
9
+ *
10
+ * 两步确认(无状态):预览返回 token = sha256(标题 + 落点 + 正文)。
11
+ * 确认时按同一组参数重新渲染、重算 token,一致才真正投递,防止「预览 A、投递 B」。
12
+ */
13
+ import { createHash } from 'node:crypto';
14
+
15
+ const DEFAULT_PORT = 5599;
16
+ const MAX_MARKDOWN = 60000;
17
+ const DEFAULT_TIMEOUT = 5000;
18
+
19
+ /** 配置集中解析:全部来自环境变量,启动时不需要外部依赖。 */
20
+ export function stageConfig(env = process.env) {
21
+ const port = String(env.SIYUAN_BRIDGE_PORT || DEFAULT_PORT);
22
+ const url = String(env.SIYUAN_BRIDGE_URL || `http://127.0.0.1:${port}`).replace(/\/+$/, '');
23
+ return {
24
+ url,
25
+ enabled: env.SESSION_INDEX_STAGE !== '0',
26
+ timeoutMs: Math.min(60000, Math.max(1000, Number(env.SESSION_INDEX_STAGE_TIMEOUT_MS) || DEFAULT_TIMEOUT)),
27
+ defaultHpath: env.SESSION_INDEX_STAGE_HPATH || '/会话札记/思源',
28
+ defaultNotebook: env.SESSION_INDEX_STAGE_NOTEBOOK || '思源桥',
29
+ maxMarkdown: MAX_MARKDOWN,
30
+ };
31
+ }
32
+
33
+ /** 拒绝非回环地址:这个工具只应该跟本机的思源桥说话。 */
34
+ export function assertLoopback(url) {
35
+ let parsed;
36
+ try {
37
+ parsed = new URL(url);
38
+ } catch {
39
+ throw new Error('SIYUAN_BRIDGE_URL 不是合法 URL');
40
+ }
41
+ const host = parsed.hostname;
42
+ const ok = parsed.protocol === 'http:' && (host === '127.0.0.1' || host === 'localhost' || host === '::1' || host === '[::1]');
43
+ if (!ok) throw new Error('思源桥地址必须是本机回环地址(127.0.0.1 / localhost)');
44
+ return parsed;
45
+ }
46
+
47
+ /** 类型化错误:客户端只看到规范化结构,不暴露堆栈与内部细节。 */
48
+ export class StageError extends Error {
49
+ constructor(code, message, detail) {
50
+ super(message);
51
+ this.name = 'StageError';
52
+ this.code = code;
53
+ this.detail = detail;
54
+ }
55
+ toJSON() {
56
+ return { ok: false, code: this.code, error: this.message, detail: this.detail };
57
+ }
58
+ }
59
+
60
+ const clean = (s, max = 200) => String(s ?? '').replace(/[\\/:*?"<>|\r\n]+/g, ' ').trim().slice(0, max);
61
+ const stamp = (ts) => {
62
+ if (!ts) return '时间未知';
63
+ const d = new Date(ts);
64
+ return Number.isNaN(d.getTime()) ? '时间未知' : d.toLocaleString('zh-CN', { hour12: false });
65
+ };
66
+
67
+ /** 渲染会话整理文档:纯函数,无副作用,便于单测。 */
68
+ export function buildStageDocument(sources = {}, opts = {}) {
69
+ const template = ['brief', 'decisions', 'detail'].includes(opts.template) ? opts.template : 'brief';
70
+ const sessions = Array.isArray(sources.sessions) ? sources.sessions : [];
71
+ const memories = Array.isArray(sources.memories) ? sources.memories : [];
72
+ const hits = Array.isArray(sources.hits) ? sources.hits : [];
73
+ const topic = clean(opts.query || '', 60);
74
+ const date = new Date();
75
+ const day = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
76
+ const hhmm = `${String(date.getHours()).padStart(2, '0')}时${String(date.getMinutes()).padStart(2, '0')}分`;
77
+ const title = clean(opts.title || `会话整理 · ${day} ${hhmm} · ${topic || `${sessions.length} 个会话`}`, 120)
78
+ || `会话整理 · ${day} ${hhmm}`;
79
+
80
+ const dshCount = sessions.filter((s) => (s.app || 'dsh') === 'dsh').length;
81
+ const wbCount = sessions.length - dshCount;
82
+ const lines = [
83
+ `# ${title}`,
84
+ '',
85
+ `> 来源:session-index · ${sessions.length} 个会话(DSH ${dshCount} / WorkBuddy ${wbCount})· 生成于 ${stamp(date.getTime())}`,
86
+ `> 模板:${template} · 范围:${opts.app || 'all'}${topic ? ` · 主题:${topic}` : ''}`,
87
+ '> 本稿仅投递到思源桥「待入库」队列,**未写入笔记库**,需人工确认。',
88
+ '',
89
+ ];
90
+
91
+ if (!sessions.length && !memories.length && !hits.length) {
92
+ lines.push('(没有可整理的内容:换个主题词、放宽时间范围,或先确认索引已建好)', '');
93
+ }
94
+
95
+ if (sessions.length) {
96
+ if (template === 'decisions') {
97
+ lines.push('## 结论与决策', '');
98
+ let n = 0;
99
+ for (const s of sessions) {
100
+ for (const d of (s.decisions || [])) {
101
+ lines.push(`- ${String(d).replace(/\s+/g, ' ').trim()} — ${clean(s.title, 60)}`);
102
+ n++;
103
+ }
104
+ }
105
+ if (!n) lines.push('(这些会话卡里没有抽取到结论,可改用 template=detail 看原文片段)');
106
+ lines.push('');
107
+ } else {
108
+ lines.push('## 会话速览', '');
109
+ sessions.forEach((s, i) => {
110
+ lines.push(`### ${i + 1}. ${clean(s.title || '(无标题)', 90)}`, '');
111
+ lines.push(`- 应用:${s.app || 'dsh'} · 更新:${stamp(s.updatedAt)} · 会话:\`${clean(s.id, 48)}\``);
112
+ if (s.workspace) lines.push(`- 工作区:\`${clean(s.workspace, 90)}\``);
113
+ if (s.digest) lines.push(`- 摘要:${String(s.digest).replace(/\s+/g, ' ').trim()}`);
114
+ if ((s.decisions || []).length) {
115
+ lines.push('- 结论:');
116
+ for (const d of s.decisions) lines.push(` - ${String(d).replace(/\s+/g, ' ').trim()}`);
117
+ }
118
+ if ((s.questions || []).length) lines.push(`- 提问:${s.questions.map((q) => String(q).replace(/\s+/g, ' ').trim()).join(';')}`);
119
+ if ((s.keywords || []).length) lines.push(`- 关键词:${s.keywords.join('、')}`);
120
+ lines.push('');
121
+ });
122
+ }
123
+ }
124
+
125
+ if (memories.length && template !== 'decisions') {
126
+ lines.push('## 长期记忆', '');
127
+ for (const m of memories) lines.push(`- [${m.type || 'note'} · 重要度 ${m.importance ?? 0}] ${String(m.text).replace(/\s+/g, ' ').trim()}`);
128
+ lines.push('');
129
+ } else if (memories.length) {
130
+ const decisions = memories.filter((m) => ['decision', 'fact', 'pitfall'].includes(m.type));
131
+ if (decisions.length) {
132
+ lines.push('## 相关长期记忆', '');
133
+ for (const m of decisions) lines.push(`- [${m.type}] ${String(m.text).replace(/\s+/g, ' ').trim()}`);
134
+ lines.push('');
135
+ }
136
+ }
137
+
138
+ if (template === 'detail' && hits.length) {
139
+ lines.push('## 检索命中片段', '');
140
+ for (const h of hits) {
141
+ lines.push(`**${clean(h.title || h.sessionId || '片段', 80)}** · ${h.app || ''} · seq ${h.seq ?? '-'}`, '');
142
+ lines.push('```text', String(h.snippet || h.text || '').trim(), '```', '');
143
+ }
144
+ }
145
+
146
+ lines.push('---', '', `共 ${sessions.length} 个会话 · ${memories.length} 条记忆 · ${hits.length} 个片段`);
147
+ const markdown = lines.join('\n');
148
+ if (markdown.length > MAX_MARKDOWN) {
149
+ throw new StageError('DOC_TOO_LARGE', `文档超过 ${MAX_MARKDOWN} 字上限`, `${markdown.length} 字,请调小 limit / maxChars`);
150
+ }
151
+ return { title, markdown, chars: markdown.length };
152
+ }
153
+
154
+ /** 预览令牌:把标题、落点与正文一起绑定,避免确认时内容被换掉。 */
155
+ export function stageToken(doc, opts = {}) {
156
+ const raw = [doc.title, opts.notebook || '', opts.hpath || '', opts.note || '', doc.markdown].join('\n');
157
+ return createHash('sha256').update(raw).digest('hex').slice(0, 24);
158
+ }
159
+
160
+ async function requestJSON(url, path, { method = 'GET', body, timeoutMs } = {}) {
161
+ assertLoopback(url);
162
+ let res;
163
+ try {
164
+ res = await fetch(url + path, {
165
+ method,
166
+ headers: body ? { 'content-type': 'application/json' } : undefined,
167
+ body: body ? JSON.stringify(body) : undefined,
168
+ signal: AbortSignal.timeout(timeoutMs),
169
+ });
170
+ } catch (err) {
171
+ throw new StageError('BRIDGE_UNREACHABLE', '思源桥服务不可达', `${url}${path} · ${err.name}: ${err.message}`);
172
+ }
173
+ const text = await res.text().catch(() => '');
174
+ let data;
175
+ try {
176
+ data = text ? JSON.parse(text) : {};
177
+ } catch {
178
+ throw new StageError('BRIDGE_BAD_RESPONSE', '思源桥返回了非 JSON 响应', `${path} · HTTP ${res.status}`);
179
+ }
180
+ if (!res.ok) {
181
+ const msg = data.error || data.message || `HTTP ${res.status}`;
182
+ throw new StageError('BRIDGE_ERROR', '思源桥拒绝了请求', `${path} · ${msg}`);
183
+ }
184
+ return data;
185
+ }
186
+
187
+ /** 只读探测:服务是否活着、队列里有多少篇、自动接入目录在哪。 */
188
+ export async function bridgeOverview(cfg) {
189
+ const [status, queue, autostage] = await Promise.all([
190
+ requestJSON(cfg.url, '/api/status', { timeoutMs: cfg.timeoutMs }),
191
+ requestJSON(cfg.url, '/api/stage', { timeoutMs: cfg.timeoutMs }),
192
+ requestJSON(cfg.url, '/api/autostage', { timeoutMs: cfg.timeoutMs }).catch(() => null),
193
+ ]);
194
+ return {
195
+ ok: true,
196
+ service: cfg.url,
197
+ kernel: status.kernel || null,
198
+ queued: Array.isArray(queue.items) ? queue.items.length : (status.staged ?? 0),
199
+ discarded: queue.discarded ?? 0,
200
+ autoStageDir: autostage?.dir || null,
201
+ autoStageEnabled: autostage?.enabled ?? null,
202
+ note: '只读取状态,未做任何写入',
203
+ };
204
+ }
205
+
206
+ /** 按「标题 + 字数」检查队列里是否已有同一份稿,避免重复入队。 */
207
+ export async function findDuplicate(cfg, doc) {
208
+ const queue = await requestJSON(cfg.url, '/api/stage', { timeoutMs: cfg.timeoutMs });
209
+ const items = Array.isArray(queue.items) ? queue.items : [];
210
+ return items.find((it) => String(it.title || '').trim() === doc.title && Number(it.chars) === doc.chars) || null;
211
+ }
212
+
213
+ /** 唯一允许的写操作:投递到待入库队列。绝不 commit。 */
214
+ export async function postStage(cfg, doc, opts = {}) {
215
+ const payload = {
216
+ title: doc.title,
217
+ markdown: doc.markdown,
218
+ notebook: opts.notebook || cfg.defaultNotebook,
219
+ hpath: opts.hpath || cfg.defaultHpath,
220
+ note: clean(opts.note || `session-index · ${opts.template || 'brief'} · 预览确认后投递`, 200),
221
+ };
222
+ const result = await requestJSON(cfg.url, '/api/stage', {
223
+ method: 'POST', body: payload, timeoutMs: cfg.timeoutMs,
224
+ });
225
+ return {
226
+ ok: true, staged: true,
227
+ id: result.id || null, title: result.title || doc.title,
228
+ chars: doc.chars, notebook: payload.notebook, hpath: payload.hpath,
229
+ next: '在思源桥「待入库」里勾选写入;本工具不会自动写库',
230
+ };
231
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-session-index-lau",
3
- "version": "2.4.1",
3
+ "version": "2.5.1",
4
4
  "type": "module",
5
5
  "description": "完全化的 DSH + WorkBuddy 全会话 MCP 插件:装到 DSH 里即用(bundle 自动挂载,无需手工改配置)、自动建索引、文件事件驱动识别新会话、MCP 变更通知、自动打包插件与技能。零第三方依赖,源会话只读。",
6
6
  "engines": {
@@ -37,7 +37,8 @@
37
37
  "*.yml",
38
38
  "lib/",
39
39
  "skill/",
40
- "!dist/*.zip"
40
+ "tools/",
41
+ "dist/*.zip"
41
42
  ],
42
43
  "dsh": {
43
44
  "bundle": {
package/plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-session-index-lau",
3
- "version": "2.4.1",
3
+ "version": "2.5.1",
4
4
  "icon": "icon.svg",
5
5
  "displayName": {
6
6
  "zh": "全会话索引与跨会话记忆",
@@ -34,7 +34,7 @@
34
34
  "entry": "server.mjs",
35
35
  "resolvedBy": "cordis.patch.yml 的 !!js 表达式(按 环境变量 → 宿主记下的路径文件 → profile 标准安装位置 依次探测)",
36
36
  "capabilities": ["tools", "prompts", "resources", "instructions", "listChanged"],
37
- "toolCount": 13,
37
+ "toolCount": 14,
38
38
  "tools": [
39
39
  "session_index_status",
40
40
  "session_index_brief",
@@ -48,7 +48,8 @@
48
48
  "session_index_forget",
49
49
  "session_index_package",
50
50
  "session_index_vectors",
51
- "session_index_refresh"
51
+ "session_index_refresh",
52
+ "session_index_stage"
52
53
  ],
53
54
  "prompts": ["session-context", "resume-topic", "catch-up", "quickstart"],
54
55
  "resources": [
@@ -83,8 +84,8 @@
83
84
  },
84
85
  "packages": {
85
86
  "directory": "dist",
86
- "plugin": "dsh-session-index-lau-v2.4.1.zip",
87
- "skill": "session-index-skill-v2.4.1.zip",
87
+ "plugin": "dsh-session-index-lau-v2.5.1.zip",
88
+ "skill": "session-index-skill-v2.5.1.zip",
88
89
  "generator": "lib/pack.mjs (zero dependency ZIP writer)",
89
90
  "contents": ["MANIFEST.json", "INSTALL.md"],
90
91
  "excludes": ["会话索引分片 *.jsonl.gz", "meta/cards/notes/vectors.json", "备份与临时文件", "已有 zip"]