dsh-toolbox-ui 1.6.2 → 1.6.4

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 dsh-mcp-manager-ui contributors
3
+ Copyright (c) 2026 dsh-toolbox-ui contributors
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.en.md CHANGED
@@ -1,203 +1,152 @@
1
- # dsh-toolbox-ui
2
-
3
- [中文](README.md) | English
4
-
5
1
  <p align="center">
6
- <a href="https://linux.do/" title="LINUX DO"><img src="docs/images/linux-do-logo.svg" alt="LINUX DO" width="40" height="40"></a>
2
+ <img src="docs/images/logo.svg" width="104" height="104" alt="dsh-toolbox-ui">
7
3
  </p>
8
4
 
5
+ <h1 align="center">dsh-toolbox-ui</h1>
6
+
7
+ <p align="center"><b>MCP management panel</b> + <b>skills manager</b> for DeepSeek Harness Web</p>
8
+
9
9
  <p align="center">
10
- <a href="https://github.com/Imzl-zl/dsh-mcp-manager-ui/actions/workflows/ci.yml"><img src="https://github.com/Imzl-zl/dsh-mcp-manager-ui/actions/workflows/ci.yml/badge.svg" alt="ci"></a>
11
10
  <a href="https://www.npmjs.com/package/dsh-toolbox-ui"><img src="https://img.shields.io/npm/v/dsh-toolbox-ui" alt="npm"></a>
12
11
  <a href="https://www.npmjs.com/package/dsh-toolbox-ui"><img src="https://img.shields.io/npm/dm/dsh-toolbox-ui" alt="downloads"></a>
12
+ <a href="https://github.com/Imzl-zl/dsh-toolbox-ui/actions/workflows/ci.yml"><img src="https://github.com/Imzl-zl/dsh-toolbox-ui/actions/workflows/ci.yml/badge.svg" alt="ci"></a>
13
13
  <a href="./LICENSE"><img src="https://img.shields.io/npm/l/dsh-toolbox-ui" alt="license"></a>
14
- <img src="https://img.shields.io/badge/DSH-%3E%3D0.1.5--rc.1-blue" alt="DSH">
14
+ <img src="https://img.shields.io/badge/DSH-%E2%89%A50.1.5--rc.1-blue" alt="DSH >= 0.1.5-rc.1">
15
15
  </p>
16
16
 
17
- An **MCP management panel and skills manager** for DeepSeek Harness Web: click the floating button (bottom-right) or the sidebar **MCP** entry to inspect connection status, add servers, and edit configuration. The panel floats above the session — **it never interrupts an ongoing conversation**. A capability switch at the top of the panel toggles between MCP and **skills**: see each installed skill's real scope, enable/disable or remove it, diagnose why a skill is not taking effect. The skills market is backed by the external **[skillhub.cn](https://skillhub.cn)** source: browse by category and search, with publisher certification and security-scan results disclosed before install plus an **explicit confirmation** (validated server-side). Upstream entries carry no license information, so this plugin makes **no license judgement** of its own rather than inventing one. See [skills manager](docs/skills.md) (Chinese).
18
-
19
- > In-depth design notes (shared-connection model, connection-state semantics, known limits) are currently Chinese-only: [docs/design.md](docs/design.md).
17
+ Click the floating button or the sidebar's "Toolbox" entry: the panel floats above the session, so changing configuration or reading skills never interrupts the conversation you are in. Two ability faces live in the same panel — **MCP** and **skills**.
20
18
 
21
19
  ## Screenshots
22
20
 
23
- ### Global panel
24
-
25
- ![MCP management panel](docs/images/mcp-manager-overview.png)
21
+ ### MCP market
26
22
 
27
- ### Project scope (`.dsh/mcp.json`)
23
+ ![MCP market](docs/images/mcp-market.png)
28
24
 
29
- ![Project MCP](docs/images/mcp-manager-workspace.png)
25
+ 47 services, each verified against first-party sources, across 10 categories. Search-first, with a "keyless only" filter and sorting. Every entry states its free tier honestly and what you must supply (key / connection details / arguments to edit after install); one click installs from the card, and already-configured entries are recognised and skipped.
30
26
 
31
- ### Connection details and adding a server
27
+ ### Skills pane
32
28
 
33
- ![MCP connection details](docs/images/mcp-manager-detail.png)
29
+ ![Skills pane](docs/images/skills-overview.png)
34
30
 
35
- ![Add MCP](docs/images/mcp-manager-add.png)
31
+ See every installed skill's **real scope** and cross-scope shadowing, enable/disable, remove, read the body and diagnostics — and flip **model auto-invocation** with one switch, which edits `disable-model-invocation` in `SKILL.md` in place, touching only that one line.
36
32
 
37
33
  ## Features
38
34
 
39
- - Inspect every MCP server's status, transport, connection parameters, and tool list; expand a tool to see its full input JSON Schema (required/optional, types, enums, defaults, raw JSON)
40
- - Filter by transport (HTTP/stdio) and status, search by name/command/URL
41
- - Enable, disable, reconnect, add, edit, remove; reveal masked credentials temporarily with the eye button and copy with one click
42
- - **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
43
- - **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)
44
- - **MCP market**: 47 services across 10 categories (search, dev, security & reverse engineering, data, AI & knowledge, browser automation, cloud & infra, productivity, files, comms), each verified against first-party sources. Search-first, with category and "keyless only" filters, sorting, and one-click install from the card; every entry states its free tier honestly and what you must supply (key / connection details / arguments to edit after install). Already-configured entries are detected and skipped, never overwritten
45
- - Import MCP configuration from other clients installed on this machine: Claude Code, Codex (`config.toml`), OpenCode (including `.jsonc`), pi, Claude Desktop, Cursor, Windsurf, VS Code, Gemini CLI, Roo Code, and the cross-tool shared config are detected and listed, and one click imports from any of them; you can also paste `mcpServers` JSON from Claude, Cursor, Cline, or Roo, or `servers` JSON from VS Code. Everything is previewed before writing, with "merge" and "replace" modes
46
- - **Skills pane** (the toolbox's second ability): see every installed skill's **real scope** and cross-scope shadowing, enable/disable (`.disabled`), remove, read the body and diagnostics, and **flip "model auto-invocation" with one switch** — it edits `disable-model-invocation` in `SKILL.md` in place, touching only that one line, and the state always comes from the file itself (unreadable frontmatter reads as "unknown", never a guess); the market is backed by skillhub.cn with an honest disclosure and an explicit acknowledgement before install. See [Skills](docs/skills.md)
47
- - **Moving servers between workspaces**: copy this project's MCP servers into another registered workspace in one go (done inside the Host, so credentials never pass through the browser), or export the whole project configuration as JSON to download / copy (same shape as `.dsh/mcp.json`, ready to drop into another workspace). See [Moving servers between workspaces](#moving-servers-between-workspaces)
48
- - Follows the DSH dark/light theme and adapts to narrow/mobile widths
49
- - Update notice and **one-click upgrade**: a dismissible banner when a newer release exists (at most one check per day; disable with `DSH_MCP_MANAGER_DISABLE_UPDATE_CHECK`). Nothing happens until you press Upgrade — it **never auto-updates**. The upgrade runs through DSH's official pluginManager and tells you to restart the host when done. A local dev copy installed via `link:` / `file:` / `github:` gets no button (upgrading it would replace your link with an npm package)
50
- - **What changed, said once**: after the version changes, the first time you open the panel a small card lists what's new and where to find it (e.g. that the "Entry" control sits in the panel header); confirming it never shows it again, and it uses no network and sends nothing
35
+ **MCP**
51
36
 
52
- ## Install
37
+ - Per-entry status, transport, connection settings and the **registered tools**; expand a tool for its full input JSON Schema (required/optional, types, enums, defaults)
38
+ - Enable, disable, reconnect, add, edit, remove; credential fields are masked by default — reveal the effective runtime value temporarily and copy it in one click
39
+ - **Global + project scopes**: global writes the Web profile (available to every project), project writes that project's `.dsh/mcp.json` (visible only to its sessions); a project can also **block** a global MCP
40
+ - **Import** configuration from other clients installed on this machine: Claude Code, Codex, OpenCode, pi, Claude Desktop, Cursor, Windsurf, VS Code, Gemini CLI, Roo Code and the cross-tool shared config — or paste JSON directly. Previewed before writing, with merge and replace modes
41
+ - **Move servers between workspaces**: copy this project's MCP servers into another registered workspace (done inside the Host, so credentials never pass through the browser), or export the whole project config as JSON
53
42
 
54
- ```sh
55
- # Recommended: install from npm — `dsh plugin update` then upgrades within ^1.x automatically
56
- dsh plugin --profile web add dsh-toolbox-ui@^1.6.2
43
+ **Skills**
57
44
 
58
- # Or pin a GitHub release tag (no automatic upgrades; re-add with a new tag to upgrade)
59
- dsh plugin --profile web add github:Imzl-zl/dsh-mcp-manager-ui#v1.6.2
60
- ```
45
+ - Real scope and cross-scope shadowing: the same name in different scopes is a legal configuration; the panel lists each copy and marks which one wins
46
+ - Enable/disable (`.disabled`), remove, read the body and diagnostics — the reason a skill "does not take effect" is right there in the panel
47
+ - **Model auto-invocation switch**: edits `disable-model-invocation` in `SKILL.md`; turning it off deletes that line (off → on round-trips byte-for-byte). The state always comes from the skill file itself — when it cannot be read the panel says "unknown" instead of guessing
48
+ - Market backed by [skillhub.cn](https://skillhub.cn): publisher verification and security-scan findings are disclosed before install, followed by one **explicit confirmation** (validated server-side)
61
49
 
62
- Then **restart `dsh web`**. To upgrade:
50
+ **Shared**
51
+
52
+ - Pick one or both entry points: the floating button (draggable, position remembered) and the sidebar "Toolbox" entry — at least one stays on
53
+ - Follows the DSH dark/light theme and adapts to narrow widths
54
+ - Update notice and one-click upgrade: at most one npm check per day, dismissible, **never auto-updates**; after an upgrade it explains what changed, without any network request
55
+
56
+ ## Install
63
57
 
64
58
  ```sh
65
- dsh plugin --profile web update dsh-toolbox-ui
59
+ # Recommended: from npm — `dsh plugin update` then upgrades within ^1.x
60
+ dsh plugin --profile web add dsh-toolbox-ui@^1.6.4
61
+
62
+ # Or pin a GitHub release tag (no automatic upgrades; re-add with a new tag)
63
+ dsh plugin --profile web add github:Imzl-zl/dsh-toolbox-ui#v1.6.4
66
64
  ```
67
65
 
68
- To uninstall (your configured MCP entries stay in place):
66
+ Then **restart `dsh web`**. To uninstall (your existing MCP entries stay):
69
67
 
70
68
  ```sh
71
69
  dsh plugin --profile web remove dsh-toolbox-ui
72
70
  ```
73
71
 
74
- Details, local development installs, and troubleshooting: [docs/installation.md](docs/installation.md) (Chinese).
75
-
76
- > Do not add `mcp-manager-ui` to the profile's `cordis.patch.yml`, an agent preset, or an extra `--patch` file by hand — the plugin command already does that, and duplicating it fails loudly on purpose.
77
-
78
- ## Usage
72
+ > Do not add `mcp-manager-ui` to the profile's `cordis.patch.yml`, an agent preset, or an extra `--patch` file by hand — the plugin command already does it, and assembling it twice fails loudly.
79
73
 
80
- ### Opening the panel
74
+ ## Upgrade
81
75
 
82
- Two entry points, one panel:
76
+ ```sh
77
+ dsh plugin --profile web update dsh-toolbox-ui
78
+ ```
83
79
 
84
- - **Floating button (bottom-right)**: draggable, position remembered;
85
- - **Sidebar "MCP"**: above "Settings" when expanded, icon-only in the 56px rail.
80
+ When a newer version exists the panel shows a dismissible banner; pressing Upgrade runs DSH's official pluginManager.
86
81
 
87
- The panel floats above the session; closing it ("Close" or Esc) does not affect the conversation.
82
+ > ⚠️ **1.6.1 and earlier will not announce 1.6.2 or anything after it.** Their update check read this repository's release feed, whose only `latest` entry is held by the legacy package `dsh-mcp-manager-ui` (frozen at 1.5.5) — so it always answered "up to date". 1.6.2 fixed that by querying **this package's latest on npm**, and the fix can only ship inside a newer version: upgrade once by hand, then the banner works again.
83
+ >
84
+ > ```sh
85
+ > dsh plugin --profile web update dsh-toolbox-ui
86
+ > ```
87
+ >
88
+ > Still on the legacy package (≤1.5.3)? Use the **migration** path: press Upgrade in the panel to reach 1.5.5, then press "Migrate" (it installs this package and removes the old one). See [Install & upgrade](docs/installation.md).
88
89
 
89
- Both entries can be turned off independently: the panel header's "Entry" control offers `Floating button + sidebar` (default), `Floating button only`, or `Sidebar only`. They **cannot both be hidden** — the panel is only reachable through these two entries, so this is a three-way choice, not two independent switches. The choice is stored in the browser (localStorage), so a different browser or a cleared cache starts from the default again.
90
+ ## Quick start
90
91
 
91
- ### Global vs project
92
+ **Open the panel**: the floating button at the bottom right, or "Toolbox" at the bottom of the sidebar. It floats above the session; Esc or "Close" hides it without touching the session.
92
93
 
93
- Tabs switch between "Global" and each project:
94
+ **Two scopes**:
94
95
 
95
- | Tab | Stored in | Visible to | When changes take effect |
96
+ | Tab | Stored in | Visible to | Takes effect |
96
97
  |---|---|---|---|
97
- | Global | The Web profile's `cordis.patch.yml` | every project | DSH hot-reloads, usually immediately |
98
- | A project | That project's `.dsh/mcp.json` | only that project's sessions | the next newly created connection (see below) |
98
+ | Global | the Web profile's `cordis.patch.yml` | every project | DSH hot-reloads, usually immediately |
99
+ | A project | that project's `.dsh/mcp.json` | only that project's sessions | **on write** — no need to reopen the session or restart the Host |
99
100
 
100
- `serverName` is scoped: different projects, and global vs project, **may reuse the same name** with independent connections; duplicates inside one scope are rejected.
101
+ `serverName` is unique per scope: different projects, and global vs. project, **may share a name** and connect independently.
101
102
 
102
- ### Common actions
103
-
104
- From a row or the detail pane:
105
-
106
- - **Toggle**: enable / disable (global writes the profile patch's `disabled`, project writes `.dsh/mcp.json`);
107
- - **Reconnect**: global only (project connections are held by sessions and `mcp-client` reconnects them on its own);
108
- - **Edit / Remove**: removal only affects the current scope;
109
- - **Hide** (a global MCP, from a project tab): writes `exclude` so that project's new sessions no longer see it — the global instance keeps running, so other projects are unaffected.
110
-
111
- ### Adding a server
112
-
113
- Three ways, all from the panel toolbar:
114
-
115
- 1. **Add MCP**: fill in `command`/`args`/`env`/`cwd` (stdio) or `url`/`headers` (HTTP), optionally set the call timeout, startup-failure policy, and reconnect policy; presets are available for common servers.
116
- 2. **Import JSON**: paste another client's configuration, preview, then write. "Merge (update by name)" and "replace" modes; field mapping and limits in [docs/json-import.md](docs/json-import.md) (Chinese).
117
- 3. **Built-in MCP**: tick the servers you want (Exa, Tavily, Firecrawl, Chrome DevTools, Playwright). The first three work without a key and need no local dependencies; the last two are local tools requiring Node.js and a browser.
118
-
119
- ### Moving servers between workspaces
120
-
121
- Two entries sit next to the "This project's MCP" heading in a project tab (both apply to the current project only):
122
-
123
- - **Copy to…**: tick the servers to move (all selected by default), pick a target workspace, and write them in one go. The copy happens inside the Host, so configuration and credentials never pass through the browser. When the target already has the same name the entry is **skipped by default** (the target's own configuration wins); you can explicitly choose "overwrite same names". The result names exactly what was added, overwritten, or skipped. The target's `exclude` list is not carried over — that is the target project's own visibility decision, not configuration data.
124
- - **Export**: turn this project's configuration into JSON (same shape as `.dsh/mcp.json`) that you can copy or download — for backups, moving to another machine, or sharing. **The export is the full configuration**: literal credentials (tokens in URLs, literal header / env values) appear in it verbatim, so treat it as a secret. Locations written as `${VAR}` keep the reference form and are never resolved to plaintext. The file can be dropped into another workspace's `.dsh/mcp.json`, or pasted back through "Import MCP" (pasting reads `mcpServers` only; `exclude` has to be set by hand).
125
-
126
- The target workspace must be registered with DSH (i.e. it appears as a tab in the panel) — "Copy to…" does not accept an arbitrary directory, and that limit is enforced by the Host, not just hidden in the UI.
127
-
128
- ### Tools and credentials
129
-
130
- The detail pane lists the tools a server currently registers; expand one for its full input schema. Credential fields are masked by default — the eye button asks the Host for the **effective runtime value** (environment references already resolved) and shows it temporarily, with one-click copy. If you do not actually edit a field, saving keeps the original reference instead of writing secrets back into the configuration file.
131
-
132
- ## When configuration changes take effect (important)
133
-
134
- | Scope | When a change takes effect |
135
- |---|---|
136
- | Global | DSH hot-reloads it, usually immediately (including running sessions) |
137
- | Project | **As soon as it is written**: an added server mounts into live sessions, a removed one retracts its tools, and a changed one reloads in place (tool names stay the same) |
138
-
139
- Project MCPs are shared by every session of that project and configured in `.dsh/mcp.json`. Saving in the panel aligns the running sessions immediately — **no need to reopen a session and no Host restart**; editing the file by hand is picked up at the **next session mount**.
140
-
141
- > **The connection follows the configuration, not the sessions**: when the last session ends the connection is not torn down — it goes idle and the next session reuses it, so it is already up when you need it (this is also the host's own contract: tools must appear before the first turn). Idle recycling defaults to 5 minutes; `idleTimeoutMs` tunes it and `0` means never recycle automatically.
142
- >
143
- > **How to read the state**: a project row states two independent facts instead of merging them.
144
- > - **Is the connection there**: `已连接 · 当前无会话使用(连接保留,下次直接用)` / `已连接 · N 个会话在用` / `连接中…` / `连接失败` / `尚未建立连接(新会话自动挂载)`.
145
- > - **How many sessions use it**: the `N 个会话在用` part above. The old "待会话挂载" wording merged both into one phrase and read like "not connected yet".
146
- >
147
- > **Reconnect is a required exit**: mcp-client has a bounded reconnect budget and, once exhausted, it unregisters the tools and stops until the configuration is reloaded or the harness restarts (its README says so). A project MCP is not a resident service, so the panel's "reconnect" button tears the old connection down, rebuilds it from the current configuration, and re-projects it into live sessions.
103
+ **Adding MCP**: use "Add MCP" for the form, or "Import MCP" to paste another client's configuration (previewed before writing). Common servers ship with presets.
148
104
 
149
105
  ## FAQ
150
106
 
151
- **The panel says a server is "disabled", but the model reports `unknown tool "mcp__xxx__..."`.**
152
- That server is disabled, so it registers no tools. Enable it in the panel (global writes the profile patch and hot-reloads; project writes `.dsh/mcp.json` and applies to new sessions).
107
+ **When does a change in a project take effect?** The moment you save: new entries attach to live sessions, removed ones retract their tools, edits reload the connection in place — no need to reopen the session or restart the Host. Editing `.dsh/mcp.json` by hand is picked up when the next session mounts.
153
108
 
154
- **A row shows "mount failed", "connection failed", or "scope isolation failed" — what is the difference?**
155
- Three different things, and the detail pane gives the specific reason: **mount failed** = the plugin could not mount it during session setup (a `${VAR}` resolved to empty, startup failed under `failOnStartupError`, tool registration was rejected); **connection failed** = `mcp-client` could not connect or exhausted its reconnect budget (the reason comes from its logs); **scope isolation failed** = the connection is fine but the tools are not in the shared scope layer (two copies of `@deepseek-ai/dsh-scope` ended up loaded in the Host). See [docs/design.md](docs/design.md) (Chinese) for the diagnosis.
109
+ **The connection follows the configuration, not the session.** When the last session ends the connection is not torn down; it goes idle and the next session reuses it, so it is already there when you need it. Two knobs live on the server entry: `idleTimeoutMs` (how long an idle connection is kept, default 5 minutes, `0` = never reclaim) and `readyTimeoutMs` (the total budget for waiting until ready on mount, default 5s, `0` = do not wait). See [Design & limits](docs/design.md#项目级专有字段dshmcpjson-里每个-server-可写).
156
110
 
157
- **What do "waiting for a session", "pending configuration", and "N sessions sharing" on a project row mean?**
158
- Respectively: no session of that project holds the shared connection yet / the configuration changed but the old connection is still being reused (see above) / how many sessions currently share it.
111
+ **What do the project row's status strings mean?** They separate two independent facts: **whether a connection exists** (`已连接 · 当前无会话使用(连接保留,下次直接用)` / `已连接 · N 个会话在用` / `连接中…` / `连接失败` / `尚未建立连接(新会话自动挂载)`) and **how many sessions are using it** — the old "waiting for a session to mount" wording merged both into one sentence and read like "not connected yet".
159
112
 
160
- **How do I upgrade? Why did the panel not notify me?**
161
- If you installed from npm, `dsh plugin --profile web update dsh-toolbox-ui` upgrades within `^1.x` — or just press Upgrade on the panel banner, which does the same thing without the command. The banner only queries the npm registry (this package's latest dist-tag), at most once a day, is dismissible, and never acts on its own. Tag-pinned installs never cross versions — re-add with the new tag. ⚠️ **1.6.1 and earlier asked a different source** (this repository's release feed, whose only latest entry carries the other product line and is marked pre-release), so they will not announce 1.6.2 — the fix ships inside 1.6.2 itself; run `dsh plugin --profile web update dsh-toolbox-ui` once by hand.
113
+ **The panel says "disabled" but the model reports `unknown tool "mcp__xxx__…"`.** That MCP is disabled, so it registers no tools. Enable it in the panel.
162
114
 
163
- **Will editing touch my other configuration?**
164
- No. Global edits only rewrite the entries declared by `@deepseek-ai/dsh-mcp-client` that this plugin manages, preserving other plugin rows, comments, and `!!js` expressions; "replace" imports only replace that part and never delete MCPs owned by another bundle or agent preset.
115
+ **How do I tell "mount failed", "connection failed" and "scope isolation failed" apart?** Three different things: **mount failed** = it never attached during session mount; **connection failed** = `mcp-client` cannot connect or ran out of retries; **scope isolation failed** = the connection is fine but its tools are not in the shared scope layer. The detail pane gives the specific reason; see [Design & limits](docs/design.md#连接状态语义).
165
116
 
166
- **Does uninstalling remove my configured MCPs?**
167
- No. Uninstalling the plugin leaves existing `@deepseek-ai/dsh-mcp-client` rows in the profile untouched, and a project's `.dsh/mcp.json` stays where it is (it is an ordinary JSON file).
117
+ **Will it touch my other configuration?** No. Global edits only touch the entries declared by `@deepseek-ai/dsh-mcp-client` and managed by this plugin, preserving other plugin entries, comments and `!!js` expressions. Uninstalling leaves your existing MCP entries and project `.dsh/mcp.json` alone.
168
118
 
169
119
  ## Compatibility
170
120
 
171
121
  | Item | Verified |
172
122
  |---|---|
173
- | DeepSeek Harness | `0.1.5-rc.1` and above (verified through `0.2.0-rc.2`) |
174
- | Node.js | whatever runtime DSH ships/supports |
175
- | Platform | Windows; Linux/macOS share the same DSH Web contracts |
123
+ | DeepSeek Harness | `0.1.5-rc.1` and above (verified up to `0.2.0-rc.2`) |
124
+ | Node.js | the runtime DSH ships with / supports |
125
+ | Platform | Windows; Linux and macOS use the same DSH Web contract |
176
126
 
177
- Only 0.1.5 and above are supported (earlier versions lack "`serverName` uniqueness per registration scope" and "`setup` receives the agent"); there are no fallback branches for older versions. Rationale and dependency details: [docs/design.md](docs/design.md) (Chinese).
127
+ Requires 0.1.5 or newer (older versions lack per-scope `serverName` uniqueness and the `agent` passed to setup); no downgrade branches are kept for older hosts.
178
128
 
179
129
  ## Development
180
130
 
181
131
  ```sh
182
- npm test # full suite (includes real-host integration tests)
183
- dsh --profile web --dump-config # inspect the effective profile configuration
184
- dsh web # run the Web UI for real verification
132
+ npm test # full suite, including host-level integration tests (real cordis / dsh-tools / dsh-scope / dsh-mcp-client + a real stdio MCP child process)
133
+ dsh web # start the Web app for hands-on verification
185
134
  ```
186
135
 
187
- Besides fixture-based tests (which check internal consistency), `npm test` includes **real-host integration tests** that run genuine `cordis` + `dsh-tools` + `dsh-scope` + `dsh-mcp-client` plus a real stdio MCP child process, covering "two sessions of one project share a single connection, tools are projected into each session, nothing leaks into the global view, and the child process exits with the last session". CI (push to `main` / pull requests, and tag pushes for releases) runs the same gate; see [docs/installation.md](docs/installation.md#发布流程维护者) (Chinese) for the release flow.
188
-
189
136
  ## Documentation
190
137
 
191
- - [Installation and upgrades](docs/installation.md) (Chinese)
192
- - [JSON import](docs/json-import.md) (Chinese)
193
- - [Design and limits](docs/design.md) (Chinese)
194
- - [Official DeepSeek Harness plugin publishing guide](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/basic/publish.md)
138
+ - [Install & upgrade](docs/installation.md) — three install flavours, release flow, troubleshooting
139
+ - [Skills](docs/skills.md) — scopes and shadowing, enable/disable, invocation policy, the skill market
140
+ - [JSON import](docs/json-import.md) — field mapping and limits
141
+ - [Design & limits](docs/design.md) — shared connection model, connection-state semantics, known limits, dependency details
142
+ - [DeepSeek Harness plugin publishing guide](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/basic/publish.md)
195
143
 
196
- ## Links
144
+ ## Friend link
197
145
 
198
- - [LINUX DO](https://linux.do/)
199
- - [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)
200
- - [GitHub `dsh-plugin` topic](https://github.com/topics/dsh-plugin)
146
+ <p align="center">
147
+ <a href="https://linux.do/" title="LINUX DO"><img src="docs/images/linux-do-logo.svg" alt="LINUX DO" width="44" height="44"></a><br>
148
+ <a href="https://linux.do/">LINUX DO</a>
149
+ </p>
201
150
 
202
151
  ## License
203
152
 
package/README.md CHANGED
@@ -1,168 +1,120 @@
1
- # dsh-toolbox-ui
2
-
3
- 中文 | [English](README.en.md)
4
-
5
1
  <p align="center">
6
- <a href="https://linux.do/" title="LINUX DO"><img src="docs/images/linux-do-logo.svg" alt="LINUX DO" width="40" height="40"></a>
2
+ <img src="docs/images/logo.svg" width="104" height="104" alt="dsh-toolbox-ui">
7
3
  </p>
8
4
 
5
+ <h1 align="center">dsh-toolbox-ui</h1>
6
+
7
+ <p align="center">DeepSeek Harness Web 的 <b>MCP 管理面板</b> + <b>技能管理面板</b></p>
8
+
9
9
  <p align="center">
10
- <a href="https://github.com/Imzl-zl/dsh-mcp-manager-ui/actions/workflows/ci.yml"><img src="https://github.com/Imzl-zl/dsh-mcp-manager-ui/actions/workflows/ci.yml/badge.svg" alt="ci"></a>
11
10
  <a href="https://www.npmjs.com/package/dsh-toolbox-ui"><img src="https://img.shields.io/npm/v/dsh-toolbox-ui" alt="npm"></a>
12
11
  <a href="https://www.npmjs.com/package/dsh-toolbox-ui"><img src="https://img.shields.io/npm/dm/dsh-toolbox-ui" alt="downloads"></a>
12
+ <a href="https://github.com/Imzl-zl/dsh-toolbox-ui/actions/workflows/ci.yml"><img src="https://github.com/Imzl-zl/dsh-toolbox-ui/actions/workflows/ci.yml/badge.svg" alt="ci"></a>
13
13
  <a href="./LICENSE"><img src="https://img.shields.io/npm/l/dsh-toolbox-ui" alt="license"></a>
14
- <img src="https://img.shields.io/badge/DSH-%3E%3D0.1.5--rc.1-blue" alt="DSH">
14
+ <img src="https://img.shields.io/badge/DSH-%E2%89%A50.1.5--rc.1-blue" alt="DSH >= 0.1.5-rc.1">
15
15
  </p>
16
16
 
17
- DeepSeek Harness Web 的 **MCP 管理面板 + 技能管理面板**:右下角悬浮按钮或侧栏「MCP」入口,点开就能看连接状态、加服务器、改配置——面板浮在会话之上,**不打断正在进行的对话**。面板顶部可切换能力层(MCP / 技能):技能面能看本机技能的真实作用域、启停与删除、诊断「为什么这个技能不生效」。技能市场接外部源 **[skillhub.cn](https://skillhub.cn)**:按分类浏览与搜索,安装前披露发布方认证与安全扫描结论、并要一次**显式确认**(该确认在服务端校验);上游条目没有许可信息,所以本插件**不做许可判定**,也不替它编一个出来。详见 [技能管理](docs/skills.md)。
17
+ 点右下角悬浮按钮或侧栏「工具箱」打开,面板浮在会话之上——改配置、看技能,都不打断正在进行的对话。两个能力面在同一个面板里切换:**MCP** 与**技能**。
18
18
 
19
19
  ## 界面预览
20
20
 
21
- ### 全局管理面板
21
+ ### MCP 市场
22
22
 
23
- ![MCP 管理面板](docs/images/mcp-manager-overview.png)
23
+ ![MCP 市场](docs/images/mcp-market.png)
24
24
 
25
- ### 项目作用域(`.dsh/mcp.json`)
25
+ 47 个逐条一手核实过的服务,分 10 类;搜索为主操作,配「只要免密钥」筛选与排序。每条如实标注免费额度、要准备什么(密钥 / 连接信息 / 装完还得改的参数),卡片直接安装;已配置的只识别并跳过。
26
26
 
27
- ![项目 MCP](docs/images/mcp-manager-workspace.png)
27
+ ### 技能面板
28
28
 
29
- ### 连接详情与新增
29
+ ![技能面板](docs/images/skills-overview.png)
30
30
 
31
- ![MCP 连接详情](docs/images/mcp-manager-detail.png)
31
+ 看本机每个技能的**真实作用域**与同名遮蔽,启停、删除、读正文与诊断,并一键切换「模型自动调用」——它直接改 `SKILL.md` 的 `disable-model-invocation`,只动那一行。
32
32
 
33
- ![新增 MCP](docs/images/mcp-manager-add.png)
33
+ ## 特性
34
34
 
35
- ## 功能
35
+ **MCP 面**
36
36
 
37
- - 查看每个 MCP 的状态、传输方式、连接参数与工具列表;展开工具可看完整输入 JSON Schema(必填/可选、类型、枚举、默认值与原始 JSON)
38
- - 按传输方式(HTTP/stdio)与状态筛选,按名称/命令/URL 搜索
39
- - 启用、禁用、重连、添加、编辑、移除;需要时用眼睛临时揭示被掩码的凭据并一键复制
40
- - **全局 + 项目双作用域**:全局写 Web profile,一次注册所有项目可用;项目写该项目 `.dsh/mcp.json`,只该项目会话可见;项目里还能「屏蔽」某个全局 MCP
41
- - **两个入口,同一个面板**:右下角悬浮按钮(可拖拽、记忆位置)与官方侧栏入口(展开态在「设置」上方显示「MCP」,收起成 56px 轨道时只剩图标)共享同一开关状态;面板右上角「入口」可以把两者只留一个(不能都关)
42
- - **MCP 市场**:47 个逐条一手核实过的服务,分 10 类(搜索、开发工具、安全与逆向、数据库与数据、AI 与知识、浏览器自动化、云与基础设施、效率协作、文件与本地、通讯)。搜索为主操作,配分类与「只要免密钥」筛选、排序、卡片直接安装;每条如实标注免费额度与需要准备什么(密钥 / 连接信息 / 装完还得改的参数)。已配置的只识别并跳过,不覆盖
43
- - **技能面**(工具箱的第二个能力面):看本机全部技能的**真实作用域**与同名遮蔽、启停(`.disabled`)、删除、读正文与诊断,并**一键切换「模型自动调用」**——直接改 `SKILL.md` 的 `disable-model-invocation`,只动那一行、不重排你的文件,状态一律取自文件本身(读不出来就说「未知」);市场接 skillhub.cn,安装前如实披露 + 一次显式确认。详见[技能管理](docs/skills.md)
44
- - 导入本机其他客户端的 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。写入前预览,支持「合并(同名更新)」与「替换」
45
- - **跨工作区搬运**:在项目标签页里把本项目的 MCP 一次勾选复制到另一个已注册的工作区(在宿主内完成,凭据不经过浏览器),或把整个项目配置导出成 JSON 下载 / 复制(与 `.dsh/mcp.json` 同格式,可直接放进另一个工作区)。详见[工作区之间复制与导出](#工作区之间复制与导出)
46
- - 跟随 DSH 深色/浅色主题,适配窄屏与移动宽度
47
- - 更新提示与**一键升级**:有新版时在面板顶部显示可关闭的提示条(每天最多查一次,可用 `DSH_MCP_MANAGER_DISABLE_UPDATE_CHECK` 关闭)。点了「升级」才动,**不会自动升级**;升级由 DSH 官方的 pluginManager 执行,装完提示重启宿主生效。用 `link:` / `file:` / `github:` 装的本地开发副本不给按钮(那会把链接换成 npm 包)
48
- - **升级后主动说一次变了什么**:版本变了后第一次打开面板弹一张小卡片,列出本次更新内容,并指路新功能在哪(例如「入口」开关在面板右上角);确认一次后不再提示,不联网、不发任何数据
37
+ - 逐条看状态、传输方式、连接参数与**已注册的工具**;展开工具可看完整输入 JSON Schema(必填/可选、类型、枚举、默认值)
38
+ - 启停、重连、添加、编辑、移除;凭据类字段默认掩码,点眼睛临时揭示有效运行值并一键复制
39
+ - **全局 + 项目双作用域**:全局写 Web profile(所有项目可用),项目写该项目 `.dsh/mcp.json`(只该项目会话可见);项目里还能「屏蔽」某个全局 MCP
40
+ - 从本机其他客户端**导入**:Claude Code、Codex、OpenCode、pi、Claude Desktop、Cursor、Windsurf、VS Code、Gemini CLI、Roo Code 与跨工具共享配置,也能直接粘贴 JSON;写入前预览,支持「合并」与「替换」
41
+ - **跨工作区搬运**:把本项目的 MCP 复制到另一个已注册工作区(在宿主内完成,凭据不经过浏览器),或导出成 JSON 备份 / 分享
49
42
 
50
- ## 安装
43
+ **技能面**
51
44
 
52
- ```sh
53
- # 推荐:npm 安装 —— 之后 `dsh plugin update` 会在 ^1.x 内自动升到最新小版本
54
- dsh plugin --profile web add dsh-toolbox-ui@^1.6.2
45
+ - 真实作用域与同名遮蔽:同名技能跨作用域是合法配置,面板逐条列出并标出哪一份生效
46
+ - 启停(`.disabled`)、删除、读正文与诊断——「技能不生效」的原因直接摆在面板里
47
+ - **「模型自动调用」开关**:直接改 `SKILL.md` 的 `disable-model-invocation`,关掉=删掉那一行(开→关能逐字节还原原文);状态一律取自技能文件本身,读不出来就说「未知」而不是猜一个值
48
+ - 市场接 [skillhub.cn](https://skillhub.cn):安装前披露发布方认证与安全扫描结论,并要一次**显式确认**(该确认在服务端校验)
55
49
 
56
- # 或固定 GitHub release tag(不会自动跨版本,升级要换 tag 重新 add)
57
- dsh plugin --profile web add github:Imzl-zl/dsh-mcp-manager-ui#v1.6.2
58
- ```
50
+ **公共**
59
51
 
60
- 装完**重启 `dsh web`**。升级:
52
+ - 入口三选一:右下角悬浮按钮(可拖拽、记忆位置)+ 侧栏「工具箱」;可以只留一个,不能都关
53
+ - 跟随 DSH 深色/浅色主题,适配窄屏与移动宽度
54
+ - 更新提示与一键升级:每天最多查一次 npm、可关闭、**绝不自动升级**;升级后弹一次「本次更新内容」,不联网
55
+
56
+ ## 安装
61
57
 
62
58
  ```sh
63
- dsh plugin --profile web update dsh-toolbox-ui
59
+ # 推荐:npm 安装 —— 之后 `dsh plugin update` 在 ^1.x 内自动升小版本
60
+ dsh plugin --profile web add dsh-toolbox-ui@^1.6.4
61
+
62
+ # 或固定 GitHub release tag(不自动跨版本,升级要换 tag 重新 add)
63
+ dsh plugin --profile web add github:Imzl-zl/dsh-toolbox-ui#v1.6.4
64
64
  ```
65
65
 
66
- 卸载(不会删除 profile 里已有的 MCP 条目):
66
+ 装完**重启 `dsh web`**。卸载(不会删除 profile 里已有的 MCP 条目):
67
67
 
68
68
  ```sh
69
69
  dsh plugin --profile web remove dsh-toolbox-ui
70
70
  ```
71
71
 
72
- 细节、本地开发安装与故障排查见 [安装与升级](docs/installation.md)。
73
-
74
72
  > 不要把 `mcp-manager-ui` 手工插进 profile 的 `cordis.patch.yml`、Agent preset 或额外的 `--patch` 文件——插件命令已经完成这件事,重复装配会明确报错。
75
73
 
76
- ## 使用
74
+ ## 升级
77
75
 
78
- ### 打开面板
79
-
80
- 两个入口,同一个面板:
76
+ ```sh
77
+ dsh plugin --profile web update dsh-toolbox-ui
78
+ ```
81
79
 
82
- - **右下角悬浮按钮**:可拖拽移动,位置会被记住;
83
- - **侧栏底部「MCP」**:展开态在「设置」上方,侧栏收起成 56px 轨道时只剩图标。
80
+ 面板顶部有新版时会显示可关闭的提示条,点「升级」走 DSH 官方的 pluginManager。
84
81
 
85
- 面板浮在会话之上,关掉它(右上角「关闭」或 Esc)不会影响会话。
82
+ > ⚠️ **1.6.1 及更早的版本不会提示 1.6.2 及之后的新版。** 它们的更新检查读的是本仓库的 release feed,而那条 feed 的 latest 被另一条产品线、即已冻结在 1.5.5 的旧包占着,于是永远显示「已是最新」。这个缺陷在 1.6.2 修掉了(改成查**本包在 npm 上的 latest**),修复只能装在新版里——所以请**手动升一次**,之后就恢复正常:
83
+ >
84
+ > ```sh
85
+ > dsh plugin --profile web update dsh-toolbox-ui
86
+ > ```
87
+ >
88
+ > 仍装着旧包 `dsh-mcp-manager-ui`(≤1.5.3)的话走**迁移**:面板点「升级」升到 1.5.5,再点「迁移」即可(它会装本包、卸掉旧包),详见[安装与升级](docs/installation.md)。
86
89
 
87
- 两个入口都能单独关掉:面板右上角「入口」里选 `悬浮按钮 + 侧栏入口`(默认)/ `仅悬浮按钮` / `仅侧栏入口`。**不能两个都关**——面板只能从这两个入口打开,所以这是一个三选一,而不是两个独立的开关。选择存在浏览器本地(localStorage),换浏览器或清缓存要重选一次。
90
+ ## 快速上手
88
91
 
89
- ### 全局 vs 项目
92
+ **打开面板**:右下角悬浮按钮,或侧栏底部「工具箱」。面板浮在会话之上,按 Esc 或点「关闭」收起,不影响会话。
90
93
 
91
- 顶部标签页在「全局」与各项目之间切换:
94
+ **两个作用域**:
92
95
 
93
96
  | 标签页 | 存哪 | 谁可见 | 改动何时生效 |
94
97
  |---|---|---|---|
95
98
  | 全局 | Web profile 的 `cordis.patch.yml` | 所有项目 | DSH 热加载,通常立即生效 |
96
- | 某项目 | 该项目 `.dsh/mcp.json` | 只该项目会话 | 写盘即生效(见下) |
99
+ | 某项目 | 该项目 `.dsh/mcp.json` | 只该项目会话 | **写盘即生效**,不需重开会话或重启 Host |
97
100
 
98
101
  `serverName` 按作用域判重:不同项目之间、全局与项目之间**可以同名**,各自独立连接;同一作用域内不能重名。
99
102
 
100
- ### 常用操作
101
-
102
- 在列表行或详情页里:
103
-
104
- - **开关**:启用 / 禁用(全局写 profile 补丁的 `disabled`,项目写 `.dsh/mcp.json`);
105
- - **重连**:全局条目重载 loader 条目;项目连接会拆掉旧连接、按当前配置重建并重新投射给存活会话(`mcp-client` 的重连预算耗尽后必须靠它恢复);
106
- - **编辑 / 移除**:移除只影响当前作用域;
107
- - **屏蔽**(项目页里对全局 MCP):写 `exclude`,该项目的新会话不再看到它——全局实例仍在跑,所以不影响其他项目。
108
-
109
- ### 添加 MCP
110
-
111
- 三条路,都从面板顶部工具栏进:
112
-
113
- 1. **添加 MCP**:表单填 `command`/`args`/`env`/`cwd`(stdio)或 `url`/`headers`(HTTP),可设调用超时、启动失败策略与重连策略;常用服务器有预设模板可一键套用。
114
- 2. **导入 JSON**:粘贴其他客户端的配置,先预览再写入。支持「合并(同名更新)」与「替换」,字段映射与限制见 [JSON 导入](docs/json-import.md)。
115
- 3. **内置 MCP**:勾选未配置的项一次装好(Exa、Tavily、Firecrawl、Chrome DevTools、Playwright)。前三项有免密额度、不需要本地依赖;后两项是本地工具,需要 Node.js 与浏览器。已配置的项只识别并跳过,不会覆盖。
116
-
117
- ### 工作区之间复制与导出
118
-
119
- 项目标签页里,「此项目的 MCP」标题右侧有两个入口(都只作用于当前项目):
120
-
121
- - **复制到…**:勾选要搬的 MCP(默认全选),选一个目标工作区。界面会先把这次的结果算给你看——**新增 / 覆盖 / 跳过**哪些条目(切换「跳过 / 覆盖」时立即重算),确认后才写过去。复制在宿主内完成,配置与凭据不经过浏览器。目标已有同名条目时**默认跳过**(保留目标现有配置),也可以显式选「覆盖目标同名」。结果会如实说出哪几条新增、哪几条覆盖、哪几条被跳过——不会只报一句「已复制」。目标工作区的 `exclude`(屏蔽了哪些全局 MCP)不跟着搬:那是那个项目自己的意图,不是配置数据。
122
- - **导出**:把本项目配置导出成 JSON(与 `.dsh/mcp.json` 同格式),可复制或下载成文件。用于备份、跨机器迁移或分享给同事。**导出内容是完整配置**:字面写死的凭据(URL 里的 token、字面 header / env 值)会原样出现在里面,请按密钥对待;用 `${VAR}` 引用的位置保持引用写法,不会被解析成明文。这份文件可以直接放进目标工作区的 `.dsh/mcp.json`,也可以用「导入 MCP」粘贴回来(粘贴只读 `mcpServers`,`exclude` 需要手动设)。
123
-
124
- 目标工作区必须在 DSH 里注册过(也就是出现在面板顶部标签里的那些)——「复制到…」不接受任意目录,这条限制在 Host 侧强制,不只是界面不显示。
125
-
126
- ### 看工具与敏感值
127
-
128
- 详情页列出该 MCP 当前注册的工具;展开可看完整输入 Schema。凭据类字段默认掩码,点眼睛图标会向 Host 读取**有效运行值**(已解析过的环境变量)并临时显示,再点一次恢复;已显示的值可一键复制。编辑时若没实际改动输入,保存仍保留原来的引用写法,不会把密钥写回配置文件。
129
-
130
- ## 配置生效时机(重要)
131
-
132
- | 作用域 | 修改后何时生效 |
133
- |---|---|
134
- | 全局 | DSH 热加载,通常立即生效(含运行中的会话) |
135
- | 项目 | **写盘即生效**:新增立刻挂上存活会话、删除立刻撤掉工具、改动就地重载连接(工具名不变) |
136
-
137
- 项目 MCP 由本项目所有会话共用一份连接,配置写在 `.dsh/mcp.json`。面板保存的那一刻就会对齐到正在运行的会话,**不需要重开会话,也不需要重启 Host**;手工编辑该文件则在**下一次会话挂载**时被采纳。
138
-
139
- > **连接跟配置走,不跟会话走**:最后一个会话结束不会拆掉连接,它只进入空闲等待,下一次会话直接复用——这样"要用的时候"它已经就绪(这也是宿主自己的契约:工具必须在首轮之前出现)。两个旋钮写在 server 条目里:`idleTimeoutMs`(空闲保留多久,默认 5 分钟,`0` = 永不回收)与 `readyTimeoutMs`(挂载时等就绪的总预算,默认 5s,`0` = 不等)。字段速查与边界见 [设计与限制](docs/design.md#项目级专有字段dshmcpjson-里每个-server-可写)。
140
- >
141
- > **状态怎么看**:项目行把两件独立的事分开说,不合成一个状态。
142
- > - **连接在不在**:`已连接 · 当前无会话使用(连接保留,下次直接用)` / `已连接 · N 个会话在用` / `连接中…` / `连接失败` / `尚未建立连接(新会话自动挂载)`。
143
- > - **几个会话在用**:见上一行的 `N 个会话在用`;不再出现「待会话挂载」那种把两者压成一句、容易被读成"还没连上"的写法。
144
- >
145
- > **重连是必需出口**:mcp-client 的自动重连有次数上限,耗尽后它会注销工具并停止,只能靠重载配置或重启宿主恢复(它的 README 明写)。项目 MCP 又不是常驻服务,所以面板的「重连」按钮会拆掉旧连接、按当前配置重建,并重新投射给存活会话。
103
+ **加 MCP**:面板顶部「添加 MCP」填表单,「导入 MCP」粘贴其他客户端的配置(先预览再写入)。常用服务器有预设模板。
146
104
 
147
105
  ## 常见问题
148
106
 
149
- **面板里显示「已禁用」,但模型调用报 `unknown tool "mcp__xxx__..."`**
150
- 那个 MCP 被禁用了,所以它没有注册任何工具。在面板里点「启用」即可(全局会写 profile 补丁并热加载,项目写 `.dsh/mcp.json`、写盘即生效)。
107
+ **项目里的改动什么时候生效?** 保存的那一刻就对齐到正在运行的会话:新增立刻挂上存活会话、删除立刻撤掉工具、改动就地重载连接——不需要重开会话,也不需要重启 Host。手工编辑 `.dsh/mcp.json` 则在**下一次会话挂载**时被采纳。
151
108
 
152
- **那一行显示「挂载失败」「连接失败」或「作用域隔离失败」,怎么区分?**
153
- 三类是不同的事,详情页会给出具体原因:**挂载失败** = **会话挂载阶段**(`agent/created` 监听器)就没挂上(变量求值为空、`failOnStartupError` 下启动失败、工具注册被拒);**连接失败** = `mcp-client` 连不上或重连耗尽(原因取自它的日志);**作用域隔离失败** = 连接正常但工具不在共享作用域层(宿主里出现了两份 `@deepseek-ai/dsh-scope`)。排查与原理见 [设计与限制](docs/design.md#连接状态语义)。
109
+ **连接跟配置走,不跟会话走。** 最后一个会话结束不会拆掉连接,它只进入空闲等待,下一次会话直接复用——「要用的时候」它已经就绪。两个旋钮写在 server 条目里:`idleTimeoutMs`(空闲保留多久,默认 5 分钟,`0` = 永不回收)与 `readyTimeoutMs`(挂载时等就绪的总预算,默认 5s,`0` = 不等)。字段速查见[设计与限制](docs/design.md#项目级专有字段dshmcpjson-里每个-server-可写)。
154
110
 
155
- **项目行那些状态文案分别是什么意思?**
156
- 它们把两件独立的事分开说:**连接在不在** —— `已连接 · 当前无会话使用(连接保留,下次直接用)`(最后一个会话结束了,连接按空闲策略留着)/ `已连接 · N 个会话在用 · M 工具` / `连接中…` / `连接失败` / `尚未建立连接(新会话自动挂载)`;**几个会话在用** —— 就是那句里的 `N 个会话在用`。旧版本的「待会话挂载」正是把这两件事压成一句,所以容易被读成"还没连上"。
111
+ **项目行那些状态文案是什么意思?** 它们把两件独立的事分开说:**连接在不在**(`已连接 · 当前无会话使用(连接保留,下次直接用)` / `已连接 · N 个会话在用` / `连接中…` / `连接失败` / `尚未建立连接(新会话自动挂载)`)与**几个会话在用**——不再有「待会话挂载」那种把两者压成一句、容易被读成「还没连上」的合成状态。
157
112
 
158
- **怎么升级?为什么面板没提示?**
159
- 从 npm 装的直接 `dsh plugin --profile web update dsh-toolbox-ui` 就会升小版本,也可以直接点面板提示条上的「升级」(同一件事,免去敲命令)。面板的提示条只查 npm registry 上**本包的 latest**、每天最多一次、可关闭、不会自己动;固定 tag 安装的用户不会自动跨版本,需要用新 tag 重新 `add`(见 [安装与升级](docs/installation.md))。⚠️ **1.6.1 及更早的版本问的是另一个源**(本仓库的 release feed,那里只有一条被标成 pre-release 的另一产品线),所以它们不会提示 1.6.2——这个修复装在 1.6.2 自己里,先手动 `dsh plugin --profile web update dsh-toolbox-ui` 升一次即可。
113
+ **面板显示「已禁用」,模型却报 `unknown tool "mcp__xxx__…"`?** 那个 MCP 被禁用了,所以它没有注册任何工具。在面板里点「启用」即可。
160
114
 
161
- **改动会不会碰到我的其他配置?**
162
- 不会。全局改动只动 profile 补丁里由 `@deepseek-ai/dsh-mcp-client` 声明、且由本插件管理的条目,保留其他插件条目、注释与 `!!js` 表达式;导入「替换」也只替换这一部分,不会删除其他 bundle 或 Agent preset 自带的 MCP。
115
+ **「挂载失败」「连接失败」「作用域隔离失败」怎么区分?** 三件不同的事:**挂载失败**=会话挂载阶段就没挂上;**连接失败**=`mcp-client` 连不上或重连耗尽;**作用域隔离失败**=连接正常但工具不在共享作用域层。详情页会给具体原因,排查见[设计与限制](docs/design.md#连接状态语义)。
163
116
 
164
- **卸载后我配置的 MCP 会消失吗?**
165
- 卸载插件不会删除 profile 里已有的 `@deepseek-ai/dsh-mcp-client` 条目;项目的 `.dsh/mcp.json` 也留在原处(它本来就是普通 JSON 文件)。
117
+ **会碰到我的其他配置吗?** 不会。全局改动只动由 `@deepseek-ai/dsh-mcp-client` 声明、且由本插件管理的条目,保留其他插件条目、注释与 `!!js` 表达式;卸载也不会删除你已有的 MCP 条目与项目的 `.dsh/mcp.json`。
166
118
 
167
119
  ## 兼容性
168
120
 
@@ -172,30 +124,29 @@ dsh plugin --profile web remove dsh-toolbox-ui
172
124
  | Node.js | DSH 自带/支持的运行时 |
173
125
  | 平台 | Windows;Linux/macOS 使用同一 DSH Web 契约 |
174
126
 
175
- 只支持 0.1.5 起(更早的版本缺少「按注册作用域判 `serverName` 唯一性」与「setup 传入 agent」两个能力),不为旧版本保留降级分支。原因与依赖细节见 [设计与限制](docs/design.md#兼容性与依赖细节)。
127
+ 只支持 0.1.5 起(更早的版本缺少「按注册作用域判 `serverName` 唯一性」与「setup 传入 agent」两个能力),不为旧版本保留降级分支。
176
128
 
177
129
  ## 开发
178
130
 
179
131
  ```sh
180
- npm test # 全部测试(含真机集成用例)
181
- dsh --profile web --dump-config # 看 profile 的有效配置
182
- dsh web # 起 Web 做真实操作验证
132
+ npm test # 全部测试(含真机集成用例:真 cordis / dsh-tools / dsh-scope / dsh-mcp-client + 真 stdio MCP 子进程)
133
+ dsh web # 起 Web 做真实操作验证
183
134
  ```
184
135
 
185
- `npm test` 里除替身用例(验证插件内部自洽)外,还有一组**真机集成用例**:真 `cordis` + 真 `dsh-tools` + 真 `dsh-scope` + 真 `dsh-mcp-client` + 一个真 stdio MCP 子进程,覆盖「同项目两个会话共用一份连接、工具投射进会话层、不泄漏到全局、子进程随最后一个会话退出」。CI(push 到 main / PR,以及推 tag 发版)跑的是同一条门槛,发版流程见 [安装与升级](docs/installation.md#发布流程维护者)。
186
-
187
136
  ## 文档
188
137
 
189
- - [安装与升级](docs/installation.md)
190
- - [JSON 导入](docs/json-import.md)
191
- - [设计与限制](docs/design.md)(共享连接模型、连接状态语义、已知限制、依赖细节)
138
+ - [安装与升级](docs/installation.md) —— 三种安装方式、发布流程、故障排查
139
+ - [技能管理](docs/skills.md) —— 作用域与遮蔽、启停、调用权限、技能市场
140
+ - [JSON 导入](docs/json-import.md) —— 字段映射与限制
141
+ - [设计与限制](docs/design.md) —— 共享连接模型、连接状态语义、已知限制、依赖细节
192
142
  - [DeepSeek Harness 官方插件发布指南](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/basic/publish.md)
193
143
 
194
- ## 相关链接
144
+ ## 友情链接
195
145
 
196
- - [LINUX DO](https://linux.do/)(友链)
197
- - [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)
198
- - [GitHub `dsh-plugin` 主题](https://github.com/topics/dsh-plugin)
146
+ <p align="center">
147
+ <a href="https://linux.do/" title="LINUX DO"><img src="docs/images/linux-do-logo.svg" alt="LINUX DO" width="44" height="44"></a><br>
148
+ <a href="https://linux.do/">LINUX DO</a>
149
+ </p>
199
150
 
200
151
  ## License
201
152
 
@@ -15,17 +15,17 @@
15
15
  推荐用 npm 安装:包里的 `^1.x` 范围让后续 `dsh plugin update` 能自动升级小版本。
16
16
 
17
17
  ```sh
18
- dsh plugin --profile web add dsh-toolbox-ui@^1.6.2
18
+ dsh plugin --profile web add dsh-toolbox-ui@^1.6.4
19
19
  ```
20
20
 
21
21
  也可以固定 GitHub release tag(适合需要锁定某个具体版本时):
22
22
 
23
23
  ```sh
24
24
  # 固定 release tag
25
- dsh plugin --profile web add github:Imzl-zl/dsh-mcp-manager-ui#v1.6.2
25
+ dsh plugin --profile web add github:Imzl-zl/dsh-toolbox-ui#v1.6.4
26
26
 
27
27
  # 或固定某个 commit
28
- dsh plugin --profile web add github:Imzl-zl/dsh-mcp-manager-ui#<commit>
28
+ dsh plugin --profile web add github:Imzl-zl/dsh-toolbox-ui#<commit>
29
29
  ```
30
30
 
31
31
  两种方式的**代码与运行行为完全相同**(包里带预构建的 `lib/`,不需要运行 `prepare`,也不需要配置 `allowBuilds`);区别只在升级:`^1.x` 范围能被 `dsh plugin update` 自动推进,而 tag/commit 是不可变规格、`update` 只会重新解析同一个规格。
@@ -73,7 +73,7 @@ dsh plugin --profile web update dsh-toolbox-ui
73
73
 
74
74
  | 安装规格 | `update` 的效果 | 怎么跨版本 |
75
75
  |---|---|---|
76
- | `dsh-toolbox-ui@^1.6.2`(npm,推荐) | 在 `^1.x` 内自动升到最新小版本,不会跨到 2.0.0 | 已是自动;跨大版本显式安装 `@^2` |
76
+ | `dsh-toolbox-ui@^1.6.4`(npm,推荐) | 在 `^1.x` 内自动升到最新小版本,不会跨到 2.0.0 | 已是自动;跨大版本显式安装 `@^2` |
77
77
  | `github:...#v1.4.0`(tag) | 重新解析同一个 tag,**不会动** | 换成新 tag 重新 `add`:`... #<新 tag>` |
78
78
  | `github:...#<commit>` | 同上,固定在 commit | 用新 commit/tag 重新 `add` |
79
79
  | `github:...#main`(分支) | 拉取最新 main | 已是自动;但不推荐生产(无 release 门槛) |
@@ -96,7 +96,8 @@ npm 与 GitHub Releases 必须**同一次发布里都更新**:用户从 npm
96
96
  npm publish
97
97
  ```
98
98
 
99
- 2. 在 npmjs.com 该包的 Settings → **Trusted Publisher** → GitHub Actions,填:organization/user `Imzl-zl`、repository `dsh-mcp-manager-ui`、workflow filename `release.yml`(可另填 environment)。
99
+ 2. 在 npmjs.com 该包的 Settings → **Trusted Publisher** → GitHub Actions,填:organization/user `Imzl-zl`、repository `dsh-toolbox-ui`、workflow filename `release.yml`(可另填 environment)。
100
+ ⚠️ 这里的 repository 是 **GitHub 仓库名**,它必须与仓库当前的 slug 一致——仓库此前叫 `dsh-mcp-manager-ui`,改名之后不同步改这一项,`npm publish` 会被拒(OIDC 凭据里带的就是仓库名)。失败是安全的:CI 那一步会红,不会建 Release。
100
101
  3. 之后可选但推荐:把包的 Publishing access 设为 **Require two-factor authentication and disallow tokens** —— trusted publishing 不受该限制影响(它用 OIDC,不是 token),而长期写凭据因此彻底用不上。
101
102
 
102
103
  ### 每次发布
@@ -112,7 +113,7 @@ dsh web # 打开面板点一遍
112
113
  # 3) 提交并推 tag(CI 会校验 tag 与 version 一致,不一致就拒绝发布)
113
114
  git add -A && git commit -m "..."
114
115
  git push origin main
115
- git tag -a v1.6.2 -m "v1.4.2 ..." && git push origin v1.6.2
116
+ git tag -a v1.6.4 -m "dsh-toolbox-ui 1.6.4" && git push origin v1.6.4
116
117
  ```
117
118
 
118
119
  推 tag 后 CI 依次做:校验版本 → `pnpm install --frozen-lockfile` → `npm test` → `npm publish`(trusted publishing,无需 token)→ `gh release create`。
@@ -131,7 +132,7 @@ git tag -a v1.6.2 -m "v1.4.2 ..." && git push origin v1.6.2
131
132
 
132
133
  ```sh
133
134
  npm publish # 会弹 2FA 挑战;若用 bypass-2FA 的 granular token 则非交互
134
- gh release create v1.6.2 --title "dsh-toolbox-ui v1.6.2" --notes-file <notes>
135
+ gh release create v1.6.4 --title "dsh-toolbox-ui v1.6.4" --notes-file <notes>
135
136
  ```
136
137
 
137
138
  `npm login` 的会话 token 只有 2 小时,超时需要重新登录;classic token 已被 npm 永久吊销,能非交互发布的只剩勾了 **Bypass 2FA** 的 granular access token——但它正在被 npm 收窄(已不能做账号/包管理类操作,且 2027-01 起将失去直接发布能力),所以长期方案是上面的 trusted publishing。
@@ -149,9 +150,9 @@ dsh plugin --profile web remove dsh-toolbox-ui
149
150
  本地开发才使用目录安装:
150
151
 
151
152
  ```sh
152
- cd C:\sudy\github\dsh-mcp-manager-ui
153
+ cd C:\path\to\dsh-toolbox-ui
153
154
  pnpm install
154
- dsh plugin --profile web add C:\sudy\github\dsh-mcp-manager-ui
155
+ dsh plugin --profile web add C:\path\to\dsh-toolbox-ui
155
156
  ```
156
157
 
157
158
  修改 Host 代码后需要重启 `dsh web`;浏览器 bundle 是否热更新取决于当前 DSH 的 client HMR 状态。
package/docs/skills.md CHANGED
@@ -39,7 +39,15 @@ MCP 面的市场走的是同一个模式的同一种形态(内联网格,不
39
39
  | 500 | `<AGENTS_HOME>/skills` | 全局 |
40
40
  | 600 | 内置(bundled) | **只读**,不可启停/删除 |
41
41
 
42
- 同名技能出现在多个作用域时,面板会标出**哪一份生效、哪一份被遮蔽**——被遮蔽的那份模型收不到,但你确实装了它。⚠️ 边界:宿主按名字只报告**生效**的那一份,所以落选副本只有在以 `SKILL.md.disabled` 形态留下时才看得见;两份都启用时,落选那份目前不在面板数据里(要报它得自己扫各根,见 TODO row 21 旁边的同名副本扫描)。
42
+ 同名技能出现在多个作用域时,面板会标出**哪一份生效、哪一份被遮蔽**——被遮蔽的那份模型收不到,但你确实装了它。生效者按优先级数值**最小**者判定,与它在列表里的位置无关。⚠️ 边界:宿主按名字只报告**生效**的那一份,所以落选副本只有在以 `SKILL.md.disabled` 形态留下时才看得见;两份都启用时,落选那份目前不在面板数据里(要报它得自己扫各根,见 TODO row 21 旁边的同名副本扫描)。
43
+
44
+ ### 同一个目录被两处指到时
45
+
46
+ 优先级表里的两个位置**可能算出来是同一个目录**——最常见的是 `DSH_HOME` 就在工作区里:设 `DSH_HOME=D:\DSH\.dsh`、工作区是 `D:\DSH`(它自己是 git 根),那么项目级 `.dsh/skills` 与全局 `<DSH_HOME>/skills` 是同一个物理目录。
47
+
48
+ 面板按**物理位置**去重:同一个文件只占一行,留优先级更前的那份(也就是模型实际收到的那份);安装目标的候选里同一个目录也只出现一次。反过来,**不同目录**里的同名技能一定各占一行——那是真实的跨作用域遮蔽,合并它会把你真装了的那份藏起来。
49
+
50
+ 同理,技能目录本身是**符号链接**(把它们指向外部同步库是常见做法)时,面板照样能定位到它:宿主报的展示路径与真实路径不一致这件事已经被处理,不会退化成「位置未知」。
43
51
 
44
52
  ## 启用与停用
45
53
 
package/lib/client.js CHANGED
@@ -782,12 +782,26 @@ const selectedSkillRow = (rows, key) => (Array.isArray(rows) ? rows.find((row) =
782
782
  * DSH 按 rank 裁决同名技能谁生效,被遮蔽的那份模型收不到——但用户确实装了它。
783
783
  * 不提示的话,用户会以为自己操作的是生效的那份(本仓库的独有价值之一)。
784
784
  *
785
+ * ⚠️ 判据必须是 **rank**,不是数组位置。曾经取「排在最前的那个」当生效者,而那个前提是错的:
786
+ * 宿主 `list` 的输出按**名字**排序(dsh-skill 的 compareSkillSummary),面板自己又按
787
+ * 「全局在前、项目在后」拼接——而项目级 rank(100/200) 恰好**小于**用户级(400/500),
788
+ * 于是"最前的"正好是 rank 最大、也就是真正**落选**的那份,方向完全相反(真机报告 + 已复现)。
789
+ * 结果是面板指着生效的那份说「你被遮蔽了」,用户照它去操作,改的其实是模型收不到的那份。
790
+ *
791
+ * 同名里 rank 最小者胜;rank 相同(同一档的不同项目)时按输入顺序取先者——那种情况下
792
+ * 谁生效由宿主的 providerOrder 决定,面板拿不到,但两个不同项目里的同名技能互不影响,
793
+ * 标一份为胜者只是为了让提示有个主语,不会误导操作。
794
+ *
795
+ * 已停用的行不参与:它的文件是 `SKILL.md.disabled`,模型根本看不到它,说它"遮蔽/被遮蔽"
796
+ * 都是假的。它只该出现在诊断里(见 skillDiagnostics)。
797
+ *
785
798
  * @param {object[]} rows
786
799
  * @returns {object[]} 被遮蔽的行(附生效者信息)。
787
800
  */
788
801
  function shadowedSkills(rows) {
789
802
  const groups = new Map();
790
803
  for (const row of Array.isArray(rows) ? rows : []) {
804
+ if (row?.enabled === false) continue;
791
805
  const list = groups.get(row.name) || [];
792
806
  list.push(row);
793
807
  groups.set(row.name, list);
@@ -795,9 +809,12 @@ function shadowedSkills(rows) {
795
809
  const shadowed = [];
796
810
  for (const [, list] of groups) {
797
811
  if (list.length < 2) continue;
798
- // 生效者是排在最前的那个(宿主已按 rank 排序给出),其余都是被遮蔽的。
799
- const [winner, ...rest] = list;
800
- for (const row of rest) shadowed.push({ ...row, shadowedBy: winner });
812
+ // 生效者 = rank 最小者(同 rank 取先出现的)。不能改用位置,理由见上。
813
+ const winner = list.reduce((best, row) => (sourceRank(row.source) < sourceRank(best.source) ? row : best), list[0]);
814
+ for (const row of list) {
815
+ if (row === winner) continue;
816
+ shadowed.push({ ...row, shadowedBy: winner });
817
+ }
801
818
  }
802
819
  return shadowed;
803
820
  }
@@ -3302,7 +3319,7 @@ const McpTab = (props) => {
3302
3319
  : h('a', {
3303
3320
  key: 'link',
3304
3321
  className: 'dsh-mcp-btn sm',
3305
- href: (updateHint && updateHint.url) || 'https://github.com/Imzl-zl/dsh-mcp-manager-ui/releases',
3322
+ href: (updateHint && updateHint.url) || 'https://github.com/Imzl-zl/dsh-toolbox-ui/releases',
3306
3323
  target: '_blank',
3307
3324
  rel: 'noreferrer',
3308
3325
  }, upgradeState.phase === 'done' ? '看这次改了什么' : '查看更新方式'),
@@ -4123,6 +4140,26 @@ const SkillGroupHeader = ({ group, collapsed, onToggleCollapse }) => h(
4123
4140
  h('span', { className: 'dsh-skills-group-count' }, String(group.rows.length)),
4124
4141
  );
4125
4142
 
4143
+ /**
4144
+ * 「调用权限未知」到底是哪种未知——给用户一句**说得对**的原因。
4145
+ *
4146
+ * 为什么不能一律写成「frontmatter 读不出来」:那是把两种不同的事说成了一种,而且真机报告
4147
+ * 证明它会把用户引向错误的方向。技能目录是符号链接时根定位不到(`root === undefined`),
4148
+ * 文件本身完全合法、宿主也照常加载,用户却看到「frontmatter 读不出来(宿主会跳过这条技能)」
4149
+ * ——一句凭空的断言,照它去查 frontmatter 只会白费功夫。
4150
+ *
4151
+ * 这里能分开的是两种(行上有 root 与否就是面板掌握的全部线索):
4152
+ * · 根没定位到 → 面板根本不知道去哪读这个文件(定位问题,不是文件问题);
4153
+ * · 有根却读不出权限 → 文件没了/搬走了/没权限,或 frontmatter 本身有问题。
4154
+ * 第二种没法再细分:读取失败的原因在宿主侧(`invocationOf` 只把结果压成 null),
4155
+ * 面板拿不到更细的话,就不编一个——正文页会把真正的失败原文摆出来。
4156
+ */
4157
+ function invocationUnknownReason(row) {
4158
+ if (typeof row?.root !== 'string' || row.root === '')
4159
+ return '面板没能定位这条技能的目录(技能目录可能是符号链接),因此读不到它的调用权限——文件本身可能是好的';
4160
+ return 'SKILL.md 的 frontmatter 读不出来(宿主会跳过这条技能),无法判断它的调用权限';
4161
+ }
4162
+
4126
4163
  /** 管理模式的右侧详情:元信息 + 正文 + 启停/删除(与 MCP 面同一张详情卡片)。 */
4127
4164
  const SkillDetail = ({ row, shadowedBy, body, busy, onRetry, onToggle, onRemove, onModelInvocable, tab = 'overview', onTab }) => {
4128
4165
  if (!row) return h('div', { className: 'dsh-mcp-detail' }, h('div', { className: 'dsh-mcp-d-empty' },
@@ -4165,7 +4202,7 @@ const SkillDetail = ({ row, shadowedBy, body, busy, onRetry, onToggle, onRemove,
4165
4202
  h(Badge, { cls: disabled ? 's-off' : shadowedBy ? 's-warn' : 's-on' }, disabled ? '已停用' : shadowedBy ? '被遮蔽' : '已启用'),
4166
4203
  invocationKnown
4167
4204
  ? h(Badge, { cls: row.modelInvocable ? null : 's-off' }, row.modelInvocable ? '模型可调用' : '模型不可调用')
4168
- : h(Badge, { cls: 's-off', title: 'SKILL.md 的 frontmatter 读不出来(宿主会跳过这条技能),无法判断它的调用权限' }, '调用权限未知'),
4205
+ : h(Badge, { cls: 's-off', title: invocationUnknownReason(row) }, '调用权限未知'),
4169
4206
  invocationKnown && !row.userInvocable ? h(Badge, { cls: 's-off' }, '用户不可调用') : null,
4170
4207
  ),
4171
4208
  h('p', { className: 'dsh-mcp-d-desc' }, row.description || '(无说明)'),
@@ -4213,7 +4250,7 @@ const SkillDetail = ({ row, shadowedBy, body, busy, onRetry, onToggle, onRemove,
4213
4250
  ? '内置技能只读'
4214
4251
  : invocationKnown
4215
4252
  ? '写入 SKILL.md 的 disable-model-invocation;宿主重新发现这条技能后才影响模型(重启 DSH 最稳妥)'
4216
- : 'SKILL.md 的 frontmatter 读不出来(宿主会跳过这条技能),无法判断当前状态',
4253
+ : invocationUnknownReason(row),
4217
4254
  onChange: () => onModelInvocable(row, row.modelInvocable !== true),
4218
4255
  }),
4219
4256
  ' ',
package/lib/index.js CHANGED
@@ -353,7 +353,7 @@ function updateLatestUrl() {
353
353
  }
354
354
 
355
355
  /** 说明页:两条线的 tag 都是 vX.Y.Z,故由版本号拼本仓库的 release 页(与老线的 updateHint 一致)。 */
356
- const releasePageOf = (version) => `https://github.com/Imzl-zl/dsh-mcp-manager-ui/releases/tag/v${version}`;
356
+ const releasePageOf = (version) => `https://github.com/Imzl-zl/dsh-toolbox-ui/releases/tag/v${version}`;
357
357
 
358
358
  function pluginVersion() {
359
359
  try {
@@ -6,7 +6,7 @@
6
6
  //
7
7
  // 本层只读:不写盘、不做启停(那些在 lib/skills-file-ops.js)。
8
8
 
9
- import { access, stat } from 'node:fs/promises'
9
+ import { access, realpath, stat } from 'node:fs/promises'
10
10
  import { homedir } from 'node:os'
11
11
  import { dirname, join, resolve } from 'node:path'
12
12
  import { readSkillDocument, resolveSkillTarget, SkillNotFoundError } from './skills-file-ops.js'
@@ -23,6 +23,36 @@ function scopeOfSource(source) {
23
23
  return PROJECT_SOURCES.has(source) ? 'project' : 'global'
24
24
  }
25
25
 
26
+ /**
27
+ * 宿主六级 source 的 rank(与 provider 的 *_RANK 常量、docs/skills.md 的表一致)。
28
+ * 数值小者优先——这是宿主裁决同名技能的依据,面板的「谁生效」与去重都用它。
29
+ */
30
+ const SKILL_SOURCE_RANK = {
31
+ 'project-dsh': 100,
32
+ 'project-agents': 200,
33
+ custom: 300,
34
+ 'user-dsh': 400,
35
+ 'user-agents': 500,
36
+ bundled: 600,
37
+ }
38
+
39
+ /**
40
+ * 统一分隔符后再比较:宿主给的是**本机原生**路径(Windows 上是反斜杠),而技能根也可能来自
41
+ * 用户配置的 customSkillDirs(两种分隔符都写得出来)。join() 不够用——它是平台相关的,
42
+ * 拿不到「另一种分隔符」的等价形式。只用于**比较与做键**,绝不用于喂给 fs。
43
+ */
44
+ const toSlashes = (value) => String(value).replace(/\\/g, '/')
45
+
46
+ /**
47
+ * 一个**目录**的物理身份(realpath,统一分隔符)。
48
+ *
49
+ * 目录不存在时退回字面值——这时它不可能与别的根重合(都不存在,谁也指不到谁),
50
+ * 而 realpath 会抛 ENOENT:既不能让它把整个列表读失败,也不该为它编一个值。
51
+ */
52
+ async function physicalDirectory(path) {
53
+ return toSlashes(await realpath(path).catch(() => path))
54
+ }
55
+
26
56
  /**
27
57
  * 从宿主 summary 推出技能的**根目录**与**形态**。
28
58
  *
@@ -33,33 +63,58 @@ function scopeOfSource(source) {
33
63
  * 目录捆绑:path = `<技能目录>/SKILL.md`,resourceBase.path = `<技能目录>` → 根 = dirname(base)
34
64
  * 扁平文件:path = `<根>/<name>.md`, resourceBase.path = `<根>` → 根 = base
35
65
  *
36
- * 判别式必须用「path 是否**恰为** `<base>/SKILL.md`」。曾用「dirname(path) === base」判别——
37
- * 那个条件在两种形态下**都成立**(捆绑时 path 的父目录就是 base),不是判别式,会把扁平技能
38
- * 的根算成上一层(实测把 `~/.agents/skills` 算成了 `~/.agents`)。
66
+ * 判别式与「形态不认识就不猜」的纪律见 matchLocator。
67
+ *
68
+ * ⚠️ 两个字段来自**不同的路径口径**,不能只做字面比较(真机缺陷,2026-10-11 复现):
69
+ * `resourceBase.path` = `locator.directory` —— **展示路径**(配置原样,配置写链接就是链接)
70
+ * `path` = `fs.processPath(target)` —— **realpath**(fs-local 里就是 targetKey)
71
+ * 技能目录本身是符号链接/junction 时两者必然分叉,只做字面比较会落进「形态不认识」→ 根推不出来
72
+ * → **整行不可操作**(宿主自己的用例也钉着这个分叉:skill-filesystem.spec.ts 的 linked-dir 那条)。
73
+ * 所以字面没命中时,把 base 规范化到真实路径再判别一次。
74
+ *
75
+ * 返回的 root 一律是**展示路径**:`resolveTargetReference` 要求它与 skillTargetsView 算出的
76
+ * 目标精确相等,返回 realpath 会让所有文件操作对不上(那是更坏的方向)。
39
77
  *
40
78
  * 推不出就返回空:宁可不填,也不猜一个根——猜错会让启停/删除作用在错误的目录上。
41
79
  *
42
80
  * @param {object} summary - 宿主 SkillSummary。
43
- * @returns {{ root?: string, kind?: 'bundle'|'flat' }}
81
+ * @returns {Promise<{ root?: string, kind?: 'bundle'|'flat' }>}
44
82
  */
45
- function locateSkill(summary) {
83
+ async function locateSkill(summary) {
46
84
  const base = summary?.resourceBase?.path
47
85
  const path = summary?.path
48
86
  if (typeof base !== 'string' || base === '' || typeof path !== 'string' || path === '') return {}
49
87
 
50
- // 比较前统一分隔符:宿主给的是**本机原生**路径(Windows 上是反斜杠),而技能根也可能来自
51
- // 用户配置的 customSkillDirs(两种分隔符都写得出来)。join() 在这里不够——它是平台相关的,
52
- // 拿不到「另一种分隔符」的等价形式。返回值仍用**原生** base,不做转换(要给 fs 用)。
53
- const toSlashes = (value) => value.replace(/\\/g, '/')
88
+ // 比较前统一分隔符(见文件上方的 toSlashes 说明)。返回值仍用**原生** base,不做转换(要给 fs 用)。
54
89
  const baseSlashes = toSlashes(base)
55
90
  const pathSlashes = toSlashes(path)
56
91
 
57
- // 判别式:path 是否**恰为** <base>/SKILL.md(目录捆绑)。
58
- // 不能只看 dirname(path) === base —— 那种情况下扁平与捆绑都成立,会把扁平技能的根算成上一层。
59
- if (pathSlashes === `${baseSlashes}/${BUNDLE_FILE}`) return { root: dirname(base), kind: 'bundle' }
60
- if (dirname(pathSlashes) === baseSlashes) return { root: base, kind: 'flat' }
61
- // 形态与两种已知 locator 都不符:不猜。
62
- return {}
92
+ // 先按字面比一次:**没有链接时两个字段本来就同源**,这一步就命中,不必碰磁盘
93
+ // (技能列表每次刷新都要走这里,几十条技能各多一次 realpath 是白花的)。
94
+ const literal = matchLocator(base, baseSlashes, pathSlashes)
95
+ if (literal !== undefined) return literal
96
+
97
+ // 字面没命中:可能是两个字段口径不同(链接目录)。把 base 规范化到真实路径再比一次。
98
+ // 只在**这一条**上付 realpath 的代价,而且这一步同时兜住 Windows 上 realpath 会
99
+ // 规范化盘符/目录名大小写的情形——那种情况下字面同样不相等,但物理上是同一个目录。
100
+ const physical = await physicalDirectory(base)
101
+ if (physical === baseSlashes) return {} // 规范化前后一样,说明字面失败不是口径问题:不猜
102
+ return matchLocator(base, physical, pathSlashes) ?? {}
103
+ }
104
+
105
+ /**
106
+ * 按给定的 base 形态判别 locator(宿主 discoverRoot 的两种形状)。
107
+ *
108
+ * 判别式必须是「path 是否**恰为** `<base>/SKILL.md`」。曾用「dirname(path) === base」判别——
109
+ * 那个条件在两种形态下**都成立**(捆绑时 path 的父目录就是 base),不是判别式,会把扁平技能
110
+ * 的根算成上一层(实测把 `~/.agents/skills` 算成了 `~/.agents`)。
111
+ *
112
+ * @returns {{ root: string, kind: 'bundle'|'flat' } | undefined} 认不出时返回 undefined。
113
+ */
114
+ function matchLocator(base, baseCompared, pathSlashes) {
115
+ if (pathSlashes === `${baseCompared}/${BUNDLE_FILE}`) return { root: dirname(base), kind: 'bundle' }
116
+ if (dirname(pathSlashes) === baseCompared) return { root: base, kind: 'flat' }
117
+ return undefined
63
118
  }
64
119
 
65
120
  /** 取宿主 skills 服务;宿主的服务名就是 'skills'(dsh-skill 的 super(ctx, "skills"))。 */
@@ -155,7 +210,7 @@ async function invocationOf(row) {
155
210
  * @returns {Promise<object>} 面板行。
156
211
  */
157
212
  async function toView(summary, { cwd }) {
158
- const located = locateSkill(summary)
213
+ const located = await locateSkill(summary)
159
214
  const scope = scopeOfSource(summary.source)
160
215
  // 调用权限**不读** summary.invocation:那一份来自宿主的发现缓存(见 invocationOf 的说明)。
161
216
  const invocation = await invocationOf({ name: summary.name, root: located.root, scope })
@@ -205,6 +260,16 @@ async function queryScope(skills, options) {
205
260
  * 进 cwd 分支,而用户根与 bundled 根的 push 在该分支之外无条件执行)。所以项目查询的
206
261
  * 结果已经包含全局技能,直接 concat 会让每个全局技能在每个项目里各重复一份。
207
262
  *
263
+ * ⚠️ 但「按 source 过滤」只挡住了**同一根被两种查询各报一次**这一种重复。还有第二种:
264
+ * 两个根算出来是**同一个物理目录**。真机形状(issue #5,2026-10-11 复现):`DSH_HOME=D:\DSH\.dsh`
265
+ * 而工作区 `D:\DSH` 本身是 git 根,于是 project-dsh 根 = `<DSH_HOME>/skills` = user-dsh 根;
266
+ * 实测同一个文件被列成 N+1 行(全局 1 份 + 每个工作区 1 份),名字相同 → 面板互判「被遮蔽」,
267
+ * 安装下拉里同一个目录也以两个身份各出现一次。
268
+ *
269
+ * 所以去重判据必须是**物理位置**,不是字符串也不只是 source:同一个文件只留一行,
270
+ * 留 rank 更前的那份(= 模型真正会收到的那份)。反过来,**不同文件**的同名技能必须保留
271
+ * 两行——那是真实的跨作用域遮蔽,压掉它等于把用户装的东西藏起来(看 docs/skills.md 的作用域表)。
272
+ *
208
273
  * @param {object} ctx - 宿主上下文(或 { get } 替身)。
209
274
  * @param {{ workspaces?: string[], signal?: AbortSignal }} options
210
275
  * @returns {Promise<object[]>}
@@ -227,12 +292,66 @@ async function listSkillViews(ctx, options = {}) {
227
292
  // 那份同名技能误当成重复而丢掉。
228
293
  return rows.filter((row) => row.scope === 'project')
229
294
  }))
230
- return [...globalRows, ...projectGroups.flat()]
295
+ return dedupeByPhysicalLocation([...globalRows, ...projectGroups.flat()])
231
296
  } finally {
232
297
  await lease.dispose()
233
298
  }
234
299
  }
235
300
 
301
+ /**
302
+ * 同一个**物理文件**只留一行(rank 更前者胜)。
303
+ *
304
+ * 身份用行上的 `path`——那正是宿主给的 realpath(`fs.processPath`),所以链接与真实目录
305
+ * 会归一到同一个键。拿不到 path 的行(旧宿主/替身)退回它的字面身份:宁可少去重一次,
306
+ * 也不能把两行不同的技能误判成同一个。
307
+ *
308
+ * 为什么保留 rank 更前者:那一份才是宿主的胜出条目,也就是模型真正收到的那份;面板的
309
+ * 「谁生效」与文件操作都该指向它。落选的那一行是**同一个文件**(不是副本),留着只会让
310
+ * 用户看到两个都写着"已启用"、还互相说被遮蔽的行。
311
+ *
312
+ * @param {object[]} rows
313
+ * @returns {object[]} 去重后的行,保持输入顺序(全局在前、项目在后)。
314
+ */
315
+ function dedupeByPhysicalLocation(rows) {
316
+ const winnerAt = new Map()
317
+ const result = []
318
+ for (const row of rows) {
319
+ const key = physicalKeyOf(row)
320
+ const seen = winnerAt.get(key)
321
+ if (seen === undefined) {
322
+ winnerAt.set(key, result.length)
323
+ result.push(row)
324
+ continue
325
+ }
326
+ // 同一物理文件:rank 更前(数值更小)的胜出,替换掉先前那份但**留在原位**。
327
+ if (rankOfSource(row.source) < rankOfSource(result[seen].source)) result[seen] = row
328
+ }
329
+ return result
330
+ }
331
+
332
+ /**
333
+ * 一行的**物理身份**:技能文件的真实路径(宿主给的 `path` 已经是 realpath)**+ 技能名**。
334
+ *
335
+ * 为什么必须带上名字:去重的意图是「同一个物理技能被两个根各报了一次」。而技能名来自
336
+ * 文件自己的 frontmatter——同一个文件只可能有一条技能,所以「同路径 + 同名」才是同一个技能。
337
+ * 只看路径会把「同路径但不同名」的两行也压掉,而那两行要么是替身数据不严谨,要么是
338
+ * 宿主行为出了别的问题;无论哪种,静默吞掉一行都比留着它更难查。
339
+ *
340
+ * 拿不到 path 时退回**行的完整身份**(source + cwd + name,与 skillRowKey 同一套):
341
+ * 那种情况下物理位置无从得知,而「不同项目里的同名技能」必须各占一行(它们真的是两份)。
342
+ */
343
+ function physicalKeyOf(row) {
344
+ if (typeof row?.path === 'string' && row.path !== '') {
345
+ return `path\u0000${toSlashes(row.path)}\u0000${row?.name ?? ''}`
346
+ }
347
+ return `row\u0000${row?.source ?? ''}\u0000${row?.cwd ?? ''}\u0000${row?.name ?? ''}`
348
+ }
349
+
350
+ /** source → 宿主 rank(数值小者优先);表外的来源排在最后。 */
351
+ function rankOfSource(source) {
352
+ return SKILL_SOURCE_RANK[source] ?? Number.MAX_SAFE_INTEGER
353
+ }
354
+
236
355
  /**
237
356
  * 加载单个技能正文的旧路径:**已被 detail 的按根读取取代**(2026-10-10)。
238
357
  *
@@ -259,15 +378,6 @@ async function listSkillViews(ctx, options = {}) {
259
378
  // 3) `findProjectRoot` 照抄 provider 的实现:往上找含 `.git` 的目录,找不到就用原路径。
260
379
  // ⚠️ provider 改算法时这里要一起改;语义由 test/skill-targets.test.mjs 钉住。
261
380
 
262
- /** 宿主的 source 档位与 rank(与 provider 的 *_RANK 常量、docs/skills.md 的表一致)。 */
263
- const SKILL_SOURCE_RANK = {
264
- 'project-dsh': 100,
265
- 'project-agents': 200,
266
- custom: 300,
267
- 'user-dsh': 400,
268
- 'user-agents': 500,
269
- bundled: 600,
270
- }
271
381
  const DSH_HOME_ENV = 'DSH_HOME'
272
382
  const AGENTS_HOME_ENV = 'DSH_AGENTS_HOME'
273
383
  const BUNDLED_SKILL_DIR_ENV = 'DSH_BUNDLED_SKILL_DIR'
@@ -390,8 +500,40 @@ async function skillTargetsView(ctx, options = {}) {
390
500
  }
391
501
  if (typeof bundledDir === 'string' && bundledDir !== '') await push('bundled', resolve(bundledDir))
392
502
 
393
- targets.sort((left, right) => left.rank - right.rank || left.root.localeCompare(right.root))
394
- return { targets }
503
+ // 同一个**物理目录**只列一次:DSH_HOME 与工作区重合时,project-dsh 与 user-dsh 算出来是
504
+ // 同一个目录(真机形状见 listSkillViews)。留着会让安装下拉里同一个位置出现两次,
505
+ // 用户以为有两个可选的落点。
506
+ //
507
+ // ⚠️ 在**排序之前**去重,并且留 rank 更前的那份:rank 决定宿主优先采纳哪个根,
508
+ // 而 `targets[0]` 正是安装弹窗的默认选项(见客户端的 installTargets)——留错那份会让
509
+ // 默认落点指向一个"名字对了、身份不对"的位置。
510
+ const unique = await dedupeTargetsByPhysicalLocation(targets)
511
+ unique.sort((left, right) => left.rank - right.rank || left.root.localeCompare(right.root))
512
+ return { targets: unique }
513
+ }
514
+
515
+ /**
516
+ * 同一个物理目录的目标只留一个(rank 更前者胜)。
517
+ *
518
+ * 为什么单独一步而不是并进 push:`exists` 是**观察**(要问磁盘),去重是**身份**判断,
519
+ * 混在一处会让「先问盘再决定收不收」这种顺序依赖悄悄长出来。
520
+ */
521
+ async function dedupeTargetsByPhysicalLocation(targets) {
522
+ const seenPhysical = new Map()
523
+ const result = []
524
+ for (const target of targets) {
525
+ const physical = await physicalDirectory(target.root)
526
+ const previous = seenPhysical.get(physical)
527
+ if (previous === undefined) {
528
+ seenPhysical.set(physical, result.length)
529
+ result.push(target)
530
+ continue
531
+ }
532
+ // 同一物理目录:rank 更前(数值更小)的胜出,就地替换先前那份(索引不变,
533
+ // 顺序由后面的排序定)。
534
+ if (target.rank < result[previous].rank) result[previous] = target
535
+ }
536
+ return result
395
537
  }
396
538
 
397
539
  /**
package/lib/whats-new.js CHANGED
@@ -201,4 +201,30 @@ export const WHATS_NEW = [
201
201
  },
202
202
  ],
203
203
  },
204
+ {
205
+ version: '1.6.3',
206
+ items: [
207
+ {
208
+ text: '**这一版没有功能改动**:改的是门面与发布资产——README 重写(更短、只留两张关键截图)、换上新图标,仓库也从旧名改成了 `dsh-toolbox-ui`,与包名终于对上。旧地址 GitHub 会自动跳转,已有的安装、书签与 `github:` 链接都不受影响',
209
+ where: '仓库主页',
210
+ },
211
+ ],
212
+ },
213
+ {
214
+ version: '1.6.4',
215
+ items: [
216
+ {
217
+ text: '**技能目录是符号链接时不再「整行不可用」**:这种情况下面板以前显示「位置(未报告)/调用权限未知」,点开还读不出正文——悬浮提示把它说成 frontmatter 有问题,其实是面板没能定位到那个目录(文件一直是好的)。现在链接目录照常定位,位置与调用权限都正常显示,启停、删除、读正文也都可用',
218
+ where: '技能列表与详情',
219
+ },
220
+ {
221
+ text: '**同一个目录被两处指到时不再重复列出**:当 `DSH_HOME` 就在工作区里(例如 `DSH_HOME=D:\\DSH\\.dsh` 而工作区是 `D:\\DSH`),项目级与全局的技能目录其实是同一个。以前同一个技能会被列成好几行、还互相标「被遮蔽」,安装位置里同一个目录也出现两次。现在同一个文件只占一行(留优先级更高的那份,也就是模型实际收到的那份);**不同目录**里的同名技能仍然各占一行——那是真实的遮蔽,不是重复',
222
+ where: '技能列表、安装到',
223
+ },
224
+ {
225
+ text: '**「哪一份生效」以前说反了**:同名技能里,面板指着的其实是**模型收不到**的那份,于是你可能照着提示去操作了另一份。现在按 DSH 的优先级如实判定(项目级优先于全局),并说明生效的那份在哪',
226
+ where: '技能列表的「被遮蔽」徽章与诊断页',
227
+ },
228
+ ],
229
+ },
204
230
  ]
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "dsh-toolbox-ui",
3
- "version": "1.6.2",
3
+ "version": "1.6.4",
4
4
  "description": "Responsive MCP management UI and skills manager for DeepSeek Harness Web — floating panel, JSON import, profile-backed Host Remote management, and a skill market with a hard license gate.",
5
5
  "repository": {
6
6
  "type": "git",
7
- "url": "git+https://github.com/Imzl-zl/dsh-mcp-manager-ui.git"
7
+ "url": "git+https://github.com/Imzl-zl/dsh-toolbox-ui.git"
8
8
  },
9
- "homepage": "https://github.com/Imzl-zl/dsh-mcp-manager-ui#readme",
9
+ "homepage": "https://github.com/Imzl-zl/dsh-toolbox-ui#readme",
10
10
  "bugs": {
11
- "url": "https://github.com/Imzl-zl/dsh-mcp-manager-ui/issues"
11
+ "url": "https://github.com/Imzl-zl/dsh-toolbox-ui/issues"
12
12
  },
13
13
  "type": "module",
14
14
  "main": "lib/index.js",