dsh-conversation-recycle-bin 0.0.0-stage → 0.2.1

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/CHANGELOG.md ADDED
@@ -0,0 +1,136 @@
1
+ # Changelog
2
+
3
+ All notable changes to `dsh-conversation-recycle-bin` are documented here.
4
+ The format follows Keep a Changelog; the project uses Semantic Versioning.
5
+
6
+ ## [0.2.1] — 2026-10-18
7
+
8
+ ### Fixed
9
+
10
+ - **包元数据缺少 `repository`**:`package.json` 里没有 `repository` / `homepage` / `bugs` / `author`,npm 页面因此没有源码链接;更关键的是 awesome-dsh-plugin 的 `scripts/probe-npm.mjs` 只在 npm 包的 `repository` 字段回指同一个 GitHub 仓库时才把包写进 `data/npm-map.json`,缺了它市场拿不到 npm 安装源。四个字段已补齐,代码与运行时行为无变化。
11
+
12
+ ## [0.2.0] — 2026-10-17
13
+
14
+ ### Changed
15
+
16
+ - **Renamed** the package from `dsh-delete-conversation` to `dsh-conversation-recycle-bin`: the old npm name is owned by another author (`jiuling`, 1.0.1, a different implementation). The Client module id, the Cordis row id (`conversation-recycle-bin`), the HTTP prefix (`/conversation-recycle-bin/api`), the private header and all docs now use the new name.
17
+ - **Compatibility:** durable data stays under `<DSH_HOME>/delete-conversation/`, so existing recycle-bin entries and the audit trail remain visible after the rename; the storage namespace is intentionally not migrated. A profile must replace the old bundle entry with the new package name and restart DSH.
18
+
19
+ ### Fixed
20
+
21
+ - **记忆计数自相矛盾**:`preview.memory.adapters[].count` 用 `Object.values(Map)` 求和(对 `Map` 恒为 `[]`),所以每个可用适配器都显示 0 条、而 `memory.total` 却非 0。现在按填充 `bySession` 的同一批查找累加,`Σ adapters[].count === total`。
22
+ - **失败的删除永久卡在回收站**:删除含记忆的会话时,记忆阶段回滚成功后 bundle 目录仍留在条目里,`POST /trash/discard` 因此判定「仍持有记忆文件」而拒绝,而 `restore`(status ≠ `trashed`)与 `purge`(status ∉ `trashed/restored`)也拒绝 —— 条目再也无法清理。现在 `restoreEntry` 在数据放回后删除已消费的 bundle 目录,未完成条目恢复可清理。
23
+ - **`quiesce` 失败会把会话静默归档**:live 会话在 `liveDrainMs` 内停不下来时,`quiesce` 在抛错前尚未登记逆操作,整笔回滚后会话仍处于「已归档」(活动已停、会话列表隐藏)状态。现在归档的逆操作在等待之前登记,超时先撤销归档再抛 `E_POLICY_LIVE`。
24
+ - **catalog 半途失败留下旁路修改**:hard 模式 `remove()` 依次执行 unarchive → unpin → detach,某个后续步骤失败时没有任何逆操作,回滚后会话仍处于「已取消归档/已取消固定」,等于一次失败的删除悄悄把会话放回了列表。现在每完成一步即登记该步的逆操作,失败时按逆序先还原。
25
+ - **未完成条目本来无法恢复**:`performRestore` 只接受 `trashed`,但 `discard` 的提示语却说「请改用恢复或彻底删除」,构成死路。现在 `pending / failed / restoring / purging` 也可恢复:只执行 manifest 仍记录着 payload 的步骤(例如日志或 catalog 缺失时只把记忆放回),失败时沿用原状态而不再升格为 `trashed`;确实已无 payload 的条目则明确拒绝并提示改用 `discard`。
26
+ - **客户端本地化判断不一致**:`t()` 对 locale id 做小写归一而 `errorText()` 没有,遇到 `ZH-CN` 这类混合大小写 id 时错误文案回退英文、同屏其他标签仍是中文。两者现在共用 `isZhLocale()`。
27
+ - **`GET /state` 泄露宿主绝对路径**:响应中的 `storage.{trashDir,auditFile,dataDir}` 未被任何客户端使用,且与本模块「客户端只拿计数与状态、不拿文件句柄」的契约相冲突,已移除。
28
+ - **locale 资源文案与实际行为不符**:`locale/{en,zh}.json` 的 `dialog.memoryCount` 写成「删除记忆」(实际是移入回收站)、`dialog.modeHardHint` 写成「从磁盘移除、回收站清理后不可恢复」(实际在回收站条目被清理前仍可恢复),已按实际语义更正。
29
+
30
+ ### Tests
31
+
32
+ - 84 checks (was 77); added per-adapter count/total consistency, failed-delete → discard cleanup, quiesce rollback + un-archive, catalog half-failure rollback, restore-from-incomplete-entry (3 cases), mixed-case locale text.
33
+
34
+ ## [0.1.5] — 2026-10-17
35
+
36
+ ### Fixed
37
+
38
+ - 批量**部分失败**不再被客户端当成「宿主拒绝」:`/delete` 完成即返回 `ok: true`(并附 `partial` 标志),`results` / `failures` 同时可见;客户端对旧宿主 `ok:false` + `results/failures` 的响应也按结果处理。此前成功的会话已删除、列表却不刷新,界面只显示英文 `The Host refused the request.`,`dialog.partial` 永远不可达。
39
+ - `/delete` 的拒绝保留真实策略码(`E_POLICY_LIVE` / `E_POLICY_PROTECTED` / `E_POLICY_ROOT` / `E_NOT_FOUND`…),不再一律写成 `E_POLICY_ORPHAN`,客户端因此能显示正确的中文原因。
40
+ - 批量上限与限流不再互相打架:新增 `limits.effectiveBatch = min(maxBatchSessions, 本分钟剩余额度)`,`/preview` 与 `/delete` 都按它校验;批量页超限时禁用按钮并提示,避免 21–50 的批量必然 429。额度为 0 时 `/preview` 直接返回带 `retryAfterMs` 的 429,不再让用户填完确认框才失败。
41
+ - 0.1.3 的无主锁修复补齐:等待预算只在锁为 0 字节时延长到 `ownerlessLockStaleMs + 500ms`(有内容的锁仍用调用方的超时),否则 2.5s 超时仍会早于 5s 判死;锁句柄写入失败时会关闭句柄并删除锁文件,不再遗留 0 字节锁。
42
+ - 回收站不再吞掉未完成条目:`GET /trash` 同时返回 `incomplete`(failed/pending/restoring/purging),设置页单独展示并可「清理」(`POST /trash/discard`,需输入 `DELETE`);清理前检查磁盘上的 `session/` 与 `memory/` 副本(不只信 manifest),仍保存数据的条目会被拒绝并提示改用恢复/彻底删除。本机此前 3 条 `failed` 条目对 UI 完全不可见。
43
+ - 客户端变更请求超时按批量大小计算(`30s + 5s × 数量`,上限 180s),不再固定 15s;超时提示明确说明"操作可能仍在后台继续"。此前 8 个 live 会话的硬删除会在服务端仍在执行时先报超时,且 token 已被消费。
44
+ - `ui.showRowAction` / `ui.showHeaderAction` 真正生效:客户端启动时读一次 `/state`,按标志隐藏「…」菜单项与头部按钮。
45
+ - 0.1.4 设置页「批量删除」在删除后不刷新:新增 mutation 通知,设置页订阅并重取会话/回收站;同时清理已不存在的选中项。
46
+ - 0.1.4 当前会话关闭依赖 `uiWorkspace.clearArchivedCurrent/clearMain`(宿主声明为 private):两者都不存在时回退整页刷新,不再留下"日志已被移走"的幽灵会话。
47
+ - 失败后重新预览保留已输入的确认短语(此前每次限流/过期都会清空,批量需重输数量)。
48
+ - `performRestore` 的内存步骤在回滚时不再调用 `undefined` 撤销函数。
49
+ - 新增 `E_POLICY_FOREIGN_WRITER` 与 `foreignWriterGraceMs`(默认 1500ms):硬删除移动日志前复检最新 mtime,日志仍在增长(另一个 DSH Host 正在写)则中止,降低桌面端与网页端同时运行时的写坏风险。
50
+ - `PLUGIN_VERSION` 改由 `lib/version.js` 提供并与 `package.json` 断言一致(此前长期停在 `0.1.0`);客户端补齐 `E_INTERNAL` / `E_NOT_FOUND` / `E_BAD_REQUEST` / `E_TRASH_*` / `E_FORBIDDEN_*` 等错误的中文文案。
51
+ - 确认 token 超过 512 个时只淘汰最旧的,不再一次性清空所有打开中的计划。
52
+
53
+ ### Tests
54
+
55
+ - 77 checks; added partial-batch wire shape, real refusal code, effective batch limit, incomplete-entry listing/discard, foreign-writer guard (both directions), fresh ownerless lock, client partial payload, private-method reload fallback, UI-flag hiding, version consistency.
56
+
57
+ ## [0.1.4] — 2026-10-17
58
+
59
+ ### Changed
60
+
61
+ - 删除、恢复、回收站清理成功后不再整页刷新:Client 通过宿主 `sessions.refresh()` 原地拉取会话列表;若删掉的正是当前打开的会话,再用 `uiWorkspace.clearArchivedCurrent()` / `clearMain()` 关闭当前视图,回到空白页。恢复只刷新列表,不打断当前会话;回收站清理只刷新回收站本身。
62
+ - `window.location.reload()` 仅作为兜底:宿主缺少 client `sessions` 服务或刷新接口失败时才启用,避免升级到旧宿主后留下过期列表。
63
+
64
+ ### Tests
65
+
66
+ - Tests: 64 checks; added `client: updates the shell in place after a mutation`(原地刷新、关闭当前会话、恢复不打断、兜底重载四条路径)。
67
+
68
+ ## [0.1.3] — 2026-10-17
69
+
70
+ ### Fixed
71
+
72
+ - 彻底删除在「对话框显示未找到关联记忆」时仍失败:灵枢适配器的 `exportSession()` 过去无论有没有要迁移的节点都会获取 `_index.json.lock`;灵枢遗留的 0 字节锁(例如 `_sustain` 写入异常退出)会让每次删除等待 2.5s 后报 `E_MEMORY_FAILED: The memory store is busy`。现在**先读索引、零个可删节点直接返回空 bundle,不取锁、不等待**;非空导出仍在锁内重新计算节点,保持并发安全。
73
+ - 无主锁(0 字节、无 pid)超过 `ownerlessLockStaleMs`(默认 5s)即判定为崩溃遗留并清除,不再被 `staleLockMs=120s` 长时间阻塞;带内容的锁仍是原有 120s 阈值。索引只有几十 KB,5s 不会误判正常写入。
74
+ - 删除失败的报错不再一闪而过:Client 的 `load()` 重新预览时会清空 `error`,导致失败横幅在重载后立刻消失、按钮看起来「没反应」。现在失败后的刷新使用 `{ preserveError: true }`,错误保留在对话框中。
75
+ - Tests: 63 checks; added `a conversation with no linked memory does not need the store lock` and `an ownerless stale lock is broken instead of blocking forever`.
76
+
77
+ ## [0.1.2] — 2026-10-17
78
+
79
+ ### Changed
80
+
81
+ - 「彻底删除」不再要求输入 `DELETE`(批量也不再输入数量):在确认框里点删除按钮即执行。Host 仍要求 `/preview` 签发的单次 token(绑定排序后的 sessionIds + includeMemory + mode,TTL 120s),因此过期、篡改、重放的计划仍会被拒绝;批量**软删除**仍输入数量,回收站「永久清除」仍输入 `DELETE`(不可逆操作)。
82
+ - Client 在 hard 模式下不再渲染确认短语输入框;live 会话「会先停止该会话」的警告保留。
83
+ - Tests: 61 checks; added `batch: permanent deletion is one click, with no batch-size phrase`, updated hard/live/rollback tests to delete without a phrase.
84
+
85
+ ## [0.1.1] — 2026-10-17
86
+
87
+ ### Fixed
88
+
89
+ - 彻底删除曾被 live 会话策略挡住:DSH 会在内存中保留最近用过的会话(`live: true`),即使空闲,hard 模式也直接返回 `E_POLICY_LIVE`,确认框因此禁用删除按钮,用户无法直接彻底删除。
90
+ - Hard 删除与回收站彻底清理现在先执行 `quiesce`:`sessionController.cancel` 取消当前轮次 → `workspaceRegistry.archiveSession({stopActivity:true})` 建立停止栅栏 → 在有界的 `liveDrainMs`(默认 2000ms)内轮询到 `running=false`,再移动日志;停不下来则整个事务回滚并报可读的 `E_POLICY_LIVE`。
91
+ - 配置项 `allowLiveSessionDelete` 由 `stopLiveSessions`(默认 `true`)与 `liveDrainMs`(默认 `2000`)取代;`stopLiveSessions: false` 仍可恢复严格拒绝。
92
+ - 确认框在 hard 模式选中 live 会话时显示「会先停止该会话」的警告,而不是禁用删除;live 会话的 hard 删除保留归档墓碑,防止已排队的唤醒在日志移走后重建会话(恢复回收站条目时清除)。
93
+
94
+ ## [0.1.0] — 2026-10-17
95
+
96
+ Initial implementation for DSH `0.2.1-rc.2` (Web profile).
97
+
98
+ ### Added
99
+
100
+ - Host + Web Client two-half bundle, zero build (plain ESM + `window.__ModuleLoader__`), installed through `plugin_manager install_bundle`.
101
+ - Delete entry points:
102
+ - `sidebar.workspaces.session.menu.item` row ("删除对话…", order 500) that closes the menu before opening the dialog;
103
+ - `conversation.session.header.actions` icon action (order 100);
104
+ - `settings.section` page (order 1300) with recycle bin, batch deletion and audit tabs;
105
+ - `shell.overlay` dialog host, so the dialog survives the menu item's unmount.
106
+ - Confirmation dialog with soft-delete default, memory checkbox default OFF, live memory count from `POST /preview`, protected-entry count, and typed second confirmation (batch size for batches, `DELETE` for permanent deletion).
107
+ - Transactional cascade `memory → messages → session/catalog` with reverse-order rollback per conversation; batch items are independent transactions.
108
+ - Recycle bin under `<DSH_HOME>/delete-conversation/trash/`:
109
+ - soft delete archives through `workspaceRegistry.archiveSession({stopActivity:true})` and keeps the log on disk;
110
+ - hard delete moves the session log directory into the bundle;
111
+ - restore/really-delete endpoints; purge requires the `DELETE` phrase.
112
+ - Memory adapters:
113
+ - `lingshu`: `_index.json` schema 2 adapter keyed by the node `session` field; protected/immutable nodes preserved; cross-node `edges/depends_on/derived_from` references stripped on export and restored on rollback; holds the store's `_index.json.lock` and writes the restore bundle before rewriting the index;
114
+ - `generic-json`: configurable JSON-index adapter for other local stores;
115
+ - `none`: honest unavailable fallback (reports a reason instead of a misleading zero).
116
+ - Safety and audit:
117
+ - same-origin check, private `x-conversation-recycle-bin: 1` header, loopback-only by default, body-size ceiling;
118
+ - sliding-window rate limit (default 20 deletions/minute) and `maxBatchSessions` (default 50);
119
+ - single-use confirmation tokens bound to `(sessionIds, includeMemory, mode)` with TTL;
120
+ - JSONL audit trail with success / rolled-back / rollback-incomplete / denied / restore / purge records and size-based compaction;
121
+ - policy refusals for protected ids, orphan sessions, subagent sessions, out-of-root workspaces and live-session hard deletion.
122
+ - No agent tool is registered: deletion remains a user-only authority.
123
+ - Configuration: `normalizeConfig` (strict defaults, safe fallbacks, warnings), `config.schema.json`, `cordis.patch.yml` defaults layer; deliberately no `Config` schema export (no schema-runtime dependency).
124
+ - Tests: 54 checks in seven files covering the required matrix (single delete, batch delete, memory checked, no permission, delete failure + rollback) plus manifest/locale/icon, a browser-free Client-half registration/render harness, and adapter round-trip tests.
125
+ - Docs: `README.md` (host contract, data model, cascade, safety, honest limitations), `INSTALL.md` (install, verification checklist, rollback, troubleshooting), this changelog.
126
+
127
+ ### Fixed
128
+
129
+ - Client bundle registration id changed from `delete-conversation` to the package name `dsh-delete-conversation`. The boot graph keys every client row by `package.json#name`; a bundle registering under another id was treated as "loaded without registering", re-executed from its one-resource URL, and aborted web boot with `client-modules: duplicate factory registration for "delete-conversation"` plus `dsh-delete-conversation: import failed`. `test/client-module.test.mjs` now asserts the registration id equals the package name.
130
+
131
+ ### Known limitations
132
+
133
+ - DSH exposes no official session-delete API; hard deletion is a plugin-owned offline move and must not run against a live writer.
134
+ - The 灵枢 adapter edits the md_cg index directly (with its lock) and does not write 灵枢's own `_deletions.jsonl` ledger; use `lingshu_cg op=forget` when that protocol is required.
135
+ - Hindsight (bank-scoped, disabled for DSH, no delete tool) and Mnemon documents (archive-only, cross-session) are intentionally not part of the default cascade and are reported as unsupported.
136
+ - The UI is validated at manifest/slot-contract level; visual acceptance requires a browser (see INSTALL §2.4).
package/INSTALL.md ADDED
@@ -0,0 +1,118 @@
1
+ # 安装与回滚(dsh-conversation-recycle-bin)
2
+
3
+ 本插件只使用 DSH 的公开扩展点:一个 bundle patch 行(Host)+ 一个 `dsh.client` Web 半(Client)。不需要 pnpm 安装依赖、不需要构建步骤。
4
+
5
+ - 包目录:`D:\Users\34332\AI\dsh-conversation-recycle-bin`
6
+ - 宿主 profile:当前 GUI 实际使用的 profile。本机实测为 `C:\Users\34332\.dsh\profiles\desktop`;若你在其他 profile(例如 `profiles\web`)里使用,`install_bundle` 也必须在该 profile 对应的会话中执行。
7
+ - 数据目录:`<DSH_HOME>/delete-conversation/`(默认 `C:\Users\34332\.dsh\delete-conversation`)。目录名沿用旧插件名,改名不会迁移已有回收站/审计数据,这是有意为之;不要因为插件改名去重命名该目录。
8
+
9
+ ## 1. 安装(推荐:Plugin Manager)
10
+
11
+ 在 DSH 会话中执行(需要 danger-full-access 或批准):
12
+
13
+ ```text
14
+ plugin_manager action=install_bundle target="D:\\Users\\34332\\AI\\dsh-conversation-recycle-bin"
15
+ ```
16
+
17
+ `install_bundle` 会完成:登记 profile 的 `dsh.profile.bundles`、解析本地包、按 `dsh.bundle.patch` 插入 Host 行、由 `dsh.client` 提供 Web 半。不要手写 profile 的 `package.json` 或 `cordis.patch.yml` 来代替它。
18
+
19
+ 安装结果以返回的 `application` 与 `warnings` 为准:
20
+
21
+ - `applied`:已生效(新的 bundle 通常经 HMR 激活);
22
+ - `restart-required`:重启 DSH 后生效;
23
+ - `failed` / `overridden`:先诊断,不要重复安装。
24
+
25
+ ## 2. 安装后验证
26
+
27
+ ### 2.1 Host 行与服务
28
+
29
+ ```text
30
+ cordis_inspect_query platform=host provider=Config method=listConfigs name=dsh-conversation-recycle-bin
31
+ ```
32
+
33
+ 应能看到 `conversation-recycle-bin` 行。该行的 `status` 为 `absent` 属于预期:本插件不导出 `Config` schema(不想依赖 schema 运行时),配置在 `apply()` 内由 `normalizeConfig` 校验,`cordis.patch.yml` 给出默认层。
34
+
35
+ ### 2.2 HTTP API
36
+
37
+ 浏览器或同源请求访问:
38
+
39
+ ```text
40
+ GET http://127.0.0.1:<Web 端口>/conversation-recycle-bin/api/state
41
+ ```
42
+
43
+ 端口以当前 GUI 地址栏为准(本机最近为 `8080`)。
44
+
45
+ 预期 `{"ok":true,"state":{...}}`,其中 `state.memory.adapters` 应显示 `lingshu available: true`(若本机存在 `.dsh-memory/data/mdcg`)。命令行走 `Invoke-WebRequest` 时若被同源策略拒绝属于预期;用浏览器 DevTools 或页面内请求验证。
46
+
47
+ ### 2.3 UI 槽位
48
+
49
+ ```text
50
+ cordis_inspect_query platform=client provider=Slots method=listSubTree input={"root":"sidebar.workspaces.session.menu.item"}
51
+ cordis_inspect_query platform=client provider=Slots method=listSubTree input={"root":"conversation.session.header.actions"}
52
+ cordis_inspect_query platform=client provider=Slots method=listSubTree input={"root":"settings.section"}
53
+ ```
54
+
55
+ 应出现 `registrant` 为 `conversation-recycle-bin`、id 为 `conversation-recycle-bin`、order 分别为 500 / 100 / 1300 的占用。
56
+
57
+ ### 2.4 手工验收清单(需要浏览器)
58
+
59
+ 1. 会话列表「…」菜单出现「删除对话…」,点击弹出确认框;
60
+ 2. 确认框默认「移入回收站」+ 记忆复选框未勾选;勾选后条数文案变化;
61
+ 3. 单个会话软删除:条目消失、设置页「回收站」出现条目;
62
+ 4. 回收站恢复:会话与记忆回到原位;
63
+ 5. 彻底删除:无需输入,点击删除按钮直接执行;回收站「永久清除」才需输入 `DELETE`;
64
+ 6. 批量删除:勾选 2 个以上会话,批量**软删除**必须输入数量;批量彻底删除无需输入;超过当前有效上限(`min(maxBatchSessions, 本分钟剩余额度)`)时按钮禁用并提示上限,超过限流返回可读的 429 文案;
65
+ 7. 无权限:对未登记工作区的会话,确认框显示不可删除原因;
66
+ 8. 设置页「审计留痕」能看到成功/失败/回滚记录;
67
+ 9. 若回收站下方出现「未完成条目」,点「清理」并输入 `DELETE` 应移除该条目;仍持有会话日志的条目会被拒绝并提示改用恢复/彻底删除。
68
+
69
+ > 无法控制浏览器时,安装与槽位注册只能证明「入口已挂载」,不能证明视觉效果;请如实记录这一限制。
70
+
71
+ ## 3. 回滚
72
+
73
+ ### 3.1 移除插件(推荐)
74
+
75
+ ```text
76
+ plugin_manager action=remove_bundle target=dsh-conversation-recycle-bin
77
+ ```
78
+
79
+ 返回 `restart-required` 时重启 DSH。移除后:
80
+
81
+ - 会话数据不受影响(插件从不删除未被用户确认的会话);
82
+ - 插件自己的数据目录**保留**:`<DSH_HOME>/delete-conversation/`。确认不再需要回收站后,可手动删除该目录释放空间;
83
+ - 如果移除前有未恢复的 hard 删除条目,先执行恢复:`POST /conversation-recycle-bin/api/trash/restore`(或先在 UI 恢复),否则该目录删除后无法再通过插件恢复。
84
+
85
+ ### 3.2 手工回退(仅在 Plugin Manager 不可用时)
86
+
87
+ 1. 从 `<DSH_HOME>/profiles/<profile>/package.json` 的 `dsh.profile.bundles` 中移除 `dsh-conversation-recycle-bin`;
88
+ 2. 删除/移除 profile `node_modules` 下对应的链接(若存在);
89
+ 3. 若曾手工编辑 `cordis.patch.yml`,删除 `conversation-recycle-bin` 行;
90
+ 4. 重启 DSH。
91
+
92
+ 不要在 Host 仍持有文件句柄时手工移动 `<DSH_HOME>/delete-conversation/trash/<id>/session/`;用恢复 API 或先停 DSH。
93
+
94
+ ## 4. 升级
95
+
96
+ 1. 修改 `D:\Users\34332\AI\dsh-conversation-recycle-bin`(覆盖安装目录后需重启以加载新的 Host 模块代际);
97
+ 2. 运行 `npm test` 与 `npm run check`;
98
+ 3. 重新 `install_bundle`(或 `set_bundle`)并重启;
99
+ 4. 升级后用一个**测试会话**跑一次单条软删除 + 回收站恢复,再投入日常使用。
100
+
101
+ ## 5. 故障排查
102
+
103
+ | 症状 | 原因 / 处理 |
104
+ |---|---|
105
+ | 菜单/头部/设置页没有入口 | Client 半未激活。检查 `dsh.client` 是否存在、`client.js` 是否可被宿主打包路由加载、`cordis_inspect_query Slots` 是否有 `conversation-recycle-bin` 占用;重启后重试 |
106
+ | 入口在但弹出 403 | 请求被同源/loopback 校验拒绝。确认页面地址与 `Host` 头一致;反向代理/远程访问需在配置里显式 `allowNonLoopback: true`(会放宽本机限制,谨慎) |
107
+ | `E_POLICY_ORPHAN` | 会话未登记到任何工作区。确属要删时在配置中 `allowOrphanSessions: true`,或先在工作区里登记 |
108
+ | `E_POLICY_LIVE` | 配置了 `stopLiveSessions: false` 且会话仍活跃;或插件在 `liveDrainMs` 内无法停住该会话(仍在运行)。改为默认配置,或等该会话空闲后重试 |
109
+ | `E_POLICY_FOREIGN_WRITER` | 另一个 DSH Host(桌面端或 `dsh web`)仍在写这个会话日志。先关闭那一端的会话再重试;`foreignWriterGraceMs` 只能降低窗口,不能替代关闭写入方 |
110
+ | `memory.adapters` 全部 unavailable | 本机没有可识别的记忆存储;检查 `<DSH_HOME>/.dsh-memory/data/mdcg/_index.json` 是否存在且 `schema === 2` |
111
+ | 删除返回 `E_TRANSACTION_FAILED` 且 `rolledBack: true` | 事务已回滚,数据未丢。看审计 `rollback.errors` 定位失败阶段(memory/messages/session) |
112
+ | 升级 DSH 后删除失败 | 先跑测试;确认会话目录仍为 `<DSH_HOME>/sessions/<slug>/<id>/`、`_index.json` 仍为 schema 2;必要时在测试会话上验证后再用 |
113
+
114
+ ## 6. 数据与隐私
115
+
116
+ - 审计日志包含操作时间、会话 id、标题、模式、是否勾选记忆、计数与结果,不包含对话正文;
117
+ - 回收站包含被移除的会话日志与记忆正文,位于用户自己的 `<DSH_HOME>` 下;
118
+ - 插件不向任何外部服务发送数据。
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dsh-conversation-recycle-bin contributors
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,224 @@
1
- # Temporary Holding Version
1
+ # dsh-conversation-recycle-bin
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 给 DeepSeek Harness (DSH) Web 增加「删除对话」:会话列表的「…」菜单与会话详情页头部各有一个删除入口;确认框内可勾选「同时删除关联记忆」(默认不勾选),并实时显示将被影响的记忆条数。删除默认进入插件回收站,可恢复;每次操作写入审计日志;批量删除有上限与限流,批量软删除需输入数量,彻底删除一键执行;事务任一步失败即回滚。
4
+
5
+ > 状态:`0.2.0`,Host + Web Client 双半,零构建(plain JS ESM),只依赖宿主已发布的公开扩展点。
6
+ > 包名:`dsh-conversation-recycle-bin`(原 `dsh-delete-conversation` 因 npm 被他人占用而改名,见 CHANGELOG 0.2.0)。
7
+ > 本 README 同时是设计说明与验收依据;安装/回滚步骤见 [INSTALL.md](INSTALL.md);变更见 [CHANGELOG.md](CHANGELOG.md)。
8
+
9
+ ---
10
+
11
+ ## 1. 宿主平台规范确认(第一步)
12
+
13
+ 以下结论来自对当前安装的 DSH `0.2.1-rc.2`(桌面端 desktop profile)的 `cordis_inspect_query` 与实现源码核对,不是假设:
14
+
15
+ | 项目 | DSH 事实 | 本插件的落点 |
16
+ |---|---|---|
17
+ | 插件单元 | 一个 bundle = `package.json` 的 `dsh.bundle.patch` + `cordis.patch.yml` 的 `- insert:` 行;行 `id` 是 Plugins 页与配置的稳定键 | `cordis.patch.yml` 插入一行 `id: conversation-recycle-bin` |
18
+ | 权限/配置声明 | 宿主校验服务访问靠 Cordis `inject`;行配置靠 `export const Config`(需要 schema 运行时依赖)。社区插件的无 schema 路径:在 `apply()` 里自校验并在 `/state` 暴露问题 | 无 `Config` 导出;`lib/core/config.js#normalizeConfig` 严格校验 + `config.schema.json` 文档 + `/state` 报告 |
19
+ | 服务注入 | Host:`export const inject = [...]`;可选服务用 `ctx.get(name)` 逐次探测。`webServer` 在插件行挂载时可能尚未发布,一次性 `ctx.get('webServer')` 会永久拿到 `undefined` | `inject = ['webServer']`(路由是产品本体);其余服务请求时探测,缺失返回可读错误 |
20
+ | UI 扩展 | 只通过 Slot:`ctx.slots.inject(key, () => ctx.slots.register(options, Component))`;Client 半必须 `exports.inject = ['slots']`,否则 `apply` 可能先于 slot 服务运行而静默不注册 | `client.js` 注册 4 个槽位(见 §3) |
21
+ | 会话列表入口 | `sidebar.workspaces.session.menu.item`(list / root):owner props `{sessionId, displayTitle}`,额外注入 `useMenuOpenState()`,用 `setOpen(false)` 关闭菜单 | 「…」菜单新增一行「删除对话…」 |
22
+ | 会话详情页入口 | `conversation.session.header.actions`(list / session):标准 props 直接含 `sessionId` | 头部新增一个删除图标按钮 |
23
+ | 对话框落点 | 菜单项随下拉菜单卸载;对话框必须挂在根级 overlay | `shell.overlay` 里的 `DialogHost`,菜单项只写模块级 dialog store |
24
+ | Client→Host | 生成式 `ctx.remote.*` 在构建期固定,纯 JS 插件无法新增命名空间;Client `ctx.connection` 没有 `fetch`;推荐 Host `ctx.webServer.register({kind:'prefix', path, handler})` + 同源 `fetch(document.baseURI)` | 自定义路由 `/conversation-recycle-bin/api/*` |
25
+ | 路由鉴权 | Web server 本身不鉴权;官方插件用 `ctx.connection.requestRejection(req)` 兜底。本插件另加:同源校验、loopback 默认、私有无头 header、JSON content-type、体积上限 | 见 `lib/host/api.js#requestOriginVerdict` |
26
+ | Agent 工具 | 用户授权类操作应保持 user-only(skill `user-actions`) | **不注册任何 agent tool**,模型无法执行或授权删除 |
27
+
28
+ **操作者模型**:DSH 是本地单用户应用,没有账号/角色系统。因此本插件把「操作者权限」落实在传输层(同源页面、默认仅 loopback、私有请求头)与数据归属策略(工作区登记、允许根目录、受保护 id、子代理来源、live 状态)上,而不是虚构一套用户体系。
29
+
30
+ ---
31
+
32
+ ## 2. 会话 / 消息 / 记忆的数据模型与级联(第二步)
33
+
34
+ ### 2.1 存储事实
35
+
36
+ | 实体 | 位置 / 主键 | 说明 |
37
+ |---|---|---|
38
+ | 会话 | `<DSH_HOME>/sessions/<project-slug>/<encoded-session-id>/`,主键 `SessionHeader.id`;当前日志 `session.v4.jsonl.zstd`(v0/v3 代际可能并存) | 没有会话目录索引文件;列表是目录扫描 + `workspace.json` 补充 |
39
+ | 消息 | **没有独立消息库**。消息是会话日志里的事件:`user/message`、`assistant/message`、`tool/result`、`system/message`、`developer/message`;持久键 `(session.id, event.seq)`,消息对象另带 UUID `id` | 删除消息 = 移走日志;恢复 = 移回 |
40
+ | 工作区账本 | `<DSH_HOME>/storages/workspace.json`:`tables.workspaces[wid].sessionIds[]`、`global.archivedSessionIds[]`、`global.pinnedSessionIds[]` | 官方 `workspaceRegistry` / `Workspace.*` 是唯一写入口 |
41
+ | 投影缓存 | `<DSH_HOME>/storages/session_projcache/sessions/<id>.json`(v7,权威);根级 `session_projcache.json` 是旧版残留 | 派生缓存,可自愈;硬删时尽力清理 |
42
+ | 灵枢记忆 | `<DSH_HOME>/.dsh-memory/data/mdcg/_index.json`(schema 2):`nodes.<id>.session` 就是会话 id;正文 `<layer>/<id>.md` | 本插件唯一默认的记忆存储适配器 |
43
+ | Hindsight / dsh-mnemon | Hindsight 为 bank 级且 DSH 处已禁用;Mnemon documents 有 `sessionIds[]` 但只有 archive、无 unarchive | 不纳入默认删除路径(理由见 §5.3) |
44
+
45
+ ### 2.2 关系
46
+
47
+ - **会话 1 : 1 日志**:一个 `session.id` 对应一个目录(多代际日志共存于同一目录)。
48
+ - **会话 1 : N 消息事件**:消息由事件派生;`seq` 是稳定顺序,`message.id` 是内容 id。
49
+ - **会话 1 : N 灵枢记忆节点**:`_index.json` 的 `nodes[*].session` 等于会话 id。一个节点只归属一个会话;但节点之间可有 `edges / depends_on / derived_from` 引用,因此删除一个会话的节点可能在其他会话的节点里留下悬空引用。
50
+ - **孤儿记忆归属**:某节点带 `session`、但该会话的日志已不存在时,**本插件永不自动删除它**。删除入口只会统计并移动「目标会话 id 精确匹配」的节点;跨会话的引用会被清掉并在回收站 bundle 里记录(恢复时原样补回),而孤立节点本身保留给用户/灵枢自行处理。
51
+ - **工作区归属**:`workspace.sessionIds[]` 只登记了一部分会话(本机 36/92)。未登记的会话默认拒绝删除(`E_POLICY_ORPHAN`),避免误删游离日志;可在配置中显式放开。
52
+
53
+ ### 2.3 级联顺序与事务
54
+
55
+ ```
56
+ memory → messages → session/catalog
57
+ │ │ │
58
+ │ │ └─ soft: workspaceRegistry.archiveSession(stopActivity)
59
+ │ │ hard: unarchive → unpin → workspace.detachSession
60
+ │ └─ hard: 把会话日志目录 move 到回收站;soft: 保留日志(软删除语义)
61
+ └─ 勾选时:灵枢节点 + 正文移入回收站 bundle,并记录被剥离的跨节点引用
62
+ ```
63
+
64
+ `soft` 模式有意不移动日志:它用宿主官方 archive 把会话从侧栏隐藏,磁盘内容保持完整,直到用户在回收站里「彻底删除」。`hard` 模式移动日志目录(回收站内仍可恢复);若会话仍被活跃 Agent 持有,会先取消当前轮次、`archiveSession({stopActivity:true})` 并在 `liveDrainMs`(默认 2000ms)内等待其停止,然后才移动日志。**移动前还会复检日志的最新 mtime**:桌面端与 `dsh web` 共享同一个 `DSH_HOME`,本进程的 live 表看不到另一个 Host 的写入;若日志在 `foreignWriterGraceMs`(默认 1500ms)窗口内仍在增长,就中止并报 `E_POLICY_FOREIGN_WRITER`(best-effort,不能替代"关闭另一端的会话")。两种模式都把条目写进插件回收站与审计。
65
+
66
+ 每次单会话删除是一个 `runSteps` 事务:每一步返回 undo,后续步骤失败时按**逆序**回滚;回滚失败会被收集并在错误详情与审计中标记 `rolledBack: false`,不会被吞掉。批量删除的每个会话各自一个事务,互不连坐。
67
+
68
+ ---
69
+
70
+ ## 3. 删除入口与确认框(第三步)
71
+
72
+ | 入口 | 槽位 | 行为 |
73
+ |---|---|---|
74
+ | 会话列表「…」 | `sidebar.workspaces.session.menu.item`(order 500) | 点击后关闭菜单,打开全局确认框 |
75
+ | 会话详情页头部 | `conversation.session.header.actions`(order 100) | 图标按钮打开同一确认框 |
76
+ | 设置页 | `settings.section`(order 1300) | 「回收站 / 批量删除 / 审计留痕」三个标签页 |
77
+ | 全局确认框 | `shell.overlay` | 唯一删除发起处;挂在根级,不会随菜单卸载 |
78
+
79
+ 确认框内容:
80
+
81
+ - 标题与影响说明(单个显示会话标题;批量显示数量);
82
+ - 删除方式单选:**默认「移入回收站」**,可选「彻底删除」;
83
+ - 复选框「同时删除该对话的关联记忆」:**默认不勾选**;勾选状态改变会重新向 Host 取计划,因此条数与确认 token 始终对应同一计划;
84
+ - 实时记忆条数:`N 条关联记忆`(未勾选时显示「找到 N 条」,勾选后显示「将把 N 条移入回收站」);不可用时明确写「记忆存储不可用:原因」,不会伪装成 0;
85
+ - 受保护节点单独显示「另有 N 条受保护记忆将保留」;
86
+ - 二次确认:批量**软删除**需输入数量;**彻底删除无需输入**,点击删除按钮即执行(Host 仍校验单次 token,过期、篡改、重放的计划会被拒绝);回收站「永久清除」仍需输入 `DELETE`;
87
+ - 失败时显示可读错误码/原因,并自动重新拉取计划(token 已消费或数据已变化时不会执行旧计划)。
88
+ - 删除/恢复成功后**不整页刷新**:Client 调 `sessions.refresh()` 原地更新会话列表;若删掉的是当前打开的会话,再关闭当前视图回到空白页。恢复只刷新列表,不打断当前会话;只有 client 半缺少 `sessions` 服务时才回退到 `location.reload()`。
89
+
90
+ ---
91
+
92
+ ## 4. 删除逻辑与安全机制(第四、五步)
93
+
94
+ ### 4.1 执行链(Host,`lib/host/api.js`)
95
+
96
+ 1. `POST /preview`:校验 session 存在、工作区归属、是否受保护/子代理/超出允许根;hard 模式下活跃会话不再拒绝,而是给出「会先停止该会话」的 warning(`stopLiveSessions:false` 时仍拒绝)。随后统计日志条数与记忆数,签发**单次使用**确认 token(绑定排序后的 sessionIds + includeMemory + mode,TTL 默认 120s)。
97
+ 2. `POST /delete`:
98
+ - 同源/loopback/私有 header 校验;
99
+ - token(彻底删除无需短语;批量软删除校验数量)、**有效批量上限** `min(maxBatchSessions, 本分钟剩余额度)`、限流(默认 20 次/分钟,滑动窗口)全部通过后才消费 token;
100
+ - 先对**全部**会话做策略预检,任一被拒绝则整批不执行(不会半成功);
101
+ - 逐会话执行 `quiesce(live) → memory → messages → session/catalog` 事务;
102
+ - 返回 `{ok: true, partial, results, failures}`:`ok` 表示计划已执行,`partial` 表示有会话失败;只有**全部**失败时才抛首个错误(HTTP 状态来自 `ERROR_STATUS`,错误码保留真实策略码)。
103
+ 3. `GET /trash`(返回 `entries`(可恢复)与 `incomplete`(failed/pending/restoring/purging))、`POST /trash/restore`、`POST /trash/purge`(必须输入 `DELETE`)、`POST /trash/discard`(清理未完成条目,必须输入 `DELETE`;仍保存会话日志的条目会被拒绝)。
104
+ 4. `GET /audit?limit=N`。
105
+
106
+ ### 4.2 安全清单
107
+
108
+ | 风险 | 措施 |
109
+ |---|---|
110
+ | 误删 | 默认软删除;记忆复选框默认关;回收站;彻底删除一键但仍写回收站可恢复;批量软删除需输入数量 |
111
+ | 批量误操作 | `maxBatchSessions`(实际上限还会被本分钟剩余额度收紧,避免"合法批量必然 429");批量需输入数量;每个会话独立事务 |
112
+ | 暴力/脚本调用 | 同源校验 + `x-conversation-recycle-bin: 1` 私有 header + 限流;默认仅接受 loopback 绑定,`allowNonLoopback=false` |
113
+ | 越权/越界 | 不在工作区登记的会话默认拒绝;可配置允许根目录白名单;受保护 id 列表;子代理会话默认拒绝 |
114
+ | 活跃会话被写坏 | hard 模式对 live 会话先走 `quiesce`:取消当前轮次 → `archiveSession({stopActivity:true})` → 在 `liveDrainMs` 内等待 `running=false`,之后才移动日志;仍停不下来则整笔回滚并报 `E_POLICY_LIVE`。`stopLiveSessions:false` 可恢复为直接拒绝 |
115
+ | 另一个 Host 正在写 | 移动前按 `foreignWriterGraceMs` 复检日志 mtime;仍在增长则报 `E_POLICY_FOREIGN_WRITER`。best-effort:两端同时运行时仍应先关闭另一端的会话 |
116
+ | 失败半删 | 每会话事务 + 逆序回滚;回滚失败进入错误详情和审计 |
117
+ | 不可追溯 | `<DSH_HOME>/delete-conversation/audit.jsonl` 追加成功/失败/回滚/拒绝/恢复/清理记录;设置页可查看 |
118
+ | 外部记忆存储损坏 | 灵枢适配器持有其 `_index.json.lock`,先写 restore bundle 再原子改写索引;任何一步失败先还原正文与索引;详情见 §5.3 |
119
+
120
+ ### 4.3 回收站布局
121
+
122
+ 存储目录沿用旧插件的 `delete-conversation`(不是新包名),这样 0.2.0 改名后已有的回收站条目和审计日志仍然可见。
123
+
124
+ ```
125
+ <DSH_HOME>/delete-conversation/
126
+ audit.jsonl
127
+ trash/<trashId>/
128
+ manifest.json # format 1:会话、模式、catalog 快照、memory bundle 引用、状态
129
+ session/ # 仅 hard:移走的会话日志目录
130
+ memory/lingshu/
131
+ bundle.json # 节点全文、正文文件、被剥离的跨节点引用
132
+ files/<nodeId>.content
133
+ ```
134
+
135
+ `manifest.status` 只允许 `pending → trashed → restoring/restored / purging`;崩溃在 `pending` 的条目不会出现在回收站 UI,也不会被自动恢复。恢复顺序是删除的逆序:日志 → catalog → 记忆。
136
+
137
+ ---
138
+
139
+ ## 5. 记忆适配器与诚实边界
140
+
141
+ ### 5.1 灵枢(默认,已实机核对格式)
142
+
143
+ - 计数:读 `_index.json`,按 `nodes[*].session === sessionId` 过滤;`protected === true` 或 `immutable === true` 的节点只计数、不删除。
144
+ - 删除:持 `_index.json.lock`(与 md_cg 写入方同一把锁,避免丢更新),把节点正文移到回收站 bundle,从索引删除节点,并剥离其他节点指向被删节点的 `edges/depends_on/derived_from` 引用;先写 bundle 再改索引,失败时还原。**没有任何可删节点的会话会先读索引直接返回空 bundle,不取锁**,因此记忆库繁忙/遗留锁不会拖住一次本来无记忆可删的彻底删除;0 字节无主锁超过 5s 视为崩溃遗留并清除。
145
+ - 恢复:按 bundle 把正文放回、节点条目与引用补回;若目标 id 已存在则拒绝并报 `E_TRASH_CONFLICT`。
146
+ - **边界**:这是「离线索引适配器」,不经过灵枢的 `forget` 工具,因此不会写它的 `_deletions.jsonl` 墓碑、`trash/` 与 review queue;`_recent.jsonl` 滚动窗口里的旧文本会自然过期。对格式做的是 schema 2 的受支持子集。若要求走灵枢自己的软删/审计语义,请使用 `lingshu_cg op=forget`(可能进入 review queue,需校验 receipt),本插件不冒充该协议。
147
+
148
+ ### 5.2 通用 JSON 索引适配器
149
+
150
+ `memory.generic` 可指向任何形如 `{nodes: {"<id>": {"session": "...", "path": "..."}}}` 的本地索引,用于其他记忆库;同样先 bundle 后索引、可恢复。未配置时不会启用。
151
+
152
+ ### 5.3 为什么没有默认纳入 Hindsight / Mnemon documents
153
+
154
+ - Hindsight:本机 DSH harness 已禁用;记忆是 bank 级而非会话级,虽有 `metadata.session_id` 但**没有暴露删除工具**。
155
+ - Mnemon documents:有 `sessionIds[]` 但删除只有 archive、没有 unarchive,且一个文档可跨多个会话,无法满足「失败回滚/可恢复」的质量要求。
156
+ - 两者在 `/state` 与 README 中明确标为未支持,而不是静默忽略。
157
+
158
+ ---
159
+
160
+ ## 6. 目录结构与代码地图
161
+
162
+ ```
163
+ dsh-conversation-recycle-bin/
164
+ ├─ package.json # bundle + dsh.client 声明
165
+ ├─ cordis.patch.yml # Host 行与默认配置
166
+ ├─ config.schema.json # 配置/权限的机器可读声明
167
+ ├─ client.js # Web Client:4 个槽位 + 确认框 + 设置页
168
+ ├─ icon.svg / locale/{en,zh}.json
169
+ ├─ lib/
170
+ │ ├─ index.js # apply(ctx, config):注册路由
171
+ │ ├─ core/ # 纯逻辑(无 DSH 依赖,可单测)
172
+ │ │ ├─ config.js # 默认值/归一化/JSON pointer
173
+ │ │ ├─ errors.js # 错误码 ↔ HTTP 状态
174
+ │ │ ├─ policy.js # 权限与数据归属
175
+ │ │ ├─ rate-limit.js # 滑动窗口限流
176
+ │ │ ├─ transaction.js # 逆序回滚原语
177
+ │ │ ├─ cascade.js # 三阶段级联 + 恢复 + 彻底删除
178
+ │ │ ├─ trash.js # 回收站 manifest
179
+ │ │ ├─ audit.js # JSONL 审计 + 压缩
180
+ │ │ └─ memory/ # 记忆适配器(lingshu / generic-json / none)
181
+ │ └─ host/ # DSH 服务适配
182
+ │ ├─ paths.js # DSH_HOME、会话目录发现、原子写
183
+ │ ├─ gateway.js # 会话/标题/归属归一化
184
+ │ ├─ catalog.js # archive/detach/pin 与日志移动
185
+ │ ├─ lifecycle.js # live 会话停机:cancel + archive + drain
186
+ │ ├─ memory.js # 适配器装配
187
+ │ └─ api.js # 同源 HTTP API、确认 token、限流
188
+ └─ test/ # 84 个用例,覆盖用户要求的 5 类场景 + live 删除 + 记忆锁 + 原地刷新 + 部分失败/跨进程/未完成条目 + Client 半渲染冒烟
189
+ ```
190
+
191
+ ---
192
+
193
+ ## 7. 测试与验收
194
+
195
+ ```powershell
196
+ cd D:\Users\34332\AI\dsh-conversation-recycle-bin
197
+ npm test # = manifest && client-module && core && cascade && memory && trash-audit && api(84 个用例)
198
+ npm run check # 语法 + manifest
199
+ ```
200
+
201
+ | 用户要求场景 | 用例 |
202
+ |---|---|
203
+ | 单条删除 | `api.test.mjs`「soft delete…」「hard delete…」;`cascade.test.mjs` 顺序/回滚 |
204
+ | 批量删除 | `api.test.mjs`「batch: double confirmation…」(软删)「batch: permanent deletion is one click…」(彻底)「a partial failure is a result…」(部分失败)「the effective limit…」(批量/限流上限);`preview: batch size limit` |
205
+ | 勾选记忆 | `api.test.mjs`「preview: counts…」「soft delete…」「restore…」;`memory.test.mjs` 全量 |
206
+ | 无权限 | `api.test.mjs`「authorization: orphan / protected」「security…」「confirmation replay」;`core.test.mjs` 策略表 |
207
+ | 删除失败回滚 | `api.test.mjs`「rollback: a missing log…」;`cascade.test.mjs` 三个失败/回滚用例;`trash-audit.test.mjs` 审计 `rolled-back` |
208
+ | Client 半冒烟 | `client-module.test.mjs`:按宿主 `__ModuleLoader__` 契约加载、`apply()` 注册 4 个槽位、每个组件做一次惰性渲染、部分失败响应不被当作拒绝、UI 开关隐藏入口、私有方法缺失时兜底重载 |
209
+ | 回收站/未完成条目 | `api.test.mjs`「trash: an entry that never committed…」;`trash-audit.test.mjs` 审计 `rolled-back` / `discard` |
210
+ | 删除后界面更新 | `client-module.test.mjs`「updates the shell in place」:原地刷新、关闭被删的当前会话、恢复不打断当前视图、旧宿主兜底重载 |
211
+
212
+ 宿主规范校验(安装后由宿主完成):`plugin_manager install_bundle` 会校验 manifest/patch;`cordis_inspect_query Config.listConfigs` 确认行挂载;`Slots.listSubTree` 确认 4 个槽位占用出现。**本 README 不声称浏览器视觉验收已完成**——在未安装/未控制浏览器前,UI 只做到源码与槽位契约级验证(见 INSTALL.md 的验证清单)。
213
+
214
+ ---
215
+
216
+ ## 8. 已知限制
217
+
218
+ 1. DSH 没有官方会话删除 API;hard 模式是插件自己负责的「移动式删除」。对 live 会话会先通过官方 `sessionController.cancel` + `workspaceRegistry.archiveSession({stopActivity:true})` 停机并等待 `liveDrainMs`,但底层日志布局仍可能与宿主升级耦合(升级后请先跑测试/在测试会话上验证)。
219
+ 2. `workspace.json` 里的 `archivedSessionIds`:非 live 会话的 hard 删除会无条件 `unarchiveSession` 清理归档标记;live 会话则**保留归档墓碑**,防止已排队的唤醒在日志移走后重新创建会话(恢复回收站条目会清除墓碑)。宿主没有公开「Agent 已释放」通知,因此已彻底清理的 live 条目可能留下一个无害的归档 id;若宿主以后改变归档语义需复核。同理,hard 删除 live 会话后,被删会话的 live 对象可能仍留在**当前宿主进程**的内存列表里(磁盘日志已移走),侧栏由归档墓碑隐藏;宿主重启后该对象不再出现。这也是「不整页刷新」仍能立即让列表正确的原因之一。
220
+ 3. 灵枢适配器不写其删除台账(§5.1);跨节点引用只处理 `edges/depends_on/derived_from` 三个已知字段。
221
+ 4. 投影缓存清理是 best-effort;旧日志/附件按宿主语义保留。
222
+ 5. UI 依赖当前版本的 slot 约定;宿主升级若改 slot props,入口会失效但不会破坏宿主。0.1.4 的原地刷新还会调用 `uiWorkspace.clearArchivedCurrent/clearMain`(宿主 `.d.ts` 中标为 private);两者都不存在时会退回整页刷新,不会留下过期视图,但属于对私有方法的依赖,宿主升级需复核。
223
+ 6. 跨进程写保护是 best-effort:`foreignWriterGraceMs` 只能发现"窗口内仍在增长"的日志;另一个 Host 的空闲 live 会话(两次写入之间)无法被本进程识别。桌面端与 `dsh web` 同时运行时,仍应先关闭另一端的会话再彻底删除。
224
+ 7. 回收站里 `restoring`/`purging` 且仍持有已移动会话日志的条目,出于数据安全不允许 `discard`;需按 INSTALL 的排障步骤手工处理(UI 会显示状态并给出提示)。