@lijian-ui/dsh-session-cleaner 0.1.0 → 0.1.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/lib/index.js CHANGED
@@ -5,25 +5,48 @@ import * as fs from "fs";
5
5
  import * as path from "path";
6
6
  import { homedir } from "os";
7
7
  import { zstdDecompressSync } from "node:zlib";
8
+
8
9
  //#region src/session-store.ts
9
10
  /**
10
- * 会话存储层(session-cleaner host half)
11
- * ------------------------------------------------------------------
12
- * 直接操作 dsh 官方文件层(无需官方 API):
13
- *
14
- * sessions 树: ~/.dsh/sessions/<encoded-workspace>/<sessionId>/session.jsonl.zstd
15
- * workspace 状态:~/.dsh/storages/workspace.json
16
- * - global.archivedSessionIds ← 归档状态(与官方 UI 共享)
17
- * - tables.workspaces[*].sessionIds ← 会话归属
18
- * projcache: ~/.dsh/storages/session_projcache.json(删除时必须清,否则旧投影残留)
19
- *
20
- * 删除 = 三处清理:session 目录 + projcache + workspace.json(sessionIds/archivedSessionIds)。
21
- * 归档/恢复 = 读写 workspace.json 的 global.archivedSessionIds(单真相源与官方打通)。
22
- *
23
- * ⚠️ 已知边界:归档/恢复直接改文件,host 内存态不会热更新——插件页面以文件为准(准确),
24
- * 官方 UI 的隐藏/恢复在 dsh 重启后同步。删除同理(已删除的会话从文件层消失,官方 UI
25
- * 重启后不再列出)。
11
+
12
+ * dsh 会话 ID ↔ 文件系统目录名 编解码(与 dsh-session-persistence-jsonl 的 encodeSegment 对齐)。
13
+
14
+ * dsh 把 sessionId 中的非 [A-Za-z0-9._-] 字符编码为 ~XXXX(charCode 的 4 位十六进制大写)。
15
+
16
+ * 例如 `im:qq-s5ey:c2c:...` → `im~003Aqq-s5ey~003Ac2c~003A...`。
17
+
18
+ * session-cleaner 扫描文件系统得到编码后的目录名,需要解码回原始 ID 才能与 workspace.json 的 sessionIds 匹配。
19
+
26
20
  */
21
+ function encodeSegment(raw) {
22
+ if (raw.length === 0) return raw;
23
+ if (raw === ".") return "~002E";
24
+ if (raw === "..") return "~002E~002E";
25
+ let out = "";
26
+ for (let i = 0; i < raw.length; i++) {
27
+ const code = raw.charCodeAt(i);
28
+ const ch = String.fromCharCode(code);
29
+ if (ch !== "~" && /^[A-Za-z0-9._-]$/.test(ch)) out += ch;
30
+ else out += "~" + code.toString(16).toUpperCase().padStart(4, "0");
31
+ }
32
+ return out;
33
+ }
34
+ function decodeSegment(encoded) {
35
+ let out = "";
36
+ for (let i = 0; i < encoded.length;) {
37
+ if (encoded[i] === "~" && i + 5 <= encoded.length) {
38
+ const hex = encoded.slice(i + 1, i + 5);
39
+ if (/^[0-9A-Fa-f]{4}$/.test(hex)) {
40
+ out += String.fromCharCode(parseInt(hex, 16));
41
+ i += 5;
42
+ continue;
43
+ }
44
+ }
45
+ out += encoded[i];
46
+ i++;
47
+ }
48
+ return out;
49
+ }
27
50
  /** dsh 用户数据目录(与桌面壳 buildDshEnv 的 DSH_HOME 保持一致) */
28
51
  function dshHome() {
29
52
  return process.env.DSH_HOME ?? path.join(homedir(), ".dsh");
@@ -77,8 +100,11 @@ function writeArchived(archived) {
77
100
  writeJsonAtomic(workspaceFile(), ws);
78
101
  }
79
102
  /**
103
+
80
104
  * 扫描全部 session(文件层权威)。
105
+
81
106
  * 遍历 ~/.dsh/sessions/<workspace>/<sessionId>/,结合 workspace.json 标注状态。
107
+
82
108
  */
83
109
  function listSessions() {
84
110
  const root = sessionsRoot();
@@ -86,7 +112,7 @@ function listSessions() {
86
112
  if (!fs.existsSync(root)) return out;
87
113
  const archived = readArchived();
88
114
  const ws = readWorkspace();
89
- const activeIds = /* @__PURE__ */ new Set();
115
+ const activeIds = new Set();
90
116
  for (const w of Object.values(ws.tables?.workspaces ?? {})) for (const id of w.sessionIds ?? []) activeIds.add(id);
91
117
  for (const workspaceDir of fs.readdirSync(root, { withFileTypes: true })) {
92
118
  if (!workspaceDir.isDirectory()) continue;
@@ -106,13 +132,13 @@ function listSessions() {
106
132
  continue;
107
133
  }
108
134
  out.push(stripUndefined({
109
- id: entry.name,
135
+ id: decodeSegment(entry.name),
110
136
  workspaceDir: workspaceDir.name,
111
137
  dir,
112
138
  sizeBytes,
113
139
  mtimeMs,
114
- archived: archived.has(entry.name),
115
- active: activeIds.has(entry.name),
140
+ archived: archived.has(decodeSegment(entry.name)),
141
+ active: activeIds.has(decodeSegment(entry.name)),
116
142
  title: readSessionTitle(logFile)
117
143
  }));
118
144
  }
@@ -136,11 +162,12 @@ function removeFromProjcache(id) {
136
162
  if (!fs.existsSync(file)) return;
137
163
  const data = readJson(file, null);
138
164
  if (data === null) return;
165
+ const encodedId = encodeSegment(id);
139
166
  const walk = (o) => {
140
167
  if (!o || typeof o !== "object") return false;
141
168
  let changed = false;
142
169
  for (const k of Object.keys(o)) {
143
- if (String(k).includes(id)) {
170
+ if (String(k).includes(id) || String(k).includes(encodedId)) {
144
171
  delete o[k];
145
172
  changed = true;
146
173
  continue;
@@ -152,16 +179,21 @@ function removeFromProjcache(id) {
152
179
  if (walk(data)) writeJsonAtomic(file, data);
153
180
  }
154
181
  /**
182
+
155
183
  * 真删除会话(不可恢复,调用方必须已二次确认):
184
+
156
185
  * ① 删会话目录;② 清 projcache;③ 清 workspace.json(sessionIds + archivedSessionIds)。
186
+
157
187
  * 返回已删除的目录路径。
188
+
158
189
  */
159
190
  function deleteSession(id) {
160
191
  const root = sessionsRoot();
192
+ const encodedId = encodeSegment(id);
161
193
  let target = null;
162
194
  if (fs.existsSync(root)) for (const workspaceDir of fs.readdirSync(root, { withFileTypes: true })) {
163
195
  if (!workspaceDir.isDirectory()) continue;
164
- const dir = path.join(root, workspaceDir.name, id);
196
+ const dir = path.join(root, workspaceDir.name, encodedId);
165
197
  if (fs.existsSync(path.join(dir, "session.jsonl.zstd"))) {
166
198
  target = dir;
167
199
  break;
@@ -198,9 +230,13 @@ function unarchiveSession(id) {
198
230
  return { ok: true };
199
231
  }
200
232
  /**
233
+
201
234
  * 多帧 zstd 解码:dsh 的 session 日志每次追加写一个独立 zstd 帧
235
+
202
236
  * (文件里多个 magic 头),Node 的 zstdDecompressSync 只解第一帧——
237
+
203
238
  * 必须按 magic 位置逐帧解压拼接。
239
+
204
240
  */
205
241
  function zstdDecompressAll(input) {
206
242
  const magic = Buffer.from([
@@ -228,7 +264,8 @@ function zstdDecompressAll(input) {
228
264
  function readSessionLines(logFile) {
229
265
  try {
230
266
  if (!fs.existsSync(logFile)) return null;
231
- return zstdDecompressAll(fs.readFileSync(logFile)).toString("utf8").split("\n").filter(Boolean);
267
+ const raw = zstdDecompressAll(fs.readFileSync(logFile));
268
+ return raw.toString("utf8").split("\n").filter(Boolean);
232
269
  } catch {
233
270
  return null;
234
271
  }
@@ -255,14 +292,15 @@ function extractHeader(lines) {
255
292
  cwd: header.cwd
256
293
  };
257
294
  } catch {
258
- return;
295
+ return void 0;
259
296
  }
260
297
  }
261
298
  /** 从消息事件提取纯文本(宽容解析各种 content 形态) */
262
299
  function messageText(message) {
263
300
  if (typeof message === "string") return message;
264
301
  if (!message || typeof message !== "object") return "";
265
- const content = message["content"];
302
+ const m = message;
303
+ const content = m["content"];
266
304
  if (typeof content === "string") return content;
267
305
  if (Array.isArray(content)) return content.map((part) => {
268
306
  if (typeof part === "string") return part;
@@ -287,15 +325,19 @@ function readSessionTitle(logFile) {
287
325
  return extractTitle(lines);
288
326
  }
289
327
  /**
328
+
290
329
  * 读取会话预览:header 元数据 + 最近 N 条用户/助手消息文本。
330
+
291
331
  * 找不到文件抛错(调用方转 RPC 错误)。
332
+
292
333
  */
293
334
  function previewSession(id, limit = 12) {
294
335
  const root = sessionsRoot();
336
+ const encodedId = encodeSegment(id);
295
337
  let logFile = null;
296
338
  if (fs.existsSync(root)) for (const workspaceDir of fs.readdirSync(root, { withFileTypes: true })) {
297
339
  if (!workspaceDir.isDirectory()) continue;
298
- const candidate = path.join(root, workspaceDir.name, id, "session.jsonl.zstd");
340
+ const candidate = path.join(root, workspaceDir.name, encodedId, "session.jsonl.zstd");
299
341
  if (fs.existsSync(candidate)) {
300
342
  logFile = candidate;
301
343
  break;
@@ -331,6 +373,7 @@ function previewSession(id, limit = 12) {
331
373
  messages: messages.slice(-limit)
332
374
  });
333
375
  }
376
+
334
377
  //#endregion
335
378
  //#region src/remote.ts
336
379
  const SERVICE = "sessionCleanerRemote";
@@ -341,8 +384,11 @@ const codec = (typeSymbol) => ({
341
384
  schema: { parse: (v) => v }
342
385
  });
343
386
  /**
387
+
344
388
  * 注册到 `ctx.typert` 的 host 面契约。
389
+
345
390
  * face: 'host' + invocations(client 侧的 $mount 贡献则带 descriptors)。
391
+
346
392
  */
347
393
  const CONTRIBUTION = {
348
394
  package: PACKAGE,
@@ -428,8 +474,11 @@ const CONTRIBUTION = {
428
474
  }
429
475
  };
430
476
  /**
477
+
431
478
  * Remote 服务实现。构造时注册 `sessionCleanerRemote` cordis 服务,
479
+
432
480
  * 上面的贡献让 API gateway 能分发四个方法。
481
+
433
482
  */
434
483
  var SessionCleanerApi = class extends TypertRemoteService {
435
484
  constructor(ctx) {
@@ -461,6 +510,7 @@ function registerSessionCleanerRemote(ctx) {
461
510
  new SessionCleanerApi(ctx);
462
511
  ctx.effect(() => ctx.get("typert").register(CONTRIBUTION), "session-cleaner: typert contribution");
463
512
  }
513
+
464
514
  //#endregion
465
515
  //#region src/index.ts
466
516
  /**
@@ -493,17 +543,19 @@ function apply(ctx) {
493
543
  */
494
544
  function installConsoleLoggerExporter(ctx) {
495
545
  try {
496
- ctx.root.logger?.exporter?.({
546
+ const logger = ctx.root.logger;
547
+ logger?.exporter?.({
497
548
  colors: 0,
498
549
  export: (message) => {
499
550
  const { name = "session-cleaner", type = "log", args = [] } = message;
500
- const line = `[${name}] ${args.map((arg) => arg instanceof Error ? arg.stack ?? arg.message : typeof arg === "string" ? arg : (() => {
551
+ const rendered = args.map((arg) => arg instanceof Error ? arg.stack ?? arg.message : typeof arg === "string" ? arg : (() => {
501
552
  try {
502
553
  return JSON.stringify(arg);
503
554
  } catch {
504
555
  return String(arg);
505
556
  }
506
- })()).join(" ")}`;
557
+ })()).join(" ");
558
+ const line = `[${name}] ${rendered}`;
507
559
  if (type === "error") console.error(line);
508
560
  else if (type === "warn") console.warn(line);
509
561
  else if (type === "debug") console.debug(line);
@@ -512,5 +564,6 @@ function installConsoleLoggerExporter(ctx) {
512
564
  });
513
565
  } catch {}
514
566
  }
567
+
515
568
  //#endregion
516
- export { Config, apply, inject };
569
+ export { Config, apply, inject };
package/package.json CHANGED
@@ -1,87 +1,87 @@
1
- {
2
- "name": "@lijian-ui/dsh-session-cleaner",
3
- "version": "0.1.0",
4
- "description": "Session management plugin for DeepSeek Harness (dsh): list / delete / archive & restore sessions, with a built-in \"Archived\" group and a delete confirm dialog (real delete, not trash). 为 DeepSeek Harness 提供会话管理:列表 / 真删除(二次确认)/ 归档分组与恢复。",
5
- "license": "MIT",
6
- "type": "module",
7
- "main": "lib/index.js",
8
- "types": "lib/index.d.ts",
9
- "exports": {
10
- ".": {
11
- "types": "./lib/index.d.ts",
12
- "default": "./lib/index.js"
13
- },
14
- "./client": {
15
- "default": "./lib/client.js"
16
- },
17
- "./cordis.patch.yml": "./cordis.patch.yml",
18
- "./package.json": "./package.json"
19
- },
20
- "files": [
21
- "lib/**/*.js",
22
- "lib/**/*.d.ts",
23
- "cordis.patch.yml",
24
- "README.md",
25
- "README.en.md",
26
- "LICENSE"
27
- ],
28
- "scripts": {
29
- "build": "tsdown",
30
- "watch": "tsdown --watch",
31
- "typecheck": "tsc --noEmit",
32
- "prepare": "npm run build"
33
- },
34
- "keywords": [
35
- "deepseek-harness",
36
- "dsh",
37
- "dsh-plugin",
38
- "session",
39
- "session-cleaner",
40
- "archive",
41
- "delete",
42
- "cordis"
43
- ],
44
- "repository": {
45
- "type": "git",
46
- "url": "git+https://github.com/lijian-ui/dsh-session-cleaner.git"
47
- },
48
- "dsh": {
49
- "bundle": {
50
- "patch": "./cordis.patch.yml"
51
- },
52
- "client": {
53
- "inject": [
54
- "@deepseek-ai/dsh-client-runtime",
55
- "@deepseek-ai/dsh-client-locale",
56
- "@deepseek-ai/dsh-api-gateway"
57
- ],
58
- "platform": "web",
59
- "immediately": true
60
- }
61
- },
62
- "peerDependencies": {
63
- "@deepseek-ai/cordis": "*",
64
- "@deepseek-ai/schemastery": "*",
65
- "@deepseek-ai/dsh-settings": "*",
66
- "@deepseek-ai/dsh-typert-protocol": "*"
67
- },
68
- "devDependencies": {
69
- "@deepseek-ai/cordis": "4.0.1",
70
- "@deepseek-ai/schemastery": "3.18.1",
71
- "@deepseek-ai/dsh-settings": "0.1.0-rc.6",
72
- "@deepseek-ai/dsh-typert-protocol": "0.1.0-rc.6",
73
- "@deepseek-ai/dsh-client-connection": "0.1.0-rc.6",
74
- "@deepseek-ai/dsh-client-locale": "0.1.0-rc.6",
75
- "@deepseek-ai/dsh-client-runtime": "0.1.0-rc.6",
76
- "@deepseek-ai/dsh-client-schema-form": "0.1.0-rc.6",
77
- "@deepseek-ai/dsh-client-ui-slots": "0.1.0-rc.6",
78
- "@deepseek-ai/dsh-client-ui-settings": "0.1.0-rc.6",
79
- "@deepseek-ai/dsh-client-ui-settings-plugins": "0.1.0-rc.6",
80
- "@deepseek-ai/dsh-api-gateway": "0.1.0-rc.6",
81
- "@types/node": "^22.0.0",
82
- "@types/react": "^18.3.0",
83
- "react": "^18.2.0",
84
- "tsdown": "^0.12.0",
85
- "typescript": "^5.6.0"
86
- }
87
- }
1
+ {
2
+ "name": "@lijian-ui/dsh-session-cleaner",
3
+ "version": "0.1.2",
4
+ "description": "Session management plugin for DeepSeek Harness (dsh): list / delete / archive & restore sessions, with a built-in \"Archived\" group and a delete confirm dialog (real delete, not trash). 为 DeepSeek Harness 提供会话管理:列表 / 真删除(二次确认)/ 归档分组与恢复。",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "lib/index.js",
8
+ "types": "lib/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./lib/index.d.ts",
12
+ "default": "./lib/index.js"
13
+ },
14
+ "./client": {
15
+ "default": "./lib/client.js"
16
+ },
17
+ "./cordis.patch.yml": "./cordis.patch.yml",
18
+ "./package.json": "./package.json"
19
+ },
20
+ "files": [
21
+ "lib/**/*.js",
22
+ "lib/**/*.d.ts",
23
+ "cordis.patch.yml",
24
+ "README.md",
25
+ "README.en.md",
26
+ "LICENSE"
27
+ ],
28
+ "scripts": {
29
+ "build": "tsdown",
30
+ "watch": "tsdown --watch",
31
+ "typecheck": "tsc --noEmit",
32
+ "prepare": "npm run build"
33
+ },
34
+ "keywords": [
35
+ "deepseek-harness",
36
+ "dsh",
37
+ "dsh-plugin",
38
+ "session",
39
+ "session-cleaner",
40
+ "archive",
41
+ "delete",
42
+ "cordis"
43
+ ],
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "git+https://github.com/lijian-ui/dsh-session-cleaner.git"
47
+ },
48
+ "dsh": {
49
+ "bundle": {
50
+ "patch": "./cordis.patch.yml"
51
+ },
52
+ "client": {
53
+ "inject": [
54
+ "@deepseek-ai/dsh-client-runtime",
55
+ "@deepseek-ai/dsh-client-locale",
56
+ "@deepseek-ai/dsh-api-gateway"
57
+ ],
58
+ "platform": "web",
59
+ "immediately": true
60
+ }
61
+ },
62
+ "peerDependencies": {
63
+ "@deepseek-ai/cordis": "*",
64
+ "@deepseek-ai/dsh-settings": "*",
65
+ "@deepseek-ai/dsh-typert-protocol": "*",
66
+ "@deepseek-ai/schemastery": "*"
67
+ },
68
+ "devDependencies": {
69
+ "@deepseek-ai/cordis": "4.0.1",
70
+ "@deepseek-ai/dsh-api-gateway": "0.1.0-rc.6",
71
+ "@deepseek-ai/dsh-client-connection": "0.1.0-rc.6",
72
+ "@deepseek-ai/dsh-client-locale": "0.1.0-rc.6",
73
+ "@deepseek-ai/dsh-client-runtime": "0.1.0-rc.6",
74
+ "@deepseek-ai/dsh-client-schema-form": "0.1.0-rc.6",
75
+ "@deepseek-ai/dsh-client-ui-settings": "0.1.0-rc.6",
76
+ "@deepseek-ai/dsh-client-ui-settings-plugins": "0.1.0-rc.6",
77
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.0-rc.6",
78
+ "@deepseek-ai/dsh-settings": "0.1.0-rc.6",
79
+ "@deepseek-ai/dsh-typert-protocol": "0.1.0-rc.6",
80
+ "@deepseek-ai/schemastery": "3.18.1",
81
+ "@types/node": "^22.0.0",
82
+ "@types/react": "^18.3.0",
83
+ "react": "^18.2.0",
84
+ "tsdown": "^0.12.0",
85
+ "typescript": "^5.6.0"
86
+ }
87
+ }
File without changes