@michengai/dsh-skills-manager 0.1.17 → 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 +98 -56
- package/README.zh-CN.md +98 -56
- package/assets/branding/dsh-logo.png +0 -0
- package/assets/screenshots/skills-manager-en.png +0 -0
- package/assets/screenshots/skills-manager.png +0 -0
- package/assets/screenshots/skills-public.png +0 -0
- package/assets/screenshots/slash-command.png +0 -0
- package/assets/screenshots/upload-plugin.png +0 -0
- package/lib/client.js +270 -11
- package/lib/core.js +57 -22
- package/lib/index.js +24 -9
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,41 +1,66 @@
|
|
|
1
|
-
<
|
|
2
|
-
|
|
3
|
-
|
|
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)
|
|
32
|
+
[](https://www.npmjs.com/package/@michengai/dsh-skills-manager)
|
|
33
|
+
[](https://github.com/MichengAI/dsh-skills-manager)
|
|
34
|
+
[](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
|
-
|
|
39
|
+
## Features
|
|
6
40
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
+

|
|
25
52
|
|
|
26
|
-
|
|
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
|
-
|
|
55
|
+

|
|
31
56
|
|
|
32
|
-
|
|
57
|
+
The compact upload dialog accepts a plugin `SKILL.md`, a plugin directory, or drag-and-drop:
|
|
58
|
+
|
|
59
|
+

|
|
60
|
+
|
|
61
|
+
Enabling a DSH-local skill restores its `/` command in the chat composer:
|
|
33
62
|
|
|
34
|
-
|
|
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
|
+

|
|
39
64
|
|
|
40
65
|
## Prerequisites
|
|
41
66
|
|
|
@@ -45,18 +70,54 @@
|
|
|
45
70
|
|
|
46
71
|
## Installation
|
|
47
72
|
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
+

|
|
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
|
package/README.zh-CN.md
CHANGED
|
@@ -1,41 +1,66 @@
|
|
|
1
|
-
<
|
|
2
|
-
|
|
3
|
-
|
|
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
|
+
[](LICENSE)
|
|
32
|
+
[](https://www.npmjs.com/package/@michengai/dsh-skills-manager)
|
|
33
|
+
[](https://github.com/MichengAI/dsh-skills-manager)
|
|
34
|
+
[](https://nodejs.org/)
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
> DSH Skills Manager 是社区维护的 DeepSeek Harness(DSH)插件,并非 DeepSeek AI 官方产品。
|
|
4
38
|
|
|
5
|
-
|
|
39
|
+
## 功能概览
|
|
6
40
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
41
|
+
- 在「设置 → 技能」中按分类筛选或搜索,查看 DSH 本地技能与公共 Agent 技能。
|
|
42
|
+
- 对 DSH 本地技能执行启用、停用、上传、覆盖和删除。
|
|
43
|
+
- 公共 Agent 技能始终只读,不改写共享的全局元数据。
|
|
44
|
+
- 使用系统文件选择器导入包含 `SKILL.md` 的插件目录,并对同名覆盖强制确认。
|
|
45
|
+
- 可把一句话复制到 DSH、Codex 或 WorkBuddy,让对方代装到本机 DSH。
|
|
10
46
|
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
+

|
|
25
52
|
|
|
26
|
-
|
|
27
|
-
<img src="assets/screenshots/skills-manager.png" alt="技能管理设置页面" width="960">
|
|
28
|
-
</p>
|
|
53
|
+
本地 DSH 技能为空时,公共 Agent 技能仍然可见:
|
|
29
54
|
|
|
30
|
-
|
|
55
|
+

|
|
31
56
|
|
|
32
|
-
|
|
57
|
+
紧凑上传弹窗支持选择 `SKILL.md`、选择插件目录,或直接拖放文件:
|
|
33
58
|
|
|
34
|
-
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
-
|
|
59
|
+

|
|
60
|
+
|
|
61
|
+
启用 DSH 本地技能后,聊天输入框会恢复对应的 `/` 命令:
|
|
62
|
+
|
|
63
|
+

|
|
39
64
|
|
|
40
65
|
## 前置条件
|
|
41
66
|
|
|
@@ -45,22 +70,58 @@
|
|
|
45
70
|
|
|
46
71
|
## 安装
|
|
47
72
|
|
|
48
|
-
|
|
73
|
+
`dsh plugin add` 会转发到 profile 目录里的 `pnpm add`。不写版本、不指定官方源时,本机镜像和最短发布间隔可能让你停在旧版。
|
|
74
|
+
|
|
75
|
+
### 交给其他 Agent 一句话安装
|
|
76
|
+
|
|
77
|
+
本插件运行在 DeepSeek Harness Web 里。把下面其中一句复制到 DSH、Codex 或 WorkBuddy,让它代你安装到本机 `web` profile。
|
|
78
|
+
|
|
79
|
+
从 npm 安装:
|
|
49
80
|
|
|
50
|
-
|
|
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 安装最新版
|
|
108
|
+
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+

|
|
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
|
## 权限与安全边界
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/lib/client.js
CHANGED
|
@@ -40,6 +40,16 @@ window.__ModuleLoader__.load({
|
|
|
40
40
|
"summary.enabled.one": "个已启用",
|
|
41
41
|
"summary.enabled.other": "个已启用",
|
|
42
42
|
"summary.group": "{count} 个",
|
|
43
|
+
"filter.category": "分类",
|
|
44
|
+
"filter.all": "全部",
|
|
45
|
+
"filter.option": "{name}({count})",
|
|
46
|
+
"filter.showing.one": "显示 {count} 个",
|
|
47
|
+
"filter.showing.other": "显示 {count} 个",
|
|
48
|
+
"search": "搜索",
|
|
49
|
+
"search.placeholder": "名称、形态或简介",
|
|
50
|
+
"search.clear": "清除搜索",
|
|
51
|
+
"empty.search": "没有匹配的插件。试试其他关键词,或切换到「{all}」。",
|
|
52
|
+
"empty.search.reset": "清除筛选",
|
|
43
53
|
"note.missing": "未提供简介",
|
|
44
54
|
"empty.dir.uncreated": "目录尚未创建,上传第一个插件后会自动创建。",
|
|
45
55
|
"empty.dir.missing": "公共目录不存在。",
|
|
@@ -128,6 +138,16 @@ window.__ModuleLoader__.load({
|
|
|
128
138
|
"summary.enabled.one": "enabled",
|
|
129
139
|
"summary.enabled.other": "enabled",
|
|
130
140
|
"summary.group": "{count} items",
|
|
141
|
+
"filter.category": "Category",
|
|
142
|
+
"filter.all": "All",
|
|
143
|
+
"filter.option": "{name} ({count})",
|
|
144
|
+
"filter.showing.one": "Showing {count} skill",
|
|
145
|
+
"filter.showing.other": "Showing {count} skills",
|
|
146
|
+
"search": "Search",
|
|
147
|
+
"search.placeholder": "Name, kind, or description",
|
|
148
|
+
"search.clear": "Clear search",
|
|
149
|
+
"empty.search": "No matching plugins. Try another keyword, or switch to {all}.",
|
|
150
|
+
"empty.search.reset": "Clear filters",
|
|
131
151
|
"note.missing": "No description provided",
|
|
132
152
|
"empty.dir.uncreated": "The directory has not been created yet; it will be created automatically after the first upload.",
|
|
133
153
|
"empty.dir.missing": "The public directory does not exist.",
|
|
@@ -202,7 +222,8 @@ window.__ModuleLoader__.load({
|
|
|
202
222
|
.dssm-row{display:flex;align-items:center;gap:12px;padding:13px 16px;border-bottom:1px solid var(--dsw-alias-border-l1)}.dssm-row:last-child{border-bottom:0}.dssm-row-main{display:flex;min-width:0;flex:1;flex-direction:column;gap:3px}.dssm-row-id{display:flex;align-items:center;gap:7px;min-width:0}.dssm-row-name{overflow:hidden;font-size:13px;font-weight:500;line-height:20px;text-overflow:ellipsis;white-space:nowrap}.dssm-tag{flex:none;padding:1px 6px;border:1px solid var(--dsw-alias-border-l3);border-radius:4px;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:16px}.dssm-tag-on{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}.dssm-tag-off{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}.dssm-note{overflow:hidden;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;text-overflow:ellipsis;white-space:nowrap}.dssm-empty{padding:28px 16px;color:var(--dsw-alias-label-tertiary);font-size:13px;text-align:center}.dssm-error{color:var(--dsw-alias-state-error-primary);font-size:13px;line-height:20px}
|
|
203
223
|
.dssm-mask{position:fixed;z-index:1100;inset:0;display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:100%;height:100%;padding:24px;background:rgba(0,0,0,.58)}.dssm-modal{box-sizing:border-box;display:flex;width:min(480px,100%)!important;height:auto!important;max-width:100%;max-height:100%;min-width:0;min-height:0;flex:0 0 auto!important;align-self:center!important;flex-direction:column;gap:18px;padding:24px;overflow:auto;border:1px solid var(--dsw-alias-border-l2);border-radius:16px;background:var(--dsw-alias-bg-layer-2);box-shadow:var(--dsw-shadow-lv3)}.dssm-modal-upload{width:min(480px,100%)!important;height:auto!important;max-width:100%;max-height:100%;align-self:center!important;flex:0 0 auto!important;overflow:auto}.dssm-modal-head{display:flex;align-items:center;gap:12px}.dssm-modal-title{margin:0;flex:1;font-size:17px;line-height:24px;font-weight:650}.dssm-icon-btn{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border:0;border-radius:7px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:20px;line-height:1;cursor:pointer}.dssm-icon-btn:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
204
224
|
.dssm-dropzone{box-sizing:border-box;display:flex;width:100%;min-height:144px;flex:none;flex-direction:column;align-items:center;justify-content:center;gap:8px;padding:18px;border:1px dashed var(--dsw-alias-border-l3);border-radius:12px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);cursor:pointer;transition:border-color 180ms ease,background 180ms ease}.dssm-dropzone:hover,.dssm-dropzone-active{border-color:var(--dsw-alias-state-success-primary);background:var(--dsw-alias-interactive-bg-hover)}.dssm-dropzone-title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:560}.dssm-dropzone-copy{font-size:12px;line-height:18px;text-align:center}.dssm-file{display:flex;align-items:center;gap:8px;padding:9px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;color:var(--dsw-alias-label-primary);font-size:13px}.dssm-file-name{overflow:hidden;flex:1;text-overflow:ellipsis;white-space:nowrap}.dssm-modal-head,.dssm-file,.dssm-modal-actions{flex:none}.dssm-modal-actions{display:flex;justify-content:flex-end;flex-wrap:wrap;gap:8px}.dssm-hidden-input{display:none}
|
|
205
|
-
|
|
225
|
+
.dssm-filters{display:flex;align-items:flex-end;gap:10px}.dssm-field{display:flex;min-width:0;flex:1;flex-direction:column;gap:6px}.dssm-field-category{flex:0 1 220px}.dssm-field-search{flex:1 1 240px}.dssm-label{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:16px}.dssm-control{box-sizing:border-box;width:100%;min-height:32px;padding:0 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px}.dssm-control:focus-visible{outline:2px solid var(--dsw-alias-state-success-primary);outline-offset:2px}.dssm-select{position:relative}.dssm-select-trigger{box-sizing:border-box;display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;min-height:32px;padding:0 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;line-height:20px;text-align:left;cursor:pointer}.dssm-select-trigger:hover{background:var(--dsw-alias-interactive-bg-hover)}.dssm-select-trigger:focus-visible{outline:2px solid var(--dsw-alias-state-success-primary);outline-offset:2px}.dssm-select-trigger[aria-expanded="true"]{border-color:var(--dsw-alias-state-success-primary)}.dssm-select-value{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dssm-select-caret{flex:none;width:12px;height:12px;color:var(--dsw-alias-label-tertiary)}.dssm-select-menu{position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:30;box-sizing:border-box;max-height:280px;overflow:auto;padding:4px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-specific-menu,var(--dsw-alias-bg-layer-2));box-shadow:var(--dsw-shadow-lv3)}.dssm-select-option{box-sizing:border-box;display:flex;align-items:center;width:100%;min-height:32px;padding:0 10px;border:0;border-radius:8px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;line-height:20px;text-align:left;cursor:pointer}.dssm-select-option:hover,.dssm-select-option[data-active="true"]{background:var(--dsw-alias-interactive-bg-hover)}.dssm-select-option[aria-selected="true"]{color:var(--dsw-alias-state-success-primary)}.dssm-search-wrap{position:relative;display:flex;align-items:center}.dssm-search{padding-right:32px}.dssm-search::-webkit-search-cancel-button,.dssm-search::-webkit-search-decoration{-webkit-appearance:none;appearance:none}.dssm-search-clear{position:absolute;right:4px;display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-tertiary);font:inherit;font-size:16px;line-height:1;cursor:pointer}.dssm-search-clear:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.dssm-search-clear:focus-visible{outline:2px solid var(--dsw-alias-state-success-primary);outline-offset:2px}.dssm-empty-filter{display:flex;flex-direction:column;align-items:center;gap:12px;padding:28px 16px;border:1px dashed var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px;text-align:center}.dssm-filter-meta{margin-left:auto;color:var(--dsw-alias-label-tertiary);font-size:12px}
|
|
226
|
+
@media (max-width:560px){.dssm-mask{padding:12px;align-items:center}.dssm-modal{width:100%!important;height:auto!important;padding:16px;gap:12px;align-self:center!important}.dssm-modal-upload{width:100%!important;align-self:center!important}.dssm-toolbar{flex-wrap:wrap}.dssm-actions{margin-left:0}.dssm-path{display:none}.dssm-row{align-items:flex-start;flex-wrap:wrap}.dssm-row>.dssm-btn{margin-left:auto}.dssm-filters{flex-direction:column;align-items:stretch}.dssm-field-category,.dssm-field-search{flex:none}.dssm-filter-meta{margin-left:0}.dssm-modal-actions{justify-content:stretch}.dssm-modal-actions .dssm-btn{flex:1 1 auto}}@media (prefers-reduced-motion:reduce){.dssm-btn,.dssm-dropzone{transition:none}}
|
|
206
227
|
`;
|
|
207
228
|
|
|
208
229
|
function callApi(path, options) {
|
|
@@ -258,6 +279,50 @@ window.__ModuleLoader__.load({
|
|
|
258
279
|
return skill.invocationPolicyValid && skill.modelInvocable && skill.userInvocable;
|
|
259
280
|
}
|
|
260
281
|
|
|
282
|
+
/** 规范化检索词:去首尾空白并转小写,便于名称、形态和简介统一匹配。 */
|
|
283
|
+
function normalizeSkillQuery(query) {
|
|
284
|
+
return String(query == null ? "" : query).trim().toLowerCase();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** 按名称、简介、形态或分类标签做包含匹配;空检索视为全部命中。 */
|
|
288
|
+
function matchSkillQuery(skill, query) {
|
|
289
|
+
var q = normalizeSkillQuery(query);
|
|
290
|
+
if (q === "") return true;
|
|
291
|
+
return [
|
|
292
|
+
skill.name,
|
|
293
|
+
skill.description,
|
|
294
|
+
skill.kind,
|
|
295
|
+
skill.kindLabel,
|
|
296
|
+
skill.statusLabel,
|
|
297
|
+
skill.rootKey,
|
|
298
|
+
skill.rootLabel,
|
|
299
|
+
].some(function (field) {
|
|
300
|
+
return String(field == null ? "" : field).toLowerCase().includes(q);
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** 先按目录分类收窄,再按检索词过滤。rootKey 为空表示全部分类。 */
|
|
305
|
+
function filterSkills(list, options) {
|
|
306
|
+
var query = options && options.query;
|
|
307
|
+
var rootKey = options && options.rootKey != null ? options.rootKey : "";
|
|
308
|
+
return list.filter(function (skill) {
|
|
309
|
+
return (rootKey === "" || skill.rootKey === rootKey) && matchSkillQuery(skill, query);
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function skillSearchMeta(skill, root, t) {
|
|
314
|
+
var enabled = isSkillEnabled(skill);
|
|
315
|
+
return {
|
|
316
|
+
name: skill.name,
|
|
317
|
+
description: skill.description,
|
|
318
|
+
kind: skill.kind,
|
|
319
|
+
kindLabel: t(skill.kind === "bundle" ? "status.bundle" : "status.single"),
|
|
320
|
+
statusLabel: skill.invocationPolicyValid ? (enabled ? t("status.enabled") : t("status.disabled")) : t("status.invalid"),
|
|
321
|
+
rootKey: root.key,
|
|
322
|
+
rootLabel: translateOrFallback(t, "root." + root.key, root.label),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
261
326
|
function modalFocusable(modal) {
|
|
262
327
|
return Array.prototype.slice.call(modal.querySelectorAll("button:not(:disabled), [href], input:not([type=hidden]):not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex=\"-1\"])"));
|
|
263
328
|
}
|
|
@@ -273,6 +338,122 @@ window.__ModuleLoader__.load({
|
|
|
273
338
|
}
|
|
274
339
|
}
|
|
275
340
|
|
|
341
|
+
function CategorySelect(props) {
|
|
342
|
+
var openState = react.useState(false);
|
|
343
|
+
var open = openState[0];
|
|
344
|
+
var setOpen = openState[1];
|
|
345
|
+
var selectedIndex = 0;
|
|
346
|
+
for (var i = 0; i < props.options.length; i++) {
|
|
347
|
+
if (props.options[i].value === props.value) { selectedIndex = i; break; }
|
|
348
|
+
}
|
|
349
|
+
var activeState = react.useState(selectedIndex);
|
|
350
|
+
var active = activeState[0];
|
|
351
|
+
var setActive = activeState[1];
|
|
352
|
+
var rootRef = react.useRef(null);
|
|
353
|
+
var triggerRef = react.useRef(null);
|
|
354
|
+
var listRef = react.useRef(null);
|
|
355
|
+
var wasOpen = react.useRef(false);
|
|
356
|
+
var selected = props.options[selectedIndex];
|
|
357
|
+
|
|
358
|
+
react.useEffect(function () {
|
|
359
|
+
if (!open) return undefined;
|
|
360
|
+
setActive(selectedIndex);
|
|
361
|
+
function onPointerDown(event) {
|
|
362
|
+
var node = rootRef.current;
|
|
363
|
+
if (node && event.target && node.contains(event.target)) return;
|
|
364
|
+
setOpen(false);
|
|
365
|
+
}
|
|
366
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
367
|
+
return function () { document.removeEventListener("pointerdown", onPointerDown); };
|
|
368
|
+
}, [open, selectedIndex]);
|
|
369
|
+
|
|
370
|
+
react.useEffect(function () {
|
|
371
|
+
if (open) {
|
|
372
|
+
if (listRef.current) listRef.current.focus();
|
|
373
|
+
wasOpen.current = true;
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (wasOpen.current) {
|
|
377
|
+
if (triggerRef.current) triggerRef.current.focus();
|
|
378
|
+
wasOpen.current = false;
|
|
379
|
+
}
|
|
380
|
+
}, [open]);
|
|
381
|
+
|
|
382
|
+
react.useEffect(function () {
|
|
383
|
+
if (!open) return;
|
|
384
|
+
var option = document.getElementById(props.id + "-opt-" + String(active));
|
|
385
|
+
if (option) option.scrollIntoView({ block: "nearest" });
|
|
386
|
+
}, [active, open, props.id]);
|
|
387
|
+
|
|
388
|
+
function choose(value) {
|
|
389
|
+
props.onChange(value);
|
|
390
|
+
setOpen(false);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function move(next) {
|
|
394
|
+
if (!props.options.length) return;
|
|
395
|
+
setActive(Math.min(props.options.length - 1, Math.max(0, next)));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function onTriggerKeyDown(event) {
|
|
399
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp" || event.key === "Enter" || event.key === " ") {
|
|
400
|
+
event.preventDefault();
|
|
401
|
+
setOpen(true);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function onListKeyDown(event) {
|
|
406
|
+
if (event.key === "ArrowDown") { event.preventDefault(); move(active + 1); return; }
|
|
407
|
+
if (event.key === "ArrowUp") { event.preventDefault(); move(active - 1); return; }
|
|
408
|
+
if (event.key === "Home") { event.preventDefault(); move(0); return; }
|
|
409
|
+
if (event.key === "End") { event.preventDefault(); move(props.options.length - 1); return; }
|
|
410
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
411
|
+
event.preventDefault();
|
|
412
|
+
var option = props.options[active];
|
|
413
|
+
if (option) choose(option.value);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (event.key === "Escape" || event.key === "Tab") setOpen(false);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return h("div", { className: "dssm-select", ref: rootRef },
|
|
420
|
+
h("button", {
|
|
421
|
+
id: props.id,
|
|
422
|
+
ref: triggerRef,
|
|
423
|
+
type: "button",
|
|
424
|
+
className: "dssm-select-trigger",
|
|
425
|
+
"aria-haspopup": "listbox",
|
|
426
|
+
"aria-expanded": open,
|
|
427
|
+
"aria-controls": props.id + "-list",
|
|
428
|
+
onClick: function () { setOpen(function (current) { return !current; }); },
|
|
429
|
+
onKeyDown: onTriggerKeyDown,
|
|
430
|
+
},
|
|
431
|
+
h("span", { className: "dssm-select-value" }, selected ? selected.label : ""),
|
|
432
|
+
h("svg", { className: "dssm-select-caret", viewBox: "0 0 12 12", "aria-hidden": true, focusable: false },
|
|
433
|
+
h("path", { d: "M2.5 4.5L6 8l3.5-3.5", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }))),
|
|
434
|
+
open ? h("div", {
|
|
435
|
+
id: props.id + "-list",
|
|
436
|
+
ref: listRef,
|
|
437
|
+
className: "dssm-select-menu",
|
|
438
|
+
role: "listbox",
|
|
439
|
+
tabIndex: 0,
|
|
440
|
+
"aria-activedescendant": props.id + "-opt-" + String(active),
|
|
441
|
+
onKeyDown: onListKeyDown,
|
|
442
|
+
}, props.options.map(function (option, index) {
|
|
443
|
+
return h("button", {
|
|
444
|
+
key: option.value === "" ? "all" : option.value,
|
|
445
|
+
id: props.id + "-opt-" + String(index),
|
|
446
|
+
type: "button",
|
|
447
|
+
role: "option",
|
|
448
|
+
className: "dssm-select-option",
|
|
449
|
+
"aria-selected": option.value === props.value,
|
|
450
|
+
"data-active": index === active,
|
|
451
|
+
onMouseEnter: function () { setActive(index); },
|
|
452
|
+
onClick: function () { choose(option.value); },
|
|
453
|
+
}, option.label);
|
|
454
|
+
})) : null);
|
|
455
|
+
}
|
|
456
|
+
|
|
276
457
|
// 渲染机制按 register 的 locale 字段把命名空间绑定的 t 注入组件 props。
|
|
277
458
|
function SkillManagerSection(props) {
|
|
278
459
|
var t = props.t;
|
|
@@ -297,6 +478,13 @@ window.__ModuleLoader__.load({
|
|
|
297
478
|
var deleteState = react.useState(null);
|
|
298
479
|
var confirmDelete = deleteState[0];
|
|
299
480
|
var setConfirmDelete = deleteState[1];
|
|
481
|
+
var inflightRef = react.useRef(false);
|
|
482
|
+
var queryState = react.useState("");
|
|
483
|
+
var query = queryState[0];
|
|
484
|
+
var setQuery = queryState[1];
|
|
485
|
+
var categoryState = react.useState("");
|
|
486
|
+
var category = categoryState[0];
|
|
487
|
+
var setCategory = categoryState[1];
|
|
300
488
|
var inputRef = react.useRef(null);
|
|
301
489
|
var modalRef = react.useRef(null);
|
|
302
490
|
|
|
@@ -337,12 +525,15 @@ window.__ModuleLoader__.load({
|
|
|
337
525
|
}, [uploadOpen, confirmImport, confirmDelete]);
|
|
338
526
|
|
|
339
527
|
function action(path, body) {
|
|
340
|
-
if (busy) return;
|
|
528
|
+
if (busy || inflightRef.current) return;
|
|
529
|
+
inflightRef.current = true;
|
|
341
530
|
setBusy(path);
|
|
342
531
|
callApi(path, { method: "POST", headers: MUTATION_HEADERS, body: JSON.stringify(body || {}) }).then(function () {
|
|
532
|
+
inflightRef.current = false;
|
|
343
533
|
setBusy(null);
|
|
344
534
|
refresh();
|
|
345
535
|
}).catch(function (error) {
|
|
536
|
+
inflightRef.current = false;
|
|
346
537
|
setBusy(null);
|
|
347
538
|
setSnapshot({ loading: false, error: error, data: snapshot.data });
|
|
348
539
|
});
|
|
@@ -387,31 +578,38 @@ window.__ModuleLoader__.load({
|
|
|
387
578
|
}
|
|
388
579
|
|
|
389
580
|
function executeImport(source, conflict) {
|
|
581
|
+
if (inflightRef.current && busy !== "import-check") return;
|
|
582
|
+
inflightRef.current = true;
|
|
390
583
|
setBusy("import");
|
|
391
584
|
setConfirmImport(null);
|
|
392
585
|
callApi("/import", { method: "POST", headers: MUTATION_HEADERS, body: JSON.stringify({ source: source, conflict: conflict }) }).then(function (data) {
|
|
586
|
+
inflightRef.current = false;
|
|
393
587
|
setBusy(null);
|
|
394
588
|
setImportResult(data);
|
|
395
589
|
setSelected(null);
|
|
396
590
|
setUploadOpen(false);
|
|
397
591
|
refresh();
|
|
398
592
|
}).catch(function (error) {
|
|
593
|
+
inflightRef.current = false;
|
|
399
594
|
setBusy(null);
|
|
400
595
|
setImportResult({ error: error });
|
|
401
596
|
});
|
|
402
597
|
}
|
|
403
598
|
|
|
404
599
|
function installSelected() {
|
|
405
|
-
if (!selected || busy) return;
|
|
600
|
+
if (!selected || busy || inflightRef.current) return;
|
|
601
|
+
inflightRef.current = true;
|
|
406
602
|
setBusy("import-check");
|
|
407
603
|
callApi("/import", { method: "POST", headers: MUTATION_HEADERS, body: JSON.stringify({ source: selected.source, dryRun: true }) }).then(function (data) {
|
|
408
604
|
if (data.conflicts && data.conflicts.length) {
|
|
605
|
+
inflightRef.current = false;
|
|
409
606
|
setBusy(null);
|
|
410
607
|
setConfirmImport({ source: selected.source, conflicts: data.conflicts });
|
|
411
608
|
return;
|
|
412
609
|
}
|
|
413
610
|
executeImport(selected.source, "skip");
|
|
414
611
|
}).catch(function (error) {
|
|
612
|
+
inflightRef.current = false;
|
|
415
613
|
setBusy(null);
|
|
416
614
|
setImportResult({ error: error });
|
|
417
615
|
});
|
|
@@ -429,7 +627,7 @@ window.__ModuleLoader__.load({
|
|
|
429
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"))),
|
|
430
628
|
h("div", { className: "dssm-note", title: skill.description || "" }, skill.description || t("note.missing"))),
|
|
431
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,
|
|
432
|
-
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);
|
|
433
631
|
}
|
|
434
632
|
|
|
435
633
|
var nodes = [h("style", { key: "css" }, CSS)];
|
|
@@ -443,19 +641,72 @@ window.__ModuleLoader__.load({
|
|
|
443
641
|
h("div", { className: "dssm-actions" },
|
|
444
642
|
h("button", { className: "dssm-btn dssm-btn-secondary", disabled: !!busy, onClick: refresh }, t("btn.refresh")),
|
|
445
643
|
h("button", { className: "dssm-btn", disabled: !!busy, onClick: function () { setImportResult(null); setUploadOpen(true); } }, t("btn.upload")))));
|
|
644
|
+
var searchable = roots.reduce(function (all, root) {
|
|
645
|
+
return all.concat((root.skills || []).map(function (skill) { return skillSearchMeta(skill, root, t); }));
|
|
646
|
+
}, []);
|
|
647
|
+
var filtered = filterSkills(searchable, { query: query, rootKey: category });
|
|
648
|
+
var filteredCount = filtered.length;
|
|
649
|
+
var hasQuery = normalizeSkillQuery(query) !== "";
|
|
650
|
+
var hasFilter = hasQuery || category !== "";
|
|
651
|
+
var resetFilters = function () { setQuery(""); setCategory(""); };
|
|
446
652
|
// 复数按 en 的 one/other 二元规则选择;zh/en 均适用,扩展多复数语言时需改 CLDR 规则。
|
|
447
653
|
nodes.push(h("div", { key: "summary", className: "dssm-summary" },
|
|
448
|
-
h("span", { className: "dssm-summary-count" }, skills.length), h("span", { className: "dssm-summary-label" }, t(skills.length === 1 ? "summary.total.one" : "summary.total.other", { count: skills.length })), h("span", { className: "dssm-summary-separator" }), h("span", { className: "dssm-summary-count" }, enabled), h("span", { className: "dssm-summary-label" }, t(enabled === 1 ? "summary.enabled.one" : "summary.enabled.other", { count: enabled }))
|
|
654
|
+
h("span", { className: "dssm-summary-count" }, skills.length), h("span", { className: "dssm-summary-label" }, t(skills.length === 1 ? "summary.total.one" : "summary.total.other", { count: skills.length })), h("span", { className: "dssm-summary-separator" }), h("span", { className: "dssm-summary-count" }, enabled), h("span", { className: "dssm-summary-label" }, t(enabled === 1 ? "summary.enabled.one" : "summary.enabled.other", { count: enabled })),
|
|
655
|
+
hasFilter ? h("span", { className: "dssm-filter-meta", "aria-live": "polite" }, t(filteredCount === 1 ? "filter.showing.one" : "filter.showing.other", { count: filteredCount })) : null));
|
|
656
|
+
nodes.push(h("div", { key: "filters", className: "dssm-filters" },
|
|
657
|
+
h("div", { className: "dssm-field dssm-field-category" },
|
|
658
|
+
h("label", { className: "dssm-label", htmlFor: "dssm-filter-category" }, t("filter.category")),
|
|
659
|
+
h(CategorySelect, {
|
|
660
|
+
id: "dssm-filter-category",
|
|
661
|
+
value: category,
|
|
662
|
+
onChange: setCategory,
|
|
663
|
+
options: [{ value: "", label: t("filter.option", { name: t("filter.all"), count: skills.length }) }].concat(roots.map(function (root) {
|
|
664
|
+
return { value: root.key, label: t("filter.option", { name: translateOrFallback(t, "root." + root.key, root.label), count: (root.skills || []).length }) };
|
|
665
|
+
})),
|
|
666
|
+
})),
|
|
667
|
+
h("div", { className: "dssm-field dssm-field-search" },
|
|
668
|
+
h("label", { className: "dssm-label", htmlFor: "dssm-filter-search" }, t("search")),
|
|
669
|
+
h("div", { className: "dssm-search-wrap" },
|
|
670
|
+
h("input", {
|
|
671
|
+
id: "dssm-filter-search",
|
|
672
|
+
className: "dssm-control dssm-search",
|
|
673
|
+
type: "search",
|
|
674
|
+
value: query,
|
|
675
|
+
autoComplete: "off",
|
|
676
|
+
spellCheck: false,
|
|
677
|
+
placeholder: t("search.placeholder"),
|
|
678
|
+
onChange: function (event) { setQuery(event.currentTarget.value); },
|
|
679
|
+
}),
|
|
680
|
+
query !== "" ? h("button", { type: "button", className: "dssm-search-clear", "aria-label": t("search.clear"), onClick: function () { setQuery(""); } }, "×") : null))));
|
|
681
|
+
var visibleRoots = [];
|
|
449
682
|
roots.forEach(function (root) {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
683
|
+
if (category !== "" && root.key !== category) return;
|
|
684
|
+
var groupSkills = (root.skills || []).filter(function (skill) {
|
|
685
|
+
return matchSkillQuery(skillSearchMeta(skill, root, t), query);
|
|
686
|
+
});
|
|
687
|
+
if (hasQuery && groupSkills.length === 0) return;
|
|
688
|
+
visibleRoots.push({ root: root, groupSkills: groupSkills });
|
|
454
689
|
});
|
|
690
|
+
if (hasQuery && visibleRoots.length === 0) {
|
|
691
|
+
nodes.push(h("div", { key: "empty-search", className: "dssm-empty-filter" },
|
|
692
|
+
h("div", null, t("empty.search", { all: t("filter.all") })),
|
|
693
|
+
h("button", { type: "button", className: "dssm-btn dssm-btn-secondary", onClick: resetFilters }, t("empty.search.reset"))));
|
|
694
|
+
} else {
|
|
695
|
+
visibleRoots.forEach(function (item) {
|
|
696
|
+
var root = item.root;
|
|
697
|
+
var groupSkills = item.groupSkills;
|
|
698
|
+
nodes.push(h("div", { key: root.key, className: "dssm-group" },
|
|
699
|
+
h("div", { className: "dssm-group-head" }, h("h3", { className: "dssm-group-title" }, translateOrFallback(t, "root." + root.key, root.label)), h("span", { className: "dssm-count" }, t("summary.group", { count: groupSkills.length })), h("span", { className: "dssm-path", title: root.path }, root.path)),
|
|
700
|
+
root.exists === false ? h("div", { className: "dssm-empty" }, root.mutable ? t("empty.dir.uncreated") : t("empty.dir.missing")) : (groupSkills.length ? groupSkills.map(function (skill) { return skillRow(skill, root); }) : h("div", { className: "dssm-empty" }, t("empty.skills.none")))));
|
|
701
|
+
});
|
|
702
|
+
}
|
|
455
703
|
} else if (snapshot.loading) {
|
|
456
704
|
nodes.push(h("div", { key: "loading", className: "dssm-note" }, t("loading")));
|
|
457
705
|
}
|
|
458
|
-
|
|
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)) {
|
|
459
710
|
var nameSeparator = t("sep.names");
|
|
460
711
|
var errorSeparator = t("sep.errors");
|
|
461
712
|
// 错误形态:业务/协议错误带 code,或 host 失败带 error(Error 对象);结果形态只有 imported/failed/skipped。
|
|
@@ -502,7 +753,7 @@ window.__ModuleLoader__.load({
|
|
|
502
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"))))));
|
|
503
754
|
}
|
|
504
755
|
if (confirmDelete) {
|
|
505
|
-
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"))))));
|
|
506
757
|
}
|
|
507
758
|
return h("section", { className: "dssm-section" }, nodes);
|
|
508
759
|
}
|
|
@@ -522,8 +773,16 @@ window.__ModuleLoader__.load({
|
|
|
522
773
|
module.exports.DICT = DICT;
|
|
523
774
|
module.exports.translateError = translateError;
|
|
524
775
|
module.exports.isSkillEnabled = isSkillEnabled;
|
|
776
|
+
module.exports.normalizeSkillQuery = normalizeSkillQuery;
|
|
777
|
+
module.exports.matchSkillQuery = matchSkillQuery;
|
|
778
|
+
module.exports.filterSkills = filterSkills;
|
|
525
779
|
module.exports.parseApiResponse = parseApiResponse;
|
|
526
780
|
module.exports.trapModalFocus = trapModalFocus;
|
|
527
781
|
return module.exports;
|
|
528
782
|
}
|
|
529
783
|
});
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
|
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
|
-
|
|
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
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
res.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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.
|
|
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": {
|