dsh-plugin-labels 0.7.0 → 0.9.0
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 +64 -21
- package/lib/index.js +505 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,20 +3,29 @@
|
|
|
3
3
|
[](https://github.com/qyy2005/dsh-plugin-labels/actions/workflows/ci.yml)
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
> 版本更新记录见 [CHANGELOG.md](CHANGELOG.md),或在 GitHub [Releases](https://github.com/qyy2005/dsh-plugin-labels/releases) 查看。
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
给 DSH(DeepSeek Harness)**已安装插件**配「中文名 + 一句话说明」:官方「插件」页的已安装插件行、插件包卡片与详情页都会**原地显示中文名与中文描述**(由 host 覆盖显示元数据);新装插件会在启动后自动翻译。同时给 **skills(技能)** 补中文说明:`/` 技能菜单里每条技能显示「英文原描述 |中文:<作用>;用法:<怎么用>」。**本插件没有任何 UI 入口**(没有 tab、没有设置项),管理通过 loopback HTTP API 或让 AI 代改。
|
|
9
|
+
|
|
10
|
+
- 当前版本:**0.9.0**(本仓库即源码目录;通过 `plugin_manager install_bundle` 从 GitHub 或 npm 安装到 DSH profile)
|
|
11
|
+
- 只支持 DSH 桌面版 Web GUI(开发验证版本 0.2.1-rc.2 / rc.3)
|
|
10
12
|
|
|
11
13
|
## 功能
|
|
12
14
|
|
|
13
|
-
- **中文名 + 一句话说明**:每个插件一条中文名(≤
|
|
15
|
+
- **中文名 + 一句话说明**:每个插件一条中文名(≤10 字)和一句说明(≤80 字);说明必须写清**怎么用**,不只是“是什么”(见下方「标签格式」)。
|
|
14
16
|
- **官方页原地中文**:官方「插件」页的已安装插件行、点开的详情页,以及插件包卡片(`listBundles`)都会直接显示中文名与中文描述;未翻译的插件回退英文。默认开启。
|
|
15
|
-
-
|
|
17
|
+
- **技能中文翻译**:`/` 技能菜单里每条技能追加「|中文:作用;用法:怎么用」,看懂技能是做什么、什么时候用(见「技能中文翻译」)。
|
|
18
|
+
- **自动翻译新插件与新技能**:host 启动约 4 秒后自动补一次缺失标签(每次最多 20 条),再补最多 10 条技能说明;只补空缺、不覆盖手改。
|
|
16
19
|
- **手改优先**:手动设置的标签不会被自动翻译覆盖;显式“重翻”才会覆盖。
|
|
17
|
-
- **无 UI 管理入口**:通过
|
|
20
|
+
- **无 UI 管理入口**:通过 7 个 loopback HTTP API(见「使用」)或让 AI/脚本直接调用它们来翻译、手改、重翻、导入导出。
|
|
18
21
|
- **导出 / 导入 API**:导出带 `source`(`ai` / `manual`)的 JSON,导入时保留来源,只接受已安装插件名(≤64KB)。
|
|
19
|
-
-
|
|
22
|
+
- **缓存**:插件标签在 `<DSH_HOME>/plugin-labels.json`(v2),技能说明在 `<DSH_HOME>/plugin-labels-skills.json`(v1);均 UTF-8 无 BOM、原子写,旧 v1 标签数据可迁移读取。
|
|
23
|
+
|
|
24
|
+
### 标签格式
|
|
25
|
+
|
|
26
|
+
- **中文名**:≤10 个汉字,命名它“做什么”,不直译包名、不用营销词。
|
|
27
|
+
- **一句话说明**:≤80 字,必须同时回答:①这插件解决什么问题;②在 DSH 里从哪里进入 / 怎么操作(例如:设置 → 某个分区、侧栏 → 某个页面、在对话里下指令、装好自动生效);③必要时给一句可执行的第一步。
|
|
28
|
+
- 模型无法从包名/描述判断入口时,会被要求写「**入口以界面为准**」并给出用户能做的第一步;**不会编造不存在的菜单或按钮**。
|
|
20
29
|
|
|
21
30
|
## 界面
|
|
22
31
|
|
|
@@ -47,12 +56,27 @@ host 启动约 **4 秒**后会自动补一次缺失标签:
|
|
|
47
56
|
- 与手动翻译共用同一个单飞锁与 120 秒超时;失败只写日志(warn),不抛错、不阻塞启动。
|
|
48
57
|
- 手动补翻译:`POST /api/plugin-labels/translate`(见「使用」)。
|
|
49
58
|
|
|
59
|
+
## 技能中文翻译
|
|
60
|
+
|
|
61
|
+
用户的技能大多是英文。本插件会在技能目录的描述末尾追加中文说明,**`/` 技能菜单里每条技能显示**:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
<英文原描述> |中文:<中文作用>;用法:<怎么用>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- **实现**:host 包装 `skills` 服务的 `snapshot`/`list`,命中缓存才追加,未命中保持原样(`/` 菜单走 `skills.list`,模型侧技能目录走同一份 description)。
|
|
68
|
+
- **缓存**:`<DSH_HOME>/plugin-labels-skills.json`(独立于插件标签缓存,`version: 1`,UTF-8 无 BOM、原子写);每条 `{zh ≤80 字, usage ≤60 字, source: "ai" | "manual", at}`。
|
|
69
|
+
- **API**(见「使用」):`GET /api/plugin-labels/skills`、`POST /api/plugin-labels/skills/translate`、`POST /api/plugin-labels/skills/label`。
|
|
70
|
+
- **自动翻译**:启动后约 4 秒的自动任务在补完插件标签后,会再补最多 **10** 条技能说明(`MAX_SKILLS_PER_AUTO`);只补空缺、不覆盖手改;`DSH_PLUGIN_LABELS_AUTO_TRANSLATE=0` 同时关闭插件与技能自动翻译。
|
|
71
|
+
- **限制**:只翻译/解释技能的目录描述,**技能正文仍是英文**;模型侧技能目录会把 description 截断到 500 字,追加在末尾的中文通常不会明显增加模型上下文;技能服务(`ctx.get("skills")`)不可用时静默跳过,不影响其它功能。
|
|
72
|
+
- **重启**:host 代码在启动时安装,**需要重启 DSH 一次**;`GET /api/plugin-labels/list` 的 `officialMeta.skills` 可确认包装是否已生效。
|
|
73
|
+
|
|
50
74
|
## 安装
|
|
51
75
|
|
|
52
76
|
### 方式一:npm 包(推荐,`npm publish` 后)
|
|
53
77
|
|
|
54
78
|
在 DSH 插件页 → 添加插件 → 填包名 `dsh-plugin-labels`;或在创造模式会话里 `plugin_manager` → `install_bundle` → `target: "dsh-plugin-labels"`。发布后 profile 依赖形如
|
|
55
|
-
`"dsh-plugin-labels": "dsh-plugin-labels@^0.
|
|
79
|
+
`"dsh-plugin-labels": "dsh-plugin-labels@^0.9.0"`。
|
|
56
80
|
|
|
57
81
|
### 方式二:从 Git 仓库安装
|
|
58
82
|
|
|
@@ -68,7 +92,7 @@ host 启动约 **4 秒**后会自动补一次缺失标签:
|
|
|
68
92
|
|
|
69
93
|
## 使用
|
|
70
94
|
|
|
71
|
-
v0.
|
|
95
|
+
v0.9.0 起没有 UI 入口,全部通过 7 个 loopback HTTP API 操作(也可以让 AI/脚本代改):
|
|
72
96
|
|
|
73
97
|
```powershell
|
|
74
98
|
# 查看已装插件 + 标签 + 诊断字段(officialMeta)
|
|
@@ -90,9 +114,21 @@ Invoke-RestMethod -Method Post -ContentType 'application/json' `
|
|
|
90
114
|
# 导入导出的 JSON(只保留已装插件名,保留 ai/manual 来源)
|
|
91
115
|
Invoke-RestMethod -Method Post -ContentType 'application/json' -InFile labels.json `
|
|
92
116
|
http://127.0.0.1:<端口>/api/plugin-labels/import
|
|
117
|
+
|
|
118
|
+
# 技能:列出技能 + 已缓存的中文说明
|
|
119
|
+
Invoke-WebRequest -UseBasicParsing http://127.0.0.1:<端口>/api/plugin-labels/skills
|
|
120
|
+
|
|
121
|
+
# 技能:补翻译缺失的技能说明(每次最多 20 条;可重复调用补剩余)
|
|
122
|
+
Invoke-RestMethod -Method Post -ContentType 'application/json' -Body '{}' `
|
|
123
|
+
http://127.0.0.1:<端口>/api/plugin-labels/skills/translate
|
|
124
|
+
|
|
125
|
+
# 技能:手改一条技能说明(source=manual,自动翻译不会覆盖)
|
|
126
|
+
Invoke-RestMethod -Method Post -ContentType 'application/json' `
|
|
127
|
+
-Body '{"name":"office","zh":"Office 文档处理","usage":"对 AI 说:用 office 技能处理文档"}' `
|
|
128
|
+
http://127.0.0.1:<端口>/api/plugin-labels/skills/label
|
|
93
129
|
```
|
|
94
130
|
|
|
95
|
-
端口就是当前 DSH Web GUI 的端口;这些路由只接受 loopback 请求且必须是 POST(`/list` 为 GET)。自动翻译会替你补空缺,手改过的条目不会被自动覆盖。
|
|
131
|
+
端口就是当前 DSH Web GUI 的端口;这些路由只接受 loopback 请求且必须是 POST(`/list`、`/skills` 为 GET)。自动翻译会替你补空缺,手改过的条目不会被自动覆盖。
|
|
96
132
|
|
|
97
133
|
## 插件速查文档
|
|
98
134
|
|
|
@@ -110,9 +146,9 @@ npm run docs # 等价于 node scripts/gen-doc.mjs
|
|
|
110
146
|
|
|
111
147
|
## 兼容性
|
|
112
148
|
|
|
113
|
-
- DSH 桌面版 **0.2.1-rc.2**(Web GUI
|
|
114
|
-
- 本插件不依赖任何 UI slot:显示覆盖包装宿主的 `pluginPackages.metaOf()`(官方页行/详情)与 `pluginManager.listBundles()
|
|
115
|
-
-
|
|
149
|
+
- DSH 桌面版 **0.2.1-rc.2**(Web GUI;技能翻译接口在 **0.2.1-rc.3** 核实);依赖官方 client 基座模块与 host 服务,只 `require("react")`。
|
|
150
|
+
- 本插件不依赖任何 UI slot:显示覆盖包装宿主的 `pluginPackages.metaOf()`(官方页行/详情)与 `pluginManager.listBundles()`(插件包卡片),技能中文包装 `skills` 服务的 `snapshot`/`list`,client 只注册 `locale` 字典。
|
|
151
|
+
- 拿不到这些内部服务时静默回退:不显示中文(回退英文)、技能说明保持英文,插件其它功能与 API 仍可用。
|
|
116
152
|
|
|
117
153
|
## 开发说明
|
|
118
154
|
|
|
@@ -122,10 +158,13 @@ npm run docs # 等价于 node scripts/gen-doc.mjs
|
|
|
122
158
|
- `POST /api/plugin-labels/translate`(无参=补空缺;`{names:[...]}`=显式重翻)
|
|
123
159
|
- `POST /api/plugin-labels/label`(`{name,title,oneLine}` 或 `{name,action:"reset"}`)
|
|
124
160
|
- `POST /api/plugin-labels/import`
|
|
125
|
-
-
|
|
126
|
-
-
|
|
127
|
-
-
|
|
128
|
-
-
|
|
161
|
+
- `GET /api/plugin-labels/skills`(技能列表 + 已缓存中文说明)
|
|
162
|
+
- `POST /api/plugin-labels/skills/translate`(补技能说明,单飞锁,并发 409)
|
|
163
|
+
- `POST /api/plugin-labels/skills/label`(`{name,zh,usage}` 手改技能说明)
|
|
164
|
+
- **自动翻译**:`apply` 里注册一个 4 秒后执行一次的定时器(`unref`),先 `translatePending` 补插件标签、再 `translateSkillsPending` 补最多 10 条技能说明;可用 `DSH_PLUGIN_LABELS_AUTO_TRANSLATE=0` 关闭;异常只 warn。
|
|
165
|
+
- **显示/技能覆盖**:`installOfficialMeta` / `installBundleMeta` / `installSkillTranslations` 用 symbol 标记识别 wrapper,幂等、可 dispose、服务替换后重打、失败静默回退。`DSH_PLUGIN_LABELS_OFFICIAL_META=0` 关闭前两个显示覆盖(技能翻译不受影响)。
|
|
166
|
+
- **安全围栏**:loopback 校验、`sec-fetch-site` 跨站拒绝、POST-only、翻译单飞锁(插件与技能各一把,并发 409)、`AbortSignal.timeout(120s)`、未预期异常统一 500 不泄漏路径。
|
|
167
|
+
- **缓存**:插件标签 `<DSH_HOME>/plugin-labels.json`(v2,旧 v1 可迁移)、技能说明 `<DSH_HOME>/plugin-labels-skills.json`(v1);均 UTF-8 无 BOM,临时文件 + `rename` 原子写,单条非法条目丢弃。
|
|
129
168
|
- **验证**:`node --check lib/index.js`、`node --check lib/client.js`;host 可用桩 `ctx` 跑冒烟。
|
|
130
169
|
- 修改 host 后需重启 DSH 生效(本插件已没有需要刷新页面的 UI 改动)。
|
|
131
170
|
|
|
@@ -142,7 +181,7 @@ dsh-plugin-labels/
|
|
|
142
181
|
├─ package.json # manifest(dsh.bundle 组合 patch、dsh.client 平台与 inject)
|
|
143
182
|
├─ cordis.patch.yml # bundle 组合 patch
|
|
144
183
|
├─ lib/
|
|
145
|
-
│ ├─ index.js # host:HTTP API + 显示覆盖 + 自动翻译 +
|
|
184
|
+
│ ├─ index.js # host:HTTP API + 显示覆盖 + 技能中文 + 自动翻译 + 缓存
|
|
146
185
|
│ └─ client.js # client:模块骨架,仅注册 locale(v0.7.0 起无 UI)
|
|
147
186
|
├─ locale/
|
|
148
187
|
│ ├─ zh.json # 插件元数据(中文)
|
|
@@ -162,8 +201,12 @@ MIT,见 [LICENSE](LICENSE)。Copyright (c) 2026 qyy2005
|
|
|
162
201
|
|
|
163
202
|
## English summary
|
|
164
203
|
|
|
165
|
-
`dsh-plugin-labels` is a DSH (DeepSeek Harness) plugin that gives every installed plugin a Chinese name and a one-line explanation. The labels are shown **in place** on the official Plugins page: the installed-plugin rows, the detail pages and the plugin package cards (`pluginManager.listBundles`) all display the Chinese name and description, falling back to English for plugins without a label. **The plugin has no UI entry point** (no tab, no settings item): labels are managed through
|
|
204
|
+
`dsh-plugin-labels` is a DSH (DeepSeek Harness) plugin that gives every installed plugin a Chinese name and a one-line explanation. The labels are shown **in place** on the official Plugins page: the installed-plugin rows, the detail pages and the plugin package cards (`pluginManager.listBundles`) all display the Chinese name and description, falling back to English for plugins without a label. **The plugin has no UI entry point** (no tab, no settings item): labels are managed through loopback HTTP routes (or by an AI/script calling them), and results are cached under `<DSH_HOME>/plugin-labels.json` with manual edits never overwritten by automatic translation.
|
|
205
|
+
|
|
206
|
+
It also translates **skills**: each skill description in the `/` skill menu gets a Chinese purpose and usage hint appended (`English description |中文:…;用法:…`), cached separately in `<DSH_HOME>/plugin-labels-skills.json` and managed through `GET/POST /api/plugin-labels/skills…`. Only the catalogue description is translated, the skill body stays English; the model-side catalogue truncates descriptions at 500 characters.
|
|
207
|
+
|
|
208
|
+
Each label is a Chinese name (≤10 characters) plus a one-line explanation (≤80 characters) that must explain **how to use** the plugin: the problem it solves, where in DSH the user enters or operates it, and a concrete first step. When the entry point cannot be inferred, the model is instructed to write 入口以界面为准 instead of inventing menus.
|
|
166
209
|
|
|
167
|
-
Newly installed plugins are translated automatically: about 4 seconds after host startup one pass fills up to 20 missing labels (set `DSH_PLUGIN_LABELS_AUTO_TRANSLATE=0` and restart DSH to disable it). The display overrides can be disabled together with `DSH_PLUGIN_LABELS_OFFICIAL_META=0`; `GET /api/plugin-labels/list` reports `officialMeta: {enabled, patched, service, bundles}` for diagnostics.
|
|
210
|
+
Newly installed plugins are translated automatically: about 4 seconds after host startup one pass fills up to 20 missing labels and then up to 10 skill explanations (set `DSH_PLUGIN_LABELS_AUTO_TRANSLATE=0` and restart DSH to disable it). The display overrides can be disabled together with `DSH_PLUGIN_LABELS_OFFICIAL_META=0`; `GET /api/plugin-labels/list` reports `officialMeta: {enabled, patched, service, bundles, skills}` for diagnostics. These hooks require one DSH restart after installation.
|
|
168
211
|
|
|
169
|
-
The client half is a hand-written `__ModuleLoader__` bundle with no build step that only requires `react` (v0.7.0: locale registration only, no UI); the host half is an ESM plugin exposing
|
|
212
|
+
The client half is a hand-written `__ModuleLoader__` bundle with no build step that only requires `react` (v0.7.0: locale registration only, no UI); the host half is an ESM plugin exposing seven loopback-only HTTP routes plus the display overrides, the skill translation hook and the startup auto-translate. Verified on DSH desktop 0.2.1-rc.2 / rc.3. MIT licensed.
|
package/lib/index.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dsh-plugin-labels — host half (v0.
|
|
2
|
+
* dsh-plugin-labels — host half (v0.9.0: plugin labels + skill translations).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* longer renders a tab):
|
|
4
|
+
* Loopback-only endpoints:
|
|
6
5
|
* GET /api/plugin-labels/list -> installed profile bundles + cached labels
|
|
7
6
|
* POST /api/plugin-labels/translate -> translate the bundles that have no label yet
|
|
8
7
|
* POST /api/plugin-labels/label -> set one manual label, or reset one back to AI
|
|
9
8
|
* POST /api/plugin-labels/import -> merge an exported labels JSON
|
|
9
|
+
* GET /api/plugin-labels/skills -> skills with their cached Chinese translations
|
|
10
|
+
* POST /api/plugin-labels/skills/translate -> translate missing skill explanations
|
|
11
|
+
* POST /api/plugin-labels/skills/label -> set one manual skill translation
|
|
10
12
|
*
|
|
11
13
|
* Translation uses the live Harness LLM service exactly like the official
|
|
12
14
|
* @deepseek-ai/dsh-experimental-auto-review plugin does:
|
|
@@ -15,19 +17,34 @@
|
|
|
15
17
|
* rename). Manual labels carry source:"manual" and are never overwritten by a
|
|
16
18
|
* later translate run; translating only fills missing entries.
|
|
17
19
|
*
|
|
20
|
+
* Label format (v0.8.0): `title` is a short Chinese name (at most 10 Chinese
|
|
21
|
+
* characters); `oneLine` (at most 80 characters) must explain HOW TO USE the
|
|
22
|
+
* plugin — what problem it solves, where in DSH the user enters or operates it,
|
|
23
|
+
* and a concrete first step when useful. The model is told never to invent
|
|
24
|
+
* menus: when the entry point cannot be inferred it must write 入口以界面为准.
|
|
25
|
+
*
|
|
26
|
+
* Skill translations (v0.9.0): every skill summary gets a Chinese purpose and a
|
|
27
|
+
* short usage hint appended to its `description`, so the `/` skill menu shows
|
|
28
|
+
* "English original |中文:…;用法:…". They live in their own cache
|
|
29
|
+
* <DSH_HOME>/plugin-labels-skills.json. Optional `whenToUse` is sent to the
|
|
30
|
+
* model, the skill body itself is not translated, and the model-side skill
|
|
31
|
+
* catalogue truncates descriptions at 500 characters (the appended Chinese
|
|
32
|
+
* usually falls past that point, so model context barely grows).
|
|
33
|
+
*
|
|
18
34
|
* Display overrides (all failure-safe; marker-detected because Cordis hands out
|
|
19
35
|
* traceable Proxies whose method references differ on every read):
|
|
20
36
|
* - `installOfficialMeta` wraps `pluginPackages.metaOf()` so the official
|
|
21
37
|
* Plugins page rows and detail pages show the Chinese labels in place.
|
|
22
38
|
* - `installBundleMeta` wraps `pluginManager.listBundles()` so the plugin
|
|
23
39
|
* package cards (which call `readPluginMeta` directly) show them too.
|
|
24
|
-
*
|
|
40
|
+
* - `installSkillTranslations` wraps the `skills` service `snapshot`/`list`.
|
|
41
|
+
* Set DSH_PLUGIN_LABELS_OFFICIAL_META=0 to disable the first two overrides.
|
|
25
42
|
*
|
|
26
43
|
* Auto translation: shortly after startup one pass fills up to
|
|
27
|
-
* MAX_PENDING_PER_CALL missing labels through the same core,
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* blocks host startup.
|
|
44
|
+
* MAX_PENDING_PER_CALL missing labels through the same core, then up to
|
|
45
|
+
* MAX_SKILLS_PER_AUTO skill explanations, so newly installed plugins and skills
|
|
46
|
+
* get Chinese text without any UI. Set DSH_PLUGIN_LABELS_AUTO_TRANSLATE=0 to
|
|
47
|
+
* disable it. It never throws into or blocks host startup.
|
|
31
48
|
*
|
|
32
49
|
* Failure policy: every handler catches its own errors and answers with JSON;
|
|
33
50
|
* nothing is allowed to escape as an unhandled rejection (that is fatal to the
|
|
@@ -45,25 +62,46 @@ export const LIST_PATH = "/api/plugin-labels/list";
|
|
|
45
62
|
export const TRANSLATE_PATH = "/api/plugin-labels/translate";
|
|
46
63
|
export const LABEL_PATH = "/api/plugin-labels/label";
|
|
47
64
|
export const IMPORT_PATH = "/api/plugin-labels/import";
|
|
65
|
+
export const SKILLS_PATH = "/api/plugin-labels/skills";
|
|
66
|
+
export const SKILLS_TRANSLATE_PATH = "/api/plugin-labels/skills/translate";
|
|
67
|
+
export const SKILLS_LABEL_PATH = "/api/plugin-labels/skills/label";
|
|
48
68
|
|
|
49
69
|
const CACHE_VERSION = 2;
|
|
50
70
|
const LEGACY_CACHE_VERSION = 1;
|
|
51
71
|
const MAX_PENDING_PER_CALL = 20;
|
|
52
72
|
const MAX_TITLE = 40;
|
|
53
|
-
const MAX_ONE_LINE =
|
|
73
|
+
const MAX_ONE_LINE = 80;
|
|
54
74
|
const MAX_BODY_BYTES = 65536;
|
|
55
75
|
const TRANSLATE_TIMEOUT_MS = 120000;
|
|
56
76
|
|
|
77
|
+
const SKILL_CACHE_VERSION = 1;
|
|
78
|
+
const MAX_SKILL_ZH = 80;
|
|
79
|
+
const MAX_SKILL_USAGE = 60;
|
|
80
|
+
const MAX_SKILL_DESCRIPTION = 600;
|
|
81
|
+
const MAX_SKILLS_PER_AUTO = 10;
|
|
82
|
+
|
|
57
83
|
const SYSTEM_PROMPT = [
|
|
58
84
|
"You label DeepSeek Harness (DSH) plugins for a Chinese-speaking user.",
|
|
59
85
|
"For every plugin you receive, produce:",
|
|
60
|
-
"1. title: a short Chinese name
|
|
61
|
-
"2. oneLine:
|
|
62
|
-
"
|
|
86
|
+
"1. title: a short Chinese name of at most 10 Chinese characters, naming what it does (do not translate the package name literally, do not use marketing words).",
|
|
87
|
+
"2. oneLine: ONE Chinese sentence of at most 80 characters that MUST explain HOW TO USE the plugin, not just what it is. It must cover:",
|
|
88
|
+
" a) the problem the plugin solves (what it is for);",
|
|
89
|
+
" b) where in DSH the user enters or operates it, for example: 设置 → 某个分区, 侧栏 → 某个页面, 在对话里下指令, or 装好后自动生效;",
|
|
90
|
+
" c) when useful, one concrete first step the user can take right now.",
|
|
91
|
+
"Style: concrete, prefer starting with a verb; no filler, no marketing words. If the package name and description do not make the entry point clear, write 入口以界面为准 and still give a first step the user can take. NEVER invent menus, buttons, pages or settings that the description does not imply.",
|
|
63
92
|
"Answer with strict JSON only, no prose, no markdown fences:",
|
|
64
93
|
'{"items":[{"name":"<exact package name>","title":"...","oneLine":"..."}]}',
|
|
65
94
|
].join("\n");
|
|
66
95
|
|
|
96
|
+
const SKILL_SYSTEM_PROMPT = [
|
|
97
|
+
"You translate DeepSeek Harness (DSH) skill descriptions for a Chinese-speaking user who cannot read English.",
|
|
98
|
+
"For every skill you receive, produce:",
|
|
99
|
+
"1. zh: a Chinese name plus one-line purpose, at most 80 Chinese characters, concrete (no marketing words, no literal package-name translation).",
|
|
100
|
+
"2. usage: how and when to use it, at most 60 Chinese characters — for example 对 AI 说\"用 X 技能\" or 输入 /<name>,when the description implies an invocation. Never invent a command that the description does not imply.",
|
|
101
|
+
"Answer with strict JSON only, no prose, no markdown fences:",
|
|
102
|
+
'{"items":[{"name":"<exact skill name>","zh":"...","usage":"..."}]}',
|
|
103
|
+
].join("\n");
|
|
104
|
+
|
|
67
105
|
/**
|
|
68
106
|
* Experimental display-layer override: wrap the host `pluginPackages.metaOf`
|
|
69
107
|
* service so the official Plugins page shows our Chinese labels in place
|
|
@@ -92,9 +130,11 @@ function displayOverrideEnabled() {
|
|
|
92
130
|
*/
|
|
93
131
|
const OFFICIAL_META_MARK = Symbol.for("dsh-plugin-labels.official-meta");
|
|
94
132
|
const BUNDLE_META_MARK = Symbol.for("dsh-plugin-labels.bundle-meta");
|
|
133
|
+
const SKILL_TRANSLATIONS_MARK = Symbol.for("dsh-plugin-labels.skill-translations");
|
|
95
134
|
|
|
96
135
|
const isOfficialMetaWrapper = (value) => Boolean(value && typeof value === "function" && value[OFFICIAL_META_MARK] === true);
|
|
97
136
|
const isBundleMetaWrapper = (value) => Boolean(value && typeof value === "function" && value[BUNDLE_META_MARK] === true);
|
|
137
|
+
const isSkillTranslationWrapper = (value) => Boolean(value && typeof value === "function" && value[SKILL_TRANSLATIONS_MARK] === true);
|
|
98
138
|
|
|
99
139
|
function markWithSymbol(wrapper, symbol) {
|
|
100
140
|
try {
|
|
@@ -118,6 +158,10 @@ function markBundleMetaWrapper(wrapper) {
|
|
|
118
158
|
return markWithSymbol(wrapper, BUNDLE_META_MARK);
|
|
119
159
|
}
|
|
120
160
|
|
|
161
|
+
function markSkillTranslationWrapper(wrapper) {
|
|
162
|
+
return markWithSymbol(wrapper, SKILL_TRANSLATIONS_MARK);
|
|
163
|
+
}
|
|
164
|
+
|
|
121
165
|
/** Split a configured specifier into its bare package name (`@scope/name/sub` -> `@scope/name`). */
|
|
122
166
|
export function bareNameOf(specifier) {
|
|
123
167
|
if (typeof specifier !== "string") return "";
|
|
@@ -430,6 +474,68 @@ function writeCache(home, cache) {
|
|
|
430
474
|
}
|
|
431
475
|
}
|
|
432
476
|
|
|
477
|
+
/**
|
|
478
|
+
* Skill explanation cache: a separate file from the plugin-label cache so the
|
|
479
|
+
* two features never overwrite each other.
|
|
480
|
+
*/
|
|
481
|
+
function skillCachePath(home) {
|
|
482
|
+
return path.join(home, "plugin-labels-skills.json");
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** Validate one cached skill translation; invalid entries are dropped. */
|
|
486
|
+
function normalizeSkillEntry(value) {
|
|
487
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
488
|
+
const zh = typeof value.zh === "string" ? value.zh.trim() : "";
|
|
489
|
+
if (zh === "") return null;
|
|
490
|
+
const usage = typeof value.usage === "string" ? value.usage.trim() : "";
|
|
491
|
+
const source = value.source === "manual" ? "manual" : "ai";
|
|
492
|
+
const entry = { zh: zh.slice(0, MAX_SKILL_ZH), usage: usage.slice(0, MAX_SKILL_USAGE), source };
|
|
493
|
+
if (typeof value.at === "string" && value.at !== "") entry.at = value.at;
|
|
494
|
+
return entry;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function emptySkillCache() {
|
|
498
|
+
return { version: SKILL_CACHE_VERSION, entries: {} };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function readSkillCache(home) {
|
|
502
|
+
let parsed;
|
|
503
|
+
try {
|
|
504
|
+
parsed = JSON.parse(fs.readFileSync(skillCachePath(home), "utf8"));
|
|
505
|
+
} catch {
|
|
506
|
+
// Missing or corrupt skill cache: start over, never fail the request.
|
|
507
|
+
return emptySkillCache();
|
|
508
|
+
}
|
|
509
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return emptySkillCache();
|
|
510
|
+
if (parsed.version !== SKILL_CACHE_VERSION) return emptySkillCache();
|
|
511
|
+
if (!parsed.entries || typeof parsed.entries !== "object" || Array.isArray(parsed.entries)) return emptySkillCache();
|
|
512
|
+
const cache = emptySkillCache();
|
|
513
|
+
for (const [name, value] of Object.entries(parsed.entries)) {
|
|
514
|
+
if (typeof name !== "string" || name.trim() === "" || RESERVED_KEYS.has(name)) continue;
|
|
515
|
+
const entry = normalizeSkillEntry(value);
|
|
516
|
+
if (entry) cache.entries[name] = entry;
|
|
517
|
+
}
|
|
518
|
+
return cache;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function writeSkillCache(home, cache) {
|
|
522
|
+
const target = skillCachePath(home);
|
|
523
|
+
const temporary = target + ".tmp-" + process.pid;
|
|
524
|
+
// UTF-8 without BOM: DSH and Node parse these files as raw JSON text.
|
|
525
|
+
const body = JSON.stringify({ version: SKILL_CACHE_VERSION, entries: cache.entries || {} }, null, 2);
|
|
526
|
+
fs.writeFileSync(temporary, body, "utf8");
|
|
527
|
+
try {
|
|
528
|
+
fs.renameSync(temporary, target);
|
|
529
|
+
} catch (error) {
|
|
530
|
+
try {
|
|
531
|
+
fs.unlinkSync(temporary);
|
|
532
|
+
} catch {
|
|
533
|
+
// The temp file is already gone.
|
|
534
|
+
}
|
|
535
|
+
throw error;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
433
539
|
/**
|
|
434
540
|
* Install the experimental official-page display override.
|
|
435
541
|
* Wraps `pluginPackages.metaOf(specifier, parentURL)` with our cached labels,
|
|
@@ -667,6 +773,282 @@ export function installBundleMeta(ctx, home) {
|
|
|
667
773
|
return { dispose: restore, tryPatch, status };
|
|
668
774
|
}
|
|
669
775
|
|
|
776
|
+
/** Read the current skill summaries from the host registry; never throws. */
|
|
777
|
+
async function skillsSnapshot(ctx) {
|
|
778
|
+
const warn = (message) => {
|
|
779
|
+
try {
|
|
780
|
+
ctx.logger?.warn?.("[plugin-labels] " + message);
|
|
781
|
+
} catch {
|
|
782
|
+
// A logger failure must not break the host.
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
try {
|
|
786
|
+
const service = ctx.get("skills");
|
|
787
|
+
if (!service || typeof service.snapshot !== "function") return { available: false, skills: [] };
|
|
788
|
+
const snapshot = await service.snapshot({});
|
|
789
|
+
if (!snapshot || !Array.isArray(snapshot.skills)) return { available: false, skills: [] };
|
|
790
|
+
return { available: true, skills: snapshot.skills };
|
|
791
|
+
} catch (error) {
|
|
792
|
+
warn("skills snapshot failed: " + String((error && error.message) || error));
|
|
793
|
+
return { available: false, skills: [] };
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function truncateChars(value, limit) {
|
|
798
|
+
const characters = Array.from(value);
|
|
799
|
+
return characters.length <= limit ? value : characters.slice(0, limit).join("");
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function buildSkillsUserText(pending) {
|
|
803
|
+
return JSON.stringify(
|
|
804
|
+
{
|
|
805
|
+
instruction: "Translate and explain every skill below. Reply with the strict JSON object only.",
|
|
806
|
+
skills: pending.map((skill) => ({
|
|
807
|
+
name: skill.name,
|
|
808
|
+
description: truncateChars(typeof skill.description === "string" ? skill.description : "", MAX_SKILL_DESCRIPTION),
|
|
809
|
+
...(typeof skill.whenToUse === "string" && skill.whenToUse.trim() !== ""
|
|
810
|
+
? { whenToUse: truncateChars(skill.whenToUse, MAX_SKILL_DESCRIPTION) }
|
|
811
|
+
: {}),
|
|
812
|
+
})),
|
|
813
|
+
},
|
|
814
|
+
null,
|
|
815
|
+
2,
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/** Parse the model's strict-JSON skill answer, keeping only names we asked about. */
|
|
820
|
+
function parseSkillTranslations(text, pending) {
|
|
821
|
+
let body = text.trim();
|
|
822
|
+
const fenced = body.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
823
|
+
if (fenced) body = fenced[1].trim();
|
|
824
|
+
const start = body.indexOf("{");
|
|
825
|
+
const end = body.lastIndexOf("}");
|
|
826
|
+
if (start < 0 || end <= start) throw new Error("model output contains no JSON object");
|
|
827
|
+
const parsed = JSON.parse(body.slice(start, end + 1));
|
|
828
|
+
const items = parsed && Array.isArray(parsed.items) ? parsed.items : [];
|
|
829
|
+
const wanted = new Set(pending.map((skill) => skill.name));
|
|
830
|
+
const out = {};
|
|
831
|
+
for (const item of items) {
|
|
832
|
+
if (!item || typeof item !== "object") continue;
|
|
833
|
+
const name = typeof item.name === "string" ? item.name : "";
|
|
834
|
+
if (!wanted.has(name)) continue;
|
|
835
|
+
const zh = typeof item.zh === "string" ? item.zh.trim() : "";
|
|
836
|
+
if (zh === "") continue;
|
|
837
|
+
const usage = typeof item.usage === "string" ? item.usage.trim() : "";
|
|
838
|
+
out[name] = {
|
|
839
|
+
zh: truncateChars(zh, MAX_SKILL_ZH),
|
|
840
|
+
usage: truncateChars(usage, MAX_SKILL_USAGE),
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
return out;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Translate the skill explanations that are still missing. Never throws: HTTP
|
|
848
|
+
* and the startup auto-run both treat failures as "nothing happened".
|
|
849
|
+
*/
|
|
850
|
+
export async function translateSkillsPending(ctx, home, limit = MAX_PENDING_PER_CALL) {
|
|
851
|
+
const warn = (message) => {
|
|
852
|
+
try {
|
|
853
|
+
ctx.logger?.warn?.("[plugin-labels] " + message);
|
|
854
|
+
} catch {
|
|
855
|
+
// A logger failure must not break the host.
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
if (skillTranslateInFlight) return { added: 0, remaining: 0, total: 0, available: true, busy: true };
|
|
859
|
+
skillTranslateInFlight = true;
|
|
860
|
+
try {
|
|
861
|
+
const { available, skills } = await skillsSnapshot(ctx);
|
|
862
|
+
if (!available) return { added: 0, remaining: 0, total: 0, available: false };
|
|
863
|
+
const cache = readSkillCache(home);
|
|
864
|
+
const pendingAll = skills.filter((skill) => skill && typeof skill.name === "string" && skill.name !== "" && !cache.entries[skill.name]);
|
|
865
|
+
const requested = Math.max(1, Number(limit) || MAX_PENDING_PER_CALL);
|
|
866
|
+
const pending = pendingAll.slice(0, requested);
|
|
867
|
+
const remaining = Math.max(0, pendingAll.length - pending.length);
|
|
868
|
+
const total = pendingAll.length;
|
|
869
|
+
if (pending.length === 0) return { added: 0, remaining, total, available: true };
|
|
870
|
+
const { provider, model } = modelOf(ctx);
|
|
871
|
+
const llm = ctx.get("llm");
|
|
872
|
+
if (!llm || typeof llm.stream !== "function") throw new Error("the llm service is not available in this profile");
|
|
873
|
+
const stream = llm.stream({
|
|
874
|
+
provider,
|
|
875
|
+
model,
|
|
876
|
+
temperature: 0,
|
|
877
|
+
system: SKILL_SYSTEM_PROMPT,
|
|
878
|
+
messages: [{ role: "user", content: [{ type: "text", text: buildSkillsUserText(pending) }] }],
|
|
879
|
+
signal: AbortSignal.timeout(TRANSLATE_TIMEOUT_MS),
|
|
880
|
+
});
|
|
881
|
+
const text = await collectText(stream);
|
|
882
|
+
const translations = parseSkillTranslations(text, pending);
|
|
883
|
+
const at = new Date().toISOString();
|
|
884
|
+
let added = 0;
|
|
885
|
+
for (const [name, value] of Object.entries(translations)) {
|
|
886
|
+
// Never overwrite an existing entry (a manual one always wins).
|
|
887
|
+
if (cache.entries[name]) continue;
|
|
888
|
+
cache.entries[name] = { zh: value.zh, usage: value.usage, source: "ai", at };
|
|
889
|
+
added += 1;
|
|
890
|
+
}
|
|
891
|
+
if (added > 0) writeSkillCache(home, cache);
|
|
892
|
+
return { added, remaining, total, available: true };
|
|
893
|
+
} catch (error) {
|
|
894
|
+
warn("skill translation failed: " + String((error && error.message) || error));
|
|
895
|
+
return { added: 0, remaining: 0, total: 0, available: true, error: String((error && error.message) || error) };
|
|
896
|
+
} finally {
|
|
897
|
+
skillTranslateInFlight = false;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/** Append the Chinese purpose/usage to an English skill description. */
|
|
902
|
+
export function skillDescriptionWithChinese(original, entry) {
|
|
903
|
+
if (typeof original !== "string" || !entry || typeof entry !== "object") return original;
|
|
904
|
+
const zh = typeof entry.zh === "string" ? entry.zh.trim() : "";
|
|
905
|
+
if (zh === "") return original;
|
|
906
|
+
const usage = typeof entry.usage === "string" ? entry.usage.trim() : "";
|
|
907
|
+
return original + " |中文:" + zh + (usage === "" ? "" : ";用法:" + usage);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/** Recover the original English description from an already-injected one. */
|
|
911
|
+
function originalSkillDescription(description, entry) {
|
|
912
|
+
if (typeof description !== "string") return "";
|
|
913
|
+
if (!entry || typeof entry !== "object") return description;
|
|
914
|
+
const zh = typeof entry.zh === "string" ? entry.zh.trim() : "";
|
|
915
|
+
if (zh === "") return description;
|
|
916
|
+
const usage = typeof entry.usage === "string" ? entry.usage.trim() : "";
|
|
917
|
+
const suffix = " |中文:" + zh + (usage === "" ? "" : ";用法:" + usage);
|
|
918
|
+
return description.endsWith(suffix) ? description.slice(0, -suffix.length) : description;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/** Return a shallow copy of one skill summary with the Chinese suffix applied. */
|
|
922
|
+
function skillWithTranslation(skill, entries) {
|
|
923
|
+
if (!skill || typeof skill !== "object") return skill;
|
|
924
|
+
const name = typeof skill.name === "string" ? skill.name : "";
|
|
925
|
+
const entry = name === "" ? undefined : entries[name];
|
|
926
|
+
const description = skillDescriptionWithChinese(skill.description, entry);
|
|
927
|
+
if (description === skill.description) return skill;
|
|
928
|
+
return { ...skill, description };
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/** Map a `snapshot`/`list` result through the skill translations. */
|
|
932
|
+
function mapSkillResult(methodName, value, home) {
|
|
933
|
+
const entries = readSkillCache(home).entries;
|
|
934
|
+
if (methodName === "list") {
|
|
935
|
+
if (!Array.isArray(value)) return value;
|
|
936
|
+
return value.map((skill) => skillWithTranslation(skill, entries));
|
|
937
|
+
}
|
|
938
|
+
if (!value || typeof value !== "object" || !Array.isArray(value.skills)) return value;
|
|
939
|
+
return { ...value, skills: value.skills.map((skill) => skillWithTranslation(skill, entries)) };
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Patch the `skills` service so the `/` skill menu shows the Chinese purpose
|
|
944
|
+
* and usage next to the English description. Marker-based, idempotent,
|
|
945
|
+
* disposable, re-applied after a service swap; every failure keeps the host's
|
|
946
|
+
* own data.
|
|
947
|
+
*/
|
|
948
|
+
export function installSkillTranslations(ctx, home) {
|
|
949
|
+
const warn = (message) => {
|
|
950
|
+
try {
|
|
951
|
+
ctx.logger?.warn?.("[plugin-labels] " + message);
|
|
952
|
+
} catch {
|
|
953
|
+
// A logger failure must not break the host.
|
|
954
|
+
}
|
|
955
|
+
};
|
|
956
|
+
let installed = null; // { service, methods: [{ name, raw, wrapper }] }
|
|
957
|
+
|
|
958
|
+
const currentService = () => {
|
|
959
|
+
try {
|
|
960
|
+
return ctx.get("skills");
|
|
961
|
+
} catch {
|
|
962
|
+
return undefined;
|
|
963
|
+
}
|
|
964
|
+
};
|
|
965
|
+
|
|
966
|
+
const restore = () => {
|
|
967
|
+
const active = installed;
|
|
968
|
+
installed = null;
|
|
969
|
+
if (!active) return;
|
|
970
|
+
for (const method of active.methods) {
|
|
971
|
+
try {
|
|
972
|
+
if (!isSkillTranslationWrapper(active.service[method.name])) continue;
|
|
973
|
+
try {
|
|
974
|
+
delete active.service[method.name];
|
|
975
|
+
} catch {
|
|
976
|
+
// Frozen/exotic service object; fall back to the saved method.
|
|
977
|
+
}
|
|
978
|
+
if (isSkillTranslationWrapper(active.service[method.name])) active.service[method.name] = method.raw;
|
|
979
|
+
} catch (error) {
|
|
980
|
+
warn("failed to restore skills." + method.name + ": " + String((error && error.message) || error));
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
};
|
|
984
|
+
|
|
985
|
+
const tryPatch = () => {
|
|
986
|
+
try {
|
|
987
|
+
const service = currentService();
|
|
988
|
+
if (!service || (typeof service !== "object" && typeof service !== "function")) return;
|
|
989
|
+
const alreadyWrapped = ["snapshot", "list"].filter((name) => isSkillTranslationWrapper(service[name]));
|
|
990
|
+
if (alreadyWrapped.length > 0) {
|
|
991
|
+
if (!installed) {
|
|
992
|
+
installed = {
|
|
993
|
+
service,
|
|
994
|
+
methods: alreadyWrapped.map((name) => ({ name, raw: service[name], wrapper: service[name] })),
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
if (installed) restore();
|
|
1000
|
+
const methods = [];
|
|
1001
|
+
for (const methodName of ["snapshot", "list"]) {
|
|
1002
|
+
const raw = service[methodName];
|
|
1003
|
+
if (typeof raw !== "function") continue;
|
|
1004
|
+
const wrapper = markSkillTranslationWrapper(function (...args) {
|
|
1005
|
+
let result;
|
|
1006
|
+
try {
|
|
1007
|
+
result = raw.apply(service, args);
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
return Promise.reject(error);
|
|
1010
|
+
}
|
|
1011
|
+
// These methods are async today but tolerate a sync result.
|
|
1012
|
+
return Promise.resolve(result).then(
|
|
1013
|
+
(value) => {
|
|
1014
|
+
try {
|
|
1015
|
+
return mapSkillResult(methodName, value, home);
|
|
1016
|
+
} catch {
|
|
1017
|
+
return value;
|
|
1018
|
+
}
|
|
1019
|
+
},
|
|
1020
|
+
(error) => {
|
|
1021
|
+
throw error;
|
|
1022
|
+
},
|
|
1023
|
+
);
|
|
1024
|
+
});
|
|
1025
|
+
try {
|
|
1026
|
+
service[methodName] = wrapper;
|
|
1027
|
+
} catch (error) {
|
|
1028
|
+
warn("could not wrap skills." + methodName + ": " + String((error && error.message) || error));
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
if (!isSkillTranslationWrapper(service[methodName])) {
|
|
1032
|
+
warn("could not wrap skills." + methodName + ": the assignment had no effect");
|
|
1033
|
+
continue;
|
|
1034
|
+
}
|
|
1035
|
+
methods.push({ name: methodName, raw, wrapper });
|
|
1036
|
+
}
|
|
1037
|
+
if (methods.length > 0) installed = { service, methods };
|
|
1038
|
+
} catch (error) {
|
|
1039
|
+
warn("skills translation hook skipped: " + String((error && error.message) || error));
|
|
1040
|
+
}
|
|
1041
|
+
};
|
|
1042
|
+
|
|
1043
|
+
const status = () => ({
|
|
1044
|
+
patched: installed !== null,
|
|
1045
|
+
service: Boolean(currentService()),
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
tryPatch();
|
|
1049
|
+
return { dispose: restore, tryPatch, status };
|
|
1050
|
+
}
|
|
1051
|
+
|
|
670
1052
|
/** Read the configured default route; fall back to the official DeepSeek route. */
|
|
671
1053
|
function modelOf(ctx) {
|
|
672
1054
|
try {
|
|
@@ -767,7 +1149,7 @@ function handleList(home, req, res, officialMetaStatus) {
|
|
|
767
1149
|
}));
|
|
768
1150
|
// Diagnostics for the experimental official-page override; clients ignore
|
|
769
1151
|
// unknown fields, so this stays backward compatible.
|
|
770
|
-
let officialMeta = { enabled: true, patched: false, service: false, bundles: false };
|
|
1152
|
+
let officialMeta = { enabled: true, patched: false, service: false, bundles: false, skills: false };
|
|
771
1153
|
if (typeof officialMetaStatus === "function") {
|
|
772
1154
|
try {
|
|
773
1155
|
officialMeta = officialMetaStatus();
|
|
@@ -888,6 +1270,8 @@ async function handleLabel(home, req, res) {
|
|
|
888
1270
|
* tokens twice and race the atomic cache rename, so refuse it instead.
|
|
889
1271
|
*/
|
|
890
1272
|
let translateInFlight = false;
|
|
1273
|
+
/** Separate single-flight lock for skill translations (HTTP 409 on concurrency). */
|
|
1274
|
+
let skillTranslateInFlight = false;
|
|
891
1275
|
|
|
892
1276
|
/**
|
|
893
1277
|
* One startup pass that fills missing labels. Never throws (a failed auto-run
|
|
@@ -986,6 +1370,90 @@ async function handleImport(home, req, res) {
|
|
|
986
1370
|
if (applied > 0) writeCache(home, cache);
|
|
987
1371
|
json(res, 200, { ok: true, applied, skipped, labels: cache.entries });
|
|
988
1372
|
}
|
|
1373
|
+
/** GET /skills: skill catalogue with the cached Chinese translations. */
|
|
1374
|
+
async function handleSkillsList(ctx, home, req, res) {
|
|
1375
|
+
if (!isLoopback(req)) {
|
|
1376
|
+
json(res, 403, { ok: false, error: "loopback only" });
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
if (!isSameOriginRequest(req) || req.method !== "GET") {
|
|
1380
|
+
json(res, 403, { ok: false, error: "cross-site or non-GET request rejected" });
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
try {
|
|
1384
|
+
const { available, skills } = await skillsSnapshot(ctx);
|
|
1385
|
+
const cache = readSkillCache(home);
|
|
1386
|
+
const items = skills.map((skill) => {
|
|
1387
|
+
const name = typeof skill.name === "string" ? skill.name : "";
|
|
1388
|
+
const entry = name === "" ? null : cache.entries[name] || null;
|
|
1389
|
+
const item = {
|
|
1390
|
+
// The service may already have our suffix injected; report the
|
|
1391
|
+
// original English description here.
|
|
1392
|
+
name,
|
|
1393
|
+
description: originalSkillDescription(skill.description, entry),
|
|
1394
|
+
translation: entry,
|
|
1395
|
+
};
|
|
1396
|
+
if (typeof skill.whenToUse === "string" && skill.whenToUse !== "") item.whenToUse = skill.whenToUse;
|
|
1397
|
+
return item;
|
|
1398
|
+
});
|
|
1399
|
+
json(res, 200, {
|
|
1400
|
+
ok: true,
|
|
1401
|
+
available,
|
|
1402
|
+
count: items.length,
|
|
1403
|
+
translated: items.filter((item) => item.translation !== null).length,
|
|
1404
|
+
pending: items.filter((item) => item.translation === null).length,
|
|
1405
|
+
skills: items,
|
|
1406
|
+
});
|
|
1407
|
+
} catch (error) {
|
|
1408
|
+
respondError(res, error);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
/** POST /skills/translate: fill missing skill explanations (single-flight). */
|
|
1413
|
+
async function handleSkillsTranslate(ctx, home, req, res) {
|
|
1414
|
+
if (!isLoopback(req)) {
|
|
1415
|
+
json(res, 403, { ok: false, error: "loopback only" });
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
if (skillTranslateInFlight) {
|
|
1419
|
+
json(res, 409, { ok: false, error: "a skill translation is already running" });
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
const result = await translateSkillsPending(ctx, home, MAX_PENDING_PER_CALL);
|
|
1423
|
+
json(res, 200, {
|
|
1424
|
+
ok: true,
|
|
1425
|
+
available: result.available,
|
|
1426
|
+
added: result.added,
|
|
1427
|
+
remaining: result.remaining,
|
|
1428
|
+
total: result.total,
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
/** POST /skills/label: set one manual skill translation (source:"manual"). */
|
|
1433
|
+
async function handleSkillsLabel(home, req, res) {
|
|
1434
|
+
if (!isLoopback(req)) {
|
|
1435
|
+
json(res, 403, { ok: false, error: "loopback only" });
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
const payload = await readJsonBody(req);
|
|
1439
|
+
const name = typeof payload.name === "string" ? payload.name.trim() : "";
|
|
1440
|
+
const zh = typeof payload.zh === "string" ? payload.zh.trim() : "";
|
|
1441
|
+
const usage = typeof payload.usage === "string" ? payload.usage.trim() : "";
|
|
1442
|
+
if (name === "") throw badRequest("name is required");
|
|
1443
|
+
if (zh === "") throw badRequest("zh is required");
|
|
1444
|
+
if (Array.from(zh).length > MAX_SKILL_ZH) throw badRequest("zh is too long (max " + MAX_SKILL_ZH + ")");
|
|
1445
|
+
if (Array.from(usage).length > MAX_SKILL_USAGE) throw badRequest("usage is too long (max " + MAX_SKILL_USAGE + ")");
|
|
1446
|
+
const cache = readSkillCache(home);
|
|
1447
|
+
cache.entries[name] = {
|
|
1448
|
+
zh: truncateChars(zh, MAX_SKILL_ZH),
|
|
1449
|
+
usage: truncateChars(usage, MAX_SKILL_USAGE),
|
|
1450
|
+
source: "manual",
|
|
1451
|
+
at: new Date().toISOString(),
|
|
1452
|
+
};
|
|
1453
|
+
writeSkillCache(home, cache);
|
|
1454
|
+
json(res, 200, { ok: true, name });
|
|
1455
|
+
}
|
|
1456
|
+
|
|
989
1457
|
/** Wrap an async handler so a rejection always becomes a JSON answer. */
|
|
990
1458
|
function guarded(res, req, work) {
|
|
991
1459
|
if (!isSameOriginRequest(req)) {
|
|
@@ -1010,9 +1478,11 @@ export function apply(ctx) {
|
|
|
1010
1478
|
// the routes; the service listener re-applies them after a service swap.
|
|
1011
1479
|
const officialMeta = installOfficialMeta(ctx, home);
|
|
1012
1480
|
const bundleMeta = installBundleMeta(ctx, home);
|
|
1481
|
+
const skillTranslations = installSkillTranslations(ctx, home);
|
|
1013
1482
|
ctx.effect(() => () => {
|
|
1014
1483
|
officialMeta.dispose();
|
|
1015
1484
|
bundleMeta.dispose();
|
|
1485
|
+
skillTranslations.dispose();
|
|
1016
1486
|
}, "plugin-labels: restore official plugin metadata wrappers");
|
|
1017
1487
|
ctx.effect(() => {
|
|
1018
1488
|
try {
|
|
@@ -1020,19 +1490,22 @@ export function apply(ctx) {
|
|
|
1020
1490
|
return ctx.on("internal/service", (serviceName) => {
|
|
1021
1491
|
if (serviceName === "pluginPackages") officialMeta.tryPatch();
|
|
1022
1492
|
if (serviceName === "pluginManager") bundleMeta.tryPatch();
|
|
1493
|
+
if (serviceName === "skills") skillTranslations.tryPatch();
|
|
1023
1494
|
});
|
|
1024
1495
|
} catch {
|
|
1025
1496
|
return () => {};
|
|
1026
1497
|
}
|
|
1027
1498
|
}, "plugin-labels: watch the official metadata services");
|
|
1028
|
-
// Auto-translate: one pass a few seconds after startup fills missing
|
|
1029
|
-
//
|
|
1030
|
-
//
|
|
1499
|
+
// Auto-translate: one pass a few seconds after startup fills missing plugin
|
|
1500
|
+
// labels, then missing skill explanations, so a newly installed plugin or
|
|
1501
|
+
// skill gets Chinese text without any UI. Never blocks startup;
|
|
1502
|
+
// DSH_PLUGIN_LABELS_AUTO_TRANSLATE=0 turns it off.
|
|
1031
1503
|
if (autoTranslateEnabled()) {
|
|
1032
1504
|
ctx.effect(() => {
|
|
1033
1505
|
const timer = setTimeout(() => {
|
|
1034
1506
|
Promise.resolve()
|
|
1035
1507
|
.then(() => runAutoTranslate(ctx, home))
|
|
1508
|
+
.then(() => translateSkillsPending(ctx, home, MAX_SKILLS_PER_AUTO))
|
|
1036
1509
|
.catch(() => {});
|
|
1037
1510
|
}, AUTO_TRANSLATE_DELAY_MS);
|
|
1038
1511
|
try {
|
|
@@ -1055,6 +1528,7 @@ export function apply(ctx) {
|
|
|
1055
1528
|
handler: (req, res) => handleList(home, req, res, () => ({
|
|
1056
1529
|
...officialMeta.status(),
|
|
1057
1530
|
bundles: bundleMeta.status().patched,
|
|
1531
|
+
skills: skillTranslations.status().patched,
|
|
1058
1532
|
})),
|
|
1059
1533
|
}), "plugin-labels: installed plugin list route");
|
|
1060
1534
|
ctx.effect(() => ctx.webServer.register({
|
|
@@ -1072,4 +1546,19 @@ export function apply(ctx) {
|
|
|
1072
1546
|
path: IMPORT_PATH,
|
|
1073
1547
|
handler: (req, res) => guarded(res, req, () => handleImport(home, req, res)),
|
|
1074
1548
|
}), "plugin-labels: import labels route");
|
|
1549
|
+
ctx.effect(() => ctx.webServer.register({
|
|
1550
|
+
kind: "exact",
|
|
1551
|
+
path: SKILLS_PATH,
|
|
1552
|
+
handler: (req, res) => handleSkillsList(ctx, home, req, res),
|
|
1553
|
+
}), "plugin-labels: skill list route");
|
|
1554
|
+
ctx.effect(() => ctx.webServer.register({
|
|
1555
|
+
kind: "exact",
|
|
1556
|
+
path: SKILLS_TRANSLATE_PATH,
|
|
1557
|
+
handler: (req, res) => guarded(res, req, () => handleSkillsTranslate(ctx, home, req, res)),
|
|
1558
|
+
}), "plugin-labels: skill translate route");
|
|
1559
|
+
ctx.effect(() => ctx.webServer.register({
|
|
1560
|
+
kind: "exact",
|
|
1561
|
+
path: SKILLS_LABEL_PATH,
|
|
1562
|
+
handler: (req, res) => guarded(res, req, () => handleSkillsLabel(home, req, res)),
|
|
1563
|
+
}), "plugin-labels: skill manual label route");
|
|
1075
1564
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-labels",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Chinese names and one-line explanations for installed DSH plugins; automatic translation and in-place display on the official Plugins page.",
|