@gehennawu/dsh-service 1.9.8 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -9,9 +9,9 @@
9
9
  <em>DeepSeek Harness (DSH) Web 服务控制与运维插件。</em>
10
10
  </p>
11
11
 
12
- [![Version](https://img.shields.io/badge/version-1.9.8-3b82f6.svg?style=flat-square)](package.json)
12
+ [![Version](https://img.shields.io/badge/version-2.0.0-3b82f6.svg?style=flat-square)](package.json)
13
13
  [![License: MIT](https://img.shields.io/badge/License-MIT-10b981.svg?style=flat-square)](LICENSE)
14
- [![DSH Compatibility](https://img.shields.io/badge/DSH-%E2%89%A50.1.1--rc.2%20%C2%B7%20compatible%20with%200.1.7--alpha.2-6366f1.svg?style=flat-square)](https://github.com/deepseek-ai/deepseek-harness)
14
+ [![DSH Compatibility](https://img.shields.io/badge/DSH-%E2%89%A50.1.1--rc.2%20%C2%B7%20compatible%20with%200.2.0--rc.1-6366f1.svg?style=flat-square)](https://github.com/deepseek-ai/deepseek-harness)
15
15
  [![Cordis](https://img.shields.io/badge/Cordis-v4.x-f59e0b.svg?style=flat-square)](https://cordis.moe/)
16
16
  [![Platform](https://img.shields.io/badge/platform-DSH%20Web-ec4899.svg?style=flat-square)](https://github.com/gehennawu/dsh-service)
17
17
  [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://github.com/gehennawu/dsh-service/issues)
@@ -74,7 +74,7 @@ Under **Plugins → Plugin configuration**, twelve host-level switches: **Health
74
74
 
75
75
  - Shows the current DSH and plugin versions, linking to GitHub Releases
76
76
  - Automatically checks npm **stable + preview** (latest / next dist-tags)
77
- - "What's new" sits right after the current version number and is always available: it opens the notes for **the version you are running**; when a new version exists, the status text ("New version: x.y.z") is clickable and expands **the new version's** release info. Either way the host fetches the body from the GitHub Releases API and the client renders it in place (plain Markdown — no iframe, no navigation), with the version number, publish date and a pre-release tag; a missing Release or a failed read each get their own message; clicking anywhere outside the version card or pressing Escape closes it
77
+ - "What's new" sits right after the current version number and is always available: it opens the notes for **the version you are running**; when a new version exists, the status text ("New version: x.y.z") is clickable and expands **the new version's** release info. Either way the host fetches the body from the GitHub Releases API and the client renders it in place (plain Markdown — no iframe, no navigation), with the version number, publish date and a pre-release tag; a missing Release or a failed read each get their own message; clicking anywhere outside the version card or pressing Escape closes it. Both rows lay their version number and "What's new" button out on **one shared set of columns**, with the version number right-aligned so that the gap before the button is fixed — they line up **regardless of version-number length**, since a longer version only widens the whole column instead of knocking one row out of place; narrow containers (phones and narrow settings panels) keep the same alignment — the version number absorbs the row's slack and right-aligns, so both rows still share their column edges
78
78
  - One-click upgrade with automatic restart; when no process manager is detected, it confirms the consequences first, keeps running, and shows manual-restart instructions
79
79
  - Between the upgrade landing and the process restart (common in a manual-launch environment) the version row reads "Installed X — restart to take effect" and the upgrade button is withdrawn; reopening the panel or refreshing the page keeps that state until the process is restarted
80
80
 
@@ -150,8 +150,9 @@ Under **Plugins → Plugin configuration**, twelve host-level switches: **Health
150
150
  - Backup records use light two-line rows (name on the first line, size · time on the second, separator layout; same style as the session list)
151
151
 
152
152
  - Creates `.tar.gz` archives of sessions, configuration, and plugin-profile manifests; sessions are snapshotted through the persistence layer's stable-read seam (active-agent writes no longer fail creation), and creation shows one continuous phase progress bar (copy / pack / verify / publish, step counter 1/4–4/4, real percentage during copy)
153
+ - **DSH version at backup time**: the archive filename carries the DSH version running when the backup was taken (`dsh-backup-YYYYMMDD-HHmmss-dsh0.1.7-rc.1.tar.gz`), and `meta/backup.json` inside the archive records the DSH version, plugin version, and creation time; both the panel row and the restore preflight show it, so you can tell which version a snapshot came from (restoring across a downgrade cannot read newer session formats). Archives written by older plugin versions have no version segment: they still list and restore normally, they simply show no version line, and import accepts filenames both with and without the segment
153
154
  - Export download / import upload / delete (two-step confirmation); unlimited, never auto-pruned; imported archives must pass the same integrity inspection used by restore
154
- - **Integrity inspection** validates gzip/tar structure, paths, and entry types. Only `sessions`, the three allowlisted config files, and `profiles/<name>/package.json` are accepted; traversal, links, special files, unknown content, corrupt archives, and invalid profile manifests are rejected
155
+ - **Integrity inspection** validates gzip/tar structure, paths, and entry types. Only `sessions`, the three allowlisted config files, `profiles/<name>/package.json`, and `meta/backup.json` are accepted; traversal, links, special files, unknown content, corrupt archives, and invalid profile manifests are rejected
155
156
  - **Restore preflight** produces a single-use five-minute plan showing the full sessions replacement, config replacements/removals, and profile-manifest updates. Final commit rechecks the archive SHA-256 and current-target fingerprint; any drift rejects the restore
156
157
  - Restore uses a transaction journal and rollback directory: sessions are replaced in full, config is made exact to the snapshot, and profiles update package.json only while keeping node_modules, credentials, and attachments. Managed runtimes restart automatically; manual launches receive hand-restart instructions
157
158
 
@@ -197,7 +198,7 @@ Under **Plugins → Plugin configuration**, twelve host-level switches: **Health
197
198
  - Sidebar becomes a drawer, details column is hidden on mobile (matching the official narrow-screen behavior), modals become full-screen panels, settings left nav a horizontal top strip
198
199
  - The model picker collapses to an icon on phones (≤480 px, mirroring the official narrow-container form; from 481 px up it still shows the model name and reasoning effort) — the plugin's mobile layout widens the composer column, so the official `@container (width<=360px)` collapse rule never fires on 428~440 px devices; this aligns with the official narrow-container form explicitly
199
200
  - The stats line under the composer ("turns/steps · tok/s | tok · cache hit") **stays on one line, centred and straight** on phones: this row's own horizontal padding is tightened (official 32 px → 2 px) along with its gaps to free up width, and the official `justify-content` centres both chips — zero truncation from ~420 px up, proportionally less text cut than the official fixed truncation below that, with no wrapping and no horizontal scrolling; this also works on older DSH (on 0.1.5-rc.2 the row spans the full width, where the earlier sub-item `flex-grow` pushed both chips as a group to the left — they no longer grow) (desktops unaffected)
200
- - The session header is re-flowed on phones (≤560 px): the title row keeps only the **title + preset chip** (the chip hugs the "…" button on the right, the title stays nearly fully visible), while the "N subagents" and "N background jobs running" counter chips park as a pair on the right of the Conversation/Trajectory tab row (tab gap 36→20, tapping a chip still opens its menu); the official crumbs **hard-clip** is eliminated, and layouts ≥561 px keep the official flow
201
+ - The session header is re-flowed on phones (≤560 px): the title row keeps only the **title + preset chip** (the chip hugs the "…" button on the right, the title stays nearly fully visible), while the "N subagents" and "N background jobs running" counter chips park as a pair on the right of the Conversation/Trajectory tab row (tab gap 36→20, tapping a chip still opens its menu); the official crumbs **hard-clip** is eliminated, and layouts ≥561 px keep the official flow. From DSH 0.1.7-rc.1 the official preset label hides itself via `@container (width<=540px)` (the container is the title row, roughly viewports up to 606 px), so the mode chip vanished on every phone; the plugin overrides that back to visible across the whole mobile range (from 390 px up the title stays untruncated, only 320–375 px trade away 15–70 px of title as an ellipsis)
201
202
  - The Agent Team panel stays on screen on phones (<1024 px): the official panel is left-anchored (`left:0`), so sitting in the header's right-hand action slot pushes it off the right edge (measured 112–352 px past the viewport at 320–1023 px). On mobile the session header becomes the positioning context and the panel is re-anchored to the **right with a 16 px inset** and capped at `100vw − 32px`, leaving 16 px on each side; desktop ≥1024 px keeps the official geometry byte-for-byte
202
203
  - Scroll immersion: inside a conversation, swiping down auto-hides the header and composer for full-screen reading (the composer also yields its layout space so the transcript really fills the screen; on reveal, if you are still at the end of the conversation it re-aligns to the bottom). Swipe up, tapping the official "Back to bottom" button, or focusing the composer brings them back (no extra floating button); already sitting at the end of the conversation, a single small backward nudge reveals it right away (no full threshold needed), and after a back-to-bottom tap the resting position gets one extra bottom snap. Programmatic scrolling (streaming pinning, anchor jumps) never triggers it
203
204
  - Swipeable drawers: with both drawers closed, a horizontally dominant swipe rightward anywhere opens the sidebar drawer and a swipe leftward anywhere opens the official right sidebar (DSH 0.1.5+; stays inert when unavailable); once open, a swipe in the reverse direction anywhere closes them. Swipes starting at the screen edge additionally benefit from stolen-gesture completion for browser edge navigation, and horizontal scrolling inside editors never misfires
@@ -299,6 +300,20 @@ Key contracts:
299
300
 
300
301
  ## ⚡ Installation
301
302
 
303
+ ### Option 1: DSH plugin manager (graphical, recommended)
304
+
305
+ 1. Open the **Plugins** page in the left sidebar.
306
+ 2. Click the **+ Add plugin** button in the top-right corner.
307
+ 3. In the dialog, enter `@gehennawu/dsh-service` in the **Package name** field and leave the source at the default **official npm registry**.
308
+ 4. Click **Install** and wait for the official layer to add the package to the current profile (the dialog reports progress and the result).
309
+ 5. The plugin then appears under **Installed**. The host half takes effect after the DSH process restarts; the client half applies on a page refresh.
310
+ - On Web: use this plugin's **Service Control → Restart**, or send `/restart` in a conversation;
311
+ - On DSH Desktop: quit the app and open it again (the desktop app owns the host process, so this plugin never exits it on its own).
312
+
313
+ > Tip: the plugin manager's Install button uses the official npm registry and the official installation path — no command line and no manual profile edits.
314
+
315
+ ### Option 2: Command line
316
+
302
317
  | Method | Command |
303
318
  | --- | --- |
304
319
  | npm (recommended) | `dsh plugin --profile web add @gehennawu/dsh-service` |
@@ -317,7 +332,7 @@ Open DSH Web Settings and select **Service Control**.
317
332
 
318
333
  The plugin only sends an exit signal; it does not start the process again. Without a process manager, restart stops DSH Web.
319
334
 
320
- The plugin passively detects a process manager (environment variables, `/.dockerenv`, `/proc/1/cgroup`, terminal TTY): with Docker/systemd/pm2/supervisord/Kubernetes detected it restarts as usual; when nothing is detected and stdin/stdout are an interactive terminal, it treats the launch as manual — flagged in Health diagnostics and switching one-click upgrade to keep running with manual-restart instructions. Heuristics cannot cover redirected output or wrappers such as NSSM/WinSW; declare `DSH_SERVICE_RUNTIME_ENV=managed|manual` explicitly.
335
+ The plugin passively detects a process manager (environment variables, `/.dockerenv`, `/proc/1/cgroup`, terminal TTY): with Docker/systemd/pm2/supervisord/Kubernetes detected it restarts as usual; when nothing is detected and stdin/stdout are an interactive terminal, it treats the launch as manual — flagged in Health diagnostics and switching one-click upgrade to keep running with manual-restart instructions. Heuristics cannot cover redirected output or wrappers such as NSSM/WinSW; declare `DSH_SERVICE_RUNTIME_ENV=managed|manual|desktop` explicitly.
321
336
 
322
337
  ### Docker Compose
323
338
 
@@ -349,11 +364,15 @@ pm2 start "dsh web --host 127.0.0.1" --name dsh-web
349
364
  | Linux + Docker Compose | Supported | Supported with a restart policy | Verified |
350
365
  | Linux + systemd / pm2 | Expected to work | Managed externally | Not separately tested |
351
366
  | macOS / Windows + pm2 or similar | Not blocked by the code | Managed externally | Not tested |
367
+ | DSH Desktop (Windows, Electron) | Supported | Owned by the desktop app | Verified (real host, 2026-09-28) |
368
+ | DSH Desktop (macOS / Linux, Electron) | Not blocked by the code | Owned by the desktop app | Not verified |
352
369
  | Direct `dsh web` execution | Supported | Not supported | Expected behavior |
353
370
 
354
371
  Requirements: Node.js `>=22`, and a DSH Web installation capable of loading both Host and Client plugin halves. Update checks require access to `registry.npmjs.org`; network failures do not affect other features.
355
372
 
356
- **DSH compatibility statement**: adapted to DSH `0.1.7-rc.1` — session format V4 (V3 logs are migrated by the official layer into a `session.v4.jsonl.zstd` generation file on first read, with the old `session.v3.jsonl.zstd` retained per the format catalog's policy; the detail view automatically archives system events), the `SettingsForms` configuration surface (plugin config now lives in the Profile's `cordis.patch.yml` with hot updates over `loader/volatile-update`, while the legacy `settings.register` remains authoritative on 0.1.5/0.1.6 so the two never cross-contaminate on a shared host), and all existing V3 adaptation (the `system/message` history, the handle-based sessionPersistence, the official right sidebar, the object-shaped turn-process), plus dual-hash compatibility for mobile bottom-row triggers, subagent turn-tail list-slot adaptive compatibility, `plugins.bundle.config` slot injection, and session-detail open fallback through `uiWorkspace`. Older DSH releases (`>=0.1.1-rc.2`) remain supported. The declared range now extends through `0.1.7-rc.1`; static structure review on rc.1 found no new code adaptation requirements, and the plugin-version compatibility gate introduced in rc.1 reads only `peerDependencies` (this plugin declares none, so the gate does not apply); real-host acceptance on DSH `0.1.7-rc.1` has passed. The settings surface and the persistence/layout seams all run in dual shapes detected from runtime capabilities, and adaptation items that target newer structures are naturally inert on older hosts (cosmetic only, no functional loss). Note: sessions written after upgrading cannot be read by older DSH releases — **backups do not restore across a version downgrade**. The plugin marketplace judges compatibility from the `engines.dsh` range in `package.json`, which is the single declaration of the supported range.
373
+ **DSH compatibility statement**: adapted to DSH `0.2.0-rc.1` — session format V4 (V3 logs are migrated by the official layer into a `session.v4.jsonl.zstd` generation file on first read, with the old `session.v3.jsonl.zstd` retained per the format catalog's policy; the detail view automatically archives system events), the `SettingsForms` configuration surface (plugin config now lives in the Profile's `cordis.patch.yml` with hot updates over `loader/volatile-update`, while the legacy `settings.register` remains authoritative on 0.1.5/0.1.6 so the two never cross-contaminate on a shared host), and all existing V3 adaptation (the `system/message` history, the handle-based sessionPersistence, the official right sidebar, the object-shaped turn-process), plus dual-hash compatibility for mobile bottom-row triggers, subagent turn-tail list-slot adaptive compatibility, `plugins.bundle.config` slot injection, and session-detail open fallback through `uiWorkspace`. Older DSH releases (`>=0.1.1-rc.2`) remain supported. The declared range now extends through `0.2.0-rc.1`; source-level and artifact-level reviews of 0.2.0-rc.1 (261 commits) found no new breakage — 17 of 20 existing seams are byte-identical, key official package artifacts are byte-identical, and CSS class anchors and theme tokens show zero drift across artifact grep; the plugin-version compatibility gate introduced in rc.1 reads only `peerDependencies` (this plugin declares none, so the gate does not apply); real-host mounting on DSH `0.2.0-rc.1` is operational. The settings surface and the persistence/layout seams all run in dual shapes detected from runtime capabilities, and adaptation items that target newer structures are naturally inert on older hosts (cosmetic only, no functional loss). Note: sessions written after upgrading cannot be read by older DSH releases — **backups do not restore across a version downgrade**. The plugin marketplace judges compatibility from the `engines.dsh` range in `package.json`, which is the single declaration of the supported range.
374
+
375
+ **DSH Desktop (Electron) statement**: the desktop app reuses the same Web frontend and the same client plugin graph (`platform: web`), so the plugin loads as usual and almost everything works unchanged; only "restart" and "one-click upgrade" differ in meaning. The host is owned by the desktop app, so **restart never calls `process.exit(42)`** there (the shell treats any non-zero exit as a host crash, shows a native failure dialog and does not relaunch it; there is no official programmatic restart RPC) — the Restart button, the `/restart` command and the post-restore restart all guide the user to quit and reopen the desktop app instead; **one-click upgrade is greyed out on the desktop** and points at the app's own plugin manager (the bundled CLI cannot boot or mutate `profiles/desktop`, so the upgrade endpoint short-circuits with `desktop-managed-upgrade` and spawns nothing); the Health diagnostics "Runtime environment" row reads "Managed by DSH Desktop (Electron)". The signal is `process.versions.electron` inside the host process (measured on the real Windows desktop build: `44.0.0`, with `ELECTRON_RUN_AS_NODE=1` and non-TTY stdio); non-Electron desktop wrappers can declare `DSH_SERVICE_RUNTIME_ENV=desktop`. Task notifications **do work on the desktop app** (the permission is granted and a real system notification has been observed); the only boundary is that **clicking a notification cannot bring the window back** (closing the window hides it and `dshDesktop` exposes no window API — use the tray). macOS / Linux desktop builds are not verified.
357
376
 
358
377
  ## 🔒 Security design
359
378
 
@@ -380,6 +399,12 @@ The plugin only sends an exit signal; a process manager brings it back (see "Aut
380
399
  It is the "likely manual terminal launch" detection — no process manager found. If it is actually managed by NSSM/WinSW or output redirection, declare `DSH_SERVICE_RUNTIME_ENV=managed` to clear it.
381
400
  </details>
382
401
 
402
+ <details>
403
+ <summary><strong>Why do task notifications not appear on DSH Desktop?</strong></summary>
404
+
405
+ Check two things first: (1) the **master notification switch** under "Configuration → Notifications" must actually be on — it defaults to off and only turns on after you click "Enable notifications" and the system grants permission (if you only see the "Enable notifications" button, permission has not been granted yet); (2) the session must really go from running to finished, and it must not be a subagent session. System notifications themselves do work on the desktop app: Windows "Settings → System → Notifications" registers an `electron.app.DeepSeek Harness` entry, and a real "task complete" toast has been observed on the actual machine. The only known boundary is that **clicking the notification does not bring the window back** — closing the window hides it and the current desktop bridge exposes no window API; use the system tray instead.
406
+ </details>
407
+
383
408
  <details>
384
409
  <summary><strong>A quota card shows "credential missing"?</strong></summary>
385
410
 
package/README.md CHANGED
@@ -9,9 +9,9 @@
9
9
  <em>A service-control &amp; operations plugin for DeepSeek Harness (DSH) Web.</em>
10
10
  </p>
11
11
 
12
- [![Version](https://img.shields.io/badge/version-1.9.8-3b82f6.svg?style=flat-square)](package.json)
12
+ [![Version](https://img.shields.io/badge/version-2.0.0-3b82f6.svg?style=flat-square)](package.json)
13
13
  [![License: MIT](https://img.shields.io/badge/License-MIT-10b981.svg?style=flat-square)](LICENSE)
14
- [![DSH Compatibility](https://img.shields.io/badge/DSH-%E2%89%A50.1.1--rc.2%20%C2%B7%20%E5%B7%B2%E9%80%82%E9%85%8D%200.1.7--alpha.2-6366f1.svg?style=flat-square)](https://github.com/deepseek-ai/deepseek-harness)
14
+ [![DSH Compatibility](https://img.shields.io/badge/DSH-%E2%89%A50.1.1--rc.2%20%C2%B7%20%E5%B7%B2%E9%80%82%E9%85%8D%200.2.0--rc.1-6366f1.svg?style=flat-square)](https://github.com/deepseek-ai/deepseek-harness)
15
15
  [![Cordis](https://img.shields.io/badge/Cordis-v4.x-f59e0b.svg?style=flat-square)](https://cordis.moe/)
16
16
  [![Platform](https://img.shields.io/badge/platform-DSH%20Web-ec4899.svg?style=flat-square)](https://github.com/gehennawu/dsh-service)
17
17
  [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://github.com/gehennawu/dsh-service/issues)
@@ -74,7 +74,7 @@ DSH Web 服务控制与运维插件:安全重启、版本管理与一键升级
74
74
 
75
75
  - 显示当前 DSH 与插件版本,链接 GitHub Releases
76
76
  - 自动检查 npm **正式版 + 预览版**(latest / next 双 tag)
77
- - 「本次更新内容」入口跟在当前版本号之后且常驻:点开读**当前这一版**的更新说明;有新版本时状态文本「有新版本:x.y.z」整体可点,展开**新版**的 release 信息。正文一律由宿主从 GitHub Releases API 取回后就地渲染(纯 Markdown,不嵌 iframe、不跳转),带版本号、发布日期与预发布标记;未建 Release 或读取失败各有明确提示;点版本卡以外的任意位置或按 Esc 即关闭
77
+ - 「本次更新内容」入口跟在当前版本号之后且常驻:点开读**当前这一版**的更新说明;有新版本时状态文本「有新版本:x.y.z」整体可点,展开**新版**的 release 信息。正文一律由宿主从 GitHub Releases API 取回后就地渲染(纯 Markdown,不嵌 iframe、不跳转),带版本号、发布日期与预发布标记;未建 Release 或读取失败各有明确提示;点版本卡以外的任意位置或按 Esc 即关闭。两行的版本号与「本次更新内容」入口按钮按同一套列对齐,版本号右对齐使它与入口按钮的间距固定,**与版本号长度无关**——长短版本号只把整列一起推宽,不会让某一行错位;窄容器(手机与窄设置面板)沿用同一对齐——版本号吃掉行内余量并右对齐,两行的版本号与入口按钮仍各自同列
78
78
  - 一键升级,完成后自动重启;未检测到进程管理器时先确认后果,保持运行并提示手动重启
79
79
  - 升级落地但进程尚未重启期间(手动启动环境尤为常见),版本行改示「已安装 X,重启后生效」并收起升级按钮,重开面板或刷新页面状态依旧;重启进程后恢复常态
80
80
 
@@ -152,8 +152,9 @@ DSH Web 服务控制与运维插件:安全重启、版本管理与一键升级
152
152
  - 备份记录列表为两行轻行(文件名主行 + 体积 · 时间次行,分隔线布局;会话管理列表同款)
153
153
 
154
154
  - 创建会话、配置与插件 profile 清单的 `.tar.gz` 归档;会话经持久化层稳定快照(活跃 agent 写入不再导致失败),创建过程以单条连续进度条分阶段显示(复制/打包/校验/发布,带步骤号 1/4–4/4,复制阶段为真实百分比)
155
+ - **备份时版本信息**:归档文件名带备份那一刻的 DSH 版本(`dsh-backup-YYYYMMDD-HHmmss-dsh0.1.7-rc.1.tar.gz`),归档内另有 `meta/backup.json` 记录 DSH 版本、插件版本与创建时刻;面板行内与恢复预检都显示该版本,便于判断这份快照出自哪个版本(跨版本降级恢复读不动新格式会话)。旧版本插件所出的归档没有版本段,列表照常显示、恢复照常进行,只是不显示版本行;导入同时接受带版本段与不带版本段两种文件名
155
156
  - 导出下载 / 导入上传 / 删除(两段式确认);不限份数、不自动清理;导入归档必须先通过与恢复相同的完整性检查
156
- - **完整性检查**:恢复前校验 gzip/tar、路径与条目类型,只接受 `sessions`、三份允许配置和 `profiles/<name>/package.json`;拒绝越界路径、链接、特殊文件、未知内容、损坏归档与非法 profile 清单
157
+ - **完整性检查**:恢复前校验 gzip/tar、路径与条目类型,只接受 `sessions`、三份允许配置、`profiles/<name>/package.json` 与 `meta/backup.json`;拒绝越界路径、链接、特殊文件、未知内容、损坏归档与非法 profile 清单
157
158
  - **恢复预检**:先生成 5 分钟有效的一次性计划,展示会话整体替换、配置覆盖/移除和 profile manifest 覆盖清单;最终确认前再次校验归档 SHA-256 与当前目标指纹,发生漂移则拒绝执行
158
159
  - 恢复提交使用事务日志和回滚目录:会话整体替换,配置按快照精确替换,profile 只更新 package.json 并保留 node_modules/凭据/附件;成功后托管环境自动重启,手动启动环境提示用户手动重启
159
160
 
@@ -212,7 +213,7 @@ DSH Web 服务控制与运维插件:安全重启、版本管理与一键升级
212
213
  - 侧栏变抽屉、详情列移动端隐藏(对齐官方窄屏语义)、模态变全屏面板、设置左列导航变顶部横滑
213
214
  - 模型选择按钮在手机上收成图标(≤480px,与官方窄容器同形态;481px 以上仍显示模型名与推理等级)——插件移动端把中列拉宽,官方 `@container (width<=360px)` 那条收起规则在 428~440px 机型上永远不会触发,故显式对齐官方窄容器形态
214
215
  - 统计条(输入框下方「轮/步/tok/s | tok/缓存命中」)在手机上**保持一行、居中且不歪**:收紧这一行自身的左右内边距(官方 32px→2px)与列间距换出可用宽度,两枚统计交给官方 `justify-content` 居中排开——≥~420px 视口零截断,更窄视口按比例各让一点(远少于官方的固定截断),不换行、不横向滚动;同时兼容老版本 DSH(0.1.5-rc.2 上这条行是整行宽,早期实现的子项 `flex-grow` 会把两枚统计整组推到左边,现已改为不生长)(桌面不受影响)
215
- - 会话顶栏手机端重排(≤560px):标题行只留**标题 + 模式芯片**(芯片右靠贴住「…」钮,标题基本完整可见),「N 个子代理」与「N 个后台任务运行中」两枚计数芯片泊到「对话/轨迹」标签行右缘成一对(标签间距 36→20,点芯片本体仍开各自菜单);官方 crumbs 放不下时的**拦腰硬裁**彻底消除,561px 以上与桌面保持官方原布局
216
+ - 会话顶栏手机端重排(≤560px):标题行只留**标题 + 模式芯片**(芯片右靠贴住「…」钮,标题基本完整可见),「N 个子代理」与「N 个后台任务运行中」两枚计数芯片泊到「对话/轨迹」标签行右缘成一对(标签间距 36→20,点芯片本体仍开各自菜单);官方 crumbs 放不下时的**拦腰硬裁**彻底消除,561px 以上与桌面保持官方原布局。DSH 0.1.7-rc.1 起官方给模式芯片加了 `@container (width<=540px)` 隐藏(容器是标题行,约合 606px 视口),手机上曾整片消失——本插件在整个移动端带内覆盖回可见(390px 以上标题仍完整不裁,仅 320~375px 档标题略带省略号)
216
217
  - Agent Team 弹窗手机端不越屏(<1024px):官方 Agent Team 面板自带左锚(`left:0`),挂在头部右侧动作槽上会整体右移出屏(实测 320~1023px 越出 112~352px)。移动端把会话头部设为定位上下文、面板改**右锚 16px** 并按 `100vw − 32px` 封顶,两缘各留 16px;桌面 ≥1024px 官方几何逐字不变
217
218
  - 滑动沉浸:会话内下滑自动收起头部与输入框全屏阅读(收起时让出输入框占位,正文真正铺满整屏;回显时若仍停在会话末尾会自动对齐到底),上滑 / 点「回到底部」浮钮 / 聚焦输入框即恢复(无额外悬浮钮);已经停在会话末尾时往回一点即回显(不再等满阈值),点回底后停留位置会补一次贴底对齐;流式贴底、锚点跳转等程序化滚动绝不误触发
218
219
  - 滑动开合抽屉:双抽屉关闭时,横移主导的右滑任意位置打开侧栏抽屉、左滑任意位置打开官方右侧栏(DSH 0.1.5+,未就绪时该方向无效);开启后反向横滑任意位置关闭;从屏幕边缘起滑额外享有浏览器手势接管的补完加成,编辑器等真横滚区内的滑动不误触发
@@ -301,6 +302,20 @@ flowchart TB
301
302
 
302
303
  ## ⚡ 安装
303
304
 
305
+ ### 方式一:DSH 插件管理页(图形界面,推荐)
306
+
307
+ 1. 打开左侧边栏的「**插件**」页。
308
+ 2. 点右上角的「**+ 添加插件**」按钮。
309
+ 3. 在弹出的对话框里,把 `@gehennawu/dsh-service` 填进「**包名**」输入框;「**安装源**」保持默认的「npm 官方源」。
310
+ 4. 点「**安装**」,等待官方把包装进当前 profile(对话框底部会给出进度与结果)。
311
+ 5. 装好后本插件出现在「**已安装**」分组里。宿主半要等 DSH 进程重启才生效,客户端半刷新页面即生效。
312
+ - 在 Web 上:用本插件的「**服务控制 → 重启**」,或在对话里发 `/restart`;
313
+ - 在 DSH Desktop 上:请退出应用后重新打开(桌面端由应用自身托管进程,本插件不会自行退出)。
314
+
315
+ > 提示:插件管理页的「安装」按钮走官方 npm 源与官方安装链路,无需命令行、无需手动改 profile 文件。
316
+
317
+ ### 方式二:命令行
318
+
304
319
  | 方式 | 命令 |
305
320
  | --- | --- |
306
321
  | npm(推荐) | `dsh plugin --profile web add @gehennawu/dsh-service` |
@@ -319,7 +334,7 @@ dsh web
319
334
 
320
335
  插件只发送退出信号,不负责拉起进程;没有进程管理器时重启会直接停止 DSH Web。
321
336
 
322
- 插件以被动信号(环境变量、`/.dockerenv`、`/proc/1/cgroup`、终端 TTY)判断进程管理器:检测到 Docker/systemd/pm2/supervisord/Kubernetes 时照常自动重启;都没有且 stdin/stdout 为交互终端时视为「疑似手动启动」——健康诊断黄色标注、一键升级改为保持运行并提示手动重启。启发式无法覆盖输出重定向、NSSM/WinSW 等场景,可用 `DSH_SERVICE_RUNTIME_ENV=managed|manual` 显式声明。
337
+ 插件以被动信号(环境变量、`/.dockerenv`、`/proc/1/cgroup`、终端 TTY)判断进程管理器:检测到 Docker/systemd/pm2/supervisord/Kubernetes 时照常自动重启;都没有且 stdin/stdout 为交互终端时视为「疑似手动启动」——健康诊断黄色标注、一键升级改为保持运行并提示手动重启。启发式无法覆盖输出重定向、NSSM/WinSW 等场景,可用 `DSH_SERVICE_RUNTIME_ENV=managed|manual|desktop` 显式声明。
323
338
 
324
339
  ### Docker Compose
325
340
 
@@ -351,11 +366,15 @@ pm2 start "dsh web --host 127.0.0.1" --name dsh-web
351
366
  | Linux + Docker Compose | 支持 | 配置 restart policy 后支持 | 已验证 |
352
367
  | Linux + systemd / pm2 | 预期支持 | 由进程管理器负责 | 未单独验证 |
353
368
  | macOS / Windows + pm2 等 | 代码未限制 | 由进程管理器负责 | 未验证 |
369
+ | DSH Desktop(Windows,Electron) | 支持 | 由桌面应用负责 | 已验证(2026-09-28 真机) |
370
+ | DSH Desktop(macOS / Linux,Electron) | 代码未限制 | 由桌面应用负责 | 未验证 |
354
371
  | 直接运行 `dsh web` | 支持 | 不支持 | 预期行为 |
355
372
 
356
373
  运行要求:Node.js `>=22`,DSH Web 能加载 Host 与 Client 两半插件。更新检查需访问 `registry.npmjs.org`;网络失败不影响其他功能。
357
374
 
358
- **DSH 适配口径**:已适配 DSH `0.1.7-rc.1`——会话格式 V4(V3 日志首读时由官方迁移为 `session.v4.jsonl.zstd` 代际文件,旧 `session.v3.jsonl.zstd` 按格式目录策略保留;详情视图自动归档系统事件)、`SettingsForms` 配置面(插件配置改由 Profile 的 `cordis.patch.yml` 承载,热更新走 `loader/volatile-update`;旧的 `settings.register` 在 0.1.5/0.1.6 上仍是权威来源,装在同一宿主上两者互不串台)、会话格式 V3 既有适配全部保留(`system/message` 入史、sessionPersistence handle 化、官方右栏、turn-process 对象化)、移动端底行触发钮双哈希兼容、子代理回合尾模型行 list 槽位自适应兼容、`plugins.bundle.config` 槽位注入、会话详情打开接入 `uiWorkspace` 降级链路。旧版 DSH(`>=0.1.1-rc.2`)保持兼容。支持区间扩展到 `0.1.7-rc.1`;rc.1 上的静态结构审查未发现新的代码适配需求,且 rc.1 新增的插件版本兼容门禁只读取 `peerDependencies`(本插件无该声明,不受门禁约束),并已通过 DSH `0.1.7-rc.1` 真机挂载验收。设置面、persistence/布局 seam 均按运行时能力探测走双形态,旧宿主上针对新结构的适配项天然不生效(纯展示,无功能损失)。注意:升级后写入的会话日志无法被旧版 DSH 读取,**备份不可跨版本降级恢复**。插件市场按 `package.json` 的 `engines.dsh` 区间判定兼容性(该字段是唯一的支持口径声明)。
375
+ **DSH 适配口径**:已适配 DSH `0.2.0-rc.1`——会话格式 V4(V3 日志首读时由官方迁移为 `session.v4.jsonl.zstd` 代际文件,旧 `session.v3.jsonl.zstd` 按格式目录策略保留;详情视图自动归档系统事件)、`SettingsForms` 配置面(插件配置改由 Profile 的 `cordis.patch.yml` 承载,热更新走 `loader/volatile-update`;旧的 `settings.register` 在 0.1.5/0.1.6 上仍是权威来源,装在同一宿主上两者互不串台)、会话格式 V3 既有适配全部保留(`system/message` 入史、sessionPersistence handle 化、官方右栏、turn-process 对象化)、移动端底行触发钮双哈希兼容、子代理回合尾模型行 list 槽位自适应兼容、`plugins.bundle.config` 槽位注入、会话详情打开接入 `uiWorkspace` 降级链路。旧版 DSH(`>=0.1.1-rc.2`)保持兼容。支持区间扩展到 `0.2.0-rc.1`;0.2.0-rc.1 源码级与产物级审查(261 commits)未发现新增破坏面——20 条既有接缝 17 条字节级相同、关键官方包产物逐字节相同、CSS 类锚点与主题令牌经产物 grep 定案零漂移;rc.1 起新增的插件版本兼容门禁只读取 `peerDependencies`(本插件无该声明,不受门禁约束),并已在 DSH `0.2.0-rc.1` 真机挂载运行。设置面、persistence/布局 seam 均按运行时能力探测走双形态,旧宿主上针对新结构的适配项天然不生效(纯展示,无功能损失)。注意:升级后写入的会话日志无法被旧版 DSH 读取,**备份不可跨版本降级恢复**。插件市场按 `package.json` 的 `engines.dsh` 区间判定兼容性(该字段是唯一的支持口径声明)。
376
+
377
+ **DSH 桌面端(DSH Desktop / Electron)口径**:桌面端复用同一份 Web 前端与同一张客户端插件图(`platform: web`),插件照常加载,绝大多数功能原样可用;语义不同的只有「重启」与「一键升级」。宿主由桌面应用托管,**重启一律不再 `process.exit(42)`**(桌面壳把任何非 0 退出判为 Host 崩溃,会弹原生「启动失败」对话框且不会自动重拉,官方也没有可编程重启 RPC)——「重启」按钮、对话里的 `/restart` 与备份恢复后的重启统一改为提示「请在桌面端退出应用后重新打开」;**一键升级在桌面端置灰**并指向应用自带的插件管理页(桌面端 CLI 不能启动/改写 `profiles/desktop`,升级端点直接短路返回 `desktop-managed-upgrade`,不产生任何 subprocess);健康诊断的「运行环境」行显示「由桌面端托管(Electron)」。判据是宿主进程内的 `process.versions.electron`(Windows 桌面版真机实测 `44.0.0`,同进程 `ELECTRON_RUN_AS_NODE=1`、stdin/stdout 均非 TTY);非 Electron 的桌面封装可用 `DSH_SERVICE_RUNTIME_ENV=desktop` 显式声明。任务通知在桌面端**可用**(权限 granted,真机已收到系统通知);唯一的边界是**点击通知唤不回窗口**(桌面端关窗=隐藏窗口,`dshDesktop` 无窗口 API,请用托盘唤回)。macOS / Linux 桌面端未验证。
359
378
 
360
379
  ## 🔒 安全设计
361
380
 
@@ -382,6 +401,12 @@ pm2 start "dsh web --host 127.0.0.1" --name dsh-web
382
401
  这是「疑似终端手动启动」的检测结果,说明当前没有检测到进程管理器。若实际由 NSSM/WinSW 或输出重定向等场景托管,可用 `DSH_SERVICE_RUNTIME_ENV=managed` 显式声明消除。
383
402
  </details>
384
403
 
404
+ <details>
405
+ <summary><strong>桌面端(DSH Desktop)收不到任务通知?</strong></summary>
406
+
407
+ 先确认两件事:① 「配置 → 通知」里的**通知总开关**必须真正打开——它默认关闭,只有点过「开启通知」且系统授予权限后才会置为开启(若只看到「开启通知」按钮,说明权限还没授予);② 会话必须真的从「运行中」变为结束,且不是子代理会话。系统通知在桌面端本身是可用的:Windows「设置 → 系统 → 通知」里会为应用建立一条 `electron.app.DeepSeek Harness` 记录(真机实测会弹出「任务完成」通知)。唯一已知边界是**点击通知不会把窗口唤回**——桌面端关窗=隐藏窗口,当前桌面桥面没有窗口显示 API,请用系统托盘唤回窗口。
408
+ </details>
409
+
385
410
  <details>
386
411
  <summary><strong>额度卡片显示「凭据未配置」?</strong></summary>
387
412
 
@@ -10,6 +10,15 @@ const CONFIG_FILES = Object.freeze(['settings.yaml', 'cordis.patch.yml', 'AGENTS
10
10
  // `cordis.patch.yml` 是 0.1.7-alpha.1 起的 Profile 用户配置层:漏掉它会出现「备份成功、
11
11
  // 恢复后个性化配置全丢」。package.json 承载 bundle 启停清单,两者必须成对。
12
12
  const PROFILE_FILES = Object.freeze(['package.json', 'cordis.patch.yml'])
13
+ // 归档内元数据(备份时的 DSH/插件版本):唯一一个「读后即丢弃、绝不落盘覆盖」的分区。
14
+ // 它必须出现在白名单里,否则 0.1.7 起新插件所出的归档会被判 `backup-entry-unexpected`
15
+ // 而全部不可恢复——这是最容易踩的反向兼容坑。
16
+ const META_PATH = 'meta/backup.json'
17
+ const MAX_META_BYTES = 4096
18
+ // 版本串的合法口径与宿主侧备份命名(index.js 的 BACKUP_VERSION_RE)逐字同源:
19
+ // semver 形、长度封顶、且**必须含数字**——少了最后一条,未解析出 DSH 包的宿主
20
+ // 写下的哨兵值 `unknown` 会被当成真版本显示出来。
21
+ const META_VERSION_RE = /^(?=.*[0-9])[0-9A-Za-z][0-9A-Za-z.-]{0,63}$/
13
22
  const PLAN_TTL_MS = 5 * 60 * 1000
14
23
  const MAX_COMPRESSED_BYTES = 512 * 1024 * 1024
15
24
  const MAX_EXPANDED_BYTES = 1024 * 1024 * 1024
@@ -98,11 +107,23 @@ function emptySections() {
98
107
  }
99
108
  }
100
109
 
110
+ // 归档元数据的解析口径:**纯展示信息,绝不参与完整性裁决**。缺条目、坏 JSON、版本串
111
+ // 非法或超限一律回 null(预检显示「未记录」),不让一份附加信息把可恢复的归档判成损坏。
112
+ function parseBackupMetadata(data) {
113
+ if (data.length === 0 || data.length > MAX_META_BYTES) return null
114
+ let parsed
115
+ try { parsed = JSON.parse(data.toString('utf8')) } catch (_) { return null }
116
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return null
117
+ const version = typeof parsed.dshVersion === 'string' && META_VERSION_RE.test(parsed.dshVersion) ? parsed.dshVersion : null
118
+ const createdAt = typeof parsed.createdAt === 'string' && !Number.isNaN(Date.parse(parsed.createdAt)) ? parsed.createdAt : null
119
+ return { dshVersion: version, createdAt }
120
+ }
121
+
101
122
  function validateEntry(path, type, data, state) {
102
123
  const sections = state.sections
103
124
  const parts = path.split('/')
104
125
  const root = parts[0]
105
- if (!['sessions', 'config', 'profiles'].includes(root)) throw domainError('backup-entry-unexpected')
126
+ if (!['sessions', 'config', 'profiles', 'meta'].includes(root)) throw domainError('backup-entry-unexpected')
106
127
  if (parts.length === 1 && type !== 'directory') throw domainError('backup-entry-type')
107
128
 
108
129
  if (root === 'sessions') {
@@ -112,6 +133,17 @@ function validateEntry(path, type, data, state) {
112
133
  return
113
134
  }
114
135
 
136
+ // meta 分区只容忍 `meta/` 目录本身与唯一一个 `meta/backup.json`:出现别的条目(别名、
137
+ // 子目录、重复)即拒,免得这层「读后即弃」的例外变成绕过条目白名单的后门。
138
+ if (root === 'meta') {
139
+ if (path === 'meta' && type === 'directory') return
140
+ if (path !== META_PATH || type !== 'file') throw domainError('backup-entry-unexpected')
141
+ state.metaSeen = (state.metaSeen ?? 0) + 1
142
+ if (state.metaSeen > 1) throw domainError('backup-entry-duplicate')
143
+ state.metadata = parseBackupMetadata(data)
144
+ return
145
+ }
146
+
115
147
  if (root === 'config') {
116
148
  state.present.add('config')
117
149
  if (parts.length === 1) return
@@ -229,7 +261,10 @@ function parseTar(expanded, options = {}) {
229
261
  // 归档格式标签:v1 = 不含 Profile 补丁(0.1.7-alpha.1 之前的插件所出);v2 = 含补丁层。
230
262
  // 恢复旧归档时据此给出「未包含项」提示而不判损坏。
231
263
  const archiveFormat = state.sections.profiles.patchFiles.length > 0 ? 'v2' : 'v1'
232
- return { entries, sections: state.sections, logicalBytes, entryCount: entries.length, archiveFormat }
264
+ // 备份时版本信息:`metadata` 为 null 表示该归档未记录(旧插件所出,或元数据非法)。
265
+ // 文件名里的版本段由调用方(index.js 的 backupVersionFromName)解析后随 source 带入,
266
+ // 两层信息在报告里并列,客户端优先信元数据。
267
+ return { entries, sections: state.sections, logicalBytes, entryCount: entries.length, archiveFormat, metadata: state.metadata ?? null }
233
268
  }
234
269
 
235
270
  async function inspectArchive(source, options = {}) {
@@ -242,6 +277,10 @@ async function inspectArchive(source, options = {}) {
242
277
  status: 'error',
243
278
  archive: { entryCount: 0, compressedBytes: source.sizeBytes, logicalBytes: 0 },
244
279
  archiveFormat: 'v1',
280
+ // 文件名里的版本段(老归档为 null)与归档内记录的版本(缺 meta 为 null)分开报,
281
+ // 界面优先显示后者;前者是廉价但可能与内容不符的镜像。
282
+ nameVersion: typeof source.nameVersion === 'string' ? source.nameVersion : null,
283
+ metadata: null,
245
284
  sections: emptySections(),
246
285
  issues: [],
247
286
  issueCount: 0,
@@ -271,6 +310,9 @@ async function inspectArchive(source, options = {}) {
271
310
  status: 'ok',
272
311
  archive: { entryCount: parsed.entryCount, compressedBytes: compressed.length, logicalBytes: parsed.logicalBytes },
273
312
  archiveFormat: parsed.archiveFormat,
313
+ // 元数据非法/缺席时退回文件名里的版本段,保证「只要有一处记了版本」就看得见。
314
+ metadata: parsed.metadata ?? null,
315
+ dshVersion: parsed.metadata?.dshVersion ?? (typeof source.nameVersion === 'string' ? source.nameVersion : null),
274
316
  sections: parsed.sections,
275
317
  },
276
318
  parsed,
@@ -551,7 +593,7 @@ export function createBackupIntegrity(options) {
551
593
  expiresAt: preparedAt + PLAN_TTL_MS,
552
594
  staging,
553
595
  sourcePath: source.path,
554
- source: { id: source.id, name: source.name, sizeBytes: source.sizeBytes, sha256: inspected.report.source.sha256 },
596
+ source: { id: source.id, name: source.name, sizeBytes: source.sizeBytes, sha256: inspected.report.source.sha256, dshVersion: inspected.report.dshVersion ?? null },
555
597
  sourceFingerprint: inspected.report.source.sha256,
556
598
  targetFingerprint: targetState.fingerprint,
557
599
  profileNames,
@@ -564,6 +606,10 @@ export function createBackupIntegrity(options) {
564
606
  profiles: profileNames.length,
565
607
  profilePatches: patchProfileNames.length,
566
608
  archiveFormat: inspected.report.archiveFormat,
609
+ // 备份时的 DSH 版本随计划下发:恢复是不可逆动作,确认前必须让用户看清
610
+ // 「这份快照来自哪个版本」——降级恢复读不动新格式会话,写清楚才不至于误点。
611
+ dshVersion: inspected.report.dshVersion ?? null,
612
+ metadata: inspected.report.metadata ?? null,
567
613
  },
568
614
  targets: {
569
615
  sessions: { action: 'replace', currentBytes: targetState.bytes, newBytes: inspected.report.sections.sessions.bytes },
@@ -659,7 +705,9 @@ export function createBackupIntegrity(options) {
659
705
  throw domainError('restore-failed')
660
706
  }
661
707
 
662
- const manual = runtimeEnv?.manualStartLikely === true
708
+ // 桌面端(electronShell)与终端手动启动同理:没有东西会把 Host 拉起来,恢复完成后不调度
709
+ // exit(42)(桌面端退出会被壳判为崩溃并弹原生恢复对话框),改由客户端示「重启应用」指引。
710
+ const manual = runtimeEnv?.manualStartLikely === true || runtimeEnv?.electronShell === true
663
711
  if (!manual) scheduleRestart()
664
712
  return {
665
713
  restoredFrom: plan.source.name,
package/backup-routes.js CHANGED
@@ -13,8 +13,10 @@ export function createBackupRoutes({
13
13
  clearBackupProgress,
14
14
  downloadTokens,
15
15
  dshHome,
16
+ dshVersion,
16
17
  setBackupProgress,
17
18
  withBackupLock,
19
+ backupNameVersionSuffix,
18
20
  createBackup,
19
21
  deleteBackup,
20
22
  exportBackup,
@@ -40,7 +42,9 @@ export function createBackupRoutes({
40
42
  'backup-create': { feature: 'backupMaintenance', audit: true, handle: async (payload, rpcEndpoint) => {
41
43
  try {
42
44
  return { ok: true, value: await withBackupLock(() => {
43
- const name = `dsh-backup-${formatBackupTimestamp(new Date())}.tar.gz`
45
+ // 文件名带备份时的 DSH 版本(`-dsh<version>`):版本号来自宿主进程而非浏览器,
46
+ // 并且只接受通过字符集校验的值——读不出运行版本时省略该段,回落到老命名式。
47
+ const name = `dsh-backup-${formatBackupTimestamp(new Date())}${backupNameVersionSuffix(dshVersion)}.tar.gz`
44
48
  const withValidator = async (source, task) => {
45
49
  const validator = createBackupIntegrity({ dshHome, resolveBackup: async () => source })
46
50
  try { return await task(validator) } finally { await validator.dispose() }