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 +23 -5
- package/README.md +25 -8
- package/lib/client.js +233 -13
- package/lib/wb-market.js +76 -30
- package/lib/whats-new-en.js +24 -0
- package/lib/whats-new.js +30 -0
- package/package.json +1 -1
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.
|
|
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 **
|
|
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
|
-
- **
|
|
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
|
-
|
|
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
|
|
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.
|
|
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**,侧栏顶部「全局面板」里点
|
|
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
|
-
-
|
|
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
|
-
-
|
|
181
|
+
- **左侧栏底部入口**:展开态是 42px 的一行(图标 + 文字),侧栏收起成轨道时只剩一个圆钮。
|
|
174
182
|
|
|
175
183
|
面板浮在会话之上,关掉它(右上角「关闭」或 Esc)不会影响会话。
|
|
176
184
|
|
|
177
|
-
|
|
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 # 全
|
|
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`,依次执行 **
|
|
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 ·
|
|
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 =
|
|
9464
|
-
const homeUrl =
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
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
|
-
}, [
|
|
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
|
-
|
|
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
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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
|
|
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 = {}) {
|
package/lib/whats-new-en.js
CHANGED
|
@@ -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.
|
|
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",
|