@michengai/dsh-skills-manager 0.1.5 → 0.1.7

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/lib/client.js CHANGED
@@ -9,6 +9,178 @@ window.__ModuleLoader__.load({
9
9
  var pickDirectory = null;
10
10
  var MUTATION_HEADERS = { "content-type": "application/json", "x-dsh-skills-manager": "1" };
11
11
 
12
+ // 词典命名空间:注册进 DSH locale 服务,渲染机制按该命名空间把 t 注入组件 props。
13
+ var NS = "skills-manager";
14
+ // 双语词典:zh 与现有硬编码文案逐字一致,保证中文用户无感知;en 为通顺英文。
15
+ var DICT = {
16
+ zh: {
17
+ "title": "技能",
18
+ "desc": "DSH 技能可上传、启停和删除;公共 Agent 技能仅供查看。",
19
+ "btn.refresh": "刷新",
20
+ "btn.upload": "上传插件",
21
+ "btn.disable": "停用",
22
+ "btn.enable": "启用",
23
+ "btn.repair.enable": "修复并启用",
24
+ "btn.delete": "删除",
25
+ "btn.cancel": "取消",
26
+ "btn.overwrite.upload": "覆盖上传",
27
+ "btn.delete.confirm": "确认删除",
28
+ "btn.file.pick": "选择本地文件",
29
+ "btn.dir.pick": "选择插件文件夹",
30
+ "btn.uploading": "上传中…",
31
+ "btn.upload.dsh": "上传到 DSH",
32
+ "status.enabled": "已启用",
33
+ "status.disabled": "已停用",
34
+ "status.invalid": "配置异常",
35
+ "status.bundle": "目录插件",
36
+ "status.single": "单文件",
37
+ "status.selected": "已选择",
38
+ "summary.total.one": "个插件",
39
+ "summary.total.other": "个插件",
40
+ "summary.enabled.one": "个已启用",
41
+ "summary.enabled.other": "个已启用",
42
+ "summary.group": "{count} 个",
43
+ "note.missing": "未提供简介",
44
+ "empty.dir.uncreated": "目录尚未创建,上传第一个插件后会自动创建。",
45
+ "empty.dir.missing": "公共目录不存在。",
46
+ "empty.skills.none": "暂未安装插件。",
47
+ "loading": "加载中…",
48
+ "error.action": "操作失败:{error}",
49
+ // 列表连接符由词典侧决定:zh 用顿号/分号,en 用逗号/分号。
50
+ "sep.names": "、",
51
+ "sep.errors": ";",
52
+ "result.failed": "上传失败:{error}",
53
+ "result.partial": "部分上传成功:{names};失败:{errors}",
54
+ "result.failed.only": "上传失败:{errors}",
55
+ "result.done": "上传完成:{names}",
56
+ "result.skipped": "未导入:同名插件已跳过:{names}",
57
+ "result.none": "未导入任何插件。",
58
+ "upload.title": "上传插件",
59
+ "upload.close": "关闭上传弹窗",
60
+ "upload.drop.title": "拖放 SKILL.md 文件到这里",
61
+ "upload.drop.copy": "或点击打开系统原生文件选择窗口",
62
+ "upload.hint": "请选择插件目录中的 SKILL.md;目录内其他文件会一并上传。",
63
+ "upload.picking.dir": "正在打开系统原生目录选择窗口…",
64
+ "select.file.invalid": "请选择插件目录中的 SKILL.md 文件。",
65
+ "select.path.missing": "已选择 SKILL.md,但当前运行环境无法读取文件路径。请点击“选择插件文件夹”继续上传。",
66
+ "select.failed": "选择失败:{error}",
67
+ "dir.selected": "已选择插件目录",
68
+ "dir.service.missing": "当前运行环境无法读取本地文件路径,也未提供原生目录选择服务。",
69
+ "dir.pick.failed": "选择插件目录失败:{error}",
70
+ "confirm.overwrite.title": "发现同名插件",
71
+ "confirm.overwrite.desc": "将覆盖现有插件:{names}",
72
+ "confirm.delete.title": "删除插件?",
73
+ "confirm.delete.desc": "“{name}”将从 DSH 技能目录中永久删除,无法恢复。",
74
+ // Step 2 业务错误码:zh 值保持与 core 原有中文文案逐字一致({action} 由 action.* 映射后替换)。
75
+ "error.root.readonly": "公共 Agent 技能目录不允许{action}",
76
+ "error.skill.notFound": "技能不存在: {name}",
77
+ "error.skill.noFrontmatter": "技能缺少完整 frontmatter,无法{action}: {name}",
78
+ "error.source.notFound": "路径不存在: {path}",
79
+ "error.source.symlink": "不支持包含符号链接的 skill 来源: {path}",
80
+ "error.source.unrecognized": "无法识别的 skill 来源: {path}",
81
+ "error.source.tooDeep": "skill 来源目录层级超过 {depth} 层: {path}",
82
+ "error.import.overlap": "导入来源不能与 DSH 技能目录相同、包含或位于其中",
83
+ "error.import.emptySource": "目录下未找到任何 skill 条目(需含 SKILL.md 的子目录或 .md 文件): {path}",
84
+ "error.import.invalidName": "无法生成合法 kebab-case 名称(原始名: {name})",
85
+ "error.import.duplicateName": "批量来源中存在多个同名插件: {name}",
86
+ "error.import.failed": "导入失败",
87
+ "error.proto.forbidden": "禁止的修改请求(缺少客户端标记)",
88
+ "error.proto.contentType": "请求体必须是 application/json",
89
+ "error.proto.method": "不支持的请求方法",
90
+ "error.proto.unknownAction": "未知操作",
91
+ "error.proto.bodyTooLarge": "请求体过大",
92
+ "error.proto.invalidJson": "请求体不是合法 JSON",
93
+ "action.enable": "启用",
94
+ "action.disable": "停用",
95
+ "action.delete": "删除",
96
+ "action.toggle": "启用或停用",
97
+ "root.dsh": "DSH 技能",
98
+ "root.agents": "公共 Agent 技能"
99
+ },
100
+ en: {
101
+ "title": "Skills",
102
+ "desc": "DSH skills can be uploaded, enabled or disabled, and deleted; shared Agent skills are view-only.",
103
+ "btn.refresh": "Refresh",
104
+ "btn.upload": "Upload",
105
+ "btn.disable": "Disable",
106
+ "btn.enable": "Enable",
107
+ "btn.repair.enable": "Repair & enable",
108
+ "btn.delete": "Delete",
109
+ "btn.cancel": "Cancel",
110
+ "btn.overwrite.upload": "Overwrite & upload",
111
+ "btn.delete.confirm": "Delete",
112
+ "btn.file.pick": "Choose local file",
113
+ "btn.dir.pick": "Choose plugin folder",
114
+ "btn.uploading": "Uploading…",
115
+ "btn.upload.dsh": "Upload to DSH",
116
+ "status.enabled": "Enabled",
117
+ "status.disabled": "Disabled",
118
+ "status.invalid": "Invalid config",
119
+ "status.bundle": "Bundle",
120
+ "status.single": "Single file",
121
+ "status.selected": "Selected",
122
+ "summary.total.one": "skill",
123
+ "summary.total.other": "skills",
124
+ "summary.enabled.one": "enabled",
125
+ "summary.enabled.other": "enabled",
126
+ "summary.group": "{count} items",
127
+ "note.missing": "No description provided",
128
+ "empty.dir.uncreated": "The directory has not been created yet; it will be created automatically after the first upload.",
129
+ "empty.dir.missing": "The public directory does not exist.",
130
+ "empty.skills.none": "No plugins installed yet.",
131
+ "loading": "Loading…",
132
+ "error.action": "Action failed: {error}",
133
+ "sep.names": ", ",
134
+ "sep.errors": "; ",
135
+ "result.failed": "Upload failed: {error}",
136
+ "result.partial": "Partially uploaded: {names}; failed: {errors}",
137
+ "result.failed.only": "Upload failed: {errors}",
138
+ "result.done": "Upload complete: {names}",
139
+ "result.skipped": "Not imported: same-name plugins were skipped: {names}",
140
+ "result.none": "Nothing was imported.",
141
+ "upload.title": "Upload plugin",
142
+ "upload.close": "Close upload dialog",
143
+ "upload.drop.title": "Drop SKILL.md file here",
144
+ "upload.drop.copy": "or click to open the native file picker",
145
+ "upload.hint": "Choose a SKILL.md inside the plugin directory; other files in that directory are uploaded too.",
146
+ "upload.picking.dir": "Opening native directory picker…",
147
+ "select.file.invalid": "Please choose the SKILL.md file inside the plugin directory.",
148
+ "select.path.missing": "SKILL.md was selected, but this environment cannot read the file path. Click “Choose plugin folder” to continue uploading.",
149
+ "select.failed": "Selection failed: {error}",
150
+ "dir.selected": "Plugin folder selected",
151
+ "dir.service.missing": "This environment cannot read local file paths and provides no native directory picker.",
152
+ "dir.pick.failed": "Failed to choose plugin folder: {error}",
153
+ "confirm.overwrite.title": "Same-name plugin found",
154
+ "confirm.overwrite.desc": "Will overwrite existing plugin(s): {names}",
155
+ "confirm.delete.title": "Delete plugin?",
156
+ "confirm.delete.desc": "“{name}” will be permanently deleted from the DSH skills directory and cannot be recovered.",
157
+ "error.root.readonly": "The shared Agent skills directory does not allow {action}",
158
+ "error.skill.notFound": "Skill not found: {name}",
159
+ "error.skill.noFrontmatter": "Skill lacks complete frontmatter, cannot {action}: {name}",
160
+ "error.source.notFound": "Path does not exist: {path}",
161
+ "error.source.symlink": "Skill source containing symbolic links is not supported: {path}",
162
+ "error.source.unrecognized": "Unrecognized skill source: {path}",
163
+ "error.source.tooDeep": "Skill source directory depth exceeds the {depth}-level limit: {path}",
164
+ "error.import.overlap": "Import source cannot be the same as, contain, or be inside the DSH skills directory",
165
+ "error.import.emptySource": "No skill entries found in the directory (needs SKILL.md subdirectories or .md files): {path}",
166
+ "error.import.invalidName": "Cannot generate a valid kebab-case name (original name: {name})",
167
+ "error.import.duplicateName": "Batch source contains multiple plugins with the same name: {name}",
168
+ "error.import.failed": "Import failed",
169
+ "error.proto.forbidden": "Forbidden mutation request (missing client marker)",
170
+ "error.proto.contentType": "Content type must be application/json",
171
+ "error.proto.method": "Method not allowed",
172
+ "error.proto.unknownAction": "Unknown action",
173
+ "error.proto.bodyTooLarge": "Request body too large",
174
+ "error.proto.invalidJson": "Invalid JSON request body",
175
+ "action.enable": "enable",
176
+ "action.disable": "disable",
177
+ "action.delete": "deleting",
178
+ "action.toggle": "enabling or disabling",
179
+ "root.dsh": "DSH skills",
180
+ "root.agents": "Shared Agent skills"
181
+ }
182
+ };
183
+
12
184
  var CSS = `
13
185
  .dssm-section{display:flex;min-width:0;flex-direction:column;gap:20px;color:var(--dsw-alias-label-primary)}
14
186
  .dssm-toolbar{display:flex;align-items:flex-start;gap:16px;padding-bottom:16px;border-bottom:1px solid var(--dsw-alias-border-l2)}
@@ -27,13 +199,51 @@ window.__ModuleLoader__.load({
27
199
  function callApi(path, options) {
28
200
  return fetch("/api/dsh-skills-manager" + path, options).then(function (response) {
29
201
  return response.json().then(function (payload) {
30
- if (!payload.ok) throw new Error(payload.error || ("HTTP " + response.status));
202
+ if (!payload.ok) {
203
+ var error = new Error(payload.error || ("HTTP " + response.status));
204
+ // 透传业务错误码与失败明细,供 translateError 按词典翻译。
205
+ if (payload.code) error.code = payload.code;
206
+ if (payload.params) error.params = payload.params;
207
+ if (payload.failed) error.failed = payload.failed;
208
+ throw error;
209
+ }
31
210
  return payload.data;
32
211
  });
33
212
  });
34
213
  }
35
214
 
36
- function SkillManagerSection() {
215
+ // 翻译业务错误:payload.code 存在则查词典(params 替换占位符),词典 miss 回退原文。
216
+ // 兼容 { code, params, error }、Error 对象、纯字符串三种形态。
217
+ function translateError(t, payload) {
218
+ if (payload && typeof payload === "object") {
219
+ var code = payload.code;
220
+ if (typeof code === "string" && code !== "") {
221
+ var params = payload.params || {};
222
+ // {action} 占位符按当前语言映射(enable/disable/delete → 启用/停用/删除)。
223
+ if (params.action !== undefined) {
224
+ var actionKey = "action." + params.action;
225
+ var actionText = t(actionKey);
226
+ if (actionText !== undefined && actionText !== actionKey) params = Object.assign({}, params, { action: actionText });
227
+ }
228
+ var translated = t(code, params);
229
+ // locale miss 时 t 返回 key 本身(或 undefined/null),此时回退原文。
230
+ if (typeof translated === "string" && translated !== code) return translated;
231
+ }
232
+ if (payload.error !== undefined) return translateError(t, payload.error);
233
+ if (payload.message !== undefined) return String(payload.message);
234
+ }
235
+ return String(payload == null ? "" : payload);
236
+ }
237
+
238
+ // 词典命中返回翻译,miss(t 返回 key 本身或 undefined/null)回退 fallback。
239
+ function translateOrFallback(t, key, fallback) {
240
+ var value = t(key);
241
+ return typeof value === "string" && value !== key ? value : fallback;
242
+ }
243
+
244
+ // 渲染机制按 register 的 locale 字段把命名空间绑定的 t 注入组件 props。
245
+ function SkillManagerSection(props) {
246
+ var t = props.t;
37
247
  var snapshotState = react.useState({ loading: true, error: null, data: null });
38
248
  var snapshot = snapshotState[0];
39
249
  var setSnapshot = snapshotState[1];
@@ -62,7 +272,7 @@ window.__ModuleLoader__.load({
62
272
  callApi("/state").then(function (data) {
63
273
  setSnapshot({ loading: false, error: null, data: data });
64
274
  }).catch(function (error) {
65
- setSnapshot({ loading: false, error: String(error && error.message || error), data: snapshot.data });
275
+ setSnapshot({ loading: false, error: error, data: snapshot.data });
66
276
  });
67
277
  }
68
278
 
@@ -91,19 +301,19 @@ window.__ModuleLoader__.load({
91
301
  refresh();
92
302
  }).catch(function (error) {
93
303
  setBusy(null);
94
- setSnapshot({ loading: false, error: String(error && error.message || error), data: snapshot.data });
304
+ setSnapshot({ loading: false, error: error, data: snapshot.data });
95
305
  });
96
306
  }
97
307
 
98
308
  function selectFile(file) {
99
309
  if (!file) return;
100
310
  if (file.name.toLowerCase() !== "skill.md") {
101
- setImportResult({ error: "请选择插件目录中的 SKILL.md 文件。" });
311
+ setImportResult({ code: "select.file.invalid" });
102
312
  return;
103
313
  }
104
314
  if (!file.path) {
105
315
  setSelected(null);
106
- setImportResult({ error: "已选择 SKILL.md,但当前运行环境无法读取文件路径。请点击“选择插件文件夹”继续上传。" });
316
+ setImportResult({ code: "select.path.missing" });
107
317
  return;
108
318
  }
109
319
  setSelected({ name: file.name, source: file.path });
@@ -112,7 +322,7 @@ window.__ModuleLoader__.load({
112
322
 
113
323
  function selectDirectory() {
114
324
  if (!pickDirectory) {
115
- setImportResult({ error: "当前运行环境无法读取本地文件路径,也未提供原生目录选择服务。" });
325
+ setImportResult({ code: "dir.service.missing" });
116
326
  return;
117
327
  }
118
328
  setImportResult(null);
@@ -120,12 +330,12 @@ window.__ModuleLoader__.load({
120
330
  pickDirectory().then(function (path) {
121
331
  setBusy(null);
122
332
  if (path) {
123
- setSelected({ name: "已选择插件目录", source: path });
333
+ setSelected({ nameKey: "dir.selected", source: path });
124
334
  setImportResult(null);
125
335
  }
126
336
  }).catch(function (error) {
127
337
  setBusy(null);
128
- setImportResult({ error: "选择插件目录失败:" + String(error && error.message || error) });
338
+ setImportResult({ code: "dir.pick.failed", params: { error: String(error && error.message || error) } });
129
339
  });
130
340
  }
131
341
 
@@ -144,7 +354,7 @@ window.__ModuleLoader__.load({
144
354
  refresh();
145
355
  }).catch(function (error) {
146
356
  setBusy(null);
147
- setImportResult({ error: String(error && error.message || error) });
357
+ setImportResult({ error: error });
148
358
  });
149
359
  }
150
360
 
@@ -160,7 +370,7 @@ window.__ModuleLoader__.load({
160
370
  executeImport(selected.source, "skip");
161
371
  }).catch(function (error) {
162
372
  setBusy(null);
163
- setImportResult({ error: String(error && error.message || error) });
373
+ setImportResult({ error: error });
164
374
  });
165
375
  }
166
376
 
@@ -172,70 +382,94 @@ window.__ModuleLoader__.load({
172
382
  h("div", { className: "dssm-row-main" },
173
383
  h("div", { className: "dssm-row-id" },
174
384
  h("span", { className: "dssm-row-name" }, skill.name),
175
- h("span", { className: "dssm-tag" }, skill.kind === "bundle" ? "目录插件" : "单文件"),
176
- h("span", { className: "dssm-tag " + (policyValid && enabled ? "dssm-tag-on" : "dssm-tag-off") }, policyValid ? (enabled ? "已启用" : "已停用") : "配置异常")),
177
- h("div", { className: "dssm-note", title: skill.description || "" }, skill.description || "未提供简介")),
178
- root.mutable ? h("button", { className: "dssm-btn dssm-btn-secondary", disabled: !!busy, onClick: function () { action(actionEnabled ? "/disable" : "/enable", { name: skill.name, root: root.key }); } }, actionEnabled ? "停用" : policyValid ? "启用" : "修复并启用") : null,
179
- root.mutable ? h("button", { className: "dssm-btn dssm-btn-danger", disabled: !!busy, onClick: function () { setConfirmDelete(skill); } }, "删除") : null);
385
+ h("span", { className: "dssm-tag" }, skill.kind === "bundle" ? t("status.bundle") : t("status.single")),
386
+ h("span", { className: "dssm-tag " + (policyValid && enabled ? "dssm-tag-on" : "dssm-tag-off") }, policyValid ? (enabled ? t("status.enabled") : t("status.disabled")) : t("status.invalid"))),
387
+ h("div", { className: "dssm-note", title: skill.description || "" }, skill.description || t("note.missing"))),
388
+ root.mutable ? h("button", { className: "dssm-btn dssm-btn-secondary", disabled: !!busy, onClick: function () { action(actionEnabled ? "/disable" : "/enable", { name: skill.name, root: root.key }); } }, actionEnabled ? t("btn.disable") : policyValid ? t("btn.enable") : t("btn.repair.enable")) : null,
389
+ root.mutable ? h("button", { className: "dssm-btn dssm-btn-danger", disabled: !!busy, onClick: function () { setConfirmDelete(skill); } }, t("btn.delete")) : null);
180
390
  }
181
391
 
182
392
  var nodes = [h("style", { key: "css" }, CSS)];
183
- if (snapshot.error) nodes.push(h("div", { key: "error", className: "dssm-error", role: "alert" }, "操作失败:" + snapshot.error));
393
+ if (snapshot.error) nodes.push(h("div", { key: "error", className: "dssm-error", role: "alert" }, t("error.action", { error: translateError(t, snapshot.error) })));
184
394
  if (snapshot.data) {
185
395
  var roots = snapshot.data.roots || [];
186
396
  var skills = roots.reduce(function (all, root) { return all.concat(root.skills || []); }, []);
187
397
  var enabled = skills.filter(function (skill) { return skill.modelInvocable; }).length;
188
398
  nodes.push(h("div", { key: "toolbar", className: "dssm-toolbar" },
189
- h("div", null, h("h2", { className: "dssm-title" }, "技能"), h("p", { className: "dssm-desc" }, "DSH 技能可上传、启停和删除;公共 Agent 技能仅供查看。")),
399
+ h("div", null, h("h2", { className: "dssm-title" }, t("title")), h("p", { className: "dssm-desc" }, t("desc"))),
190
400
  h("div", { className: "dssm-actions" },
191
- h("button", { className: "dssm-btn dssm-btn-secondary", disabled: !!busy, onClick: refresh }, "刷新"),
192
- h("button", { className: "dssm-btn", disabled: !!busy, onClick: function () { setImportResult(null); setUploadOpen(true); } }, "上传插件"))));
401
+ h("button", { className: "dssm-btn dssm-btn-secondary", disabled: !!busy, onClick: refresh }, t("btn.refresh")),
402
+ h("button", { className: "dssm-btn", disabled: !!busy, onClick: function () { setImportResult(null); setUploadOpen(true); } }, t("btn.upload")))));
403
+ // 复数按 en 的 one/other 二元规则选择;zh/en 均适用,扩展多复数语言时需改 CLDR 规则。
193
404
  nodes.push(h("div", { key: "summary", className: "dssm-summary" },
194
- h("span", { className: "dssm-summary-count" }, skills.length), h("span", { className: "dssm-summary-label" }, "个插件"), h("span", { className: "dssm-summary-separator" }), h("span", { className: "dssm-summary-count" }, enabled), h("span", { className: "dssm-summary-label" }, "个已启用")));
405
+ h("span", { className: "dssm-summary-count" }, skills.length), h("span", { className: "dssm-summary-label" }, t(skills.length === 1 ? "summary.total.one" : "summary.total.other", { count: skills.length })), h("span", { className: "dssm-summary-separator" }), h("span", { className: "dssm-summary-count" }, enabled), h("span", { className: "dssm-summary-label" }, t(enabled === 1 ? "summary.enabled.one" : "summary.enabled.other", { count: enabled }))));
195
406
  roots.forEach(function (root) {
196
407
  var groupSkills = root.skills || [];
197
408
  nodes.push(h("div", { key: root.key, className: "dssm-group" },
198
- h("div", { className: "dssm-group-head" }, h("h3", { className: "dssm-group-title" }, root.label), h("span", { className: "dssm-count" }, groupSkills.length + " 个"), h("span", { className: "dssm-path", title: root.path }, root.path)),
199
- root.exists === false ? h("div", { className: "dssm-empty" }, root.mutable ? "目录尚未创建,上传第一个插件后会自动创建。" : "公共目录不存在。") : (groupSkills.length ? groupSkills.map(function (skill) { return skillRow(skill, root); }) : h("div", { className: "dssm-empty" }, "暂未安装插件。"))));
409
+ h("div", { className: "dssm-group-head" }, h("h3", { className: "dssm-group-title" }, translateOrFallback(t, "root." + root.key, root.label)), h("span", { className: "dssm-count" }, t("summary.group", { count: groupSkills.length })), h("span", { className: "dssm-path", title: root.path }, root.path)),
410
+ root.exists === false ? h("div", { className: "dssm-empty" }, root.mutable ? t("empty.dir.uncreated") : t("empty.dir.missing")) : (groupSkills.length ? groupSkills.map(function (skill) { return skillRow(skill, root); }) : h("div", { className: "dssm-empty" }, t("empty.skills.none")))));
200
411
  });
201
412
  } else if (snapshot.loading) {
202
- nodes.push(h("div", { key: "loading", className: "dssm-note" }, "加载中…"));
413
+ nodes.push(h("div", { key: "loading", className: "dssm-note" }, t("loading")));
203
414
  }
204
415
  if (importResult) {
416
+ var nameSeparator = t("sep.names");
417
+ var errorSeparator = t("sep.errors");
418
+ // 错误形态:业务/协议错误带 code,或 host 失败带 error(Error 对象);结果形态只有 imported/failed/skipped。
419
+ var isImportError = importResult.code !== undefined || importResult.error !== undefined;
205
420
  var importedNames = (importResult.imported || []).map(function (item) { return item.name; });
206
- var failedItems = importResult.failed || [];
421
+ // catch 场景把 core 的失败明细挂在 Error.failed 上,这里一并取用。
422
+ var failedItems = importResult.failed || (importResult.error && importResult.error.failed) || [];
207
423
  var skippedNames = (importResult.skipped || []).map(function (item) { return item.name; });
208
- var failedText = failedItems.map(function (item) { return item.error; }).join(";");
209
- var resultText = importResult.error ? "上传失败:" + importResult.error : failedItems.length ? (importedNames.length ? "部分上传成功:" + importedNames.join("、") + ";失败:" + failedText : "上传失败:" + failedText) : importedNames.length ? "上传完成:" + importedNames.join("、") : skippedNames.length ? "未导入:同名插件已跳过:" + skippedNames.join("、") : "未导入任何插件。";
210
- nodes.push(h("div", { key: "import-result", className: importResult.error || failedItems.length ? "dssm-error" : "dssm-note", role: "status" }, resultText));
424
+ var failedText = failedItems.map(function (item) { return translateError(t, item); }).join(errorSeparator);
425
+ var resultText;
426
+ if (isImportError) {
427
+ // 顶层聚合错误优先展示已翻译的失败明细;无明细时由 translateError 兜底(含 Error 对象)。
428
+ resultText = t("result.failed", { error: failedItems.length ? failedText : translateError(t, importResult) });
429
+ } else if (failedItems.length) {
430
+ resultText = importedNames.length ? t("result.partial", { names: importedNames.join(nameSeparator), errors: failedText }) : t("result.failed.only", { errors: failedText });
431
+ } else if (importedNames.length) {
432
+ resultText = t("result.done", { names: importedNames.join(nameSeparator) });
433
+ } else if (skippedNames.length) {
434
+ resultText = t("result.skipped", { names: skippedNames.join(nameSeparator) });
435
+ } else {
436
+ resultText = t("result.none");
437
+ }
438
+ nodes.push(h("div", { key: "import-result", className: isImportError || failedItems.length ? "dssm-error" : "dssm-note", role: "status" }, resultText));
211
439
  }
212
440
  if (uploadOpen) {
213
441
  nodes.push(h("div", { key: "upload", className: "dssm-mask" }, h("div", { className: "dssm-modal", role: "dialog", "aria-modal": "true", "aria-labelledby": "dssm-upload-title" },
214
- h("div", { className: "dssm-modal-head" }, h("h3", { id: "dssm-upload-title", className: "dssm-modal-title" }, "上传插件"), h("button", { className: "dssm-icon-btn", "aria-label": "关闭上传弹窗", onClick: function () { setUploadOpen(false); } }, "×")),
442
+ h("div", { className: "dssm-modal-head" }, h("h3", { id: "dssm-upload-title", className: "dssm-modal-title" }, t("upload.title")), h("button", { className: "dssm-icon-btn", "aria-label": t("upload.close"), onClick: function () { setUploadOpen(false); } }, "×")),
215
443
  h("input", { ref: inputRef, className: "dssm-hidden-input", type: "file", accept: ".md", onChange: function (event) { selectFile(event.target.files && event.target.files[0]); event.target.value = ""; } }),
216
- h("div", { className: "dssm-dropzone", tabIndex: 0, onClick: openNativePicker, onKeyDown: function (event) { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); openNativePicker(); } }, onDragOver: function (event) { event.preventDefault(); }, onDrop: function (event) { event.preventDefault(); selectFile(event.dataTransfer.files && event.dataTransfer.files[0]); } }, h("span", { className: "dssm-dropzone-title" }, "拖放 SKILL.md 文件到这里"), h("span", { className: "dssm-dropzone-copy" }, "或点击打开系统原生文件选择窗口")),
217
- selected ? h("div", { className: "dssm-file", title: selected.source }, h("span", { className: "dssm-file-name" }, selected.name), h("span", { className: "dssm-tag dssm-tag-on" }, "已选择")) : h("p", { className: "dssm-desc" }, "请选择插件目录中的 SKILL.md;目录内其他文件会一并上传。"),
218
- busy === "pick-directory" ? h("div", { className: "dssm-note", role: "status" }, "正在打开系统原生目录选择窗口…") : null,
219
- importResult && importResult.error ? h("div", { className: "dssm-error", role: "alert" }, "选择失败:" + importResult.error) : null,
220
- h("div", { className: "dssm-modal-actions" }, h("button", { className: "dssm-btn dssm-btn-secondary", disabled: !!busy, onClick: openNativePicker }, "选择本地文件"), pickDirectory ? h("button", { className: "dssm-btn dssm-btn-secondary", disabled: !!busy, onClick: selectDirectory }, "选择插件文件夹") : null, h("button", { className: "dssm-btn", disabled: !selected || !!busy, onClick: installSelected }, busy === "import" || busy === "import-check" ? "上传中…" : "上传到 DSH")))));
444
+ h("div", { className: "dssm-dropzone", tabIndex: 0, onClick: openNativePicker, onKeyDown: function (event) { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); openNativePicker(); } }, onDragOver: function (event) { event.preventDefault(); }, onDrop: function (event) { event.preventDefault(); selectFile(event.dataTransfer.files && event.dataTransfer.files[0]); } }, h("span", { className: "dssm-dropzone-title" }, t("upload.drop.title")), h("span", { className: "dssm-dropzone-copy" }, t("upload.drop.copy"))),
445
+ selected ? h("div", { className: "dssm-file", title: selected.source }, h("span", { className: "dssm-file-name" }, selected.nameKey ? t(selected.nameKey) : selected.name), h("span", { className: "dssm-tag dssm-tag-on" }, t("status.selected"))) : h("p", { className: "dssm-desc" }, t("upload.hint")),
446
+ busy === "pick-directory" ? h("div", { className: "dssm-note", role: "status" }, t("upload.picking.dir")) : null,
447
+ importResult && (importResult.code !== undefined || importResult.error !== undefined) ? h("div", { className: "dssm-error", role: "alert" }, t("select.failed", { error: translateError(t, importResult) })) : null,
448
+ h("div", { className: "dssm-modal-actions" }, h("button", { className: "dssm-btn dssm-btn-secondary", disabled: !!busy, onClick: openNativePicker }, t("btn.file.pick")), pickDirectory ? h("button", { className: "dssm-btn dssm-btn-secondary", disabled: !!busy, onClick: selectDirectory }, t("btn.dir.pick")) : null, h("button", { className: "dssm-btn", disabled: !selected || !!busy, onClick: installSelected }, busy === "import" || busy === "import-check" ? t("btn.uploading") : t("btn.upload.dsh"))))));
221
449
  }
222
450
  if (confirmImport) {
223
- nodes.push(h("div", { key: "import-confirm", className: "dssm-mask" }, h("div", { className: "dssm-modal", role: "dialog", "aria-modal": "true" }, h("div", { className: "dssm-modal-head" }, h("h3", { className: "dssm-modal-title" }, "发现同名插件")), h("p", { className: "dssm-desc" }, "将覆盖现有插件:" + confirmImport.conflicts.map(function (item) { return item.name; }).join("")), h("div", { className: "dssm-modal-actions" }, h("button", { className: "dssm-btn dssm-btn-secondary", onClick: function () { setConfirmImport(null); } }, "取消"), h("button", { className: "dssm-btn", disabled: !!busy, onClick: function () { executeImport(confirmImport.source, "overwrite"); } }, "覆盖上传")))));
451
+ nodes.push(h("div", { key: "import-confirm", className: "dssm-mask" }, h("div", { className: "dssm-modal", role: "dialog", "aria-modal": "true" }, h("div", { className: "dssm-modal-head" }, h("h3", { className: "dssm-modal-title" }, t("confirm.overwrite.title"))), h("p", { className: "dssm-desc" }, t("confirm.overwrite.desc", { names: confirmImport.conflicts.map(function (item) { return item.name; }).join(t("sep.names")) })), h("div", { className: "dssm-modal-actions" }, h("button", { className: "dssm-btn dssm-btn-secondary", onClick: function () { setConfirmImport(null); } }, t("btn.cancel")), h("button", { className: "dssm-btn", disabled: !!busy, onClick: function () { executeImport(confirmImport.source, "overwrite"); } }, t("btn.overwrite.upload"))))));
224
452
  }
225
453
  if (confirmDelete) {
226
- nodes.push(h("div", { key: "delete-confirm", className: "dssm-mask" }, h("div", { className: "dssm-modal", role: "dialog", "aria-modal": "true" }, h("div", { className: "dssm-modal-head" }, h("h3", { className: "dssm-modal-title" }, "删除插件?")), h("p", { className: "dssm-desc" }, "" + confirmDelete.name + "”将从 DSH 技能目录中永久删除,无法恢复。"), h("div", { className: "dssm-modal-actions" }, h("button", { className: "dssm-btn dssm-btn-secondary", disabled: !!busy, onClick: function () { setConfirmDelete(null); } }, "取消"), h("button", { className: "dssm-btn dssm-btn-danger", disabled: !!busy, onClick: function () { setConfirmDelete(null); action("/delete", { name: confirmDelete.name }); } }, "确认删除")))));
454
+ nodes.push(h("div", { key: "delete-confirm", className: "dssm-mask" }, h("div", { className: "dssm-modal", role: "dialog", "aria-modal": "true" }, h("div", { className: "dssm-modal-head" }, h("h3", { className: "dssm-modal-title" }, t("confirm.delete.title"))), h("p", { className: "dssm-desc" }, t("confirm.delete.desc", { name: confirmDelete.name })), h("div", { className: "dssm-modal-actions" }, h("button", { className: "dssm-btn dssm-btn-secondary", disabled: !!busy, onClick: function () { setConfirmDelete(null); } }, t("btn.cancel")), h("button", { className: "dssm-btn dssm-btn-danger", disabled: !!busy, onClick: function () { setConfirmDelete(null); action("/delete", { name: confirmDelete.name }); } }, t("btn.delete.confirm"))))));
227
455
  }
228
456
  return h("section", { className: "dssm-section" }, nodes);
229
457
  }
230
458
 
231
459
  function apply(ctx) {
232
460
  pickDirectory = function () { return ctx.workspaces.pickDirectory(); };
461
+ ctx.effect(function () {
462
+ return ctx.locale.register(NS, DICT);
463
+ }, "skills-manager: dictionaries");
233
464
  ctx.slots.inject("settings.section", function () {
234
- return ctx.slots.register({ name: "settings.section", id: "skills-manager", order: 17, label: "技能" }, SkillManagerSection);
465
+ return ctx.slots.register({ name: "settings.section", id: "skills-manager", order: 17, label: function () { return ctx.locale.bind(NS)("title"); }, locale: NS }, SkillManagerSection);
235
466
  });
236
467
  }
237
468
  module.exports.apply = apply;
238
- module.exports.inject = ["slots", "workspaces"];
469
+ module.exports.inject = ["slots", "workspaces", "locale"];
470
+ // 导出词典与翻译函数供零依赖对齐测试读取(宿主只消费 apply/inject,不影响运行时)。
471
+ module.exports.DICT = DICT;
472
+ module.exports.translateError = translateError;
239
473
  return module.exports;
240
474
  }
241
475
  });
package/lib/core.js CHANGED
@@ -5,7 +5,10 @@
5
5
  // - 条目形态:<root>/<name>/SKILL.md(bundle)或 <root>/<name>.md(flat),只扫一层
6
6
  // - 前端展示 name、description 与启停状态,不做格式检查或自动修复
7
7
  //
8
- // 所有函数返回普通结果对象,业务校验失败返回 { ok: false, error };文件写入错误由路由返回给调用方。
8
+ // 所有函数返回普通结果对象,业务校验失败返回 { ok: false, error, code?, params? }
9
+ // error 保持中文原文(兼容性红线),code 为点分小写业务错误码,params 供前端词典占位符替换;
10
+ // 系统异常(fs ENOENT 等)透传 String(e.message),不加 code。
11
+ // 文件写入错误由路由返回给调用方。
9
12
 
10
13
  import { homedir } from "node:os";
11
14
  import { join, basename, dirname, resolve, relative, isAbsolute, sep } from "node:path";
@@ -16,6 +19,23 @@ const KEBAB_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
16
19
  const WINDOWS_DEVICE_NAME_RE = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/i;
17
20
  const MAX_SOURCE_DEPTH = 64;
18
21
 
22
+ // ── 业务错误码 ────────────────────────────────────────────────────────────────
23
+
24
+ /** 构造带 code/params 的业务 Error,供导入链路 throw 后透传到失败明细。 */
25
+ function codedError(message, code, params) {
26
+ const error = new Error(message);
27
+ error.code = code;
28
+ error.params = params;
29
+ return error;
30
+ }
31
+
32
+ /** 把业务 Error 的 code/params 附加到失败明细;系统异常(ENOENT 等,非 error.* 前缀)保持原文。 */
33
+ function attachCode(item, error) {
34
+ if (error && typeof error.code === "string" && /^error\./.test(error.code)) item.code = error.code;
35
+ if (error && error.params) item.params = error.params;
36
+ return item;
37
+ }
38
+
19
39
  // ── 路径解析 ────────────────────────────────────────────────────────────────
20
40
 
21
41
  export function resolveDshHome() {
@@ -42,6 +62,16 @@ function dshRootPath() {
42
62
  return userRoots().find((root) => root.key === "dsh").path;
43
63
  }
44
64
 
65
+ /** 公共 Agent 目录的只读拒绝结果;action 为可翻译语义值(toggle/delete)。 */
66
+ function readonlyError(action) {
67
+ return {
68
+ ok: false,
69
+ code: "error.root.readonly",
70
+ params: { action },
71
+ error: action === "delete" ? "公共 Agent 技能目录不允许删除" : "公共 Agent 技能目录不允许启用或停用",
72
+ };
73
+ }
74
+
45
75
  /** 判断 child 是否与 parent 相同或位于其内部。跨盘符时 relative 会返回绝对路径。 */
46
76
  function isSameOrDescendant(parent, child) {
47
77
  const rel = relative(resolve(parent), resolve(child));
@@ -252,21 +282,21 @@ export async function scanEntries(root) {
252
282
 
253
283
  /** enabled=true 恢复模型与 / 手动调用;false 同时停用两种调用入口。 */
254
284
  export async function setSkillEnabled(root, name, enabled) {
255
- if (root !== dshRootPath()) return { ok: false, error: "公共 Agent 技能目录不允许启用或停用" };
285
+ if (root !== dshRootPath()) return readonlyError("toggle");
256
286
  const resolved = await resolveEntry(root, name);
257
- if (resolved === null) return { ok: false, error: `技能不存在: ${name}` };
287
+ if (resolved === null) return { ok: false, error: `技能不存在: ${name}`, code: "error.skill.notFound", params: { name } };
258
288
  const source = await fs.readFile(resolved.docPath, "utf8");
259
289
  const updated = updateInvocationPolicy(source, enabled);
260
- if (updated === null) return { ok: false, error: `技能缺少完整 frontmatter,无法${enabled ? "启用" : "停用"}: ${name}` };
290
+ if (updated === null) return { ok: false, error: `技能缺少完整 frontmatter,无法${enabled ? "启用" : "停用"}: ${name}`, code: "error.skill.noFrontmatter", params: { name, action: enabled ? "enable" : "disable" } };
261
291
  await writeFileAtomically(resolved.docPath, updated);
262
292
  return { name, enabled };
263
293
  }
264
294
 
265
295
  /** 删除 DSH 根目录中的单个技能。调用方必须先向用户确认。 */
266
296
  export async function deleteSkill(root, name, log) {
267
- if (root !== dshRootPath()) return { ok: false, error: "公共 Agent 技能目录不允许删除" };
297
+ if (root !== dshRootPath()) return readonlyError("delete");
268
298
  const resolved = await resolveEntry(root, name);
269
- if (resolved === null) return { ok: false, error: `技能不存在: ${name}` };
299
+ if (resolved === null) return { ok: false, error: `技能不存在: ${name}`, code: "error.skill.notFound", params: { name } };
270
300
  await fs.rm(resolved.entryPath, { recursive: resolved.kind === "bundle", force: true });
271
301
  if (log) log("delete", `删除 ${resolved.entryPath}`);
272
302
  return { name };
@@ -280,9 +310,9 @@ async function analyzeSource(source) {
280
310
  try {
281
311
  st = await fs.lstat(source);
282
312
  } catch {
283
- return { kind: "none", error: `路径不存在: ${source}` };
313
+ return { kind: "none", error: `路径不存在: ${source}`, code: "error.source.notFound", params: { path: source } };
284
314
  }
285
- if (st.isSymbolicLink()) return { kind: "none", error: `不支持包含符号链接的 skill 来源: ${source}` };
315
+ if (st.isSymbolicLink()) return { kind: "none", error: `不支持包含符号链接的 skill 来源: ${source}`, code: "error.source.symlink", params: { path: source } };
286
316
  if (st.isDirectory()) {
287
317
  try {
288
318
  const sk = join(source, "SKILL.md");
@@ -301,14 +331,14 @@ async function analyzeSource(source) {
301
331
  const rawName = basename(source).slice(0, -3);
302
332
  return { kind: "single", rawName, kebab: toKebab(rawName), source, isDir: false, skillFile: source };
303
333
  }
304
- return { kind: "none", error: `无法识别的 skill 来源: ${source}` };
334
+ return { kind: "none", error: `无法识别的 skill 来源: ${source}`, code: "error.source.unrecognized", params: { path: source } };
305
335
  }
306
336
 
307
337
  async function collectCandidates(dir) {
308
338
  const items = await fs.readdir(dir, { withFileTypes: true });
309
339
  const out = [];
310
340
  for (const it of items) {
311
- if (it.isSymbolicLink()) throw new Error(`不支持包含符号链接的 skill 来源: ${join(dir, it.name)}`);
341
+ if (it.isSymbolicLink()) throw codedError(`不支持包含符号链接的 skill 来源: ${join(dir, it.name)}`, "error.source.symlink", { path: join(dir, it.name) });
312
342
  try {
313
343
  if (it.isDirectory()) {
314
344
  const sk = join(dir, it.name, "SKILL.md");
@@ -331,14 +361,14 @@ async function assertNoSymbolicLinks(source) {
331
361
  const pending = [{ path: source, depth: 0 }];
332
362
  while (pending.length) {
333
363
  const current = pending.pop();
334
- if (current.depth > MAX_SOURCE_DEPTH) throw new Error(`skill 来源目录层级超过 ${MAX_SOURCE_DEPTH} 层: ${source}`);
364
+ if (current.depth > MAX_SOURCE_DEPTH) throw codedError(`skill 来源目录层级超过 ${MAX_SOURCE_DEPTH} 层: ${source}`, "error.source.tooDeep", { depth: MAX_SOURCE_DEPTH, path: source });
335
365
  const st = await fs.lstat(current.path);
336
- if (st.isSymbolicLink()) throw new Error(`不支持包含符号链接的 skill 来源: ${current.path}`);
366
+ if (st.isSymbolicLink()) throw codedError(`不支持包含符号链接的 skill 来源: ${current.path}`, "error.source.symlink", { path: current.path });
337
367
  if (!st.isDirectory()) continue;
338
368
  const items = await fs.readdir(current.path, { withFileTypes: true });
339
369
  for (const item of items) {
340
370
  const path = join(current.path, item.name);
341
- if (item.isSymbolicLink()) throw new Error(`不支持包含符号链接的 skill 来源: ${path}`);
371
+ if (item.isSymbolicLink()) throw codedError(`不支持包含符号链接的 skill 来源: ${path}`, "error.source.symlink", { path });
342
372
  if (item.isDirectory()) pending.push({ path, depth: current.depth + 1 });
343
373
  }
344
374
  }
@@ -400,8 +430,8 @@ export async function importSkill(source, log, options = {}) {
400
430
  const dryRun = options.dryRun === true;
401
431
 
402
432
  const analysis = await analyzeSource(source);
403
- if (analysis.kind === "none") return { ok: false, error: analysis.error || "无法识别的 skill 来源" };
404
- if (pathsOverlap(analysis.source, targetRoot)) return { ok: false, error: "导入来源不能与 DSH 技能目录相同、包含或位于其中" };
433
+ if (analysis.kind === "none") return { ok: false, error: analysis.error || "无法识别的 skill 来源", code: analysis.code || "error.source.unrecognized", params: analysis.params };
434
+ if (pathsOverlap(analysis.source, targetRoot)) return { ok: false, error: "导入来源不能与 DSH 技能目录相同、包含或位于其中", code: "error.import.overlap" };
405
435
 
406
436
  let candidates = [];
407
437
  if (analysis.kind === "single") {
@@ -410,9 +440,9 @@ export async function importSkill(source, log, options = {}) {
410
440
  try {
411
441
  candidates = await collectCandidates(source);
412
442
  } catch (error) {
413
- return { ok: false, error: String(error && error.message ? error.message : error) };
443
+ return attachCode({ ok: false, error: String(error && error.message ? error.message : error) }, error);
414
444
  }
415
- if (candidates.length === 0) return { ok: false, error: `目录下未找到任何 skill 条目(需含 SKILL.md 的子目录或 .md 文件): ${source}` };
445
+ if (candidates.length === 0) return { ok: false, error: `目录下未找到任何 skill 条目(需含 SKILL.md 的子目录或 .md 文件): ${source}`, code: "error.import.emptySource", params: { path: source } };
416
446
  }
417
447
 
418
448
  const pending = [];
@@ -429,7 +459,9 @@ export async function importSkill(source, log, options = {}) {
429
459
  function failureResult() {
430
460
  return {
431
461
  ok: false,
462
+ // 聚合失败明细的原文;前端优先展示已翻译的 failed 明细,此处仅作兜底。
432
463
  error: failed.map((item) => item.error).join(";"),
464
+ code: "error.import.failed",
433
465
  kind: analysis.kind,
434
466
  imported,
435
467
  skipped,
@@ -439,11 +471,11 @@ export async function importSkill(source, log, options = {}) {
439
471
 
440
472
  for (const c of candidates) {
441
473
  if (!c.kebab || !KEBAB_RE.test(c.kebab)) {
442
- failed.push({ source: c.source, error: `无法生成合法 kebab-case 名称(原始名: ${c.rawName || basename(c.source)})` });
474
+ failed.push({ source: c.source, error: `无法生成合法 kebab-case 名称(原始名: ${c.rawName || basename(c.source)})`, code: "error.import.invalidName", params: { name: c.rawName || basename(c.source) } });
443
475
  continue;
444
476
  }
445
477
  if (nameCount.get(c.kebab) > 1) {
446
- failed.push({ source: c.source, error: `批量来源中存在多个同名插件: ${c.kebab}` });
478
+ failed.push({ source: c.source, error: `批量来源中存在多个同名插件: ${c.kebab}`, code: "error.import.duplicateName", params: { name: c.kebab } });
447
479
  continue;
448
480
  }
449
481
  const dest = c.isDir ? join(targetRoot, c.kebab) : join(targetRoot, `${c.kebab}.md`);
@@ -475,7 +507,7 @@ export async function importSkill(source, log, options = {}) {
475
507
  imported.push({ name: p.name, overwritten: false, warnings });
476
508
  if (log) log("import", `导入 ${p.source} -> ${p.dest}`);
477
509
  } catch (e) {
478
- failed.push({ source: p.source, error: String(e && e.message ? e.message : e) });
510
+ failed.push(attachCode({ source: p.source, error: String(e && e.message ? e.message : e) }, e));
479
511
  }
480
512
  }
481
513
 
@@ -487,7 +519,7 @@ export async function importSkill(source, log, options = {}) {
487
519
  imported.push({ name: c.name, overwritten: true, warnings });
488
520
  if (log) log("import-overwrite", `覆盖导入 ${c.source} -> ${dest}`);
489
521
  } catch (e) {
490
- failed.push({ source: c.source, error: String(e && e.message ? e.message : e) });
522
+ failed.push(attachCode({ source: c.source, error: String(e && e.message ? e.message : e) }, e));
491
523
  }
492
524
  }
493
525
  } else {
package/lib/index.js CHANGED
@@ -47,6 +47,7 @@ function readBody(req, limit = 1 << 20) {
47
47
  if (size > limit) {
48
48
  const error = new Error("body too large");
49
49
  error.statusCode = 413;
50
+ error.code = "error.proto.bodyTooLarge";
50
51
  fail(error);
51
52
  req.resume();
52
53
  return;
@@ -63,6 +64,7 @@ function readBody(req, limit = 1 << 20) {
63
64
  } catch (e) {
64
65
  const error = new Error(`invalid JSON body: ${e.message}`);
65
66
  error.statusCode = 400;
67
+ error.code = "error.proto.invalidJson";
66
68
  fail(error);
67
69
  }
68
70
  });
@@ -72,9 +74,9 @@ function readBody(req, limit = 1 << 20) {
72
74
 
73
75
  /** 自定义请求头使跨站 fetch 必须预检;本地接口不提供 CORS 响应。 */
74
76
  function validateMutationRequest(req) {
75
- if (req.headers[CLIENT_MARKER_HEADER] !== "1") return { statusCode: 403, error: "forbidden mutation request" };
77
+ if (req.headers[CLIENT_MARKER_HEADER] !== "1") return { statusCode: 403, code: "error.proto.forbidden", error: "forbidden mutation request" };
76
78
  const contentType = String(req.headers["content-type"] || "").split(";", 1)[0].trim().toLowerCase();
77
- if (contentType !== "application/json") return { statusCode: 415, error: "content-type must be application/json" };
79
+ if (contentType !== "application/json") return { statusCode: 415, code: "error.proto.contentType", error: "content-type must be application/json" };
78
80
  return null;
79
81
  }
80
82
 
@@ -134,7 +136,7 @@ function apply(ctx) {
134
136
  return;
135
137
  }
136
138
  if (req.method !== "POST") {
137
- json(res, 405, { ok: false, error: `method not allowed: ${req.method}` });
139
+ json(res, 405, { ok: false, code: "error.proto.method", error: `method not allowed: ${req.method}` });
138
140
  return;
139
141
  }
140
142
  const requestError = validateMutationRequest(req);
@@ -145,11 +147,11 @@ function apply(ctx) {
145
147
  const body = await readBody(req);
146
148
  switch (path) {
147
149
  case "/api/dsh-skills-manager/enable": {
148
- run(res, String(body.root || "dsh") === "dsh" ? setSkillEnabled(rootByKey.dsh, String(body.name || ""), true) : Promise.resolve({ ok: false, error: "公共 Agent 技能目录不允许启用或停用" }), invalidateSkills);
150
+ run(res, setSkillEnabled(rootByKey[String(body.root || "dsh")], String(body.name || ""), true), invalidateSkills);
149
151
  return;
150
152
  }
151
153
  case "/api/dsh-skills-manager/disable": {
152
- run(res, String(body.root || "dsh") === "dsh" ? setSkillEnabled(rootByKey.dsh, String(body.name || ""), false) : Promise.resolve({ ok: false, error: "公共 Agent 技能目录不允许启用或停用" }), invalidateSkills);
154
+ run(res, setSkillEnabled(rootByKey[String(body.root || "dsh")], String(body.name || ""), false), invalidateSkills);
153
155
  return;
154
156
  }
155
157
  case "/api/dsh-skills-manager/delete": {
@@ -163,10 +165,14 @@ function apply(ctx) {
163
165
  }), body.dryRun === true ? undefined : invalidateSkills);
164
166
  return;
165
167
  default:
166
- json(res, 404, { ok: false, error: `unknown action: ${path}` });
168
+ json(res, 404, { ok: false, code: "error.proto.unknownAction", error: `unknown action: ${path}` });
167
169
  }
168
170
  } catch (e) {
169
- json(res, Number.isInteger(e && e.statusCode) ? e.statusCode : 500, { ok: false, error: String(e && e.message ? e.message : e) });
171
+ json(res, Number.isInteger(e && e.statusCode) ? e.statusCode : 500, {
172
+ ok: false,
173
+ ...(e && e.code ? { code: e.code } : {}),
174
+ error: String(e && e.message ? e.message : e),
175
+ });
170
176
  }
171
177
  },
172
178
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michengai/dsh-skills-manager",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "NPM-installable DSH Web plugin for managing local skills and viewing shared Agent skills safely.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -31,13 +31,14 @@
31
31
  "inject": [
32
32
  "@deepseek-ai/dsh-client-runtime",
33
33
  "@deepseek-ai/dsh-client-ui-slots",
34
+ "@deepseek-ai/dsh-client-locale",
34
35
  "@deepseek-ai/dsh-client-ui-workspace"
35
36
  ],
36
37
  "platform": "web"
37
38
  }
38
39
  },
39
40
  "scripts": {
40
- "test": "node test/core-test.mjs",
41
+ "test": "node test/core-test.mjs && node test/locale-test.mjs",
41
42
  "pack:check": "npm pack --dry-run --json",
42
43
  "prepublishOnly": "npm test"
43
44
  },
@@ -49,6 +50,7 @@
49
50
  "@deepseek-ai/cordis": ">=4.0.1 <5.0.0",
50
51
  "@deepseek-ai/dsh-client-runtime": ">=0.1.0-rc.0 <0.2.0",
51
52
  "@deepseek-ai/dsh-client-ui-slots": ">=0.1.0-rc.0 <0.2.0",
53
+ "@deepseek-ai/dsh-client-locale": ">=0.1.0-rc.0 <0.2.0",
52
54
  "@deepseek-ai/dsh-host-webserver": ">=0.1.0-rc.5",
53
55
  "@deepseek-ai/dsh-skill": ">=0.1.0-rc.5",
54
56
  "@deepseek-ai/dsh-client-ui-workspace": ">=0.1.0-rc.5",