@michengai/dsh-skills-manager 0.1.18 → 0.1.19

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.md CHANGED
@@ -1,41 +1,66 @@
1
- <p align="center">
2
- <img src="assets/icon.png" alt="DSH Skills Manager" width="96">
3
- </p>
1
+ <div align="center">
2
+ <table cellpadding="0" cellspacing="0">
3
+ <tr>
4
+ <td valign="middle" align="center">
5
+ <img src="assets/branding/dsh-logo.png" alt="DeepSeek Harness logo" width="168">
6
+ </td>
7
+ <td valign="middle">
8
+ <pre>
9
+ ____ ____ _ _
10
+ | _ \/ ___|| | | |
11
+ | | | \___ \| |_| |
12
+ | |_| |___) | _ |
13
+ |____/|____/|_| |_|
14
+ SKILLS MANAGER
15
+ Local + shared Agent skills
16
+ DeepSeek Harness plugin
17
+ </pre>
18
+ </td>
19
+ </tr>
20
+ </table>
21
+ </div>
22
+
23
+ <div align="center">
24
+
25
+ # DSH Skills Manager
26
+
27
+ **Safely manage local skills and inspect shared Agent skills in DeepSeek Harness**
28
+
29
+ [简体中文](README.zh-CN.md) · [Apache-2.0](LICENSE)
30
+
31
+ [![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](LICENSE)
32
+ [![npm package](https://img.shields.io/npm/v/%40michengai%2Fdsh-skills-manager.svg?label=npm%20package)](https://www.npmjs.com/package/@michengai/dsh-skills-manager)
33
+ [![DSH Web Plugin](https://img.shields.io/badge/DSH%20Web-Plugin-0f766e.svg)](https://github.com/MichengAI/dsh-skills-manager)
34
+ [![Node.js 20 or later](https://img.shields.io/badge/Node.js-20%20or%20later-339933.svg?logo=node.js&logoColor=white)](https://nodejs.org/)
35
+ </div>
36
+
37
+ > DSH Skills Manager is a community-maintained DeepSeek Harness (DSH) plugin, not an official DeepSeek AI product.
4
38
 
5
- <h1 align="center">DSH Skills Manager</h1>
39
+ ## Features
6
40
 
7
- <p align="center">
8
- <strong>A DeepSeek Harness Web plugin for safely managing local skills and viewing shared Agent skills.</strong>
9
- </p>
41
+ - Filter by category or search, then inspect DSH-local and shared Agent skills in **Settings → Skills**.
42
+ - Enable, disable, upload, replace, and delete DSH-local skills.
43
+ - Keep shared Agent skills strictly read-only and never change their global metadata.
44
+ - Import a plugin directory containing `SKILL.md` through the native file picker and confirm every name collision.
45
+ - Paste one sentence into DSH, Codex, or WorkBuddy and let that agent install the plugin locally.
10
46
 
11
- <p align="center">
12
- <a href="https://github.com/MichengAI/dsh-skills-manager/issues">Report an issue</a>
13
- · <a href="https://www.npmjs.com/package/@michengai/dsh-skills-manager">View on npm</a>
14
- · <a href="README.zh-CN.md">简体中文</a>
15
- </p>
47
+ ## Screenshots
16
48
 
17
- <p align="center">
18
- <a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-blue.svg" alt="Apache License 2.0"></a>
19
- <a href="https://www.npmjs.com/package/@michengai/dsh-skills-manager"><img src="https://img.shields.io/npm/v/%40michengai/dsh-skills-manager?label=npm" alt="npm package"></a>
20
- <img src="https://img.shields.io/badge/DSH-Web%20Plugin-10b981" alt="DSH Web Plugin">
21
- <img src="https://img.shields.io/badge/Node.js-%E2%89%A520-339933?logo=nodedotjs&logoColor=white" alt="Node.js 20 or later">
22
- </p>
49
+ Filter by category or search in **Settings → Skills**. DSH-local skills can be enabled, disabled, or deleted; shared Agent skills stay read-only:
23
50
 
24
- > DSH Skills Manager is a community-maintained plugin, not an official DeepSeek AI product.
51
+ ![Skills Manager settings page](assets/screenshots/skills-manager-en.png)
25
52
 
26
- <p align="center">
27
- <img src="assets/screenshots/skills-manager.png" alt="Skills Manager settings page" width="960">
28
- </p>
53
+ Shared Agent skills stay visible when the DSH-local group is empty:
29
54
 
30
- > The screenshot shows DSH-local skills with enable, disable, and delete actions. Shared Agent skills stay read-only.
55
+ ![Shared Agent skills remain visible](assets/screenshots/skills-public.png)
31
56
 
32
- ## Features
57
+ The compact upload dialog accepts a plugin `SKILL.md`, a plugin directory, or drag-and-drop:
58
+
59
+ ![Upload plugin dialog](assets/screenshots/upload-plugin.png)
60
+
61
+ Enabling a DSH-local skill restores its `/` command in the chat composer:
33
62
 
34
- - Shows local `$DSH_HOME\skills` and shared `$DSH_AGENTS_HOME\skills` in one Settings page.
35
- - Enables, disables, uploads, replaces, and deletes DSH-local skills.
36
- - Keeps shared Agent skills strictly read-only and never changes their global metadata.
37
- - Imports a plugin directory containing `SKILL.md` through the native file picker and confirms every name collision.
38
- - Rejects traversal names, imports from the DSH skills directory itself, and symbolic links.
63
+ ![Chat slash command after enabling a local skill](assets/screenshots/slash-command.png)
39
64
 
40
65
  ## Prerequisites
41
66
 
@@ -45,18 +70,54 @@
45
70
 
46
71
  ## Installation
47
72
 
48
- ### Install from npm
73
+ `dsh plugin add` forwards to `pnpm add` in the profile directory. Without a version and official registry, a local mirror or minimum-release-age policy can leave you on an older build.
74
+
75
+ ### Ask another agent to install it
76
+
77
+ This plugin runs inside DeepSeek Harness Web. Copy one of the sentences below into DSH, Codex, or WorkBuddy and let that agent install it into your local `web` profile.
78
+
79
+ From npm:
80
+
81
+ ```text
82
+ Install the latest DSH plugin @michengai/dsh-skills-manager into my local web profile using the official npm registry: dsh plugin --profile web add @michengai/dsh-skills-manager@latest --registry=https://registry.npmjs.org/. Then run dsh --profile web --dump-config, confirm skills-manager is mounted, and remind me to restart DSH Web and hard-refresh the browser.
83
+ ```
84
+
85
+ From source:
86
+
87
+ ```text
88
+ Install the DSH plugin from source at https://github.com/MichengAI/dsh-skills-manager: clone it, run npm install and npm test, then run dsh plugin --profile web add . from that directory. Do not copy lib by itself. Then run dsh --profile web --dump-config, confirm skills-manager is mounted, and remind me to restart DSH Web and hard-refresh the browser.
89
+ ```
90
+
91
+ | Product | How to use it |
92
+ | --- | --- |
93
+ | DSH | Send one of the sentences above to the current session. |
94
+ | Codex | Send one of the sentences above to Codex and let it install locally. |
95
+ | WorkBuddy | Send one of the sentences above to WorkBuddy; for a source install you can also paste `https://github.com/MichengAI/dsh-skills-manager`. |
96
+
97
+ Codex and WorkBuddy only install the plugin. After that, open DSH Web and use **Settings → Skills**.
49
98
 
50
- Run this from any PowerShell directory. Install into the DSH profile through `dsh plugin`:
99
+ You can also run the same npm command yourself:
100
+
101
+ ```powershell
102
+ dsh plugin --profile web add @michengai/dsh-skills-manager@latest --registry=https://registry.npmjs.org/
103
+ ```
104
+
105
+ If `dsh` is not on PATH, replace the leading `dsh` with `npx --yes @deepseek-ai/dsh`.
106
+
107
+ ### Install the latest package from the official npm registry
108
+
109
+ Run this from any PowerShell directory:
51
110
 
52
111
  ```powershell
53
112
  [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
54
113
  $OutputEncoding = [System.Text.Encoding]::UTF8
55
- dsh plugin --profile web add @michengai/dsh-skills-manager
114
+ dsh plugin --profile web add @michengai/dsh-skills-manager@latest --registry=https://registry.npmjs.org/
56
115
  dsh --profile web --dump-config
57
116
  ```
58
117
 
59
- Restart DSH Web or reload the active Web profile. If a package mirror is behind, append `--registry=https://registry.npmjs.org/`.
118
+ To pin a release, replace `@latest` with a version such as `@0.1.19`.
119
+
120
+ The configuration output should contain `skills-manager`. Restart DSH Web and hard-refresh the browser. Do not copy client files manually: `dsh plugin add` also applies `cordis.patch.yml`.
60
121
 
61
122
  ### Install from source
62
123
 
@@ -74,7 +135,7 @@ dsh plugin --profile web add .
74
135
  dsh --profile web --dump-config
75
136
  ```
76
137
 
77
- Restart DSH Web or reload the active Web profile. Local installation reads and applies `cordis.patch.yml`; do not copy `lib` files manually.
138
+ Restart DSH Web and hard-refresh the browser. `dsh plugin ... add .` reads the package metadata and `cordis.patch.yml`; do not install by copying `lib` directly.
78
139
 
79
140
  ## Usage
80
141
 
@@ -82,37 +143,18 @@ Open **Settings → Skills**, then use the panel as follows:
82
143
 
83
144
  | Goal | Action | Scope |
84
145
  | --- | --- | --- |
146
+ | Search or filter | Use **Category** and **Search** to narrow by directory, name, kind, or description. | DSH and shared Agent skills |
85
147
  | Inspect a skill | Review its name, form, description, and invocation state. | DSH and shared Agent skills |
86
148
  | Enable or disable | Select **Enable** or **Disable**; this controls model invocation and the `/` command. | DSH-local skills only |
87
- | Upload a plugin | Select **Upload Plugin**, then choose the plugin directory’s `SKILL.md`. Its scripts and resources are copied too. | DSH-local skills only |
149
+ | Upload a plugin | Select **Upload**, then choose the plugin directory’s `SKILL.md`. Its scripts and resources are copied too. | DSH-local skills only |
88
150
  | Choose the directory | If the selected file has no usable path, choose the directory containing `SKILL.md`. | DSH-local skills only |
89
151
  | Replace or delete | Confirm a name collision, or select **Delete** for an unneeded local skill. | DSH-local skills only |
90
152
  | Inspect shared skills | Review shared Agent skills without changing their metadata. | Read-only |
91
153
 
92
- <p align="center">
93
- <img src="assets/screenshots/skills-public.png" alt="Shared Agent skills remain visible when no local skills are installed" width="960">
94
- </p>
95
-
96
- > Shared Agent skills stay visible when the DSH-local group is empty.
97
-
98
- <p align="center">
99
- <img src="assets/screenshots/upload-plugin.png" alt="Upload plugin dialog" width="960">
100
- </p>
101
-
102
- > The compact upload dialog accepts a plugin `SKILL.md`, a plugin directory, or drag-and-drop.
103
-
104
- <p align="center">
105
- <img src="assets/screenshots/delete-plugin.png" alt="Delete plugin confirmation" width="960">
106
- </p>
154
+ ![Delete plugin confirmation](assets/screenshots/delete-plugin.png)
107
155
 
108
156
  > Deleting a DSH-local skill requires confirmation and cannot be undone.
109
157
 
110
- <p align="center">
111
- <img src="assets/screenshots/slash-command.png" alt="Chat slash command after enabling a local skill" width="640">
112
- </p>
113
-
114
- > Enabling a DSH-local skill restores its `/` command in the chat composer.
115
-
116
158
  Escape closes only the frontmost upload or confirmation dialog and leaves Settings open.
117
159
 
118
160
  ## Permissions and safety limits
@@ -165,4 +207,3 @@ npm run pack:check
165
207
  Project status, usage boundaries, architecture, and iteration records begin at the [documentation entry point](docs/00-交接入口/00-阅读导航.md). The detailed operational guide is `docs\02-产品与业务\01-使用说明.md`.
166
208
 
167
209
  Licensed under [Apache License 2.0](LICENSE).
168
-
package/README.zh-CN.md CHANGED
@@ -1,41 +1,66 @@
1
- <p align="center">
2
- <img src="assets/icon.png" alt="DSH Skills Manager" width="96">
3
- </p>
1
+ <div align="center">
2
+ <table cellpadding="0" cellspacing="0">
3
+ <tr>
4
+ <td valign="middle" align="center">
5
+ <img src="assets/branding/dsh-logo.png" alt="DeepSeek Harness 标识" width="168">
6
+ </td>
7
+ <td valign="middle">
8
+ <pre>
9
+ ____ ____ _ _
10
+ | _ \/ ___|| | | |
11
+ | | | \___ \| |_| |
12
+ | |_| |___) | _ |
13
+ |____/|____/|_| |_|
14
+ SKILLS MANAGER
15
+ Local + shared Agent skills
16
+ DeepSeek Harness plugin
17
+ </pre>
18
+ </td>
19
+ </tr>
20
+ </table>
21
+ </div>
22
+
23
+ <div align="center">
24
+
25
+ # DSH Skills Manager
26
+
27
+ **在 DeepSeek Harness 中安全管理本地技能,并查看公共 Agent 技能**
28
+
29
+ [English](README.md) · [Apache-2.0](LICENSE)
30
+
31
+ [![许可证:Apache-2.0](https://img.shields.io/badge/许可证-Apache--2.0-blue.svg)](LICENSE)
32
+ [![npm package](https://img.shields.io/npm/v/%40michengai%2Fdsh-skills-manager.svg?label=npm%20package)](https://www.npmjs.com/package/@michengai/dsh-skills-manager)
33
+ [![DSH Web Plugin](https://img.shields.io/badge/DSH%20Web-Plugin-0f766e.svg)](https://github.com/MichengAI/dsh-skills-manager)
34
+ [![Node.js 20 or later](https://img.shields.io/badge/Node.js-20%20or%20later-339933.svg?logo=node.js&logoColor=white)](https://nodejs.org/)
35
+ </div>
36
+
37
+ > DSH Skills Manager 是社区维护的 DeepSeek Harness(DSH)插件,并非 DeepSeek AI 官方产品。
4
38
 
5
- <h1 align="center">DSH Skills Manager</h1>
39
+ ## 功能概览
6
40
 
7
- <p align="center">
8
- <strong>安全管理本地技能并查看公共 Agent 技能的 DeepSeek Harness Web 插件。</strong>
9
- </p>
41
+ - 在「设置 → 技能」中按分类筛选或搜索,查看 DSH 本地技能与公共 Agent 技能。
42
+ - 对 DSH 本地技能执行启用、停用、上传、覆盖和删除。
43
+ - 公共 Agent 技能始终只读,不改写共享的全局元数据。
44
+ - 使用系统文件选择器导入包含 `SKILL.md` 的插件目录,并对同名覆盖强制确认。
45
+ - 可把一句话复制到 DSH、Codex 或 WorkBuddy,让对方代装到本机 DSH。
10
46
 
11
- <p align="center">
12
- <a href="https://github.com/MichengAI/dsh-skills-manager/issues">反馈问题</a>
13
- · <a href="https://www.npmjs.com/package/@michengai/dsh-skills-manager">在 npm 查看</a>
14
- · <a href="README.md">English</a>
15
- </p>
47
+ ## 界面预览
16
48
 
17
- <p align="center">
18
- <a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-blue.svg" alt="Apache License 2.0"></a>
19
- <a href="https://www.npmjs.com/package/@michengai/dsh-skills-manager"><img src="https://img.shields.io/npm/v/%40michengai/dsh-skills-manager?label=npm" alt="npm 包"></a>
20
- <img src="https://img.shields.io/badge/DSH-Web%20Plugin-10b981" alt="DSH Web Plugin">
21
- <img src="https://img.shields.io/badge/Node.js-%E2%89%A520-339933?logo=nodedotjs&logoColor=white" alt="Node.js 20 或更高版本">
22
- </p>
49
+ 在「设置 → 技能」中按分类筛选或搜索。DSH 本地技能可启用、停用和删除;公共 Agent 技能只读:
23
50
 
24
- > DSH Skills Manager 是社区维护的插件,并非 DeepSeek AI 官方产品。
51
+ ![技能管理设置页面](assets/screenshots/skills-manager.png)
25
52
 
26
- <p align="center">
27
- <img src="assets/screenshots/skills-manager.png" alt="技能管理设置页面" width="960">
28
- </p>
53
+ 本地 DSH 技能为空时,公共 Agent 技能仍然可见:
29
54
 
30
- > 截图展示 DSH 本地技能的启用、停用和删除入口。公共 Agent 技能保持只读,不显示操作按钮。
55
+ ![未安装本地技能时仍可查看公共 Agent 技能](assets/screenshots/skills-public.png)
31
56
 
32
- ## 功能概览
57
+ 紧凑上传弹窗支持选择 `SKILL.md`、选择插件目录,或直接拖放文件:
33
58
 
34
- - 在同一设置页查看本地 `$DSH_HOME\skills` 与公共 `$DSH_AGENTS_HOME\skills`。
35
- - 对 DSH 本地技能执行启用、停用、上传、覆盖和删除。
36
- - 公共 Agent 技能始终只读,不改写共享的全局元数据。
37
- - 使用系统文件选择器导入包含 `SKILL.md` 的插件目录,并对同名覆盖强制确认。
38
- - 拒绝目录穿越、从 DSH 技能目录导入自身及符号链接导入。
59
+ ![上传插件弹窗](assets/screenshots/upload-plugin.png)
60
+
61
+ 启用 DSH 本地技能后,聊天输入框会恢复对应的 `/` 命令:
62
+
63
+ ![启用本地技能后的斜杠命令](assets/screenshots/slash-command.png)
39
64
 
40
65
  ## 前置条件
41
66
 
@@ -45,22 +70,58 @@
45
70
 
46
71
  ## 安装
47
72
 
48
- ### 从 npm 安装
73
+ `dsh plugin add` 会转发到 profile 目录里的 `pnpm add`。不写版本、不指定官方源时,本机镜像和最短发布间隔可能让你停在旧版。
74
+
75
+ ### 交给其他 Agent 一句话安装
76
+
77
+ 本插件运行在 DeepSeek Harness Web 里。把下面其中一句复制到 DSH、Codex 或 WorkBuddy,让它代你安装到本机 `web` profile。
78
+
79
+ 从 npm 安装:
80
+
81
+ ```text
82
+ 请把 DSH 插件 @michengai/dsh-skills-manager 最新版装进本机 web profile,使用官方 npm 源执行:dsh plugin --profile web add @michengai/dsh-skills-manager@latest --registry=https://registry.npmjs.org/。装完执行 dsh --profile web --dump-config,确认已挂载 skills-manager,并提醒我重启 DSH Web 后硬刷新浏览器。
83
+ ```
84
+
85
+ 从源码安装:
86
+
87
+ ```text
88
+ 请从 https://github.com/MichengAI/dsh-skills-manager 安装 DSH 插件:克隆仓库,执行 npm install 和 npm test,再在该目录执行 dsh plugin --profile web add .。不要只复制 lib。然后执行 dsh --profile web --dump-config,确认已挂载 skills-manager,并提醒我重启 DSH Web 后硬刷新浏览器。
89
+ ```
90
+
91
+ | 产品 | 怎么用 |
92
+ | --- | --- |
93
+ | DSH | 把上面其中一句发给当前会话。 |
94
+ | Codex | 把上面其中一句发给 Codex,让它在本机执行安装。 |
95
+ | WorkBuddy | 把上面其中一句发给 WorkBuddy;源码安装也可同时粘贴仓库地址 `https://github.com/MichengAI/dsh-skills-manager`。 |
96
+
97
+ Codex 和 WorkBuddy 只负责代装;装好后仍要打开 DSH Web 使用「设置 → 技能」。
98
+
99
+ 也可以自己执行同一条 npm 命令:
100
+
101
+ ```powershell
102
+ dsh plugin --profile web add @michengai/dsh-skills-manager@latest --registry=https://registry.npmjs.org/
103
+ ```
104
+
105
+ 未把 `dsh` 装进 PATH 时,把开头的 `dsh` 换成 `npx --yes @deepseek-ai/dsh`。
106
+
107
+ ### 从官方 npm 安装最新版
49
108
 
50
- 在任意 PowerShell 目录执行。请通过 `dsh plugin` 安装到 DSH profile:
109
+ 在任意 PowerShell 目录执行:
51
110
 
52
111
  ```powershell
53
112
  [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
54
113
  $OutputEncoding = [System.Text.Encoding]::UTF8
55
- dsh plugin --profile web add @michengai/dsh-skills-manager
114
+ dsh plugin --profile web add @michengai/dsh-skills-manager@latest --registry=https://registry.npmjs.org/
56
115
  dsh --profile web --dump-config
57
116
  ```
58
117
 
59
- 安装或升级后重启 DSH Web,或重新加载当前 Web profile。若镜像未同步最新版本,可在安装命令末尾追加 `--registry=https://registry.npmjs.org/`。
118
+ 需要钉死某一版时,把 `@latest` 换成具体版本,例如 `@0.1.19`。
119
+
120
+ 配置输出中应包含 `skills-manager`。安装后重启 DSH Web 并在浏览器硬刷新。不要手工复制客户端文件,`dsh plugin add` 会同时应用 `cordis.patch.yml`。
60
121
 
61
122
  ### 从源码安装
62
123
 
63
- 适用于调试或使用未发布改动。克隆后的目录会直接作为插件安装路径:
124
+ 适用于调试或使用未发布改动。克隆后的本地路径就是插件安装路径:
64
125
 
65
126
  ```powershell
66
127
  [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
@@ -74,7 +135,7 @@ dsh plugin --profile web add .
74
135
  dsh --profile web --dump-config
75
136
  ```
76
137
 
77
- 完成后重启 DSH Web 或重新加载当前 Web profile。`dsh plugin ... add .` 会自动读取并应用 `cordis.patch.yml`;不要手工复制 `lib` 文件。
138
+ 完成后重启 DSH Web 并硬刷新浏览器。`dsh plugin ... add .` 会读取当前目录的包信息和 `cordis.patch.yml`;不要改为直接复制 `lib` 目录。
78
139
 
79
140
  ## 使用
80
141
 
@@ -82,6 +143,7 @@ dsh --profile web --dump-config
82
143
 
83
144
  | 目标 | 操作 | 范围 |
84
145
  | --- | --- | --- |
146
+ | 搜索或筛选 | 用「分类」和「搜索」按目录、名称、形态或简介收窄列表。 | DSH 与公共 Agent 技能 |
85
147
  | 查看技能 | 查看名称、形态、说明和调用状态。 | DSH 与公共 Agent 技能 |
86
148
  | 启用或停用 | 点击「启用」或「停用」。它会同步控制模型调用和 `/` 手动命令。 | 仅 DSH 本地技能 |
87
149
  | 上传插件 | 点击「上传插件」,选择插件目录内的 `SKILL.md`。完整目录、脚本和资源都会被复制。 | 仅 DSH 本地技能 |
@@ -89,30 +151,10 @@ dsh --profile web --dump-config
89
151
  | 覆盖或删除 | 同名时确认覆盖;使用「删除」移除不再需要的本地技能。 | 仅 DSH 本地技能 |
90
152
  | 查看公共技能 | 查看公共 Agent 技能,不修改其元数据。 | 只读 |
91
153
 
92
- <p align="center">
93
- <img src="assets/screenshots/skills-public.png" alt="未安装本地技能时仍可查看公共 Agent 技能" width="960">
94
- </p>
95
-
96
- > 本地 DSH 技能为空时,公共 Agent 技能仍然可见。
97
-
98
- <p align="center">
99
- <img src="assets/screenshots/upload-plugin.png" alt="上传插件弹窗" width="960">
100
- </p>
101
-
102
- > 紧凑上传弹窗支持选择 `SKILL.md`、选择插件目录,或直接拖放文件。
103
-
104
- <p align="center">
105
- <img src="assets/screenshots/delete-plugin.png" alt="删除插件确认框" width="960">
106
- </p>
154
+ ![删除插件确认框](assets/screenshots/delete-plugin.png)
107
155
 
108
156
  > 删除 DSH 本地技能需要确认,删除后无法恢复。
109
157
 
110
- <p align="center">
111
- <img src="assets/screenshots/slash-command.png" alt="启用本地技能后的斜杠命令" width="640">
112
- </p>
113
-
114
- > 启用 DSH 本地技能后,聊天输入框会恢复对应的 `/` 命令。
115
-
116
158
  按 ESC 只关闭最上层上传框或确认框,设置页会保持打开。
117
159
 
118
160
  ## 权限与安全边界
@@ -165,4 +207,3 @@ npm run pack:check
165
207
  项目状态、使用边界、技术架构和迭代记录从[文档交接入口](docs/00-交接入口/00-阅读导航.md)开始。详细操作说明见 `docs\02-产品与业务\01-使用说明.md`。
166
208
 
167
209
  本项目采用 [Apache License 2.0](LICENSE)。
168
-
Binary file
package/lib/client.js CHANGED
@@ -478,6 +478,7 @@ window.__ModuleLoader__.load({
478
478
  var deleteState = react.useState(null);
479
479
  var confirmDelete = deleteState[0];
480
480
  var setConfirmDelete = deleteState[1];
481
+ var inflightRef = react.useRef(false);
481
482
  var queryState = react.useState("");
482
483
  var query = queryState[0];
483
484
  var setQuery = queryState[1];
@@ -524,12 +525,15 @@ window.__ModuleLoader__.load({
524
525
  }, [uploadOpen, confirmImport, confirmDelete]);
525
526
 
526
527
  function action(path, body) {
527
- if (busy) return;
528
+ if (busy || inflightRef.current) return;
529
+ inflightRef.current = true;
528
530
  setBusy(path);
529
531
  callApi(path, { method: "POST", headers: MUTATION_HEADERS, body: JSON.stringify(body || {}) }).then(function () {
532
+ inflightRef.current = false;
530
533
  setBusy(null);
531
534
  refresh();
532
535
  }).catch(function (error) {
536
+ inflightRef.current = false;
533
537
  setBusy(null);
534
538
  setSnapshot({ loading: false, error: error, data: snapshot.data });
535
539
  });
@@ -574,31 +578,38 @@ window.__ModuleLoader__.load({
574
578
  }
575
579
 
576
580
  function executeImport(source, conflict) {
581
+ if (inflightRef.current && busy !== "import-check") return;
582
+ inflightRef.current = true;
577
583
  setBusy("import");
578
584
  setConfirmImport(null);
579
585
  callApi("/import", { method: "POST", headers: MUTATION_HEADERS, body: JSON.stringify({ source: source, conflict: conflict }) }).then(function (data) {
586
+ inflightRef.current = false;
580
587
  setBusy(null);
581
588
  setImportResult(data);
582
589
  setSelected(null);
583
590
  setUploadOpen(false);
584
591
  refresh();
585
592
  }).catch(function (error) {
593
+ inflightRef.current = false;
586
594
  setBusy(null);
587
595
  setImportResult({ error: error });
588
596
  });
589
597
  }
590
598
 
591
599
  function installSelected() {
592
- if (!selected || busy) return;
600
+ if (!selected || busy || inflightRef.current) return;
601
+ inflightRef.current = true;
593
602
  setBusy("import-check");
594
603
  callApi("/import", { method: "POST", headers: MUTATION_HEADERS, body: JSON.stringify({ source: selected.source, dryRun: true }) }).then(function (data) {
595
604
  if (data.conflicts && data.conflicts.length) {
605
+ inflightRef.current = false;
596
606
  setBusy(null);
597
607
  setConfirmImport({ source: selected.source, conflicts: data.conflicts });
598
608
  return;
599
609
  }
600
610
  executeImport(selected.source, "skip");
601
611
  }).catch(function (error) {
612
+ inflightRef.current = false;
602
613
  setBusy(null);
603
614
  setImportResult({ error: error });
604
615
  });
@@ -616,7 +627,7 @@ window.__ModuleLoader__.load({
616
627
  h("span", { className: "dssm-tag " + (policyValid && enabled ? "dssm-tag-on" : "dssm-tag-off") }, policyValid ? (enabled ? t("status.enabled") : t("status.disabled")) : t("status.invalid"))),
617
628
  h("div", { className: "dssm-note", title: skill.description || "" }, skill.description || t("note.missing"))),
618
629
  root.mutable ? h("button", { className: "dssm-btn dssm-btn-secondary", disabled: !!busy, onClick: function () { action(actionEnabled ? "/disable" : "/enable", { name: skill.name, root: root.key }); } }, actionEnabled ? t("btn.disable") : policyValid ? t("btn.enable") : t("btn.repair.enable")) : null,
619
- root.mutable ? h("button", { className: "dssm-btn dssm-btn-danger", disabled: !!busy, onClick: function () { setConfirmDelete(skill); } }, t("btn.delete")) : null);
630
+ root.mutable ? h("button", { className: "dssm-btn dssm-btn-danger", disabled: !!busy, onClick: function () { setConfirmDelete({ name: skill.name, root: root.key }); } }, t("btn.delete")) : null);
620
631
  }
621
632
 
622
633
  var nodes = [h("style", { key: "css" }, CSS)];
@@ -692,7 +703,10 @@ window.__ModuleLoader__.load({
692
703
  } else if (snapshot.loading) {
693
704
  nodes.push(h("div", { key: "loading", className: "dssm-note" }, t("loading")));
694
705
  }
695
- if (importResult) {
706
+ function isUploadPickerError(result) {
707
+ return !!(result && typeof result.code === "string" && (result.code.indexOf("select.") === 0 || result.code.indexOf("dir.") === 0));
708
+ }
709
+ if (importResult && !isUploadPickerError(importResult)) {
696
710
  var nameSeparator = t("sep.names");
697
711
  var errorSeparator = t("sep.errors");
698
712
  // 错误形态:业务/协议错误带 code,或 host 失败带 error(Error 对象);结果形态只有 imported/failed/skipped。
@@ -739,7 +753,7 @@ window.__ModuleLoader__.load({
739
753
  nodes.push(h("div", { key: "import-confirm", className: "dssm-mask" }, h("div", { ref: modalRef, tabIndex: -1, className: "dssm-modal", role: "dialog", "aria-modal": "true", "aria-labelledby": "dssm-import-title" }, h("div", { className: "dssm-modal-head" }, h("h3", { id: "dssm-import-title", className: "dssm-modal-title" }, t("confirm.overwrite.title"))), h("p", { className: "dssm-desc" }, t("confirm.overwrite.desc", { names: confirmImport.conflicts.map(function (item) { return item.name; }).join(t("sep.names")) })), h("div", { className: "dssm-modal-actions" }, h("button", { className: "dssm-btn dssm-btn-secondary", onClick: function () { setConfirmImport(null); } }, t("btn.cancel")), h("button", { className: "dssm-btn", disabled: !!busy, onClick: function () { executeImport(confirmImport.source, "overwrite"); } }, t("btn.overwrite.upload"))))));
740
754
  }
741
755
  if (confirmDelete) {
742
- nodes.push(h("div", { key: "delete-confirm", className: "dssm-mask" }, h("div", { ref: modalRef, tabIndex: -1, className: "dssm-modal", role: "dialog", "aria-modal": "true", "aria-labelledby": "dssm-delete-title" }, h("div", { className: "dssm-modal-head" }, h("h3", { id: "dssm-delete-title", className: "dssm-modal-title" }, t("confirm.delete.title"))), h("p", { className: "dssm-desc" }, t("confirm.delete.desc", { name: confirmDelete.name })), h("div", { className: "dssm-modal-actions" }, h("button", { className: "dssm-btn dssm-btn-secondary", disabled: !!busy, onClick: function () { setConfirmDelete(null); } }, t("btn.cancel")), h("button", { className: "dssm-btn dssm-btn-danger", disabled: !!busy, onClick: function () { setConfirmDelete(null); action("/delete", { name: confirmDelete.name }); } }, t("btn.delete.confirm"))))));
756
+ nodes.push(h("div", { key: "delete-confirm", className: "dssm-mask" }, h("div", { ref: modalRef, tabIndex: -1, className: "dssm-modal", role: "dialog", "aria-modal": "true", "aria-labelledby": "dssm-delete-title" }, h("div", { className: "dssm-modal-head" }, h("h3", { id: "dssm-delete-title", className: "dssm-modal-title" }, t("confirm.delete.title"))), h("p", { className: "dssm-desc" }, t("confirm.delete.desc", { name: confirmDelete.name })), h("div", { className: "dssm-modal-actions" }, h("button", { className: "dssm-btn dssm-btn-secondary", disabled: !!busy, onClick: function () { setConfirmDelete(null); } }, t("btn.cancel")), h("button", { className: "dssm-btn dssm-btn-danger", disabled: !!busy, onClick: function () { setConfirmDelete(null); action("/delete", { name: confirmDelete.name, root: confirmDelete.root || "dsh" }); } }, t("btn.delete.confirm"))))));
743
757
  }
744
758
  return h("section", { className: "dssm-section" }, nodes);
745
759
  }
package/lib/core.js CHANGED
@@ -18,6 +18,7 @@ import { randomUUID } from "node:crypto";
18
18
  const KEBAB_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
19
19
  const WINDOWS_DEVICE_NAME_RE = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/i;
20
20
  const MAX_SOURCE_DEPTH = 64;
21
+ const MAX_ENTRY_NAME_LENGTH = 128;
21
22
 
22
23
  // ── 业务错误码 ────────────────────────────────────────────────────────────────
23
24
 
@@ -78,14 +79,38 @@ function isSameOrDescendant(parent, child) {
78
79
  return rel === "" || (!isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep}`));
79
80
  }
80
81
 
82
+ /** 解析真实路径;中间若有目录链接,按落地目录比较重叠。 */
83
+ async function resolvedPath(path) {
84
+ try {
85
+ return await fs.realpath(path);
86
+ } catch {
87
+ return resolve(path);
88
+ }
89
+ }
90
+
81
91
  /** 两个路径重叠时,覆盖导入可能删除自身来源,必须拒绝。 */
82
- function pathsOverlap(a, b) {
83
- return isSameOrDescendant(a, b) || isSameOrDescendant(b, a);
92
+ async function pathsOverlap(a, b) {
93
+ const left = await resolvedPath(a);
94
+ const right = await resolvedPath(b);
95
+ return isSameOrDescendant(left, right) || isSameOrDescendant(right, left);
96
+ }
97
+
98
+ /** 真实落地路径必须仍在技能根内,避免符号链接写出根外。 */
99
+ async function isInsideRoot(root, path) {
100
+ return isSameOrDescendant(await resolvedPath(root), await resolvedPath(path));
101
+ }
102
+
103
+ async function lstatOrNull(path) {
104
+ try {
105
+ return await fs.lstat(path);
106
+ } catch {
107
+ return null;
108
+ }
84
109
  }
85
110
 
86
111
  /** 名称只允许一个普通路径段;不把既有技能名称限制为 kebab-case。 */
87
112
  export function entryPath(root, name) {
88
- if (typeof name !== "string" || name === "" || name === "." || name === ".." || /[\\/:*?"<>|\0]/.test(name) || /[. ]$/.test(name) || WINDOWS_DEVICE_NAME_RE.test(name) || basename(name) !== name) return null;
113
+ if (typeof name !== "string" || name === "" || name === "." || name === ".." || name.startsWith(".") || name.length > MAX_ENTRY_NAME_LENGTH || /[\\/:*?"<>|\0]/.test(name) || /[. ]$/.test(name) || WINDOWS_DEVICE_NAME_RE.test(name) || basename(name) !== name) return null;
89
114
  const rootPath = resolve(root);
90
115
  const path = resolve(rootPath, name);
91
116
  return isSameOrDescendant(rootPath, path) && rootPath !== path ? path : null;
@@ -122,7 +147,7 @@ function stripBom(text) {
122
147
  export function parseSkillDoc(text) {
123
148
  const src = stripBom(String(text));
124
149
  const lines = src.split(/\r?\n/);
125
- const map = {};
150
+ const map = Object.create(null);
126
151
  const fields = [];
127
152
  let body = src;
128
153
  if (lines.length > 0 && lines[0].trim() === "---") {
@@ -223,17 +248,20 @@ export async function resolveEntry(root, name) {
223
248
  const bundlePath = entryPath(root, name);
224
249
  if (bundlePath === null) return null;
225
250
  const rootPath = resolve(root);
226
- const bundleDoc = join(bundlePath, "SKILL.md");
227
- try {
228
- const st = await fs.stat(bundleDoc);
229
- if (st.isFile()) return { kind: "bundle", docPath: bundleDoc, entryPath: bundlePath };
230
- } catch {}
251
+ const bundleStat = await lstatOrNull(bundlePath);
252
+ if (bundleStat && bundleStat.isDirectory() && !bundleStat.isSymbolicLink()) {
253
+ const bundleDoc = join(bundlePath, "SKILL.md");
254
+ const docStat = await lstatOrNull(bundleDoc);
255
+ if (docStat && docStat.isFile() && !docStat.isSymbolicLink() && await isInsideRoot(rootPath, bundleDoc)) {
256
+ return { kind: "bundle", docPath: bundleDoc, entryPath: bundlePath };
257
+ }
258
+ }
231
259
  const flatDoc = resolve(rootPath, `${name}.md`);
232
260
  if (!isSameOrDescendant(rootPath, flatDoc) || rootPath === flatDoc) return null;
233
- try {
234
- const st = await fs.stat(flatDoc);
235
- if (st.isFile()) return { kind: "flat", docPath: flatDoc, entryPath: flatDoc };
236
- } catch {}
261
+ const flatStat = await lstatOrNull(flatDoc);
262
+ if (flatStat && flatStat.isFile() && !flatStat.isSymbolicLink() && await isInsideRoot(rootPath, flatDoc)) {
263
+ return { kind: "flat", docPath: flatDoc, entryPath: flatDoc };
264
+ }
237
265
  return null;
238
266
  }
239
267
 
@@ -263,32 +291,36 @@ export async function scanEntries(root) {
263
291
  } catch {
264
292
  return { exists: false, entries: [] };
265
293
  }
266
- const entries = [];
294
+ const byName = new Map();
267
295
  for (const it of items) {
268
296
  try {
297
+ if (it.isSymbolicLink()) continue;
269
298
  if (it.isDirectory() && entryPath(root, it.name) !== null) {
270
299
  const docPath = join(root, it.name, "SKILL.md");
271
- const st = await fs.stat(docPath);
272
- if (!st.isFile()) continue;
300
+ const st = await lstatOrNull(docPath);
301
+ if (!st || !st.isFile() || st.isSymbolicLink() || !(await isInsideRoot(root, docPath))) continue;
273
302
  const doc = parseSkillDoc(await fs.readFile(docPath, "utf8"));
274
- entries.push(entryOf(it.name, "bundle", docPath, doc));
303
+ byName.set(it.name, entryOf(it.name, "bundle", docPath, doc));
275
304
  } else if (it.isFile() && it.name.toLowerCase().endsWith(".md") && it.name.toLowerCase() !== "skill.md" && entryPath(root, it.name.slice(0, -3)) !== null) {
305
+ const skillName = it.name.slice(0, -3);
306
+ if (byName.has(skillName)) continue;
276
307
  const docPath = join(root, it.name);
308
+ if (!(await isInsideRoot(root, docPath))) continue;
277
309
  const doc = parseSkillDoc(await fs.readFile(docPath, "utf8"));
278
- entries.push(entryOf(it.name.slice(0, -3), "flat", docPath, doc));
310
+ byName.set(skillName, entryOf(skillName, "flat", docPath, doc));
279
311
  }
280
312
  } catch {
281
313
  /* 跳过不可读条目 */
282
314
  }
283
315
  }
284
- entries.sort((a, b) => a.name.localeCompare(b.name));
316
+ const entries = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
285
317
  return { exists: true, entries };
286
318
  }
287
319
 
288
320
  // ── 启用 / 停用(同时控制模型与 / 手动调用,非破坏)──────────────────────────
289
321
 
290
322
  /** enabled=true 恢复模型与 / 手动调用;false 同时停用两种调用入口。 */
291
- export async function setSkillEnabled(root, name, enabled) {
323
+ export async function setSkillEnabled(root, name, enabled, log) {
292
324
  if (!isDshRoot(root)) return readonlyError("toggle");
293
325
  const resolved = await resolveEntry(root, name);
294
326
  if (resolved === null) return { ok: false, error: `技能不存在: ${name}`, code: "error.skill.notFound", params: { name } };
@@ -296,6 +328,7 @@ export async function setSkillEnabled(root, name, enabled) {
296
328
  const updated = updateInvocationPolicy(source, enabled);
297
329
  if (updated === null) return { ok: false, error: `技能缺少完整 frontmatter,无法${enabled ? "启用" : "停用"}: ${name}`, code: "error.skill.noFrontmatter", params: { name, action: enabled ? "enable" : "disable" } };
298
330
  await writeFileAtomically(resolved.docPath, updated);
331
+ if (log) log(enabled ? "enable" : "disable", `${enabled ? "启用" : "停用"} ${resolved.docPath}`);
299
332
  return { name, enabled };
300
333
  }
301
334
 
@@ -466,7 +499,7 @@ export async function importSkill(source, log, options = {}) {
466
499
 
467
500
  const analysis = await analyzeSource(source);
468
501
  if (analysis.kind === "none") return { ok: false, error: analysis.error || "无法识别的 skill 来源", code: analysis.code || "error.source.unrecognized", params: analysis.params };
469
- if (pathsOverlap(analysis.source, targetRoot)) return { ok: false, error: "导入来源不能与 DSH 技能目录相同、包含或位于其中", code: "error.import.overlap" };
502
+ if (await pathsOverlap(analysis.source, targetRoot)) return { ok: false, error: "导入来源不能与 DSH 技能目录相同、包含或位于其中", code: "error.import.overlap" };
470
503
 
471
504
  let candidates = [];
472
505
  if (analysis.kind === "single") {
@@ -536,7 +569,9 @@ export async function importSkill(source, log, options = {}) {
536
569
  return { kind: analysis.kind, pending, conflicts, failed };
537
570
  }
538
571
 
539
- await fs.mkdir(targetRoot, { recursive: true });
572
+ if (pending.length > 0 || (conflict === "overwrite" && conflicts.length > 0)) {
573
+ await fs.mkdir(targetRoot, { recursive: true });
574
+ }
540
575
 
541
576
  for (const p of pending) {
542
577
  try {
package/lib/index.js CHANGED
@@ -91,12 +91,18 @@ function validateMutationRequest(req) {
91
91
  }
92
92
 
93
93
  function json(res, code, payload) {
94
- const body = JSON.stringify(payload);
95
- res.writeHead(code, {
96
- "content-type": "application/json; charset=utf-8",
97
- "content-length": Buffer.byteLength(body),
98
- });
99
- res.end(body);
94
+ if (!res || res.writableEnded || res.destroyed) return;
95
+ res.once("error", () => {});
96
+ try {
97
+ const body = JSON.stringify(payload);
98
+ res.writeHead(code, {
99
+ "content-type": "application/json; charset=utf-8",
100
+ "content-length": Buffer.byteLength(body),
101
+ });
102
+ res.end(body);
103
+ } catch {
104
+ /* 客户端已断开 */
105
+ }
100
106
  }
101
107
 
102
108
  /** 成功统一包成 { ok: true, data };核心返回 { ok:false, error } 时透传为 400。 */
@@ -190,6 +196,15 @@ function apply(ctx) {
190
196
  if (req.method === "GET" && path === "/api/dsh-skills-manager/state") {
191
197
  return run(res, state);
192
198
  }
199
+ if (req.method === "HEAD") {
200
+ res.writeHead(path === "/api/dsh-skills-manager/state" ? 200 : 405, {
201
+ "content-type": "application/json; charset=utf-8",
202
+ "content-length": 0,
203
+ allow: "GET, HEAD, POST",
204
+ });
205
+ res.end();
206
+ return;
207
+ }
193
208
  if (req.method !== "POST") {
194
209
  json(res, 405, { ok: false, code: "error.proto.method", error: `method not allowed: ${req.method}` });
195
210
  return;
@@ -203,11 +218,11 @@ function apply(ctx) {
203
218
  return enqueueMutation(() => {
204
219
  switch (path) {
205
220
  case "/api/dsh-skills-manager/enable":
206
- return run(res, () => setSkillEnabled(rootByKey[String(body.root || "dsh")], String(body.name || ""), true), afterWrite);
221
+ return run(res, () => setSkillEnabled(rootByKey[String(body.root || "dsh")], String(body.name || ""), true, log), afterWrite);
207
222
  case "/api/dsh-skills-manager/disable":
208
- return run(res, () => setSkillEnabled(rootByKey[String(body.root || "dsh")], String(body.name || ""), false), afterWrite);
223
+ return run(res, () => setSkillEnabled(rootByKey[String(body.root || "dsh")], String(body.name || ""), false, log), afterWrite);
209
224
  case "/api/dsh-skills-manager/delete":
210
- return run(res, () => deleteSkill(rootByKey.dsh, String(body.name || ""), log), afterWrite);
225
+ return run(res, () => deleteSkill(rootByKey[String(body.root || "dsh")], String(body.name || ""), log), afterWrite);
211
226
  case "/api/dsh-skills-manager/import":
212
227
  return run(res, () => importSkill(String(body.source || ""), log, {
213
228
  conflict: body.conflict === "overwrite" ? "overwrite" : "skip",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michengai/dsh-skills-manager",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "description": "NPM-installable DSH Web plugin for managing local skills and viewing shared Agent skills safely.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -51,9 +51,9 @@
51
51
  "@deepseek-ai/dsh-client-runtime": ">=0.1.0-rc.0 <0.2.0",
52
52
  "@deepseek-ai/dsh-client-ui-slots": ">=0.1.0-rc.0 <0.2.0",
53
53
  "@deepseek-ai/dsh-client-locale": ">=0.1.0-rc.0 <0.2.0",
54
- "@deepseek-ai/dsh-host-webserver": ">=0.1.0-rc.5",
55
- "@deepseek-ai/dsh-skill": ">=0.1.0-rc.5",
56
- "@deepseek-ai/dsh-client-ui-workspace": ">=0.1.0-rc.5",
54
+ "@deepseek-ai/dsh-host-webserver": ">=0.1.0-rc.5 <0.2.0",
55
+ "@deepseek-ai/dsh-skill": ">=0.1.0-rc.5 <0.2.0",
56
+ "@deepseek-ai/dsh-client-ui-workspace": ">=0.1.0-rc.5 <0.2.0",
57
57
  "react": "^18.2.0"
58
58
  },
59
59
  "engines": {