dsh-plugin-labels 0.8.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 +55 -20
  2. package/lib/index.js +492 -12
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -3,20 +3,23 @@
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.8.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
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 标签数据可迁移读取。
20
23
 
21
24
  ### 标签格式
22
25
 
@@ -53,12 +56,27 @@ host 启动约 **4 秒**后会自动补一次缺失标签:
53
56
  - 与手动翻译共用同一个单飞锁与 120 秒超时;失败只写日志(warn),不抛错、不阻塞启动。
54
57
  - 手动补翻译:`POST /api/plugin-labels/translate`(见「使用」)。
55
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
+
56
74
  ## 安装
57
75
 
58
76
  ### 方式一:npm 包(推荐,`npm publish` 后)
59
77
 
60
78
  在 DSH 插件页 → 添加插件 → 填包名 `dsh-plugin-labels`;或在创造模式会话里 `plugin_manager` → `install_bundle` → `target: "dsh-plugin-labels"`。发布后 profile 依赖形如
61
- `"dsh-plugin-labels": "dsh-plugin-labels@^0.8.0"`。
79
+ `"dsh-plugin-labels": "dsh-plugin-labels@^0.9.0"`。
62
80
 
63
81
  ### 方式二:从 Git 仓库安装
64
82
 
@@ -74,7 +92,7 @@ host 启动约 **4 秒**后会自动补一次缺失标签:
74
92
 
75
93
  ## 使用
76
94
 
77
- v0.7.0 起没有 UI 入口,全部通过 4 个 loopback HTTP API 操作(也可以让 AI/脚本代改):
95
+ v0.9.0 起没有 UI 入口,全部通过 7 个 loopback HTTP API 操作(也可以让 AI/脚本代改):
78
96
 
79
97
  ```powershell
80
98
  # 查看已装插件 + 标签 + 诊断字段(officialMeta)
@@ -96,9 +114,21 @@ Invoke-RestMethod -Method Post -ContentType 'application/json' `
96
114
  # 导入导出的 JSON(只保留已装插件名,保留 ai/manual 来源)
97
115
  Invoke-RestMethod -Method Post -ContentType 'application/json' -InFile labels.json `
98
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
99
129
  ```
100
130
 
101
- 端口就是当前 DSH Web GUI 的端口;这些路由只接受 loopback 请求且必须是 POST(`/list` 为 GET)。自动翻译会替你补空缺,手改过的条目不会被自动覆盖。
131
+ 端口就是当前 DSH Web GUI 的端口;这些路由只接受 loopback 请求且必须是 POST(`/list`、`/skills` 为 GET)。自动翻译会替你补空缺,手改过的条目不会被自动覆盖。
102
132
 
103
133
  ## 插件速查文档
104
134
 
@@ -116,9 +146,9 @@ npm run docs # 等价于 node scripts/gen-doc.mjs
116
146
 
117
147
  ## 兼容性
118
148
 
119
- - DSH 桌面版 **0.2.1-rc.2**(Web GUI);依赖官方 client 基座模块与 host 服务,只 `require("react")`。
120
- - 本插件不依赖任何 UI slot:显示覆盖包装宿主的 `pluginPackages.metaOf()`(官方页行/详情)与 `pluginManager.listBundles()`(插件包卡片),client 只注册 `locale` 字典。
121
- - 拿不到这些内部服务时静默回退:不显示中文(回退英文),插件其它功能与 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 仍可用。
122
152
 
123
153
  ## 开发说明
124
154
 
@@ -128,10 +158,13 @@ npm run docs # 等价于 node scripts/gen-doc.mjs
128
158
  - `POST /api/plugin-labels/translate`(无参=补空缺;`{names:[...]}`=显式重翻)
129
159
  - `POST /api/plugin-labels/label`(`{name,title,oneLine}` 或 `{name,action:"reset"}`)
130
160
  - `POST /api/plugin-labels/import`
131
- - **自动翻译**:`apply` 里注册一个 4 秒后执行一次的定时器(`unref`),调用 `translatePending` 补空缺;可用 `DSH_PLUGIN_LABELS_AUTO_TRANSLATE=0` 关闭;异常只 warn。
132
- - **显示覆盖**:`installOfficialMeta` / `installBundleMeta` 用 symbol 标记识别 wrapper,幂等、可 dispose、服务替换后重打、失败静默回退。`DSH_PLUGIN_LABELS_OFFICIAL_META=0` 同时关闭两处覆盖。
133
- - **安全围栏**:loopback 校验、`sec-fetch-site` 跨站拒绝、POST-only、翻译单飞锁(并发 409)、`AbortSignal.timeout(120s)`、未预期异常统一 500 不泄漏路径。
134
- - **缓存**:`<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` 原子写,单条非法条目丢弃。
135
168
  - **验证**:`node --check lib/index.js`、`node --check lib/client.js`;host 可用桩 `ctx` 跑冒烟。
136
169
  - 修改 host 后需重启 DSH 生效(本插件已没有需要刷新页面的 UI 改动)。
137
170
 
@@ -148,7 +181,7 @@ dsh-plugin-labels/
148
181
  ├─ package.json # manifest(dsh.bundle 组合 patch、dsh.client 平台与 inject)
149
182
  ├─ cordis.patch.yml # bundle 组合 patch
150
183
  ├─ lib/
151
- │ ├─ index.js # host:HTTP API + 显示覆盖 + 自动翻译 + 标签缓存
184
+ │ ├─ index.js # host:HTTP API + 显示覆盖 + 技能中文 + 自动翻译 + 缓存
152
185
  │ └─ client.js # client:模块骨架,仅注册 locale(v0.7.0 起无 UI)
153
186
  ├─ locale/
154
187
  │ ├─ zh.json # 插件元数据(中文)
@@ -168,10 +201,12 @@ MIT,见 [LICENSE](LICENSE)。Copyright (c) 2026 qyy2005
168
201
 
169
202
  ## English summary
170
203
 
171
- `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.
172
207
 
173
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.
174
209
 
175
- 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.
176
211
 
177
- 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.8.0: labels explain how to use a plugin).
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:
@@ -21,19 +23,28 @@
21
23
  * and a concrete first step when useful. The model is told never to invent
22
24
  * menus: when the entry point cannot be inferred it must write 入口以界面为准.
23
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
+ *
24
34
  * Display overrides (all failure-safe; marker-detected because Cordis hands out
25
35
  * traceable Proxies whose method references differ on every read):
26
36
  * - `installOfficialMeta` wraps `pluginPackages.metaOf()` so the official
27
37
  * Plugins page rows and detail pages show the Chinese labels in place.
28
38
  * - `installBundleMeta` wraps `pluginManager.listBundles()` so the plugin
29
39
  * package cards (which call `readPluginMeta` directly) show them too.
30
- * 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.
31
42
  *
32
43
  * Auto translation: shortly after startup one pass fills up to
33
- * MAX_PENDING_PER_CALL missing labels through the same core, so a newly
34
- * installed plugin gets a Chinese name without any UI. Set
35
- * DSH_PLUGIN_LABELS_AUTO_TRANSLATE=0 to disable it. It never throws into or
36
- * 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.
37
48
  *
38
49
  * Failure policy: every handler catches its own errors and answers with JSON;
39
50
  * nothing is allowed to escape as an unhandled rejection (that is fatal to the
@@ -51,6 +62,9 @@ export const LIST_PATH = "/api/plugin-labels/list";
51
62
  export const TRANSLATE_PATH = "/api/plugin-labels/translate";
52
63
  export const LABEL_PATH = "/api/plugin-labels/label";
53
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";
54
68
 
55
69
  const CACHE_VERSION = 2;
56
70
  const LEGACY_CACHE_VERSION = 1;
@@ -60,6 +74,12 @@ const MAX_ONE_LINE = 80;
60
74
  const MAX_BODY_BYTES = 65536;
61
75
  const TRANSLATE_TIMEOUT_MS = 120000;
62
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
+
63
83
  const SYSTEM_PROMPT = [
64
84
  "You label DeepSeek Harness (DSH) plugins for a Chinese-speaking user.",
65
85
  "For every plugin you receive, produce:",
@@ -73,6 +93,15 @@ const SYSTEM_PROMPT = [
73
93
  '{"items":[{"name":"<exact package name>","title":"...","oneLine":"..."}]}',
74
94
  ].join("\n");
75
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
+
76
105
  /**
77
106
  * Experimental display-layer override: wrap the host `pluginPackages.metaOf`
78
107
  * service so the official Plugins page shows our Chinese labels in place
@@ -101,9 +130,11 @@ function displayOverrideEnabled() {
101
130
  */
102
131
  const OFFICIAL_META_MARK = Symbol.for("dsh-plugin-labels.official-meta");
103
132
  const BUNDLE_META_MARK = Symbol.for("dsh-plugin-labels.bundle-meta");
133
+ const SKILL_TRANSLATIONS_MARK = Symbol.for("dsh-plugin-labels.skill-translations");
104
134
 
105
135
  const isOfficialMetaWrapper = (value) => Boolean(value && typeof value === "function" && value[OFFICIAL_META_MARK] === true);
106
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);
107
138
 
108
139
  function markWithSymbol(wrapper, symbol) {
109
140
  try {
@@ -127,6 +158,10 @@ function markBundleMetaWrapper(wrapper) {
127
158
  return markWithSymbol(wrapper, BUNDLE_META_MARK);
128
159
  }
129
160
 
161
+ function markSkillTranslationWrapper(wrapper) {
162
+ return markWithSymbol(wrapper, SKILL_TRANSLATIONS_MARK);
163
+ }
164
+
130
165
  /** Split a configured specifier into its bare package name (`@scope/name/sub` -> `@scope/name`). */
131
166
  export function bareNameOf(specifier) {
132
167
  if (typeof specifier !== "string") return "";
@@ -439,6 +474,68 @@ function writeCache(home, cache) {
439
474
  }
440
475
  }
441
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
+
442
539
  /**
443
540
  * Install the experimental official-page display override.
444
541
  * Wraps `pluginPackages.metaOf(specifier, parentURL)` with our cached labels,
@@ -676,6 +773,282 @@ export function installBundleMeta(ctx, home) {
676
773
  return { dispose: restore, tryPatch, status };
677
774
  }
678
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
+
679
1052
  /** Read the configured default route; fall back to the official DeepSeek route. */
680
1053
  function modelOf(ctx) {
681
1054
  try {
@@ -776,7 +1149,7 @@ function handleList(home, req, res, officialMetaStatus) {
776
1149
  }));
777
1150
  // Diagnostics for the experimental official-page override; clients ignore
778
1151
  // unknown fields, so this stays backward compatible.
779
- let officialMeta = { enabled: true, patched: false, service: false, bundles: false };
1152
+ let officialMeta = { enabled: true, patched: false, service: false, bundles: false, skills: false };
780
1153
  if (typeof officialMetaStatus === "function") {
781
1154
  try {
782
1155
  officialMeta = officialMetaStatus();
@@ -897,6 +1270,8 @@ async function handleLabel(home, req, res) {
897
1270
  * tokens twice and race the atomic cache rename, so refuse it instead.
898
1271
  */
899
1272
  let translateInFlight = false;
1273
+ /** Separate single-flight lock for skill translations (HTTP 409 on concurrency). */
1274
+ let skillTranslateInFlight = false;
900
1275
 
901
1276
  /**
902
1277
  * One startup pass that fills missing labels. Never throws (a failed auto-run
@@ -995,6 +1370,90 @@ async function handleImport(home, req, res) {
995
1370
  if (applied > 0) writeCache(home, cache);
996
1371
  json(res, 200, { ok: true, applied, skipped, labels: cache.entries });
997
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
+
998
1457
  /** Wrap an async handler so a rejection always becomes a JSON answer. */
999
1458
  function guarded(res, req, work) {
1000
1459
  if (!isSameOriginRequest(req)) {
@@ -1019,9 +1478,11 @@ export function apply(ctx) {
1019
1478
  // the routes; the service listener re-applies them after a service swap.
1020
1479
  const officialMeta = installOfficialMeta(ctx, home);
1021
1480
  const bundleMeta = installBundleMeta(ctx, home);
1481
+ const skillTranslations = installSkillTranslations(ctx, home);
1022
1482
  ctx.effect(() => () => {
1023
1483
  officialMeta.dispose();
1024
1484
  bundleMeta.dispose();
1485
+ skillTranslations.dispose();
1025
1486
  }, "plugin-labels: restore official plugin metadata wrappers");
1026
1487
  ctx.effect(() => {
1027
1488
  try {
@@ -1029,19 +1490,22 @@ export function apply(ctx) {
1029
1490
  return ctx.on("internal/service", (serviceName) => {
1030
1491
  if (serviceName === "pluginPackages") officialMeta.tryPatch();
1031
1492
  if (serviceName === "pluginManager") bundleMeta.tryPatch();
1493
+ if (serviceName === "skills") skillTranslations.tryPatch();
1032
1494
  });
1033
1495
  } catch {
1034
1496
  return () => {};
1035
1497
  }
1036
1498
  }, "plugin-labels: watch the official metadata services");
1037
- // Auto-translate: one pass a few seconds after startup fills missing labels,
1038
- // so a newly installed plugin gets a Chinese name without any UI. Never
1039
- // 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.
1040
1503
  if (autoTranslateEnabled()) {
1041
1504
  ctx.effect(() => {
1042
1505
  const timer = setTimeout(() => {
1043
1506
  Promise.resolve()
1044
1507
  .then(() => runAutoTranslate(ctx, home))
1508
+ .then(() => translateSkillsPending(ctx, home, MAX_SKILLS_PER_AUTO))
1045
1509
  .catch(() => {});
1046
1510
  }, AUTO_TRANSLATE_DELAY_MS);
1047
1511
  try {
@@ -1064,6 +1528,7 @@ export function apply(ctx) {
1064
1528
  handler: (req, res) => handleList(home, req, res, () => ({
1065
1529
  ...officialMeta.status(),
1066
1530
  bundles: bundleMeta.status().patched,
1531
+ skills: skillTranslations.status().patched,
1067
1532
  })),
1068
1533
  }), "plugin-labels: installed plugin list route");
1069
1534
  ctx.effect(() => ctx.webServer.register({
@@ -1081,4 +1546,19 @@ export function apply(ctx) {
1081
1546
  path: IMPORT_PATH,
1082
1547
  handler: (req, res) => guarded(res, req, () => handleImport(home, req, res)),
1083
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");
1084
1564
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-labels",
3
- "version": "0.8.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.",