@duke-dsh-plugins/dsh-archive-manager 1.4.0 → 1.5.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.
Files changed (3) hide show
  1. package/README.md +9 -1
  2. package/index.js +51 -6
  3. package/package.json +4 -4
package/README.md CHANGED
@@ -22,6 +22,7 @@
22
22
  > - **DSH < 0.1.7**:完整归档管理面板(侧栏底部"归档"按钮 → 分组列表 / 搜索 / 还原 / 删除)。
23
23
  > - **DSH ≥ 0.1.7**:DSH 自带归档管理(侧栏筛选"仅显示已归档"、行内取消归档),本插件**只补上它缺少的删除功能**——归档会话的 "…" 菜单里多一行"删除会话…"、行尾悬停多一个 🗑 按钮,点击弹出永久删除二次确认框;不再显示本插件自己的面板按钮。
24
24
  > - 识别方式是**特性检测**(0.1.7 新增的两个行操作槽位是否声明),不是版本号,因此任何中间版本都会落到正确的一侧。
25
+ > - **支持 DSH 0.1.x 与 0.2.x**。0.2.0 起 DSH 会校验插件声明的 `@deepseek-ai/dsh*` peer 版本范围,不匹配就**整个 bundle 静默跳过**(插件直接消失,控制台只多一行 `skipping profile bundle ...`);v1.5.0 已放行 0.2.x 并同时保留 0.1.x。
25
26
 
26
27
  ## 功能
27
28
 
@@ -94,6 +95,8 @@ dsh-archive-manager/
94
95
  ├── client.js # Client 半:归档管理 UI bundle
95
96
  ├── typert.host.js # Typert Host manifest(Remote 方法描述)
96
97
  ├── cordis.patch.yml # dsh bundle patch(挂载行)
98
+ ├── verify-*.mjs # 四个验证脚本(构造 / typert 线上契约 / client apply / 真实安装)
99
+ ├── scripts/ # check-profile-bundle.mjs:DSH 会不会跳过本插件的自检
97
100
  ├── .github/workflows/ # GitHub Actions 发布
98
101
  ├── AGENTS.md # 面向 AI agent 的开发指南(含踩坑)
99
102
  └── LICENSE # MIT
@@ -103,12 +106,17 @@ dsh-archive-manager/
103
106
 
104
107
  ```bash
105
108
  npm run check # node --check index.js client.js typert.host.js
106
- npm run verify # 三个验证脚本:构造 / 双版本 typert 线上契约 / client apply 冒烟
109
+ npm run verify # 四个验证脚本:构造 / 双版本 typert 线上契约 / client apply 冒烟 / 真实 DSH 安装
110
+ node scripts/check-profile-bundle.mjs --sandbox # 用真实 dsh-app-boot 确认本插件不会被跳过
107
111
  dsh plugin --profile web add /path/to/dsh-archive-manager # 安装/重装到本机 DSH profile
108
112
  ```
109
113
 
110
114
  `verify-typert-compat.mjs` 会用**真实 DSH 安装**里的 `validateTypertManifest` 与 `TypertRegistry` 校验本插件的线格式(同时覆盖 0.1.5 与 0.1.7 两套规则):默认探测本机 DSH 安装与 `.tmppkg/v017/`(npm 拉取的 0.1.7 包),也可用 `DSH_INSTALL_015` / `DSH_INSTALL_017` 环境变量指定。
111
115
 
116
+ `verify-real-install.mjs` 补的是另一个洞——前三个脚本跑在仓库自带的旧版依赖上,在"根本不加载插件"的 DSH 上也会全绿。它直接拿本机 DSH 安装的 `dsh-app-boot.evaluatePluginCompatibility` 跑 0.2.0 的 peer 闸门、断言 0.1.5/0.1.7/当前运行时三条版本线都满足、把 `index.js` 重新绑定到该安装的 cordis/typert-protocol 上构造服务,并在 0.1.x 与 0.2.x 两种磁盘布局上各真删一次会话。
117
+
118
+ `scripts/check-profile-bundle.mjs --sandbox` 会建一个临时 profile、用软链把**本工作树**挂进去,再调用真实 dsh-app-boot 的 profile 加载器,直接回答"dsh 会不会跳过我"。它不碰任何 DSH home,所以随时可跑。
119
+
112
120
  详见 [AGENTS.md](AGENTS.md)——记录了 DSH 正式插件(Host/Client/Typert 三件套)的完整机制和踩坑。
113
121
 
114
122
  ## License
package/index.js CHANGED
@@ -23,8 +23,9 @@
23
23
 
24
24
  import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
25
25
  import { Service } from "@deepseek-ai/cordis";
26
- import { existsSync } from "node:fs";
26
+ import { existsSync, statSync } from "node:fs";
27
27
  import { rm } from "node:fs/promises";
28
+ import { basename, dirname, extname } from "node:path";
28
29
 
29
30
  /**
30
31
  * Mark one instance method as a Remote export without relying on decorator
@@ -71,6 +72,36 @@ function readArchived(domain) {
71
72
  return [];
72
73
  }
73
74
 
75
+ /**
76
+ * Resolve the session directory that owns a session's log artifact.
77
+ *
78
+ * `sessionPersistence.locate(header).path` changed meaning across DSH majors:
79
+ * - DSH < 0.2.0: the session DIRECTORY (`…/<project>/<sessionId>/`), which is
80
+ * what `rm(..., { recursive: true })` was written against.
81
+ * - DSH >= 0.2.0 (`dsh-session-persistence-jsonl`): the current generation's
82
+ * log FILE inside that directory (`…/<project>/<sessionId>/session.v4.jsonl.zstd`).
83
+ * Removing only that file would leave the directory and any older generation
84
+ * (`session.v3.jsonl.zstd`) behind, so a restart could resurrect the session —
85
+ * exactly what "永久删除" must not do.
86
+ *
87
+ * So: prefer `stat` on the path itself; when the artifact is already gone, fall
88
+ * back to the name shape — a located log file always carries an extension
89
+ * (`session.v4.jsonl.zstd`), while a per-session directory never does.
90
+ *
91
+ * @param {string} locatedPath - `sessionPersistence.locate(header).path`.
92
+ * @returns {string} the directory to remove.
93
+ */
94
+ function deletionTarget(locatedPath) {
95
+ const target = String(locatedPath);
96
+ try {
97
+ if (statSync(target).isDirectory()) return target;
98
+ if (statSync(target).isFile()) return dirname(target);
99
+ } catch {
100
+ /* artifact already gone — infer from the path shape below */
101
+ }
102
+ return extname(basename(target)) === "" ? target : dirname(target);
103
+ }
104
+
74
105
  export class ArchiveManagerService extends TypertRemoteService {
75
106
  static inject = [
76
107
  "workspaceRegistry",
@@ -194,9 +225,20 @@ export class ArchiveManagerService extends TypertRemoteService {
194
225
  return { ok: false, error: { code: "agent-running", message: "该会话的智能体正在运行中,请等待其完成或停止后再删除" } };
195
226
  }
196
227
  const header = await this.findHeader(sessionId);
228
+ const sessionPersistence = this.ctx.get("sessionPersistence");
229
+ if (header && (!sessionPersistence || typeof sessionPersistence.locate !== "function")) {
230
+ // The header exists but this build cannot tell us where its log lives.
231
+ // Claiming success here would silently keep the session on disk.
232
+ return {
233
+ ok: false,
234
+ error: {
235
+ code: "locate-unavailable",
236
+ message: "当前 DSH 版本的会话存储未提供 locate(),无法定位会话日志目录,已保留归档条目",
237
+ },
238
+ };
239
+ }
197
240
  let location;
198
241
  if (header) {
199
- const sessionPersistence = this.ctx.get("sessionPersistence");
200
242
  try {
201
243
  location = sessionPersistence.locate(header);
202
244
  } catch {
@@ -206,7 +248,7 @@ export class ArchiveManagerService extends TypertRemoteService {
206
248
  let removed = false;
207
249
  let fileError;
208
250
  if (location && location.path) {
209
- const target = String(location.path);
251
+ const target = deletionTarget(location.path);
210
252
  try {
211
253
  // Delete straight from the host process via node:fs — no shell, no
212
254
  // PowerShell, no sandbox escalation, so this behaves identically on
@@ -214,7 +256,8 @@ export class ArchiveManagerService extends TypertRemoteService {
214
256
  // the host's own privileges, which makes the sandbox ACL dance the
215
257
  // previous shell-based implementation needed unnecessary. maxRetries
216
258
  // absorbs transient Windows file locks (antivirus / indexers) and is
217
- // a no-op elsewhere.
259
+ // a no-op elsewhere. The whole owning session directory goes, so older
260
+ // format generations cannot survive the delete (DSH >= 0.2.0).
218
261
  await rm(target, { recursive: true, force: true, maxRetries: 3, retryDelay: 200 });
219
262
  removed = !existsSync(target);
220
263
  if (!removed) fileError = "path-still-exists";
@@ -263,8 +306,10 @@ export class ArchiveManagerService extends TypertRemoteService {
263
306
  const session = sessions.get(id);
264
307
  if (session === undefined) continue; // not live in memory → not a ghost
265
308
  const location = sessionPersistence.locate(session.header);
266
- // Live but the log directory is gone → ghost the client hides.
267
- if (location && location.path && !existsSync(String(location.path))) ghostIds.push(id);
309
+ // Live but the log is gone → ghost the client hides. Probed against the
310
+ // owning session directory, i.e. exactly what `delete` removes: on DSH
311
+ // >= 0.2.0 `locate` names the current generation's file inside it.
312
+ if (location && location.path && !existsSync(deletionTarget(location.path))) ghostIds.push(id);
268
313
  } catch {
269
314
  /* skip */
270
315
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duke-dsh-plugins/dsh-archive-manager",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Archive manager for DeepSeek Harness: DSH < 0.1.7 gets the grouped search/restore/delete panel; DSH >= 0.1.7 adds the delete action missing from the built-in archive management.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -36,14 +36,14 @@
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@deepseek-ai/cordis": "^4.0.1",
39
- "@deepseek-ai/dsh-storage-domain": "^0.1.0-rc.6",
40
- "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.6"
39
+ "@deepseek-ai/dsh-storage-domain": "^0.1.0-rc.6 || ^0.2.0-rc.1",
40
+ "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.6 || ^0.2.0-rc.1"
41
41
  },
42
42
  "dependencies": {
43
43
  "zod": "^4.4.3"
44
44
  },
45
45
  "scripts": {
46
46
  "check": "node --check index.js && node --check client.js && node --check typert.host.js",
47
- "verify": "node verify-constructor.mjs && node verify-typert-compat.mjs && node verify-client-apply.mjs"
47
+ "verify": "node verify-constructor.mjs && node verify-typert-compat.mjs && node verify-client-apply.mjs && node verify-real-install.mjs"
48
48
  }
49
49
  }