dsh-workspace-mover 2.1.1 → 2.3.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/CHANGELOG.md +285 -0
- package/README.md +357 -304
- package/README_EN.md +346 -0
- package/client/client.js +632 -100
- package/docs/ARCHITECTURE.md +81 -22
- package/docs/compatibility/dsh-0.2.0-rc.2-2026-10-04.md +39 -0
- package/docs/compatibility/dsh-0.2.0-rc.2-2026-10-13.md +45 -0
- package/lib/config.js +74 -0
- package/lib/index.js +1756 -172
- package/package.json +102 -63
- package/test/client-dom.test.mjs +796 -0
- package/test/e2e-sandbox.test.mjs +705 -60
- package/test/policy.test.mjs +415 -0
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -32,54 +32,103 @@ Single logical channel `/workspace-mover` via `ctx.connection.rpc.handle('/works
|
|
|
32
32
|
|
|
33
33
|
| Endpoint | Purpose |
|
|
34
34
|
| --- | --- |
|
|
35
|
-
| `mover.status` | Capability report (detected services / degraded items) |
|
|
35
|
+
| `mover.status` | Capability report (detected services / degraded items) plus `storageLayout`, `degradedFeatures`, and the `unarchiveApi` / `setTitleApi` flags |
|
|
36
36
|
| `mover.workspaces` | List workspaces |
|
|
37
37
|
| `mover.move` | Single-session true move |
|
|
38
|
-
| `mover.
|
|
39
|
-
| `mover.
|
|
40
|
-
| `mover.
|
|
38
|
+
| `mover.preview` | Read-only migration preview: source/target, collision, running state, adapter and estimated new snapshot bytes; `backup.reused` identifies an existing matching snapshot |
|
|
39
|
+
| `mover.moveMany` | Bulk move (≤ `batchLimit`, default 50) |
|
|
40
|
+
| `mover.scan` | Classified rescue scan (parses the newest `scanLimit`, default 400; reports `scanned` vs `scannedParsed`) |
|
|
41
|
+
| `mover.repair` | Attach / relink / home one item |
|
|
41
42
|
| `mover.repairAll` | Batch repair (per-item isolation) |
|
|
42
|
-
| `mover.history` | Recent moves (≤100) |
|
|
43
|
+
| `mover.history` | Recent moves (≤ `historyLimit`, default 100) |
|
|
43
44
|
| `mover.undo` | Undo one history entry |
|
|
44
45
|
| `mover.ws.audit` | Workspace path health |
|
|
45
46
|
| `mover.repoint` | Move-home wizard |
|
|
46
|
-
| `mover.archived` / `mover.unarchive` | List / restore archived sessions |
|
|
47
|
+
| `mover.archived` / `mover.unarchive` | List / restore archived sessions (`mover.archived` also reports `truncated` / `scannedParsed` / `scannedTotal` / `archivedTotal`) |
|
|
47
48
|
| `mover.openFolder` | Open group directory in OS file manager |
|
|
48
|
-
| `mover.session.delete` | Session → recycle bin |
|
|
49
|
-
| `mover.trash.list` / `mover.trash.restore` / `mover.trash.purge` | Recycle bin |
|
|
50
|
-
| `mover.backups.list` / `mover.backups.restore` / `mover.backups.deleteOne` |
|
|
49
|
+
| `mover.session.delete` | Session → recycle bin; optional `purgeBackups: true` also deletes that session's migration backups |
|
|
50
|
+
| `mover.trash.list` / `mover.trash.restore` / `mover.trash.purge` | Recycle bin; `purge` accepts optional `purgeBackups: true` |
|
|
51
|
+
| `mover.backups.list` / `mover.backups.restore` / `mover.backups.deleteOne` | Recovery-snapshot management; `list` reports `orphan` per group, physical `count`, plus `orphanGroups` / `orphanBytes` |
|
|
51
52
|
| `mover.tasks.list` / `mover.tasks.retry` / `mover.tasks.forget` | Migration task center |
|
|
52
|
-
| `mover.
|
|
53
|
+
| `mover.migrations.enqueue` / `mover.migrations.list` / `mover.migrations.retry` / `mover.migrations.cancel` | Persistent busy-session queue; `retry` accepts one `taskId` or a `taskIds` batch, with per-item failure isolation |
|
|
54
|
+
| `mover.bundle.export` / `mover.bundle.inspect` / `mover.bundle.import` | Portable Bundle export, dry-run inspection, and import through the adapter/backup pipeline |
|
|
55
|
+
| `mover.data.cleanup` | Dry-run and clean aged plugin data (default age = `cleanupDays`) |
|
|
56
|
+
|
|
57
|
+
### Tunable policies
|
|
58
|
+
|
|
59
|
+
`mover.data.cleanup`'s age and the caps above come from `resolvePolicy()` in `lib/index.js`, read **at the point of use** (never frozen at module load):
|
|
60
|
+
|
|
61
|
+
| Policy | Default | Environment override |
|
|
62
|
+
| --- | --- | --- |
|
|
63
|
+
| physical snapshot retention per session | 20 | `DSH_WORKSPACE_MOVER_BACKUP_KEEP` |
|
|
64
|
+
| move history length | 100 | `DSH_WORKSPACE_MOVER_HISTORY_LIMIT` |
|
|
65
|
+
| batch cap (move / repair) | 50 | `DSH_WORKSPACE_MOVER_BATCH_LIMIT` |
|
|
66
|
+
| repoint cap | 200 | `DSH_WORKSPACE_MOVER_REPOINT_LIMIT` |
|
|
67
|
+
| rescue scan cap | 400 | `DSH_WORKSPACE_MOVER_SCAN_LIMIT` |
|
|
68
|
+
| cleanup age (days) | 30 | `DSH_WORKSPACE_MOVER_CLEANUP_DAYS` |
|
|
69
|
+
|
|
70
|
+
An unparseable or out-of-range value falls back to the default, so a bad configuration can never disable a feature.
|
|
71
|
+
|
|
72
|
+
### Dependency declaration, and why every peer is `*`
|
|
73
|
+
|
|
74
|
+
`package.json` declares the eight host packages this plugin integrates with as `peerDependencies`, all `*` and all `optional` in `peerDependenciesMeta`. They are **declarative metadata for storefronts**, not a compatibility gate: the plugin fail-softs at runtime and reports any real degradation through `mover.status` capabilities and `mover_doctor`. The real floor is `engines.dsh` (`>=0.1.5-rc.1`) plus `engines.node`.
|
|
75
|
+
|
|
76
|
+
This was not the first attempt. The ranges originally used the ecosystem's "explicit prerelease branch" idiom, e.g. `>=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0`, and the host then reported the plugin as **incompatible with `0.2.0-rc.2`** — its own tested host line. The mechanism, confirmed by running dsh-market's own range evaluator:
|
|
77
|
+
|
|
78
|
+
- npm's prerelease rule is evaluated at the **comparator-set level**: one `||` alternative is one set, and a prerelease version satisfies that set only when at least one comparator in it shares the version's exact `[major, minor, patch]` tuple *and* carries a prerelease tag; only then are all comparators checked normally.
|
|
79
|
+
- `<0.2.0-0` does share the `0.2.0` tuple and does carry a prerelease, so the set is admitted — but `0.2.0-rc.2 > 0.2.0-0` (the `-0` sentinel sorts below every real prerelease), so the bound then excludes **every** 0.2.0 prerelease.
|
|
80
|
+
|
|
81
|
+
Enumerating release lines reproduces the fault on each new line: a `<0.3.0-0` bound breaks the moment a 0.3 prerelease ships. Hence `*`. dsh-market filters peer declarations to `/^@deepseek-ai\/dsh(?:-|$)/`, which is also why `@deepseek-ai/cordis` and `@deepseek-ai/schemastery` were never named in the warning — they are only evaluated when `engines.dsh` mentions them, and it does not.
|
|
82
|
+
|
|
83
|
+
`test/policy.test.mjs` asserts that the declared ranges plus `engines.dsh` derive `compatible` — never `incompatible` — for DSH versions from `0.1.5-rc.1` through `1.0.0`.
|
|
84
|
+
|
|
85
|
+
**Official Config is deliberately not registered.** The schema would need `@deepseek-ai/schemastery`, which the host loader provides but which is not resolvable from this plugin's directory (verified by `createRequire` from `lib/index.js`); the npm `schemastery` has no `.volatile()` either. Because a bundle-layer static import failure breaks **boot** (a documented ecosystem failure class), `lib/config.js` never imports it — it exposes `buildConfig(z)` / `attachConfig(moduleOrBuilder)` so a host that hands over a schema builder can attach one, and exports `Config === undefined` otherwise. `buildConfig` contains all its own failures and returns `undefined` rather than throwing.
|
|
86
|
+
|
|
87
|
+
### Official-API preference is observable
|
|
88
|
+
|
|
89
|
+
v2.2 prefers two documented host APIs and keeps an equivalent fallback for each: `registry.unarchiveSession()` (fallback: `enqueueOperation` + `setState`) and `entity.setTitle()` (formerly folded into `entity.mutate`). Both fallbacks are silent by design — the feature still works — so `mover.status.capabilities` reports `unarchiveApi` / `setTitleApi` and lists `unarchive-official-api` / `set-title-official-api` in `degradedFeatures` when the host lacks them. That is what makes "which path did it take?" answerable on a real machine instead of a guess.
|
|
53
90
|
|
|
54
91
|
Errors carry stable codes (`busy`, `conflict`, `not-found`, `rollback-failed`, …). Host logs write failures under `MOVE FAILED`.
|
|
55
92
|
|
|
56
93
|
## Agent tools (DSH ≥ 0.1.5 with `dsh-tools`)
|
|
57
94
|
|
|
58
|
-
When the host provides the `tools` service, `apply()` registers
|
|
95
|
+
When the host provides the `tools` service, `apply()` registers four plain (zero-dependency) tool definitions; each tool's execute re-enters the internal RPC dispatch, so agent calls share the panel's locks, error codes, and transaction semantics:
|
|
59
96
|
|
|
60
97
|
| Tool | Gate | Purpose |
|
|
61
98
|
| --- | --- | --- |
|
|
62
99
|
| `mover_list_sessions` | none (read-only) | Compose scan + workspace list (sessions capped at 40, counts, ghosts, recoveryCount) for id resolution |
|
|
63
100
|
| `mover_move_session` | user approval | Move one session (`sessionId`, `targetWorkspaceId`, optional `sessionTitle`) |
|
|
64
101
|
| `mover_repair_sessions` | user approval | One `mover.repairAll` pass (accounting-only) |
|
|
102
|
+
| `mover_doctor` | none (read-only) | One `mover.doctor` self-check pass (host services, data dirs, recovery records, workspace paths) |
|
|
65
103
|
|
|
66
104
|
- Mutating tools call `ctx.approval.request({ agent, toolName, reason, signal })` before acting; `allowed-once` proceeds, anything else (`denied` / `cancelled` / `unavailable`) or a throwing seam fails closed. Hosts without the approval seam (no UI) rely on conversational confirmation and proceed.
|
|
67
105
|
- `exec.signal` propagates into the RPC layer; cancellation lets the in-flight step finish rolling back to a consistent state.
|
|
68
106
|
- Hosts without `dsh-tools` skip registration silently; `mover.status` reports the `agentTools` flag.
|
|
69
107
|
|
|
70
|
-
## Session archives
|
|
108
|
+
## Session archives and storage adapter
|
|
71
109
|
|
|
72
|
-
-
|
|
73
|
-
- Mixed compression
|
|
110
|
+
- The fallback filesystem adapter recognizes names `session.jsonl[.zstd]` and `session.vN.jsonl[.zstd]`; it selects the highest generation without rewriting older files to v3.
|
|
111
|
+
- A mixed plain/Zstd directory and a file whose compression differs from the host's configured mode are rejected explicitly. Mixed compression elsewhere under the same root is diagnosed per session, not by a root-wide scan.
|
|
112
|
+
- v3/v4 filenames are recognized, but a v4 runtime fixture has not yet been verified. Filename/header mismatch is rejected only when the header contains one of the generation fields currently recognized by the adapter.
|
|
74
113
|
- True move keeps the session id and all frames; only the header `cwd` (first frame) is rewritten; other frames stay byte-identical. Publish via temp file + atomic rename.
|
|
75
114
|
|
|
115
|
+
### The storage-layout assumption, and its self-check
|
|
116
|
+
|
|
117
|
+
The adapter first reports the public `list` / `stat` / `open` surface. When a provider also exposes a physical root, the plugin uses the legacy directory adapter as a visible `filesystem-fallback`; when only official handles are available it stays read/diagnostic capable and marks physical moves as `unsupported` instead of guessing a path.
|
|
118
|
+
|
|
119
|
+
`verifyStorageLayout(ctx)` closes that gap: it takes up to five real headers from `persistence.list()`, reverse-derives each session's directory with this plugin's own encoding, and confirms an archive is actually there. Result states are `ok` / `degraded` / `empty` (no sessions to verify) / `unavailable`. It is exposed as `mover.status.capabilities.storageLayout` plus a `data-layout` check in `mover_doctor`, is strictly read-only, and never throws (any error degrades to a warning).
|
|
120
|
+
|
|
121
|
+
`mover.status.capabilities.storageAdapter` is `filesystem-fallback`, `official-handle`, or `unavailable`; `storageApi`, `physicalPath`, and `migrationQueueState` expose the exact degradation and queue state. `mover_doctor` reports the same adapter under `storage-adapter` and checks the durable migration records under `data-migrations`.
|
|
122
|
+
|
|
123
|
+
Portable Bundles contain `manifest.json`, `sessions/`, `attachments/`, `checksums.json`, and `workspace-metadata.json`. Import defaults to dry-run and reports id conflicts, missing attachments, compression incompatibilities, required/free space, and recoverable files. Execution refuses a blocked plan, backs up each artifact, verifies the rewritten header, and isolates failures per session. Attachment files are preserved and copied into the imported session directory; the bundle report calls out any missing file.
|
|
124
|
+
|
|
76
125
|
## Move pipeline
|
|
77
126
|
|
|
78
127
|
Order of operations for a cross-workspace move:
|
|
79
128
|
|
|
80
129
|
1. **Running check** — reject only mid-turn sessions (`agents.get(id)?.status === 'running'`); idle resident sessions may move.
|
|
81
130
|
2. **Read authority header** from disk; verify target ≠ source.
|
|
82
|
-
3. **
|
|
131
|
+
3. **Recovery snapshot** to `$DSH_HOME/workspace-mover/backups/` (up to `backupKeep` distinct physical files per session, default 20) **before any side effect**. The content fingerprint ignores the migration-only header `cwd`, so a move back and forth reuses the existing snapshot. A backup failure aborts with `backup-failed` and nothing has been touched.
|
|
83
132
|
4. **Preflight** — dual-accounting detect, target writability probe, disk-space hint.
|
|
84
133
|
5. **Rewrite first frame** (header cwd) only.
|
|
85
134
|
6. **Move directory** (Windows: exponential-backoff retry on EPERM; fallback copy+delete).
|
|
@@ -88,7 +137,7 @@ Order of operations for a cross-workspace move:
|
|
|
88
137
|
9. **Verify** — read archive back; id + cwd must match or the move fails.
|
|
89
138
|
10. **Rollback on any failure** — restore live header + index snapshot → original bytes back to source dir → reattach source workspace.
|
|
90
139
|
|
|
91
|
-
Concurrency: same session / same target workspace try-acquire a lock
|
|
140
|
+
Concurrency: same session / same target workspace try-acquire a lock. A running session returns `busy` to the transaction layer and is persisted in `migrations.json` as `waiting-for-idle`; the queue wakes every few seconds and on the next plugin start. Bulk moves de-duplicate first and isolate each result.
|
|
92
141
|
|
|
93
142
|
If “rollback also failed”, a durable recovery record is written and shown as a red manual-recovery row. Session files and backups stay in place — never auto-deleted.
|
|
94
143
|
|
|
@@ -110,7 +159,7 @@ All plugin metadata lives under `$DSH_HOME/workspace-mover/` and never overwrite
|
|
|
110
159
|
|
|
111
160
|
| Path | Use |
|
|
112
161
|
| --- | --- |
|
|
113
|
-
| `backups/` | Byte-level
|
|
162
|
+
| `backups/` | Byte-level recovery snapshots; new files include a SHA-256 content fingerprint, while legacy timestamp-only names remain readable |
|
|
114
163
|
| `history.json` | Last 100 cross-workspace moves |
|
|
115
164
|
| trash / task / recovery files | Recycle bin manifest, task center, recovery ledger |
|
|
116
165
|
|
|
@@ -118,12 +167,13 @@ Official session files and workspace registry are only mutated via host APIs: re
|
|
|
118
167
|
|
|
119
168
|
## Compatibility boundary
|
|
120
169
|
|
|
121
|
-
Verified:
|
|
170
|
+
Verified (each entry dated; the newest verification supersedes the badge claims in the READMEs):
|
|
122
171
|
|
|
123
|
-
- DeepSeek Harness `0.
|
|
172
|
+
- DeepSeek Harness `0.2.0-rc.2` (desktop, 2026-10-04) — see `docs/compatibility/dsh-0.2.0-rc.2-2026-10-04.md`
|
|
173
|
+
- DeepSeek Harness `0.1.5-rc.1` / `0.1.5-rc.2` (earlier release line)
|
|
124
174
|
- dsh-market `1.45.1`
|
|
125
175
|
- Node.js `≥ 22`
|
|
126
|
-
- `package.json` → `engines.dsh`: `>= 0.1.5-rc.1`
|
|
176
|
+
- `package.json` → `engines.dsh`: `>= 0.1.5-rc.1` (a lower bound only; it is deliberately not an upper bound, and dsh-market reads it for its host-requirement card)
|
|
127
177
|
|
|
128
178
|
GitHub-only packages may show “host requirement unknown” in dsh-market when no npm manifest is published; that is metadata, not runtime incompatibility.
|
|
129
179
|
|
|
@@ -140,10 +190,19 @@ Version-sensitive host behaviors degrade with explicit UI messages (not silent f
|
|
|
140
190
|
- Sidebar redraw plugins that replace workspace DOM can break ARIA row lookup — features stop triggering; data is not damaged.
|
|
141
191
|
- Do not install a second document-level drag interceptor for cross-workspace moves.
|
|
142
192
|
|
|
193
|
+
## Client-half invariants
|
|
194
|
+
|
|
195
|
+
- **Every document-level listener and the menu `MutationObserver` is registered through the `listen()` helper and released in one `ctx.effect` teardown**, and `apply` is guarded by a module-level `applied` flag. The desktop client disposes and recomposes the plugin fiber on hot reload; without both, a second copy of every listener stacks up (duplicate confirm dialogs, duplicate RPC, two closures fighting over the same selection highlight).
|
|
196
|
+
- **A row is only ever mapped to a session id by a verifiable credential.** The authoritative path walks the React fiber to `memoizedProps.node.id`; the fallback aligns DOM rows to `workspace.sessionIds` by exact/longest title match. A row that cannot be confidently mapped is reported, never substituted — the previous "take the next id in order" behavior could move a different, valid session that the host cannot detect.
|
|
197
|
+
- **A workspace header is only mapped to a registry entity when the header text contains that entity's title or path** (or when no entity carries a display identity at all). `registry.list()` includes workspaces whose folder is gone while the sidebar hides them, so positional mapping is unsafe.
|
|
198
|
+
- `rowSessionId`'s fiber guard is intentionally permissive; if the official node shape changes, the fallback engages and the user is told, rather than the feature silently doing nothing.
|
|
199
|
+
- Dialogs are built by `injectOverlay()` and handed to `openDialog()`, which appends **before** focusing (an element not in the document cannot take focus in a browser), focuses the primary button, and traps Tab within the card.
|
|
200
|
+
|
|
143
201
|
## Tests & release
|
|
144
202
|
|
|
145
|
-
- `npm test` runs `test/core.test.mjs`, `test/e2e-sandbox.test.mjs`, `test/traffic-badge.test.mjs
|
|
146
|
-
- Coverage includes rollback, rescue scan/repair, history undo, workspace repoint, post-move verification, recycle bin / backup restore, task center, data-protection cleanup, concurrency locks, error codes.
|
|
203
|
+
- `npm test` runs `test/core.test.mjs`, `test/e2e-sandbox.test.mjs`, `test/policy.test.mjs`, `test/client-dom.test.mjs`, `test/traffic-badge.test.mjs` and `test/release-downloads.test.mjs`. `client-dom.test.mjs` drives the **client half** against a hand-rolled DOM stub (no devDependencies, so CI keeps running plain `node --test`); `policy.test.mjs` covers the tunable-policy layer and the guarded Config builder. Exact case count is reported by the test run — do not hard-code it here beyond the release it was written for.
|
|
204
|
+
- Coverage includes rollback, rescue scan/repair, history undo, workspace repoint, post-move verification, recycle bin / backup restore, task center, data-protection cleanup, concurrency locks, error codes, storage-layout self-check, backup orphan GC, tunable policies, and the client half's row/workspace resolution, drag payload, listener teardown and dialog dismissal.
|
|
205
|
+
- Adding a test file requires listing it in `package.json` → `scripts.test`; the script enumerates files explicitly, so a new file that is not listed never runs in CI.
|
|
147
206
|
- CI: `.github/workflows/test.yml`.
|
|
148
207
|
- Release assets may ship a `.tgz` install package (`dsh-workspace-mover-*.tgz`).
|
|
149
208
|
- Traffic/clones and release-download badges are refreshed by `.github/workflows/traffic-badge.yml` via Gist JSON endpoints (not part of the runtime plugin).
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# DSH 兼容性实测报告:0.2.0-rc.2 · 2026-10-04
|
|
2
|
+
|
|
3
|
+
> 本插件的兼容性验收记录。每适配一个 DSH 版本新增一份带日期的报告;README 的 "DSH tested" 徽章链接到最新一份。
|
|
4
|
+
> 插件版本:**2.1.0**(npm + GitHub tag v2.1.0)· 运行环境:Windows 10.0.26200 x64 · Node ≥22
|
|
5
|
+
|
|
6
|
+
## 验证范围
|
|
7
|
+
|
|
8
|
+
| 场景 | DSH 版本 | 结论 |
|
|
9
|
+
| --- | --- | --- |
|
|
10
|
+
| **桌面客户端**(Electron 44 壳,`E:\DeepSeekHarness\desktop`) | 0.2 线(0.2.0-rc.2 内核) | ✅ 已实测通过 |
|
|
11
|
+
| **web profile** | 0.2.0-rc.2 | ⏳ 与桌面版同内核、同一代码路径,启动器已切换,回归待跑 |
|
|
12
|
+
|
|
13
|
+
## 桌面版实测项(2026-10-04)
|
|
14
|
+
|
|
15
|
+
| 项目 | 结果 | 说明 |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| 数据家共享(`DSH_HOME` 指向既有 `data\`) | ✅ | 会话、工作区、投影缓存全部直接可见,无格式迁移(会话文件保持旧命名) |
|
|
18
|
+
| 桌面 profile 插件安装 | ✅ | 应用内「插件」页安装,profile 与 web 相互独立 |
|
|
19
|
+
| 拖拽跨工作区迁移 | ✅ | 会话行 → 工作区标题行,确认后真迁移 |
|
|
20
|
+
| 确认框渲染 | ✅ | 修复后为不透明卡片(0.2 主题的 `--dsw-specific-menu` 为半透明玻璃色) |
|
|
21
|
+
| 插件热重载后的移动 | ✅ | 修复了旧 fiber 失活导致的 `inactive context` 报错(connection 实例在 apply 时捕获) |
|
|
22
|
+
| agent 工具(dsh-tools 注册) | ✅ | dsh-base 在 web/桌面共享 `tools` 行;三个工具 + 审批门就绪 |
|
|
23
|
+
| `mover.status` 能力位 | ✅ | `agentTools` 标志如实汇报 |
|
|
24
|
+
|
|
25
|
+
## 已知事项(非本插件缺陷)
|
|
26
|
+
|
|
27
|
+
- **遗留 Agent 预设**:0.2 不再读取 `$DSH_HOME\.agent-presets\<id>\` 旧格式目录,旧会话恢复会报 `Unknown agent preset`。迁移步骤:[docs/dsh-0.2-preset-migration.md](../dsh-0.2-preset-migration.md)。
|
|
28
|
+
- **web profile 回归**:拖拽 / 面板 / agent 工具在 web 0.2 场景按同一代码路径适配,尚未单独实测;完成后本报告将更新。
|
|
29
|
+
|
|
30
|
+
## 测试基线
|
|
31
|
+
|
|
32
|
+
- `npm run check`(语法 + 105 用例 + pack dry-run)全绿;
|
|
33
|
+
- CI:ubuntu / windows / macos × Node 22/24 矩阵通过(v2.1.0 tag 构建)。
|
|
34
|
+
|
|
35
|
+
## 历史
|
|
36
|
+
|
|
37
|
+
| 日期 | DSH 版本 | 报告 |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| 2026-10-04 | 0.2.0-rc.2(桌面) | 本文件 |
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# DSH compatibility report: 0.2.0-rc.2 · 2026-10-13
|
|
2
|
+
|
|
3
|
+
Release: 2.3.0. Desktop results below are user-reported partial verification; they do not claim a full real-runtime smoke pass.
|
|
4
|
+
|
|
5
|
+
## Matrix
|
|
6
|
+
|
|
7
|
+
| Environment | Node | Result | Scope |
|
|
8
|
+
| --- | --- | --- | --- |
|
|
9
|
+
| DSH Desktop 0.2.0-rc.2 | pending | partial: user confirmed idle-session migration and snapshot reuse; initial return-preview failures and corrections are documented below | adapter status, v4 move, preview; queue resume still pending |
|
|
10
|
+
| DSH Web 0.2.0-rc.2 | pending | pending user verification | same smoke flow on web connection |
|
|
11
|
+
| Local sandbox fixtures | 22.23.2 | passed: `npm run check`, 171/171 tests, `npm pack --dry-run` | v3/v4 Zstd, plain JSONL, format diagnostics, rollback, queue state transitions, return preview, drop-target identity, explicit target selection, snapshot deduplication |
|
|
12
|
+
| Node 24 fixtures | 24 | not run | CI/runtime matrix remains pending |
|
|
13
|
+
|
|
14
|
+
## Implemented checks
|
|
15
|
+
|
|
16
|
+
- `filesystem-fallback`, `official-handle`, and `unavailable` adapter modes are reported through `mover.status` and `mover_doctor`.
|
|
17
|
+
- Physical operations fail with a stable `unsupported` error when the provider exposes no raw-byte path.
|
|
18
|
+
- A running session is persisted in `migrations.json` as `waiting-for-idle`; the queue drains after idle and after plugin re-mount. Process-restart validation remains pending.
|
|
19
|
+
- `mover.preview` reports source, target, collision, running state, adapter mode, warnings, and estimated new snapshot bytes; a matching snapshot is reported as reused.
|
|
20
|
+
- Recovery snapshots use a SHA-256 fingerprint of session content with migration-only `cwd` normalized. Legacy timestamp-only backup names remain readable. Each migration still records its source, target, and snapshot reference in history.
|
|
21
|
+
- Mixed compression is rejected explicitly; no format upgrade is performed. Header-generation mismatch detection only applies when the header declares a recognized generation field.
|
|
22
|
+
- The local queue test covers plugin re-mount. A full DSH process restart with a waiting task has not been exercised yet.
|
|
23
|
+
- The user confirmed snapshot reuse for an old idle session after moving it; local fixtures cover Zstd, plain JSONL, round trips, and content changes.
|
|
24
|
+
|
|
25
|
+
## Still requiring a real DSH run
|
|
26
|
+
|
|
27
|
+
- Official-runtime plain JSONL and v3/v4 fixtures, including mismatch and mixed-layout diagnostics. The local sandbox fixtures are not official DSH runtime evidence.
|
|
28
|
+
- Desktop and Web smoke flow with a real session: create, preview, move, verify id/cwd/events, conflict, attach failure and byte-exact rollback.
|
|
29
|
+
- Restart DSH while a queue item is waiting, then verify it completes after the session becomes idle.
|
|
30
|
+
|
|
31
|
+
The next report should replace the pending rows with the exact DSH build, Node version, successful scenarios, and known limits.
|
|
32
|
+
|
|
33
|
+
## Desktop feedback and correction
|
|
34
|
+
|
|
35
|
+
The user restarted Desktop and reported all doctor checks passing except the expected filesystem adapter warning. A session moved from `E:\DeepSeekHarness` to `E:\VSCode_Project\quant_research`. Read-only inspection found one `session.v4.jsonl.zstd` primary archive at the destination (419,562 bytes) and no primary archive for that ID at the source. The return preview nevertheless showed both source and target as `quant_research` and incorrectly labelled the session's own archive as a conflict.
|
|
36
|
+
|
|
37
|
+
The candidate now resolves drop targets from the rendered `data-row-key` workspace ID before React group props and unique text evidence. It refreshes workspace data at drop time and rejects ambiguous text matches. Same-workspace previews distinguish existing membership from an unregistered archive, and report no recovery backup for those operations. Doctor adapter explanations and preview notices are localized; the recovery backup copy is explicitly described as separate from the registered primary session.
|
|
38
|
+
|
|
39
|
+
The user retested after restart and still received a same-workspace return preview. The exact Desktop event target has not been captured, so the identity-based correction alone is not considered verified. Further changes remove the stale last-hover fallback when dropping outside a header and clear the hover target when leaving a header. Single-session confirmation now offers an explicit workspace selector, refreshes the preview for the selected target and fork scope, and keeps execution disabled while loading or on preview failure. Late preview responses are ignored. These flows have DOM regression coverage; The latest user feedback confirms migration and snapshot reuse, but does not separately document every selector/drag scenario.
|
|
40
|
+
|
|
41
|
+
## Release checks
|
|
42
|
+
|
|
43
|
+
- Version 2.3.0: local Node 22.23.2 checks passed, 171/171 tests, syntax checks and npm pack dry-run.
|
|
44
|
+
- Regression coverage now includes newest/default versus explicitly selected backup restoration, attachment space estimates, and insufficient-space import refusal before writing.
|
|
45
|
+
- README_EN.md and CHANGELOG.md are included in the npm package.
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// dsh-workspace-mover — 官方 Config 接入(守卫式,同步、无 boot 风险)
|
|
2
|
+
//
|
|
3
|
+
// 官方做法:导出 schemastery schema 作为插件 Config,声明 .volatile() 的字段成为设置页
|
|
4
|
+
// 上的实时字段。但 @deepseek-ai/schemastery 由**宿主的 loader** 提供(它不是本插件的
|
|
5
|
+
// 依赖,也不是本插件目录下可解析的模块)。这里实测:从 lib/index.js 与 lib/config.js
|
|
6
|
+
// 出发,`@deepseek-ai/schemastery` 与 npm 上的 `schemastery` **都解析失败**。
|
|
7
|
+
//
|
|
8
|
+
// 而官方明确记录过一类事故:bundle 层 import 失败会打断 **boot**(有插件 pin 住
|
|
9
|
+
// @deepseek-ai/dsh-settings,宿主删除该模块后整个 DSH 起不来)。所以:
|
|
10
|
+
//
|
|
11
|
+
// 1. 本模块**不做任何静态 import**,也不使用顶层 await(避免模块求值挂起);
|
|
12
|
+
// 2. schema 由注入的构造器同步构造,拿不到就导出 undefined —— 不导出 Config,
|
|
13
|
+
// 插件完全照常工作,只是没有设置页;
|
|
14
|
+
// 3. 策略的实际生效值永远有环境变量 / 代码默认值兜底(见 lib/index.js 的
|
|
15
|
+
// resolvePolicy),所以"没有设置页"不等于"策略不可调"。
|
|
16
|
+
//
|
|
17
|
+
// 宿主若通过 Cordis 的 config 机制注入 schema 构造器,apply(ctx, config) 里的
|
|
18
|
+
// attachConfig() 会把它接上;未注入时一切照旧。
|
|
19
|
+
|
|
20
|
+
/** 策略字段定义(与 lib/index.js 的 POLICY_DEFAULTS 一一对应,勿单独改动)。 */
|
|
21
|
+
const POLICY_FIELDS = [
|
|
22
|
+
{ key: 'backupKeep', def: 20, max: 1000, desc: '每个会话保留的迁移备份份数' },
|
|
23
|
+
{ key: 'historyLimit', def: 100, max: 10000, desc: '移动历史的保留条数' },
|
|
24
|
+
{ key: 'batchLimit', def: 50, max: 500, desc: '单批移动/修复的会话数上限' },
|
|
25
|
+
{ key: 'repointLimit', def: 200, max: 5000, desc: '单个工作区搬家的会话数上限' },
|
|
26
|
+
{ key: 'scanLimit', def: 400, max: 100000, desc: '救援扫描解析的档案数上限' },
|
|
27
|
+
{ key: 'cleanupDays', def: 30, max: 3650, desc: '「清理 N 天前数据」的默认天数' }
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/** schemastery 的 .volatile() 只在部分版本存在;缺失时退化为普通字段(仍可配置)。 */
|
|
31
|
+
function live(node) {
|
|
32
|
+
return typeof node?.volatile === 'function' ? node.volatile() : node;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 用给定的 schema 构造器同步构造 Config。构造过程整体包在 try 内:
|
|
37
|
+
* 不同版本的 schemastery 方法集不同(例如 .volatile 常缺),任何一步失败都
|
|
38
|
+
* 必须降级为 undefined,绝不把异常抛到插件启动路径上。
|
|
39
|
+
* @param {any} z schemastery 构造器(需有 z.object / z.number)
|
|
40
|
+
* @returns {any|undefined} schema,或构造失败时 undefined
|
|
41
|
+
*/
|
|
42
|
+
function buildConfig(z) {
|
|
43
|
+
if (!z || typeof z.object !== 'function' || typeof z.number !== 'function') return undefined;
|
|
44
|
+
try {
|
|
45
|
+
const shape = {};
|
|
46
|
+
for (const { key, def, max } of POLICY_FIELDS) {
|
|
47
|
+
let node = z.number();
|
|
48
|
+
if (typeof node.step === 'function') node = node.step(1);
|
|
49
|
+
if (typeof node.min === 'function') node = node.min(1);
|
|
50
|
+
if (typeof node.max === 'function') node = node.max(max);
|
|
51
|
+
if (typeof node.default === 'function') node = node.default(def);
|
|
52
|
+
shape[key] = live(node);
|
|
53
|
+
}
|
|
54
|
+
return z.object(shape);
|
|
55
|
+
} catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 从宿主注入的对象上取 schema 构造器并构造 Config。
|
|
62
|
+
* 只接受已经在手上的构造器——不做动态 import,因此不会影响启动时序。
|
|
63
|
+
* @param {any} schemaBuilder 可能的 schemastery 构造器(或带 default 的模块对象)
|
|
64
|
+
*/
|
|
65
|
+
function attachConfig(schemaBuilder) {
|
|
66
|
+
const z = schemaBuilder?.default ?? schemaBuilder;
|
|
67
|
+
return buildConfig(z);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 本模块导出的 Config 恒为 undefined:插件不主动 import schemastery。
|
|
71
|
+
// 宿主若注入构造器,apply 内会用它构造并注册(见 lib/index.js)。
|
|
72
|
+
const Config = undefined;
|
|
73
|
+
|
|
74
|
+
export { Config, POLICY_FIELDS, buildConfig, attachConfig, live };
|