@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 +272 -38
- package/lib/core.js +53 -21
- package/lib/index.js +13 -7
- package/package.json +4 -2
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)
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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({
|
|
311
|
+
setImportResult({ code: "select.file.invalid" });
|
|
102
312
|
return;
|
|
103
313
|
}
|
|
104
314
|
if (!file.path) {
|
|
105
315
|
setSelected(null);
|
|
106
|
-
setImportResult({
|
|
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({
|
|
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({
|
|
333
|
+
setSelected({ nameKey: "dir.selected", source: path });
|
|
124
334
|
setImportResult(null);
|
|
125
335
|
}
|
|
126
336
|
}).catch(function (error) {
|
|
127
337
|
setBusy(null);
|
|
128
|
-
setImportResult({
|
|
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:
|
|
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:
|
|
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 ? "
|
|
179
|
-
root.mutable ? h("button", { className: "dssm-btn dssm-btn-danger", disabled: !!busy, onClick: function () { setConfirmDelete(skill); } }, "
|
|
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" }, "
|
|
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" }, "
|
|
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" }, "
|
|
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
|
|
199
|
-
root.exists === false ? h("div", { className: "dssm-empty" }, root.mutable ? "
|
|
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
|
-
|
|
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
|
|
209
|
-
var resultText
|
|
210
|
-
|
|
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" }, "
|
|
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" }, "
|
|
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" }, "
|
|
218
|
-
busy === "pick-directory" ? h("div", { className: "dssm-note", role: "status" }, "
|
|
219
|
-
importResult && importResult.error ? h("div", { className: "dssm-error", role: "alert" }, "
|
|
220
|
-
h("div", { className: "dssm-modal-actions" }, h("button", { className: "dssm-btn dssm-btn-secondary", disabled: !!busy, onClick: openNativePicker }, "
|
|
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" }, "
|
|
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" }, "
|
|
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: "
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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")
|
|
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")
|
|
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, {
|
|
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.
|
|
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",
|