dsh-m 0.8.5 → 0.9.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.en.md CHANGED
@@ -8,11 +8,11 @@
8
8
 
9
9
  English · [中文](./README.md)
10
10
 
11
- A DeepSeek Harness (DSH) plugin marketplace with a customizable registry: **browse, install, uninstall, upgrade** — all local. Ships as a DSH web plugin: the "Plugin Marketplace" sidebar entry opens a three-view panel, backed by eight `dshm_*` agent tools and the `dshm` CLI.
11
+ A DeepSeek Harness (DSH) plugin marketplace with a customizable registry: **browse, install, uninstall, upgrade** — all local. Ships as a DSH web plugin: the "Plugin Marketplace" sidebar entry opens a three-view panel, backed by eight `dshm_*` agent tools and the `dshm` CLI. The same package installs into **both the Web (`web`) and official Desktop (`desktop`) profiles** (0.9.0+; capability table in the changelog).
12
12
 
13
13
  <div align="center">
14
- <img src="https://raw.githubusercontent.com/iasiv5/dsh-m/main/docs/images/marketplace.webp" alt="Plugin Marketplace panel — Market view: curated card flow with search and category filters" width="100%">
15
- <p><sub>The "Plugin Marketplace" panel · Market view: curated cards · search · category filters · one-click install</sub></p>
14
+ <img src="https://raw.githubusercontent.com/iasiv5/dsh-m/main/docs/images/marketplace.webp" alt="Plugin Marketplace panel — Market view: Community / Curated / Favorites zones, category chips, and plugin cards (verified / installed badges, npm & GitHub detail links)" width="100%">
15
+ <p><sub>The "Plugin Marketplace" panel · Market view (Community 4,200+ · Curated 23 · Favorites): click a card to install</sub></p>
16
16
  </div>
17
17
 
18
18
  ## Quick start
@@ -38,10 +38,30 @@ The default registry includes DSH Skins, ModSearch, the Lark / QQ / Weixin / WeC
38
38
  |---|---|
39
39
  | **Market** | **Zoned** (0.7.0): **Community** (default landing, the 4,000+ full catalog) and **Curated** (hand-picked primary registry, single page) as two independent zones — each keeps its own category/search/sort/pagination state across tab switches; plus a **Favorites** zone (browser-local bookmarks with delisted-entry detection and one-click cleanup). Community zone: two-row collapsible category chips with sticky collapse, sort switch (downloads/stars/added × asc/desc), windowed pagination (24/48/96 per page), card byline (owner/downloads/stars); clicking a card opens a **detail modal** (superset of the card: download window, screenshot lightbox, capabilities collapsed by default, install command); npm sources pin the latest exact version, GitHub sources pin the release/tag commit |
40
40
  | **Operations** | Install/upgrade/uninstall/toggle all flow through a global operation log (0.7.0): state lives off cards, surviving paging/searching/tab switches; persisted to localStorage and resumed after host reloads (each restored op re-validated "still applies" before executing); benignly-invalidated ops show as a neutral "skipped"; the Installed page gains "Update all (N)" batch queueing |
41
- | **Installed** | What your web profile actually has, annotated "via market / non-market"; update badges, upgrade, two-step confirm uninstall; 📖 README preview (64KB cap); **live phase badge** (●active / ●failed / ○pending) and a **one-click toggle** (0.4.0: delegates to the official pluginManager service for live application; falls back to file-level edits + restart notice when absent; dsh-m itself and official host lifelines are locked) |
41
+ | **Installed** | What the current profile (web / desktop, 0.9.0+) actually has, annotated "via market / non-market"; update badges, upgrade, two-step confirm uninstall; 📖 README preview (64KB cap); **live phase badge** (●active / ●failed / ○pending) and a **one-click toggle** (0.4.0: delegates to the official pluginManager service for live application; falls back to file-level edits + restart notice when absent; dsh-m itself and official host lifelines are locked) |
42
42
  | **Settings** | Registry address draft with "Force refresh / Validate & apply / Restore default / Download default registry.json"; configured vs active address and status at a glance (shown only when abnormal); community catalog toggle; dsh-m self-update |
43
43
 
44
- After any mutation, the already-open Market and Installed views refresh the profile state together, keeping badges and cards in sync without closing and reopening the marketplace; a "⚡ Restart" banner appears — under systemd, the DSH launcher's `appExit` hook hands the restart back to a unit configured with `Restart=on-failure` or `Restart=always`, avoiding a `systemctl` helper inside the unit cgroup that is about to stop; if `appExit` is unavailable, the fallback uses a manager-owned transient `systemd-run` service and only then a detached helper. The client confirms the replacement by boot id and dismisses the banner, leaving DSH Web's own background connection recovery in control; it does not force a full-page reload during the auth/route handoff. The restart chain has been live-verified on the current DSH Web `0.2.0-rc.2` (2026-10-01): dsh-m probes the unit's `Restart=` policy, hands the stop back via `appExit`, the `status=75/TEMPFAIL` exit is caught by the Restart policy and auto-started, and after recovery the page reconnects in the background with the panel fully functional. Historical scope: the `0.1.5-rc.1` era covered live `/dshm` ping and authenticated `303 → 200` checks; `0.1.2-rc.1` is covered by contract checks plus runtime shape-detection fallback (see the verified array) — with the deployment moved on, no live E2E is chased on retired generations; the transient `systemd-run` fallback only applies to hosts without `appExit` (all supported generations provide it) and stays a documented fallback rather than a release gate; repeated install/uninstall is covered by the profile-transaction test surface (compensation, post-install guard, 763 cases) plus the 0.4.0→0.4.2 consecutive-release evidence. Installs stream live pnpm progress (resolve → download → link → build).
44
+ <div align="center">
45
+ <img src="https://raw.githubusercontent.com/iasiv5/dsh-m/main/docs/images/settings.webp" alt="Settings view — community catalog status card (catalog version / route / entries), curated registry configuration card (address draft with force-refresh / validate & apply / restore default / download), and the About card" width="86%">
46
+ <p><sub>Settings view · community catalog status, curated registry configuration, and dsh-m self-update</sub></p>
47
+ </div>
48
+
49
+ The restart chain below applies to **Web only** (Desktop takes effect via the official app lifecycle: after installing, quit and reopen the Desktop app — see the 0.9.0 section). After any mutation, the already-open Market and Installed views refresh the profile state together, keeping badges and cards in sync without closing and reopening the marketplace; a "⚡ Restart" banner appears — under systemd, the DSH launcher's `appExit` hook hands the restart back to a unit configured with `Restart=on-failure` or `Restart=always`, avoiding a `systemctl` helper inside the unit cgroup that is about to stop; if `appExit` is unavailable, the fallback uses a manager-owned transient `systemd-run` service and only then a detached helper. The client confirms the replacement by boot id and dismisses the banner, leaving DSH Web's own background connection recovery in control; it does not force a full-page reload during the auth/route handoff. The restart chain has been live-verified on the current DSH Web `0.2.0-rc.2` (2026-10-01): dsh-m probes the unit's `Restart=` policy, hands the stop back via `appExit`, the `status=75/TEMPFAIL` exit is caught by the Restart policy and auto-started, and after recovery the page reconnects in the background with the panel fully functional. Historical scope: the `0.1.5-rc.1` era covered live `/dshm` ping and authenticated `303 → 200` checks; `0.1.2-rc.1` is covered by contract checks plus runtime shape-detection fallback (see the verified array) — with the deployment moved on, no live E2E is chased on retired generations; the transient `systemd-run` fallback only applies to hosts without `appExit` (all supported generations provide it) and stays a documented fallback rather than a release gate; repeated install/uninstall is covered by the profile-transaction test surface (compensation, post-install guard, 813 cases) plus the 0.4.0→0.4.2 consecutive-release evidence. Installs stream live pnpm progress (resolve → download → link → build).
50
+
51
+ ### New in 0.9.1 — Header version chip upgrade notice (lights up only when an update exists)
52
+
53
+ - **Silent by default**: the panel-header `dsh-m vX.Y.Z` chip keeps the static look introduced in 0.7.5 — up to date, failed check, or a local dev build ahead of npm (ahead) never interrupt (ahead only shows a "local dev build" hint in the hover title).
54
+ - **Lights up only when outdated**: when `self-check` (npm latest vs the installed version, read-only) finds a newer version, the chip turns into a warn state showing `⬆ v<latest>`; clicking it runs `self-upgrade` (same mutation session + post-install guard) and a "⚡ Restart" banner follows on success. On Desktop the click is rejected structurally by the capability table (409, with official lifecycle guidance).
55
+ - **TTL cache + version guard**: check results are cached in browser localStorage for 30 minutes so opening the panel doesn't hit npm every time; after an upgrade + restart the cache auto-invalidates by version. Failed checks stay silent — no error is shown.
56
+
57
+ ### New in 0.9.0 — Official Desktop (dual profile) support
58
+
59
+ - **One package, two profiles**: dsh-m now installs into the official Desktop's `desktop` profile (`~/.dsh/profiles/desktop`) alongside the Web `web` profile; the catalog, panel, and agent tools are shared, and everything manages the host's current profile (official `profileContext` as the single source of truth).
60
+ - **Entrust admission to the host**: every `/dshm` method (including ping and unknown methods) delegates to the official `connection.requestRejection()` before any body is read (trustedHosts / loopback / cross-site / `Origin: null` semantics come from the host); rejected requests consume no body and call no business logic; hosts without the capability fail closed. **Behavior change**: the old home-grown "missing Origin → 403" guard is gone — Origin-less requests from the Desktop bridge are admitted per official semantics, and unauthenticated health probes now follow host trust instead of a blanket 403.
61
+ - **Desktop first-release capability table**: read-only market + **installing new packages** (delegated to the official `pluginManager.installBundle`; integrity, locking, and application phases stay with the official manager) + **toggles** (delegated; structured refusal when the service is absent, never a file-level fallback); **upgrade / uninstall / self-update / one-click restart** return structured 409 refusals with official-entry guidance (no official upgrade API exists; restarts belong to the Electron lifecycle); build scripts retry with the exact official `pendingBuilds` list — never an allow-all.
62
+ - **Per-profile read model and cache**: market installed-badges, the installed list, and README previews only read the current profile; registry / community / accepted-source caches are segmented per profile (web keeps its legacy paths — zero migration, zero clearing); favorites and operation logs stay per browser origin and do not sync between Web and Desktop.
63
+ - **CLI always targets the web profile**: `--profile web` is explicit; `--profile desktop` is rejected with a pointer to the official Desktop plugin management page.
64
+ - **Honest limitations**: Desktop on-device (Win/macOS) E2E has not been run; the `registry.json` verified arrays gain **no** Desktop generation (to be recorded after real testing); dsh-m's file-level post-install guard is skipped on Desktop (app.asar probing blind spot) and replaced by official result checks plus a `listBundles` re-read.
45
65
 
46
66
  ### Fixed in 0.8.5
47
67
 
@@ -156,6 +176,8 @@ dshm restart --yes
156
176
 
157
177
  When the registry is unavailable, `registry` / `search` / `outdated` print the configured vs active address and exit 1; `list` still shows installed plugins. The CLI uses its own cache namespace and never touches the Web side's.
158
178
 
179
+ **Profile target (0.9.0)**: the CLI always acts on the web profile — `--profile web` makes it explicit; `--profile desktop` is rejected outright (Desktop plugin management belongs to the official Desktop plugin management page).
180
+
159
181
  ## Registry
160
182
 
161
183
  `registry.json` is hand-curated and fetched at runtime in order: **GitHub raw (`@main`) → GitHub mirror (jsDelivr CDN, backup line) → local 60-min TTL cache → bundled snapshot** — listing updates are decoupled from plugin releases. To add or amend a listing, edit `registry.json` and open a PR; CI validates the strict schema, npm/GitHub existence, duplicate ids and URL reachability.
@@ -202,6 +224,9 @@ Release: `npm version patch|minor|major && git push --tags` → OIDC trusted pub
202
224
  **1. Why don't GitHub-sourced update hints follow main?**
203
225
  Intermediate commits on main can be unstable. dsh-m tracks **releases / tags** only (`releases/latest` first, tags list as fallback) and pins the commit SHA the tag points to.
204
226
 
227
+ **1.5 What works on the official Desktop?**
228
+ Desktop (the `desktop` profile) first release supports: browsing the market, **installing new packages**, and plugin toggles; upgrade/uninstall/self-update/one-click restart return structured refusals with official-entry guidance (official plugin management page / Desktop app restart). Installed badges and the installed list reflect only what Desktop itself has installed; favorites and operation logs do not sync with the Web side.
229
+
205
230
  **2. Does uninstalling dsh-m delete my data?**
206
231
  No. Only the package reference in the profile is removed (live UI disabled first), and suspected leftover paths are reported to you.
207
232
 
package/README.md CHANGED
@@ -8,11 +8,11 @@
8
8
 
9
9
  [English](./README.en.md) · 中文
10
10
 
11
- 可自定义收录清单(Registry)的 DeepSeek Harness (DSH) 插件市场:**收录 · 安装 · 卸载 · 升级**,全部本机完成。以 DSH web 插件形态运行——侧栏「插件市场」打开三视图面板,同时提供 `dshm_*` agent 工具与 `dshm` CLI。
11
+ 可自定义收录清单(Registry)的 DeepSeek Harness (DSH) 插件市场:**收录 · 安装 · 卸载 · 升级**,全部本机完成。以 DSH web 插件形态运行——侧栏「插件市场」打开三视图面板,同时提供 `dshm_*` agent 工具与 `dshm` CLI;同一包装入 **Web(`web`)与官方 Desktop(`desktop`)两个 profile**(0.9.0 起,能力表见变更节)。
12
12
 
13
13
  <div align="center">
14
- <img src="https://raw.githubusercontent.com/iasiv5/dsh-m/main/docs/images/marketplace.webp" alt="侧栏「插件市场」面板——市场视图:收录卡片流、关键词搜索与分类筛选" width="100%">
15
- <p><sub>侧栏「插件市场」· 市场视图:收录卡片流 · 搜索 · 分类筛选 · 展开即装</sub></p>
14
+ <img src="https://raw.githubusercontent.com/iasiv5/dsh-m/main/docs/images/marketplace.webp" alt="侧栏「插件市场」面板——市场视图:社区 / 精选 / 收藏三分区、分类 chips 与插件卡片(已实测 / 已安装徽标、npm 与 GitHub 详情链接)" width="100%">
15
+ <p><sub>侧栏「插件市场」· 市场视图:社区 4,200+ · 精选 23 · 收藏,卡片即点即装</sub></p>
16
16
  </div>
17
17
 
18
18
  ## 30 秒上手
@@ -38,10 +38,30 @@
38
38
  |---|---|
39
39
  | **市场** | **分区制**(0.7.0):**社区**(默认落地页,4,000+ 条全量目录)与**精选**(手工策展主清单,单页直出)两个独立分区,各有自己的分类/搜索/排序/分页状态(切 tab 互不重置);另有**收藏**分区(浏览器本地书签,自动检测下架条目并一键清理)。社区区:分类 chips 两行折叠 + 吸顶收缩、排序(下载量/Star/收录日期 × 升降)、页码窗口化分页(24/48/96 每页)、卡片 byline(作者/下载量/Star);卡片点开**详情 Modal**(卡片信息超集:下载量窗口三要素、截图灯箱、能力披露默认收起、安装命令);npm 源锁定最新精确版本,GitHub 源锁定 release/tag 指向的 commit |
40
40
  | **操作记录** | 安装/升级/卸载/开关全部走全局操作记录(0.7.0):状态不挂卡片,翻页/搜索/切 tab 不丢;localStorage 持久化,宿主重载后自动恢复未完成操作(逐条校验「此刻仍成立才执行」);良性前提消失以中性「已跳过」呈现;已装页支持「全部更新 (N)」批量入队 |
41
- | **已装** | web profile 实装列表,标注「市场安装 / 非市场安装」;可升级徽标、升级、两段式确认卸载;📖 README 预览(64KB 截断);**运行相位徽标**(●active / ●failed / ○pending)与**一键开关**(0.4.0:委派官方 pluginManager 服务活体生效,服务缺席时文件级编辑 + 重启提示;dsh-m 自身与官方宿主命脉锁定不可开关) |
41
+ | **已装** | 当前 profile(web / desktop,0.9.0 起)实装列表,标注「市场安装 / 非市场安装」;可升级徽标、升级、两段式确认卸载;📖 README 预览(64KB 截断);**运行相位徽标**(●active / ●failed / ○pending)与**一键开关**(0.4.0:委派官方 pluginManager 服务活体生效,服务缺席时文件级编辑 + 重启提示;dsh-m 自身与官方宿主命脉锁定不可开关) |
42
42
  | **设置** | registry 地址草稿 +「强制刷新 / 校验并应用 / 恢复默认 / 下载默认 registry.json」;配置地址、生效来源与状态一目了然(仅异常时提示);社区目录开关;dsh-m 自更新 |
43
43
 
44
- 安装 / 卸载 / 升级完成后,当前已打开的市场页与已装页会一起重新读取 profile 状态并同步徽标/卡片,不需要关闭后重新打开插件市场;随后出现「⚡ 一键重启」横幅——受 systemd 管理时通过 DSH launcher 的 `appExit` 交给服务的 `Restart` 策略,避免在待停止 unit 的 cgroup 内启动 `systemctl` helper;无 `appExit` 的 systemd 兜底改用 manager-owned transient `systemd-run`,最后才退回 detached-helper。客户端按 boot id 确认新进程已恢复后关闭横幅,交由 DSH Web 自身的后台连接重试恢复页面,不强制整页刷新,避免认证/路由切换期间白屏。重启链路已在当前 DSH Web `0.2.0-rc.2` 实机核验(2026-10-01):dsh-m 探测 unit `Restart=` 策略后经 `appExit` 交还 systemd,`status=75/TEMPFAIL` 退出由 Restart 策略接住自动拉起,服务恢复后页面后台重连、面板全功能可用。历史口径:`0.1.5-rc.1` 时代已核验 `/dshm` ping 与带认证 `303 → 200`;`0.1.2-rc.1` 按契约核对 + 形态探测兜底收录(verified 数组),部署面演进后不再追旧代 live E2E;transient `systemd-run` 兜底仅适用于无 `appExit` 的宿主(受支持代际均提供 `appExit`),保持设计兜底而非发布门槛;连续安装/卸载由 profile 事务测试面(补偿事务、装后守卫等 763 用例)与 0.4.0→0.4.2 连续发布实证覆盖。安装过程实时显示 pnpm 进度(解析 → 下载 → 链接 → 构建)。
44
+ <div align="center">
45
+ <img src="https://raw.githubusercontent.com/iasiv5/dsh-m/main/docs/images/settings.webp" alt="设置视图——社区清单状态卡(目录版本 / 获取线路 / 收录条目)、精选清单配置卡(地址草稿与强制刷新 / 校验并应用 / 恢复默认 / 下载默认清单)与关于卡" width="86%">
46
+ <p><sub>设置视图 · 社区清单状态、精选清单配置与 dsh-m 自更新</sub></p>
47
+ </div>
48
+
49
+ 以下重启链路仅适用于 **Web**(Desktop 的生效走官方应用生命周期:安装完成后退出并重新打开 Desktop 应用即可,见 0.9.0 节)。安装 / 卸载 / 升级完成后,当前已打开的市场页与已装页会一起重新读取 profile 状态并同步徽标/卡片,不需要关闭后重新打开插件市场;随后出现「⚡ 一键重启」横幅——受 systemd 管理时通过 DSH launcher 的 `appExit` 交给服务的 `Restart` 策略,避免在待停止 unit 的 cgroup 内启动 `systemctl` helper;无 `appExit` 的 systemd 兜底改用 manager-owned transient `systemd-run`,最后才退回 detached-helper。客户端按 boot id 确认新进程已恢复后关闭横幅,交由 DSH Web 自身的后台连接重试恢复页面,不强制整页刷新,避免认证/路由切换期间白屏。重启链路已在当前 DSH Web `0.2.0-rc.2` 实机核验(2026-10-01):dsh-m 探测 unit `Restart=` 策略后经 `appExit` 交还 systemd,`status=75/TEMPFAIL` 退出由 Restart 策略接住自动拉起,服务恢复后页面后台重连、面板全功能可用。历史口径:`0.1.5-rc.1` 时代已核验 `/dshm` ping 与带认证 `303 → 200`;`0.1.2-rc.1` 按契约核对 + 形态探测兜底收录(verified 数组),部署面演进后不再追旧代 live E2E;transient `systemd-run` 兜底仅适用于无 `appExit` 的宿主(受支持代际均提供 `appExit`),保持设计兜底而非发布门槛;连续安装/卸载由 profile 事务测试面(补偿事务、装后守卫等 813 用例)与 0.4.0→0.4.2 连续发布实证覆盖。安装过程实时显示 pnpm 进度(解析 → 下载 → 链接 → 构建)。
50
+
51
+ ### 0.9.1 新增:头部版本角标升级提示(有更新才点亮)
52
+
53
+ - **静默口径**:面板头部 `dsh-m vX.Y.Z` 角标常态维持 0.7.5 起的静态展示——已是最新、检查失败、本地 dev 版领先 npm(ahead)一律不打扰(ahead 仅在悬停 title 里提示「本地开发版」)。
54
+ - **仅 outdated 点亮**:`self-check`(npm latest vs 装机版本,只读)判定有新版本时,角标点亮为 warn 态并显示 `⬆ v<最新>`;点击即触发 `self-upgrade`(同一 mutation session + 装后守卫),成功后出「⚡ 一键重启」横幅。Desktop 下点击按能力表结构化拒绝(409,带官方生命周期指引)。
55
+ - **TTL 缓存 + 版本护栏**:检查结果在浏览器 localStorage 缓存 30 分钟,开面板不重复打 npm registry;升级重启后缓存按版本号自动作废。检查失败静默,不弹任何错误。
56
+
57
+ ### 0.9.0 新增:官方 Desktop(双 profile)支持
58
+
59
+ - **同一包、两个 profile**:dsh-m 现在可装入官方 Desktop 的 `desktop` profile(`~/.dsh/profiles/desktop`),与 Web 的 `web` profile 并列;市场目录、面板与 agent 工具同一套,管理对象始终是宿主当前 profile(官方 `profileContext` 单一事实源)。
60
+ - **入口信任检查改委派官方**:`/dshm` 全部 method(含 ping 与未知 method)在读取请求体之前委派官方 `connection.requestRejection()` 判定(trustedHosts / loopback / 跨站 / `Origin: null` 语义随宿主),被拒请求零 body 消耗、零业务调用;宿主缺该能力时 fail-closed 全拒。**行为变化**:旧版自制守卫「缺 Origin 一律 403」不再存在——Desktop 桥合法剥除 Origin 的请求按官方语义放行,无凭据的健康检查探针从「一律 403」变为「按宿主信任判定」。
61
+ - **Desktop 首发能力表**:只读市场 + **安装新包**(委派官方 `pluginManager.installBundle`,完整性/锁/生效相位归官方)+ **开关**(委派官方管理器,服务缺席结构化拒绝、绝不文件级 fallback);**升级 / 卸载 / dsh-m 自更新 / 一键重启**在 Desktop 结构化拒绝(409,带官方入口指引——官方暂无 upgrade API,重启归 Electron 生命周期);构建脚本按官方 `pendingBuilds` 名单精确重试,绝不全量放行。
62
+ - **读模型与缓存按 profile 隔离**:市场安装标注、已装列表、README 预览只读当前 profile;registry / 社区清单 / accepted-source 缓存按 profile 分段(web 沿用旧路径,零迁移零清空);收藏与操作记录按浏览器 origin 各自独立,Web 与 Desktop 不自动同步。
63
+ - **CLI 恒作用于 web profile**:`--profile web` 显式声明;`--profile desktop` 明确拒绝并指引官方 Desktop 插件管理页。
64
+ - **如实声明**:Desktop 实机(Win/macOS)E2E 未跑,`registry.json` verified 数组**不新增** Desktop 代际(实测后按收录纪律补录);Desktop 下不做 dsh-m 文件级装后守卫(app.asar 打包布局探测盲区),以官方结果判定 + `listBundles` 复读替代。
45
65
 
46
66
  ### 0.8.5 修复
47
67
 
@@ -156,6 +176,8 @@ dshm restart --yes
156
176
 
157
177
  清单不可用时 `registry` / `search` / `outdated` 打印配置与实际生效地址并退出码 1;`list` 仍列出已装插件。CLI 固定独立缓存命名空间,不影响 Web 端。
158
178
 
179
+ **profile 目标(0.9.0)**:CLI 恒作用于 web profile——`--profile web` 为显式声明;`--profile desktop` 直接拒绝(Desktop profile 的插件管理走官方 Desktop 插件管理页)。
180
+
159
181
  ## 收录清单(registry)
160
182
 
161
183
  `registry.json` 手工 curated,运行时按 **GitHub 原始文件(raw @main)→ GitHub 镜像(jsDelivr CDN,备用线路)→ 本地 60 分钟 TTL 缓存 → 包内快照** 的顺序获取——收录更新与插件发版**解耦**,push 后最多等一个缓存周期(可在设置页强制刷新)。收录 / 修订直接改 `registry.json` 发 PR,CI 自动校验:严格 schema、npm 包与 GitHub 仓库存在性、重复 id、URL 可达性。
@@ -202,6 +224,9 @@ node scripts/validate-registry.mjs
202
224
  **1. 为什么 GitHub 来源的更新提示不走 main HEAD?**
203
225
  main 上的中间提交可能不稳定。dsh-m 只跟踪 **release / tag**(优先 `releases/latest`,无 release 回退 tags 列表),安装时锁定 tag 指向的 commit SHA。
204
226
 
227
+ **1.5 官方 Desktop 上能用哪些功能?**
228
+ Desktop(desktop profile)首发支持:浏览市场、**安装新包**、插件开关;升级/卸载/自更新/一键重启会返回结构化拒绝并指引官方入口(官方插件管理页 / Desktop 应用重启)。市场安装标注、已装列表只反映 Desktop 自己装了什么;与 Web 端的收藏、操作记录互不同步。
229
+
205
230
  **2. 卸载 dsh-m 会删我的数据吗?**
206
231
  不会。只移除 profile 中的包引用(卸载前先下线运行中的界面),并把疑似残留路径报告给你。
207
232
 
package/docs/DESIGN.md CHANGED
@@ -119,7 +119,7 @@ npm 安装 / GitHub 安装 / 升级 / 自升级 / 卸载是**同一个事务模
119
119
  - **卸载**(事务定序写死):validate(严格读取:NOT_INSTALLED / NOT_DSH_PLUGIN / PLUGIN_METADATA_UNREADABLE,全部零写入零快照)→ 快照 → live-disable(先让 client bundle 下线,避免 404;回滚时尽力反向)→ 摘除该包补丁条目(`pnpm-workspace.yaml` 顶层 `patchedDependencies` 与 `package.json#pnpm.patchedDependencies`;残留条目会令 pnpm 以 `ERR_PNPM_UNUSED_PATCH` 整单失败,只精确匹配 `pkg` / `pkg@ver`,补丁文件本体保留并计入残留报告)→ remove → verify gone(仍在则回滚)。**不清理插件产生的数据/配置**,但把检测到的疑似残留路径(如 `~/.dsh/<plugin>.json`)列出报告。
120
120
  - **升级**:**按需检查**(`dshm_outdated` / `dshm_list` 时实时比对本地版本 vs npm latest / GitHub release,半自动——展示升级计划,确认后执行,即重开一次安装事务)。**不做后台定时器**。
121
121
  - **自更新**:dsh-m 对自己同样做版本比对 + 提示升级(设置页呈现);执行即 `install-npm` 事务(integrity 缺失直接拒绝,不进事务)。
122
- - **开关(0.4.0,术语见 CONTEXT.md「开关/行覆盖/Bundle 选择/委派降级」)**:已装插件运行状态的可逆切换,**不经 Profile 变更事务**。粒度自动路由——包内补丁层单 insert 行且无配置补丁行 → 行覆盖(`cordis.patch.yml` 保注释 `disabled` 编辑,官方 `writePluginEnabled` 同款语义,启用写显式 `disabled: false`);多行/不可解析 → Bundle 选择(`dsh.profile.bundles` 数组增删)。写路径委派优先:运行时探测官方 `pluginManager` 服务(`ctx.get`,按存在性不判版本号)→ `setPluginEnabled`/`setBundleEnabled`(白拿保护判定、unaddressable 识别、hmr 活体重组;applied 以复读 `listPlugins()` 为准);服务缺席降级 loader `entry.update()` 直操作 + 锁内文件编辑(bundle 级恒 restart-required)。**必须持官方同款锁**(`withFileLock(<profile>/package.json)`)防与官方 UI 并发丢更新。架构决策见 [ADR-0001](./adr/0001-delegate-with-fallback-for-plugin-manager.md)。
122
+ - **开关(0.4.0,术语见 GLOSSARY.md「开关/行覆盖/Bundle 选择/委派降级」)**:已装插件运行状态的可逆切换,**不经 Profile 变更事务**。粒度自动路由——包内补丁层单 insert 行且无配置补丁行 → 行覆盖(`cordis.patch.yml` 保注释 `disabled` 编辑,官方 `writePluginEnabled` 同款语义,启用写显式 `disabled: false`);多行/不可解析 → Bundle 选择(`dsh.profile.bundles` 数组增删)。写路径委派优先:运行时探测官方 `pluginManager` 服务(`ctx.get`,按存在性不判版本号)→ `setPluginEnabled`/`setBundleEnabled`(白拿保护判定、unaddressable 识别、hmr 活体重组;applied 以复读 `listPlugins()` 为准);服务缺席降级 loader `entry.update()` 直操作 + 锁内文件编辑(bundle 级恒 restart-required)。**必须持官方同款锁**(`withFileLock(<profile>/package.json)`)防与官方 UI 并发丢更新。架构决策见 [ADR-0001](./adr/0001-delegate-with-fallback-for-plugin-manager.md)。
123
123
  - **运行相位(0.4.0)**:loader fiber 状态投影(active/failed/pending/loading/unloading;fiber 缺失即已停用),读路径始终自读 loader entries,读路径零服务依赖。
124
124
  - **保护名单(0.4.0)**:`dsh-m` 自身 + 逐字镜像官方 `protectedModules` 16 项全集(锚点 `dsh-plugin-manager@0.1.7-rc.2 lib/index.js L1077`;DSH 升级后 diff 官方名单)。作用于**开关与卸载**(保护门为开关路径最先判定,纯名单检查先于在装枚举);升级不受影响;委派路径信任官方 `readOnlyReason`。
125
125
  - **peer 兼容预检(0.4.0)**:install/upgrade 事务外 metadata 阶段,校验 `@deepseek-ai/dsh(-*)` peers 与运行时版本(`workspace:^/~/ *` 视为运行时版本;semver includePrerelease;官方 `evaluatePluginCompatibility` 同语义)。运行时版本解析失败或预检元数据读取失败 → 不拦(如实标注未检);不兼容 → GUI 弹确认 / agent 回结构化 issue / CLI `--force`;无豁免机制(确认即通道);GitHub 源明示 `compatSkipped`。
package/lib/cli.js CHANGED
@@ -138,6 +138,9 @@ const HELP = `dshm — DSH Marketplace(DSH 插件市场:精选策展 + 社
138
138
  dshm restart --yes
139
139
  注意:变更互斥仅在进程内生效——变更执行期间不要同时从 GUI / Agent 工具发起另一次变更。
140
140
 
141
+ profile 目标(0.9.0):CLI 恒作用于 web profile;--profile web 为显式声明,
142
+ --profile desktop 会被拒绝——Desktop profile 的插件管理走官方 Desktop 插件管理页。
143
+
141
144
  环境变量:DSHM_REGISTRY_URL(registry 源覆盖)、DSHM_TIMEOUT_MS、DSHM_CACHE_TTL_MIN、DSHM_CACHE_DIR、
142
145
  DSHM_COMMUNITY_CATALOG=0(退出社区清单)、DSHM_COMMUNITY_CATALOG_PIN(锁定社区目录版本)
143
146
  `;
@@ -166,6 +169,15 @@ async function runCliDispatch(argv, deps, io) {
166
169
  };
167
170
  const { cmd, flags } = parseArgs(argv);
168
171
  const cfg = cliConfig();
172
+ // 0.9.0 双 profile(ADR-0005):CLI 是 Web-only 入口(独立进程拿不到宿主 profileContext,
173
+ // 目标恒为 web profile);--profile desktop / 其他非 web 值显式拒绝并指引官方入口,
174
+ // 绝不隐式回落 web(报告 §5.3「不能从不存在的 Desktop 模式推断」)。
175
+ const profileFlag = typeof flags.profile === 'string' ? flags.profile.trim() : '';
176
+ if (profileFlag !== '' && profileFlag !== 'web') {
177
+ err(`错误:dshm CLI 仅作用于 web profile,不支持 --profile ${profileFlag}。`);
178
+ err('Desktop profile 的插件管理请使用官方 Desktop 的插件管理页(Settings → Plugins),或回到 DSH Web 端使用 dsh-m。');
179
+ return 1;
180
+ }
169
181
  switch (cmd) {
170
182
  case 'help':
171
183
  case '--help':
package/lib/client.js CHANGED
@@ -965,6 +965,73 @@ var init_favorites = __esm({
965
965
  }
966
966
  });
967
967
 
968
+ // src/client/self-check.js
969
+ var self_check_exports = {};
970
+ __export(self_check_exports, {
971
+ SELF_CHECK_STORAGE_KEY: () => SELF_CHECK_STORAGE_KEY,
972
+ SELF_CHECK_TTL_MS: () => SELF_CHECK_TTL_MS,
973
+ clearSelfCheckCache: () => clearSelfCheckCache,
974
+ deriveChipState: () => deriveChipState,
975
+ readSelfCheckCache: () => readSelfCheckCache,
976
+ writeSelfCheckCache: () => writeSelfCheckCache
977
+ });
978
+ function readSelfCheckCache(storage, opts = {}) {
979
+ if (!storage || typeof storage.getItem !== "function") return null;
980
+ const now = typeof opts.now === "number" ? opts.now : Date.now();
981
+ const ttlMs = typeof opts.ttlMs === "number" ? opts.ttlMs : SELF_CHECK_TTL_MS;
982
+ let raw = null;
983
+ try {
984
+ raw = storage.getItem(SELF_CHECK_STORAGE_KEY);
985
+ } catch {
986
+ return null;
987
+ }
988
+ if (typeof raw !== "string" || raw === "") return null;
989
+ let box;
990
+ try {
991
+ box = JSON.parse(raw);
992
+ } catch {
993
+ return null;
994
+ }
995
+ if (!box || typeof box !== "object") return null;
996
+ if (typeof box.ts !== "number" || !Number.isFinite(box.ts)) return null;
997
+ if (now - box.ts > ttlMs) return null;
998
+ const data = box.data;
999
+ if (!data || typeof data !== "object" || typeof data.current !== "string") return null;
1000
+ if (typeof opts.version === "string" && opts.version !== "" && data.current !== opts.version) return null;
1001
+ return data;
1002
+ }
1003
+ function writeSelfCheckCache(storage, data, opts = {}) {
1004
+ if (!storage || typeof storage.setItem !== "function") return;
1005
+ if (!data || typeof data !== "object" || typeof data.current !== "string") return;
1006
+ const now = typeof opts.now === "number" ? opts.now : Date.now();
1007
+ try {
1008
+ storage.setItem(SELF_CHECK_STORAGE_KEY, JSON.stringify({ ts: now, data }));
1009
+ } catch {
1010
+ }
1011
+ }
1012
+ function clearSelfCheckCache(storage) {
1013
+ try {
1014
+ if (storage && typeof storage.removeItem === "function") storage.removeItem(SELF_CHECK_STORAGE_KEY);
1015
+ } catch {
1016
+ }
1017
+ }
1018
+ function deriveChipState(check, version) {
1019
+ if (!check || typeof check !== "object") return { kind: "idle" };
1020
+ if (check.outdated === true && typeof check.latest === "string" && check.latest !== "") {
1021
+ return { kind: "outdated", latest: check.latest };
1022
+ }
1023
+ if (check.ahead === true) return { kind: "ahead", latest: typeof check.latest === "string" ? check.latest : null };
1024
+ return { kind: "idle" };
1025
+ }
1026
+ var SELF_CHECK_STORAGE_KEY, SELF_CHECK_TTL_MS;
1027
+ var init_self_check = __esm({
1028
+ "src/client/self-check.js"() {
1029
+ "use strict";
1030
+ SELF_CHECK_STORAGE_KEY = "dshm-selfcheck";
1031
+ SELF_CHECK_TTL_MS = 30 * 60 * 1e3;
1032
+ }
1033
+ });
1034
+
968
1035
  // src/client/main.jsx
969
1036
  var React = require("react");
970
1037
  var rd = require("react-dom");
@@ -982,6 +1049,7 @@ var { RESTART_POLL_MS: RESTART_POLL_MS2, RESTART_DEADLINE_MS: RESTART_DEADLINE_M
982
1049
  var { refreshAfterMutation: refreshAfterMutation2 } = (init_view_refresh(), __toCommonJS(view_refresh_exports));
983
1050
  var { createOperationsStore: createOperationsStore2, restoreRecords: restoreRecords2, createOpsPump: createOpsPump2, opAppliesTo: opAppliesTo2 } = (init_operations(), __toCommonJS(operations_exports));
984
1051
  var { createFavoritesStore: createFavoritesStore2, partitionStale: partitionStale2 } = (init_favorites(), __toCommonJS(favorites_exports));
1052
+ var { readSelfCheckCache: readSelfCheckCache2, writeSelfCheckCache: writeSelfCheckCache2, clearSelfCheckCache: clearSelfCheckCache2, deriveChipState: deriveChipState2 } = (init_self_check(), __toCommonJS(self_check_exports));
985
1053
  var ZH = {
986
1054
  "market.title": "\u63D2\u4EF6\u5E02\u573A",
987
1055
  "tab.market": "\u5E02\u573A",
@@ -1206,6 +1274,8 @@ var ZH = {
1206
1274
  "notify.leftovers": "\uFF1B\u68C0\u6D4B\u5230\u7591\u4F3C\u6B8B\u7559\u6570\u636E\uFF1A{paths}",
1207
1275
  "notify.upgraded": "\u5DF2\u5347\u7EA7 {pkg}\uFF08{from} \u2192 {to}\uFF09",
1208
1276
  "notify.upgradehint": "\uFF08\u6CE8\u610F\uFF1A\u8BE5\u63D2\u4EF6\u6267\u884C\u4E86\u6784\u5EFA\u811A\u672C\uFF09",
1277
+ "notify.selfupgraded": "dsh-m \u5DF2\u5347\u7EA7\uFF08v{from} \u2192 v{to}\uFF09",
1278
+ "notify.selfupgraded.failed": "dsh-m \u5347\u7EA7\u5931\u8D25\uFF1A{err}",
1209
1279
  "failed.install": "\u5B89\u88C5\u5931\u8D25\uFF1A{err}",
1210
1280
  "failed.uninstall": "\u5378\u8F7D\u5931\u8D25\uFF1A{err}",
1211
1281
  "failed.upgrade": "\u5347\u7EA7\u5931\u8D25\uFF1A{err}",
@@ -1219,6 +1289,11 @@ var ZH = {
1219
1289
  "restart.failed": "\u91CD\u542F\u5931\u8D25\uFF1A{err}",
1220
1290
  "restart.timeout": "\u91CD\u542F\u8D85\u65F6\uFF0C\u8BF7\u624B\u52A8\u68C0\u67E5 dsh web \u670D\u52A1\u72B6\u6001",
1221
1291
  "restart.hint.done": "\u5DF2\u8BF7\u6C42\u91CD\u542F DSH web\uFF08via {via}\uFF09\u3002\u670D\u52A1\u6062\u590D\u540E DSH Web \u4F1A\u5728\u540E\u53F0\u81EA\u52A8\u91CD\u8FDE\u3002",
1292
+ "profile.restartHint": "\u53D8\u66F4\u5B8C\u6210\u3002Desktop \u63D2\u4EF6\u7531\u5B98\u65B9\u5E94\u7528\u7BA1\u7406\uFF1A\u8BF7\u9000\u51FA\u5E76\u91CD\u65B0\u6253\u5F00 Desktop \u5E94\u7528\uFF08\u5173\u95ED\u7A97\u53E3\u53EF\u80FD\u53EA\u662F\u9690\u85CF\uFF09\u4EE5\u52A0\u8F7D\u65B0\u72B6\u6001\u3002",
1293
+ "profile.chipTitle": "\u5F53\u524D DSH profile\uFF1A{name}\uFF08Desktop \u9996\u53D1\u4EC5\u652F\u6301\u53EA\u8BFB\u5E02\u573A\u3001\u5B89\u88C5\u65B0\u5305\u4E0E\u5F00\u5173\uFF09",
1294
+ "selfupdate.available": "\u53D1\u73B0\u65B0\u7248\u672C v{v}\uFF0C\u70B9\u51FB\u5347\u7EA7",
1295
+ "selfupdate.ahead": "\u672C\u5730\u5F00\u53D1\u7248\uFF08\u9886\u5148 npm\uFF1Av{v}\uFF09",
1296
+ "selfupdate.checking": "\u68C0\u67E5\u66F4\u65B0\u4E2D\u2026",
1222
1297
  "phase.resolving": "\u89E3\u6790\u4F9D\u8D56",
1223
1298
  "phase.downloading": "\u4E0B\u8F7D",
1224
1299
  "phase.linking": "\u94FE\u63A5\u5B89\u88C5",
@@ -1459,6 +1534,8 @@ var EN = {
1459
1534
  "notify.leftovers": "; possible leftover data: {paths}",
1460
1535
  "notify.upgraded": "Upgraded {pkg} ({from} \u2192 {to})",
1461
1536
  "notify.upgradehint": " (note: this plugin ran build scripts)",
1537
+ "notify.selfupgraded": "dsh-m upgraded (v{from} \u2192 v{to})",
1538
+ "notify.selfupgraded.failed": "dsh-m upgrade failed: {err}",
1462
1539
  "failed.install": "Install failed: {err}",
1463
1540
  "failed.uninstall": "Uninstall failed: {err}",
1464
1541
  "failed.upgrade": "Upgrade failed: {err}",
@@ -1472,6 +1549,11 @@ var EN = {
1472
1549
  "restart.failed": "Restart failed: {err}",
1473
1550
  "restart.timeout": "Restart timed out \u2014 check the dsh web service manually",
1474
1551
  "restart.hint.done": "Restart requested (via {via}). DSH Web will reconnect in the background after the service returns.",
1552
+ "profile.restartHint": "Changes applied. Desktop plugins are managed by the official app: quit and reopen the Desktop app (closing the window may only hide it) to load the new state.",
1553
+ "profile.chipTitle": "Current DSH profile: {name} (Desktop first release supports read-only market, installing new packages, and toggles)",
1554
+ "selfupdate.available": "New version v{v} available \u2014 click to upgrade",
1555
+ "selfupdate.ahead": "Local dev build (ahead of npm: v{v})",
1556
+ "selfupdate.checking": "Checking for updates\u2026",
1475
1557
  "phase.resolving": "Resolving",
1476
1558
  "phase.downloading": "Downloading",
1477
1559
  "phase.linking": "Linking",
@@ -1696,6 +1778,12 @@ button.dshm-badge:hover{filter:brightness(.95)}
1696
1778
  /* \u5934\u90E8 dsh-m \u7248\u672C\u89D2\u6807\uFF1A\u7B49\u5BBD\u5C0F\u5B57\u5706\u89D2\uFF0C\u9759\u6001\u5C55\u793A\u4E0D\u52A0\u7C97\u4E0D\u53EF\u70B9\uFF080.7.5 \u8D77\u6539\u663E dsh-m \u7248\u672C\uFF09 */
1697
1779
  .dshm-dshchip{display:inline-flex;align-items:center;gap:4px;border:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:var(--dsw-alias-bg-layer-1,#f5f6f8);color:var(--dsw-alias-label-secondary,#4b5563);border-radius:999px;padding:5px 10px;font:11px/1 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;white-space:nowrap;flex:none}
1698
1780
  .dshm-dshchip-v{font-weight:400;color:inherit}
1781
+ /* 0.9.1\uFF1A\u4EC5 self-check \u5224 outdated \u624D\u70B9\u4EAE\u2014\u2014warn \u6001\u4E3A\u53EF\u70B9\u6309\u94AE\uFF08\u70B9\u51FB\u5373\u81EA\u5347\u7EA7\uFF09\uFF1B\u5176\u4F59\u9759\u9ED8\u590D\u7528\u9759\u6001\u6837\u5F0F */
1782
+ button.dshm-dshchip{cursor:pointer}
1783
+ button.dshm-dshchip:disabled{cursor:default;opacity:.75}
1784
+ .dshm-dshchip.warn{border-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#b45309) 45%,transparent);background:var(--dsw-alias-state-warn-tertiary,#fffbeb);color:var(--dsw-alias-state-warn-primary,#b45309)}
1785
+ .dshm-dshchip.warn:hover:not(:disabled){filter:brightness(.96)}
1786
+ .dshm-dshchip .dshm-up{font-weight:600}
1699
1787
 
1700
1788
  /* 0.4.0\uFF1A\u5F00\u5173 / \u76F8\u4F4D\u70B9 / \u517C\u5BB9\u786E\u8BA4\u5F39\u7A97 */
1701
1789
  .dshm-dot{font-size:9px;line-height:1;vertical-align:middle;margin-right:2px}
@@ -1913,7 +2001,7 @@ function TwoStepButton({ label, confirmLabel, className, onConfirm, disabled })
1913
2001
  arm ? confirmLabel : label
1914
2002
  );
1915
2003
  }
1916
- function RestartBanner({ note, onDone }) {
2004
+ function RestartBanner({ note, onDone, desktop }) {
1917
2005
  const [phase, setPhase] = useState("idle");
1918
2006
  const [err, setErr] = useState(null);
1919
2007
  const restart = useCallback(async () => {
@@ -1956,7 +2044,7 @@ function RestartBanner({ note, onDone }) {
1956
2044
  { className: "dshm-banner-text" },
1957
2045
  phase === "restarting" ? lookup("restart.doing") : phase === "waiting" ? lookup("restart.waiting") : err ? lookup("restart.failed", { err }) : note || lookup("banner.done")
1958
2046
  ),
1959
- phase === "idle" && !err ? h("button", { className: "dshm-btn primary sm", onClick: restart }, lookup("restart.now")) : null,
2047
+ phase === "idle" && !err && !desktop ? h("button", { className: "dshm-btn primary sm", onClick: restart }, lookup("restart.now")) : null,
1960
2048
  phase === "restarting" || phase === "waiting" ? Spin() : null,
1961
2049
  phase === "idle" && err ? h("button", { className: "dshm-btn sm", onClick: () => onDone(false) }, lookup("common.ok")) : null,
1962
2050
  phase === "idle" && !err ? h("button", { className: "dshm-btn sm", onClick: () => onDone(false) }, lookup("common.later")) : null
@@ -3254,10 +3342,64 @@ var TABS = [
3254
3342
  ["installed", "tab.installed", "installed"],
3255
3343
  ["settings", "tab.settings", null]
3256
3344
  ];
3257
- function DshmVersionChip({ version }) {
3345
+ function DshmVersionChip({ version, notify, initialCheck }) {
3346
+ const [check, setCheck] = useState(
3347
+ () => initialCheck !== void 0 ? initialCheck : typeof window !== "undefined" && window.localStorage ? readSelfCheckCache2(window.localStorage, { version }) : null
3348
+ );
3349
+ const [busy, setBusy] = useState(false);
3350
+ useEffect(() => {
3351
+ if (initialCheck !== void 0) return;
3352
+ let live = true;
3353
+ api("self-check").then((data) => {
3354
+ if (!live) return;
3355
+ if (data && typeof data === "object" && typeof data.current === "string") {
3356
+ writeSelfCheckCache2(typeof window !== "undefined" && window.localStorage ? window.localStorage : null, data);
3357
+ setCheck(data);
3358
+ }
3359
+ }).catch(() => {
3360
+ });
3361
+ return () => {
3362
+ live = false;
3363
+ };
3364
+ }, [initialCheck]);
3365
+ const doUpgrade = async () => {
3366
+ if (busy) return;
3367
+ setBusy(true);
3368
+ try {
3369
+ const res = await api("self-upgrade");
3370
+ clearSelfCheckCache2(typeof window !== "undefined" && window.localStorage ? window.localStorage : null);
3371
+ setCheck({ current: version, latest: null, outdated: false, ahead: false });
3372
+ notify({
3373
+ kind: "ok",
3374
+ needsRestart: true,
3375
+ text: lookup("notify.selfupgraded", { from: version, to: res.version || "latest" }) + (res.buildApprovals && res.buildApprovals.length ? lookup("notify.builds", { names: res.buildApprovals.join(", ") }) : res.fallbackAllBuilds ? lookup("notify.builds.fallback") : "")
3376
+ });
3377
+ } catch (e) {
3378
+ notify({ kind: "err", text: lookup("notify.selfupgraded.failed", { err: e && e.message || e }) });
3379
+ } finally {
3380
+ setBusy(false);
3381
+ }
3382
+ };
3383
+ const state = deriveChipState2(check, version);
3384
+ if (state.kind === "outdated") {
3385
+ return h(
3386
+ "button",
3387
+ {
3388
+ type: "button",
3389
+ className: "dshm-dshchip warn",
3390
+ title: lookup("selfupdate.available", { v: state.latest }),
3391
+ onClick: doUpgrade,
3392
+ disabled: busy
3393
+ },
3394
+ "dsh-m ",
3395
+ h("span", { className: "dshm-dshchip-v" }, `v${version}`),
3396
+ busy ? h(Spin) : h("span", { className: "dshm-up" }, `\u2B06 v${state.latest}`)
3397
+ );
3398
+ }
3399
+ const title = state.kind === "ahead" && state.latest ? lookup("selfupdate.ahead", { v: state.latest }) : `dsh-m v${version}`;
3258
3400
  return h(
3259
3401
  "span",
3260
- { className: "dshm-dshchip", title: `dsh-m v${version}` },
3402
+ { className: "dshm-dshchip", title },
3261
3403
  "dsh-m ",
3262
3404
  h("span", { className: "dshm-dshchip-v" }, `v${version}`)
3263
3405
  );
@@ -3332,10 +3474,13 @@ function MarketPanel({ onClose }) {
3332
3474
  removeIds: useCallback((ids) => setFavList(favStore.removeIds(ids)), [favStore])
3333
3475
  };
3334
3476
  const [pluginVersion, setPluginVersion] = useState(null);
3477
+ const [profile, setProfile] = useState(null);
3335
3478
  useEffect(() => {
3336
3479
  let live = true;
3337
3480
  api("ping").then((r) => {
3338
- if (live) setPluginVersion(typeof r?.version === "string" && r.version ? r.version : null);
3481
+ if (!live) return;
3482
+ setPluginVersion(typeof r?.version === "string" && r.version ? r.version : null);
3483
+ setProfile(r?.profile && typeof r.profile === "object" ? r.profile : null);
3339
3484
  }).catch(() => {
3340
3485
  });
3341
3486
  return () => {
@@ -3421,8 +3566,12 @@ function MarketPanel({ onClose }) {
3421
3566
  }, [toast]);
3422
3567
  const notify = useCallback(({ kind, text, needsRestart }) => {
3423
3568
  setToast({ kind, text });
3424
- if (kind === "ok" && needsRestart) setBanner({ text: lookup("banner.done") });
3425
- }, []);
3569
+ if (kind === "ok" && needsRestart) {
3570
+ setBanner(
3571
+ profile && profile.kind === "desktop" ? { desktop: true, text: lookup("profile.restartHint") } : { text: lookup("banner.done") }
3572
+ );
3573
+ }
3574
+ }, [profile]);
3426
3575
  return h(
3427
3576
  "div",
3428
3577
  { className: full ? "dshm-overlay full" : "dshm-overlay", onClick: onClose },
@@ -3455,7 +3604,8 @@ function MarketPanel({ onClose }) {
3455
3604
  )
3456
3605
  ),
3457
3606
  h("span", { className: "dshm-spacer" }),
3458
- pluginVersion ? h(DshmVersionChip, { version: pluginVersion }) : null,
3607
+ pluginVersion ? h(DshmVersionChip, { version: pluginVersion, notify }) : null,
3608
+ profile && profile.kind !== "web" ? h("span", { className: "dshm-dshchip", title: lookup("profile.chipTitle", { name: profile.name }) }, profile.name) : null,
3459
3609
  // 窗口控制组(0.7.7):最大化/还原 + 关闭,连体设计(系统化窗口按钮)
3460
3610
  h(
3461
3611
  "div",
@@ -3498,7 +3648,7 @@ function MarketPanel({ onClose }) {
3498
3648
  { className: toast.kind === "err" ? "dshm-banner err" : "dshm-banner" },
3499
3649
  h("span", { className: "dshm-banner-text" }, toast.text)
3500
3650
  ) : null,
3501
- banner ? h(RestartBanner, { note: banner.text, onDone: () => setBanner(null) }) : null
3651
+ banner ? h(RestartBanner, { note: banner.text, onDone: () => setBanner(null), desktop: banner.desktop === true }) : null
3502
3652
  )
3503
3653
  );
3504
3654
  }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * ActiveProfile(0.9.0 双 profile,ADR-0005):宿主当前 profile 的单一事实源。
3
+ *
4
+ * 事实源是官方 `ctx.get('profileContext')`(@deepseek-ai/dsh-app-boot,仅 dsh 启动的
5
+ * profile 存在);缺席(旧宿主 / 测试 / CLI)时降级 fallback = web 推导目录,且 source
6
+ * 如实标注 'fallback'。kind 决定能力表:desktop 只放行 install/set-enabled;
7
+ * 未知名一律拒写(报告 §4「对未知 profile 显式拒写」)。不得从页面 Origin、DSH_HOME、
8
+ * process.cwd() 或文件存在性猜 profile(报告 §4 同业旁证 #655/#744)。
9
+ */
10
+ import { webProfileDir, WEB_PROFILE } from './env.js';
11
+ /** Desktop 首发能力表(ADR-0005):只读市场 + 安装新包 + 开关;其余结构化拒绝并指引官方入口。 */
12
+ const DESKTOP_ALLOWED = new Set(['install', 'set-enabled']);
13
+ /** 结构化拒绝的官方入口指引(zh;GUI/工具/HTTP 三入口共用同一文案源)。 */
14
+ export const WRITE_GUIDANCE = {
15
+ install: '',
16
+ 'set-enabled': '',
17
+ upgrade: 'Desktop 暂不支持在市场内升级插件:请在官方 Desktop 的插件管理(Settings → Plugins)检查更新,或回到 DSH Web 使用 dsh-m 升级。',
18
+ uninstall: 'Desktop 暂不支持在市场内卸载插件:请使用官方 Desktop 的插件管理(Settings → Plugins)卸载。',
19
+ 'self-upgrade': 'Desktop 暂不支持 dsh-m 自更新:请在 DSH Web 端执行,或关注官方 Desktop 的插件更新入口。',
20
+ restart: 'Desktop 不由 dsh-m 重启:请退出并重新打开 Desktop 应用(关闭窗口可能只是隐藏),让新安装的插件生效。',
21
+ };
22
+ export class ProfileUnsupportedError extends Error {
23
+ code;
24
+ action;
25
+ profile;
26
+ guidance;
27
+ constructor(action, profile) {
28
+ const guidance = WRITE_GUIDANCE[action];
29
+ super(`当前 profile(${profile.name})不支持该操作(${action})。${guidance}`);
30
+ this.name = 'ProfileUnsupportedError';
31
+ this.code = 'unsupported-on-profile';
32
+ this.action = action;
33
+ this.profile = profile.name;
34
+ this.guidance = guidance;
35
+ }
36
+ }
37
+ function kindOf(name) {
38
+ if (name === 'desktop')
39
+ return 'desktop';
40
+ if (name === WEB_PROFILE)
41
+ return 'web';
42
+ return 'unknown';
43
+ }
44
+ /**
45
+ * 守卫式解析:宿主注入 profileContext → 事实取宿主;缺席/抛错/畸形 → fallback web。
46
+ * 在 host 生命周期内只应调用一次(host.ts apply 期),结果作为不可变对象传递。
47
+ */
48
+ export function resolveActiveProfile(ctx) {
49
+ let pc;
50
+ try {
51
+ const getter = ctx?.get;
52
+ if (typeof getter === 'function') {
53
+ pc = getter.call(ctx, 'profileContext');
54
+ }
55
+ }
56
+ catch {
57
+ pc = undefined;
58
+ }
59
+ const name = typeof pc?.name === 'string' && pc.name.trim() !== '' ? pc.name.trim() : undefined;
60
+ const dir = typeof pc?.dir === 'string' && pc.dir.trim() !== '' ? pc.dir.trim() : undefined;
61
+ if (pc === undefined || pc === null || name === undefined || dir === undefined) {
62
+ return { name: WEB_PROFILE, kind: 'web', dir: webProfileDir(), source: 'fallback' };
63
+ }
64
+ return { name, kind: kindOf(name), dir, source: 'host' };
65
+ }
66
+ /** 能力表守卫:违反即抛结构化 ProfileUnsupportedError(HTTP 409 / 工具 error result 共用)。 */
67
+ export function assertWriteAllowed(profile, action) {
68
+ if (profile.kind === 'web')
69
+ return;
70
+ if (profile.kind === 'desktop' && DESKTOP_ALLOWED.has(action))
71
+ return;
72
+ throw new ProfileUnsupportedError(action, profile);
73
+ }
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { mkdir, readFile, rm, rename, writeFile } from 'node:fs/promises';
10
10
  import { isAbsolute, join, relative } from 'node:path';
11
- import { cacheDir } from './env.js';
11
+ import { cacheRoot } from './env.js';
12
12
  import { decodeUtf8Fatal, fetchBytesLimited, describeFetchFailure } from './httpx.js';
13
13
  import { isExactVersion, npmLatest } from './versions.js';
14
14
  import { communityOutcome } from './market.js';
@@ -106,8 +106,9 @@ const ROUTE_LABELS = ['jsdelivr', 'npmmirror', 'unpkg'];
106
106
  const META_LATEST = 'meta.json';
107
107
  const metaPinName = (v) => `meta-pin-${v}.json`;
108
108
  const bodyName = (v) => `catalog-${v}.json`;
109
- function awesomeDir(namespace) {
110
- return join(cacheDir(), namespace, 'awesome');
109
+ /** 社区缓存目录(0.9.0 双 profile:web=旧路径,非 web 加 `<root>/<profile>` 段)。 */
110
+ function awesomeDir(namespace, profile = 'web') {
111
+ return join(cacheRoot(profile), namespace, 'awesome');
111
112
  }
112
113
  /** 路径 containment:目标必须仍在 dir 内(防御拼接穿越)。 */
113
114
  function contained(dir, p) {
@@ -198,7 +199,7 @@ function trimPin(cfg) {
198
199
  }
199
200
  async function runChain(cfg, opts, ctrl) {
200
201
  const namespace = opts.namespace ?? 'host';
201
- const dir = awesomeDir(namespace);
202
+ const dir = awesomeDir(namespace, opts.profile);
202
203
  const pin = trimPin(cfg);
203
204
  const budgetMs = opts.flightBudgetMs ?? COMMUNITY_CHAIN_BUDGET_MS;
204
205
  const startedAt = Date.now();
@@ -341,7 +342,7 @@ function flightKey(opts, pin) {
341
342
  const routesSig = opts.routes
342
343
  ? `${opts.routes.registryBase ?? ''}|${(opts.routes.fileBases ?? []).join(',')}`
343
344
  : 'default';
344
- return [ns, pin ?? '', opts.force ? 'force' : 'normal', routesSig].join('§');
345
+ return [ns, opts.profile ?? 'web', pin ?? '', opts.force ? 'force' : 'normal', routesSig].join('§');
345
346
  }
346
347
  function abortError() {
347
348
  const err = new Error('Aborted');
package/lib/core/env.js CHANGED
@@ -8,9 +8,21 @@ export function dshHome() {
8
8
  export function webProfileDir() {
9
9
  return join(dshHome(), 'profiles', WEB_PROFILE);
10
10
  }
11
- /** registry 缓存目录(可被 DSHM_CACHE_DIR 覆盖,便于测试) */
11
+ /**
12
+ * 缓存根(0.9.0 双 profile,ADR-0005):web 恒走旧路径(DSHM_CACHE_DIR 或
13
+ * `$DSH_HOME/dshm/cache`)——不迁移不清空;非 web profile 加同名段
14
+ * (`<root>/<profile>`),实现 profile 专属缓存互不污染。
15
+ * 测试经 DSHM_CACHE_DIR 指到临时目录(红线路径与既有用例一致)。
16
+ */
17
+ export function cacheRoot(profile = WEB_PROFILE) {
18
+ const base = process.env.DSHM_CACHE_DIR || join(dshHome(), 'dshm', 'cache');
19
+ if (profile === WEB_PROFILE)
20
+ return base;
21
+ return join(base, profile);
22
+ }
23
+ /** registry 缓存目录(可被 DSHM_CACHE_DIR 覆盖,便于测试)。web 语义的历史别名 = cacheRoot('web')。 */
12
24
  export function cacheDir() {
13
- return process.env.DSHM_CACHE_DIR || join(dshHome(), 'dshm', 'cache');
25
+ return cacheRoot(WEB_PROFILE);
14
26
  }
15
27
  /** 安装类操作的超时(毫秒) */
16
28
  export function installTimeoutMs() {