@michengai/dsh-skills-manager 0.1.16 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -165,3 +165,4 @@ npm run pack:check
165
165
  Project status, usage boundaries, architecture, and iteration records begin at the [documentation entry point](docs/00-交接入口/00-阅读导航.md). The detailed operational guide is `docs\02-产品与业务\01-使用说明.md`.
166
166
 
167
167
  Licensed under [Apache License 2.0](LICENSE).
168
+
package/README.zh-CN.md CHANGED
@@ -165,3 +165,4 @@ npm run pack:check
165
165
  项目状态、使用边界、技术架构和迭代记录从[文档交接入口](docs/00-交接入口/00-阅读导航.md)开始。详细操作说明见 `docs\02-产品与业务\01-使用说明.md`。
166
166
 
167
167
  本项目采用 [Apache License 2.0](LICENSE)。
168
+
package/lib/client.js CHANGED
@@ -40,6 +40,16 @@ window.__ModuleLoader__.load({
40
40
  "summary.enabled.one": "个已启用",
41
41
  "summary.enabled.other": "个已启用",
42
42
  "summary.group": "{count} 个",
43
+ "filter.category": "分类",
44
+ "filter.all": "全部",
45
+ "filter.option": "{name}({count})",
46
+ "filter.showing.one": "显示 {count} 个",
47
+ "filter.showing.other": "显示 {count} 个",
48
+ "search": "搜索",
49
+ "search.placeholder": "名称、形态或简介",
50
+ "search.clear": "清除搜索",
51
+ "empty.search": "没有匹配的插件。试试其他关键词,或切换到「{all}」。",
52
+ "empty.search.reset": "清除筛选",
43
53
  "note.missing": "未提供简介",
44
54
  "empty.dir.uncreated": "目录尚未创建,上传第一个插件后会自动创建。",
45
55
  "empty.dir.missing": "公共目录不存在。",
@@ -55,6 +65,8 @@ window.__ModuleLoader__.load({
55
65
  "result.done": "上传完成:{names}",
56
66
  "result.skipped": "未导入:同名插件已跳过:{names}",
57
67
  "result.none": "未导入任何插件。",
68
+ "result.warnings": "警告:{warnings}",
69
+ "warning.backupUncleaned": "旧版本备份未清理: {path}({error})",
58
70
  "upload.title": "上传插件",
59
71
  "upload.close": "关闭上传弹窗",
60
72
  "upload.drop.title": "拖放 SKILL.md 文件到这里",
@@ -83,7 +95,9 @@ window.__ModuleLoader__.load({
83
95
  "error.import.invalidName": "无法生成合法 kebab-case 名称(原始名: {name})",
84
96
  "error.import.duplicateName": "批量来源中存在多个同名插件: {name}",
85
97
  "error.import.failed": "导入失败",
98
+ "error.import.rollbackFailed": "覆盖导入回滚失败,备份保留在: {path}({error})",
86
99
  "error.proto.forbidden": "禁止的修改请求(缺少客户端标记)",
100
+ "error.proto.forbiddenHost": "禁止的请求来源(非法 Host)",
87
101
  "error.proto.contentType": "请求体必须是 application/json",
88
102
  "error.proto.method": "不支持的请求方法",
89
103
  "error.proto.unknownAction": "未知操作",
@@ -124,6 +138,16 @@ window.__ModuleLoader__.load({
124
138
  "summary.enabled.one": "enabled",
125
139
  "summary.enabled.other": "enabled",
126
140
  "summary.group": "{count} items",
141
+ "filter.category": "Category",
142
+ "filter.all": "All",
143
+ "filter.option": "{name} ({count})",
144
+ "filter.showing.one": "Showing {count} skill",
145
+ "filter.showing.other": "Showing {count} skills",
146
+ "search": "Search",
147
+ "search.placeholder": "Name, kind, or description",
148
+ "search.clear": "Clear search",
149
+ "empty.search": "No matching plugins. Try another keyword, or switch to {all}.",
150
+ "empty.search.reset": "Clear filters",
127
151
  "note.missing": "No description provided",
128
152
  "empty.dir.uncreated": "The directory has not been created yet; it will be created automatically after the first upload.",
129
153
  "empty.dir.missing": "The public directory does not exist.",
@@ -138,6 +162,8 @@ window.__ModuleLoader__.load({
138
162
  "result.done": "Upload complete: {names}",
139
163
  "result.skipped": "Not imported: same-name plugins were skipped: {names}",
140
164
  "result.none": "Nothing was imported.",
165
+ "result.warnings": "Warning: {warnings}",
166
+ "warning.backupUncleaned": "Old version backup was not cleaned up: {path} ({error})",
141
167
  "upload.title": "Upload plugin",
142
168
  "upload.close": "Close upload dialog",
143
169
  "upload.drop.title": "Drop SKILL.md file here",
@@ -166,7 +192,9 @@ window.__ModuleLoader__.load({
166
192
  "error.import.invalidName": "Cannot generate a valid kebab-case name (original name: {name})",
167
193
  "error.import.duplicateName": "Batch source contains multiple plugins with the same name: {name}",
168
194
  "error.import.failed": "Import failed",
195
+ "error.import.rollbackFailed": "Overwrite import rollback failed; backups kept at: {path} ({error})",
169
196
  "error.proto.forbidden": "Forbidden mutation request (missing client marker)",
197
+ "error.proto.forbiddenHost": "Forbidden request origin (invalid host)",
170
198
  "error.proto.contentType": "Content type must be application/json",
171
199
  "error.proto.method": "Method not allowed",
172
200
  "error.proto.unknownAction": "Unknown action",
@@ -194,7 +222,8 @@ window.__ModuleLoader__.load({
194
222
  .dssm-row{display:flex;align-items:center;gap:12px;padding:13px 16px;border-bottom:1px solid var(--dsw-alias-border-l1)}.dssm-row:last-child{border-bottom:0}.dssm-row-main{display:flex;min-width:0;flex:1;flex-direction:column;gap:3px}.dssm-row-id{display:flex;align-items:center;gap:7px;min-width:0}.dssm-row-name{overflow:hidden;font-size:13px;font-weight:500;line-height:20px;text-overflow:ellipsis;white-space:nowrap}.dssm-tag{flex:none;padding:1px 6px;border:1px solid var(--dsw-alias-border-l3);border-radius:4px;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:16px}.dssm-tag-on{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}.dssm-tag-off{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}.dssm-note{overflow:hidden;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;text-overflow:ellipsis;white-space:nowrap}.dssm-empty{padding:28px 16px;color:var(--dsw-alias-label-tertiary);font-size:13px;text-align:center}.dssm-error{color:var(--dsw-alias-state-error-primary);font-size:13px;line-height:20px}
195
223
  .dssm-mask{position:fixed;z-index:1100;inset:0;display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:100%;height:100%;padding:24px;background:rgba(0,0,0,.58)}.dssm-modal{box-sizing:border-box;display:flex;width:min(480px,100%)!important;height:auto!important;max-width:100%;max-height:100%;min-width:0;min-height:0;flex:0 0 auto!important;align-self:center!important;flex-direction:column;gap:18px;padding:24px;overflow:auto;border:1px solid var(--dsw-alias-border-l2);border-radius:16px;background:var(--dsw-alias-bg-layer-2);box-shadow:var(--dsw-shadow-lv3)}.dssm-modal-upload{width:min(480px,100%)!important;height:auto!important;max-width:100%;max-height:100%;align-self:center!important;flex:0 0 auto!important;overflow:auto}.dssm-modal-head{display:flex;align-items:center;gap:12px}.dssm-modal-title{margin:0;flex:1;font-size:17px;line-height:24px;font-weight:650}.dssm-icon-btn{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border:0;border-radius:7px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:20px;line-height:1;cursor:pointer}.dssm-icon-btn:hover{background:var(--dsw-alias-interactive-bg-hover)}
196
224
  .dssm-dropzone{box-sizing:border-box;display:flex;width:100%;min-height:144px;flex:none;flex-direction:column;align-items:center;justify-content:center;gap:8px;padding:18px;border:1px dashed var(--dsw-alias-border-l3);border-radius:12px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);cursor:pointer;transition:border-color 180ms ease,background 180ms ease}.dssm-dropzone:hover,.dssm-dropzone-active{border-color:var(--dsw-alias-state-success-primary);background:var(--dsw-alias-interactive-bg-hover)}.dssm-dropzone-title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:560}.dssm-dropzone-copy{font-size:12px;line-height:18px;text-align:center}.dssm-file{display:flex;align-items:center;gap:8px;padding:9px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;color:var(--dsw-alias-label-primary);font-size:13px}.dssm-file-name{overflow:hidden;flex:1;text-overflow:ellipsis;white-space:nowrap}.dssm-modal-head,.dssm-file,.dssm-modal-actions{flex:none}.dssm-modal-actions{display:flex;justify-content:flex-end;flex-wrap:wrap;gap:8px}.dssm-hidden-input{display:none}
197
- @media (max-width:560px){.dssm-mask{padding:12px;align-items:center}.dssm-modal{width:100%!important;height:auto!important;padding:16px;gap:12px;align-self:center!important}.dssm-modal-upload{width:100%!important;align-self:center!important}.dssm-toolbar{flex-wrap:wrap}.dssm-actions{margin-left:0}.dssm-path{display:none}.dssm-row{align-items:flex-start;flex-wrap:wrap}.dssm-row>.dssm-btn{margin-left:auto}.dssm-modal-actions{justify-content:stretch}.dssm-modal-actions .dssm-btn{flex:1 1 auto}}@media (prefers-reduced-motion:reduce){.dssm-btn,.dssm-dropzone{transition:none}}
225
+ .dssm-filters{display:flex;align-items:flex-end;gap:10px}.dssm-field{display:flex;min-width:0;flex:1;flex-direction:column;gap:6px}.dssm-field-category{flex:0 1 220px}.dssm-field-search{flex:1 1 240px}.dssm-label{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:16px}.dssm-control{box-sizing:border-box;width:100%;min-height:32px;padding:0 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px}.dssm-control:focus-visible{outline:2px solid var(--dsw-alias-state-success-primary);outline-offset:2px}.dssm-select{position:relative}.dssm-select-trigger{box-sizing:border-box;display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;min-height:32px;padding:0 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;line-height:20px;text-align:left;cursor:pointer}.dssm-select-trigger:hover{background:var(--dsw-alias-interactive-bg-hover)}.dssm-select-trigger:focus-visible{outline:2px solid var(--dsw-alias-state-success-primary);outline-offset:2px}.dssm-select-trigger[aria-expanded="true"]{border-color:var(--dsw-alias-state-success-primary)}.dssm-select-value{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dssm-select-caret{flex:none;width:12px;height:12px;color:var(--dsw-alias-label-tertiary)}.dssm-select-menu{position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:30;box-sizing:border-box;max-height:280px;overflow:auto;padding:4px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-specific-menu,var(--dsw-alias-bg-layer-2));box-shadow:var(--dsw-shadow-lv3)}.dssm-select-option{box-sizing:border-box;display:flex;align-items:center;width:100%;min-height:32px;padding:0 10px;border:0;border-radius:8px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;line-height:20px;text-align:left;cursor:pointer}.dssm-select-option:hover,.dssm-select-option[data-active="true"]{background:var(--dsw-alias-interactive-bg-hover)}.dssm-select-option[aria-selected="true"]{color:var(--dsw-alias-state-success-primary)}.dssm-search-wrap{position:relative;display:flex;align-items:center}.dssm-search{padding-right:32px}.dssm-search::-webkit-search-cancel-button,.dssm-search::-webkit-search-decoration{-webkit-appearance:none;appearance:none}.dssm-search-clear{position:absolute;right:4px;display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-tertiary);font:inherit;font-size:16px;line-height:1;cursor:pointer}.dssm-search-clear:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.dssm-search-clear:focus-visible{outline:2px solid var(--dsw-alias-state-success-primary);outline-offset:2px}.dssm-empty-filter{display:flex;flex-direction:column;align-items:center;gap:12px;padding:28px 16px;border:1px dashed var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px;text-align:center}.dssm-filter-meta{margin-left:auto;color:var(--dsw-alias-label-tertiary);font-size:12px}
226
+ @media (max-width:560px){.dssm-mask{padding:12px;align-items:center}.dssm-modal{width:100%!important;height:auto!important;padding:16px;gap:12px;align-self:center!important}.dssm-modal-upload{width:100%!important;align-self:center!important}.dssm-toolbar{flex-wrap:wrap}.dssm-actions{margin-left:0}.dssm-path{display:none}.dssm-row{align-items:flex-start;flex-wrap:wrap}.dssm-row>.dssm-btn{margin-left:auto}.dssm-filters{flex-direction:column;align-items:stretch}.dssm-field-category,.dssm-field-search{flex:none}.dssm-filter-meta{margin-left:0}.dssm-modal-actions{justify-content:stretch}.dssm-modal-actions .dssm-btn{flex:1 1 auto}}@media (prefers-reduced-motion:reduce){.dssm-btn,.dssm-dropzone{transition:none}}
198
227
  `;
199
228
 
200
229
  function callApi(path, options) {
@@ -250,6 +279,50 @@ window.__ModuleLoader__.load({
250
279
  return skill.invocationPolicyValid && skill.modelInvocable && skill.userInvocable;
251
280
  }
252
281
 
282
+ /** 规范化检索词:去首尾空白并转小写,便于名称、形态和简介统一匹配。 */
283
+ function normalizeSkillQuery(query) {
284
+ return String(query == null ? "" : query).trim().toLowerCase();
285
+ }
286
+
287
+ /** 按名称、简介、形态或分类标签做包含匹配;空检索视为全部命中。 */
288
+ function matchSkillQuery(skill, query) {
289
+ var q = normalizeSkillQuery(query);
290
+ if (q === "") return true;
291
+ return [
292
+ skill.name,
293
+ skill.description,
294
+ skill.kind,
295
+ skill.kindLabel,
296
+ skill.statusLabel,
297
+ skill.rootKey,
298
+ skill.rootLabel,
299
+ ].some(function (field) {
300
+ return String(field == null ? "" : field).toLowerCase().includes(q);
301
+ });
302
+ }
303
+
304
+ /** 先按目录分类收窄,再按检索词过滤。rootKey 为空表示全部分类。 */
305
+ function filterSkills(list, options) {
306
+ var query = options && options.query;
307
+ var rootKey = options && options.rootKey != null ? options.rootKey : "";
308
+ return list.filter(function (skill) {
309
+ return (rootKey === "" || skill.rootKey === rootKey) && matchSkillQuery(skill, query);
310
+ });
311
+ }
312
+
313
+ function skillSearchMeta(skill, root, t) {
314
+ var enabled = isSkillEnabled(skill);
315
+ return {
316
+ name: skill.name,
317
+ description: skill.description,
318
+ kind: skill.kind,
319
+ kindLabel: t(skill.kind === "bundle" ? "status.bundle" : "status.single"),
320
+ statusLabel: skill.invocationPolicyValid ? (enabled ? t("status.enabled") : t("status.disabled")) : t("status.invalid"),
321
+ rootKey: root.key,
322
+ rootLabel: translateOrFallback(t, "root." + root.key, root.label),
323
+ };
324
+ }
325
+
253
326
  function modalFocusable(modal) {
254
327
  return Array.prototype.slice.call(modal.querySelectorAll("button:not(:disabled), [href], input:not([type=hidden]):not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex=\"-1\"])"));
255
328
  }
@@ -265,6 +338,122 @@ window.__ModuleLoader__.load({
265
338
  }
266
339
  }
267
340
 
341
+ function CategorySelect(props) {
342
+ var openState = react.useState(false);
343
+ var open = openState[0];
344
+ var setOpen = openState[1];
345
+ var selectedIndex = 0;
346
+ for (var i = 0; i < props.options.length; i++) {
347
+ if (props.options[i].value === props.value) { selectedIndex = i; break; }
348
+ }
349
+ var activeState = react.useState(selectedIndex);
350
+ var active = activeState[0];
351
+ var setActive = activeState[1];
352
+ var rootRef = react.useRef(null);
353
+ var triggerRef = react.useRef(null);
354
+ var listRef = react.useRef(null);
355
+ var wasOpen = react.useRef(false);
356
+ var selected = props.options[selectedIndex];
357
+
358
+ react.useEffect(function () {
359
+ if (!open) return undefined;
360
+ setActive(selectedIndex);
361
+ function onPointerDown(event) {
362
+ var node = rootRef.current;
363
+ if (node && event.target && node.contains(event.target)) return;
364
+ setOpen(false);
365
+ }
366
+ document.addEventListener("pointerdown", onPointerDown);
367
+ return function () { document.removeEventListener("pointerdown", onPointerDown); };
368
+ }, [open, selectedIndex]);
369
+
370
+ react.useEffect(function () {
371
+ if (open) {
372
+ if (listRef.current) listRef.current.focus();
373
+ wasOpen.current = true;
374
+ return;
375
+ }
376
+ if (wasOpen.current) {
377
+ if (triggerRef.current) triggerRef.current.focus();
378
+ wasOpen.current = false;
379
+ }
380
+ }, [open]);
381
+
382
+ react.useEffect(function () {
383
+ if (!open) return;
384
+ var option = document.getElementById(props.id + "-opt-" + String(active));
385
+ if (option) option.scrollIntoView({ block: "nearest" });
386
+ }, [active, open, props.id]);
387
+
388
+ function choose(value) {
389
+ props.onChange(value);
390
+ setOpen(false);
391
+ }
392
+
393
+ function move(next) {
394
+ if (!props.options.length) return;
395
+ setActive(Math.min(props.options.length - 1, Math.max(0, next)));
396
+ }
397
+
398
+ function onTriggerKeyDown(event) {
399
+ if (event.key === "ArrowDown" || event.key === "ArrowUp" || event.key === "Enter" || event.key === " ") {
400
+ event.preventDefault();
401
+ setOpen(true);
402
+ }
403
+ }
404
+
405
+ function onListKeyDown(event) {
406
+ if (event.key === "ArrowDown") { event.preventDefault(); move(active + 1); return; }
407
+ if (event.key === "ArrowUp") { event.preventDefault(); move(active - 1); return; }
408
+ if (event.key === "Home") { event.preventDefault(); move(0); return; }
409
+ if (event.key === "End") { event.preventDefault(); move(props.options.length - 1); return; }
410
+ if (event.key === "Enter" || event.key === " ") {
411
+ event.preventDefault();
412
+ var option = props.options[active];
413
+ if (option) choose(option.value);
414
+ return;
415
+ }
416
+ if (event.key === "Escape" || event.key === "Tab") setOpen(false);
417
+ }
418
+
419
+ return h("div", { className: "dssm-select", ref: rootRef },
420
+ h("button", {
421
+ id: props.id,
422
+ ref: triggerRef,
423
+ type: "button",
424
+ className: "dssm-select-trigger",
425
+ "aria-haspopup": "listbox",
426
+ "aria-expanded": open,
427
+ "aria-controls": props.id + "-list",
428
+ onClick: function () { setOpen(function (current) { return !current; }); },
429
+ onKeyDown: onTriggerKeyDown,
430
+ },
431
+ h("span", { className: "dssm-select-value" }, selected ? selected.label : ""),
432
+ h("svg", { className: "dssm-select-caret", viewBox: "0 0 12 12", "aria-hidden": true, focusable: false },
433
+ h("path", { d: "M2.5 4.5L6 8l3.5-3.5", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }))),
434
+ open ? h("div", {
435
+ id: props.id + "-list",
436
+ ref: listRef,
437
+ className: "dssm-select-menu",
438
+ role: "listbox",
439
+ tabIndex: 0,
440
+ "aria-activedescendant": props.id + "-opt-" + String(active),
441
+ onKeyDown: onListKeyDown,
442
+ }, props.options.map(function (option, index) {
443
+ return h("button", {
444
+ key: option.value === "" ? "all" : option.value,
445
+ id: props.id + "-opt-" + String(index),
446
+ type: "button",
447
+ role: "option",
448
+ className: "dssm-select-option",
449
+ "aria-selected": option.value === props.value,
450
+ "data-active": index === active,
451
+ onMouseEnter: function () { setActive(index); },
452
+ onClick: function () { choose(option.value); },
453
+ }, option.label);
454
+ })) : null);
455
+ }
456
+
268
457
  // 渲染机制按 register 的 locale 字段把命名空间绑定的 t 注入组件 props。
269
458
  function SkillManagerSection(props) {
270
459
  var t = props.t;
@@ -289,6 +478,12 @@ window.__ModuleLoader__.load({
289
478
  var deleteState = react.useState(null);
290
479
  var confirmDelete = deleteState[0];
291
480
  var setConfirmDelete = deleteState[1];
481
+ var queryState = react.useState("");
482
+ var query = queryState[0];
483
+ var setQuery = queryState[1];
484
+ var categoryState = react.useState("");
485
+ var category = categoryState[0];
486
+ var setCategory = categoryState[1];
292
487
  var inputRef = react.useRef(null);
293
488
  var modalRef = react.useRef(null);
294
489
 
@@ -435,15 +630,65 @@ window.__ModuleLoader__.load({
435
630
  h("div", { className: "dssm-actions" },
436
631
  h("button", { className: "dssm-btn dssm-btn-secondary", disabled: !!busy, onClick: refresh }, t("btn.refresh")),
437
632
  h("button", { className: "dssm-btn", disabled: !!busy, onClick: function () { setImportResult(null); setUploadOpen(true); } }, t("btn.upload")))));
633
+ var searchable = roots.reduce(function (all, root) {
634
+ return all.concat((root.skills || []).map(function (skill) { return skillSearchMeta(skill, root, t); }));
635
+ }, []);
636
+ var filtered = filterSkills(searchable, { query: query, rootKey: category });
637
+ var filteredCount = filtered.length;
638
+ var hasQuery = normalizeSkillQuery(query) !== "";
639
+ var hasFilter = hasQuery || category !== "";
640
+ var resetFilters = function () { setQuery(""); setCategory(""); };
438
641
  // 复数按 en 的 one/other 二元规则选择;zh/en 均适用,扩展多复数语言时需改 CLDR 规则。
439
642
  nodes.push(h("div", { key: "summary", className: "dssm-summary" },
440
- 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 }))));
643
+ 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 })),
644
+ hasFilter ? h("span", { className: "dssm-filter-meta", "aria-live": "polite" }, t(filteredCount === 1 ? "filter.showing.one" : "filter.showing.other", { count: filteredCount })) : null));
645
+ nodes.push(h("div", { key: "filters", className: "dssm-filters" },
646
+ h("div", { className: "dssm-field dssm-field-category" },
647
+ h("label", { className: "dssm-label", htmlFor: "dssm-filter-category" }, t("filter.category")),
648
+ h(CategorySelect, {
649
+ id: "dssm-filter-category",
650
+ value: category,
651
+ onChange: setCategory,
652
+ options: [{ value: "", label: t("filter.option", { name: t("filter.all"), count: skills.length }) }].concat(roots.map(function (root) {
653
+ return { value: root.key, label: t("filter.option", { name: translateOrFallback(t, "root." + root.key, root.label), count: (root.skills || []).length }) };
654
+ })),
655
+ })),
656
+ h("div", { className: "dssm-field dssm-field-search" },
657
+ h("label", { className: "dssm-label", htmlFor: "dssm-filter-search" }, t("search")),
658
+ h("div", { className: "dssm-search-wrap" },
659
+ h("input", {
660
+ id: "dssm-filter-search",
661
+ className: "dssm-control dssm-search",
662
+ type: "search",
663
+ value: query,
664
+ autoComplete: "off",
665
+ spellCheck: false,
666
+ placeholder: t("search.placeholder"),
667
+ onChange: function (event) { setQuery(event.currentTarget.value); },
668
+ }),
669
+ query !== "" ? h("button", { type: "button", className: "dssm-search-clear", "aria-label": t("search.clear"), onClick: function () { setQuery(""); } }, "×") : null))));
670
+ var visibleRoots = [];
441
671
  roots.forEach(function (root) {
442
- var groupSkills = root.skills || [];
443
- nodes.push(h("div", { key: root.key, className: "dssm-group" },
444
- 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)),
445
- 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")))));
672
+ if (category !== "" && root.key !== category) return;
673
+ var groupSkills = (root.skills || []).filter(function (skill) {
674
+ return matchSkillQuery(skillSearchMeta(skill, root, t), query);
675
+ });
676
+ if (hasQuery && groupSkills.length === 0) return;
677
+ visibleRoots.push({ root: root, groupSkills: groupSkills });
446
678
  });
679
+ if (hasQuery && visibleRoots.length === 0) {
680
+ nodes.push(h("div", { key: "empty-search", className: "dssm-empty-filter" },
681
+ h("div", null, t("empty.search", { all: t("filter.all") })),
682
+ h("button", { type: "button", className: "dssm-btn dssm-btn-secondary", onClick: resetFilters }, t("empty.search.reset"))));
683
+ } else {
684
+ visibleRoots.forEach(function (item) {
685
+ var root = item.root;
686
+ var groupSkills = item.groupSkills;
687
+ nodes.push(h("div", { key: root.key, className: "dssm-group" },
688
+ 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)),
689
+ 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")))));
690
+ });
691
+ }
447
692
  } else if (snapshot.loading) {
448
693
  nodes.push(h("div", { key: "loading", className: "dssm-note" }, t("loading")));
449
694
  }
@@ -457,6 +702,13 @@ window.__ModuleLoader__.load({
457
702
  var failedItems = importResult.failed || (importResult.error && importResult.error.failed) || [];
458
703
  var skippedNames = (importResult.skipped || []).map(function (item) { return item.name; });
459
704
  var failedText = failedItems.map(function (item) { return translateError(t, item); }).join(errorSeparator);
705
+ var warningItems = [];
706
+ (importResult.imported || []).forEach(function (item) {
707
+ if (item.warnings && item.warnings.length) {
708
+ for (var i = 0; i < item.warnings.length; i++) warningItems.push(item.warnings[i]);
709
+ }
710
+ });
711
+ var warningText = warningItems.map(function (item) { return translateError(t, item); }).join(errorSeparator);
460
712
  var resultText;
461
713
  if (isImportError) {
462
714
  // 顶层聚合错误优先展示已翻译的失败明细;无明细时由 translateError 兜底(含 Error 对象)。
@@ -470,6 +722,7 @@ window.__ModuleLoader__.load({
470
722
  } else {
471
723
  resultText = t("result.none");
472
724
  }
725
+ if (warningText) resultText = resultText + errorSeparator + t("result.warnings", { warnings: warningText });
473
726
  nodes.push(h("div", { key: "import-result", className: isImportError || failedItems.length ? "dssm-error" : "dssm-note", role: "status" }, resultText));
474
727
  }
475
728
  if (uploadOpen) {
@@ -506,8 +759,16 @@ window.__ModuleLoader__.load({
506
759
  module.exports.DICT = DICT;
507
760
  module.exports.translateError = translateError;
508
761
  module.exports.isSkillEnabled = isSkillEnabled;
762
+ module.exports.normalizeSkillQuery = normalizeSkillQuery;
763
+ module.exports.matchSkillQuery = matchSkillQuery;
764
+ module.exports.filterSkills = filterSkills;
509
765
  module.exports.parseApiResponse = parseApiResponse;
510
766
  module.exports.trapModalFocus = trapModalFocus;
511
767
  return module.exports;
512
768
  }
513
769
  });
770
+
771
+
772
+
773
+
774
+
package/lib/core.js CHANGED
@@ -426,14 +426,16 @@ async function replaceWithCopy(source, dest, isDir, existing = []) {
426
426
  try {
427
427
  await fs.rename(item.backup, item.path);
428
428
  } catch (rollbackError) {
429
- rollbackFailures.push(`${item.backup}(${String(rollbackError && rollbackError.message ? rollbackError.message : rollbackError)})`);
429
+ rollbackFailures.push(item.backup);
430
430
  }
431
431
  }
432
432
  if (rollbackFailures.length) {
433
- const restoreError = new Error(`${String(error && error.message ? error.message : error)};覆盖导入回滚失败,备份保留在: ${rollbackFailures.join("、")}`);
434
- if (error && error.code) restoreError.code = error.code;
435
- if (error && error.params) restoreError.params = error.params;
436
- throw restoreError;
433
+ const causeText = String(error && error.message ? error.message : error);
434
+ throw codedError(
435
+ `${causeText};覆盖导入回滚失败,备份保留在: ${rollbackFailures.join("、")}`,
436
+ "error.import.rollbackFailed",
437
+ { path: rollbackFailures.join("; "), error: causeText },
438
+ );
437
439
  }
438
440
  throw error;
439
441
  }
@@ -442,7 +444,11 @@ async function replaceWithCopy(source, dest, isDir, existing = []) {
442
444
  try {
443
445
  await fs.rm(item.backup, { recursive: true, force: true });
444
446
  } catch (error) {
445
- warnings.push(`旧版本备份未清理: ${item.backup}(${String(error && error.message ? error.message : error)})`);
447
+ warnings.push({
448
+ code: "warning.backupUncleaned",
449
+ params: { path: item.backup, error: String(error && error.message ? error.message : error) },
450
+ error: `旧版本备份未清理: ${item.backup}(${String(error && error.message ? error.message : error)})`,
451
+ });
446
452
  }
447
453
  }
448
454
  return warnings;
package/lib/index.js CHANGED
@@ -76,7 +76,7 @@ function readBody(req, limit = 1 << 20) {
76
76
  /** 只允许 loopback Host,避免 DNS rebinding 读取 /state 或改写本地技能。 */
77
77
  function validateLoopbackHost(req) {
78
78
  const host = String(req.headers.host || "").toLowerCase();
79
- if (!/^(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/.test(host)) return { statusCode: 403, code: "error.proto.forbidden", error: "forbidden host" };
79
+ if (!/^(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/.test(host)) return { statusCode: 403, code: "error.proto.forbiddenHost", error: "forbidden host" };
80
80
  return null;
81
81
  }
82
82
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michengai/dsh-skills-manager",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
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": {