dsh-session-index-lau 2.4.1 → 2.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +164 -14
- package/dist/dsh-session-index-lau-v2.4.1.zip +0 -0
- package/dist/session-index-skill-v2.4.1.zip +0 -0
- package/install.cmd +2 -2
- package/install.sh +2 -2
- package/lib/index-store.mjs +20 -1
- package/lib/stage.mjs +231 -0
- package/package.json +3 -2
- package/plugin.json +6 -5
- package/server.mjs +131 -1
- package/smoke.mjs +67 -4
- package/tools/push-doc.mjs +335 -0
- package/update.cmd +2 -2
- package/update.sh +2 -2
- package//346/223/215/344/275/234/344/275/277/347/224/250/346/211/213/345/206/214.html +72 -28
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
# dsh-session-index
|
|
1
|
+
# dsh-session-index
|
|
2
2
|
|
|
3
3
|
> DSH(DeepSeek Harness)+ WorkBuddy 的**全会话索引与跨会话记忆** MCP 插件。
|
|
4
4
|
> 拉取仓库 → 双击安装 → 之后全自动。零第三方依赖,源会话文件全程只读。
|
|
5
5
|
|
|
6
6
|
| 项目 | 值 |
|
|
7
7
|
|---|---|
|
|
8
|
-
| 版本 | **v2.
|
|
8
|
+
| 版本 | **v2.5.2** |
|
|
9
9
|
| 运行要求 | Node **≥ 22.15**(需要 `zlib.zstdDecompressSync`) |
|
|
10
|
-
| MCP 能力 | **
|
|
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
|
|
45
|
-
| `mcp-session-index` | `@deepseek-ai/dsh-mcp-client` | MCP stdio 客户端:
|
|
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,150 @@ 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
|
+
node tools/push-doc.mjs --from-workbuddy --seed # 只记状态基线,不投递任何文档
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
数据源是 `~/.workbuddy/artifact-index/<会话id>.json` 里 `contentType: document` 的条目
|
|
277
|
+
(`WORKBUDDY_ARTIFACT_INDEX` 可改)。默认会**排除** `node_modules/dist/.git`、`.verify-*`、
|
|
278
|
+
`DSH-probe`、旧开发副本 `dsh-session-index`、备份与 UI 源码,并按**内容哈希 + 路径**去重,
|
|
279
|
+
避免同一份 README 被推十几遍。每篇标题带相对路径、备注带来源会话,落点统一可在界面改。
|
|
280
|
+
|
|
281
|
+
实测(2026-11-14):70 条文档命中 → 排除 26 条临时/依赖 → 文件已不存在 4 →
|
|
282
|
+
路径或内容重复 18 → **唯一 22 篇全部投递成功**,队列 23 篇(含此前单推的手册)。
|
|
283
|
+
可用 `--limit`(默认 60)与 `--since` 收窄。
|
|
284
|
+
|
|
285
|
+
**判重:对齐思源桥 `autoStage` 的 `.auto-staged.json`,但丢弃后不重新获取。**
|
|
286
|
+
|
|
287
|
+
状态文件 `~/.dsh/session-index/push-doc-state.json`(`PUSH_DOC_STATE` 可改)按
|
|
288
|
+
「文档路径 + 内容 sha256」记录已推送过的文档,**判重不看队列里还在不在**:
|
|
289
|
+
|
|
290
|
+
- 在思源桥里「丢弃」某篇 → 下次自动化**不会**把它重新推送回来
|
|
291
|
+
- 队列被清空、服务重启、定时任务重跑 → 同样不会重复入队
|
|
292
|
+
- 内容真的改了 → 作为新版本投递一次,之后又不再重复
|
|
293
|
+
- `--force` 可强制绕过;`--seed` 只写状态基线、不投递(首次启用或迁移时用)
|
|
294
|
+
|
|
295
|
+
- **默认只预览**,加 `--send` 才入队;预览写到 `dist/push-doc-preview.md`
|
|
296
|
+
- HTML 会先裁掉侧栏导航、复制按钮、脚本、样式与统计卡,再用思源桥自带的
|
|
297
|
+
`/api/paste/html`(只读)转成 Markdown,不会把界面文字塞进笔记
|
|
298
|
+
- 同样**只入队、不 commit**,写入仍由你在思源桥界面确认
|
|
299
|
+
|
|
300
|
+
### 插件怎么更新(DSH 插件页有没有「更新」按钮)
|
|
301
|
+
|
|
302
|
+
**有,但只对特定安装来源显示**,这是 DSH 插件页的规则(从 DSH 内置文档确认):
|
|
303
|
+
|
|
304
|
+
| 安装来源 | 更新方式 |
|
|
305
|
+
|---|---|
|
|
306
|
+
| 注册表(npm)安装的第三方组合包 | 详情页有「版本」区块与**更新**按钮,可查新版本并升级 |
|
|
307
|
+
| Git 仓库安装的组合包(`update: 'git'`) | 通过「发现」页 / 详情页的**更新到**升级 |
|
|
308
|
+
| 官方按需插件 | 有显式**更新**(版本或开发链接来源不同时) |
|
|
309
|
+
| **本地路径 / `link:` / 压缩包安装的** | **不显示版本区块**,文档写明「通过卸载后重新安装升级」 |
|
|
310
|
+
|
|
311
|
+
**本机当前是 npm registry 来源**:profile 依赖 `dsh-session-index-lau: ^2.5.2`,
|
|
312
|
+
属于上表第一行,所以插件页**有「版本」区块与「更新」按钮**——以后发新版后
|
|
313
|
+
**点更新即可,不需要卸载重装**。命令行 `pnpm update <包名>` 与点按钮是同一条路径。
|
|
314
|
+
|
|
315
|
+
发布侧走 `dist/publish/` 下的标准流程(`插件发布标准流程.md`):
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
node sync-publish.mjs session-index # 生成改名后的发布副本并自检
|
|
319
|
+
cd dsh-session-index-lau && npm publish --access public
|
|
320
|
+
# 等 npm staged publishing 生效(实测约 1 分钟)
|
|
321
|
+
cd ~/.dsh/profiles/desktop && npx -y pnpm@11.28.5 update dsh-session-index-lau@<新版本>
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
注意 `pnpm-workspace.yaml` 的 `minimumReleaseAgeExclude` 要同步登记新版本,
|
|
325
|
+
否则供应链冷却期会让 pnpm 静默回退旧版。更新后**完全退出 DSH 再打开**,
|
|
326
|
+
运行中的 MCP 子进程才会换成新版。
|
|
327
|
+
|
|
328
|
+
> 只有从本地路径 / `link:` / 压缩包安装的才没有更新按钮(需卸载重装);
|
|
329
|
+
> 这类挂载因为直接指向仓库目录,`git pull` + 重启即生效,同样不用重装。
|
|
330
|
+
> 发布给别人用需要 `git push` + `npm publish`;本机开发目录则 `git pull` 即可。
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
189
334
|
## 更新 / 校验 / 回滚
|
|
190
335
|
|
|
191
336
|
```bash
|
|
@@ -202,7 +347,7 @@ node install.mjs --verify # 6 项状态校验
|
|
|
202
347
|
node install.mjs --dry-run # 预览改动,不写文件
|
|
203
348
|
node install.mjs --uninstall # 移除配置块与自动装的 Skill(先备份)
|
|
204
349
|
node install.mjs --reset-env # 用默认值覆盖 env 自定义
|
|
205
|
-
node smoke.mjs #
|
|
350
|
+
node smoke.mjs # 80 项端到端自测
|
|
206
351
|
```
|
|
207
352
|
|
|
208
353
|
---
|
|
@@ -213,8 +358,8 @@ node smoke.mjs # 66 项端到端自测
|
|
|
213
358
|
|
|
214
359
|
| 文件 | 内容 | 用途 |
|
|
215
360
|
|---|---|---|
|
|
216
|
-
| `dist/dsh-session-index-
|
|
217
|
-
| `dist/session-index-skill-v2.
|
|
361
|
+
| `dist/dsh-session-index-v2.5.2.zip` | 全部源码 + 清单 + 文档 + 手册 + 脚本 + `MANIFEST.json` + `INSTALL.md` | 完整分发,解压后双击 `install.cmd` |
|
|
362
|
+
| `dist/session-index-skill-v2.5.2.zip` | 根 `SKILL.md` + 说明 | 直接导入 DSH「MCP 与技能」 |
|
|
218
363
|
|
|
219
364
|
包内**排除**:索引分片 `*.jsonl.gz`、`meta/cards/notes/vectors.json`、备份与临时文件、已有 zip。
|
|
220
365
|
已验证包内无任何会话数据与密钥,可安全分发。也可随时 `node pack.mjs` 重打,或调 MCP 工具 `session_index_package`。
|
|
@@ -225,7 +370,7 @@ node smoke.mjs # 66 项端到端自测
|
|
|
225
370
|
|
|
226
371
|
全部通过配置块的 `env` 设置。改哪里取决于安装方式:
|
|
227
372
|
|
|
228
|
-
- **插件页安装**:改插件包内的 `cordis.patch.yml`(`<profile>/node_modules/dsh-session-index
|
|
373
|
+
- **插件页安装**:改插件包内的 `cordis.patch.yml`(`<profile>/node_modules/dsh-session-index/cordis.patch.yml`),改完重启 DSH;
|
|
229
374
|
- **clone + 脚本**:改 `~/.dsh/profiles/desktop/cordis.patch.yml` 里 `>>> session-index` 标记块内的 `env`,重跑 `node install.mjs` 会保留你写的值。
|
|
230
375
|
|
|
231
376
|
| 变量 | 默认 | 说明 |
|
|
@@ -247,13 +392,13 @@ node smoke.mjs # 66 项端到端自测
|
|
|
247
392
|
## 目录结构
|
|
248
393
|
|
|
249
394
|
```text
|
|
250
|
-
dsh-session-index
|
|
251
|
-
├── server.mjs # MCP stdio server(
|
|
395
|
+
dsh-session-index/ # 仓库根 = 插件根
|
|
396
|
+
├── server.mjs # MCP stdio server(14 工具 / prompts / resources)
|
|
252
397
|
├── cordis.patch.yml # bundle patch:挂宿主行 + MCP 行(含 !!js 自解析路径)
|
|
253
398
|
├── build-index.mjs # 索引构建(多帧 zstd → 可检索记录 + 会话卡)
|
|
254
399
|
├── install.mjs # 安装 / 校验 / 卸载 / 自动打包
|
|
255
400
|
├── pack.mjs # 打包 CLI(零依赖 ZIP writer)
|
|
256
|
-
├── smoke.mjs #
|
|
401
|
+
├── smoke.mjs # 80 项端到端自测
|
|
257
402
|
├── install.cmd / install.sh # 一键安装(双击 / 执行)
|
|
258
403
|
├── update.cmd / update.sh # 更新(git pull + 重装)
|
|
259
404
|
├── icon.svg # 插件页图标(package.json 的 icon 字段指向它)
|
|
@@ -272,8 +417,10 @@ dsh-session-index-lau/ # 仓库根 = 插件根
|
|
|
272
417
|
│ ├── notes.mjs # 长期记忆库
|
|
273
418
|
│ ├── auto.mjs # 自动建索引 / 定时刷新
|
|
274
419
|
│ ├── watch.mjs # 文件事件监听
|
|
275
|
-
│
|
|
420
|
+
│ ├── pack.mjs # ZIP writer
|
|
421
|
+
│ └── stage.mjs # 会话整理 → 思源桥待入库(渲染 + HTTP 投递)
|
|
276
422
|
├── skill/SKILL.md # 自动接入用技能
|
|
423
|
+
├── tools/push-doc.mjs # 推送任意 md/html 到思源桥待入库(默认只读预览)
|
|
277
424
|
└── dist/ # 自动生成的分发包(git 忽略)
|
|
278
425
|
```
|
|
279
426
|
|
|
@@ -292,7 +439,7 @@ dsh-session-index-lau/ # 仓库根 = 插件根
|
|
|
292
439
|
|
|
293
440
|
## 卸载
|
|
294
441
|
|
|
295
|
-
- **插件页安装的**:插件页里禁用或卸载 `dsh-session-index
|
|
442
|
+
- **插件页安装的**:插件页里禁用或卸载 `dsh-session-index`,重启 DSH 即可(MCP 行随 bundle 一起消失)。
|
|
296
443
|
- **clone + 脚本安装的**:`node install.mjs --uninstall`,移除配置块与自动安装的 Skill(先备份)。
|
|
297
444
|
|
|
298
445
|
两种情况下索引目录 `~/.dsh/session-index/` 都保留(不影响原始会话),需要时手动删除。
|
|
@@ -303,6 +450,9 @@ dsh-session-index-lau/ # 仓库根 = 插件根
|
|
|
303
450
|
|
|
304
451
|
| 版本 | 变更 |
|
|
305
452
|
|---|---|
|
|
453
|
+
| **v2.5.2** | 发布流程标准化:新增发布前/后核验门禁(`dist/publish/check-release.mjs`),把「检查并核验 → 确认版本可用 → 才推进标准流程」固化为强制步骤;标准流程文档同步新机制(批量推送、持久化判重、定时自动化)与版本统一矩阵。把源仓库已生效的持久化判重脚本同步进发布包与 DSH 安装,消除「同版本号、内容不同」的分叉。 |
|
|
454
|
+
| **v2.5.1** | `tools/push-doc.mjs` 增加 **`--from-workbuddy` 批量模式**:扫描 WorkBuddy 全部会话产出的文档,按内容哈希去重、排除临时/依赖目录、逐篇查重后一次性投递到思源桥待入库;默认只读预览,`--send` 才入队,`--no-exclude` / `--include-txt` / `--source` / `--limit` / `--since` / `--force` 可控。修掉按 `chars` 判重不稳的问题(思源桥会规整正文)。README 补「插件怎么更新」章节。 |
|
|
455
|
+
| **v2.5.0** | 新增第 14 个工具 `session_index_stage`:**会话整理文档 → 思源桥「待入库」队列**。两步确认(预览返回令牌,原样回传 `confirmWith` 才投递)、按标题+字数去重、支持主题/指定会话/三种模板/落点配置,**绝不自动写入思源笔记库**。读取状态与队列可传 `status: true`。 |
|
|
306
456
|
| **v2.4.1** | 新增 `install.mjs --doctor` **环境体检**(7 项,只读),覆盖插件页安装失败的四个真实成因:git URL 重写镜像、pnpm `fetch-timeout` 超上限、远端大 tarball、lockfile 残留 `git+ssh`。README 增加「插件页装不上」排障章节。 |
|
|
307
457
|
| **v2.4.0** | 支持 **DSH 插件页直接安装**:新增 `dsh.bundle.patch` 声明与 `cordis.patch.yml`(含 `!!js` 自解析表达式),新增宿主半身 `lib/index.mjs`。安装路径由运行时探测,不再依赖手工配置;`install.mjs` 自动识别 bundle 模式,避免与插件页安装重复注册。仓库转公开。 |
|
|
308
458
|
| v2.3.0 | 仓库收敛为单插件,仓库根即插件根;新增 `install.cmd/sh`、`update.cmd/sh`;重装保留手工改过的 env。 |
|
|
Binary file
|
|
Binary file
|
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
|
|
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
|
|
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
|
|
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
|
|
12
|
+
echo " dsh-session-index 一键安装"
|
|
13
13
|
echo " 插件目录: $PLUGIN_DIR"
|
|
14
14
|
echo "============================================================"
|
|
15
15
|
echo
|
package/lib/index-store.mjs
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
3
|
+
"version": "2.5.2",
|
|
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
|
-
"
|
|
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.
|
|
3
|
+
"version": "2.5.2",
|
|
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":
|
|
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.
|
|
87
|
-
"skill": "session-index-skill-v2.
|
|
87
|
+
"plugin": "dsh-session-index-lau-v2.5.2.zip",
|
|
88
|
+
"skill": "session-index-skill-v2.5.2.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"]
|