slumberpavilion 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +154 -2
- package/assets/icon.svg +19 -0
- package/cordis.patch.yml +15 -0
- package/lib/cli.js +61 -0
- package/lib/index.js +893 -0
- package/lib/reader.js +252 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +64 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AngleNaris
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,155 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 沉眠阁 · slumberpavilion
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
把 DSH 里「已归档」的会话**真正搬出加载路径**,并内置读取归档内容的能力。
|
|
4
|
+
|
|
5
|
+
## 它为什么需要存在
|
|
6
|
+
|
|
7
|
+
DSH 的「归档」只是一个标签,写进 `storages/workspace.json` 的 `archivedSessionIds`。
|
|
8
|
+
官方 `archived-session-gate` 的原话是:
|
|
9
|
+
|
|
10
|
+
> an archived Session ... must not run a model step until it is restored.
|
|
11
|
+
|
|
12
|
+
它拦的是**能不能跑**,不是**占不占地方**。归档之后:
|
|
13
|
+
|
|
14
|
+
- 会话日志仍留在 `.dsh/sessions/<工作区>/<会话id>/`
|
|
15
|
+
- 投影记录仍留在 `.dsh/storages/session_projcache/sessions/<id>.json`
|
|
16
|
+
|
|
17
|
+
而 `session_projcache` 用的是 JSON 存储后端,官方注释写明「读写共享存储域内一致的内存状态」——
|
|
18
|
+
**全部记录常驻内存**。实测:2395 条记录 / 169.5 MB JSON,其中 126 条已归档的占 40.8 MB(24.1%),
|
|
19
|
+
而它们只占条目数的 5%。归档的大多是长篇会话,正是最占地方的那批。
|
|
20
|
+
|
|
21
|
+
本插件做三件事:
|
|
22
|
+
|
|
23
|
+
1. 每隔一段时间检查 `archivedSessionIds`,发现新归档的会话
|
|
24
|
+
2. 把它的日志目录与投影记录**移动**到 `~/DSH-Archive`(不是删除)
|
|
25
|
+
3. `manifest.json` 记下来源与去向,随时可还原
|
|
26
|
+
|
|
27
|
+
## 安装
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install slumberpavilion
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
然后在 profile 的 `package.json` 里把包装进 bundles:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"slumberpavilion": "^0.1.0"
|
|
38
|
+
},
|
|
39
|
+
"dsh": {
|
|
40
|
+
"profile": {
|
|
41
|
+
"bundles": [
|
|
42
|
+
"...",
|
|
43
|
+
"slumberpavilion"
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
重启 DSH。本地开发时可以用 `file:` 依赖 + 一条符号链接代替 npm 安装:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
ln -s ../plugins/slumberpavilion ~/.dsh/profiles/desktop/node_modules/slumberpavilion
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 配置
|
|
56
|
+
|
|
57
|
+
在 `cordis.patch.yml` 里,全部字段都是 volatile(改完热生效):
|
|
58
|
+
|
|
59
|
+
| 字段 | 默认 | 含义 |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| `enabled` | `true` | 总开关 |
|
|
62
|
+
| `archiveRoot` | `''` | 归档根目录,留空 = `~/DSH-Archive` |
|
|
63
|
+
| `checkIntervalMs` | `20000` | 检查间隔(毫秒) |
|
|
64
|
+
| `moveLogs` | `true` | 是否移动 `sessions` 下的日志目录 |
|
|
65
|
+
| `moveProjections` | `true` | 是否移动 `session_projcache` 记录(**内存收益主要来自这里**) |
|
|
66
|
+
|
|
67
|
+
## 读取归档
|
|
68
|
+
|
|
69
|
+
### 方式一:命令行(不依赖 DSH,随时可用)
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
node lib/cli.js list 20
|
|
73
|
+
node lib/cli.js read f8ab825e 上下文压缩 50
|
|
74
|
+
node lib/cli.js info f8ab825e
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 方式二:Agent 工具
|
|
78
|
+
|
|
79
|
+
注册五个工具,对话里可直接调用:
|
|
80
|
+
|
|
81
|
+
| 工具 | 作用 |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `archive_list` | 列出归档会话(按体积降序) |
|
|
84
|
+
| `archive_read` | 读一个归档会话;`keyword` 过滤、`compact` 只留对话轮次、`maxChars` 逐条截断 |
|
|
85
|
+
| `archive_session` | **执行归档**:标记 + 立刻搬走,不用等后台扫描;先 `dryRun=true` 看清目标 |
|
|
86
|
+
| `archive_restore` | **还原归档**:搬回 `.dsh`、撤销归档标记、清清单;同样支持 `dryRun` |
|
|
87
|
+
| `archive_delete` | 永久删除一个归档会话;先 `dryRun=true` 看清目标再动手 |
|
|
88
|
+
|
|
89
|
+
`archive_session` 走的是宿主的 `ctx.workspaceRegistry.archiveSession()`,所以归档状态
|
|
90
|
+
是 DSH 自己写进 `workspace.json` 的,不靠手改文件;标记落盘后插件立刻 `sweep()` 一轮,
|
|
91
|
+
把日志与投影搬进归档区,返回里会报出最终目录。
|
|
92
|
+
|
|
93
|
+
- 只认唯一命中:ID 或片段模糊命中多条时**拒绝执行**并列出候选,免得搬错会话。
|
|
94
|
+
- 活跃会话默认拒绝(`WorkspaceActiveSessionError`);确认要归档正在跑的会话,加
|
|
95
|
+
`stopActivity=true`,宿主会先请求停掉它的工作再归档。
|
|
96
|
+
- 拒绝归档「正在执行这条工具的那个会话」,不然等于自断。
|
|
97
|
+
|
|
98
|
+
`archive_restore` 是它的逆操作,顺序是「搬文件 → `unarchiveSession()` → 清清单」:
|
|
99
|
+
|
|
100
|
+
- 目标位置已有同名目录时**拒绝**,让你先确认是不是重复还原。
|
|
101
|
+
- 清单里记着原始的 `from`,还原优先搬回原位;没有记录时退回
|
|
102
|
+
`.dsh/sessions/<工作区>/<会话 ID>`。
|
|
103
|
+
- 投影记录(`projcache`)跟着一起搬回;目标已存在的不覆盖。
|
|
104
|
+
- 撤销标记失败时仍然搬文件,并在返回里明确警告「它仍算已归档,请在界面取消归档」。
|
|
105
|
+
|
|
106
|
+
这层曾经整个是哑的,原因是三个叠在一起的问题,现在都修了:
|
|
107
|
+
|
|
108
|
+
1. **没有声明 `inject`**。Cordis 里不 `export const inject = ["tools"]` 就读不了
|
|
109
|
+
`ctx.tools`,一读抛 `cannot get property "tools" without inject`;原来的代码把这句
|
|
110
|
+
写在 `try` 里,异常被吞掉,只剩一条 warn,工具永远静默缺席。
|
|
111
|
+
2. **`await import("@deepseek-ai/dsh-tools")` 解析不到**。这个包只随宿主打包在
|
|
112
|
+
`app.asar` 里,profile 的 `node_modules` 没有它,Node 的 ESM 解析器又不认 asar。
|
|
113
|
+
改用 `createRequire(import.meta.url)`(Electron 主进程认得 asar 路径),同步取。
|
|
114
|
+
3. **工具定义形状不对**。`defineTool` 收的是「属性表」,必填用 `required: true` 标在
|
|
115
|
+
属性上;`output.render(args, value)` 必须返回 `ContentBlock[]`。原来传的是完整
|
|
116
|
+
JSON Schema、`render` 直接返回字符串。
|
|
117
|
+
|
|
118
|
+
取不到 `defineTool` 时还有一层本地兜底:`normalizeToolDefinition()` 自己把属性表
|
|
119
|
+
展开成完整 JSON Schema,所以换机器/换宿主也不会两头空。
|
|
120
|
+
|
|
121
|
+
> 排障:插件往 `%TEMP%\slumber-pavilion-diag.log` 记一行 apply、一行
|
|
122
|
+
> defineTool 是否取到、每个工具是否注册成功。改了代码重载插件后先看这个文件。
|
|
123
|
+
|
|
124
|
+
## 还原
|
|
125
|
+
|
|
126
|
+
两种方式,效果一样:
|
|
127
|
+
|
|
128
|
+
- 对话里 `archive_restore`(推荐):搬文件、撤销标记、清清单一次做完,先跑 `dryRun` 看目标。
|
|
129
|
+
- 手动:`manifest.json` 里每个条目都记着 `from`(原路径)与 `to`(归档路径),把 `to` 移回
|
|
130
|
+
`from`,再去界面里取消归档、从清单删掉那一条。
|
|
131
|
+
|
|
132
|
+
## 已知边界
|
|
133
|
+
|
|
134
|
+
- **扫描只处理「已归档」的会话**。归档标记由 DSH 写;`archive_session` 只是替你把这个
|
|
135
|
+
标记写进去,然后立刻搬文件。
|
|
136
|
+
- 移动后 `workspace.json` 里的 `archivedSessionIds` 不会清理,指向一个不存在的会话。
|
|
137
|
+
实测这不影响启动(列表按投影记录渲染,记录没了就不显示)。
|
|
138
|
+
- 若会话日志已被 DSH 重新折叠出新的投影记录,下次扫描会再搬一次。
|
|
139
|
+
- 「已搬过」的判据覆盖**顶层与各批次目录**的 `manifest.json`。早先只读顶层那份,老批次
|
|
140
|
+
条目会被当成没搬过、每轮空转一次;现在不会了。
|
|
141
|
+
- `archive_session` 只认磁盘上还在 `.dsh/sessions` 里的会话;只剩归档副本的请用
|
|
142
|
+
`archive_list` / `archive_read`。
|
|
143
|
+
|
|
144
|
+
## 为什么解压要手写
|
|
145
|
+
|
|
146
|
+
DSH 的 `session.v4.jsonl.zstd` 是**多帧** zstd(边写边追加,每追加一次一个独立帧)。
|
|
147
|
+
Node 的 `zstdDecompressSync` 和 `createZstdDecompress` 都只解第一帧就收工——
|
|
148
|
+
实测 1.1 MB 只解出 285 字节,恰好是会话头。
|
|
149
|
+
|
|
150
|
+
所以 `reader.js` 按帧魔数 `28 B5 2F FD` 切开逐个解,切错就往后找下一个魔数重试。
|
|
151
|
+
实测那个 60.6 MB 的会话:**31,528 帧,56,701 行,零失败**,压缩比约 ×3.8。
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
MIT
|
package/assets/icon.svg
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128" role="img" aria-label="沉眠阁:浅底深形,旋转四十五度后四瓣呈菱形排列,中央一枚金点,四角紫色直角折线大幅出血出画">
|
|
2
|
+
<rect width="128" height="128" fill="#F3F0E9"/>
|
|
3
|
+
|
|
4
|
+
<g transform="translate(64 64) rotate(45) scale(0.95) translate(-64 -64)">
|
|
5
|
+
<rect x="30" y="30" width="28" height="28" fill="#1B1725"/>
|
|
6
|
+
<rect x="70" y="30" width="28" height="28" fill="#1B1725"/>
|
|
7
|
+
<rect x="30" y="70" width="28" height="28" fill="#1B1725"/>
|
|
8
|
+
<rect x="70" y="70" width="28" height="28" fill="#1B1725"/>
|
|
9
|
+
|
|
10
|
+
<rect x="60" y="60" width="8" height="8" fill="#B08A3E"/>
|
|
11
|
+
|
|
12
|
+
<g fill="none" stroke="#6B5CA5" stroke-width="2" stroke-linecap="butt" stroke-linejoin="miter">
|
|
13
|
+
<path d="M58 14 L14 14 L14 58"/>
|
|
14
|
+
<path d="M70 14 L114 14 L114 58"/>
|
|
15
|
+
<path d="M58 114 L14 114 L14 70"/>
|
|
16
|
+
<path d="M70 114 L114 114 L114 70"/>
|
|
17
|
+
</g>
|
|
18
|
+
</g>
|
|
19
|
+
</svg>
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# 沉眠阁 / slumberpavilion 的 bundle 补丁层。
|
|
2
|
+
#
|
|
3
|
+
# 安装:把本包加进 profile 的 dsh.profile.bundles,下次启动加载器会自动应用这一层。
|
|
4
|
+
#
|
|
5
|
+
# 归档根目录留空('')时使用默认值 ~/DSH-Archive。
|
|
6
|
+
# 所有字段都是 volatile:在运行时改完立即生效,不需要重启。
|
|
7
|
+
- insert:
|
|
8
|
+
- id: slumber-pavilion
|
|
9
|
+
name: slumberpavilion
|
|
10
|
+
config:
|
|
11
|
+
enabled: true
|
|
12
|
+
archiveRoot: ''
|
|
13
|
+
checkIntervalMs: 20000
|
|
14
|
+
moveLogs: true
|
|
15
|
+
moveProjections: true
|
package/lib/cli.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 归档会话的命令行入口。不依赖 DSH 运行时,任何 node 都能跑。
|
|
3
|
+
//
|
|
4
|
+
// node lib/cli.js list [条数]
|
|
5
|
+
// node lib/cli.js read <会话ID或片段> [关键词] [行数]
|
|
6
|
+
// node lib/cli.js info <会话ID或片段>
|
|
7
|
+
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { archiveRoot, findArchivedSession, listArchivedSessions, readArchivedSession } from "./reader.js";
|
|
10
|
+
|
|
11
|
+
const [, , cmd = "list", a1, a2, a3] = process.argv;
|
|
12
|
+
const ROOT = archiveRoot(process.env.DSH_ARCHIVE_ROOT || "");
|
|
13
|
+
|
|
14
|
+
function mb(n) {
|
|
15
|
+
return ((n || 0) / 1048576).toFixed(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (cmd === "list") {
|
|
19
|
+
const all = listArchivedSessions(ROOT);
|
|
20
|
+
const limit = Math.max(1, Math.min(500, Number(a1) || 30));
|
|
21
|
+
console.log("归档根目录:" + ROOT);
|
|
22
|
+
console.log("共 " + all.length + " 个会话,列出前 " + Math.min(limit, all.length) + " 个:\n");
|
|
23
|
+
for (const [i, x] of all.slice(0, limit).entries()) {
|
|
24
|
+
console.log(String(i + 1).padStart(3) + ". " + mb(x.bytes).padStart(8) + " MB " + x.id + " @" + x.workspace);
|
|
25
|
+
}
|
|
26
|
+
} else if (cmd === "info") {
|
|
27
|
+
const hit = findArchivedSession(ROOT, a1);
|
|
28
|
+
if (!hit) {
|
|
29
|
+
console.log("找不到:" + a1);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
const r = readArchivedSession(hit.dir, { limit: 1 });
|
|
33
|
+
console.log("会话:" + hit.id);
|
|
34
|
+
console.log("工作区:" + hit.workspace);
|
|
35
|
+
console.log("体积:" + mb(hit.bytes) + " MB");
|
|
36
|
+
console.log("目录:" + hit.dir);
|
|
37
|
+
console.log("总行数:" + r.total + ",压缩帧:" + r.frames);
|
|
38
|
+
} else if (cmd === "read") {
|
|
39
|
+
const hit = findArchivedSession(ROOT, a1);
|
|
40
|
+
if (!hit) {
|
|
41
|
+
console.log("找不到:" + a1 + "\n先用 list 看看有哪些。");
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
const keyword = a2 && a2 !== "-" ? a2 : "";
|
|
45
|
+
const limit = Number(a3) || 40;
|
|
46
|
+
const r = readArchivedSession(hit.dir, { keyword, limit });
|
|
47
|
+
|
|
48
|
+
console.log("会话 " + hit.id + " @" + hit.workspace);
|
|
49
|
+
console.log("总行数 " + r.total + ",匹配 " + r.matched + ",压缩帧 " + r.frames);
|
|
50
|
+
console.log("-".repeat(50));
|
|
51
|
+
for (const x of r.items) {
|
|
52
|
+
const tag = (x.kind ? "[" + x.kind + "]" : "") + (x.where || "");
|
|
53
|
+
console.log(tag + " " + x.body);
|
|
54
|
+
}
|
|
55
|
+
if (r.hasMore) console.log("… 还有更多,加大行数或用关键词过滤。");
|
|
56
|
+
} else {
|
|
57
|
+
console.log("用法:");
|
|
58
|
+
console.log(" node lib/cli.js list [条数]");
|
|
59
|
+
console.log(" node lib/cli.js read <会话ID或片段> [关键词| - ] [行数]");
|
|
60
|
+
console.log(" node lib/cli.js info <会话ID或片段>");
|
|
61
|
+
}
|