@michengai/dsh-skills-manager 0.1.6 → 0.1.8
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 +76 -56
- package/README.zh-CN.md +74 -54
- package/lib/client.js +24 -10
- package/lib/core.js +2 -2
- package/lib/index.js +11 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,16 +5,13 @@
|
|
|
5
5
|
<h1 align="center">DSH Skills Manager</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<strong>
|
|
8
|
+
<strong>A DeepSeek Harness Web plugin for safely managing local skills and viewing shared Agent skills.</strong>
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
12
|
<a href="https://github.com/MichengAI/dsh-skills-manager/issues">Report an issue</a>
|
|
13
13
|
· <a href="https://www.npmjs.com/package/@michengai/dsh-skills-manager">View on npm</a>
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
<p align="center">
|
|
17
|
-
<strong>English</strong> · <a href="README.zh-CN.md">简体中文</a>
|
|
14
|
+
· <a href="README.zh-CN.md">简体中文</a>
|
|
18
15
|
</p>
|
|
19
16
|
|
|
20
17
|
<p align="center">
|
|
@@ -24,94 +21,117 @@
|
|
|
24
21
|
<img src="https://img.shields.io/badge/Node.js-%E2%89%A520-339933?logo=nodedotjs&logoColor=white" alt="Node.js 20 or later">
|
|
25
22
|
</p>
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
<img src="assets/screenshots/skills-manager.png" alt="Native file picker opened from the Skills settings panel" width="960">
|
|
29
|
-
</p>
|
|
24
|
+
> DSH Skills Manager is a community-maintained plugin, not an official DeepSeek AI product.
|
|
30
25
|
|
|
31
|
-
|
|
26
|
+
## Features
|
|
32
27
|
|
|
33
|
-
|
|
28
|
+
- Shows local `$DSH_HOME\skills` and shared `$DSH_AGENTS_HOME\skills` in one Settings page.
|
|
29
|
+
- Enables, disables, uploads, replaces, and deletes DSH-local skills.
|
|
30
|
+
- Keeps shared Agent skills strictly read-only and never changes their global metadata.
|
|
31
|
+
- Imports a plugin directory containing `SKILL.md` through the native file picker and confirms every name collision.
|
|
32
|
+
- Rejects traversal names, imports from the DSH skills directory itself, and symbolic links.
|
|
34
33
|
|
|
35
|
-
|
|
34
|
+

|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
- **Local lifecycle controls** — enable, disable, upload, replace, and delete only DSH-local skills.
|
|
39
|
-
- **Safe shared-skill view** — public Agent skills are visible but strictly read-only; their global metadata is never changed.
|
|
40
|
-
- **Native file selection** — select a plugin `SKILL.md` from the operating system file picker, or explicitly choose its directory when the client cannot expose the selected file path.
|
|
41
|
-
- **Protected replacement** — a same-name import always requires confirmation, including a flat skill and a bundled skill with the same normalized name.
|
|
42
|
-
- **Safer imports** — importing from the DSH skills directory itself, its parent, or a child path is rejected; symbolic links are also rejected.
|
|
43
|
-
- **Predictable dialogs** — Escape closes only the active upload or confirmation dialog, leaving the Settings page open.
|
|
36
|
+
## Prerequisites
|
|
44
37
|
|
|
45
|
-
|
|
38
|
+
- A working DeepSeek Harness Web installation with `dsh` available in PowerShell.
|
|
39
|
+
- Examples use the `web` profile; replace it with the target profile.
|
|
40
|
+
- Source installation and development require Node.js 20+. npm installation does not require running `npm install` in an arbitrary directory.
|
|
46
41
|
|
|
47
|
-
|
|
48
|
-
| --- | --- | --- | --- | --- |
|
|
49
|
-
| `$DSH_HOME\skills` | Yes | Yes | Yes | Yes |
|
|
50
|
-
| `$DSH_AGENTS_HOME\skills` | Yes | No | No | No |
|
|
42
|
+
## Installation
|
|
51
43
|
|
|
52
|
-
|
|
44
|
+
### Install from npm
|
|
53
45
|
|
|
54
|
-
|
|
46
|
+
Run this from any PowerShell directory. Install into the DSH profile through `dsh plugin`:
|
|
55
47
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
48
|
+
```powershell
|
|
49
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
50
|
+
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
51
|
+
dsh plugin --profile web add @michengai/dsh-skills-manager
|
|
52
|
+
dsh --profile web --dump-config
|
|
53
|
+
```
|
|
59
54
|
|
|
60
|
-
|
|
55
|
+
Restart DSH Web or reload the active Web profile. If a package mirror is behind, append `--registry=https://registry.npmjs.org/`.
|
|
61
56
|
|
|
62
|
-
|
|
57
|
+
### Install from source
|
|
63
58
|
|
|
64
|
-
|
|
59
|
+
Use this for debugging or unpublished changes. The cloned directory becomes the plugin source path:
|
|
65
60
|
|
|
66
61
|
```powershell
|
|
67
62
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
68
63
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
69
|
-
|
|
64
|
+
Set-Location D:\Repository\deepseek-harness-plugin
|
|
65
|
+
git clone https://github.com/MichengAI/dsh-skills-manager.git
|
|
66
|
+
Set-Location .\dsh-skills-manager
|
|
67
|
+
npm install
|
|
68
|
+
npm test
|
|
69
|
+
dsh plugin --profile web add .
|
|
70
|
+
dsh --profile web --dump-config
|
|
70
71
|
```
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
3. Open **Settings → Skills**. No source checkout is required.
|
|
73
|
+
Restart DSH Web or reload the active Web profile. Local installation reads and applies `cordis.patch.yml`; do not copy `lib` files manually.
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
## Usage
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
Open **Settings → Skills**, then use the panel as follows:
|
|
78
78
|
|
|
79
|
-
| Goal |
|
|
79
|
+
| Goal | Action | Scope |
|
|
80
80
|
| --- | --- | --- |
|
|
81
|
-
|
|
|
82
|
-
| Enable or disable |
|
|
83
|
-
| Upload a plugin |
|
|
84
|
-
|
|
|
85
|
-
| Replace or delete | Confirm a
|
|
81
|
+
| Inspect a skill | Review its name, form, description, and invocation state. | DSH and shared Agent skills |
|
|
82
|
+
| Enable or disable | Select **Enable** or **Disable**; this controls model invocation and the `/` command. | DSH-local skills only |
|
|
83
|
+
| 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 |
|
|
84
|
+
| Choose the directory | If the selected file has no usable path, choose the directory containing `SKILL.md`. | DSH-local skills only |
|
|
85
|
+
| Replace or delete | Confirm a name collision, or select **Delete** for an unneeded local skill. | DSH-local skills only |
|
|
86
86
|
| Inspect shared skills | Review shared Agent skills without changing their metadata. | Read-only |
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
Escape closes only the frontmost upload or confirmation dialog and leaves Settings open.
|
|
89
|
+
|
|
90
|
+
## Permissions and safety limits
|
|
91
|
+
|
|
92
|
+
| Directory | View | Enable or disable | Upload or replace | Delete |
|
|
93
|
+
| --- | --- | --- | --- | --- |
|
|
94
|
+
| `$DSH_HOME\skills` | Yes | Yes | Yes | Yes |
|
|
95
|
+
| `$DSH_AGENTS_HOME\skills` | Yes | No | No | No |
|
|
96
|
+
|
|
97
|
+
- Enable, disable, and delete accept only one ordinary skill-name path segment.
|
|
98
|
+
- Replacements copy to a temporary sibling path first and keep the original until that succeeds.
|
|
99
|
+
- Write endpoints require JSON and the DSH client request marker, so cross-site browser requests cannot trigger local file operations.
|
|
89
100
|
|
|
90
|
-
##
|
|
101
|
+
## Secondary development
|
|
102
|
+
|
|
103
|
+
This repository has no `src` directory. `lib` is directly maintained runtime source, which is its current layout rather than the recommended layout for new plugins. New plugins should prefer `src` built to `lib`.
|
|
104
|
+
|
|
105
|
+
- [lib\index.js](lib/index.js): host service and local skill file operations.
|
|
106
|
+
- [lib\client.js](lib/client.js): Settings page, upload, and confirmation interactions.
|
|
107
|
+
- `test\core-test.mjs`: file-operation, permission, and import boundary tests.
|
|
108
|
+
- `test\locale-test.mjs`: UI locale tests.
|
|
109
|
+
|
|
110
|
+
After changing the runtime source, test, inspect package contents, and install from the local directory:
|
|
91
111
|
|
|
92
112
|
```powershell
|
|
93
113
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
94
114
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
95
|
-
|
|
115
|
+
npm test
|
|
116
|
+
npm run pack:check
|
|
117
|
+
dsh plugin --profile web add .
|
|
96
118
|
```
|
|
97
119
|
|
|
98
|
-
|
|
120
|
+
Preserve path validation, temporary-copy replacement, and shared-skill read-only behavior when changing file-mutation code.
|
|
99
121
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
## Maintainer release
|
|
103
|
-
|
|
104
|
-
The package is published as [`@michengai/dsh-skills-manager`](https://www.npmjs.com/package/@michengai/dsh-skills-manager). To release a new version:
|
|
122
|
+
## Validation
|
|
105
123
|
|
|
106
124
|
```powershell
|
|
107
125
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
108
126
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
109
|
-
npm
|
|
110
|
-
npm
|
|
127
|
+
npm test
|
|
128
|
+
npm run pack:check
|
|
111
129
|
```
|
|
112
130
|
|
|
113
|
-
`prepublishOnly` runs the core
|
|
131
|
+
`prepublishOnly` runs the core tests before publishing.
|
|
132
|
+
|
|
133
|
+
## Documentation and license
|
|
114
134
|
|
|
115
|
-
|
|
135
|
+
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`.
|
|
116
136
|
|
|
117
|
-
Licensed under
|
|
137
|
+
Licensed under [Apache License 2.0](LICENSE).
|
package/README.zh-CN.md
CHANGED
|
@@ -5,16 +5,13 @@
|
|
|
5
5
|
<h1 align="center">DSH Skills Manager</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<strong
|
|
8
|
+
<strong>安全管理本地技能并查看公共 Agent 技能的 DeepSeek Harness Web 插件。</strong>
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
12
|
<a href="https://github.com/MichengAI/dsh-skills-manager/issues">反馈问题</a>
|
|
13
13
|
· <a href="https://www.npmjs.com/package/@michengai/dsh-skills-manager">在 npm 查看</a>
|
|
14
|
-
</
|
|
15
|
-
|
|
16
|
-
<p align="center">
|
|
17
|
-
<a href="README.md">English</a> · <strong>简体中文</strong>
|
|
14
|
+
· <a href="README.md">English</a>
|
|
18
15
|
</p>
|
|
19
16
|
|
|
20
17
|
<p align="center">
|
|
@@ -24,94 +21,117 @@
|
|
|
24
21
|
<img src="https://img.shields.io/badge/Node.js-%E2%89%A520-339933?logo=nodedotjs&logoColor=white" alt="Node.js 20 或更高版本">
|
|
25
22
|
</p>
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
<img src="assets/screenshots/skills-manager.png" alt="从技能设置面板打开的系统原生文件选择器" width="960">
|
|
29
|
-
</p>
|
|
24
|
+
> DSH Skills Manager 是社区维护的插件,并非 DeepSeek AI 官方产品。
|
|
30
25
|
|
|
31
|
-
|
|
26
|
+
## 功能概览
|
|
32
27
|
|
|
33
|
-
|
|
28
|
+
- 在同一设置页查看本地 `$DSH_HOME\skills` 与公共 `$DSH_AGENTS_HOME\skills`。
|
|
29
|
+
- 对 DSH 本地技能执行启用、停用、上传、覆盖和删除。
|
|
30
|
+
- 公共 Agent 技能始终只读,不改写共享的全局元数据。
|
|
31
|
+
- 使用系统文件选择器导入包含 `SKILL.md` 的插件目录,并对同名覆盖强制确认。
|
|
32
|
+
- 拒绝目录穿越、从 DSH 技能目录导入自身及符号链接导入。
|
|
34
33
|
|
|
35
|
-
|
|
34
|
+

|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
- **本地技能管理**:仅对 DSH 本地技能提供启用、停用、上传、覆盖和删除。
|
|
39
|
-
- **公共技能安全查看**:公共 Agent 技能可见但严格只读,绝不会改写其全局元数据。
|
|
40
|
-
- **系统原生选择**:通过操作系统文件选择器选择插件的 `SKILL.md`;客户端无法提供所选文件路径时,可显式选择插件目录。
|
|
41
|
-
- **覆盖保护**:导入同名技能一定要求确认;flat 与 bundle 两种形态同名时同样如此。
|
|
42
|
-
- **更安全的导入**:拒绝从 DSH 技能目录自身、其父目录或子目录导入,也拒绝符号链接。
|
|
43
|
-
- **弹窗行为可预期**:按 ESC 只关闭当前上传框或确认框,不会关闭设置页面。
|
|
36
|
+
## 前置条件
|
|
44
37
|
|
|
45
|
-
|
|
38
|
+
- 已可正常运行 DeepSeek Harness Web,且可在 PowerShell 中使用 `dsh`。
|
|
39
|
+
- 以下示例使用 `web` profile;请替换为实际目标 profile。
|
|
40
|
+
- 从源码安装或二次开发需要 Node.js 20+;仅从 npm 安装无需在任意目录执行 `npm install`。
|
|
46
41
|
|
|
47
|
-
|
|
48
|
-
| --- | --- | --- | --- | --- |
|
|
49
|
-
| `$DSH_HOME\skills` | 支持 | 支持 | 支持 | 支持 |
|
|
50
|
-
| `$DSH_AGENTS_HOME\skills` | 支持 | 不支持 | 不支持 | 不支持 |
|
|
42
|
+
## 安装
|
|
51
43
|
|
|
52
|
-
|
|
44
|
+
### 从 npm 安装
|
|
53
45
|
|
|
54
|
-
|
|
46
|
+
在任意 PowerShell 目录执行。请通过 `dsh plugin` 安装到 DSH profile:
|
|
55
47
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
48
|
+
```powershell
|
|
49
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
50
|
+
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
51
|
+
dsh plugin --profile web add @michengai/dsh-skills-manager
|
|
52
|
+
dsh --profile web --dump-config
|
|
53
|
+
```
|
|
59
54
|
|
|
60
|
-
|
|
55
|
+
安装或升级后重启 DSH Web,或重新加载当前 Web profile。若镜像未同步最新版本,可在安装命令末尾追加 `--registry=https://registry.npmjs.org/`。
|
|
61
56
|
|
|
62
|
-
|
|
57
|
+
### 从源码安装
|
|
63
58
|
|
|
64
|
-
|
|
59
|
+
适用于调试或使用未发布改动。克隆后的目录会直接作为插件安装路径:
|
|
65
60
|
|
|
66
61
|
```powershell
|
|
67
62
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
68
63
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
69
|
-
|
|
64
|
+
Set-Location D:\Repository\deepseek-harness-plugin
|
|
65
|
+
git clone https://github.com/MichengAI/dsh-skills-manager.git
|
|
66
|
+
Set-Location .\dsh-skills-manager
|
|
67
|
+
npm install
|
|
68
|
+
npm test
|
|
69
|
+
dsh plugin --profile web add .
|
|
70
|
+
dsh --profile web --dump-config
|
|
70
71
|
```
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
3. 打开「设置 → 技能」。无需下载源码。
|
|
73
|
+
完成后重启 DSH Web 或重新加载当前 Web profile。`dsh plugin ... add .` 会自动读取并应用 `cordis.patch.yml`;不要手工复制 `lib` 文件。
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
## 使用
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
打开「设置 → 技能」,再按下表操作:
|
|
78
78
|
|
|
79
79
|
| 目标 | 操作 | 范围 |
|
|
80
80
|
| --- | --- | --- |
|
|
81
|
-
| 查看技能 |
|
|
82
|
-
| 启用或停用 | 点击「启用」或「停用」。它会同步控制模型调用和 `/`
|
|
83
|
-
| 上传插件 | 点击「上传插件」,选择插件目录内的 `SKILL.md
|
|
84
|
-
| 改选插件目录 |
|
|
85
|
-
| 覆盖或删除 |
|
|
81
|
+
| 查看技能 | 查看名称、形态、说明和调用状态。 | DSH 与公共 Agent 技能 |
|
|
82
|
+
| 启用或停用 | 点击「启用」或「停用」。它会同步控制模型调用和 `/` 手动命令。 | 仅 DSH 本地技能 |
|
|
83
|
+
| 上传插件 | 点击「上传插件」,选择插件目录内的 `SKILL.md`。完整目录、脚本和资源都会被复制。 | 仅 DSH 本地技能 |
|
|
84
|
+
| 改选插件目录 | 所选文件无可用路径时,选择包含 `SKILL.md` 的目录。 | 仅 DSH 本地技能 |
|
|
85
|
+
| 覆盖或删除 | 同名时确认覆盖;使用「删除」移除不再需要的本地技能。 | 仅 DSH 本地技能 |
|
|
86
86
|
| 查看公共技能 | 查看公共 Agent 技能,不修改其元数据。 | 只读 |
|
|
87
87
|
|
|
88
|
-
按 ESC
|
|
88
|
+
按 ESC 只关闭最上层上传框或确认框,设置页会保持打开。
|
|
89
|
+
|
|
90
|
+
## 权限与安全边界
|
|
91
|
+
|
|
92
|
+
| 目录 | 查看 | 启用或停用 | 上传或覆盖 | 删除 |
|
|
93
|
+
| --- | --- | --- | --- | --- |
|
|
94
|
+
| `$DSH_HOME\skills` | 支持 | 支持 | 支持 | 支持 |
|
|
95
|
+
| `$DSH_AGENTS_HOME\skills` | 支持 | 不支持 | 不支持 | 不支持 |
|
|
89
96
|
|
|
90
|
-
|
|
97
|
+
- 启用、停用和删除只接受单个普通技能名称,目录穿越名称会被拒绝。
|
|
98
|
+
- 覆盖前先复制到同目录临时路径;复制成功前不会改动现有技能。
|
|
99
|
+
- 写入接口要求 JSON 与 DSH 客户端请求标记,跨站浏览器请求不能触发本地文件操作。
|
|
100
|
+
|
|
101
|
+
## 二次开发
|
|
102
|
+
|
|
103
|
+
当前仓库未提供 `src` 源目录,`lib` 是直接维护的运行源码;这是当前仓库的实现方式,不是新插件的推荐布局。新插件建议使用 `src` 开发并构建到 `lib`:
|
|
104
|
+
|
|
105
|
+
- [lib\index.js](lib/index.js):Host 服务与本地技能文件操作入口。
|
|
106
|
+
- [lib\client.js](lib/client.js):设置页、上传和确认交互。
|
|
107
|
+
- `test\core-test.mjs`:文件操作、权限和导入边界测试。
|
|
108
|
+
- `test\locale-test.mjs`:界面词条测试。
|
|
109
|
+
|
|
110
|
+
修改后运行测试、检查发布内容并以本地目录安装验证:
|
|
91
111
|
|
|
92
112
|
```powershell
|
|
93
113
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
94
114
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
95
|
-
|
|
115
|
+
npm test
|
|
116
|
+
npm run pack:check
|
|
117
|
+
dsh plugin --profile web add .
|
|
96
118
|
```
|
|
97
119
|
|
|
98
|
-
|
|
120
|
+
修改文件写入逻辑时必须保留路径校验、临时目录复制和公共技能只读限制。
|
|
99
121
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
## 维护者发布
|
|
103
|
-
|
|
104
|
-
本包已发布为 [`@michengai/dsh-skills-manager`](https://www.npmjs.com/package/@michengai/dsh-skills-manager)。发布新版本时执行:
|
|
122
|
+
## 验证
|
|
105
123
|
|
|
106
124
|
```powershell
|
|
107
125
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
108
126
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
109
|
-
npm
|
|
110
|
-
npm
|
|
127
|
+
npm test
|
|
128
|
+
npm run pack:check
|
|
111
129
|
```
|
|
112
130
|
|
|
113
|
-
|
|
131
|
+
`prepublishOnly` 会在发布前自动执行核心测试。
|
|
132
|
+
|
|
133
|
+
## 项目文档与许可证
|
|
114
134
|
|
|
115
|
-
|
|
135
|
+
项目状态、使用边界、技术架构和迭代记录从[文档交接入口](docs/00-交接入口/00-阅读导航.md)开始。详细操作说明见 `docs\02-产品与业务\01-使用说明.md`。
|
|
116
136
|
|
|
117
137
|
本项目采用 [Apache License 2.0](LICENSE)。
|
package/lib/client.js
CHANGED
|
@@ -84,6 +84,12 @@ window.__ModuleLoader__.load({
|
|
|
84
84
|
"error.import.invalidName": "无法生成合法 kebab-case 名称(原始名: {name})",
|
|
85
85
|
"error.import.duplicateName": "批量来源中存在多个同名插件: {name}",
|
|
86
86
|
"error.import.failed": "导入失败",
|
|
87
|
+
"error.proto.forbidden": "禁止的修改请求(缺少客户端标记)",
|
|
88
|
+
"error.proto.contentType": "请求体必须是 application/json",
|
|
89
|
+
"error.proto.method": "不支持的请求方法",
|
|
90
|
+
"error.proto.unknownAction": "未知操作",
|
|
91
|
+
"error.proto.bodyTooLarge": "请求体过大",
|
|
92
|
+
"error.proto.invalidJson": "请求体不是合法 JSON",
|
|
87
93
|
"action.enable": "启用",
|
|
88
94
|
"action.disable": "停用",
|
|
89
95
|
"action.delete": "删除",
|
|
@@ -160,6 +166,12 @@ window.__ModuleLoader__.load({
|
|
|
160
166
|
"error.import.invalidName": "Cannot generate a valid kebab-case name (original name: {name})",
|
|
161
167
|
"error.import.duplicateName": "Batch source contains multiple plugins with the same name: {name}",
|
|
162
168
|
"error.import.failed": "Import failed",
|
|
169
|
+
"error.proto.forbidden": "Forbidden mutation request (missing client marker)",
|
|
170
|
+
"error.proto.contentType": "Content type must be application/json",
|
|
171
|
+
"error.proto.method": "Method not allowed",
|
|
172
|
+
"error.proto.unknownAction": "Unknown action",
|
|
173
|
+
"error.proto.bodyTooLarge": "Request body too large",
|
|
174
|
+
"error.proto.invalidJson": "Invalid JSON request body",
|
|
163
175
|
"action.enable": "enable",
|
|
164
176
|
"action.disable": "disable",
|
|
165
177
|
"action.delete": "deleting",
|
|
@@ -296,12 +308,12 @@ window.__ModuleLoader__.load({
|
|
|
296
308
|
function selectFile(file) {
|
|
297
309
|
if (!file) return;
|
|
298
310
|
if (file.name.toLowerCase() !== "skill.md") {
|
|
299
|
-
setImportResult({
|
|
311
|
+
setImportResult({ code: "select.file.invalid" });
|
|
300
312
|
return;
|
|
301
313
|
}
|
|
302
314
|
if (!file.path) {
|
|
303
315
|
setSelected(null);
|
|
304
|
-
setImportResult({
|
|
316
|
+
setImportResult({ code: "select.path.missing" });
|
|
305
317
|
return;
|
|
306
318
|
}
|
|
307
319
|
setSelected({ name: file.name, source: file.path });
|
|
@@ -310,7 +322,7 @@ window.__ModuleLoader__.load({
|
|
|
310
322
|
|
|
311
323
|
function selectDirectory() {
|
|
312
324
|
if (!pickDirectory) {
|
|
313
|
-
setImportResult({
|
|
325
|
+
setImportResult({ code: "dir.service.missing" });
|
|
314
326
|
return;
|
|
315
327
|
}
|
|
316
328
|
setImportResult(null);
|
|
@@ -318,12 +330,12 @@ window.__ModuleLoader__.load({
|
|
|
318
330
|
pickDirectory().then(function (path) {
|
|
319
331
|
setBusy(null);
|
|
320
332
|
if (path) {
|
|
321
|
-
setSelected({
|
|
333
|
+
setSelected({ nameKey: "dir.selected", source: path });
|
|
322
334
|
setImportResult(null);
|
|
323
335
|
}
|
|
324
336
|
}).catch(function (error) {
|
|
325
337
|
setBusy(null);
|
|
326
|
-
setImportResult({
|
|
338
|
+
setImportResult({ code: "dir.pick.failed", params: { error: String(error && error.message || error) } });
|
|
327
339
|
});
|
|
328
340
|
}
|
|
329
341
|
|
|
@@ -403,13 +415,15 @@ window.__ModuleLoader__.load({
|
|
|
403
415
|
if (importResult) {
|
|
404
416
|
var nameSeparator = t("sep.names");
|
|
405
417
|
var errorSeparator = t("sep.errors");
|
|
418
|
+
// 错误形态:业务/协议错误带 code,或 host 失败带 error(Error 对象);结果形态只有 imported/failed/skipped。
|
|
419
|
+
var isImportError = importResult.code !== undefined || importResult.error !== undefined;
|
|
406
420
|
var importedNames = (importResult.imported || []).map(function (item) { return item.name; });
|
|
407
421
|
// catch 场景把 core 的失败明细挂在 Error.failed 上,这里一并取用。
|
|
408
422
|
var failedItems = importResult.failed || (importResult.error && importResult.error.failed) || [];
|
|
409
423
|
var skippedNames = (importResult.skipped || []).map(function (item) { return item.name; });
|
|
410
424
|
var failedText = failedItems.map(function (item) { return translateError(t, item); }).join(errorSeparator);
|
|
411
425
|
var resultText;
|
|
412
|
-
if (
|
|
426
|
+
if (isImportError) {
|
|
413
427
|
// 顶层聚合错误优先展示已翻译的失败明细;无明细时由 translateError 兜底(含 Error 对象)。
|
|
414
428
|
resultText = t("result.failed", { error: failedItems.length ? failedText : translateError(t, importResult) });
|
|
415
429
|
} else if (failedItems.length) {
|
|
@@ -421,16 +435,16 @@ window.__ModuleLoader__.load({
|
|
|
421
435
|
} else {
|
|
422
436
|
resultText = t("result.none");
|
|
423
437
|
}
|
|
424
|
-
nodes.push(h("div", { key: "import-result", className:
|
|
438
|
+
nodes.push(h("div", { key: "import-result", className: isImportError || failedItems.length ? "dssm-error" : "dssm-note", role: "status" }, resultText));
|
|
425
439
|
}
|
|
426
440
|
if (uploadOpen) {
|
|
427
441
|
nodes.push(h("div", { key: "upload", className: "dssm-mask" }, h("div", { className: "dssm-modal", role: "dialog", "aria-modal": "true", "aria-labelledby": "dssm-upload-title" },
|
|
428
442
|
h("div", { className: "dssm-modal-head" }, h("h3", { id: "dssm-upload-title", className: "dssm-modal-title" }, t("upload.title")), h("button", { className: "dssm-icon-btn", "aria-label": t("upload.close"), onClick: function () { setUploadOpen(false); } }, "×")),
|
|
429
443
|
h("input", { ref: inputRef, className: "dssm-hidden-input", type: "file", accept: ".md", onChange: function (event) { selectFile(event.target.files && event.target.files[0]); event.target.value = ""; } }),
|
|
430
444
|
h("div", { className: "dssm-dropzone", tabIndex: 0, onClick: openNativePicker, onKeyDown: function (event) { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); openNativePicker(); } }, onDragOver: function (event) { event.preventDefault(); }, onDrop: function (event) { event.preventDefault(); selectFile(event.dataTransfer.files && event.dataTransfer.files[0]); } }, h("span", { className: "dssm-dropzone-title" }, t("upload.drop.title")), h("span", { className: "dssm-dropzone-copy" }, t("upload.drop.copy"))),
|
|
431
|
-
selected ? h("div", { className: "dssm-file", title: selected.source }, h("span", { className: "dssm-file-name" }, selected.name), h("span", { className: "dssm-tag dssm-tag-on" }, t("status.selected"))) : h("p", { className: "dssm-desc" }, t("upload.hint")),
|
|
445
|
+
selected ? h("div", { className: "dssm-file", title: selected.source }, h("span", { className: "dssm-file-name" }, selected.nameKey ? t(selected.nameKey) : selected.name), h("span", { className: "dssm-tag dssm-tag-on" }, t("status.selected"))) : h("p", { className: "dssm-desc" }, t("upload.hint")),
|
|
432
446
|
busy === "pick-directory" ? h("div", { className: "dssm-note", role: "status" }, t("upload.picking.dir")) : null,
|
|
433
|
-
importResult && importResult.error ? h("div", { className: "dssm-error", role: "alert" }, t("select.failed", { error: translateError(t, importResult) })) : null,
|
|
447
|
+
importResult && (importResult.code !== undefined || importResult.error !== undefined) ? h("div", { className: "dssm-error", role: "alert" }, t("select.failed", { error: translateError(t, importResult) })) : null,
|
|
434
448
|
h("div", { className: "dssm-modal-actions" }, h("button", { className: "dssm-btn dssm-btn-secondary", disabled: !!busy, onClick: openNativePicker }, t("btn.file.pick")), pickDirectory ? h("button", { className: "dssm-btn dssm-btn-secondary", disabled: !!busy, onClick: selectDirectory }, t("btn.dir.pick")) : null, h("button", { className: "dssm-btn", disabled: !selected || !!busy, onClick: installSelected }, busy === "import" || busy === "import-check" ? t("btn.uploading") : t("btn.upload.dsh"))))));
|
|
435
449
|
}
|
|
436
450
|
if (confirmImport) {
|
|
@@ -448,7 +462,7 @@ window.__ModuleLoader__.load({
|
|
|
448
462
|
return ctx.locale.register(NS, DICT);
|
|
449
463
|
}, "skills-manager: dictionaries");
|
|
450
464
|
ctx.slots.inject("settings.section", function () {
|
|
451
|
-
return ctx.slots.register({ name: "settings.section", id: "skills-manager", order: 17, label: function () { return ctx.locale.bind(NS)("title"); }, locale: NS }, SkillManagerSection);
|
|
465
|
+
return ctx.slots.register({ name: "settings.section", id: "skills-manager", order: 17, label: function () { return ctx.locale.bind(NS)("title"); }, icon: "skill", locale: NS }, SkillManagerSection);
|
|
452
466
|
});
|
|
453
467
|
}
|
|
454
468
|
module.exports.apply = apply;
|
package/lib/core.js
CHANGED
|
@@ -29,9 +29,9 @@ function codedError(message, code, params) {
|
|
|
29
29
|
return error;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
/** 把业务 Error 的 code/params
|
|
32
|
+
/** 把业务 Error 的 code/params 附加到失败明细;系统异常(ENOENT 等,非 error.* 前缀)保持原文。 */
|
|
33
33
|
function attachCode(item, error) {
|
|
34
|
-
if (error && typeof error.code === "string") item.code = error.code;
|
|
34
|
+
if (error && typeof error.code === "string" && /^error\./.test(error.code)) item.code = error.code;
|
|
35
35
|
if (error && error.params) item.params = error.params;
|
|
36
36
|
return item;
|
|
37
37
|
}
|
package/lib/index.js
CHANGED
|
@@ -47,6 +47,7 @@ function readBody(req, limit = 1 << 20) {
|
|
|
47
47
|
if (size > limit) {
|
|
48
48
|
const error = new Error("body too large");
|
|
49
49
|
error.statusCode = 413;
|
|
50
|
+
error.code = "error.proto.bodyTooLarge";
|
|
50
51
|
fail(error);
|
|
51
52
|
req.resume();
|
|
52
53
|
return;
|
|
@@ -63,6 +64,7 @@ function readBody(req, limit = 1 << 20) {
|
|
|
63
64
|
} catch (e) {
|
|
64
65
|
const error = new Error(`invalid JSON body: ${e.message}`);
|
|
65
66
|
error.statusCode = 400;
|
|
67
|
+
error.code = "error.proto.invalidJson";
|
|
66
68
|
fail(error);
|
|
67
69
|
}
|
|
68
70
|
});
|
|
@@ -72,9 +74,9 @@ function readBody(req, limit = 1 << 20) {
|
|
|
72
74
|
|
|
73
75
|
/** 自定义请求头使跨站 fetch 必须预检;本地接口不提供 CORS 响应。 */
|
|
74
76
|
function validateMutationRequest(req) {
|
|
75
|
-
if (req.headers[CLIENT_MARKER_HEADER] !== "1") return { statusCode: 403, error: "forbidden mutation request" };
|
|
77
|
+
if (req.headers[CLIENT_MARKER_HEADER] !== "1") return { statusCode: 403, code: "error.proto.forbidden", error: "forbidden mutation request" };
|
|
76
78
|
const contentType = String(req.headers["content-type"] || "").split(";", 1)[0].trim().toLowerCase();
|
|
77
|
-
if (contentType !== "application/json") return { statusCode: 415, error: "content-type must be application/json" };
|
|
79
|
+
if (contentType !== "application/json") return { statusCode: 415, code: "error.proto.contentType", error: "content-type must be application/json" };
|
|
78
80
|
return null;
|
|
79
81
|
}
|
|
80
82
|
|
|
@@ -134,7 +136,7 @@ function apply(ctx) {
|
|
|
134
136
|
return;
|
|
135
137
|
}
|
|
136
138
|
if (req.method !== "POST") {
|
|
137
|
-
json(res, 405, { ok: false, error: `method not allowed: ${req.method}` });
|
|
139
|
+
json(res, 405, { ok: false, code: "error.proto.method", error: `method not allowed: ${req.method}` });
|
|
138
140
|
return;
|
|
139
141
|
}
|
|
140
142
|
const requestError = validateMutationRequest(req);
|
|
@@ -163,10 +165,14 @@ function apply(ctx) {
|
|
|
163
165
|
}), body.dryRun === true ? undefined : invalidateSkills);
|
|
164
166
|
return;
|
|
165
167
|
default:
|
|
166
|
-
json(res, 404, { ok: false, error: `unknown action: ${path}` });
|
|
168
|
+
json(res, 404, { ok: false, code: "error.proto.unknownAction", error: `unknown action: ${path}` });
|
|
167
169
|
}
|
|
168
170
|
} catch (e) {
|
|
169
|
-
json(res, Number.isInteger(e && e.statusCode) ? e.statusCode : 500, {
|
|
171
|
+
json(res, Number.isInteger(e && e.statusCode) ? e.statusCode : 500, {
|
|
172
|
+
ok: false,
|
|
173
|
+
...(e && e.code ? { code: e.code } : {}),
|
|
174
|
+
error: String(e && e.message ? e.message : e),
|
|
175
|
+
});
|
|
170
176
|
}
|
|
171
177
|
},
|
|
172
178
|
});
|
package/package.json
CHANGED