dsh-m 0.9.45 → 0.9.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,21 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
8
8
 
9
9
  ## 中文
10
10
 
11
+ ### 0.9.47 变更:README 预览内嵌 HTML 子集渲染(GitHub 风 README 不再满屏标签)
12
+
13
+ - **问题**:市场/收藏/已装详情 Modal 的 README 折叠页,遇到 GitHub 风重度内嵌 HTML 的 README(`<p align="center">` 徽章墙、`<img>` Logo、`<details>` 折叠等——npm packument 兜底与本地读取都常见)会把标签按字面整屏显示,观感如「乱码」。根因是渲染端自研 markdown 渲染器此前完全不解析 HTML 标签(原始内容本身没有问题,GitHub 上渲染正常)。
14
+ - **渲染子集(`src/client/markdown.js`)**:块级包裹标签(`p/div/center/blockquote/details/ul/ol/li/table/thead/tbody/tr/td/th/h1-6/figure/section/dl` 等)剥壳后按完整 markdown 递归渲染,内部标题/列表/表格/围栏结构全保留;行内标签(`a/img/strong/em/del/code/kbd/mark/sub/sup/u/small/span/br/hr` 等)token 化构造元素;`align` 属性映射 `textAlign`,`<img width/height>` 映射显式尺寸并放开徽章 20px 高度帽(无 width 的徽章仍走小图帽);`<details>/<summary>` 原生折叠;文本含标签的普通段落/标题/表格单元格也走行内 token 流(`Press <kbd>Ctrl</kbd>` 类可渲染)。
15
+ - **安全模型(不变式)**:绝不向 DOM 注入原始 HTML 字符串——只解析出白名单标签与白名单属性,全部经 `h()` 构造 React 元素,URL 一律过 `safeUrl` 闸门(`javascript:` 等归 `#`);未知标签剥壳留文本,`script/style/svg/iframe` 等容器连同内容整体丢弃;递归深度护栏(>8 层降级纯文本)。无 `dangerouslySetInnerHTML`,不存在 XSS 注入面。
16
+ - **已知边界**:npm packument 兜底的 README 不携带仓库基址,相对路径图片(如 `docs/assets/logo.svg`)按 safeUrl 归 `#` 后由 onError 隐藏(徽章等外链图片不受影响);`<picture><source>` 的 source 分支丢弃、保 `img` 回退。
17
+ - **测试**:`client-markdown.test.mjs` 净增 10 例(块级剥壳递归、徽章行连排、行内混排、script 丢弃与 javascript: 归化、未知标签剥壳、details/summary、整篇 div 包裹、HTML 表格、HTML 标题与行内元素、未闭合标签降级与深度护栏);真实 dsh-TUI README(25926 字符)渲染验证零标签泄漏、64 个徽章链接正常构造;全量测试 1175/1175,typecheck 零错误。纯客户端变更,刷新页面即生效。
18
+
19
+ ### 0.9.46 变更:双语 README 精简重构+实拍 WebP 截图集(文档版)
20
+
21
+ - **README×2 重构**:中文 17.5KB → 8.3KB、英文 18.1KB → 9.4KB——以用户任务动线重组(30 秒上手 → 浏览/搜索/安装 → 已装与设置 → Agent 工具与 CLI → 兼容与 FAQ);获取线路、缓存语义等实现细节不再展开,统一移交 [`docs/DESIGN.md`](docs/DESIGN.md) 与 ADR 承接;运行相位/生效时机表述与激活分桶语义对齐(不硬承诺「一定重启/一定刷新」,以操作返回为准)。
22
+ - **截图集(`docs/images/`,8 张 WebP ≈248KB)**:市场首页、社区分类条、精选分类条、跨区搜索(精选置顶+社区分组)、插件详情(未装态安装入口)、已装管理、设置页全部换为当前实机实拍;官方安装对话框图沿用。已装图对宿主本机 profile 绝对路径做脱敏改写。
23
+ - **新增 `scripts/capture-readme.mjs`(开发用,不入 npm 包——`assert-pack` 的 scripts/ 禁带清单继续兜底)**:playwright 只读驱动当前 Web 实例复现整套截图(`npm run capture:readme`);不执行安装/升级/开关/删除,仅浏览态与弹窗;每张截图前断言面板内无未脱敏的 profile 绝对路径。新增 devDependency `playwright-core@1.62.1`(锁官方 registry 解析)。
24
+ - **验证**:typecheck 零错误;全量测试 1165/1165(首轮 1 项进程树终止时序断言在并发负载下偶发,单跑与全量重跑均通过,未改运行时代码);`npm pack --dry-run` + `assert-pack` 白名单通过;双语 README 各 8 处 WebP 引用与文件一一对应、无缺失无冗余。
25
+
11
26
  ### 0.9.45 变更:市场页两段加载 + 强制刷新穿透探测 + 精选页动线批
12
27
 
13
28
  - **市场页两段加载([ADR-0013](docs/adr/0013-market-two-phase-probe.md))**:浏览态 market 查询拆两段——第一段 `probeMode:'cache-only'` 只回 TTL 内缓存命中、零网络瞬时回页,有缺口(`latestComplete=false`,豁免社区 github 条目——Q46 永久缺口不构成第二段理由)才自动发起第二段既有探测语义就地补徽标;重启后首开不再被全页重探阻塞(弱网 8–16s → 恒秒开 + 徽标异步补全)。`mergeLatestFields` 纯函数保证会话内切换筛选态时徽标不回退;快照只在终态写入。服务端 latest 缓存保持纯内存不动(ADR-0006 不翻案);tools/CLI 的 `withLatest:false` 通路零变化。
@@ -342,6 +357,21 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
342
357
 
343
358
  ## English
344
359
 
360
+ ### Added in 0.9.47 — embedded-HTML subset rendering for the README preview (GitHub-style READMEs no longer show raw tags)
361
+
362
+ - **Problem**: the README fold in the detail Modal (market/curated/installed) showed GitHub-style HTML-heavy READMEs (`<p align="center">` badge walls, `<img>` logos, `<details>` blocks — common in both the npm packument fallback and local reads) with every tag rendered literally, looking like mojibake. Root cause: the in-house markdown renderer never parsed HTML tags at all. The source content itself was fine — GitHub renders it normally.
363
+ - **Rendering subset (`src/client/markdown.js`)**: block-level wrapper tags (`p/div/center/blockquote/details/ul/ol/li/table/thead/tbody/tr/td/th/h1-6/figure/section/dl` etc.) are unwrapped and their inner content re-rendered as full markdown, preserving headings/lists/tables/fences; inline tags (`a/img/strong/em/del/code/kbd/mark/sub/sup/u/small/span/br/hr` etc.) are tokenized into React elements; `align` maps to `textAlign`; `<img width/height>` maps to explicit size and lifts the 20px badge height cap (badges without width keep the small-image cap); `<details>/<summary>` render as native disclosure; ordinary paragraphs/headings/table cells whose text contains tags also go through the inline token stream (`Press <kbd>Ctrl</kbd>` renders properly).
364
+ - **Security model (invariant)**: raw HTML is never injected into the DOM — only whitelisted tags with whitelisted attributes are parsed, everything is built via `h()` React elements, and URLs pass the `safeUrl` gate (`javascript:` etc. collapse to `#`); unknown tags are unwrapped keeping their text, `script/style/svg/iframe` containers are dropped together with their content, and a recursion depth guard (>8) degrades to plain text. No `dangerouslySetInnerHTML` — no XSS surface.
365
+ - **Known limits**: the npm packument fallback carries no repository base URL, so relative-path images (e.g. `docs/assets/logo.svg`) collapse to `#` via safeUrl and hide on error (externally hosted badge images are unaffected); inside `<picture><source>` the source branch is dropped and the `img` fallback kept.
366
+ - **Testing**: 10 new cases in `client-markdown.test.mjs` (block unwrap recursion, badge rows, inline mix, script dropping + `javascript:` neutralization, unknown-tag unwrap, details/summary, whole-document div wrapper, HTML tables, HTML headings + inline elements, unclosed-tag degradation + depth guard); the real dsh-TUI README (25,926 chars) renders with zero tag leaks and 64 badge links constructed; full suite 1175/1175, typecheck clean. Client-only change — a page refresh takes effect.
367
+
368
+ ### Added in 0.9.46 — bilingual README restructure + live-captured WebP screenshot set (docs release)
369
+
370
+ - **README×2 restructured**: zh-CN 17.5KB → 8.3KB, en 18.1KB → 9.4KB — reorganized around the user's task flow (quick start → browse/search/install → installed & settings → agent tools & CLI → compatibility & FAQ). Fetch-chain and cache-semantics details no longer live here; they are delegated to [`docs/DESIGN.md`](docs/DESIGN.md) and the ADRs. Runtime-phase / takes-effect wording now matches the activation-bucket semantics (no hard promises of "always restart" or "always refresh" — follow the operation result).
371
+ - **Screenshot set (`docs/images/`, 8 WebP ≈248KB)**: marketplace home, community category strip, curated category strip, cross-catalog search (curated hits first, community grouped), plugin detail (not-installed state with install action), installed management, and settings are all re-captured from the current live Web instance; the official install-dialog shot is retained. The installed shot redacts the host's absolute profile path.
372
+ - **New `scripts/capture-readme.mjs` (development-only, not shipped in the npm package — `assert-pack`'s scripts/ deny-list keeps enforcing that)**: drives the running Web instance read-only via playwright to reproduce the full set (`npm run capture:readme`); performs no install/upgrade/toggle/uninstall — browsing and modals only; every shot first asserts no unredacted absolute profile path inside the panel. New devDependency `playwright-core@1.62.1` (pinned to the official registry resolution).
373
+ - **Verification**: typecheck clean; full suite 1165/1165 (one process-tree termination timing assertion flaked once under concurrent load; passed both in isolation and on a full rerun, with no runtime code changes); `npm pack --dry-run` + `assert-pack` allowlist green; each README's 8 WebP references match files one-to-one — nothing missing, nothing unreferenced.
374
+
345
375
  ### Added in 0.9.45 — market two-phase loading + force probe passthrough + curated-zone motion batch
346
376
 
347
377
  - **Market two-phase loading ([ADR-0013](docs/adr/0013-market-two-phase-probe.md))**: browsing market queries split into two phases — phase 1 sends `probeMode:'cache-only'` (TTL-fresh cache hits only, zero network, instant page), and only when gaps exist (`latestComplete=false`, community github entries exempt — a Q46 permanent gap is never a phase-2 reason) does phase 2 run the existing probe semantics to patch badges in place. First open after a DSH restart is no longer blocked by a full-page re-probe (8–16s on weak networks → always instant page + async badge completion). `mergeLatestFields` keeps badges from regressing during in-session filter switches; snapshots are written at terminal states only. The server-side latest cache stays memory-only (ADR-0006 untouched); the tools/CLI `withLatest:false` path is unchanged.
package/README.en.md CHANGED
@@ -1,194 +1,96 @@
1
1
  # dsh-m — Plugin Marketplace for DeepSeek Harness
2
2
 
3
- [![Release](https://img.shields.io/github/v/release/iasiv5/dsh-m?label=Release&sort=semver)](../../releases)
3
+ [![Release](https://img.shields.io/github/v/release/iasiv5/dsh-m?label=Release&sort=semver)](https://github.com/iasiv5/dsh-m/releases)
4
4
  [![npm](https://img.shields.io/npm/v/dsh-m?label=npm)](https://www.npmjs.com/package/dsh-m)
5
- [![Registry Check](https://img.shields.io/github/actions/workflow/status/iasiv5/dsh-m/registry.yml?branch=main&label=Registry%20Check)](../../actions/workflows/registry.yml)
5
+ [![Registry Check](https://img.shields.io/github/actions/workflow/status/iasiv5/dsh-m/registry.yml?branch=main&label=Registry%20Check)](https://github.com/iasiv5/dsh-m/actions/workflows/registry.yml)
6
6
  [![License](https://img.shields.io/github/license/iasiv5/dsh-m?label=License)](./LICENSE)
7
- [![DSH Web](https://img.shields.io/badge/DSH%20Web-0.1.2--rc.1%20%E2%86%92%200.2.0--rc.2-2563eb)](#faq)
7
+ [![DSH Web](https://img.shields.io/badge/DSH%20Web-0.1.2--rc.1%20%E2%86%92%200.2.0--rc.2-2563eb)](#compatibility--faq)
8
8
 
9
9
  English · [中文](./README.md)
10
10
 
11
- dsh-m is a plugin marketplace for DeepSeek Harness (DSH): **browse · install · uninstall · upgrade**, entirely on your machine — no server, no account. The "Plugin Marketplace" panel opens from the sidebar, with agent-tool and CLI surfaces sharing the same core.
11
+ dsh-m brings **plugin discovery, installation, management, and updates** into one DeepSeek Harness (DSH) panel. Search the community directory and hand-curated picks together. The sidebar panel, eight Agent tools, and the `dshm` CLI share marketplace data. No additional marketplace server or account is needed; the local DSH host performs plugin operations.
12
12
 
13
- **Highlights**
13
+ ![Plugin Marketplace: curated zone, categories, and plugin cards](https://raw.githubusercontent.com/iasiv5/dsh-m/main/docs/images/marketplace.webp)
14
14
 
15
- - **Dual profile** — the same package installs into Web (`web`) and official Desktop (`desktop`); everything manages the host's current profile;
16
- - **Dual-catalog market** — the 4,000+ entry community catalog (awesome-dsh-plugin) is the default, layered with a hand-curated registry; zoned browsing with click-to-install;
17
- - **Three consistent surfaces** — the GUI panel, eight `dshm_*` agent tools, and the `dshm` CLI share one data model and semantics;
18
- - **Security baseline** — npm exact versions with integrity checks, GitHub installs pinned to commit SHA, official pluginManager delegation, and key-by-key build approvals.
15
+ ## Quick start
19
16
 
20
- <div align="center">
21
- <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%">
22
- <p><sub>The "Plugin Marketplace" panel · browse Community, Curated, and Favorites with compatibility and install status</sub></p>
23
- </div>
17
+ In DSH's official **Plugin Manager → Add plugin**, enter `dsh-m`, select an installation source reachable from your network, and install. **Restart DSH Web and refresh the page; on Desktop, quit and reopen the app.** Open “Plugin Marketplace” at the bottom of the sidebar.
24
18
 
25
- ## Table of contents
19
+ ![Installing dsh-m with DSH's official plugin manager](https://raw.githubusercontent.com/iasiv5/dsh-m/main/docs/images/official-plugin-install.webp)
26
20
 
27
- - [Requirements](#requirements)
28
- - [Quick start](#quick-start)
29
- - [Panel](#panel)
30
- - [Agent tools (8)](#agent-tools-8)
31
- - [CLI](#cli)
32
- - [Community catalog (awesome-dsh-plugin)](#community-catalog-awesome-dsh-plugin)
33
- - [Curated registry (registry.json)](#curated-registry-registryjson)
34
- - [Documentation](#documentation)
35
- - [FAQ](#faq)
21
+ On a headless Web host, you can also run `dsh plugin --profile web add dsh-m`. Requires Node.js ≥ 22; see the [compatibility table](#compatibility--faq). `npm install -g dsh-m` installs **only the standalone `dshm` CLI**, not the marketplace plugin in a DSH profile.
36
22
 
37
- Version history is not expanded on this page — see [`CHANGELOG.md`](https://github.com/iasiv5/dsh-m/blob/main/CHANGELOG.md) for the complete record.
23
+ > In-place update support in the official plugin manager depends on the host version. If it explicitly says updates are unsupported, follow its guidance to uninstall the old version and install the new one. Uninstall does not silently erase plugin data.
38
24
 
39
- ## Requirements
25
+ ## Browse, search, install
40
26
 
41
- - Node.js ≥ 22 on the host side;
42
- - DSH Web or official Desktop (supported host generations: see the [FAQ matrix](#faq));
43
- - No extra services or accounts — installing and managing plugins happens entirely locally.
27
+ - **Community + Curated**: the community directory is on by default, with 4,000+ listings. Curated picks are organized into Essentials / Cui's Picks / iasi In-house / Tencent Lighthouse / Watchlist. Curated entries win when both catalogs list the same plugin; the community zone doesn't duplicate them.
28
+ - **Zoned browsing, global search**: each zone remembers its own category, sort, and pagination. A Chinese or English query searches both catalogs and shows their separate hit counts. Favorites provide a shortcut within the local profile.
29
+ - **Inspect before installing**: cards show origin, installation status, and version; details show the description, compatibility information, and installation entry point. npm installs pin an **exact version with integrity verification**; GitHub installs pin a **commit SHA**. Peer-incompatible installs require explicit risk acknowledgment.
44
30
 
45
- ## Quick start
31
+ | Community categories | Curated buckets |
32
+ |---|---|
33
+ | ![Community directory and open category filters](https://raw.githubusercontent.com/iasiv5/dsh-m/main/docs/images/community-catalog.webp) | ![The five curated marketplace buckets](https://raw.githubusercontent.com/iasiv5/dsh-m/main/docs/images/curated-registry.webp) |
46
34
 
47
- 1. In DSH, open the official plugin manager and choose "Add plugin".
48
- 2. Enter `dsh-m`, select an install source that works for your network, then click "Install".
49
- 3. Follow the host lifecycle to activate it: restart DSH Web and refresh the page; on official Desktop, quit and reopen the app.
50
- 4. Once loaded, open "Plugin Marketplace" from the bottom of the sidebar.
35
+ ![Cross-catalog search for skins: curated matches first, community matches grouped below](https://raw.githubusercontent.com/iasiv5/dsh-m/main/docs/images/search-results.webp)
51
36
 
52
- <div align="center">
53
- <img src="https://raw.githubusercontent.com/iasiv5/dsh-m/main/docs/images/official-plugin-install.webp" alt="DSH official plugin manager: the Add Plugin dialog with dsh-m entered and an install source selected" width="86%">
54
- <p><sub>Recommended · add and install dsh-m from DSH's official plugin manager</sub></p>
55
- </div>
37
+ ![Plugin details: category, version, origin, install command, and install action](https://raw.githubusercontent.com/iasiv5/dsh-m/main/docs/images/plugin-detail.webp)
56
38
 
57
- > **Upgrade note:** The official plugin manager currently says in-place updates are not supported. To upgrade through it, uninstall `dsh-m` first, then install the newer version; follow the latest UI guidance if this changes.
39
+ > **Community listing is not a security review.** With 4,000+ entries from varied sources, inspect the author, code, and requested capabilities before installing. Missing capability data means “not scanned,” **not** “no risks found.”
58
40
 
59
- ## Panel
41
+ ## Installed plugins & settings
60
42
 
61
- | View | Capabilities |
62
- |---|---|
63
- | **Market** | Community (4,000+ entries) / Curated / Favorites zones; browsing state (category, sort, pagination) is kept per zone, while search is global across zones (community + curated hits together); cards open a detail modal with install progress and results shown in place. |
64
- | **Operations** | Installs, upgrades, uninstalls, and toggles flow through one global operation log — state lives off cards and resumes automatically after host reloads. |
65
- | **Installed** | What the current profile actually has: source annotation, live phase badge, one-click toggle, upgrade, and two-step confirmed uninstall. |
66
- | **Settings** | Curated registry configuration and status, community catalog toggle, dsh-m self-update. |
43
+ **Installed** reflects the current host profile's actual packages: marketplace/non-marketplace origin, runtime phase, available updates, toggles, upgrades, and confirmed uninstall. Operation progress and results are tracked together. A toggle may apply live or require a restart; follow the returned result. Uninstall removes the package reference without silently deleting user data and reports possible leftover paths separately.
67
44
 
68
- <div align="center">
69
- <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%">
70
- <p><sub>Settings view · community catalog status, curated registry configuration, and dsh-m self-update</sub></p>
71
- </div>
45
+ ![Installed plugins: version, origin, runtime state, toggles; local profile path redacted](https://raw.githubusercontent.com/iasiv5/dsh-m/main/docs/images/installed.webp)
72
46
 
73
- How changes take effect (Web only):
47
+ **Settings** shows the community catalog's version, fetch route, and cache state, and lets you disable or pin its version. You can download the default curated registry, provide a custom address, and **validate before replacing the Curated zone as a whole** (the Community zone is unaffected). Failed validation preserves the active configuration; successful apply and restore-default are live, without a restart.
74
48
 
75
- - After install / uninstall / upgrade, the Market and Installed views sync automatically — no need to close and reopen the panel;
76
- - A "⚡ Restart" banner follows: under systemd, the DSH launcher's `appExit` hands the restart to the unit's `Restart` policy; hosts without `appExit` fall back by design;
77
- - The client confirms the new process by boot id, dismisses the banner, and lets DSH Web's background reconnection restore the page — no forced full reload;
78
- - Installs stream live pnpm progress (resolve → download → link → build).
49
+ ![Settings: community-catalog status and curated-registry configuration](https://raw.githubusercontent.com/iasiv5/dsh-m/main/docs/images/settings.webp)
79
50
 
80
- On Desktop, changes take effect via the official app lifecycle: quit and reopen the app after installing. Restart-chain mechanics and per-generation verification records live in [`docs/DESIGN.md`](https://github.com/iasiv5/dsh-m/blob/main/docs/DESIGN.md).
51
+ A custom registry may come from HTTPS, a local absolute path, or `file://` (HTTP is for loopback debugging only); limits are 2 MiB and 1,000 entries. A custom source is an independent snapshot and **does not sync automatically** with the default curated registry. If it fails, its own last good cache remains in use; dsh-m never silently switches back to the default. See the [registry copy guide](https://github.com/iasiv5/dsh-m/blob/main/docs/registry-copy-guide.md) and [design document](https://github.com/iasiv5/dsh-m/blob/main/docs/DESIGN.md) for the full rules.
81
52
 
82
- ## Agent tools (8)
53
+ ## Agent tools & CLI
83
54
 
84
- | Tool | Purpose |
55
+ | Agent tools | Purpose |
85
56
  |---|---|
86
- | `dshm_search` | Search curated & community listings (renders cards in chat) |
87
- | `dshm_list` | List installed plugins (market / non-market annotated) |
88
- | `dshm_install` | Install by listing id (peer compatibility precheck before install; incompatible results return structured data for user confirmation, then `force` retries) |
89
- | `dshm_uninstall` | Uninstall (confirm first; data kept, leftovers reported) |
90
- | `dshm_outdated` | Check for newer versions |
91
- | `dshm_upgrade` | Upgrade to the latest |
92
- | `dshm_toggle` | Toggle a plugin on/off (confirm by default, execute directly when the user already asked in the same message) |
93
- | `dshm_restart` | Restart DSH Web (with user consent) |
94
-
95
- ## CLI
57
+ | `dshm_search` | Search community and curated catalogs; render cards in the conversation |
58
+ | `dshm_list` · `dshm_outdated` | List installed and outdated plugins in the current profile |
59
+ | `dshm_install` · `dshm_upgrade` · `dshm_uninstall` | Install, upgrade, and uninstall after confirmation; surface compatibility risks first |
60
+ | `dshm_toggle` · `dshm_restart` | Toggle runtime state; restart Web with the user's consent |
96
61
 
97
62
  ```sh
98
- dshm search [--query topic] [--category ui] [--source community|primary|all] [--limit N] [--offset N]
99
- dshm list | outdated | registry
100
- dshm install --id dsh-web-search [--force] # --force: skip the precheck gate after confirming the risk
101
- dshm upgrade --pkg dsh-web-search --yes [--force]
63
+ dshm search --query theme --source all
64
+ dshm list
65
+ dshm outdated
66
+ dshm install --id dsh-web-search
67
+ dshm upgrade --pkg dsh-web-search --yes
102
68
  dshm uninstall --pkg dsh-web-search --yes
103
- dshm toggle --pkg dsh-web-search --on|--off --yes # file-level edit; applies on restart
69
+ dshm toggle --pkg dsh-web-search --off --yes
104
70
  dshm restart --yes
105
71
  ```
106
72
 
107
- 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.
108
-
109
- **Profile target**: 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 app). On headless hosts you can still install the plugin via the official `dsh` CLI: `dsh plugin --profile web add dsh-m`.
110
-
111
- > `npm install -g dsh-m` installs only the `dshm` CLI (terminal management) — it does **not** register the plugin into the web profile; it can coexist with the official-manager install.
112
-
113
- ## Community catalog (awesome-dsh-plugin)
114
-
115
- **The community catalog is dsh-m's default catalog**: anchored to the npm package [`dsh-plugin-catalog`](https://www.npmjs.com/package/dsh-plugin-catalog) (CC0-1.0, the full awesome-dsh-plugin directory, 4,000+ entries). On top of it sits the hand-curated **curated registry** (registry.json); the data layer merges and dedupes (the curated registry **always takes precedence**, duplicate community entries displaced), and the display layer splits into Community / Curated zones (ADR-0004) — the community zone no longer shows entries duplicated by the curated registry, and the curated zone stays a single curated-order page.
116
-
117
- <div align="center">
118
- <img src="https://raw.githubusercontent.com/iasiv5/dsh-m/main/docs/images/community-catalog.webp" alt="Market view, Community zone — Community / Curated / Favorites tabs, the search row, and counted open category chips" width="86%">
119
- <p><sub>Community catalog · the default landing zone: zone tabs, persistent search, and counted open category chips</sub></p>
120
- </div>
121
-
122
- - **Fetch chain**: dist-tags probe → jsDelivr pinned fetch → npmmirror → unpkg fallback; unchanged versions are not re-fetched, probes are skipped within TTL. Latest-version probes use a memory-only cache: it is dropped on every DSH restart (publish a new version, restart, and the new version shows up), and a plugin's cached entry is invalidated immediately after a successful install/upgrade/uninstall via dsh-m. On failure it falls back to the runtime cache under `<cache dir>/awesome/` and **explicitly labels it "cached snapshot"** — stale data never masquerades as fresh.
123
- - **Switch & pin**: `communityCatalog` toggle in Settings (default on, live), `communityCatalogPin` to lock the catalog version (exact semver); CLI opt-out with `DSHM_COMMUNITY_CATALOG=0`, pin with `DSHM_COMMUNITY_CATALOG_PIN`.
124
- - **Categories & search**: community categories are an open set (23 known categories with bilingual labels — Chinese maintained in-repo as the single source of truth, English from the upstream catalog, served per UI language); search runs a relevance-weighted pipeline (Chinese + English) and is **global across zones** — either zone's search box hits community and curated listings together (browsing stays zoned; a summary line reports exact per-zone hit counts with curated hits grouped on top); the community zone offers a sort switch (downloads/stars/added × asc/desc, default downloads-desc; missing downloads ≠ zero downloads).
125
- - **Install semantics**: community entries go through the same install path as curated entries (npm exact version / GitHub pinned commit SHA); the browse page probes npm entries only — GitHub entries are not probed at page level (anonymous 60 req/h quota is uncontrollable).
126
- - **Installed-page budget (best-effort)**: update checks for GitHub-sourced plugins are capped at ≤25 wire requests per run and ≤50 per rolling hour per host process (passive checks only; active install/upgrade unaffected); capped items are honestly labeled "check incomplete" instead of "all up to date". A standalone CLI process does not share the host budget and makes no guarantee under concurrency.
127
- - **Capability disclosure**: capabilities/red lines appear only in the detail fold (**absent = not scanned ≠ not detected**), never as card badges; screenshots load only in the detail layer and pass a client-side allowlist.
128
- - **Not a security review**: the community catalog is all-inclusive and **not security-reviewed** — verify plugin origin and capabilities before installing; the curated registry's curation and CI checks do not apply to community entries.
129
-
130
- ## Curated registry (registry.json)
131
-
132
- The curated registry is hand-curated, layered on top of the community catalog (duplicates always defer to it), and organized into **five curated buckets**: Essentials / Cui's Picks / iasi In-house / Tencent Lighthouse / Watchlist. It currently includes DSH Skins, DSH TUI, ModSearch, Better Sidebar, DSH Context, and more.
73
+ The GUI and Agent tools manage the **current host profile** (Web or Desktop). The standalone CLI, by contrast, **always targets the web profile** and rejects `--profile desktop`. `--force` may be used with install/upgrade after the user accepts a peer-compatibility risk. If the catalog is unavailable, `registry`, `search`, and `outdated` fail explicitly while `list` still lists installed packages. See the [design document](https://github.com/iasiv5/dsh-m/blob/main/docs/DESIGN.md) for full parameters and runtime policy.
133
74
 
134
- <div align="center">
135
- <img src="https://raw.githubusercontent.com/iasiv5/dsh-m/main/docs/images/curated-registry.webp" alt="Market view, Curated zone — Community / Curated / Favorites tabs and the five curated-bucket chips (Essentials / Cui's Picks / iasi In-house / Tencent Lighthouse / Watchlist)" width="86%">
136
- <p><sub>Curated registry · the five curated buckets: Essentials / Cui's Picks / iasi In-house / Tencent Lighthouse / Watchlist</sub></p>
137
- </div>
75
+ ## Compatibility & FAQ
138
76
 
139
- The registry is fetched at runtime over two lines — **GitHub raw (`@main`) → GitHub mirror (jsDelivr CDN, backup)** — ordered adaptively by the last successful route, falling back to the local 60-min TTL cache and the bundled snapshot. Listing updates stay decoupled from plugin releases: after a push, allow up to one cache period (or force a refresh in Settings). 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.
140
-
141
- **Custom curated registry (replaces the default)**: the Settings tab supports a single custom registry address that **replaces** the default curated registry (no merging) and **only affects the Curated zone** — the community catalog is untouched:
142
-
143
- 1. "Download default registry" gives you a copy of the official listing;
144
- 2. edit the copy yourself (add/remove entries);
145
- 3. paste its address in Settings and hit "Validate & apply" — an **HTTPS URL**, a local **absolute path / `file://`** (HTTP only for 127.0.0.1/localhost debugging);
146
- 4. validation failures (bad fields, missing path, over 2 MiB / 1,000 entries, …) are never saved — the currently active registry stays; a successful apply takes effect **immediately, no restart** (only the first deploy of a new dsh-m version needs one restart);
147
- 5. "Restore default" switches back to the official registry in one click.
148
-
149
- Rules and limits: strict v1 schema (unknown fields / invalid ids / oversized values / duplicates reject the whole file, never truncated); the copy is a standalone snapshot and does **not** auto-sync with the official listing; a failing custom source keeps its own last good cache and never silently falls back to the official registry; old custom-source caches are cleaned after switching (the default cache is kept); custom registries are not validated by official CI — install only from sources you trust; full local paths appear only on the Settings tab, tools and cards show short statuses.
150
-
151
- Security baseline: HTTPS-only fetches (loopback HTTP excepted) with per-hop redirect checks, size caps and timeouts; npm installs verify the exact version's dist integrity against the pnpm lockfile — mismatches fail closed and roll back; GitHub installs pinned to commit SHA; pnpm build scripts are allowed-by-policy with an explicit report when unblocked.
152
-
153
- ## Documentation
154
-
155
- | Doc | Read it when |
156
- |---|---|
157
- | [CHANGELOG.md](https://github.com/iasiv5/dsh-m/blob/main/CHANGELOG.md) | You want the details of any release (bilingual, complete back to 0.4.x) |
158
- | [docs/DESIGN.md](https://github.com/iasiv5/dsh-m/blob/main/docs/DESIGN.md) | Design consensus, panel behavior details, dual-generation compatibility, and the restart chain; development & release workflow in its §6 |
159
- | [docs/adr/](https://github.com/iasiv5/dsh-m/tree/main/docs/adr) | Architecture decisions: delegation & fallback, precise build approval, community catalog merge, zoned market, dual profile |
160
- | [docs/registry-copy-guide.md](https://github.com/iasiv5/dsh-m/blob/main/docs/registry-copy-guide.md) | Writing descriptions and tags for curated registry entries |
161
- | [docs/plans/](https://github.com/iasiv5/dsh-m/tree/main/docs/plans) | Implementation plan archive, one per release |
162
-
163
- ## FAQ
164
-
165
- **1. Why don't GitHub-sourced update hints follow main?**
166
- 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.
167
-
168
- **1.5 What works on the official Desktop?**
169
- Desktop (the `desktop` profile) supports browsing the market, **installing new packages**, and plugin toggles. Since 0.9.8, upgrades, uninstalls, and dsh-m self-updates are also delegated to the official `pluginManager`; an unavailable service returns a structured refusal with guidance. One-click restart remains part of the official Desktop app lifecycle, so quit and reopen the app. Installed badges and the installed list reflect only the Desktop profile; favorites and operation logs do not sync with Web.
170
-
171
- **2. Does uninstalling dsh-m delete my data?**
172
- No. Only the package reference in the profile is removed (live UI disabled first), and suspected leftover paths are reported to you.
77
+ | DSH Web generation | Settings path | Verification |
78
+ |---|---|---|
79
+ | `0.1.2-rc.1` | cordis config-file fallback | Contract checked |
80
+ | `0.1.5-rc.1 / rc.2` | `register()` scope | Checked; rc.1 also page-tested |
81
+ | `0.1.7-rc.1 / rc.2` | `.volatile()` + `settings.update` | Checked |
82
+ | `0.2.0-rc.1 / rc.2` | Same as 0.1.7 | rc.2 and restart path live-tested |
173
83
 
174
- **3. Will a custom registry slow the market down?**
175
- The market list is server-paginated (32/64/96 per page); even a 1,000-entry registry queries latest versions for the current page only, so browsing stays fast.
84
+ **Is Desktop identical to Web?** No. Desktop browsing, new-package installation, and toggles act on the desktop profile. Upgrades and uninstalls require capabilities from the official host `pluginManager`; if absent, dsh-m returns a structured refusal and guidance. Desktop uses the official app lifecycle rather than Web's one-click restart; quit and reopen as instructed. Favorites, operations, and installed states are separate between profiles.
176
85
 
177
- **4. What if my custom source goes down?**
178
- dsh-m serves its last successful cache for that source (the Settings tab's effective-source row labels it honestly); with no cache at all the market reports the listing unavailable while installed plugins stay manageable. Fix the address or restore the default anytime.
86
+ **When do installs or updates take effect?** On Web, follow the activation result: a client-only update needs a refresh; operations marked restart-required need consent before the host restarts, followed by background reconnection. systemd deployments need `Restart=on-failure` or `Restart=always`; otherwise use your deployment's restart method.
179
87
 
180
- **5. Which DSH Web versions are supported?**
88
+ **Why don't GitHub updates follow `main`?** Intermediate commits may be unstable. dsh-m tracks releases/tags and pins their commit SHAs. Because anonymous GitHub API quota is limited, an incomplete passive check is labeled as such rather than “all up to date.”
181
89
 
182
- | DSH version | Settings path | Verification |
183
- |---|---|---|
184
- | `0.1.2-rc.1` | shape-detection fallback to the cordis config-file path | contract-checked |
185
- | `0.1.5-rc.1` / `rc.2` | `register()` scope path | checked; rc.1 additionally live-verified `/dshm` ping and authenticated `303 → 200` |
186
- | `0.1.7-rc.1` / `rc.2` | Config `.volatile()` fields + `settings.update` + the `loader/volatile-update` event | checked; the two rcs ship byte-identical dsh-settings, so one implementation covers both |
187
- | `0.2.0-rc.1` / `rc.2` | same as `0.1.7` | `rc.2` is the current live runtime; the restart chain was live-verified on 2026-10-01 |
90
+ **What if my custom registry goes down?** Its last successful cache is used first. Without any cache, the registry is reported unavailable while installed plugins remain manageable. Fix the address or restore the default in Settings.
188
91
 
189
- - The path is chosen by runtime **shape detection**, not version branching; unrecognized shapes degrade to the cordis config-file path with marketplace features unaffected. Implementation details: [`docs/DESIGN.md`](https://github.com/iasiv5/dsh-m/blob/main/docs/DESIGN.md) §12.
190
- - Systemd deployments must configure `Restart=on-failure` or `Restart=always`; otherwise use the deployment's manual restart procedure.
92
+ Full history: [CHANGELOG](https://github.com/iasiv5/dsh-m/blob/main/CHANGELOG.md). Architecture and development details: [DESIGN](https://github.com/iasiv5/dsh-m/blob/main/docs/DESIGN.md), [ADRs](https://github.com/iasiv5/dsh-m/tree/main/docs/adr). Screenshots were taken from a Web instance; panel version, catalog counts, and installation states vary over time and by profile. To reproduce from the source checkout, run `npm install && npm run capture:readme` with dsh-m installed on the Web host. The development-only capture script is not distributed in the npm plugin package.
191
93
 
192
94
  ## License
193
95
 
194
- MIT
96
+ [MIT](./LICENSE)
package/README.md CHANGED
@@ -1,195 +1,96 @@
1
1
  # dsh-m — DeepSeek Harness 插件市场
2
2
 
3
- [![Release](https://img.shields.io/github/v/release/iasiv5/dsh-m?label=Release&sort=semver)](../../releases)
3
+ [![Release](https://img.shields.io/github/v/release/iasiv5/dsh-m?label=Release&sort=semver)](https://github.com/iasiv5/dsh-m/releases)
4
4
  [![npm](https://img.shields.io/npm/v/dsh-m?label=npm)](https://www.npmjs.com/package/dsh-m)
5
- [![Registry Check](https://img.shields.io/github/actions/workflow/status/iasiv5/dsh-m/registry.yml?branch=main&label=Registry%20Check)](../../actions/workflows/registry.yml)
5
+ [![Registry Check](https://img.shields.io/github/actions/workflow/status/iasiv5/dsh-m/registry.yml?branch=main&label=Registry%20Check)](https://github.com/iasiv5/dsh-m/actions/workflows/registry.yml)
6
6
  [![License](https://img.shields.io/github/license/iasiv5/dsh-m?label=License)](./LICENSE)
7
- [![DSH Web](https://img.shields.io/badge/DSH%20Web-0.1.2--rc.1%20%E2%86%92%200.2.0--rc.2-2563eb)](#faq)
7
+ [![DSH Web](https://img.shields.io/badge/DSH%20Web-0.1.2--rc.1%20%E2%86%92%200.2.0--rc.2-2563eb)](#兼容与常见问题)
8
8
 
9
9
  [English](./README.en.md) · 中文
10
10
 
11
- dsh-m 是 DeepSeek Harness(DSH)的插件市场:**收录 · 安装 · 卸载 · 升级**,全部本机完成——无服务端、无账号。侧栏「插件市场」即开即用,agent 工具与 CLI 同源同语义。
11
+ dsh-m 把 DeepSeek Harness(DSH)的插件**发现、安装、管理与更新**放进同一个界面。社区目录与人工精选同时可搜;侧栏面板、8 个 Agent 工具和 `dshm` CLI 共用一套市场数据。无需额外的市场服务器或账号,插件操作仍由本机 DSH 宿主管理。
12
12
 
13
- **亮点**
13
+ ![侧栏插件市场:精选分区、分类与插件卡片](https://raw.githubusercontent.com/iasiv5/dsh-m/main/docs/images/marketplace.webp)
14
14
 
15
- - **双 profile**:同一包装入 Web(`web`)与官方 Desktop(`desktop`),管理对象始终跟随宿主当前 profile;
16
- - **双清单市场**:4,000+ 条社区目录(awesome-dsh-plugin)为默认清单,叠加手工精选,分区浏览、卡片即点即装;
17
- - **三端同源**:GUI 面板、8 个 `dshm_*` agent 工具与 `dshm` CLI 共享同一套数据与语义;
18
- - **安全基线**:npm 精确版本 integrity 校验、GitHub 锁定 commit SHA、委派官方 pluginManager,构建脚本逐键精确放行。
15
+ ## 30 秒上手
19
16
 
20
- <div align="center">
21
- <img src="https://raw.githubusercontent.com/iasiv5/dsh-m/main/docs/images/marketplace.webp" alt="侧栏「插件市场」面板——市场视图:社区 / 精选 / 收藏三分区、分类 chips 与插件卡片(已实测 / 已安装徽标、npm 与 GitHub 详情链接)" width="100%">
22
- <p><sub>侧栏「插件市场」· 市场视图:社区、精选与收藏分区,浏览插件并查看兼容与安装状态</sub></p>
23
- </div>
17
+ 在 DSH 官方「插件管理 → 添加插件」中输入 `dsh-m`,选择可用的安装源并安装。**Web 重启服务并刷新页面;Desktop 退出并重新打开应用**。随后从侧栏底部进入「插件市场」。
24
18
 
25
- ## 目录
19
+ ![在 DSH 官方插件管理界面安装 dsh-m](https://raw.githubusercontent.com/iasiv5/dsh-m/main/docs/images/official-plugin-install.webp)
26
20
 
27
- - [环境要求](#环境要求)
28
- - [30 秒上手](#30-秒上手)
29
- - [界面(侧栏「插件市场」)](#界面侧栏插件市场)
30
- - [Agent 工具(8 个)](#agent-工具8-个)
31
- - [CLI](#cli)
32
- - [社区清单(awesome-dsh-plugin 目录)](#社区清单awesome-dsh-plugin-目录)
33
- - [精选清单(registry.json)](#精选清单registryjson)
34
- - [文档](#文档)
35
- - [FAQ](#faq)
21
+ 无图形界面的 Web 宿主也可执行 `dsh plugin --profile web add dsh-m`。Node.js 需 ≥ 22;支持的 DSH 代际见[兼容表](#兼容与常见问题)。`npm install -g dsh-m` **仅安装独立 `dshm` CLI**,不会把市场插件注册到 DSH profile。
36
22
 
37
- 本页不展开版本变更,完整历史见 [`CHANGELOG.md`](https://github.com/iasiv5/dsh-m/blob/main/CHANGELOG.md)。
23
+ > 官方插件管理界面的就地更新能力以宿主版本和界面提示为准;如果明确提示不支持,请按提示卸载旧版后重新安装。卸载不会自动清除插件数据。
38
24
 
39
- ## 环境要求
25
+ ## 浏览、搜索与安装
40
26
 
41
- - Node.js ≥ 22(宿主侧);
42
- - DSH Web 或官方 Desktop(支持的宿主代际见 [FAQ 支持矩阵](#faq));
43
- - 无需额外服务或账号,安装与管理全部本机完成。
27
+ - **社区 + 精选**:社区目录默认开启,收录 4,000+ 个条目;精选清单按「装机必备 / 崔添翼精选 / iasi自研 / 腾讯轻量云专区 / 观察区」策展。重名条目以精选为准,社区区不重复展示。
28
+ - **分区浏览、全局搜索**:分类、排序和分页在各区独立保留;输入中文或英文关键词时,同时搜索社区与精选,并分别展示命中数。收藏是本机当前 profile 的便捷入口。
29
+ - **安装前先看详情**:卡片显示来源、安装状态与版本;详情展示说明、兼容信息和可用的安装入口。npm 包锁定**精确版本并校验 integrity**,GitHub 来源锁定 **commit SHA**;peer 不兼容时先提示风险,未经确认不强行安装。
44
30
 
45
- ## 30 秒上手
31
+ | 社区分类 | 精选策展 |
32
+ |---|---|
33
+ | ![社区目录与开放分类筛选](https://raw.githubusercontent.com/iasiv5/dsh-m/main/docs/images/community-catalog.webp) | ![精选清单的五类策展分区](https://raw.githubusercontent.com/iasiv5/dsh-m/main/docs/images/curated-registry.webp) |
46
34
 
47
- 1. 在 DSH 官方插件管理界面点击「添加插件」。
48
- 2. 输入包名 `dsh-m`,按网络情况选择安装源,然后点击「安装」。
49
- 3. 安装完成后按宿主生命周期使插件生效:DSH Web 重启后刷新页面;官方 Desktop 退出并重新打开应用。
50
- 4. 加载完成后,从侧栏底部打开「插件市场」。
35
+ ![跨区搜索「皮肤」:精选命中置顶,社区命中分组呈现](https://raw.githubusercontent.com/iasiv5/dsh-m/main/docs/images/search-results.webp)
51
36
 
52
- <div align="center">
53
- <img src="https://raw.githubusercontent.com/iasiv5/dsh-m/main/docs/images/official-plugin-install.webp" alt="DSH 官方插件管理界面:添加插件对话框中填写 dsh-m,并选择安装源" width="86%">
54
- <p><sub>推荐方式 · 在 DSH 官方插件管理界面添加并安装 dsh-m</sub></p>
55
- </div>
37
+ ![插件详情:分类、版本、来源、安装命令和安装入口](https://raw.githubusercontent.com/iasiv5/dsh-m/main/docs/images/plugin-detail.webp)
56
38
 
57
- > **升级提示:** 当前官方插件管理界面提示暂不支持自动更新;若通过该界面升级,请先卸载 `dsh-m` 再安装新版,具体以界面最新提示为准。
39
+ > **社区收录不是安全审查。** 4,000+ 条目录来源广泛,安装前请核对作者、代码与所需能力;能力字段缺失表示「未扫描」,不表示「未发现风险」。
58
40
 
59
- ## 界面(侧栏「插件市场」)
41
+ ## 已装插件与设置
60
42
 
61
- | 视图 | 能力 |
62
- |---|---|
63
- | **市场** | 社区(4,000+ 条)/ 精选 / 收藏三分区;浏览态各分区独立保留分类、排序与分页状态,搜索为跨区全局(社区 + 精选一并命中);卡片点开详情 Modal,安装进度与结果就地显示;已装且可升级的条目可直接在 Modal 内一键升级并展开 README 预览。 |
64
- | **操作记录** | 安装、升级、卸载、开关统一进入全局操作日志,状态不挂卡片;宿主重载后自动恢复未完成操作。 |
65
- | **已装** | 当前 profile 实装列表:来源标注、运行相位徽标、一键开关、升级与两段式确认卸载。 |
66
- | **设置** | 精选清单配置与状态、社区目录开关、dsh-m 自更新。 |
43
+ 「**已装**」只反映当前宿主 profile 的实际插件:区分市场/非市场来源,显示运行相位与可用更新,并提供开关、升级和确认后卸载;相关操作进度与结果会统一记录。开关可能即时生效或需要重启,应以操作结果为准。卸载只移除插件引用,不会默默删除用户数据,疑似残留路径会单独报告。
67
44
 
68
- <div align="center">
69
- <img src="https://raw.githubusercontent.com/iasiv5/dsh-m/main/docs/images/settings.webp" alt="设置视图——社区清单状态卡(目录版本 / 获取线路 / 收录条目)、精选清单配置卡(地址草稿与强制刷新 / 校验并应用 / 恢复默认 / 下载默认清单)与关于卡" width="86%">
70
- <p><sub>设置视图 · 社区清单状态、精选清单配置与 dsh-m 自更新</sub></p>
71
- </div>
45
+ ![已装页:版本、来源、运行状态与开关;截图已隐藏本机 profile 路径](https://raw.githubusercontent.com/iasiv5/dsh-m/main/docs/images/installed.webp)
72
46
 
73
- 变更生效与重启(仅 Web):
47
+ 「**设置**」可查看社区目录的版本、获取线路和缓存状态,开关或锁定社区目录版本;也可下载默认精选清单、填入自定义地址并**校验后整体替换精选区**(社区区不变)。校验失败不改当前生效配置;恢复默认或成功应用均无需重启。
74
48
 
75
- - 安装 / 卸载 / 升级完成后,市场页与已装页自动同步最新状态,无需关闭重开面板;
76
- - 随后出现「⚡ 一键重启」横幅:受 systemd 管理时经 DSH launcher 的 `appExit` 交还服务的 `Restart` 策略拉起,无 `appExit` 的宿主按设计兜底;
77
- - 客户端按 boot id 确认新进程后关闭横幅,页面经 DSH Web 自身的后台重连恢复,不强制整页刷新;
78
- - 安装过程实时显示 pnpm 进度(解析 → 下载 → 链接 → 构建)。
49
+ ![设置页:社区目录状态与精选清单配置](https://raw.githubusercontent.com/iasiv5/dsh-m/main/docs/images/settings.webp)
79
50
 
80
- Desktop 的变更经官方应用生命周期生效——安装完成后退出并重新打开应用即可。重启链路的机制细节与实机核验记录见 [`docs/DESIGN.md`](https://github.com/iasiv5/dsh-m/blob/main/docs/DESIGN.md)。
51
+ 自定义地址支持 HTTPS、本机绝对路径或 `file://`(HTTP 仅限 loopback 调试),清单大小 ≤ 2 MiB、条目 ≤ 1,000。自定义源是独立快照,**不会自动同步默认精选**;源失效时保留该源最近的成功缓存,不会暗中切回默认。添加精选条目或了解完整校验规则,请看 [清单文案指南](https://github.com/iasiv5/dsh-m/blob/main/docs/registry-copy-guide.md)与[设计文档](https://github.com/iasiv5/dsh-m/blob/main/docs/DESIGN.md)。
81
52
 
82
- ## Agent 工具(8 个)
53
+ ## Agent 工具与 CLI
83
54
 
84
- | 工具 | 用途 |
55
+ | Agent 工具 | 能做什么 |
85
56
  |---|---|
86
- | `dshm_search` | 搜精选与社区清单(对话内出卡片) |
87
- | `dshm_list` | 列已装插件(市场/非市场标注) |
88
- | `dshm_install` | 按清单 id 安装(装前 peer 兼容预检;不兼容时回结构化结果,用户确认后 `force` 重试) |
89
- | `dshm_uninstall` | 卸载(先确认;不删数据,报告残留路径) |
90
- | `dshm_outdated` | 检查最新版本 |
91
- | `dshm_upgrade` | 升级到最新 |
92
- | `dshm_toggle` | 切换插件运行状态(默认先确认,用户同句已明确可直接执行) |
93
- | `dshm_restart` | 一键重启 DSH Web(先征得同意) |
94
-
95
- ## CLI
57
+ | `dshm_search` | 同时搜索社区与精选,在对话内展示插件卡片 |
58
+ | `dshm_list` · `dshm_outdated` | 列当前 profile 实装与可更新插件 |
59
+ | `dshm_install` · `dshm_upgrade` · `dshm_uninstall` | 安装、升级、确认后卸载;兼容风险先行提示 |
60
+ | `dshm_toggle` · `dshm_restart` | 切换运行状态;经用户同意后重启 Web |
96
61
 
97
62
  ```sh
98
- dshm search [--query 主题] [--category ui] [--source community|primary|all] [--limit N] [--offset N]
99
- dshm list | outdated | registry
100
- dshm install --id dsh-web-search [--force] # --force:确认兼容风险后跳过预检拦截
101
- dshm upgrade --pkg dsh-web-search --yes [--force]
63
+ dshm search --query 主题 --source all
64
+ dshm list
65
+ dshm outdated
66
+ dshm install --id dsh-web-search
67
+ dshm upgrade --pkg dsh-web-search --yes
102
68
  dshm uninstall --pkg dsh-web-search --yes
103
- dshm toggle --pkg dsh-web-search --on|--off --yes # 文件级编辑,重启生效
69
+ dshm toggle --pkg dsh-web-search --off --yes
104
70
  dshm restart --yes
105
71
  ```
106
72
 
107
- 清单不可用时 `registry` / `search` / `outdated` 打印配置与实际生效地址并退出码 1;`list` 仍列出已装插件。CLI 固定独立缓存命名空间,不影响 Web 端。
108
-
109
- **profile 目标**:CLI 恒作用于 web profile——`--profile web` 为显式声明;`--profile desktop` 直接拒绝(Desktop 的插件管理走官方 Desktop 应用)。无图形界面的宿主也可用官方 `dsh` CLI 安装插件:`dsh plugin --profile web add dsh-m`。
110
-
111
- > `npm install -g dsh-m` 只安装 `dshm` CLI(终端管理用),**不会**把插件注册进 web profile;与通过官方插件管理界面安装不冲突,可并存。
112
-
113
- ## 社区清单(awesome-dsh-plugin 目录)
114
-
115
- **社区清单是 dsh-m 的默认清单**:锚定 npm 包 [`dsh-plugin-catalog`](https://www.npmjs.com/package/dsh-plugin-catalog)(CC0-1.0,awesome-dsh-plugin 全量目录,4,000+ 条)。其上叠加手工 curated 的**精选清单**(registry.json),数据层合并去重(**精选清单恒优先**,重名条目社区侧让位),展示层按「社区 / 精选」分区呈现(ADR-0004)——社区区不再显示与精选清单重复的条目,精选区保持策展序单页直出。
116
-
117
- <div align="center">
118
- <img src="https://raw.githubusercontent.com/iasiv5/dsh-m/main/docs/images/community-catalog.webp" alt="市场视图社区分区——社区 / 精选 / 收藏分区 tabs、搜索行与带计数的开放分类 chips" width="86%">
119
- <p><sub>社区清单 · 默认落地分区:分区 tabs、常驻搜索与带计数的开放分类 chips</sub></p>
120
- </div>
121
-
122
- - **数据获取**:dist-tags 探测最新版本 → jsDelivr 按精确版本直取 → npmmirror → unpkg 三线路兜底;版本未变不重拉正文,TTL 内跳过探测。「最新版本」探测结果为纯内存缓存:**重启 DSH 即失效**(发版后重启即可看到新版本),经 dsh-m 安装/升级/卸载成功后该条目缓存立即作废。失败回落 `<缓存目录>/awesome/` 运行时缓存先展示(先回缓存、后台自愈)——过期数据绝不冒充最新;缓存状态(缓存快照/目录版本/获取线路/更新时间)在**设置页**可见,浏览页不打扰。
123
- - **开关与锁定**:设置页 `communityCatalog` 开关(默认开,live 生效)、`communityCatalogPin` 可锁定目录版本(精确 semver);CLI 用 `DSHM_COMMUNITY_CATALOG=0` 退出、`DSHM_COMMUNITY_CATALOG_PIN` 锁版本。
124
- - **npm 元数据路由(0.9.32,ADR-0012)**:安装/升级前的元数据预取按机器网络自适应选源——probe-once(2.5s 预算)探测 npmjs 与镜像谁先应答,决策持久化于 `<缓存目录>/npm-route.json`(删除即重探);检测读(升级探测/发布时刻)恒以 npmjs 权威源优先、镜像降级,履约读(精确版本元数据)走生效源、镜像未同步时自动按需同步后重试。环境变量:`DSHM_NPM_REGISTRY` 强制元数据读取源(最高优先,设了跳过探测);`DSHM_MIRROR_SYNC=0` 关闭镜像滞后自愈。**与 `DSHM_REGISTRY_URL`(收录清单源覆盖)是两回事**。调试回环 registry 时若本机开着系统代理,请配 `NO_PROXY=127.0.0.1,localhost`(代理语义跟随标准 env/npm config,不自动豁免 loopback)。
125
- - **分类与搜索**:社区分类是开放集(已知 23 个分类带中英双语标签:中文由包内单一事实源维护、英文取上游目录,按界面语言下发);搜索走相关性加权管线(中英文双匹配),且为**跨区全局**——任一分区的搜索框同时命中社区与精选(浏览保持分区,摘要行报两分区精确命中数、精选命中置顶分组);社区区排序可切(下载量/Star/收录日期 × 升降,默认下载量降序,无下载量 ≠ 0 下载)。
126
- - **安装语义**:社区条目与精选条目走同一安装链路(npm 锁精确版本 / GitHub 锁 commit SHA);市场浏览页只对 npm 条目做更新探测,GitHub 条目不做页面级探测(匿名配额 60 次/小时不可控)。
127
- - **已装页预算(best-effort)**:已装页对 GitHub 来源插件做更新检查,单次请求 ≤25 个、宿主进程滚动 1 小时 ≤50 个(仅被动检查;主动安装/升级不受限);超限条目如实标注「检查未完成」而不冒充「全部最新」。独立 CLI 进程不共享宿主内预算,并发场景不承诺 60 次/小时绝不耗尽。
128
- - **能力披露**:社区条目的 capabilities/红线只出现在详情折叠区(**缺省 = 未扫描 ≠ 未检出**),卡片不打标;截图仅在详情层加载并经客户端白名单校验。
129
- - **非安全审查警示**:社区目录为全量收录,**未经安全审查**——安装前请自行确认插件来源与能力;精选清单的策展与 CI 校验不适用于社区条目。
130
-
131
- ## 精选清单(registry.json)
132
-
133
- 精选清单手工 curated,叠加在社区清单之上(重名条目恒优先),按**策展五桶**分类:装机必备 / 崔添翼精选 / iasi自研 / 腾讯轻量云专区 / 观察区,当前收录 DSH Skins、DSH TUI、ModSearch、Better Sidebar、DSH Context 等插件。
73
+ GUI 与 Agent 工具始终跟随**当前宿主 profile**(Web / Desktop);独立 CLI 则**只操作 web profile**,`--profile desktop` 会拒绝。`--force` 可在用户已确认 peer 不兼容风险后用于安装或升级。`registry`、`search`、`outdated` 在清单不可用时明确失败,`list` 仍可列已装插件。完整参数与运行策略见[设计文档](https://github.com/iasiv5/dsh-m/blob/main/docs/DESIGN.md)。
134
74
 
135
- <div align="center">
136
- <img src="https://raw.githubusercontent.com/iasiv5/dsh-m/main/docs/images/curated-registry.webp" alt="市场视图精选分区——社区 / 精选 / 收藏分区 tabs 与策展五桶分类 chips(装机必备 / 崔添翼精选 / iasi自研 / 腾讯轻量云专区 / 观察区)" width="86%">
137
- <p><sub>精选清单 · 策展五桶分类:装机必备 / 崔添翼精选 / iasi自研 / 腾讯轻量云专区 / 观察区</sub></p>
138
- </div>
75
+ ## 兼容与常见问题
139
76
 
140
- 清单运行时经 **GitHub 原始文件(raw @main)→ GitHub 镜像(jsDelivr CDN,备用线路)** 双线路获取(按上次成功线路自适应排序),失败回落本地 60 分钟 TTL 缓存与包内快照——清单更新与插件发版**解耦**,push 后最多等一个缓存周期(可在设置页强制刷新)。增补 / 修订条目直接改 `registry.json` 发 PR,CI 自动校验:严格 schema、npm 包与 GitHub 仓库存在性、重复 id、URL 可达性。
141
-
142
- **自定义精选清单(整体替换默认清单)**:设置页支持单一自定义 registry 地址,**整体替换**默认精选清单(不合并),且**仅影响精选区**——社区目录不受影响:
143
-
144
- 1. 「下载默认清单」得到一份官方清单副本;
145
- 2. 自行编辑副本(增删条目);
146
- 3. 在设置页填入副本地址并「校验并应用」——支持 **HTTPS URL**、本机**绝对路径 / `file://`**(HTTP 仅限 127.0.0.1/localhost 本机调试);
147
- 4. 校验失败(字段错误、路径不存在、超过 2 MiB / 1,000 条等)不会保存配置,当前生效清单保持不变;应用成功**即时生效,无需重启**(仅首次部署新版本 dsh-m 需要一次重启);
148
- 5. 「恢复默认」一键回到官方清单。
149
-
150
- 规则与边界:严格 v1 schema(未知字段 / 非法 ID / 超限 / 重复一律拒绝,不截断);副本是独立快照,**不会自动同步**官方新条目;自定义源失败时保留其最近一次成功缓存,绝不静默回退官方清单;切换后旧自定义源缓存会被清理(默认缓存保留);自定义清单未经官方 CI 校验,请确认来源可信再安装;完整本地路径只在设置页显示,工具与卡片只显示短状态。
151
-
152
- 安全基线:拉取仅 HTTPS(loopback HTTP 除外)+ 重定向逐跳校验 + 体积上限 + 超时;npm 安装按精确版本 dist integrity 对照 pnpm lockfile 校验,不一致 fail closed 并回滚;GitHub 安装强制锁定 commit SHA;pnpm 构建脚本被拦时按策略放行并明确报告。
153
-
154
- ## 文档
155
-
156
- | 文档 | 什么时候读 |
157
- |---|---|
158
- | [CHANGELOG.md](https://github.com/iasiv5/dsh-m/blob/main/CHANGELOG.md) | 查看任意版本的变更明细(双语,自 0.4.x 起完整收录) |
159
- | [docs/DESIGN.md](https://github.com/iasiv5/dsh-m/blob/main/docs/DESIGN.md) | 设计共识、面板行为细则、双代兼容与重启链路;开发与发版流程见其 §6 |
160
- | [docs/adr/](https://github.com/iasiv5/dsh-m/tree/main/docs/adr) | 架构决策记录:委派与降级、精确构建放行、社区清单合并、分区市场、双 profile |
161
- | [docs/registry-copy-guide.md](https://github.com/iasiv5/dsh-m/blob/main/docs/registry-copy-guide.md) | 为精选清单条目撰写 description 与 tags |
162
- | [docs/plans/](https://github.com/iasiv5/dsh-m/tree/main/docs/plans) | 各版本实施计划存档 |
163
-
164
- ## FAQ
165
-
166
- **1. 为什么 GitHub 来源的更新提示不走 main HEAD?**
167
- main 上的中间提交可能不稳定。dsh-m 只跟踪 **release / tag**(优先 `releases/latest`,无 release 回退 tags 列表),安装时锁定 tag 指向的 commit SHA。
168
-
169
- **1.5 官方 Desktop 上能用哪些功能?**
170
- Desktop(desktop profile)支持浏览市场、**安装新包**和插件开关。自 0.9.8 起,升级、卸载与 dsh-m 自更新也已委派官方 `pluginManager`;服务不可用时会返回结构化拒绝与操作指引。一键重启仍由官方 Desktop 应用生命周期负责,请退出并重新打开应用。市场安装标注与已装列表只反映 Desktop 当前 profile;收藏和操作记录不与 Web 同步。
171
-
172
- **2. 卸载 dsh-m 会删我的数据吗?**
173
- 不会。只移除 profile 中的包引用(卸载前先下线运行中的界面),并把疑似残留路径报告给你。
77
+ | DSH Web 代际 | 设置通路 | 核验情况 |
78
+ |---|---|---|
79
+ | `0.1.2-rc.1` | cordis 配置降级 | 契约核对 |
80
+ | `0.1.5-rc.1 / rc.2` | `register()` scope | 已核对;rc.1 另有页面实测 |
81
+ | `0.1.7-rc.1 / rc.2` | `.volatile()` + `settings.update` | 已核对 |
82
+ | `0.2.0-rc.1 / rc.2` | 同 0.1.7 通路 | rc.2 与重启链路实测 |
174
83
 
175
- **3. 自定义清单会让市场变慢吗?**
176
- 市场列表是服务端分页(每页 32/64/96 可选),1,000 条清单第一页也只查询当前页的最新版本,浏览仍然流畅。
84
+ **Desktop 与 Web 完全一样吗?** 不是。Desktop 的浏览、安装新包和开关作用于 desktop profile;升级/卸载等操作需宿主官方 `pluginManager` 提供相应能力,缺失时返回结构化拒绝和指引。Desktop 不提供 Web 的一键重启;变更需按应用提示退出并重新打开。两边的收藏、操作记录和已装状态互不串联。
177
85
 
178
- **4. 自定义源挂了怎么办?**
179
- 优先使用该源最近一次成功的缓存(设置页「生效来源」会如实标注);完全没有缓存时市场会提示清单不可用,已安装插件仍可正常管理。修正地址或恢复默认即可。
86
+ **安装、更新后何时生效?** Web 按操作返回的激活要求处理:仅客户端更新可刷新页面;需要重启时先征得同意,再由宿主完成重启与后台重连。使用 systemd 时须有 `Restart=on-failure` 或 `Restart=always`;否则采用部署方的重启方式。
180
87
 
181
- **5. 支持哪些 DSH Web 版本?**
88
+ **为什么 GitHub 插件的更新不跟踪 `main`?** 中间提交可能不稳定,dsh-m 跟踪 release / tag,并锁定其 commit SHA。匿名 GitHub 配额有限,未完成的检查会如实显示,不冒充「全部最新」。
182
89
 
183
- | DSH 版本 | 设置服务通路 | 核验状态 |
184
- |---|---|---|
185
- | `0.1.2-rc.1` | 形态探测降级 cordis 配置通路 | 契约核对 |
186
- | `0.1.5-rc.1` / `rc.2` | `register()` scope 通路 | 已核对;rc.1 另有 `/dshm` ping 与带认证页面 `303 → 200` 实测 |
187
- | `0.1.7-rc.1` / `rc.2` | Config `.volatile()` 字段 + `settings.update` + `loader/volatile-update` 事件 | 已核对;两个 rc 的 dsh-settings 逐字节一致,一套实现通吃 |
188
- | `0.2.0-rc.1` / `rc.2` | 同 `0.1.7` 通路 | `rc.2` 为当前实机运行代际,重启链路于 2026-10-01 实机核验 |
90
+ **自定义清单不可用怎么办?** 优先使用该源上次成功的缓存;没有缓存时会提示清单不可用,已装管理仍能工作。修正地址或在设置页恢复默认即可。
189
91
 
190
- - 通路在运行时按**形态探测**选择,不按版本号分支;未识别的形态自动降级 cordis 配置文件通路,市场主功能不受影响。实现细节见 [`docs/DESIGN.md`](https://github.com/iasiv5/dsh-m/blob/main/docs/DESIGN.md) §12。
191
- - systemd 部署需为 unit 配置 `Restart=on-failure` 或 `Restart=always`,否则请使用部署方的手动重启方式。
92
+ 完整沿革见 [CHANGELOG](https://github.com/iasiv5/dsh-m/blob/main/CHANGELOG.md),设计与开发细节见 [DESIGN](https://github.com/iasiv5/dsh-m/blob/main/docs/DESIGN.md)、[ADR](https://github.com/iasiv5/dsh-m/tree/main/docs/adr)。截图取自 Web 实例;面板版本、条目数与安装状态会随时间和 profile 变化。在源码仓库运行 `npm install && npm run capture:readme` 可复现截图(需 Web 实例已装 dsh-m;此开发脚本不随 npm 插件包分发)。
192
93
 
193
94
  ## License
194
95
 
195
- MIT
96
+ [MIT](./LICENSE)
package/docs/DESIGN.md CHANGED
@@ -182,6 +182,8 @@ npm 安装 / GitHub 安装 / 升级 / 自升级 / 卸载是**同一个事务模
182
182
  技术:`src/client.js` 经 **esbuild** 打包为 `lib/client.js`;`window.__ModuleLoader__.load({ id: "dsh-m", factory })` 注册;**React 从 module loader require**(零额外运行时依赖);manifest 注入 `@deepseek-ai/dsh-client-runtime`、`@deepseek-ai/dsh-client-ui-slots`、`@deepseek-ai/dsh-client-ui-settings`。
183
183
  图标:GitHub 来源自动用 `https://github.com/<owner>.png?size=64`;`icon` 字段可覆盖;npm-only 条目首字母色块回退。
184
184
 
185
+ **README 预览渲染(`src/client/markdown.js`,0.9.47 起)**:自研轻量渲染器,安全模型 = 「白名单标签 + 白名单属性 + safeUrl 闸门 → 全部经 `h()` 构造 React 元素」,**绝不注入原始 HTML 字符串**(无 `dangerouslySetInnerHTML`,文本节点 React 转义兜底)。markdown 侧支持标题/列表/表格/围栏/引用/徽章链接等;HTML 侧支持 GitHub 风 README 常见内嵌子集——块级包裹标签(`p/div/center/details/table/tr/td/li/ul/ol/h1-6` 等)剥壳后按 markdown 递归(保内部结构),行内标签(`a/img/strong/em/code/kbd/mark/br/sub/sup` 等)token 化渲染,`align` 属性映射 `textAlign`、`<img width/height>` 映射显式尺寸(放开徽章 20px 高度帽);未知标签剥壳留文本,`script/style/svg/iframe` 等容器连同内容丢弃,`javascript:` 等危险 URL 归 `#`。已知边界:npm packument 兜底的 README 不携带仓库基址,相对路径图片(如 `docs/assets/x.svg`)按 safeUrl 归 `#` 后由 onError 隐藏;行内文本含标签的解析粒度到段落级(`Press <kbd>Ctrl</kbd>` 类可渲染)。数据源:已装条目读 profile 本地 `README.md`(≤64KB 截断);未装条目回源 npm packument 顶层 `readme` 字段(0.9.45 U10b)。
186
+
185
187
  ## 5. Agent 工具(host)与 CLI
186
188
 
187
189
  - 工具前缀 `dshm_`,共 7 个:`dshm_search` / `dshm_list` / `dshm_install` / `dshm_uninstall` / `dshm_outdated` / `dshm_upgrade` / `dshm_restart`。Agent tools 运行于 Host 进程,与 Web GUI **共用 `host` namespace 缓存与 active config**;`dshm_search` 走服务端过滤(metadata-only,`withLatest=false`、limit ≤80),返回不含本地路径的短 summary。
package/lib/client.js CHANGED
@@ -407,6 +407,50 @@ function safeUrl(u) {
407
407
  if (/^[/#]/.test(t)) return t;
408
408
  return "#";
409
409
  }
410
+ function parseHtmlAttrs(s) {
411
+ const attrs = {};
412
+ if (!s) return attrs;
413
+ const re = /([a-zA-Z][a-zA-Z0-9-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
414
+ let m;
415
+ while (m = re.exec(s)) attrs[m[1].toLowerCase()] = m[2] !== void 0 ? m[2] : m[3] !== void 0 ? m[3] : m[4] !== void 0 ? m[4] : "";
416
+ return attrs;
417
+ }
418
+ function htmlAlignStyle(attrs) {
419
+ const a = String(attrs.align || "").toLowerCase();
420
+ return a === "center" || a === "left" || a === "right" || a === "justify" ? { textAlign: a } : void 0;
421
+ }
422
+ function htmlImgStyle(attrs) {
423
+ const size = (v) => /^\d+(\.\d+)?$/.test(v) ? Number(v) : /^\d+(\.\d+)?%$/.test(v) ? v : null;
424
+ const st = {};
425
+ const w = size(String(attrs.width || ""));
426
+ if (w !== null) {
427
+ st.width = w;
428
+ st.maxWidth = "100%";
429
+ st.height = "auto";
430
+ st.maxHeight = "none";
431
+ }
432
+ const hgt = size(String(attrs.height || ""));
433
+ if (hgt !== null) st.height = hgt;
434
+ return Object.keys(st).length ? st : void 0;
435
+ }
436
+ function htmlBlockEnd(lines, i) {
437
+ const om = /<([a-zA-Z][a-zA-Z0-9-]*)/.exec(lines[i]);
438
+ const tag = om ? om[1].toLowerCase() : "";
439
+ if (!tag || HTML_VOID_TAGS.has(tag)) return i;
440
+ if (new RegExp(`</${tag}\\s*>`, "i").test(lines[i].slice(om.index))) return i;
441
+ const closeRe = new RegExp(`</${tag}\\s*>`, "i");
442
+ let lastNonBlank = i;
443
+ for (let j = i + 1; j < lines.length; j++) {
444
+ if (!lines[j].trim()) {
445
+ if (!HTML_SPAN_BLANK.has(tag) || j - i > 400) return lastNonBlank;
446
+ continue;
447
+ }
448
+ lastNonBlank = j;
449
+ if (closeRe.test(lines[j])) return j;
450
+ if (j - i > 400) return j;
451
+ }
452
+ return lastNonBlank;
453
+ }
410
454
  function createMarkdown(h2) {
411
455
  function ExtLink2({ href, className, children }) {
412
456
  return h2(
@@ -421,11 +465,13 @@ function createMarkdown(h2) {
421
465
  children
422
466
  );
423
467
  }
424
- function MdImg2({ src, alt }) {
468
+ function MdImg2({ src, alt, style, title }) {
425
469
  return h2("img", {
426
470
  className: "dshm-md-img",
427
471
  src: safeUrl(src),
428
472
  alt: alt || "",
473
+ title: title || void 0,
474
+ style: style || void 0,
429
475
  referrerPolicy: "no-referrer",
430
476
  onError: (e) => {
431
477
  e.currentTarget.style.display = "none";
@@ -474,7 +520,7 @@ function createMarkdown(h2) {
474
520
  if (last < src.length) nodes.push(src.slice(last));
475
521
  return nodes;
476
522
  }
477
- function mdBlocks(lines, kb) {
523
+ function mdBlocks(lines, kb, depth = 0) {
478
524
  const out = [];
479
525
  let i = 0;
480
526
  let n = 0;
@@ -485,6 +531,8 @@ function createMarkdown(h2) {
485
531
  const isUl = (s) => /^\s*[-*+]\s+/.test(s);
486
532
  const isOl = (s) => /^\s*\d+[.)]\s+/.test(s);
487
533
  const isTableRow = (s) => s.includes("|") && /^\s*\|/.test(s);
534
+ const isHtmlBlock = (s) => /^\s*<\/?[a-zA-Z][a-zA-Z0-9-]*(?:\s[^>]*?)?\/?>/.test(s);
535
+ const inlineOf = (s, key) => /<[a-zA-Z]/.test(s) ? mdHtmlInline(s, key) : mdInline(s, key);
488
536
  while (i < lines.length) {
489
537
  const line = lines[i];
490
538
  if (!line.trim()) {
@@ -492,6 +540,12 @@ function createMarkdown(h2) {
492
540
  continue;
493
541
  }
494
542
  const k = `${kb}-b${n++}`;
543
+ if (isHtmlBlock(line)) {
544
+ const end = htmlBlockEnd(lines, i);
545
+ out.push(...mdHtmlBlock(lines.slice(i, end + 1).join("\n"), `${k}H`, depth));
546
+ i = end + 1;
547
+ continue;
548
+ }
495
549
  if (isFence(line)) {
496
550
  const buf2 = [];
497
551
  i++;
@@ -502,7 +556,7 @@ function createMarkdown(h2) {
502
556
  }
503
557
  if (isHeading(line)) {
504
558
  const hm = /^(#{1,6})\s+(.*)$/.exec(line);
505
- out.push(h2(`h${hm[1].length}`, { key: k }, mdInline(hm[2], k)));
559
+ out.push(h2(`h${hm[1].length}`, { key: k }, inlineOf(hm[2], k)));
506
560
  i++;
507
561
  continue;
508
562
  }
@@ -514,7 +568,7 @@ function createMarkdown(h2) {
514
568
  if (isQuote(line)) {
515
569
  const buf2 = [];
516
570
  while (i < lines.length && isQuote(lines[i])) buf2.push(lines[i++].replace(/^\s*>\s?/, ""));
517
- out.push(h2("blockquote", { key: k }, mdBlocks(buf2, k)));
571
+ out.push(h2("blockquote", { key: k }, mdBlocks(buf2, k, depth)));
518
572
  continue;
519
573
  }
520
574
  if (isUl(line) || isOl(line)) {
@@ -522,7 +576,7 @@ function createMarkdown(h2) {
522
576
  const re = ordered ? /^\s*\d+[.)]\s+(.*)$/ : /^\s*[-*+]\s+(.*)$/;
523
577
  const items = [];
524
578
  while (i < lines.length && (ordered ? isOl(lines[i]) : isUl(lines[i]))) {
525
- items.push(h2("li", { key: `li${items.length}` }, mdInline(re.exec(lines[i])[1], `${k}-${items.length}`)));
579
+ items.push(h2("li", { key: `li${items.length}` }, inlineOf(re.exec(lines[i])[1], `${k}-${items.length}`)));
526
580
  i++;
527
581
  }
528
582
  out.push(h2(ordered ? "ol" : "ul", { key: k }, items));
@@ -541,30 +595,138 @@ function createMarkdown(h2) {
541
595
  h2(
542
596
  "table",
543
597
  { key: k },
544
- h2("thead", null, h2("tr", null, head.map((c, x) => h2("th", { key: x }, mdInline(c, `${k}h${x}`))))),
545
- h2("tbody", null, rows.map((r, y) => h2("tr", { key: y }, r.map((c, x) => h2("td", { key: x }, mdInline(c, `${k}${y}x${x}`))))))
598
+ h2("thead", null, h2("tr", null, head.map((c, x) => h2("th", { key: x }, inlineOf(c, `${k}h${x}`))))),
599
+ h2("tbody", null, rows.map((r, y) => h2("tr", { key: y }, r.map((c, x) => h2("td", { key: x }, inlineOf(c, `${k}${y}x${x}`))))))
546
600
  )
547
601
  );
548
602
  continue;
549
603
  }
550
604
  const buf = [line];
551
605
  i++;
552
- while (i < lines.length && lines[i].trim() && !isFence(lines[i]) && !isHeading(lines[i]) && !isHr(lines[i]) && !isQuote(lines[i]) && !isUl(lines[i]) && !isOl(lines[i])) {
606
+ while (i < lines.length && lines[i].trim() && !isFence(lines[i]) && !isHeading(lines[i]) && !isHr(lines[i]) && !isQuote(lines[i]) && !isUl(lines[i]) && !isOl(lines[i]) && !isHtmlBlock(lines[i])) {
553
607
  buf.push(lines[i]);
554
608
  i++;
555
609
  }
556
- out.push(h2("p", { key: k }, mdInline(buf.join(" "), k)));
610
+ out.push(h2("p", { key: k }, inlineOf(buf.join(" "), k)));
557
611
  }
558
612
  return out;
559
613
  }
560
- function renderMarkdown2(src) {
561
- return mdBlocks(String(src || "").replace(/\r\n?/g, "\n").split("\n"), "md");
614
+ function mdHtmlInline(src, kb) {
615
+ const out = [];
616
+ const stack = [];
617
+ let last = 0;
618
+ let n = 0;
619
+ let m;
620
+ const cur = () => stack.length ? stack[stack.length - 1].kids : out;
621
+ const pushText = (t) => {
622
+ if (!t) return;
623
+ if (!String(t).trim()) {
624
+ cur().push(" ");
625
+ return;
626
+ }
627
+ const parts = String(t).split(/\n[ \t]*\n+/);
628
+ parts.forEach((part, idx) => {
629
+ const nodes = mdInline(part.replace(/\s+/g, " "), `${kb}t${n++}`);
630
+ if (!nodes.length) return;
631
+ if (idx === 0 || stack.length) cur().push(...nodes);
632
+ else out.push(h2("p", { key: `${kb}tp${n++}` }, ...nodes));
633
+ });
634
+ };
635
+ const closeTop = () => {
636
+ const f = stack.pop();
637
+ cur().push(f.build(f.kids));
638
+ };
639
+ const sepNode = (name, key) => {
640
+ const sep = HTML_UNWRAP_SEP[name];
641
+ if (sep === "br") cur().push(h2("br", { key }));
642
+ else if (sep === "sp") cur().push(" ");
643
+ };
644
+ HTML_TAG_RE.lastIndex = 0;
645
+ while (m = HTML_TAG_RE.exec(src)) {
646
+ if (m.index > last) pushText(src.slice(last, m.index));
647
+ const full = m[0];
648
+ const name = m[2].toLowerCase();
649
+ const key = `${kb}h${n++}`;
650
+ if (m[1]) {
651
+ const idx = stack.map((f) => f.name).lastIndexOf(name);
652
+ if (idx >= 0) {
653
+ while (stack.length > idx + 1) closeTop();
654
+ closeTop();
655
+ } else sepNode(name, key);
656
+ } else if (HTML_SKIP_TAGS.has(name)) {
657
+ const cm = new RegExp(`</${name}\\s*>`, "i").exec(src.slice(m.index + full.length));
658
+ HTML_TAG_RE.lastIndex = cm ? m.index + full.length + cm.index + cm[0].length : src.length;
659
+ last = HTML_TAG_RE.lastIndex;
660
+ continue;
661
+ } else if (name === "img") {
662
+ const attrs = parseHtmlAttrs(m[3]);
663
+ cur().push(h2(MdImg2, { key, src: attrs.src, alt: attrs.alt, title: attrs.title, style: htmlImgStyle(attrs) }));
664
+ } else if (name === "br" || name === "hr") {
665
+ cur().push(h2(name, { key }));
666
+ } else if (HTML_VOID_TAGS.has(name) || m[4]) {
667
+ } else if (HTML_INLINE_MAP[name]) {
668
+ const attrs = parseHtmlAttrs(m[3]);
669
+ if (name === "a") {
670
+ stack.push({ name, kids: [], build: (kids) => h2(ExtLink2, { key, href: attrs.href, title: attrs.title }, kids) });
671
+ } else {
672
+ const type = HTML_INLINE_MAP[name];
673
+ const style = htmlAlignStyle(attrs);
674
+ stack.push({ name, kids: [], build: (kids) => h2(type, style ? { key, style } : { key }, kids) });
675
+ }
676
+ } else if (/^h[1-6]$/.test(name)) {
677
+ const style = htmlAlignStyle(parseHtmlAttrs(m[3]));
678
+ const lvl = name;
679
+ stack.push({ name, kids: [], build: (kids) => h2(lvl, style ? { key, style } : { key }, kids) });
680
+ } else {
681
+ sepNode(name, key);
682
+ }
683
+ last = m.index + full.length;
684
+ }
685
+ if (last < src.length) pushText(src.slice(last));
686
+ while (stack.length) closeTop();
687
+ return out;
688
+ }
689
+ function mdHtmlBlock(raw, kb, depth) {
690
+ const s = String(raw).trim();
691
+ if (depth > 8) return [s];
692
+ const om = /^<([a-zA-Z][a-zA-Z0-9-]*)((?:\s[^>]*)?)>/.exec(s);
693
+ if (!om) return mdHtmlInline(s, kb);
694
+ const tag = om[1].toLowerCase();
695
+ if (!HTML_BLOCK_WRAP.has(tag)) return mdHtmlInline(s, kb);
696
+ const cm = new RegExp(`</${tag}\\s*>\\s*$`, "i").exec(s.slice(om[0].length));
697
+ if (!cm) return mdHtmlInline(s, kb);
698
+ let inner = s.slice(om[0].length, om[0].length + cm.index);
699
+ const attrs = parseHtmlAttrs(om[2] || "");
700
+ const style = htmlAlignStyle(attrs);
701
+ if (tag === "details") {
702
+ const sm = /<summary[^>]*>([\s\S]*?)<\/summary>/i.exec(inner);
703
+ let sumNode = null;
704
+ if (sm) {
705
+ inner = inner.slice(0, sm.index) + inner.slice(sm.index + sm[0].length);
706
+ sumNode = h2("summary", { key: `${kb}sum` }, ...mdHtmlInline(sm[1], `${kb}sm`));
707
+ }
708
+ const kids = sumNode ? [sumNode, ...renderMarkdown2(inner, depth + 1)] : renderMarkdown2(inner, depth + 1);
709
+ return [h2("details", attrs.open !== void 0 ? { key: kb, open: true } : { key: kb }, ...kids)];
710
+ }
711
+ if (/^h[1-6]$/.test(tag)) return [h2(tag, style ? { key: kb, style } : { key: kb }, ...mdHtmlInline(inner, kb))];
712
+ return [h2(tag, style ? { key: kb, style } : { key: kb }, ...renderMarkdown2(inner, depth + 1))];
713
+ }
714
+ function renderMarkdown2(src, depth = 0) {
715
+ return mdBlocks(String(src || "").replace(/\r\n?/g, "\n").split("\n"), "md", depth);
562
716
  }
563
717
  return { ExtLink: ExtLink2, MdImg: MdImg2, renderMarkdown: renderMarkdown2 };
564
718
  }
719
+ var HTML_TAG_RE, HTML_VOID_TAGS, HTML_SKIP_TAGS, HTML_BLOCK_WRAP, HTML_SPAN_BLANK, HTML_INLINE_MAP, HTML_UNWRAP_SEP;
565
720
  var init_markdown = __esm({
566
721
  "src/client/markdown.js"() {
567
722
  "use strict";
723
+ HTML_TAG_RE = /<(\/?)([a-zA-Z][a-zA-Z0-9-]*)((?:\s+[a-zA-Z][a-zA-Z0-9-]*(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?)*)\s*(\/?)>/g;
724
+ HTML_VOID_TAGS = /* @__PURE__ */ new Set(["br", "hr", "img", "source", "input", "wbr", "area", "col", "link", "meta"]);
725
+ HTML_SKIP_TAGS = /* @__PURE__ */ new Set(["script", "style", "iframe", "object", "embed", "noscript", "svg", "title", "textarea", "select", "video", "audio", "canvas", "template", "form", "button", "head"]);
726
+ HTML_BLOCK_WRAP = /* @__PURE__ */ new Set(["p", "div", "center", "blockquote", "details", "ul", "ol", "li", "table", "thead", "tbody", "tr", "td", "th", "figure", "figcaption", "section", "article", "main", "header", "footer", "nav", "aside", "dl", "dt", "dd"]);
727
+ HTML_SPAN_BLANK = /* @__PURE__ */ new Set(["details", "div", "blockquote", "table", "thead", "tbody", "tr", "ul", "ol", "dl", "figure", "section", "article", "td", "th", "li"]);
728
+ HTML_INLINE_MAP = { a: "a", strong: "strong", b: "strong", em: "em", i: "em", del: "del", s: "del", strike: "del", code: "code", kbd: "kbd", sub: "sub", sup: "sup", u: "u", small: "small", mark: "mark", span: "span" };
729
+ HTML_UNWRAP_SEP = { tr: "br", li: "br", p: "br", h1: "br", h2: "br", h3: "br", h4: "br", h5: "br", h6: "br", table: "br", ul: "br", ol: "br", td: "sp", th: "sp" };
568
730
  }
569
731
  });
570
732
 
@@ -2031,6 +2193,12 @@ button.dshm-badge:hover{filter:brightness(.95)}
2031
2193
  .dshm-readme.md th{background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.1))}
2032
2194
  .dshm-readme.md hr{border:0;border-top:1px solid var(--dsw-alias-border-l2,#cbd5e1);margin:8px 0}
2033
2195
  .dshm-readme.md .dshm-md-note{margin-top:8px;padding-top:6px;border-top:1px dashed var(--dsw-alias-border-l2,#cbd5e1);color:var(--dsw-alias-label-caption,#9ca3af);font-size:11px}
2196
+ /* 0.9.47 HTML \u5B50\u96C6\uFF1Adetails/summary/kbd/mark \u4E0E li\xB7td \u5185\u5C42 p \u95F4\u8DDD\u6536\u655B */
2197
+ .dshm-readme.md details{border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:8px;padding:4px 10px;margin:6px 0}
2198
+ .dshm-readme.md summary{cursor:pointer;font-weight:600;margin:2px 0}
2199
+ .dshm-readme.md kbd{background:rgba(127,127,127,.16);border:1px solid var(--dsw-alias-border-l2,#cbd5e1);border-bottom-width:2px;border-radius:4px;padding:0 4px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px}
2200
+ .dshm-readme.md mark{background:rgba(255,213,0,.35);color:inherit;border-radius:2px;padding:0 2px}
2201
+ .dshm-readme.md li>p,.dshm-readme.md td>p,.dshm-readme.md th>p,.dshm-readme.md summary>p,.dshm-readme.md p>p{margin:2px 0}
2034
2202
  .dshm-links{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-top:7px;padding-top:6px;border-top:1px solid var(--dsw-alias-border-l2,#e5e7eb);font-size:11px;color:var(--dsw-alias-label-caption,#6b7280)}
2035
2203
  .dshm-links-k,.dshm-links-sep{color:var(--dsw-alias-label-caption,#9ca3af)}
2036
2204
  .dshm-links a{color:var(--dsw-alias-state-business-primary,#4d6bfe);text-decoration:none;font-weight:500}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-m",
3
- "version": "0.9.45",
3
+ "version": "0.9.47",
4
4
  "description": "可自定义收录清单的 DSH 插件市场:收录、安装、卸载、升级一站完成,附 agent 工具与 dshm CLI。",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -47,7 +47,8 @@
47
47
  "typecheck": "tsc -p tsconfig.json --noEmit",
48
48
  "test": "node --test tests/*.test.mjs",
49
49
  "pretest": "npm run build",
50
- "prepare": "npm run build"
50
+ "prepare": "npm run build",
51
+ "capture:readme": "node scripts/capture-readme.mjs"
51
52
  },
52
53
  "dsh": {
53
54
  "bundle": {
@@ -77,6 +78,7 @@
77
78
  "@types/react-dom": "^18.3.0",
78
79
  "@types/semver": "7.8.0",
79
80
  "esbuild": "^0.25.0",
81
+ "playwright-core": "1.62.1",
80
82
  "react": "^18.3.1",
81
83
  "react-dom": "^18.3.1",
82
84
  "typescript": "^5.6.0"