dsh-workspace-mover 2.1.0 → 2.1.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/README.md CHANGED
@@ -1,26 +1,38 @@
1
1
  # dsh-workspace-mover
2
2
 
3
- **_> 非官方项目,由社区成员独立开发和维护。_**
3
+ > [!IMPORTANT]
4
+ > 非官方项目,由社区成员独立开发和维护。
4
5
 
5
6
  <!-- Hero -->
6
7
  <div align="center">
7
8
  <b style="font-size: 1.15em;">在侧边栏把会话拖到另一个工作区——真迁移原始档案,而不是复制</b><br /><br />
9
+ <p style="font-size: 0; line-height: 1;">
10
+ <a href="dsh://plugin/install?id=dsh-workspace-mover&name=dsh-workspace-mover&version=2.1.1&repo=PianoPrince%2Fdsh-workspace-mover&permissions=%E6%9C%AC%E5%9C%B0%E4%BC%9A%E8%AF%9D%E6%96%87%E4%BB%B6%E8%AF%BB%E5%86%99%2C%E5%B7%A5%E4%BD%9C%E5%8C%BA%E8%AE%B0%E8%B4%A6%E5%8F%98%E6%9B%B4&downloadUrl=https%3A%2F%2Fgithub.com%2FPianoPrince%2Fdsh-workspace-mover%2Farchive%2Frefs%2Fheads%2Fmain.zip">
11
+ <img src="https://img.shields.io/badge/DeepSeek%20Harness-%F0%9F%9A%80%20%E5%94%A4%E8%B5%B7%E6%A1%8C%E9%9D%A2%E5%AE%A2%E6%88%B7%E7%AB%AF%E4%B8%80%E9%94%AE%E5%AE%89%E8%A3%85-10B981?style=for-the-badge&logo=deepseek&logoColor=white" alt="唤起桌面客户端一键安装" />
12
+ </a>
13
+ </p>
14
+ <p style="font-size:12px;color:var(--dsw-alias-label-tertiary,#999)">官方桌面客户端对该深链目前降级为聚焦窗口;Hub 系第三方客户端支持完整安装流程。</p>
8
15
  <p style="font-size: 0; line-height: 1;">
9
16
  <a href="https://github.com/PianoPrince/dsh-workspace-mover/actions/workflows/test.yml"><img alt="CI" src="https://github.com/PianoPrince/dsh-workspace-mover/actions/workflows/test.yml/badge.svg" style="height:20px; margin:0 2px;" /></a>
10
17
  <a href="https://www.npmjs.com/package/dsh-workspace-mover"><img alt="npm version" src="https://img.shields.io/npm/v/dsh-workspace-mover" style="height:20px; margin:0 2px;" /></a>
11
- <img alt="plugin version" src="https://img.shields.io/badge/dsh--plugin-2.1.0-4d6bfe" style="height:20px; margin:0 2px;" />
18
+ <img alt="plugin version" src="https://img.shields.io/badge/dsh--plugin-2.1.1-4d6bfe" style="height:20px; margin:0 2px;" />
19
+ <img alt="DSH tested 0.2.0-rc.2" src="https://img.shields.io/badge/DSH_tested-0.2.0--rc.2-4c8dff" style="height:20px; margin:0 2px;" />
12
20
  <img alt="DSH engines" src="https://img.shields.io/badge/DSH-%E2%89%A50.1.5--rc.1-339933" style="height:20px; margin:0 2px;" />
13
- <a href="https://github.com/PianoPrince/dsh-workspace-mover/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/PianoPrince/dsh-workspace-mover" style="height:20px; margin:0 2px;" /></a>
14
- <a href="./LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-yellow.svg" style="height:20px; margin:0 2px;" /></a>
15
21
  <img alt="Node" src="https://img.shields.io/badge/Node-%E2%89%A522-339933" style="height:20px; margin:0 2px;" />
16
22
  <img alt="npm 依赖:0" src="https://img.shields.io/badge/npm%20%E4%BE%9D%E8%B5%96-0-4d6bfe" style="height:20px; margin:0 2px;" />
17
- <a href="https://awesome-dsh-plugin.com"><img alt="Awesome DSH Plugin" src="https://awesome-dsh-plugin.com/badge.svg" style="height:20px; margin:0 2px;" /></a>
18
23
  </p>
19
24
  <p style="font-size: 0; line-height: 1;">
25
+ <a href="https://github.com/PianoPrince/dsh-workspace-mover/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/PianoPrince/dsh-workspace-mover" style="height:20px; margin:0 2px;" /></a>
26
+ <a href="./LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-yellow.svg" style="height:20px; margin:0 2px;" /></a>
27
+ <a href="https://awesome-dsh-plugin.com"><img alt="Awesome DSH Plugin" src="https://awesome-dsh-plugin.com/badge.svg" style="height:20px; margin:0 2px;" /></a>
28
+ <a href="https://www.dsh.so/artifact/dsh-workspace-mover/"><img alt="dsh.so risk: low" src="https://www.dsh.so/badge/dsh-workspace-mover.svg" style="height:20px; margin:0 2px;" /></a>
29
+ <a href="https://github.com/XingLingQAQ/dsh-plugin-registry"><img alt="Listed in dsh-plugin-registry" src="https://img.shields.io/badge/registry-dsh--plugin--registry-2d6a8f" style="height:20px; margin:0 2px;" /></a>
20
30
  <img alt="真迁移" src="https://img.shields.io/badge/-真迁移-4d6bfe" style="height:20px; margin:0 2px;" />
21
31
  <img alt="零 token 消耗" src="https://img.shields.io/badge/-零%20token%20消耗-4d6bfe" style="height:20px; margin:0 2px;" />
22
32
  <img alt="备份回滚" src="https://img.shields.io/badge/-备份回滚-4d6bfe" style="height:20px; margin:0 2px;" />
23
- <img alt="GitHub clones observed(含 unique)" src="https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FPianoPrince%2Fc14345658550a4a308570acfbaf9d170%2Fraw%2Fwsm-clones-total.json" style="height:20px; margin:0 2px;" />
33
+ </p>
34
+ <p style="font-size: 0; line-height: 1;">
35
+ <img alt="GitHub clones observed(自 2026-08-25 累计,含 unique)" src="https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FPianoPrince%2Fc14345658550a4a308570acfbaf9d170%2Fraw%2Fwsm-clones-total.json" style="height:20px; margin:0 2px;" />
24
36
  <!-- release-downloads-badge:start --><!-- release-downloads-badge:end -->
25
37
  </p>
26
38
  </div>
@@ -38,7 +50,7 @@
38
50
  - [🔌 如何接入 DSH](#-如何接入-dsh)
39
51
  - [🤝 与其他插件共存](#-与其他插件共存)
40
52
  - [🧩 兼容性与卸载](#-兼容性与卸载)
41
- - [🔐 安全承诺](#-安全承诺)
53
+ - [🔐 安全台账](#-安全台账safety-ledger)
42
54
  - [⚠️ 已知限制](#️-已知限制)
43
55
  - [🆕 最近版本](#-最近版本)
44
56
 
@@ -54,7 +66,9 @@ DeepSeek Harness 侧边栏可以拖拽排序**同一工作区**内的会话,
54
66
  - **🏠 工作区搬家向导**:项目文件夹被移动/改名后,一键把失效分组原地重定向到新位置;id、标题、排序、归档位保持,名下会话连同旧路径散件一起迁移
55
67
  - **🛟 会话救援**(设置 → 会话救援):一键找回「失联」「未记账」「挂错分组」的会话;已归档会话可恢复;支持一键修复与筛选
56
68
  - **🗑️ 回收站与备份**:删除先进回收站,可还原到原位置或任意分组;迁移自动生成备份,可按会话恢复或清理
69
+ - **🛡️ 数据保护**:回收站与备份的份数/占用一屏可见;「清理 30 天前数据」先预览释放量,确认后执行
57
70
  - **⏪ 移动历史与撤回**:记录最近 100 次跨工作区移动,批量聚合为一条,整批可一键撤回
71
+ - **🧾 迁移任务中心**:批量迁移逐项持久化记录(完成/失败、最后错误与尝试时间),失败项一键重试——按会话当前位置重新迁移,不用陈旧路径
58
72
  - **📂 空分组清理 / 打开文件夹**:只列出真正零成员的工作区;组菜单可直达系统文件管理器
59
73
  - **🤖 Agent 工具**:`mover_list_sessions` / `mover_move_session` / `mover_repair_sessions` 三个模型可调用工具——在对话里说「把这个会话挪到某组」即可触发与面板完全相同的备份回滚管线;迁移与修复执行前经宿主审批弹窗确认,未装 dsh-tools 的宿主自动停用
60
74
 
@@ -106,6 +120,7 @@ dsh plugin --profile web add "link:C:/path/to/dsh-workspace-mover"
106
120
  | 移动失败的 toast | 操作前有自动备份、失败会回滚;按提示处理后重试。详细原因见宿主日志中的 `MOVE FAILED` |
107
121
  | 移动成功但侧边栏没归位 | 刷新页面即可 |
108
122
  | 有些会话从侧边栏不见了 | 打开 **设置 → 会话救援** 自动扫描并找回 |
123
+ | 升级 DSH 0.2 后,旧会话切换模型报 `Unknown agent preset` | 0.2 不再读取 0.1.x 旧格式的自定义预设目录,需迁移为声明行——与插件无关,完整步骤见 [docs/dsh-0.2-preset-migration.md](docs/dsh-0.2-preset-migration.md) |
109
124
 
110
125
  </details>
111
126
 
@@ -167,7 +182,7 @@ dsh plugin --profile web add "link:C:/path/to/dsh-workspace-mover"
167
182
 
168
183
  ### 对话里迁移(Agent 工具)
169
184
 
170
- 1. 装有 `dsh-tools` 的宿主(web 默认)自动注册三个模型工具:清单(只读)、迁移、一键修复;
185
+ 1. 装有 `dsh-tools` 的宿主(web 与桌面版默认)自动注册三个模型工具:清单(只读)、迁移、一键修复;
171
186
  2. 在对话里说「列出我的会话」「把会话 X 挪到 Y 组」「跑一次修复」——模型先用清单工具把名字解析成精确 id,再执行动作;
172
187
  3. 迁移与修复在动手前经宿主审批弹窗请你确认(与面板确认同级);拒绝或关闭弹窗则不执行;
173
188
  4. 宿主没有 `dsh-tools` 时自动停用,不影响面板与拖拽。
@@ -199,8 +214,10 @@ dsh plugin --profile web add "link:C:/path/to/dsh-workspace-mover"
199
214
 
200
215
  | 同装插件类目 | 兼容性 | 说明 |
201
216
  | --- | --- | --- |
217
+ | 官方内置插件(0.2 起的 8 个:终端 / 网页搜索 / 子智能体等) | ✅ 无冲突 | 功能面不相交;Agent 工具经官方 `dsh-tools` 注册表共存,本插件工具名以 `mover_` 前缀隔离 |
202
218
  | 侧边栏增强 / better-sidebar / 终端 / 费用统计 / 记忆 / 导出分享 | ✅ 无冲突 | 面板与数据面完全不同 |
203
219
  | 归档管理类插件 | ✅ 兼容 | 双方都使用官方归档数据,数据层一致(面板功能可能有些重复) |
220
+ | 回收站 / 会话删除类插件 | ✅ 数据隔离 | 各插件维护独立的回收站目录,互不读写;建议只启用一家的删除入口,避免操作混淆 |
204
221
  | 排序 / 置顶类插件 | ⚠️ 基本兼容 | 移动后会精确重排「最近更新」;与置顶/排序插件并存通常无碍,仅各自面板展示顺序可能不完全一致 |
205
222
  | 重画侧边栏的插件(自绘工作区树) | ⚠️ 降级共存 | 对方若替换官方侧边栏结构,本插件可能暂时无法识别会话行——表现为功能不触发,**不会损坏数据** |
206
223
  | 其他会话移动器 | ❌ 建议二选一 | 同类插件可能重复处理同一次拖拽;本插件已覆盖移动 / 批量 / 合并 / 归位 / 归档恢复 |
@@ -219,17 +236,30 @@ dsh plugin --profile web add "link:C:/path/to/dsh-workspace-mover"
219
236
  - **其他已知组合**:dsh-market `1.45.1`。不修改 DSH 源码,仅通过官方扩展点接入。
220
237
  - **质量门禁**:提交前运行 `npm run check`(语法检查 + 测试 + `npm pack --dry-run`);CI 在 ubuntu / windows / macos × Node 22/24 上执行同等步骤。
221
238
  - **市场卡片的「宿主要求未知」**:GitHub-only 插件没有 npm 发布清单时,dsh-market 可能无法从远端元数据推导宿主版本;显示「未知」**不代表**运行时不兼容。
239
+ - **升级到 DSH 0.2 的预设迁移**:0.2 不再读取 0.1.x 旧格式的自定义预设目录(`$DSH_HOME\.agent-presets\`),旧会话恢复会报 `Unknown agent preset`(与本插件无关,属宿主变更)。迁移步骤与实测记录见 [docs/dsh-0.2-preset-migration.md](docs/dsh-0.2-preset-migration.md)。
222
240
  - **卸载可逆**:移除插件不会删除会话档案、工作区或官方记账。插件自己的历史、任务、备份和回收站数据仍保留在插件数据目录(见 [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md));移除后刷新或重启 DSH 即可清理注入的界面。
223
241
  - **共存建议**:不要与另一个跨工作区拖拽移动器同时启用;其他类型插件可按上表共存。
224
242
 
225
243
  部分高级操作依赖较新的 DSH 能力;不支持时界面会明确提示,而不是静默失败。
226
244
 
227
- ## 🔐 安全承诺
245
+ ## 🔐 安全台账(Safety Ledger)
246
+
247
+ 每一次迁移都走同一条带安全边界的管线:**字节级备份先行 → 逐步回滚 → 迁移后校验 → 恢复账本兜底**。任何一步失败都会回到移动前的状态;连回滚都失败的极端情况会写入持久化恢复记录并在救援面板标红,等待人工确认——数据永不消失。
248
+
249
+ **Never-does(绝不):**
250
+
251
+ - 绝不执行任何 git 写操作(commit / stash / reset / checkout 都不碰);
252
+ - 绝不在字节级备份落盘之前改动任何文件;
253
+ - 绝不静默丢弃会话数据——恢复不了的极端情况会明确标红并保留现场;
254
+ - 绝不绕过确认执行破坏性操作(对话内的迁移 / 修复还须经宿主审批弹窗);
255
+ - 绝不上传任何本地数据——插件无网络请求,全部工作在本机完成。
256
+
257
+ **具体承诺:**
228
258
 
229
259
  - **移动前自动备份**;搬运落地后再核对会话身份与路径,不符则整体回退;
230
260
  - **失败时恢复到移动前状态**(文件与记账一并恢复);
231
261
  - **删除先进回收站**,可还原到原位置或任意分组;彻底删除需二次确认;
232
- - **无法自动恢复时**,会在救援面板明确标出,需要人工确认;会话文件与备份始终保留,**绝不静默丢弃**;
262
+ - **无法自动恢复时**,会在救援面板明确标出,需要人工确认;会话文件与备份始终保留;
233
263
  - **近期打开过、仍驻留在 Harness 内存中的会话**不能直接删除,需重启 Harness 后再删(界面会给出明确提示)。
234
264
 
235
265
  > 失败路径优先保数据,不静默丢会话。<br>
@@ -245,6 +275,13 @@ dsh plugin --profile web add "link:C:/path/to/dsh-workspace-mover"
245
275
 
246
276
  ## 🆕 最近版本
247
277
 
278
+ ### v2.1.1 · 2026-10-05
279
+
280
+ - `mover.doctor` 自检:面板新增「自检」按钮 + `mover_doctor` 模型工具——服务、数据目录、恢复记录、工作区路径约 13 项检查,pass/warn/fail 三态汇报,DSH 升级后先自检再动手
281
+ - 信任资产:`dsh://` 一键安装深链按钮、dsh.so 风险徽章(已收录且 L5 实测 / risk low)、dsh-plugin-registry 收录徽章、"DSH tested 0.2.0-rc.2" 徽章
282
+ - 兼容性报告:`docs/compatibility/` 落地桌面版实测记录,此后每适配一个 DSH 版本新增带日期报告
283
+ - 安全承诺章节品牌化为「安全台账(Safety Ledger)」+ never-does 清单
284
+
248
285
  ### v2.1.0 · 2026-10-04
249
286
 
250
287
  - Agent 工具:`mover_list_sessions`(只读清单)/ `mover_move_session`(迁移单个会话)/ `mover_repair_sessions`(一键修复)注册进官方 `dsh-tools`,在对话里即可调用
package/client/client.js CHANGED
@@ -131,6 +131,22 @@ window.__ModuleLoader__.load({
131
131
  sessionBusy: "该会话正在处理中(可能有迁移 / 还原 / 删除操作尚未完成),请稍后再试。",
132
132
  contextStale: "插件上下文已失效(客户端热重载导致)。请刷新页面(或重启应用)后重试,功能会自动恢复。",
133
133
  recoveryNote: "检测到 {n} 条自动回滚也失败的记录,需人工确认:会话文件与备份均保留在原处,未丢失。请勿手动清理 pluginData 目录,查看 recovery.json 或联系开发者处理。",
134
+ doctorBtn: "自检",
135
+ doctorChecking: "自检中…",
136
+ doctorSummary: "自检完成:{pass} 通过 · {warn} 警告 · {fail} 失败",
137
+ doctorHostRegistry: "工作区注册表服务",
138
+ doctorHostPersistence: "会话持久化服务",
139
+ doctorHostProjection: "投影缓存",
140
+ doctorHostArchive: "归档通道",
141
+ doctorHostAgents: "Agents 服务",
142
+ doctorHostApproval: "审批接缝",
143
+ doctorAgentTools: "Agent 工具",
144
+ doctorDataRecycle: "回收站",
145
+ doctorDataBackups: "备份目录",
146
+ doctorDataHistory: "移动历史",
147
+ doctorDataTasks: "任务记录",
148
+ doctorRecovery: "恢复记录",
149
+ doctorWorkspaces: "工作区路径",
134
150
  repairAllBtn: "一键修复",
135
151
  repairAllDone: "✓ 已修复 {n} · 跳过 {s} · 失败 {f}",
136
152
  skipNeedsTarget: "需选择目标分组",
@@ -262,6 +278,22 @@ window.__ModuleLoader__.load({
262
278
  sessionBusy: "This session is busy: a move / restore / delete is still in flight. Try again in a moment.",
263
279
  contextStale: "The plugin context went stale (client-side hot reload). Refresh the page (or restart the app) and the features will work again.",
264
280
  recoveryNote: "{n} record(s) need manual recovery: automatic rollback also failed. Session files and backups are all kept in place — nothing was lost. Do not clean the pluginData directory by hand; inspect recovery.json or contact the developer.",
281
+ doctorBtn: "Doctor",
282
+ doctorChecking: "Running checks…",
283
+ doctorSummary: "Doctor finished: {pass} pass · {warn} warn · {fail} fail",
284
+ doctorHostRegistry: "Workspace registry service",
285
+ doctorHostPersistence: "Session persistence service",
286
+ doctorHostProjection: "Projection cache",
287
+ doctorHostArchive: "Archive channel",
288
+ doctorHostAgents: "Agents service",
289
+ doctorHostApproval: "Approval seam",
290
+ doctorAgentTools: "Agent tools",
291
+ doctorDataRecycle: "Recycle bin",
292
+ doctorDataBackups: "Backups",
293
+ doctorDataHistory: "Move history",
294
+ doctorDataTasks: "Task records",
295
+ doctorRecovery: "Recovery records",
296
+ doctorWorkspaces: "Workspace paths",
265
297
  repairAllBtn: "Fix all",
266
298
  repairAllDone: "✓ Fixed {n} · skipped {s} · failed {f}",
267
299
  skipNeedsTarget: "needs a target group",
@@ -335,6 +367,7 @@ window.__ModuleLoader__.load({
335
367
  .wsm-mono{font-family:ui-monospace,Consolas,Menlo,monospace}
336
368
  .wsm-badge{flex:none;font-size:11px;padding:1px 7px;border-radius:99px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.15));color:var(--dsw-alias-label-secondary,#666)}
337
369
  .wsm-badge.err{color:var(--dsw-alias-state-error-primary,#dc2626);border-color:currentColor}
370
+ .wsm-badge.warn{color:var(--dsw-alias-state-warning-primary,#d97706);border-color:currentColor}
338
371
  .wsm-cwd{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-tertiary,#999)}
339
372
  .wsm-select{font:inherit;font-size:12px;background:var(--dsw-specific-input-major,#fff);color:var(--dsw-alias-label-primary,#111);border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.15));border-radius:6px;padding:3px 6px;max-width:220px}
340
373
  .wsm-input{flex:1.4;min-width:170px;font:inherit;font-size:12px;background:var(--dsw-specific-input-major,#fff);color:var(--dsw-alias-label-primary,#111);border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.15));border-radius:6px;padding:3px 7px}
@@ -637,6 +670,7 @@ window.__ModuleLoader__.load({
637
670
  const [trash, setTrash] = useState(null);
638
671
  const [backups, setBackups] = useState(null);
639
672
  const [tasks, setTasks] = useState(null);
673
+ const [doctor, setDoctor] = useState(null);
640
674
  const [query, setQuery] = useState("");
641
675
 
642
676
  const call = async (endpoint, payload) => {
@@ -675,6 +709,21 @@ window.__ModuleLoader__.load({
675
709
 
676
710
  useEffect(() => { void runScan(); }, []);
677
711
 
712
+ // 自检(v2.2):只读诊断,结果常驻面板直到下次运行或重新扫描
713
+ const runDoctorNow = async () => {
714
+ setBusy(true);
715
+ setNote(t("doctorChecking"));
716
+ try {
717
+ const value = await call("mover.doctor");
718
+ setDoctor(value);
719
+ setNote(t("doctorSummary", value.summary));
720
+ } catch (err) {
721
+ setNote(t("failed", { msg: errMsg(err) }));
722
+ } finally {
723
+ setBusy(false);
724
+ }
725
+ };
726
+
678
727
  const relink = async (item) => {
679
728
  const target = picked[item.sessionId];
680
729
  if (!target) return void setNote(t("pickTarget"));
@@ -1185,6 +1234,7 @@ window.__ModuleLoader__.load({
1185
1234
  h("div", { className: "wsm-scanrow" },
1186
1235
  h("button", { className: "wsm-btn small primary", disabled: busy, onClick: () => void runScan() }, busy ? t("scanning") : t("scan")),
1187
1236
  h("button", { className: "wsm-btn small", disabled: busy, onClick: () => void repairAll() }, t("repairAllBtn")),
1237
+ h("button", { className: "wsm-btn small", disabled: busy, onClick: () => { setDoctor(null); void runDoctorNow(); } }, busy ? t("doctorChecking") : t("doctorBtn")),
1188
1238
  h("input",
1189
1239
  {
1190
1240
  className: "wsm-input", placeholder: t("filterPh"), value: query, disabled: busy,
@@ -1195,6 +1245,13 @@ window.__ModuleLoader__.load({
1195
1245
  t("scannedN", { n: scan.scanned }) + (summaryParts.length ? ` · ${summaryParts.join(" · ")}` : ""))
1196
1246
  : null
1197
1247
  ),
1248
+ doctor ? h("div", { className: "wsm-list", style: { marginTop: "8px" } },
1249
+ doctor.checks.map((c) => h("div", { className: "wsm-item", key: c.id },
1250
+ h("span", { className: "wsm-badge" + (c.state === "fail" ? " err" : c.state === "warn" ? " warn" : "") },
1251
+ c.state === "pass" ? "✓" : c.state === "warn" ? "⚠" : "✗"),
1252
+ h("span", { className: "wsm-cwd", title: c.detail }, t(`doctor${c.id.split("-").map((w) => w[0].toUpperCase() + w.slice(1)).join("")}`) + (c.detail ? ` — ${c.detail}` : ""))
1253
+ ))
1254
+ ) : null,
1198
1255
  brokenWorkspaces.length > 0 ? h(Caption, { text: t("wsSection"), help: t("wsHelp") }) : null,
1199
1256
  brokenWorkspaces.length > 0 ? h("div", { className: "wsm-list" },
1200
1257
  brokenWorkspaces.map((row) => h("div", { className: "wsm-item", key: row.workspaceId },
package/lib/index.js CHANGED
@@ -2359,6 +2359,17 @@ function createAgentTools(ctx, dispatchRef) {
2359
2359
  await approveOrThrow(ctx, exec, 'mover_repair_sessions', 'Run a workspace-mover repair pass (accounting-only fixes for misfiled / unregistered sessions; no files are moved).');
2360
2360
  return await callDispatch(dispatchRef, 'mover.repairAll', {}, exec?.signal);
2361
2361
  }
2362
+ },
2363
+ {
2364
+ name: 'mover_doctor',
2365
+ description: `Run the workspace-mover self-check (read-only diagnostics): host services (registry / persistence / projection cache / archive channel / agents / approval seam), agent-tool registration, data directories (recycle bin, backups, move history, tasks), pending manual-recovery records, and workspace folder presence. Returns per-check pass / warn / fail states with details and a summary — use it after a DSH upgrade or when a feature misbehaves, to see what is degraded before attempting anything else. No user approval needed; nothing is modified.`,
2366
+ parameters: { type: 'object', properties: {}, additionalProperties: false },
2367
+ output: { schema: { type: 'object' }, render: JSON_TEXT },
2368
+ isConcurrencySafe: () => true,
2369
+ timeoutMs: 30000,
2370
+ async execute(args, exec) {
2371
+ return await callDispatch(dispatchRef, 'mover.doctor', {}, exec?.signal);
2372
+ }
2362
2373
  }
2363
2374
  ];
2364
2375
  return tools;
@@ -2378,6 +2389,86 @@ function registerAgentTools(ctx, dispatchRef, logger, agentToolsState = { regist
2378
2389
  }
2379
2390
  //#endregion
2380
2391
 
2392
+ //#region doctor(v2.2):只读自检——服务、数据目录、恢复记录、工作区路径
2393
+ // 纯 ok(value),无锁无变更;刻意不跑全量 scan,保证秒级返回。
2394
+ async function runDoctor(ctx, agentToolsState) {
2395
+ const checks = [];
2396
+ const add = (id, state, detail = '') => checks.push({ id, state, detail });
2397
+ const pass = (id, detail) => add(id, 'pass', detail);
2398
+ const warn = (id, detail) => add(id, 'warn', detail);
2399
+ const fail = (id, detail) => add(id, 'fail', detail);
2400
+
2401
+ const registry = ctx?.workspaceRegistry;
2402
+ const persistence = ctx?.sessionPersistence;
2403
+
2404
+ if (!registry) fail('host-registry', 'workspace registry service unavailable');
2405
+ else {
2406
+ let count = 0;
2407
+ try { count = registry.list().length; } catch { /* list 失败按 0 报告 */ }
2408
+ pass('host-registry', `${count} workspaces`);
2409
+ }
2410
+
2411
+ if (!persistence) fail('host-persistence', 'session persistence service unavailable');
2412
+ else if (typeof persistence.root === 'string' && dirExists(persistence.root)) pass('host-persistence', String(persistence.root));
2413
+ else fail('host-persistence', `sessions root missing: ${persistence.root ?? '(none)'}`);
2414
+
2415
+ const projection = projectionTable(ctx);
2416
+ const projectionRead = typeof projection?.get === 'function';
2417
+ const projectionWrite = projectionRead && typeof projection?.put === 'function' && typeof projection?.delete === 'function';
2418
+ if (projectionWrite) pass('host-projection', 'read + write');
2419
+ else if (projectionRead) warn('host-projection', 'read-only — projection title restore degraded');
2420
+ else warn('host-projection', 'absent — projection title restore degraded');
2421
+
2422
+ if (typeof registry?.enqueueOperation === 'function') pass('host-archive', 'archive-set channel ready');
2423
+ else warn('host-archive', 'archive-set changes degraded');
2424
+
2425
+ if (peekService(ctx, 'agents')) pass('host-agents', 'running-state checks active');
2426
+ else warn('host-agents', 'running-state checks degraded');
2427
+
2428
+ if (typeof ctx?.approval?.request === 'function') pass('host-approval', 'approval seam ready');
2429
+ else warn('host-approval', 'agent approval seam absent — conversational moves rely on model discipline');
2430
+
2431
+ if (agentToolsState?.registered) pass('agent-tools', 'registered with dsh-tools');
2432
+ else warn('agent-tools', 'not registered — dsh-tools absent on this host');
2433
+
2434
+ try { const trash = await listTrash(ctx); pass('data-recycle', `${trash.items.length} entries`); }
2435
+ catch (err) { fail('data-recycle', String(err?.message ?? err).slice(0, 140)); }
2436
+
2437
+ try { const backups = await listBackups(ctx); pass('data-backups', `${backups.items.length} session groups`); }
2438
+ catch (err) { fail('data-backups', String(err?.message ?? err).slice(0, 140)); }
2439
+
2440
+ try { const history = await listHistory(); pass('data-history', `${history.items?.length ?? 0} records`); }
2441
+ catch (err) { fail('data-history', String(err?.message ?? err).slice(0, 140)); }
2442
+
2443
+ try { const tasks = await listTasks(); pass('data-tasks', `${tasks.items?.length ?? 0} records`); }
2444
+ catch (err) { fail('data-tasks', String(err?.message ?? err).slice(0, 140)); }
2445
+
2446
+ const recovered = recoveryCount();
2447
+ if (recovered === 0) pass('recovery', 'no manual-recovery records');
2448
+ else warn('recovery', `${recovered} record(s) need manual review — see the rescue panel`);
2449
+
2450
+ if (!registry) warn('workspaces', 'skipped — registry unavailable');
2451
+ else {
2452
+ let total = 0;
2453
+ let missing = [];
2454
+ try {
2455
+ for (const entity of registry.list()) {
2456
+ total++;
2457
+ try { if (!dirExists(entity.path)) missing.push(String(entity.id)); }
2458
+ catch { missing.push(String(entity.id)); }
2459
+ }
2460
+ } catch { /* 同上 */ }
2461
+ if (total === 0) warn('workspaces', 'no registered workspaces');
2462
+ else if (missing.length === 0) pass('workspaces', `${total} paths all present`);
2463
+ else warn('workspaces', `${missing.length}/${total} folders missing: ${missing.slice(0, 5).join(', ')}`);
2464
+ }
2465
+
2466
+ const summary = { pass: 0, warn: 0, fail: 0 };
2467
+ for (const c of checks) summary[c.state]++;
2468
+ return { checks, summary, generatedAt: new Date().toISOString() };
2469
+ }
2470
+ //#endregion
2471
+
2381
2472
  export function apply(ctx) {
2382
2473
  const logger = ctx.logger ?? console;
2383
2474
  const dispatchRef = { fn: null }; // agent 工具在调用期才用到 dispatch,挂载时序无关
@@ -2419,6 +2510,8 @@ export function apply(ctx) {
2419
2510
  recoveryCount: recoveryCount()
2420
2511
  });
2421
2512
  }
2513
+ case 'mover.doctor':
2514
+ return ok(await runDoctor(ctx, agentToolsState));
2422
2515
  case 'mover.workspaces':
2423
2516
  return ok(await listWorkspaces(ctx));
2424
2517
  case 'mover.history':
package/package.json CHANGED
@@ -1,63 +1,63 @@
1
- {
2
- "name": "dsh-workspace-mover",
3
- "version": "2.1.0",
4
- "description": "在 DSH 侧边栏安全迁移会话:真迁移原始档案,支持失联会话恢复、移动历史、一键撤回和 agent 工具。",
5
- "type": "module",
6
- "main": "lib/index.js",
7
- "exports": {
8
- ".": "./lib/index.js",
9
- "./client": "./client/client.js",
10
- "./package.json": "./package.json"
11
- },
12
- "files": [
13
- "lib",
14
- "client",
15
- "test",
16
- "cordis.patch.yml",
17
- "README.md",
18
- "docs/ARCHITECTURE.md",
19
- "docs/media",
20
- "LICENSE"
21
- ],
22
- "scripts": {
23
- "test": "node --test test/core.test.mjs test/e2e-sandbox.test.mjs test/traffic-badge.test.mjs test/release-downloads.test.mjs",
24
- "check:syntax": "node --check lib/index.js && node --check client/client.js",
25
- "check:pack": "npm pack --dry-run",
26
- "check": "npm run check:syntax && npm test && npm run check:pack"
27
- },
28
- "keywords": [
29
- "deepseek-harness",
30
- "dsh",
31
- "dsh-plugin",
32
- "workspace",
33
- "session",
34
- "session-migration",
35
- "drag-and-drop",
36
- "agent-tools"
37
- ],
38
- "engines": {
39
- "node": ">=22",
40
- "dsh": ">=0.1.5-rc.1"
41
- },
42
- "license": "MIT",
43
- "repository": {
44
- "type": "git",
45
- "url": "git+https://github.com/PianoPrince/dsh-workspace-mover.git"
46
- },
47
- "bugs": {
48
- "url": "https://github.com/PianoPrince/dsh-workspace-mover/issues"
49
- },
50
- "homepage": "https://github.com/PianoPrince/dsh-workspace-mover#readme",
51
- "dsh": {
52
- "bundle": {
53
- "patch": "./cordis.patch.yml"
54
- },
55
- "client": {
56
- "inject": [
57
- "@deepseek-ai/dsh-client-connection",
58
- "@deepseek-ai/dsh-client-ui-slots"
59
- ],
60
- "platform": "web"
61
- }
62
- }
63
- }
1
+ {
2
+ "name": "dsh-workspace-mover",
3
+ "version": "2.1.1",
4
+ "description": "在 DSH 侧边栏安全迁移会话:真迁移原始档案,支持失联会话恢复、移动历史、一键撤回和 agent 工具。",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./client": "./client/client.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "files": [
13
+ "lib",
14
+ "client",
15
+ "test",
16
+ "cordis.patch.yml",
17
+ "README.md",
18
+ "docs/ARCHITECTURE.md",
19
+ "docs/media",
20
+ "LICENSE"
21
+ ],
22
+ "scripts": {
23
+ "test": "node --test test/core.test.mjs test/e2e-sandbox.test.mjs test/traffic-badge.test.mjs test/release-downloads.test.mjs",
24
+ "check:syntax": "node --check lib/index.js && node --check client/client.js",
25
+ "check:pack": "npm pack --dry-run",
26
+ "check": "npm run check:syntax && npm test && npm run check:pack"
27
+ },
28
+ "keywords": [
29
+ "deepseek-harness",
30
+ "dsh",
31
+ "dsh-plugin",
32
+ "workspace",
33
+ "session",
34
+ "session-migration",
35
+ "drag-and-drop",
36
+ "agent-tools"
37
+ ],
38
+ "engines": {
39
+ "node": ">=22",
40
+ "dsh": ">=0.1.5-rc.1"
41
+ },
42
+ "license": "MIT",
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/PianoPrince/dsh-workspace-mover.git"
46
+ },
47
+ "bugs": {
48
+ "url": "https://github.com/PianoPrince/dsh-workspace-mover/issues"
49
+ },
50
+ "homepage": "https://github.com/PianoPrince/dsh-workspace-mover#readme",
51
+ "dsh": {
52
+ "bundle": {
53
+ "patch": "./cordis.patch.yml"
54
+ },
55
+ "client": {
56
+ "inject": [
57
+ "@deepseek-ai/dsh-client-connection",
58
+ "@deepseek-ai/dsh-client-ui-slots"
59
+ ],
60
+ "platform": "web"
61
+ }
62
+ }
63
+ }
@@ -1825,7 +1825,7 @@ const FAKE_EXEC = () => ({ agent: { session: {} }, signal: undefined });
1825
1825
  test('v2.1 apply 在带 dsh-tools 的宿主上注册三个 agent 工具,status 汇报能力位', async () => {
1826
1826
  const { registered } = mountToolStubs();
1827
1827
  apply(ctx);
1828
- assert.deepEqual(registered.map((t) => t.name), ['mover_list_sessions', 'mover_move_session', 'mover_repair_sessions']);
1828
+ assert.deepEqual(registered.map((t) => t.name), ['mover_list_sessions', 'mover_move_session', 'mover_repair_sessions', 'mover_doctor']);
1829
1829
  for (const def of registered) {
1830
1830
  assert.equal(typeof def.execute, 'function', `${def.name}.execute`);
1831
1831
  assert.equal(typeof def.output?.render, 'function', `${def.name}.output.render`);
@@ -1919,3 +1919,44 @@ test('v2.1 mover_repair_sessions 走审批并修复未记账夹具会话', async
1919
1919
  assert.equal(approvalCalls.length, 1);
1920
1920
  assert.equal(approvalCalls[0].toolName, 'mover_repair_sessions');
1921
1921
  });
1922
+
1923
+ // ===== v2.2.0 mover.doctor 自检 =====
1924
+
1925
+ test('v2.2 mover.doctor 沙箱全绿:服务/数据/工作区全部 pass', async () => {
1926
+ apply(ctx);
1927
+ const res = await call('mover.doctor');
1928
+ assert.equal(res.ok, true, JSON.stringify(res));
1929
+ const { checks, summary } = res.value;
1930
+ assert.ok(checks.length >= 12, `至少 12 项检查,实际 ${checks.length}`);
1931
+ for (const c of checks) assert.ok(['pass', 'warn', 'fail'].includes(c.state), `state 合法: ${c.id}`);
1932
+ assert.equal(summary.fail, 0, `不应有 fail: ${JSON.stringify(checks.filter((c) => c.state === 'fail'))}`);
1933
+ const ids = new Set(checks.map((c) => c.id));
1934
+ for (const expected of ['host-registry', 'host-persistence', 'host-projection', 'data-recycle', 'data-backups', 'recovery', 'workspaces']) {
1935
+ assert.ok(ids.has(expected), `缺少检查项 ${expected}`);
1936
+ }
1937
+ assert.equal(typeof res.value.generatedAt, 'string');
1938
+ });
1939
+
1940
+ test('v2.2 mover.doctor 降级:摘除注册表后 fail + warn,但不抛错', async () => {
1941
+ apply(ctx);
1942
+ ctx.workspaceRegistry = undefined;
1943
+ const res = await call('mover.doctor');
1944
+ assert.equal(res.ok, true, 'doctor 本身永远 ok');
1945
+ const registryCheck = res.value.checks.find((c) => c.id === 'host-registry');
1946
+ assert.equal(registryCheck.state, 'fail');
1947
+ const workspacesCheck = res.value.checks.find((c) => c.id === 'workspaces');
1948
+ assert.equal(workspacesCheck.state, 'warn');
1949
+ assert.equal(res.value.summary.fail >= 1, true);
1950
+ });
1951
+
1952
+ test('v2.2 mover.doctor 恢复记录告警:recovery.json 存在时 warn', async () => {
1953
+ apply(ctx);
1954
+ const dir = join(root, 'workspace-mover');
1955
+ mkdirSync(dir, { recursive: true });
1956
+ writeFileSync(join(dir, 'recovery.json'), JSON.stringify({ version: 1, records: [{ id: 'rec-x', kind: 'delete', phase: 'test', createdAt: new Date().toISOString() }] }));
1957
+ const res = await call('mover.doctor');
1958
+ assert.equal(res.ok, true);
1959
+ const recovery = res.value.checks.find((c) => c.id === 'recovery');
1960
+ assert.equal(recovery.state, 'warn');
1961
+ assert.match(recovery.detail, /1 record/);
1962
+ });
@@ -1,6 +1,6 @@
1
1
  import test from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- import { buildBadgeFiles, buildState, mergeHistory, normalizeClonesResponse, normalizePathsResponse, normalizeReferrersResponse, normalizeViewsResponse, parseHistory } from '../.github/scripts/traffic-badge-core.mjs';
3
+ import { buildBadgeFiles, buildState, buildStructuredDaily, mergeHistory, normalizeClonesResponse, normalizePathsResponse, normalizeReferrersResponse, normalizeViewsResponse, parseHistory } from '../.github/scripts/traffic-badge-core.mjs';
4
4
 
5
5
  const snapshot = {
6
6
  count: 12,
@@ -55,12 +55,26 @@ test('schema migration preserves the old value as a legacy estimate only', () =>
55
55
  assert.equal(state.lastWindow.count, 12);
56
56
  });
57
57
 
58
- test('badge exposes only observed GitHub clones and 14d uniques', () => {
58
+ test('badge exposes cumulative clones and cumulative uniques since the observation start', () => {
59
59
  const state = buildState({}, parseHistory('{"date":"2026-09-08","count":5,"uniques":3}\n{"date":"2026-09-09","count":7,"uniques":4}\n'), normalizeClonesResponse(snapshot), '2026-09-10T00:00:00Z');
60
+ assert.equal(state.cumulativeUniques, 7);
60
61
  const files = buildBadgeFiles(state);
61
62
  const total = JSON.parse(files['wsm-clones-total.json'].content);
62
63
  assert.equal(total.label, 'GitHub clones observed');
63
- assert.match(total.message, /12 since 2026-09-08/);
64
- assert.match(total.message, /7 unique/);
64
+ assert.equal(total.message, '12 since 2026-09-08 · 7 unique');
65
65
  assert.equal(files['wsm-clones-14d.json'], undefined);
66
66
  });
67
+
68
+ test('structured daily backfills JSONL-only early days without clobbering views', () => {
69
+ const existing = [{ date: '2026-09-09', clones: { date: '2026-09-09', count: 7, uniques: 4 }, views: { date: '2026-09-09', count: 2, uniques: 1 } }];
70
+ const historyRows = parseHistory('{"date":"2026-09-07","count":1,"uniques":1}\n{"date":"2026-09-08","count":5,"uniques":3}\n{"date":"2026-09-09","count":99,"uniques":99}\n');
71
+ const cloneDays = [{ date: '2026-09-09', count: 7, uniques: 4 }];
72
+ const viewDays = [{ date: '2026-09-09', count: 2, uniques: 1 }];
73
+ const daily = buildStructuredDaily(existing, historyRows, cloneDays, viewDays);
74
+ assert.deepEqual(daily.map((row) => row.date), ['2026-09-07', '2026-09-08', '2026-09-09']);
75
+ assert.deepEqual(daily[0].clones, { date: '2026-09-07', count: 1, uniques: 1 });
76
+ assert.equal(daily[0].views, undefined, 'JSONL 回填行没有 views 数据');
77
+ // JSONL 与快照冲突时快照优先:09-09 保持 7/4,且 views 不丢
78
+ assert.deepEqual(daily[2].clones, { date: '2026-09-09', count: 7, uniques: 4 });
79
+ assert.deepEqual(daily[2].views, { date: '2026-09-09', count: 2, uniques: 1 });
80
+ });