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.
Files changed (3) hide show
  1. package/README.md +64 -21
  2. package/lib/index.js +505 -16
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -3,20 +3,29 @@
3
3
  [![CI](https://github.com/qyy2005/dsh-plugin-labels/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/qyy2005/dsh-plugin-labels/actions/workflows/ci.yml)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
5
5
 
6
- 给 DSH(DeepSeek Harness)**已安装插件**配「中文名 + 一句话说明」:官方「插件」页的已安装插件行、插件包卡片与详情页都会**原地显示中文名与中文描述**(由 host 覆盖显示元数据);新装插件会在启动后自动翻译。**本插件没有任何 UI 入口**(没有 tab、没有设置项),管理通过 loopback HTTP API 或让 AI 代改。
6
+ > 版本更新记录见 [CHANGELOG.md](CHANGELOG.md),或在 GitHub [Releases](https://github.com/qyy2005/dsh-plugin-labels/releases) 查看。
7
7
 
8
- - 当前版本:**0.7.0**(本仓库即源码目录;通过 `plugin_manager install_bundle` 从 GitHub 或 npm 安装到 DSH profile)
9
- - 只支持 DSH 桌面版 Web GUI(开发验证版本 0.2.1-rc.2)
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
- - **中文名 + 一句话说明**:每个插件一条中文名(≤8 字)和一句中文介绍(≤40 字)。
15
+ - **中文名 + 一句话说明**:每个插件一条中文名(≤10 字)和一句说明(≤80 字);说明必须写清**怎么用**,不只是“是什么”(见下方「标签格式」)。
14
16
  - **官方页原地中文**:官方「插件」页的已安装插件行、点开的详情页,以及插件包卡片(`listBundles`)都会直接显示中文名与中文描述;未翻译的插件回退英文。默认开启。
15
- - **自动翻译新插件**:host 启动约 4 秒后自动补一次缺失标签(每次最多 20 条,只补空缺、不覆盖手改),不用点任何按钮;可用 `DSH_PLUGIN_LABELS_AUTO_TRANSLATE=0` 关闭。
17
+ - **技能中文翻译**:`/` 技能菜单里每条技能追加「|中文:作用;用法:怎么用」,看懂技能是做什么、什么时候用(见「技能中文翻译」)。
18
+ - **自动翻译新插件与新技能**:host 启动约 4 秒后自动补一次缺失标签(每次最多 20 条),再补最多 10 条技能说明;只补空缺、不覆盖手改。
16
19
  - **手改优先**:手动设置的标签不会被自动翻译覆盖;显式“重翻”才会覆盖。
17
- - **无 UI 管理入口**:通过 4 个 loopback HTTP API(见「使用」)或让 AI/脚本直接调用它们来翻译、手改、重翻、导入导出。
20
+ - **无 UI 管理入口**:通过 7 个 loopback HTTP API(见「使用」)或让 AI/脚本直接调用它们来翻译、手改、重翻、导入导出。
18
21
  - **导出 / 导入 API**:导出带 `source`(`ai` / `manual`)的 JSON,导入时保留来源,只接受已安装插件名(≤64KB)。
19
- - **缓存 v2**:标签缓存在 `<DSH_HOME>/plugin-labels.json`,UTF-8 无 BOM、原子写,旧 v1 数据可迁移读取。
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.7.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.7.0 起没有 UI 入口,全部通过 4 个 loopback HTTP API 操作(也可以让 AI/脚本代改):
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);依赖官方 client 基座模块与 host 服务,只 `require("react")`。
114
- - 本插件不依赖任何 UI slot:显示覆盖包装宿主的 `pluginPackages.metaOf()`(官方页行/详情)与 `pluginManager.listBundles()`(插件包卡片),client 只注册 `locale` 字典。
115
- - 拿不到这些内部服务时静默回退:不显示中文(回退英文),插件其它功能与 API 仍可用。
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
- - **自动翻译**:`apply` 里注册一个 4 秒后执行一次的定时器(`unref`),调用 `translatePending` 补空缺;可用 `DSH_PLUGIN_LABELS_AUTO_TRANSLATE=0` 关闭;异常只 warn。
126
- - **显示覆盖**:`installOfficialMeta` / `installBundleMeta` 用 symbol 标记识别 wrapper,幂等、可 dispose、服务替换后重打、失败静默回退。`DSH_PLUGIN_LABELS_OFFICIAL_META=0` 同时关闭两处覆盖。
127
- - **安全围栏**:loopback 校验、`sec-fetch-site` 跨站拒绝、POST-only、翻译单飞锁(并发 409)、`AbortSignal.timeout(120s)`、未预期异常统一 500 不泄漏路径。
128
- - **缓存**:`<DSH_HOME>/plugin-labels.json`,UTF-8 无 BOM,临时文件 + `rename` 原子写;缓存 `version: 2`,旧 v1 数据可迁移读取,单条非法条目丢弃。
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 four 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.
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. Both overrides require one DSH restart after installation.
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 four loopback-only HTTP routes plus the display overrides and the startup auto-translate. Verified on DSH desktop 0.2.1-rc.2. MIT licensed.
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.7.0: display overrides, no plugin UI).
2
+ * dsh-plugin-labels — host half (v0.9.0: plugin labels + skill translations).
3
3
  *
4
- * Four loopback-only endpoints (kept for scripts/AI even though the client no
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
- * Set DSH_PLUGIN_LABELS_OFFICIAL_META=0 to disable both display overrides.
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, so a newly
28
- * installed plugin gets a Chinese name without any UI. Set
29
- * DSH_PLUGIN_LABELS_AUTO_TRANSLATE=0 to disable it. It never throws into or
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 = 120;
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, at most 8 Chinese characters, naming what it does (not a literal translation of the package name).",
61
- "2. oneLine: one Chinese sentence, at most 40 characters, telling the user what the plugin does and whether it matters to them.",
62
- "Be concrete. Avoid marketing words. If the description is unclear, say what the package name plus description imply.",
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 labels,
1029
- // so a newly installed plugin gets a Chinese name without any UI. Never
1030
- // blocks startup; DSH_PLUGIN_LABELS_AUTO_TRANSLATE=0 turns it off.
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.7.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.",