@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.
- package/README.md +9 -1
- package/index.js +51 -6
- 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 #
|
|
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 =
|
|
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
|
|
267
|
-
|
|
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.
|
|
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
|
}
|