@chaoset/session-archive 0.1.0 → 0.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 +16 -6
- package/client/client.cjs +1 -0
- package/lib/index.mjs +61 -14
- package/lib/remote.mjs +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,8 +10,9 @@ web 侧边栏底部新增 **归档** 面板(🗂):
|
|
|
10
10
|
- **一次多选批量操作**:
|
|
11
11
|
- **恢复归档**(unarchive):把勾选的会话移回会话树,恢复其在原工作区的
|
|
12
12
|
位置(归档时保留的 slot 会计不被破坏)。
|
|
13
|
-
- **彻底删除**(delete
|
|
13
|
+
- **彻底删除**(delete):删除勾选会话的持久化文件与会话目录。删除为
|
|
14
14
|
两段式确认(第一次点击进入确认态,4 秒内再次点击执行),避免误删。
|
|
15
|
+
删除后该会话在侧边栏对话列表与归档面板中都不会再出现。
|
|
15
16
|
- 工具条提供**全选**、已选计数;**运行中**(live)的会话显示黄色徽标且
|
|
16
17
|
禁止勾选删除(请先停止会话);列表随会话树变化自动可刷新(⟳)。
|
|
17
18
|
|
|
@@ -34,11 +35,16 @@ dsh plugin --profile web add @chaoset/session-archive
|
|
|
34
35
|
文本消息(`user/message` / `assistant/message` 的 text 块),不做任何
|
|
35
36
|
写入/修复。
|
|
36
37
|
- **删除**:live 会话拒绝;每个会话删除持久化文件与会话目录(`locate()`
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
定位)。删除后**保留**该会话在归档集合中的 ghost id:宿主的
|
|
39
|
+
`archiveSession` 不停止内存会话,若把 id 移出归档集合,仍挂在内存中的
|
|
40
|
+
会话会因"不再归档"而立刻重新出现在侧边栏对话列表(效果等同"恢复");
|
|
41
|
+
保留 ghost id 后由 `list()` 的存在性过滤隐藏,面板与侧边栏都不再显示。
|
|
42
|
+
归档集合没有官方写入 API,插件复用 registry 自身的串行化写入通道
|
|
43
|
+
(`enqueueOperation → requireState → setState`,与 `archiveSession` 同一
|
|
44
|
+
路径);若内部形状变化会自动降级为"仅删文件",归档列表按文件存在性
|
|
45
|
+
过滤,功能不受影响。
|
|
46
|
+
- **恢复**:仅从归档集合移除**仍存在持久化文件**的会话 id,会话数据不动,
|
|
47
|
+
恢复后回到原工作区位置;文件已删的已删除会话拒绝恢复(防"复活")。
|
|
42
48
|
|
|
43
49
|
## Remote API(`ctx.remote.sessionArchive`)
|
|
44
50
|
|
|
@@ -47,6 +53,10 @@ dsh plugin --profile web add @chaoset/session-archive
|
|
|
47
53
|
| `list()` | — | `{ items: ArchiveRow[] }` |
|
|
48
54
|
| `detail(sessionId)` | 会话 id | `{ sessionId, header, title, messageCount, messages, live }` |
|
|
49
55
|
| `delete(sessionIds[])` | id 数组 | `{ deleted, failed, removedFromArchive }` |
|
|
56
|
+
|
|
57
|
+
> `delete` 的 `removedFromArchive` 恒为 0:删除保留归档 ghost id(见上),
|
|
58
|
+
> 侧边栏不会重新显示已删除的会话;`unarchive` 的 `removedFromArchive` 为
|
|
59
|
+
> 实际从归档集合移除的 id 数。
|
|
50
60
|
| `unarchive(sessionIds[])` | id 数组 | `{ restored, removedFromArchive }` |
|
|
51
61
|
|
|
52
62
|
`ArchiveRow`:`{ sessionId, title, cwd, createdAt, updatedAt, size, live }`。
|
package/client/client.cjs
CHANGED
|
@@ -33,6 +33,7 @@ window.__ModuleLoader__.load({
|
|
|
33
33
|
".sa_actionDanger:hover:not(:disabled){background:var(--dsw-alias-state-error-primary)}",
|
|
34
34
|
".sa_actionDanger:disabled{opacity:.4}",
|
|
35
35
|
".sa_confirm{color:var(--dsw-alias-state-error-primary);border:1px solid var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent)}",
|
|
36
|
+
".sa_confirm:hover:not(:disabled){background:var(--dsw-alias-state-error-primary);color:#fff}",
|
|
36
37
|
".sa_body{flex:1;min-height:0;padding:4px 12px 12px;overflow-y:auto}",
|
|
37
38
|
".sa_empty{color:var(--dsw-alias-label-tertiary);margin:24px 0;text-align:center;font-size:12px;line-height:18px}",
|
|
38
39
|
".sa_error{color:var(--dsw-alias-state-error-primary);margin:8px 0;font-size:12px;line-height:18px}",
|
package/lib/index.mjs
CHANGED
|
@@ -14,10 +14,19 @@
|
|
|
14
14
|
* 2. detail(id) — readFrom(id, 0) 只读取会话事件:标题 + 文本消息
|
|
15
15
|
* (user/assistant),供面板"查看"展开。
|
|
16
16
|
* 3. delete(ids) — 批量彻底删除:live 会话拒绝;逐个删除持久化文件
|
|
17
|
-
* (locate() 定位)+
|
|
18
|
-
*
|
|
17
|
+
* (locate() 定位)+ 会话目录。删除后**保留**该会话在
|
|
18
|
+
* 归档集合中的 ghost id(不调用移除):宿主的
|
|
19
|
+
* archiveSession 只改归档注册表、不停止内存会话,web
|
|
20
|
+
* 客户端重连还会把旧 tab 恢复进内存——删除时若把 id
|
|
21
|
+
* 移出归档集合,仍挂在内存里的会话会因"不再归档"而
|
|
22
|
+
* 立刻重新出现在侧边栏对话列表(效果等同"恢复")。
|
|
23
|
+
* 保留 ghost id 后由 list() 的存在性过滤隐藏,归档面板
|
|
24
|
+
* 与侧边栏都不会再显示该会话。
|
|
25
|
+
* 4. unarchive(ids) — 批量恢复:仅从归档集合移除**仍存在持久化文件**的
|
|
26
|
+
* 会话 id(文件已删的 ghost id 拒绝恢复,防止已彻底
|
|
27
|
+
* 删除的会话"复活"回侧边栏);会话数据不动。
|
|
19
28
|
*
|
|
20
|
-
* 归档集合(workspaceRegistry.archivedSessionIds
|
|
29
|
+
* 归档集合(workspaceRegistry.archivedSessionIds)的写入没有官方 API,
|
|
21
30
|
* 这里复用 registry 自身的串行化写入通道(enqueueOperation → requireState
|
|
22
31
|
* → setState,与 archiveSession 相同的路径);若 registry 内部形状变化,
|
|
23
32
|
* 自动降级为"仅删文件",归档列表会以存在性过滤幽灵 id,功能仍正确。
|
|
@@ -101,10 +110,29 @@ export function createArchiveHost(ctx, cfg) {
|
|
|
101
110
|
const registry = ctx.workspaceRegistry;
|
|
102
111
|
const persistence = ctx.sessionPersistence;
|
|
103
112
|
|
|
104
|
-
/**
|
|
105
|
-
const
|
|
113
|
+
/** 当前归档集合快照。 */
|
|
114
|
+
const archivedSet = () => new Set(
|
|
115
|
+
typeof registry.archivedSessionIds === 'function'
|
|
116
|
+
? registry.archivedSessionIds()
|
|
117
|
+
: (Array.isArray(registry.archivedSessionIds) ? registry.archivedSessionIds : []),
|
|
118
|
+
);
|
|
106
119
|
|
|
107
|
-
/**
|
|
120
|
+
/** 会话是否"活跃"(不能安全删除)。
|
|
121
|
+
* 宿主的 archiveSession 只改归档注册表、不停止内存会话,web 客户端
|
|
122
|
+
* 重连还会把旧 tab 恢复进内存——归档会话因此长期"内存存在"。但归档
|
|
123
|
+
* 会话已从会话列表移除、无法继续对话,不会再写持久化文件,删除安全;
|
|
124
|
+
* 若把内存存在当作 live,归档面板会永远显示"运行中"且无法勾选删除。
|
|
125
|
+
* 因此 live 仅对"内存存在且**未归档**"的会话为真(防将来误用),
|
|
126
|
+
* 归档面板中恒为 false。 */
|
|
127
|
+
const isLive = (sessionId) =>
|
|
128
|
+
ctx.sessions.get(sessionId) !== undefined && !archivedSet().has(sessionId);
|
|
129
|
+
|
|
130
|
+
/** 归档瞬间的生成流兜底:会话仍在内存且文件最近有写入(60s 内)时
|
|
131
|
+
* 视为忙碌——删除文件后进行中的请求会把半截日志 append 回来。 */
|
|
132
|
+
const isBusy = (sessionId, file) =>
|
|
133
|
+
file !== null && ctx.sessions.get(sessionId) !== undefined && Date.now() - file.mtimeMs < 60_000;
|
|
134
|
+
|
|
135
|
+
/** 从归档集合移除若干 id(恢复用;删除不调用——见 deleteArchived),返回实际移除的 id。 */
|
|
108
136
|
async function removeFromArchiveSet(ids) {
|
|
109
137
|
const set = new Set(ids);
|
|
110
138
|
const canWrite =
|
|
@@ -158,8 +186,7 @@ export function createArchiveHost(ctx, cfg) {
|
|
|
158
186
|
return {
|
|
159
187
|
/** 列出全部归档会话(存在性过滤:文件已删的幽灵归档记录不显示)。 */
|
|
160
188
|
async list() {
|
|
161
|
-
const
|
|
162
|
-
const archived = Array.isArray(rawArchived) ? rawArchived : [];
|
|
189
|
+
const archived = [...archivedSet()];
|
|
163
190
|
if (archived.length === 0) return { items: [] };
|
|
164
191
|
const headers = await persistence.list();
|
|
165
192
|
const byId = new Map(headers.map((header) => [header.id, header]));
|
|
@@ -206,7 +233,11 @@ export function createArchiveHost(ctx, cfg) {
|
|
|
206
233
|
|
|
207
234
|
/**
|
|
208
235
|
* 批量彻底删除归档会话。live 会话拒绝(先停止再删);每个会话删除
|
|
209
|
-
*
|
|
236
|
+
* 持久化文件与会话目录。删除后**不**从归档集合移除 id(保留 ghost id):
|
|
237
|
+
* 归档不停止内存会话,一旦把 id 移出归档集合,侧边栏(以"不在归档
|
|
238
|
+
* 集合中"作为显示条件)会立刻把仍在内存中的会话重新显示出来,等同
|
|
239
|
+
* "恢复"。ghost id 由 list() 的存在性过滤隐藏,面板与侧边栏均不再
|
|
240
|
+
* 显示该会话;`removedFromArchive` 因此恒为 0。
|
|
210
241
|
*/
|
|
211
242
|
async deleteArchived(sessionIds) {
|
|
212
243
|
const unique = [...new Set(sessionIds)];
|
|
@@ -220,7 +251,7 @@ export function createArchiveHost(ctx, cfg) {
|
|
|
220
251
|
const headers = await persistence.list();
|
|
221
252
|
const header = headers.find((item) => item.id === sessionId);
|
|
222
253
|
if (header === undefined) {
|
|
223
|
-
//
|
|
254
|
+
// 会话文件已不存在:幂等删除(ghost id 保留在归档集合中)。
|
|
224
255
|
deleted.push(sessionId);
|
|
225
256
|
continue;
|
|
226
257
|
}
|
|
@@ -229,6 +260,10 @@ export function createArchiveHost(ctx, cfg) {
|
|
|
229
260
|
deleted.push(sessionId);
|
|
230
261
|
continue;
|
|
231
262
|
}
|
|
263
|
+
if (isBusy(sessionId, file)) {
|
|
264
|
+
failed.push({ sessionId, reason: 'busy' });
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
232
267
|
try {
|
|
233
268
|
await rm(file.path, { force: true });
|
|
234
269
|
await rm(dirname(file.path), { recursive: true, force: true });
|
|
@@ -237,14 +272,26 @@ export function createArchiveHost(ctx, cfg) {
|
|
|
237
272
|
failed.push({ sessionId, reason: error && error.message ? String(error.message) : 'delete-failed' });
|
|
238
273
|
}
|
|
239
274
|
}
|
|
240
|
-
|
|
241
|
-
return { deleted, failed, removedFromArchive: removedIds.length };
|
|
275
|
+
return { deleted, failed, removedFromArchive: 0 };
|
|
242
276
|
},
|
|
243
277
|
|
|
244
|
-
/**
|
|
278
|
+
/**
|
|
279
|
+
* 批量恢复归档会话(仅从归档集合移除,会话数据不动)。只恢复仍存在
|
|
280
|
+
* 持久化文件的会话:文件已删的 ghost id(已彻底删除的会话)拒绝恢复,
|
|
281
|
+
* 避免删除后的会话再次出现在侧边栏对话列表。
|
|
282
|
+
*/
|
|
245
283
|
async unarchive(sessionIds) {
|
|
246
284
|
const unique = [...new Set(sessionIds)];
|
|
247
|
-
const
|
|
285
|
+
const headers = await persistence.list();
|
|
286
|
+
const restorable = [];
|
|
287
|
+
for (const sessionId of unique) {
|
|
288
|
+
const header = headers.find((item) => item.id === sessionId);
|
|
289
|
+
if (header === undefined) continue; // 持久化记录已删:拒绝恢复
|
|
290
|
+
const file = await fileInfo(header);
|
|
291
|
+
if (file === null) continue; // 有记录无文件:拒绝恢复
|
|
292
|
+
restorable.push(sessionId);
|
|
293
|
+
}
|
|
294
|
+
const restored = await removeFromArchiveSet(restorable);
|
|
248
295
|
return { restored, removedFromArchive: restored.length };
|
|
249
296
|
},
|
|
250
297
|
};
|
package/lib/remote.mjs
CHANGED
|
@@ -68,7 +68,8 @@ function runPendingMarks(instance) {
|
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
70
|
* 归档管理远程服务:list 列出归档会话;detail 读取会话内容;
|
|
71
|
-
* delete
|
|
71
|
+
* delete 批量删除归档会话(文件删除,归档集合保留 ghost id 防止内存会话
|
|
72
|
+
* 重现侧边栏);unarchive 批量恢复归档(仅限仍存在文件的会话)。
|
|
72
73
|
* 所有逻辑委托给 host 模块(lib/index.mjs 传入的 archiveHost)。
|
|
73
74
|
* @param ctx - 插件上下文。
|
|
74
75
|
* @param config - { host: archiveHost, serviceKey: 远程服务名 }。
|
package/package.json
CHANGED