dsh-armory 2.4.0 → 2.4.2

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
@@ -5,7 +5,7 @@
5
5
  > **The all-in-one control board for DeepSeek Harness**: MCP servers · skills · experts · plugins · tool-usage ledger · four markets — browse, search, install and summon in one place.
6
6
  > Forked from [Imzl-zl/dsh-mcp-manager-ui](https://github.com/Imzl-zl/dsh-mcp-manager-ui) (MIT) and extended into a full armory.
7
7
  >
8
- > **New in v2.3.2**: fixed a whole-panel crash when installing an OAuth connector (a string style in the authorization dialog triggered React error #62); a failed browser open is now retried automatically. See the [changelog](.github/release-notes/v2.3.2.md).
8
+ > **New in v2.4.2**: documentation release — the README (both languages) now matches what actually ships: the three entry seats with six modes, how to hide the floating button, and the v2.4.1 SkillHub detail fixes. The runtime code is byte-identical to v2.4.1. See the [changelog](.github/release-notes/v2.4.2.md).
9
9
 
10
10
  <p align="center">
11
11
  <a href="https://github.com/2026heshao/dsh-armory/stargazers"><img src="https://img.shields.io/github/stars/2026heshao/dsh-armory?style=social" alt="stars"></a>
@@ -26,7 +26,7 @@ dsh plugin --profile web add dsh-armory
26
26
  dsh plugin --profile web add github:2026heshao/dsh-armory
27
27
  ```
28
28
 
29
- Restart DSH, then open **Skills · MCP · Experts** from the global panel list at the top of the sidebar (or the floating button bottom-right). Desktop users on the `desktop` profile: swap `--profile web` for `--profile desktop`.
29
+ Restart DSH, then open **Big Fish Armory** from the global panel list at the top of the sidebar (next to "Plugins") — or use the floating button bottom-right, or the sidebar bottom entry. Desktop users on the `desktop` profile: swap `--profile web` for `--profile desktop`.
30
30
 
31
31
  ## What it solves
32
32
 
@@ -34,6 +34,10 @@ Restart DSH, then open **Skills · MCP · Experts** from the global panel list a
34
34
  |---|---|
35
35
  | MCP config means hand-editing YAML with no feedback on whether it connects | Visual panel: status, tool schemas, enable/disable, reconnect — global & per-project scopes, project changes apply to live sessions instantly |
36
36
  | Installing skills means hunting for `SKILL.md` repos everywhere | **Direct markets**: SkillHub (190k+ skills), WorkBuddy connectors (316) — search, filter, one-click install with per-file sha256 verification |
37
+ | You install a skill from the market, then cannot find it on the board or tell which one is new | **Recognisable installs**: cards show "installed …" with a 7-day highlight and relative time; the filter bar lays skills out newest-first as a "recently installed" timeline; when the board name differs from the market name, the original is shown |
38
+ | The list card shows a summary, but opening the detail makes it blank and the title falls back to the slug | **Details read the right fields**: SkillHub puts the name and summary under the response's `skill` object (`displayName` / `summary_zh`); empty values from the detail endpoint never overwrite what is already on screen, and the list summary shows while loading |
39
+ | The detail's "homepage" opens `api.skillhub.cn` — a JSON endpoint a browser cannot render (405) | **Links go where browsers can go**: API URLs are rewritten to the public page `https://skillhub.cn/<namespace>/<slug>`, and the button is not rendered at all when there is no browsable page |
40
+ | The floating button sits on top of board content and offers no way to close it | **You own the entries**: right-click or long-press the button to hide it (with a hint on how to reopen), it steps aside while the board occupies the centre column, and the six entry modes guarantee you are never left with no way in |
37
41
  | Every MCP's tool descriptions eat context tokens; zero-use tools sit there for free | **Tool Tailor**: scans local session logs for per-tool call counts / errors / latency, plus a skill-level usage ledger; prune dead tools with one click |
38
42
  | Migrating from Claude/Cursor/VS Code means copying config by hand | Detects local configs from 11 clients and imports them with a preview |
39
43
  | OAuth-protected or key-protected servers and plugins just fail, with no hint why | **Plugin market** (the DSH community list) with per-entry installability, **one-click MCP OAuth** (RFC 9728 → 8414 → 7591 → 7636 PKCE → 8707, three callback fallbacks ending at "paste a token"), and a **credential prompt before install** that fills `${...}` placeholders so the server works on first connect |
@@ -104,7 +108,7 @@ Experts (WorkBuddy expert center, 429 experts / 15 industries; category filter +
104
108
  - Filter by transport (HTTP/stdio) and status, search by name/command/URL
105
109
  - Enable, disable, reconnect, add, edit, remove; reveal masked credentials temporarily with the eye button and copy with one click
106
110
  - **Global + project scopes**: global servers live in the Web profile and are available to every project; project servers live in that project's `.dsh/mcp.json` and are visible only to its sessions. A project can also hide any global MCP
107
- - **Two entry points, one panel**: the draggable floating button (position remembered) and the official sidebar entry share the same state; the "Entry" control in the panel header can keep just one of them (never neither)
111
+ - **Three entry seats, six modes**: the **Big Fish Armory** row in the global panel list at the top of the sidebar (the host's own mechanism, next to "Plugins"), the draggable floating button (position remembered) and the sidebar bottom entry all open the same panel. The "Entries" menu in the panel header switches between six combinations (two / one / three seats); **all three can never be off**, and the floating button cannot be hidden while it is your only entry (since 2.4.1 you can also right-click or long-press the button itself to hide it)
108
112
  - **WB Market (direct installs)**: a third board tab with two markets in one place —
109
113
  - **Connectors (MCP)**: reads the official WorkBuddy distribution ZIP (`static.workbuddy.cn`, the same source the WorkBuddy desktop app uses) on demand — 316 connectors, 242 installable (mostly streamable-http), while 74 CLI-based or WorkBuddy-token-dependent ones are honestly labeled "view only" with the reason. Installing = merging the connector's `mcpServers` into your MCP config through the same pipeline as "Import JSON" (admission, duplicate skip, entry-id reservation), in either global or project scope. The ZIP is ETag-cached under `~/.dsh/wb-market/` and decompressed with Node's built-in zlib — **zero new runtime dependencies**
110
114
  - **Skills (SkillHub)**: reads the public SkillHub API (`api.skillhub.cn`, 190k+ skills) with keyword search, four sort orders, and pagination; details include publisher verification and security-scan status. Installing = downloading file-by-file, verifying each against the manifest's sha256, and writing into `~/.agents/skills/` (the same discovery scope as the official skill subsystem) — effective immediately. Installed skills show an "installed" badge, support one-click uninstall, and reinstalling updates them. After an install or uninstall the **Skill** tab's count badge and board list realign with the new set immediately — no tab switch or panel reopen needed. Downloads sorting is now done **within the current page** (the upstream `sortBy=downloads` hangs), while the other sorts still go remote; each request gives up after 15s and reports a localized error
@@ -134,6 +138,9 @@ Experts (WorkBuddy expert center, 429 experts / 15 industries; category filter +
134
138
  - **Skill board categories (new in 2.0.0)**: skills are grouped into **dev / design & creation / docs & office / productivity & automation / ops / uncategorised**, with grid and list views, a category filter and a persistent "showing N/M skills" line. Details add the category, source directory and file location, with one-click "copy SKILL.md path" and "copy @session reference"
135
139
  - **Skill uninstall (new in 2.2.0)**: skills in user-level / project-level directories can be deleted from the detail pane (inline second confirmation, **permanent, no undo**), while skills shipped with DSH, registered at runtime, or living in custom directories stay read-only and say so. The host only exposes read endpoints, so removal is handled by this plugin's `removeSkill`: source allowlist → strict root containment → parent-directory realpath → symlinks unlinked without touching their targets → the folder must contain `SKILL.md` and stay under the root
136
140
  - **Live skill-set refresh (new in 2.2.0)**: installing or uninstalling from the market, and uninstalling from the board, all broadcast on a module-level change bus. The top **Skill** badge and the board list subscribe to the same bus and re-read the truth, with a sequence guard on the badge fetch so rapid toggles cannot roll the number back. The new set is reflected immediately — no tab switch or panel reopen
141
+ - **Recognisable skill installs (new in 2.4.0)**: every Skill card / list row shows an "installed …" badge — green with relative time within 7 days ("installed 3 hours ago"), "N days ago" within 30 days, an absolute date beyond. The timestamp prefers the first-install time recorded in the install-state ledger, then the directory creation time, then its mtime; skills shipped with DSH or registered at runtime have no install event and show no badge. The filter bar adds a **recently installed** timeline (newest first), and when a board name (from `SKILL.md`'s `name:`) differs from the market name recorded in the ledger, the card carries a "from market: original name" badge and the detail pane adds an original-name row. Data comes from the new `skillInstallInfo` RPC (the host scans the standard skill roots and reads only the first 4KB of each `SKILL.md`); the host must be restarted once to register it, and the board says so explicitly instead of showing nothing
142
+ - **Skill details and market homepage fixed (new in 2.4.1)**: SkillHub's detail endpoint keeps the name and summary under the `skill` object (`displayName` / `summary_zh`), while the old mapping only read the root — so the detail dialog showed the slug as the title and an empty body. Values now fall back through `skill.displayName → root name → slug` and `skill.summary_zh → skill.summary → legacy fields`, icon / created date / category / source / publisher come from the same place, and merging the response into the list entry **never lets empty strings overwrite what is already shown** (`0` and `false` still win). The summary from the list is visible while the detail loads. A `homepage` pointing at `api.skillhub.cn` (a JSON API that returns 405 in a browser) is rewritten to `https://skillhub.cn/<namespace>/<slug>`, with a second guard at render time, and feedback links prefer the GitHub repository when one can be resolved (issue #4)
143
+ - **The floating button can be hidden, and it steps aside for the board (new in 2.4.1)**: right-click or long-press (about half a second, works with touch and stylus) opens a menu with **Hide floating button**, followed by a 6-second hint saying where to reopen it. When the board occupies the centre column the button collapses automatically instead of covering list and detail text, and returns when you leave the board — without changing your chosen mode. If the button is your only remaining entry the item is disabled and explains why: pick another mode in the "Entries" menu before hiding it, so you can never end up with an armory that cannot be opened
137
144
  - **Custom suites (new in 2.2.0)**: bundle skills you use together. The **Suites** button in the Skill board toolbar opens a manager: pick skills on the left, arrange them on the right (click **+** or drag one in, drag to reorder, delete with an inline confirmation), and one save overwrites the whole set into `~/.dsh/armory/suites.json` (staging + atomic rename; invalid input is rejected without touching the file). The composer's Skills submenu shows a suite group at the top — one click expands the suite in member order into multiple `/name` tokens in the draft. Limits: 100 suites, 40 members each; members count individually toward "recently used" rather than the suite name
138
145
  - **Marketplace registry (new in 2.0.0)**: market sources are no longer hard-coded — they live in `~/.dsh/armory/marketplaces.json` (`REGISTRY_VERSION = 1`), fall back to the built-in defaults when missing or corrupt, persist on first read, and can be added / edited / removed. Writes are staging + atomic rename
139
146
  - **Instant community list (new in 2.0.0)**: the last successful slim list is mirrored into `localStorage` (7 days / 4MB cap, older snapshot kept on quota errors), so the first paint renders the snapshot before the real result silently replaces it (zero network, zero IPC). When no source is reachable it says "showing the local cache" instead of an empty list
@@ -186,13 +193,21 @@ Details, local development installs, and troubleshooting: [docs/installation.md]
186
193
 
187
194
  ### Opening the panel
188
195
 
189
- Two entry points, one panel:
196
+ Three entry seats, one panel:
190
197
 
198
+ - **The "Big Fish Armory" row in the global panel list** (top of the sidebar): the host's official panel list, right next to "Plugins" — the primary entry;
191
199
  - **Floating button (bottom-right)**: draggable, position remembered;
192
- - **Sidebar entry "Skills · MCP · Experts"**: above "Settings" when expanded, icon-only in the 56px rail.
200
+ - **Sidebar bottom entry**: a 42px row (icon + label) when the sidebar is expanded, an icon button once it collapses into the rail.
193
201
 
194
202
  The panel floats above the session; closing it ("Close" or Esc) does not affect the conversation.
195
203
 
204
+ Which seats stay is chosen in the panel header's **Entries** menu, six modes: `Global panel + floating button (recommended)` (default) / `Global panel only (top-left, next to "Plugins")` / `Global panel + sidebar bottom entry (no floating button)` / `Floating button only` / `Sidebar bottom entry only` / `All three entries`. **All three can never be off** — the panel opens only from those seats, so the menu is a "keep at least one" combination rather than three independent switches. The choice lives in `localStorage`, so a different browser or cleared storage means picking again.
205
+
206
+ Two self-service rules for the floating button (since 2.4.1):
207
+
208
+ - **Right-click or long-press it** (about half a second) for a menu with **Hide floating button** — no hunt for the Entries menu. A 6-second hint then says where to reopen it. If the button is the only entry left, that item is greyed out and points you to the Entries menu: keep another seat before losing this one.
209
+ - **It auto-collapses while the board occupies the centre column**: the button is an overlay and would sit on the list and detail text; it comes back as soon as you leave the board, and your mode is untouched.
210
+
196
211
  ### Global vs project
197
212
 
198
213
  Tabs switch between "Global" and each project:
@@ -220,6 +235,9 @@ The detail pane lists the tools a server currently registers; expand one for its
220
235
  **The panel says a server is "disabled", but the model reports `unknown tool "mcp__xxx__..."`.**
221
236
  That server is disabled, so it registers no tools. Enable it in the panel.
222
237
 
238
+ **I upgraded but the skill detail is still blank, or the floating button behaves as before.**
239
+ Check two things. First, that the new package actually landed: after `dsh plugin --profile web update dsh-armory`, look at the version at the bottom of the panel. Second, **quit DSH completely and start it again** — the market field mapping lives in host-side modules (`lib/wb-market.js`, `lib/index.js`) that are loaded once at process start, so reloading the panel only refetches the UI bundle. If one specific skill still has no summary, post its slug in an issue and that response gets checked on its own.
240
+
223
241
  **How do I upgrade? Why did the panel not notify me?**
224
242
  GitHub tag-pinned installs never cross versions — re-add with the new tag (`github:2026heshao/dsh-armory#v1.9.0`). The banner only queries this repo's GitHub Releases, at most once a day, is dismissible, and never acts on its own.
225
243
 
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  > **DeepSeek Harness 的一体化管理看板**:MCP · 技能 · 专家 · 插件 · 工具账本 · 四大市场,浏览、搜索、一键安装、一键召唤。
6
6
  > fork 自 [Imzl-zl/dsh-mcp-manager-ui](https://github.com/Imzl-zl/dsh-mcp-manager-ui)(MIT),现已扩展为完整军械库。
7
7
  >
8
- > **最新 v2.3.2**:修复安装需 OAuth 连接器时的整面板崩溃(授权弹窗 style 字符串触发 React #62),浏览器打开失败会自动重试唤起。详见 [更新日志](.github/release-notes/v2.3.2.md)。
8
+ > **最新 v2.4.2**:文档版——README(中 / 英)与实机功能对齐:三个入口席位与六种档位、悬浮按钮怎么隐藏、看板打开时它如何自动让位,以及 v2.4.1 的 SkillHub 详情修正都写进来了;运行代码与 v2.4.1 完全相同。详见 [更新日志](.github/release-notes/v2.4.2.md)。
9
9
 
10
10
  <p align="center">
11
11
  <a href="https://github.com/2026heshao/dsh-armory/stargazers"><img src="https://img.shields.io/github/stars/2026heshao/dsh-armory?style=social" alt="stars"></a>
@@ -26,7 +26,7 @@ dsh plugin --profile web add dsh-armory
26
26
  dsh plugin --profile web add github:2026heshao/dsh-armory
27
27
  ```
28
28
 
29
- 装完**重启 DSH**,侧栏顶部「全局面板」里点 **技能 · MCP · 专家**(或右下角悬浮按钮)。桌面端装在 `desktop` profile 时把 `--profile web` 换成 `--profile desktop`。
29
+ 装完**重启 DSH**,侧栏顶部「全局面板」里点 **大肥鱼军械库**(或在右下角悬浮按钮、左侧栏底部入口,三个席位见下文)。桌面端装在 `desktop` profile 时把 `--profile web` 换成 `--profile desktop`。
30
30
 
31
31
  ## 它解决什么
32
32
 
@@ -34,6 +34,10 @@ dsh plugin --profile web add github:2026heshao/dsh-armory
34
34
  |---|---|
35
35
  | MCP 配置要手改 YAML,改完不知道连没连上 | 可视化面板:状态、工具 Schema、启停、重连,全局与项目双作用域,项目级改完即对存活会话生效 |
36
36
  | 装技能要去各处翻 `SKILL.md` 仓库 | **市场直连**:SkillHub 19 万+ 技能、WorkBuddy 316 个连接器,搜索/筛选/一键安装,逐文件 sha256 校验 |
37
+ | 在市场装了技能,回看板却找不到,也分不清哪个是刚装的 | **技能安装可辨识**:卡片标「装于 …」,7 天内高亮并给相对时间;筛选条按安装时间倒序成「最近安装」;看板名与市场原名不一致时标出「来自市场 原名」 |
38
+ | 技能列表有两行摘要,点进详情反而整块空白,标题还退化成 slug | **详情读得到**:SkillHub 把名称与介绍放在接口的 `skill` 子对象(`displayName` / `summary_zh`),映射按此取值;详情回包里的空串不会覆盖已显示的内容,加载中也先给摘要 |
39
+ | 详情里的「来源主页」跳过去是 `api.skillhub.cn`,浏览器只会拿到 405 | **链接点得开**:接口域名统一改写成 `https://skillhub.cn/<namespace>/<slug>` 公开页,渲染层再拦一道,拿不到可浏览主页就不渲染这个按钮 |
40
+ | 右下角悬浮球盖住看板内容,又找不到关闭的地方 | **入口自己管**:右键 / 长按悬浮按钮即可隐藏(并提示从哪重开),看板占据中栏时自动让位;三个入口席位六档可切,不会出现「全关了自己开不了」 |
37
41
  | 每个 MCP 的工具描述都在吃上下文 token,零调用的工具白白占位 | **工具裁缝**:扫描本机会话日志统计每个工具的调用/错误/延迟,并给出技能维度的使用账本;死工具清单一键禁用 |
38
42
  | 从 Claude/Cursor/VS Code 迁移要逐条抄配置 | 自动检出 11 种客户端的本机配置,预览后一键导入 |
39
43
  | 插件散在宿主自带与社区清单两处,格式与可装性各不相同 | **插件页**:把 DSH 社区清单一并呈现并标明可装性(宿主自带组件单独标注),安装统一走宿主 `pluginManager`,装完明确提示需重启 |
@@ -90,7 +94,7 @@ dsh plugin --profile web add github:2026heshao/dsh-armory
90
94
  - 按传输方式(HTTP/stdio)与状态筛选,按名称/命令/URL 搜索
91
95
  - 启用、禁用、重连、添加、编辑、移除;需要时用眼睛临时揭示被掩码的凭据并一键复制
92
96
  - **全局 + 项目双作用域**:全局写 Web profile,一次注册所有项目可用;项目写该项目 `.dsh/mcp.json`,只该项目会话可见;项目里还能「屏蔽」某个全局 MCP
93
- - **两个入口,同一个面板**:右下角悬浮按钮(可拖拽、记忆位置)与官方侧栏入口(展开态在「设置」上方显示「MCP」,收起成 56px 轨道时只剩图标)共享同一开关状态;面板右上角「入口」可以把两者只留一个(不能都关)
97
+ - **三个入口席位,六档可切**:左上角「全局面板」里的**大肥鱼军械库**一行(宿主官方机制,与「插件」同一套)、右下角悬浮按钮(可拖拽、记忆位置)、左侧栏底部入口(展开态 42px 行、收起成轨道时是圆钮)共用同一个面板;面板右上角「入口」菜单在六档之间切换(两个 / 一个 / 三个席位),**不能三个都关**,悬浮按钮若是唯一入口则不可单独隐藏(2.4.1 起还能右键 / 长按悬浮按钮直接隐藏它)
94
98
  - 导入本机其他客户端的 MCP 配置:自动检测 Claude Code、Codex(`config.toml`)、OpenCode(含 `.jsonc`)、pi、Claude Desktop、Cursor、Windsurf、VS Code、Gemini CLI、Roo Code 与跨工具的共享配置,逐个列出,点一条即导入;也能粘贴 Claude/Cursor/Cline/Roo 的 `mcpServers` 或 VS Code 的 `servers` JSON。写入前预览,支持「合并(同名更新)」与「替换」
95
99
  - 跟随 DSH 深色/浅色主题,适配窄屏与移动宽度
96
100
  - 更新提示与**一键升级**:有新版时在面板顶部显示可关闭的提示条(每天最多查一次,可用 `DSH_ARMORY_DISABLE_UPDATE_CHECK` 关闭)。点了「升级」才动,**不会自动升级**;升级由 DSH 官方的 pluginManager 执行,装完提示重启宿主生效。用 `link:` / `file:` / `github:` 装的本地开发副本不给按钮(那会把链接换成 npm 包)
@@ -125,6 +129,9 @@ dsh plugin --profile web add github:2026heshao/dsh-armory
125
129
  - **技能看板分类(2.0.0 新增)**:技能按 **编程开发 / 设计与创作 / 文档办公 / 效率与自动化 / 开发运维 / 未分类** 六类归置,网格与列表双视图可切、可按分类过滤,常驻「显示 N / M 个 skill」;详情新增所属分类、来源目录、文件位置,可一键「复制 SKILL.md 路径」「复制 @会话引用」
126
130
  - **技能卸载(2.2.0 新增)**:用户级 / 项目级目录里的 skill 可在详情里直接卸载(就地二次确认、**永久删除且不可撤销**);随 DSH 分发、运行时注册与自定义目录的 skill 只读,并说明不在可卸载范围内。宿主本身只提供读取接口,卸载由本插件的 `removeSkill` 完成:来源白名单 → 根目录严格包含性 → 父目录 realpath → 符号链接只 unlink 不碰目标 → 目录必须含 `SKILL.md` 且在根内
127
131
  - **技能集合实时刷新(2.2.0 新增)**:市场安装 / 卸载技能、看板自己卸载,三处都会向模块级变更总线广播;顶部 **Skill** 数量角标与看板列表订阅同一条总线,各自重读真值,角标重取带序号守卫,快速连点也不会被乱序回包回滚。装完 / 卸完立刻对新集合,不必切页或重开面板
132
+ - **技能安装可辨识(2.4.0 新增)**:Skill 看板每张卡片 / 列表行显示「装于 …」——7 天内绿色高亮并给相对时间(「3 小时前装」),30 天内显示「N 天前装」,更早给绝对日期。时间优先取安装状态账本登记的首次安装时间,其次目录创建时间,最后目录修改时间;随 DSH 分发 / 运行时注册的技能没有安装事件,不显示徽标。筛选条新增「最近安装」时间线,按安装时间倒序平铺;看板名(取自 `SKILL.md` 的 `name:`)与账本登记的市场名不一致时,卡片出现「来自市场 原名」徽标,详情面板新增「市场原名」行。数据由新增 RPC `skillInstallInfo` 提供(宿主自扫标准技能根目录、只读每个 `SKILL.md` 头部 4KB 解析 `name:`),宿主需重启一次才注册;未注册时看板显式提示,不静默
133
+ - **技能详情与市场主页修正(2.4.1 新增)**:SkillHub 的详情接口把名称与介绍放在 `skill` 子对象(`displayName` / `summary_zh`),旧映射只读根层,于是详情弹层标题退化成 slug、介绍整块空白。现在按 `skill.displayName → 根层 name → slug`、`skill.summary_zh → skill.summary → 旧字段` 逐级回退,图标 / 上架时间 / 分类 / 来源 / 发布方名也从 `skill` 与 `owner` 取;列表项与详情回包合并时**空串不再覆盖已显示的内容**(`0` 与 `false` 照旧保留),加载中就先显示列表里的摘要,不等接口。`homepage` 落在 `api.skillhub.cn`(浏览器打开是 405)时统一改写成 `https://skillhub.cn/<namespace>/<slug>` 公开页,渲染层再拦一道,接口地址不会再被当成网页交出去;能解析到 GitHub 仓库的,反馈链接优先指向仓库(issue #4)
134
+ - **悬浮按钮可隐藏、看板打开时自动让位(2.4.1 新增)**:右键或长按(约半秒,触屏与手写笔同样可用)悬浮按钮弹出菜单,可直接「隐藏悬浮按钮」,隐藏后有一条 6 秒提示说明从哪儿重新打开;看板占据中栏时按钮自动收起,回到会话再出现,不再压在列表与详情文字上。若悬浮按钮是当前唯一入口,菜单里那一项会置灰并说明原因,先到「入口」换档位再隐藏——避免出现「按钮没了、面板也开不了」的死局。档位一览见下面的「打开面板」
128
135
  - **自定义套件(2.2.0 新增)**:把常一起用的 skill 打包成「套件」——Skill 看板工具栏的「套件」打开管理窗,左侧挑技能、右侧编排(点 **+** 或拖入添加,拖拽排序,删除就地二次确认),一次保存即整集合覆盖落盘到 `~/.dsh/armory/suites.json`(staging + rename 原子写,坏数据拒绝且原文件不动)。输入框召唤菜单的「技能」子菜单顶部会出现套件分组,点一次就按成员顺序把多个 `/name` 一并插进草稿。上限 100 个套件 / 每套 40 个成员;成员按各自的技能名进「最近使用」,不会把套件名记成一条孤立记录
129
136
  - **多市场注册表(2.0.0 新增)**:市场源不再硬编码,落 `~/.dsh/armory/marketplaces.json`(`REGISTRY_VERSION = 1`),缺失或损坏回落内置默认项、首读即落盘,支持增删改;写盘一律 staging + rename 原子替换
130
137
  - **社区目录秒开(2.0.0 新增)**:上次成功的精简清单镜像进浏览器 `localStorage`(7 天 / 4MB 上限,超配额保留旧快照),首屏先渲染快照再被真实结果静默替换(0 网络 0 IPC);源不可达时明说「显示的是本地缓存」而非给空列表
@@ -167,14 +174,20 @@ dsh plugin --profile web remove dsh-armory
167
174
 
168
175
  ### 打开面板
169
176
 
170
- 两个入口,同一个面板:
177
+ 三个入口席位,同一个面板:
171
178
 
179
+ - **左上角「全局面板」里的「大肥鱼军械库」一行**:走宿主官方的面板列表,在「插件」旁边,是主入口;
172
180
  - **右下角悬浮按钮**:可拖拽移动,位置会被记住;
173
- - **侧栏「技能 · MCP · 专家」**:展开态在「设置」上方,侧栏收起成 56px 轨道时只剩图标。
181
+ - **左侧栏底部入口**:展开态是 42px 的一行(图标 + 文字),侧栏收起成轨道时只剩一个圆钮。
174
182
 
175
183
  面板浮在会话之上,关掉它(右上角「关闭」或 Esc)不会影响会话。
176
184
 
177
- 两个入口都能单独关掉:面板右上角「入口」里选 `悬浮按钮 + 侧栏入口`(默认)/ `仅悬浮按钮` / `仅侧栏入口`。**不能两个都关**——面板只能从这两个入口打开,所以这是一个三选一,而不是两个独立的开关。选择存在浏览器本地(localStorage),换浏览器或清缓存要重选一次。
185
+ 席位怎么留由面板右上角的「入口」菜单决定,共六档:`全局面板 + 悬浮按钮(推荐,默认)` / `仅全局面板(左上角,"插件"旁)` / `全局面板 + 侧栏底部入口(无悬浮按钮)` / `仅悬浮按钮` / `仅侧栏底部入口` / `三个入口全开`。**三个不能全关**——面板只能从这三个席位打开,所以它是「至少留一个」的组合,而不是三个互不相干的开关。选择存在浏览器本地(localStorage),换浏览器或清缓存要重选一次。
186
+
187
+ 悬浮按钮另外两条自助规则(2.4.1 起):
188
+
189
+ - **右键或长按(约半秒)它**弹出菜单,可直接「隐藏悬浮按钮」——不必再去面板里找「入口」;隐藏后有 6 秒提示说明从哪儿重新打开。若它是当前唯一入口,这一项会置灰并指路到「入口」菜单:先留好别的入口,再关这一个。
190
+ - **看板占据中栏时它自动收起**:悬浮按钮是浮层,看板打开时正好盖住列表与详情的文字;离开看板页它就回来,不改变你选的档位。
178
191
 
179
192
  ### 全局 vs 项目
180
193
 
@@ -235,6 +248,9 @@ dsh plugin --profile web remove dsh-armory
235
248
  **项目行那些状态文案分别是什么意思?**
236
249
  它们把两件独立的事分开说:**连接在不在** —— `已连接 · 当前无会话使用(连接保留,下次直接用)`(最后一个会话结束了,连接按空闲策略留着)/ `已连接 · N 个会话在用 · M 工具` / `连接中…` / `连接失败` / `尚未建立连接(新会话自动挂载)`;**几个会话在用** —— 就是那句里的 `N 个会话在用`。旧版本的「待会话挂载」正是把这两件事压成一句,所以容易被读成"还没连上"。
237
250
 
251
+ **升级后技能详情页还是空的,或者悬浮按钮的行为没变?**
252
+ 两步确认。先确认新包真装上去了:`dsh plugin --profile web update dsh-armory` 之后看面板底部的版本号。再**完全退出 DSH 重新启动**——市场的字段映射在宿主侧模块(`lib/wb-market.js`、`lib/index.js`)里,只在进程启动时加载一次,面板里刷新只会重取界面 JS,宿主那份映射仍是旧的。若只有某个具体技能的介绍仍然缺,欢迎把它的 slug 发到 issue 里,按那条响应单独对一次。
253
+
238
254
  **怎么升级?为什么面板没提示?**
239
255
  从 npm 装的直接 `dsh plugin --profile web update dsh-armory` 就会升小版本,也可以直接点面板提示条上的「升级」(同一件事,免去敲命令)。面板的提示条只查 GitHub Releases、每天最多一次、可关闭、不会自己动;固定 tag 安装的用户不会自动跨版本,需要用新 tag 重新 `add`(见 [安装与升级](docs/installation.md))。
240
256
 
@@ -268,14 +284,14 @@ dsh plugin --profile web remove dsh-armory
268
284
 
269
285
  ```sh
270
286
  npm install --no-save yaml@2 eslint@9 # 测试与风格检查的唯一裸依赖(运行依赖全部由宿主提供)
271
- npm test # 全 42 套件(与 CI / 发版门槛同一条)
287
+ npm test # 全 54 套件(与 CI / 发版门槛同一条)
272
288
  npm run lint # ESLint:no-undef / no-unused-vars / no-empty 等
273
289
  npm run test:e2e # 可选:用宿主补丁算法验证专家预设进入组合树
274
290
  dsh --profile web --dump-config # 看 profile 的有效配置
275
291
  dsh web # 起 Web 做真实操作验证
276
292
  ```
277
293
 
278
- `npm test` 跑的是 `test/run-all.mjs`,依次执行 **42** 个独立套件(各自是独立 node 进程,任一非零退出即整体失败)。套件是显式白名单而非 `node --test` 自动发现——`test/` 下还有带副作用的迁移脚本与需要宿主模块的端到端脚本,自动发现会误跑。
294
+ `npm test` 跑的是 `test/run-all.mjs`,依次执行 **54** 个独立套件(各自是独立 node 进程,任一非零退出即整体失败)。套件是显式白名单而非 `node --test` 自动发现——`test/` 下还有带副作用的迁移脚本与需要宿主模块的端到端脚本,自动发现会误跑。
279
295
 
280
296
  | 分组 | 套件 | 覆盖 |
281
297
  | --- | --- | --- |
@@ -283,6 +299,7 @@ dsh web # 起 Web 做真实操作验证
283
299
  | 契约与入口 | `contract-consistency` `client-entry` `host-import-sources` | 面板可调方法必须在 typert 声明 / client 契约数组 / 宿主方法集三处同时存在;三个入口「全关」不可表达;VS Code `${input:}` 降级 |
284
300
  | 客户端质量 | `client-i18n` `i18n-coverage` `whats-new-i18n` `error-boundary-copy` `persona-truncate` `tailor-window` | 中文皆包 `t()`;en-US 字典双向差集与 zh/非 zh 回落;更新日志英文覆盖逐条对齐(含缺译文回落);崩溃详情可复制;persona 4000 预览阈值;工具裁缝时间窗四档 |
285
301
  | 工具裁缝趋势(2.2.0) | `tailor-timeline` `tailor-tax-ui` `tailor-cache-line` | 真实下发税的时间维度与跨度归因(无线上数据必须为 null 而非 0);裁缝新字段在界面上的接线(vm 真跑源码片段);账本缓存行只剩读数(「清空缓存」按钮已删,钉住不许复活) |
302
+ | SkillHub 修正(2.4.1) | `skillhub-detail-fix` `skillhub-ux-fix` | 详情字段回退链与目录 / 详情映射(fetch 全替身,逐条对上 issue #4 的响应形状);`skillhubHomepage` 域名改写与缺字段兜底;合并时空值不得覆盖、`0`/`false` 必须保留;`browsableHomepageUrl` 拦住接口域名;悬浮按钮订阅 / 看板激活自动收起 / 右键与长按菜单 / 唯一入口置灰 / 卸载清理(hooks 替身真渲染) |
286
303
  | 自定义套件(2.2.0) | `suites-registry` `suites-client` | 整集合覆盖 / 幂等 / 拒绝与截断两态;id 归属契约(保存后不分裂)、引用套件展开为多个 `/name`、三级写通道降级 |
287
304
  | 技能集合(2.2.0) | `skill-remove` `skill-refresh` | 卸载来源白名单与路径边界(根自身 / `..` 穿越 / 跨来源根 / 符号链接只删链接);装 / 卸后角标与看板实时刷新、乱序回包守卫 |
288
305
  | 专家与团队 | `run-expert-manual` `session-persona` `team-bridge` `expert-card-actions` `recent-sort-regression` | 专家市场写链(真网目录 + 人设 + patch 往返,33 条断言);会话级挂/换/摘;团队桥接;卡片按钮真值表;「最近使用」排序 |
package/lib/client.js CHANGED
@@ -221,6 +221,7 @@ const EN_US_MESSAGES = {
221
221
  "仅在本页 {0} 条里筛出已安装的(SkillHub 是远程分页,无法跨页筛选)": "Filters installed ones within this page's {0} entries only (SkillHub pages remotely; cross-page filtering isn't possible)",
222
222
  "仅展示": "Display only",
223
223
  "仅悬浮按钮": "Floating button only",
224
+ "悬浮按钮设置": "Floating button settings",
224
225
  "从左侧列表点选一台服务器,可以查看它的连接信息、参数与工具清单。": "Pick a server from the list on the left to view its connection info, parameters and tool list.",
225
226
  "从左侧选择一个 skill 查看详情": "Pick a skill on the left to see details",
226
227
  "从未": "Never",
@@ -246,6 +247,7 @@ const EN_US_MESSAGES = {
246
247
  "全局配置": "Global config",
247
248
  "全局配置(在「全局」标签页可编辑)": "Global config (editable on the 'Global' tab)",
248
249
  "全局面板 + 悬浮按钮(推荐)": "Global panel + floating button (recommended)",
250
+ "全局面板 + 侧栏底部入口(无悬浮按钮)": "Global panel + sidebar bottom entry (no floating button)",
249
251
  "全部": "All",
250
252
  "全部传输": "All transports",
251
253
  "全部状态": "All states",
@@ -492,7 +494,7 @@ const EN_US_MESSAGES = {
492
494
  "技能 / MCP / 专家 快速调用": "Skills / MCPs / Experts quick invoke",
493
495
  "技能 · MCP · 专家": "Skills · MCPs · Experts",
494
496
  "技能 · MCP · 专家看板": "Skills · MCPs · Experts board",
495
- "技能 · MCP · 专家看板(可拖拽移动)": "Skills · MCPs · Experts board (draggable)",
497
+ "技能 · MCP · 专家看板(拖拽移动,右键或长按隐藏)": "Skills · MCPs · Experts board (drag to move; right-click or long-press to hide)",
496
498
  "技能·专家": "Skills·Experts",
497
499
  "技能按需触发:选择后以 /技能名 插入输入框": "Skills trigger on demand: picking inserts /skill-name into the input box",
498
500
  "技能来自 SkillHub 公开 API,可能还没同步下来。": "Skills come from the public SkillHub API and may not have synced yet.",
@@ -749,6 +751,9 @@ const EN_US_MESSAGES = {
749
751
  "随 DSH 分发": "Ships with DSH",
750
752
  "隐藏 ": "Hide ",
751
753
  "隐藏原值:": "Hide value: ",
754
+ "隐藏悬浮按钮": "Hide floating button",
755
+ "这是当前唯一入口;先到面板「入口」菜单切换模式。": "This is the only entry right now; switch modes in the panel's \"Entries\" menu first.",
756
+ "悬浮按钮已隐藏,可从左上角「大肥鱼军械库」或面板「入口」菜单重新打开。": "Floating button hidden. Reopen it from \"Big Fish Armory\" (top-left) or the panel's \"Entries\" menu.",
752
757
  "零调用": "Zero-call",
753
758
  "零调用工具": "Zero-call tools",
754
759
  "零调用的 MCP 工具 · 点击下钻": "Zero-call MCP tools · click to drill down",
@@ -2056,6 +2061,11 @@ body[data-ds-dark-theme]{--mcp-cat-dev:var(--dsw-static-deepseek-400);--mcp-cat-
2056
2061
  .dsh-mcp-fab:hover{transform:scale(1.05);background:var(--dsw-alias-button-primary-hover)}
2057
2062
  .dsh-mcp-fab:active{transform:scale(.97)}
2058
2063
  .dsh-mcp-fab svg{width:22px;height:22px}
2064
+ .dsh-mcp-tab-pop.dsh-mcp-fab-menu{position:fixed;right:auto;min-width:196px;max-width:240px;z-index:191}
2065
+ .dsh-mcp-fab-menu-item:disabled{opacity:.55;cursor:not-allowed}
2066
+ .dsh-mcp-fab-menu-item:disabled:hover{background:transparent}
2067
+ .dsh-mcp-fab-menu-note{margin:4px 6px 2px;color:var(--mcp-mut);font-size:11px;line-height:1.5}
2068
+ .dsh-mcp-fab-hint{position:fixed;left:50%;bottom:22px;transform:translateX(-50%);z-index:191;max-width:min(440px,82vw);padding:9px 14px;border-radius:999px;background:var(--mcp-bg-2);color:var(--dsw-alias-label-primary);box-shadow:var(--dsw-elevation-panel);font-size:12px;line-height:1.5;text-align:center;pointer-events:none}
2059
2069
  .dsh-mcp-panel-overlay{position:fixed;inset:0;background:var(--dsw-alias-bg-overlay);display:flex;align-items:center;justify-content:center;z-index:180;padding:26px;pointer-events:auto;animation:mcpFade .14s ease-out}
2060
2070
  .dsh-mcp-panel{box-sizing:border-box;width:1000px;max-width:95vw;height:88vh;background:var(--mcp-bg-2);border:0;border-radius:var(--mcp-r-panel);box-shadow:var(--dsw-elevation-prominent);display:flex;flex-direction:column;overflow:hidden;animation:mcpPop .18s cubic-bezier(.2,.9,.3,1.15)}
2061
2071
  .dsh-mcp-panel .dsh-mcp-wrap{height:100%;min-height:0;padding:18px}
@@ -5228,6 +5238,8 @@ const McpTab = (props) => {
5228
5238
  // left/top 重绘成本可忽略,故保留 left/top 定位以复用现有 :hover/:active 的 transform 缩放反馈。
5229
5239
  const FAB_POS_KEY = 'dsh-armory/fab-pos';
5230
5240
  const DRAG_THRESHOLD = 5;
5241
+ // 触屏 / 手写笔长按多久叫出隐藏菜单:与常见长按菜单同一量级,短于拖动判定的误触窗口。
5242
+ const FAB_MENU_PRESS_MS = 520;
5231
5243
 
5232
5244
  function fabViewport() {
5233
5245
  const w = typeof window !== 'undefined' ? window.innerWidth : 0;
@@ -5278,6 +5290,9 @@ const ENTRY_KEY = 'dsh-armory/entry';
5278
5290
  const ENTRY_VISIBILITY = {
5279
5291
  both: { panel: true, fab: true, sidebar: false },
5280
5292
  panel: { panel: true, fab: false, sidebar: false },
5293
+ // 「隐藏悬浮按钮」要保留另外两个入口:两种「面板 + 侧栏」组合各有一档,
5294
+ // 否则从 all 隐藏后会连带丢掉侧栏底部入口(issue #4)。
5295
+ panelSidebar: { panel: true, fab: false, sidebar: true },
5281
5296
  fab: { panel: false, fab: true, sidebar: false },
5282
5297
  sidebar: { panel: false, fab: false, sidebar: true },
5283
5298
  all: { panel: true, fab: true, sidebar: true },
@@ -5287,6 +5302,7 @@ const ENTRY_VISIBILITY = {
5287
5302
  const ENTRY_LABELS = {
5288
5303
  get both() { return t('全局面板 + 悬浮按钮(推荐)'); },
5289
5304
  get panel() { return t('仅全局面板(左上角,"插件"旁)'); },
5305
+ get panelSidebar() { return t('全局面板 + 侧栏底部入口(无悬浮按钮)'); },
5290
5306
  get fab() { return t('仅悬浮按钮'); },
5291
5307
  get sidebar() { return t('仅侧栏底部入口'); },
5292
5308
  get all() { return t('三个入口全开'); },
@@ -5299,6 +5315,18 @@ const ENTRY_MODES = Object.keys(ENTRY_VISIBILITY);
5299
5315
  function normalizeEntryMode(value) {
5300
5316
  return Object.prototype.hasOwnProperty.call(ENTRY_VISIBILITY, value) ? value : ENTRY_DEFAULT;
5301
5317
  }
5318
+ // 「隐藏悬浮按钮」= 换成同一组其它席位不变的档位,而不是直接写 fab:false。
5319
+ // 表里没有对应档位(仅悬浮按钮是唯一入口)时原样返回,调用方据此禁用隐藏项,
5320
+ // 「三个入口全关」因此仍然无法表达。
5321
+ function entryModeWithoutFab(mode) {
5322
+ const current = normalizeEntryMode(mode);
5323
+ if (!ENTRY_VISIBILITY[current].fab) return current;
5324
+ const target = ENTRY_MODES.find((value) => {
5325
+ const visibility = ENTRY_VISIBILITY[value];
5326
+ return !visibility.fab && visibility.panel === ENTRY_VISIBILITY[current].panel && visibility.sidebar === ENTRY_VISIBILITY[current].sidebar;
5327
+ });
5328
+ return target || current;
5329
+ }
5302
5330
  function readEntryMode() {
5303
5331
  try {
5304
5332
  if (typeof localStorage === 'undefined') return ENTRY_DEFAULT;
@@ -5358,6 +5386,12 @@ function overlaySeats(mode) {
5358
5386
  // 两个 id 必须是同一个字符串:侧栏行点击后宿主会先校验 main slot 里存在同 key 的
5359
5387
  // 条目(layout.selectPanel → hasMainPanel),不存在会直接抛错,侧栏就点不动了。
5360
5388
  const PANEL_ID = 'mcp-manager';
5389
+ // 悬浮按钮这一帧该不该出现:入口偏好允许 + 当前不在看板页。
5390
+ // 看板本身占据中栏时按钮只会盖住列表/详情(issue #4),所以自动收起;
5391
+ // 回到会话后 panelInfo 变回别的 id,按钮自然复现。纯函数便于直接断言。
5392
+ function fabVisibleFor(mode, activePanelId) {
5393
+ return overlaySeats(mode).fab === true && activePanelId !== PANEL_ID;
5394
+ }
5361
5395
  // 入口排序:插件管理器用 0,定时任务用 10;取 5 让本入口正好落在「插件」旁边。
5362
5396
  const PANEL_ORDER = 5;
5363
5397
  // 看板五分类(MCP / Skill / 专家 / 工具裁缝 / 市场)随名称一并示人。
@@ -9432,6 +9466,26 @@ const CommunityPresence = ({ call, repo, owner, avatar, updatedAt, compact }) =>
9432
9466
  ),
9433
9467
  );
9434
9468
  };
9469
+ // 详情接口的 homepage 历史上可能是 api.skillhub.cn(JSON 接口,浏览器打开 405)。
9470
+ // 数据层已统一成公开页(lib/wb-market.js 的 skillhubHomepage);渲染层再兜一层,
9471
+ // 旧缓存或未来上游变化都不会把接口地址交给浏览器。
9472
+ const browsableHomepageUrl = (value) => {
9473
+ const url = String(value || '').trim();
9474
+ if (!/^https?:\/\//i.test(url)) return '';
9475
+ if (/^https?:\/\/api\.skillhub\.cn(?=[/?#]|$)/i.test(url)) return '';
9476
+ return url;
9477
+ };
9478
+ // 详情回包只覆盖「有值」的字段:上游对缺失字段返回空串 / 空数组时,不能把列表项里
9479
+ // 已有的中文名、介绍、上架时间冲掉(issue #4)。0 与 false 是合法值,照常覆盖。
9480
+ const mergeSkillDetail = (listItem, detail) => {
9481
+ const merged = { ...(listItem || {}) };
9482
+ for (const [key, value] of Object.entries(detail || {})) {
9483
+ if (value === '' || value === null || value === undefined) continue;
9484
+ if (Array.isArray(value) && value.length === 0) continue;
9485
+ merged[key] = value;
9486
+ }
9487
+ return merged;
9488
+ };
9435
9489
  const feedbackKindLabel = (kind) =>
9436
9490
  kind === 'skill' ? t('技能') : kind === 'expert' ? t('专家') : kind === 'plugin' ? t('插件') : kind === 'connector' ? t('连接器') : t('条目');
9437
9491
  const feedbackTemplate = (item, kind, repo, homepage) => {
@@ -9460,8 +9514,8 @@ const FeedbackLinks = ({ item, kind, repo, homepage, sourceLabel }) => {
9460
9514
  const name = String(item?.name || item?.id || item?.plugin || item?.slug || '').trim() || t('未命名条目');
9461
9515
  const repoKey = parseGithubRepoKey(repo || item?.repo || item?.repository);
9462
9516
  const repoUrl = repoKey ? `https://github.com/${repoKey}` : '';
9463
- const home = String(homepage || item?.homepage || item?.upstreamUrl || '').trim();
9464
- const homeUrl = /^https?:\/\//i.test(home) && home !== repoUrl ? home : '';
9517
+ const home = browsableHomepageUrl(homepage || item?.homepage || item?.upstreamUrl);
9518
+ const homeUrl = home && home !== repoUrl ? home : '';
9465
9519
  const template = feedbackTemplate(item, kind, repoUrl || homeUrl, homeUrl);
9466
9520
  const title = t('{0} · {1} 的反馈', [feedbackKindLabel(kind), name]);
9467
9521
  const issueUrl = ARMORY_REPO_URL + '/issues/new?title=' + encodeURIComponent(title) + '&body=' + encodeURIComponent(template);
@@ -10434,7 +10488,8 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
10434
10488
  try {
10435
10489
  const res = await call('wbSkillDetail', { slug: skill.slug, namespace: skill.namespace });
10436
10490
  if (!res?.ok) throw new Error(res?.error || t('读取详情失败'));
10437
- setSkillDetail({ ...skill, ...res, loading: false });
10491
+ // 回包里的空串不能覆盖列表项已有的中文名 / 介绍(issue #4)。
10492
+ setSkillDetail({ ...mergeSkillDetail(skill, res), loading: false });
10438
10493
  } catch (e) {
10439
10494
  setSkillDetail({ ...skill, loading: false, detailError: String(e?.message || e) });
10440
10495
  }
@@ -10989,7 +11044,7 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
10989
11044
  owner: skill.ownerName || skill.namespace,
10990
11045
  avatar: skill.iconUrl || githubAvatarUrl(skill.homepage),
10991
11046
  updatedAt: skill.updatedAt,
10992
- href: /^https?:\/\//i.test(String(skill.homepage || '')) ? skill.homepage : '',
11047
+ href: browsableHomepageUrl(skill.homepage),
10993
11048
  className: 'dsh-mcp-presence-card',
10994
11049
  }),
10995
11050
  h('div', { className: 'dsh-mcp-card-tags' },
@@ -11337,15 +11392,20 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
11337
11392
  }),
11338
11393
  h(FeedbackLinks, {
11339
11394
  item: skillDetail, kind: 'skill',
11340
- repo: parseGithubRepoKey(skillDetail.homepage || skillDetail.upstreamUrl) ? (skillDetail.homepage || skillDetail.upstreamUrl) : '',
11395
+ // upstream_url(GitHub)优先,其次才是技能公开页:公开页不是仓库时
11396
+ // 不该占掉「去仓库」的位置。
11397
+ repo: parseGithubRepoKey(skillDetail.upstreamUrl)
11398
+ ? skillDetail.upstreamUrl
11399
+ : (parseGithubRepoKey(skillDetail.homepage) ? skillDetail.homepage : ''),
11341
11400
  homepage: skillDetail.homepage || skillDetail.upstreamUrl,
11342
11401
  sourceLabel: skillDetail.ownerName || skillDetail.publisher || t('SkillHub'),
11343
11402
  }),
11403
+ // 介绍先从列表项渲染(不必等详情回来),详情到达后原地替换为完整正文。
11404
+ skillDetail.description ? h('p', { className: 'dsh-mcp-wb-modal-desc' }, skillDetail.description) : null,
11344
11405
  skillDetail.loading
11345
11406
  ? h('p', { className: 'dsh-mcp-wb-modal-note' }, t('正在读取详情…'))
11346
11407
  : h('div', null,
11347
11408
  skillDetail.detailError ? h('p', { className: 'dsh-mcp-wb-error' }, skillDetail.detailError) : null,
11348
- skillDetail.description ? h('p', { className: 'dsh-mcp-wb-modal-desc' }, skillDetail.description) : null,
11349
11409
  h('div', { className: 'dsh-mcp-card-tags' },
11350
11410
  skillDetail.version ? h(Badge, null, 'v' + skillDetail.version) : null,
11351
11411
  skillDetail.publisher ? h(Badge, null, skillDetail.publisher + (skillDetail.verifiedPublisher ? t('(已认证)') : '')) : null,
@@ -12282,24 +12342,120 @@ const WhatsNewModal = ({ call }) => {
12282
12342
  );
12283
12343
  };
12284
12344
 
12345
+ // 当前主面板 id:与 openBoardPanel 读同一份 layout.panelInfo 快照。
12346
+ // 取不到(旧宿主 / layout 还没装好)就返回 undefined —— 悬浮按钮保持原样,不误隐藏。
12347
+ function readActivePanelId() {
12348
+ try {
12349
+ const ctx = MODULE_CTX.ctx;
12350
+ const layout = ctx && typeof ctx.get === 'function' ? ctx.get('layout') : null;
12351
+ const info = layout && layout.panelInfo;
12352
+ const snapshot = info && typeof info.getSnapshot === 'function'
12353
+ ? info.getSnapshot()
12354
+ : (info && typeof info.getState === 'function' ? info.getState() : null);
12355
+ return snapshot && typeof snapshot.activePanelId === 'string' ? snapshot.activePanelId : undefined;
12356
+ } catch {
12357
+ return undefined;
12358
+ }
12359
+ }
12360
+ // 订阅宿主的面板切换。panelInfo 有 subscribe 就真订阅(zustand 风格 store),
12361
+ // 没有就 1 秒轮询兜底——面板开合是低频事件,这个代价可以忽略。
12362
+ const useActivePanelId = () => {
12363
+ const [active, setActive] = useState(readActivePanelId);
12364
+ useEffect(() => {
12365
+ let disposed = false;
12366
+ let off = null;
12367
+ let timer = 0;
12368
+ const bind = () => {
12369
+ if (disposed) return;
12370
+ const ctx = MODULE_CTX.ctx;
12371
+ const layout = ctx && typeof ctx.get === 'function' ? ctx.get('layout') : null;
12372
+ const info = layout && layout.panelInfo;
12373
+ if (!info) {
12374
+ timer = setTimeout(bind, 1000);
12375
+ return;
12376
+ }
12377
+ const read = () => { if (!disposed) setActive(readActivePanelId()); };
12378
+ read();
12379
+ if (typeof info.subscribe === 'function') {
12380
+ const stop = info.subscribe(read);
12381
+ if (typeof stop === 'function') off = stop;
12382
+ return;
12383
+ }
12384
+ timer = setInterval(read, 1000);
12385
+ };
12386
+ bind();
12387
+ return () => {
12388
+ disposed = true;
12389
+ clearTimeout(timer);
12390
+ clearInterval(timer);
12391
+ if (typeof off === 'function') off();
12392
+ };
12393
+ }, []);
12394
+ return active;
12395
+ };
12396
+
12285
12397
  // ── 悬浮按钮宿主(shell.overlay):只剩这一个席位 ────────────────────────────────
12286
12398
  const McpOverlay = () => {
12287
12399
  useI18nRevision(); // 宿主切语言后重渲染,模块级 getter 文案即时更新
12288
12400
  const mode = useEntryMode();
12289
- const seats = overlaySeats(mode);
12401
+ const activePanelId = useActivePanelId();
12402
+ // 看板页打开时自动收起:悬浮按钮固定在中栏之上,会直接盖住列表与详情文字(issue #4)。
12403
+ const fabVisible = fabVisibleFor(mode, activePanelId);
12404
+ const canHideFab = entryModeWithoutFab(mode) !== mode;
12290
12405
  const [pos, setPos] = useState(() => readFabPos() || defaultFabPos());
12406
+ const [menuOpen, setMenuOpen] = useState(false);
12407
+ const [hideHint, setHideHint] = useState(false);
12291
12408
  const fabRef = useRef(null);
12409
+ const menuRef = useRef(null);
12292
12410
  const dragRef = useRef(null);
12293
12411
  const suppressClickRef = useRef(false);
12412
+ const pressTimerRef = useRef(0);
12294
12413
 
12295
12414
  // 窗口尺寸变化时把按钮拉回可视区,避免记忆的位置因分辨率变化而出界。
12296
- // 悬浮按钮被偏好关掉时不订阅:给一个看不见的按钮算位置只是白渲染。
12415
+ // 悬浮按钮不可见时不订阅:给一个看不见的按钮算位置只是白渲染。
12297
12416
  useEffect(() => {
12298
- if (!ENTRY_VISIBILITY[mode].fab) return;
12417
+ if (!fabVisible) return;
12299
12418
  const onResize = () => setPos((current) => clampFabPos(current.x, current.y));
12300
12419
  window.addEventListener('resize', onResize);
12301
12420
  return () => window.removeEventListener('resize', onResize);
12302
- }, [mode]);
12421
+ }, [fabVisible]);
12422
+
12423
+ // 按钮被隐藏(手动 / 看板页自动收起)时,菜单不能留在屏幕上。
12424
+ useEffect(() => {
12425
+ if (!fabVisible) setMenuOpen(false);
12426
+ }, [fabVisible]);
12427
+
12428
+ // 菜单外侧点击 / Esc 关闭。菜单项自己的 click 由 stopPropagation 挡住,不会误关。
12429
+ useEffect(() => {
12430
+ if (!menuOpen) return;
12431
+ const onDocClick = (event) => {
12432
+ if (menuRef.current && menuRef.current.contains(event.target)) return;
12433
+ setMenuOpen(false);
12434
+ };
12435
+ const onKey = (event) => { if (event.key === 'Escape') setMenuOpen(false); };
12436
+ document.addEventListener('click', onDocClick);
12437
+ document.addEventListener('keydown', onKey);
12438
+ return () => {
12439
+ document.removeEventListener('click', onDocClick);
12440
+ document.removeEventListener('keydown', onKey);
12441
+ };
12442
+ }, [menuOpen]);
12443
+
12444
+ // 隐藏后的提示条自动收起(文案在渲染期取 t(),切语言即时变)。
12445
+ useEffect(() => {
12446
+ if (!hideHint) return;
12447
+ const id = setTimeout(() => setHideHint(false), 6000);
12448
+ return () => clearTimeout(id);
12449
+ }, [hideHint]);
12450
+
12451
+ // 长按(触屏 / 手写笔)与右键都能叫出菜单;鼠标拖动与长按互不抢占。
12452
+ const cancelPressMenu = () => {
12453
+ if (pressTimerRef.current) {
12454
+ clearTimeout(pressTimerRef.current);
12455
+ pressTimerRef.current = 0;
12456
+ }
12457
+ };
12458
+ useEffect(() => cancelPressMenu, []);
12303
12459
 
12304
12460
  const handlePointerDown = (event) => {
12305
12461
  if (event.button !== undefined && event.button !== 0) return;
@@ -12316,6 +12472,15 @@ const McpOverlay = () => {
12316
12472
  moved: false,
12317
12473
  };
12318
12474
  try { el.setPointerCapture(event.pointerId); } catch { /* 忽略不支持捕获的环境 */ }
12475
+ if (event.pointerType && event.pointerType !== 'mouse') {
12476
+ cancelPressMenu();
12477
+ pressTimerRef.current = setTimeout(() => {
12478
+ pressTimerRef.current = 0;
12479
+ dragRef.current = null;
12480
+ suppressClickRef.current = true;
12481
+ setMenuOpen(true);
12482
+ }, FAB_MENU_PRESS_MS);
12483
+ }
12319
12484
  };
12320
12485
  const handlePointerMove = (event) => {
12321
12486
  const drag = dragRef.current;
@@ -12325,6 +12490,7 @@ const McpOverlay = () => {
12325
12490
  const dy = event.clientY - drag.startClientY;
12326
12491
  if (!drag.moved && Math.hypot(dx, dy) < DRAG_THRESHOLD) return;
12327
12492
  drag.moved = true;
12493
+ cancelPressMenu();
12328
12494
  drag.live = clampFabPos(drag.startX + dx, drag.startY + dy);
12329
12495
  // 跟手:直接写 DOM,不触发 React 渲染;松手时才 setPos 提交。
12330
12496
  el.style.left = drag.live.x + 'px';
@@ -12332,6 +12498,7 @@ const McpOverlay = () => {
12332
12498
  el.style.cursor = 'grabbing';
12333
12499
  };
12334
12500
  const handlePointerEnd = () => {
12501
+ cancelPressMenu();
12335
12502
  const drag = dragRef.current;
12336
12503
  dragRef.current = null;
12337
12504
  if (fabRef.current) fabRef.current.style.cursor = '';
@@ -12346,24 +12513,53 @@ const McpOverlay = () => {
12346
12513
  };
12347
12514
  const handleClick = () => {
12348
12515
  if (suppressClickRef.current) { suppressClickRef.current = false; return; }
12516
+ // 菜单开着时,点按钮先把菜单收起(否则会同时打开看板 + 留下菜单)。
12517
+ if (menuOpen) { setMenuOpen(false); return; }
12349
12518
  openBoardPanel();
12350
12519
  };
12520
+ const handleContextMenu = (event) => {
12521
+ event.preventDefault();
12522
+ setMenuOpen(true);
12523
+ };
12524
+ const handleHideFab = () => {
12525
+ setMenuOpen(false);
12526
+ const next = entryModeWithoutFab(mode);
12527
+ if (next === mode) return;
12528
+ entryMode.set(next);
12529
+ setHideHint(true);
12530
+ };
12351
12531
 
12352
12532
  const style = { left: pos.x + 'px', top: pos.y + 'px', right: 'auto', bottom: 'auto' };
12533
+ // 菜单贴着按钮,但夹在视口内;下方放不下就翻到按钮上面。
12534
+ const viewportWidth = typeof window === 'undefined' ? 1024 : window.innerWidth;
12535
+ const viewportHeight = typeof window === 'undefined' ? 768 : window.innerHeight;
12536
+ const menuWidth = 216;
12537
+ const menuHeight = canHideFab ? 78 : 112;
12538
+ const menuBelow = pos.y + FAB_SIZE + 8;
12539
+ const menuStyle = {
12540
+ left: Math.max(8, Math.min(pos.x, viewportWidth - menuWidth - 8)) + 'px',
12541
+ top: (menuBelow + menuHeight <= viewportHeight - 8
12542
+ ? menuBelow
12543
+ : Math.max(8, pos.y - menuHeight - 8)) + 'px',
12544
+ width: menuWidth + 'px',
12545
+ };
12353
12546
 
12354
12547
  return h(
12355
12548
  React2.Fragment,
12356
12549
  null,
12357
- seats.fab ? h(
12550
+ fabVisible ? h(
12358
12551
  'button',
12359
12552
  {
12360
12553
  ref: fabRef,
12361
12554
  type: 'button',
12362
12555
  className: 'dsh-mcp-fab',
12363
- title: t('技能 · MCP · 专家看板(可拖拽移动)'),
12556
+ title: t('技能 · MCP · 专家看板(拖拽移动,右键或长按隐藏)'),
12364
12557
  'aria-label': t('打开技能 · MCP · 专家看板'),
12558
+ 'aria-haspopup': 'menu',
12559
+ 'aria-expanded': menuOpen,
12365
12560
  style,
12366
12561
  onClick: handleClick,
12562
+ onContextMenu: handleContextMenu,
12367
12563
  onPointerDown: handlePointerDown,
12368
12564
  onPointerMove: handlePointerMove,
12369
12565
  onPointerUp: handlePointerEnd,
@@ -12371,6 +12567,30 @@ const McpOverlay = () => {
12371
12567
  },
12372
12568
  h(Icon, { d: ICONS.board, size: 22 }),
12373
12569
  ) : null,
12570
+ fabVisible && menuOpen ? h(
12571
+ 'div',
12572
+ {
12573
+ ref: menuRef,
12574
+ className: 'dsh-mcp-tab-pop dsh-mcp-fab-menu',
12575
+ role: 'menu',
12576
+ 'aria-label': t('悬浮按钮设置'),
12577
+ style: menuStyle,
12578
+ onClick: (event) => event.stopPropagation(),
12579
+ onContextMenu: (event) => event.preventDefault(),
12580
+ },
12581
+ h('button', {
12582
+ type: 'button',
12583
+ role: 'menuitem',
12584
+ className: 'dsh-mcp-entry-item dsh-mcp-fab-menu-item',
12585
+ disabled: !canHideFab,
12586
+ onClick: handleHideFab,
12587
+ }, t('隐藏悬浮按钮')),
12588
+ canHideFab
12589
+ ? null
12590
+ : h('p', { className: 'dsh-mcp-fab-menu-note' }, t('这是当前唯一入口;先到面板「入口」菜单切换模式。')),
12591
+ ) : null,
12592
+ hideHint ? h('div', { className: 'dsh-mcp-fab-hint', role: 'status' },
12593
+ t('悬浮按钮已隐藏,可从左上角「大肥鱼军械库」或面板「入口」菜单重新打开。')) : null,
12374
12594
  );
12375
12595
  };
12376
12596
 
package/lib/wb-market.js CHANGED
@@ -505,6 +505,33 @@ export async function wbConnectorCachedCount() {
505
505
  // 详情:/api/v1/skills/:slug?namespace=<handle>(带 latestVersion、publisher、securityReports)
506
506
  // 文件:/api/v1/skills/:slug/files?namespace=<handle>[&version=];/file 同形 + &path=
507
507
  // namespace 用 namespace.handle(列表项里是对象), 不是 canonicalName。
508
+ const SKILLHUB_PUBLIC_ORIGIN = "https://skillhub.cn";
509
+ /**
510
+ * 把 SkillHub 的 homepage 归一成「浏览器能打开」的地址(issue #4)。
511
+ * 上游列表/详情的 homepage 实际是 `https://api.skillhub.cn/<namespace>/<slug>`——那是
512
+ * JSON 接口,浏览器直接打开只得到 405。人可读页面在 `https://skillhub.cn/<namespace>/<slug>`
513
+ * (2026-10 实测 200 text/html)。优先用 namespace + slug 自己拼公开页,这样即使上游
514
+ * 换成别的内部域名也不会把接口地址交给浏览器;GitHub 等其它可浏览地址原样保留。
515
+ */
516
+ export function skillhubHomepage(rawHomepage, namespace, slug) {
517
+ const handle = String(namespace || "").trim();
518
+ const skillSlug = String(slug || "").trim();
519
+ const publicPage = handle && skillSlug
520
+ ? `${SKILLHUB_PUBLIC_ORIGIN}/${encodeURIComponent(handle)}/${encodeURIComponent(skillSlug)}`
521
+ : "";
522
+ const raw = String(rawHomepage || "").trim();
523
+ if (!raw) return publicPage;
524
+ let parsed;
525
+ try {
526
+ parsed = new URL(raw);
527
+ } catch {
528
+ return raw;
529
+ }
530
+ if (parsed.hostname.toLowerCase() === "api.skillhub.cn") {
531
+ return publicPage || `${SKILLHUB_PUBLIC_ORIGIN}${parsed.pathname}${parsed.search}${parsed.hash}`;
532
+ }
533
+ return raw;
534
+ }
508
535
  const SKILLHUB_SORTS = new Set(["downloads", "installs", "stars", "score"]);
509
536
  // 2026-10 实测(api.skillhub.cn):`sortBy=downloads` 已不再是可用值——TCP 能连上、
510
537
  // 但服务端不回任何字节,一直挂到客户端 AbortSignal 超时;installs / stars / score /
@@ -543,32 +570,39 @@ export async function wbSkillCatalog(payload = {}) {
543
570
  if (category) params.set("category", category.slice(0, 64));
544
571
  const body = await skillhubJson(`/api/skills?${params.toString()}`);
545
572
  const skills = Array.isArray(body?.data?.skills) ? body.data.skills : [];
546
- const rows = skills.map((skill) => ({
547
- slug: String(skill.slug || ""),
548
- name: String(skill.name || skill.slug || ""),
549
- namespace: String(skill.namespace?.handle || ""),
550
- canonical: String(skill.namespace?.canonicalName || ""),
551
- // 「人」的字段:发布方显示名、作品图标、公开详情页、上架时间与认领状态。
552
- // 这些字段列表接口本来就有,不额外发请求——卡片上的人味信号零成本。
553
- ownerName: String(skill.ownerName || skill.namespace?.displayName || skill.namespace?.handle || ""),
554
- iconUrl: String(skill.iconUrl || ""),
555
- homepage: String(skill.homepage || ""),
556
- createdAt: Number(skill.created_at) || 0,
557
- claimState: String(skill.claim_state || ""),
558
- source: String(skill.source || ""),
559
- description: String(skill.description_zh || skill.description || ""),
560
- descriptionEn: String(skill.description_en || ""),
561
- downloads: Number(skill.downloads) || 0,
562
- installs: Number(skill.installs) || 0,
563
- stars: Number(skill.stars) || 0,
564
- score: Number(skill.score) || 0,
565
- version: String(skill.version || ""),
566
- verified: !!skill.verified,
567
- updatedAt: Number(skill.updated_at) || 0,
568
- category: String(skill.category || ""),
569
- tags: Array.isArray(skill.tags) ? skill.tags.slice(0, 6).map(String) : [],
570
- upstreamUrl: String(skill.upstream_url || skill.homepage || ""),
571
- })).filter((skill) => skill.slug && skill.namespace);
573
+ const rows = skills.map((skill) => {
574
+ const slug = String(skill.slug || "");
575
+ const namespace = String(skill.namespace?.handle || "");
576
+ const homepage = skillhubHomepage(skill.homepage, namespace, slug);
577
+ return {
578
+ slug,
579
+ name: String(skill.name || skill.slug || ""),
580
+ namespace,
581
+ canonical: String(skill.namespace?.canonicalName || ""),
582
+ // 「人」的字段:发布方显示名、作品图标、公开详情页、上架时间与认领状态。
583
+ // 这些字段列表接口本来就有,不额外发请求——卡片上的人味信号零成本。
584
+ ownerName: String(skill.ownerName || skill.namespace?.displayName || skill.namespace?.handle || ""),
585
+ iconUrl: String(skill.iconUrl || ""),
586
+ // 上游把 homepage 填成了 api.skillhub.cn 的 JSON 地址(405),换成公开页。
587
+ homepage,
588
+ createdAt: Number(skill.created_at) || 0,
589
+ claimState: String(skill.claim_state || ""),
590
+ source: String(skill.source || ""),
591
+ description: String(skill.description_zh || skill.description || ""),
592
+ descriptionEn: String(skill.description_en || ""),
593
+ downloads: Number(skill.downloads) || 0,
594
+ installs: Number(skill.installs) || 0,
595
+ stars: Number(skill.stars) || 0,
596
+ score: Number(skill.score) || 0,
597
+ version: String(skill.version || ""),
598
+ verified: !!skill.verified,
599
+ updatedAt: Number(skill.updated_at) || 0,
600
+ category: String(skill.category || ""),
601
+ tags: Array.isArray(skill.tags) ? skill.tags.slice(0, 6).map(String) : [],
602
+ // upstream_url 是真正的外部来源(GitHub / ClawHub):能打开就保留,缺失回落公开页。
603
+ upstreamUrl: skillhubHomepage(skill.upstream_url, namespace, slug) || homepage,
604
+ };
605
+ }).filter((skill) => skill.slug && skill.namespace);
572
606
  // 下载量排序上游已不可用,退化为「当前页内按下载量降序」:跨页顺序仍是上游默认序,
573
607
  // 所以这是页内排序而不是全库排序(UI 侧「最多下载」的含义随之收窄,见下)。
574
608
  if (!remoteSort) rows.sort((a, b) => b.downloads - a.downloads);
@@ -590,20 +624,32 @@ export async function wbSkillDetail(payload = {}) {
590
624
  // 详情端点**没有** {code,data,message} 包装:latestVersion 等字段直接在根上
591
625
  // (列表端点才有 data 包装)。两种形状都兼容,避免上游改包装方式时把面板打空。
592
626
  const data = body?.latestVersion ? body : (body?.data || {});
627
+ // 技能本体挂在 data.skill 下,字段名与列表不同(displayName / summary_zh / summary /
628
+ // iconUrl),根层没有 name、description、homepage。只读根层会得到「标题退化成 slug、
629
+ // 介绍空白」(issue #4 实测)。下面逐字段回退到列表的旧字段名,两种形状都兼容。
630
+ const skill = data.skill && typeof data.skill === "object" ? data.skill : {};
631
+ const handle = String(data.namespace?.handle || namespace);
593
632
  const reports = data.securityReports && typeof data.securityReports === "object" ? data.securityReports : {};
594
633
  const reportStatus = Object.values(reports).map((report) => String(report?.status || "")).filter(Boolean);
595
634
  return {
596
635
  slug,
597
- namespace,
598
- name: String(data.name || slug),
636
+ namespace: handle,
637
+ name: String(skill.displayName || data.name || slug),
599
638
  version: String(data.latestVersion?.version || ""),
600
639
  changelog: String(data.latestVersion?.changelog || ""),
601
- description: String(data.description_zh || data.description || ""),
640
+ description: String(skill.summary_zh || skill.summary || data.description_zh || data.description || ""),
641
+ iconUrl: String(skill.iconUrl || data.iconUrl || ""),
642
+ ownerName: String(data.owner?.displayName || data.namespace?.displayName || ""),
602
643
  publisher: String(data.publisher?.name || data.owner?.displayName || ""),
603
644
  verifiedPublisher: !!data.publisher?.verified,
604
645
  certifiedName: String(data.publisher?.certifiedName || ""),
605
646
  security: reportStatus.length ? reportStatus : ["unknown"],
606
- homepage: String(data.homepage || ""),
647
+ // 详情根层没有 homepage;公开页由 namespace + slug 拼,绝不把 api.skillhub.cn 交出去。
648
+ homepage: skillhubHomepage(data.homepage || skill.homepage, handle, slug),
649
+ // 详情缺 createdAt 时不回 0:列表项自带的上架时间会被 0 覆盖掉。
650
+ ...(Number(skill.createdAt) ? { createdAt: Number(skill.createdAt) } : {}),
651
+ category: String(skill.category || ""),
652
+ source: String(skill.source || ""),
607
653
  };
608
654
  }
609
655
  export async function wbSkillFiles(payload = {}) {
@@ -434,6 +434,30 @@ export const WHATS_NEW_EN = {
434
434
  where: 'Skill board',
435
435
  },
436
436
  ],
437
+ '2.4.1': [
438
+ {
439
+ text: '**Skill details no longer go blank and titles no longer fall back to the slug**: SkillHub\'s detail endpoint keeps the name and summary under the `skill` object (`displayName` / `summary_zh`), while the old mapping only read the root level — the title became the slug and the summary was empty. It now falls back `summary_zh -> summary -> legacy fields` and merges the list item\'s existing name, summary and created time into the detail; empty strings from the response no longer overwrite what is already on screen, and the list summary stays visible while the detail loads (issue #4)',
440
+ where: 'Market -> Skills -> detail',
441
+ },
442
+ {
443
+ text: '**"Source homepage" now opens a readable page**: upstream `homepage` is actually the `api.skillhub.cn` JSON endpoint (405 in a browser). It is now rewritten to the public `https://skillhub.cn/<namespace>/<slug>` page, with a second guard at render time so the API host is never handed to the browser (issue #4)',
444
+ where: 'Market -> Skills -> Source homepage',
445
+ },
446
+ {
447
+ text: '**The floating button can be hidden and steps aside on the board**: right-click (or a 0.5 s long-press on touch) opens a Hide action with a hint on how to bring it back from the top-left entry or the "Entries" menu; a new "panel + sidebar entry" mode keeps at least two entry points. The button hides automatically while the board occupies the center panel and returns when you go back to the conversation (issue #4)',
448
+ where: 'Floating button / Panel -> Entries',
449
+ },
450
+ ],
451
+ '2.4.2': [
452
+ {
453
+ text: '**Documentation release**: the README (Chinese and English) now matches what actually ships — the three entry seats with six modes, hiding the floating button by right-click or long-press, how it steps aside while the board occupies the centre column, and the 2.4.1 SkillHub detail fixes. Runtime code is identical to 2.4.1; nothing changes in behaviour',
454
+ where: 'Project README',
455
+ },
456
+ {
457
+ text: '**A new FAQ entry**: if the detail dialog is still blank after upgrading, confirm the installed package version and then quit DSH completely and start it again — the market field mapping lives in host-side modules loaded once at process start, so reloading the panel is not enough',
458
+ where: 'README -> FAQ',
459
+ },
460
+ ],
437
461
  };
438
462
 
439
463
  /**
package/lib/whats-new.js CHANGED
@@ -499,4 +499,34 @@ export const WHATS_NEW = [
499
499
  },
500
500
  ],
501
501
  },
502
+ {
503
+ version: '2.4.1',
504
+ items: [
505
+ {
506
+ text: '**技能详情不再空白、标题不再退化成 slug**:SkillHub 详情接口把名称与介绍放在 `skill` 子对象(`displayName` / `summary_zh`),旧映射只读根层,于是标题变 slug、介绍全空。现在按 `summary_zh → summary → 旧字段` 逐级回退,并把列表项已有的中文名、介绍、上架时间合并进详情——回包里的空串不再覆盖已显示的内容,加载中也先显示列表里的介绍(issue #4)',
507
+ where: '市场 → 技能 → 详情',
508
+ },
509
+ {
510
+ text: '**「来源主页」改开可读页面**:上游 `homepage` 实际是 `api.skillhub.cn` 的 JSON 接口(浏览器打开 405)。现在统一改写成 `https://skillhub.cn/<namespace>/<slug>` 公开页,渲染层再拦一道,接口域名不会再被当成网页交出去(issue #4)',
511
+ where: '市场 → 技能 → 来源主页',
512
+ },
513
+ {
514
+ text: '**悬浮按钮可隐藏,看板打开时自动收起**:右键(触屏长按 0.5 秒)悬浮按钮可隐藏并提示从左上角或「入口」菜单重开;新增「面板 + 侧栏底部入口」档位,隐藏后仍保留至少两个入口。看板占据中栏时按钮自动让位,回到会话再恢复(issue #4)',
515
+ where: '悬浮按钮 / 面板 → 入口',
516
+ },
517
+ ],
518
+ },
519
+ {
520
+ version: '2.4.2',
521
+ items: [
522
+ {
523
+ text: '**文档版**:README(中 / 英)与实机功能对齐——三个入口席位与六档切换、悬浮按钮右键 / 长按隐藏、看板占据中栏时自动让位,以及 2.4.1 的 SkillHub 详情修正都写进了说明;运行代码与 2.4.1 完全相同,本次没有任何行为变化',
524
+ where: '项目主页 README',
525
+ },
526
+ {
527
+ text: '**新增一条常见问题**:升级后详情页仍空白时,先确认已安装的包版本,再完全退出 DSH 重新启动——市场的字段映射在宿主侧模块里,只在进程启动时加载一次,只刷新面板不会生效',
528
+ where: 'README → 常见问题',
529
+ },
530
+ ],
531
+ },
502
532
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-armory",
3
- "version": "2.4.0",
3
+ "version": "2.4.2",
4
4
  "description": "Unified Skill + MCP + Expert + Plugin board for DeepSeek Harness (desktop app and Web GUI) — custom skill suites, skill install/uninstall with live refresh, global panel page, JSON import, WorkBuddy connector & SkillHub & Expert & DSH community plugin market installs, MCP OAuth authorization, tool-usage tailor with wire-tax trends, and profile-backed Host Remote management.",
5
5
  "repository": {
6
6
  "type": "git",