@michengai/dsh-skills-manager 0.1.33 → 0.1.35

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/CHANGELOG.md CHANGED
@@ -6,6 +6,24 @@ The five most recent published versions are listed below.
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## 0.1.35 - 2026-09-03
10
+
11
+ - Aligned Settings header actions with the archive plugin: a GitHub project link and an Issues feedback link, with matching icons, accessible labels, responsive layout, and localized copy.
12
+ - Updated the DSH compatibility contract to `>=0.1.0-rc.5 <0.2.0`, added the runtime injection peers used by the plugin, and tested development against DSH `0.1.2-alpha.5`.
13
+ - Allowed the publish workflow to install the intentionally pinned DSH alpha dependencies without pnpm's release-age policy blocking the release validation.
14
+
15
+ Published package: [`@michengai/dsh-skills-manager@0.1.35`](https://www.npmjs.com/package/@michengai/dsh-skills-manager/v/0.1.35).
16
+
17
+ ## 0.1.34 — 2026-09-02
18
+
19
+ - Made direct Skill entries the canonical owner when the same real bundle is also exposed through Agent distribution links. The canonical provider candidate inherits the strongest distribution rank, so disabling CC Switch cannot fall back to a linked public-Agent copy.
20
+ - Required discovered real bundle and document paths for both user and project Provider candidates, revalidated both paths at load time, and aligned policy toggles with real-document reads.
21
+ - Scanned user roots concurrently and reused one resolved set of trusted read-only roots per scan without caching external filesystem state across requests.
22
+ - Preserved a per-Skill disable while its current distribution entry changes ownership to a canonical source. A canonical source's explicit policy still takes precedence over inherited distribution-link policies.
23
+ - Made detail visibility reproduce the same user-root deduplication as state snapshots from the requested entry's physical owner, avoiding unrelated Skill document reads; entry resolution, detail reads, and Provider reads fail closed when files disappear during a filesystem race.
24
+
25
+ Published package: [`@michengai/dsh-skills-manager@0.1.34`](https://www.npmjs.com/package/@michengai/dsh-skills-manager/v/0.1.34).
26
+
9
27
  ## 0.1.33 — 2026-09-02
10
28
 
11
29
  - Added an enabled-by-default, read-only `CC Switch` source at `~\.cc-switch\skills` with source-wide and per-Skill local policy toggles. Legacy state files gain the new source without failing closed.
@@ -6,6 +6,24 @@
6
6
 
7
7
  ## 未发布
8
8
 
9
+ ## 0.1.35 - 2026-09-03
10
+
11
+ - 设置页标题操作与归档插件统一:新增 GitHub 项目链接和 Issues 问题反馈链接,图标、无障碍标签、响应式布局及中英文文案保持一致。
12
+ - DSH 兼容范围统一为 `>=0.1.0-rc.5 <0.2.0`,补齐插件运行时使用的注入 peer,并以 DSH `0.1.2-alpha.5` 作为开发测试版本。
13
+ - 发布工作流安装有意固定的 DSH alpha 依赖时,显式关闭 pnpm 最小发布时间策略,避免发布校验被该策略阻断。
14
+
15
+ 发布包:[`@michengai/dsh-skills-manager@0.1.35`](https://www.npmjs.com/package/@michengai/dsh-skills-manager/v/0.1.35)。
16
+
17
+ ## 0.1.34 — 2026-09-02
18
+
19
+ - 同一真实 Skill 同时存在直接条目和 Agent 分发链接时,改为由直接条目归属;规范候选继承所有分发入口中的最强 rank,停用 CC Switch 后不会回流到公共 Agent 链接副本。
20
+ - 用户级与项目级 Provider 候选必须携带扫描时发现的真实 bundle 和文档路径,加载时同时复验;启停读取也统一使用真实文档路径。
21
+ - 用户根改为并发扫描,并在单次扫描内复用已解析的受信只读根;不跨请求缓存外部文件系统状态。
22
+ - 当前分发条目归属迁移到规范来源时,保留原来源的逐 Skill 停用;规范来源自身的显式策略优先于继承的分发链接策略。
23
+ - 详情接口按被请求条目的物理所有者复现状态快照的用户根去重结果,避免读取无关 Skill 文档;文件在扫描后消失时,条目解析、详情读取和 Provider 读取均失败关闭。
24
+
25
+ 发布包:[`@michengai/dsh-skills-manager@0.1.34`](https://www.npmjs.com/package/@michengai/dsh-skills-manager/v/0.1.34)。
26
+
9
27
  ## 0.1.33 — 2026-09-02
10
28
 
11
29
  - 新增默认开启的只读 `CC Switch` 来源,固定读取 `~\.cc-switch\skills`,支持来源级与逐 Skill 本地启停;旧状态文件自动补齐新来源,不会因缺少新键失败关闭。
package/lib/client.js CHANGED
@@ -6,12 +6,13 @@ window.__ModuleLoader__.load({
6
6
  Object.defineProperty(module.exports, Symbol.toStringTag, { value: "Module" });
7
7
  var react = require("react");
8
8
  var h = react.createElement;
9
+ var primitives = require("@deepseek-ai/dsh-client-ui-primitives");
9
10
  var MUTATION_HEADERS = { "content-type": "application/json", "x-dsh-skills-manager": "1" };
10
11
  var NS = "skills-manager";
11
12
 
12
13
  var DICT = {
13
14
  zh: {
14
- "title": "技能", "desc": "统一加载和管理本机 Agent Skills。",
15
+ "title": "技能", "desc": "统一加载和管理本机 Agent Skills。", "link.project": "GitHub", "link.feedback": "问题反馈",
15
16
  "btn.create": "创建技能", "btn.import": "导入", "btn.refresh": "刷新", "btn.cancel": "取消", "btn.close": "关闭", "btn.detail": "查看详情", "btn.trash": "移到回收站", "btn.restore": "恢复", "btn.delete.forever": "永久删除", "btn.file.pick": "选择文件", "btn.folder.pick": "选择文件夹", "btn.import.now": "安装", "btn.create.now": "创建技能", "btn.disable": "停用", "btn.enable": "启用",
16
17
  "status.enabled": "已启用", "status.disabled": "已停用", "status.invalid": "诊断异常", "status.shadowed": "被覆盖", "status.readonly": "源文件只读", "status.manageable": "可管理", "status.project": "项目级", "status.rank": "优先级 {rank}", "status.source.on": "已启用", "status.source.off": "已停用", "status.bundle": "目录技能", "status.single": "单文件",
17
18
  "summary.total.one": "{count} 个技能", "summary.total.other": "{count} 个技能", "summary.enabled.one": "{count} 个已启用", "summary.enabled.other": "{count} 个已启用", "summary.disabled.one": "{count} 个已停用", "summary.disabled.other": "{count} 个已停用", "summary.issues.one": "{count} 个诊断项", "summary.issues.other": "{count} 个诊断项", "summary.group.one": "{count} 个技能", "summary.group.other": "{count} 个技能", "table.skill": "技能名称与描述", "table.status": "调用状态",
@@ -38,7 +39,7 @@ window.__ModuleLoader__.load({
38
39
  "root.dsh": "DSH 技能", "root.agents": "公共 Agent", "root.ccswitch": "CC Switch", "root.projectDsh": "项目 DSH", "root.projectAgents": "项目 Agent", "root.codex": "Codex", "root.claude": "Claude", "root.gemini": "Gemini", "root.opencode": "OpenCode"
39
40
  },
40
41
  en: {
41
- "title": "Skills", "desc": "Load and manage Agent Skills on this computer in one place.",
42
+ "title": "Skills", "desc": "Load and manage Agent Skills on this computer in one place.", "link.project": "GitHub", "link.feedback": "Issues",
42
43
  "btn.create": "Create skill", "btn.import": "Import", "btn.refresh": "Refresh", "btn.cancel": "Cancel", "btn.close": "Close", "btn.detail": "View details", "btn.trash": "Move to trash", "btn.restore": "Restore", "btn.delete.forever": "Delete forever", "btn.file.pick": "Choose file", "btn.folder.pick": "Choose folder", "btn.import.now": "Install", "btn.create.now": "Create skill", "btn.disable": "Disable", "btn.enable": "Enable",
43
44
  "status.enabled": "Enabled", "status.disabled": "Disabled", "status.invalid": "Needs attention", "status.shadowed": "Shadowed", "status.readonly": "Source read-only", "status.manageable": "Manageable", "status.project": "Project scoped", "status.rank": "Rank {rank}", "status.source.on": "Enabled", "status.source.off": "Disabled", "status.bundle": "Bundle", "status.single": "Single file",
44
45
  "summary.total.one": "{count} skill", "summary.total.other": "{count} skills", "summary.enabled.one": "{count} enabled", "summary.enabled.other": "{count} enabled", "summary.disabled.one": "{count} disabled", "summary.disabled.other": "{count} disabled", "summary.issues.one": "{count} diagnostic", "summary.issues.other": "{count} diagnostics", "summary.group.one": "{count} skill", "summary.group.other": "{count} skills", "table.skill": "Skill name and description", "table.status": "Invocation status",
@@ -67,7 +68,7 @@ window.__ModuleLoader__.load({
67
68
  };
68
69
 
69
70
  var CSS = `
70
- .dssm-section{box-sizing:border-box;display:flex;width:100%;max-width:820px;min-width:0;margin:0 auto;padding:2px 0 36px;container-type:inline-size;flex-direction:column;gap:14px;color:var(--dsw-alias-label-primary);font-family:inherit}.dssm-head{display:flex;align-items:flex-start;gap:20px}.dssm-title{margin:0;font-size:22px;line-height:30px;font-weight:680;letter-spacing:-.3px}.dssm-desc{margin:3px 0 0;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:20px}.dssm-actions{display:flex;gap:8px;margin-left:auto;flex:none}
71
+ .dssm-section{box-sizing:border-box;display:flex;width:100%;max-width:820px;min-width:0;margin:0 auto;padding:2px 0 36px;container-type:inline-size;flex-direction:column;gap:14px;color:var(--dsw-alias-label-primary);font-family:inherit}.dssm-head{display:flex;align-items:flex-start;gap:20px}.dssm-title-block{min-width:0}.dssm-title-row{display:flex;align-items:center;gap:8px;min-width:0}.dssm-feedback-links{display:flex;align-items:center;gap:4px}.dssm-title{margin:0;font-size:22px;line-height:30px;font-weight:680;letter-spacing:-.3px}.dssm-feedback-link{display:inline-flex;min-height:28px;align-items:center;gap:5px;padding:0 8px;border:1px solid var(--dsw-alias-border-l2);border-radius:7px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;font-weight:500;line-height:18px;text-decoration:none;white-space:nowrap}.dssm-feedback-link:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.dssm-feedback-link:focus-visible{outline:2px solid var(--dsw-alias-state-success-primary);outline-offset:2px}.dssm-feedback-link svg{flex:none}.dssm-desc{margin:3px 0 0;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:20px}.dssm-actions{display:flex;gap:8px;margin-left:auto;flex:none}
71
72
  .dssm-btn{box-sizing:border-box;display:inline-flex;min-height:34px;align-items:center;justify-content:center;padding:0 13px;border:1px solid transparent;border-radius:8px;background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);font:inherit;font-size:13px;font-weight:580;white-space:nowrap;cursor:pointer}.dssm-btn:hover:not(:disabled){filter:brightness(1.08)}.dssm-btn:disabled{opacity:.48;cursor:default}.dssm-btn-secondary,.dssm-btn-quiet{border-color:var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-primary)}.dssm-btn-quiet{min-height:28px;padding:0 9px;color:var(--dsw-alias-label-secondary);font-size:12px}.dssm-btn-danger{border-color:var(--dsw-alias-state-error-primary);background:transparent;color:var(--dsw-alias-state-error-primary)}.dssm-btn:focus-visible,.dssm-control:focus-visible,.dssm-select-trigger:focus-visible,.dssm-source-head:focus-visible,.dssm-switch:focus-visible,.dssm-upload-link:focus-visible,.dssm-file-remove:focus-visible{outline:2px solid var(--dsw-alias-state-success-primary);outline-offset:2px}
72
73
  .dssm-summary{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2)}.dssm-stat{padding:12px 14px;border-right:1px solid var(--dsw-alias-border-l1);font-size:13px;color:var(--dsw-alias-label-secondary)}.dssm-stat:last-child{border-right:0}.dssm-stat strong{margin-right:5px;color:var(--dsw-alias-label-primary);font-size:17px;font-weight:680}.dssm-filters{display:flex;gap:9px}.dssm-search{flex:1}.dssm-source-filter{width:210px;flex:none}
73
74
  .dssm-control,.dssm-select-trigger{box-sizing:border-box;width:100%;min-height:34px;padding:0 11px;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::placeholder{color:var(--dsw-alias-label-tertiary)}textarea.dssm-control{min-height:160px;padding-top:9px;resize:vertical;line-height:20px}.dssm-select{position:relative}.dssm-select-trigger{display:flex;align-items:center;justify-content:space-between;text-align:left;cursor:pointer}.dssm-select-menu{position:absolute;z-index:40;top:calc(100% + 5px);right:0;left:0;display:flex;max-height:260px;padding:5px;overflow:auto;flex-direction:column;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-bg-layer-3);box-shadow:var(--dsw-shadow-lv2)}.dssm-option{padding:8px;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;text-align:left;cursor:pointer}.dssm-option:hover,.dssm-option[aria-selected=true]{background:var(--dsw-alias-interactive-bg-hover)}
@@ -75,7 +76,7 @@ window.__ModuleLoader__.load({
75
76
  .dssm-table-head,.dssm-row{display:grid;grid-template-columns:minmax(180px,1fr) 120px 90px max-content;align-items:center;column-gap:12px;padding:0 13px}.dssm-table-head{min-height:32px;border-bottom:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-tertiary);font-size:11px}.dssm-row{min-height:58px;border-bottom:1px solid var(--dsw-alias-border-l1)}.dssm-row:last-child{border-bottom:0}.dssm-main{min-width:0}.dssm-name{overflow:hidden;font-size:13px;font-weight:570;text-overflow:ellipsis;white-space:nowrap}.dssm-note{overflow:hidden;margin-top:2px;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:17px;text-overflow:ellipsis;white-space:nowrap}.dssm-tags{display:flex;align-items:center;gap:5px;flex-wrap:wrap}.dssm-tag{display:inline-flex;min-height:19px;align-items:center;padding:0 6px;border:1px solid var(--dsw-alias-border-l3);border-radius:4px;color:var(--dsw-alias-label-secondary);font-size:10px;white-space:nowrap}.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-enabled{color:var(--dsw-alias-state-success-primary);font-size:12px;white-space:nowrap}.dssm-disabled{color:#d49245;font-size:12px;white-space:nowrap}.dssm-shadowed{color:var(--dsw-alias-label-tertiary);font-size:12px;white-space:nowrap}.dssm-row-actions{display:flex;align-items:center;justify-content:flex-end;gap:7px}
76
77
  .dssm-switch{position:relative;width:34px;height:20px;flex:none;padding:0;border:0;border-radius:999px;background:var(--dsw-alias-border-l3);cursor:pointer}.dssm-switch:after{position:absolute;top:3px;left:3px;width:14px;height:14px;border-radius:50%;background:#fff;content:"";transition:transform 160ms ease}.dssm-switch-on{background:var(--dsw-alias-state-success-primary)}.dssm-switch-on:after{transform:translateX(14px)}.dssm-switch:disabled{opacity:.45;cursor:default}.dssm-trash-row{display:flex;min-height:48px;align-items:center;padding:0 13px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2);color:inherit;font:inherit;font-size:13px;cursor:pointer}.dssm-trash-count{margin-left:auto;padding:2px 7px;border-radius:99px;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary);font-size:11px}.dssm-empty{padding:25px 14px;color:var(--dsw-alias-label-tertiary);font-size:12px;text-align:center}.dssm-feedback{padding:9px 11px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;color:var(--dsw-alias-label-secondary);font-size:12px}.dssm-warning{border-color:#d49245;color:#d49245}.dssm-error{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}
77
78
  .dssm-mask{position:fixed;z-index:1100;inset:0;display:flex;align-items:center;justify-content:center;padding:24px;background:rgba(0,0,0,.62)}.dssm-modal{box-sizing:border-box;display:flex;width:min(560px,100%)!important;max-height:min(760px,calc(100vh - 48px));min-width:0;flex-direction:column;gap:16px;padding:22px;overflow:auto;border:1px solid var(--dsw-alias-border-l2);border-radius:14px;background:var(--dsw-alias-bg-layer-2);box-shadow:var(--dsw-shadow-lv3)}.dssm-modal-wide{width:min(720px,100%)!important}.dssm-modal-import{width:min(480px,100%)!important;padding:24px}.dssm-modal-head{display:flex;align-items:flex-start;gap:12px}.dssm-modal-title{margin:0;flex:1;font-size:17px;line-height:24px;font-weight:670}.dssm-form,.dssm-field,.dssm-detail-section{display:flex;flex-direction:column}.dssm-form{gap:12px}.dssm-field{gap:6px}.dssm-label,.dssm-detail-title{font-size:12px}.dssm-label{color:var(--dsw-alias-label-secondary)}.dssm-detail-title{font-weight:650}.dssm-help{margin:0;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:18px}.dssm-modal-actions{display:flex;justify-content:flex-end;flex-wrap:wrap;gap:8px}.dssm-hidden-input{display:none}.dssm-dropzone{box-sizing:border-box;display:flex;width:100%;min-height:170px;flex-direction:column;align-items:center;justify-content:center;gap:7px;padding:22px;border:1px dashed var(--dsw-alias-border-l3);border-radius:12px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);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:620}.dssm-dropzone-copy{font-size:12px;line-height:18px;text-align:center}.dssm-upload-choices{display:flex;align-items:center;gap:7px}.dssm-upload-link,.dssm-file-remove{padding:0;border:0;background:transparent;font:inherit;font-size:12px;cursor:pointer}.dssm-upload-link{color:var(--dsw-alias-label-secondary)}.dssm-upload-link:hover{color:var(--dsw-alias-label-primary);text-decoration:underline}.dssm-upload-link:disabled{opacity:.45;cursor:default}.dssm-upload-divider{color:var(--dsw-alias-label-tertiary);font-size:11px}.dssm-file{display:flex;align-items:center;gap:9px;padding:10px 11px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font-size:12px}.dssm-file-kind{display:inline-flex;min-width:30px;height:24px;align-items:center;justify-content:center;border-radius:5px;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary);font-size:9px;font-weight:700}.dssm-file-name{min-width:0;overflow:hidden;flex:1;text-overflow:ellipsis;white-space:nowrap}.dssm-file-meta{color:var(--dsw-alias-label-tertiary);font-size:11px;white-space:nowrap}.dssm-file-remove{width:24px;height:24px;color:var(--dsw-alias-label-secondary);font-size:18px}.dssm-upload-requirements{padding:1px 1px 0}.dssm-upload-requirements ul{display:flex;margin:7px 0 0;padding-left:18px;flex-direction:column;gap:5px;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:17px}.dssm-detail-section{gap:7px}.dssm-detail-path,.dssm-code{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:11px}.dssm-detail-path{padding:8px 10px;border-radius:7px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);word-break:break-all}.dssm-code{max-height:280px;margin:0;padding:12px;overflow:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);line-height:18px;white-space:pre-wrap}.dssm-diag{padding:8px 10px;border-left:2px solid #d49245;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font-size:12px}.dssm-trash-item{display:flex;align-items:center;gap:10px;padding:11px 0;border-bottom:1px solid var(--dsw-alias-border-l1)}.dssm-trash-item:last-child{border-bottom:0}.dssm-trash-main{min-width:0;flex:1}
78
- @container(max-width:780px){.dssm-table-head{display:none}.dssm-row{grid-template-columns:minmax(0,1fr) max-content;gap:8px;padding:11px 13px}.dssm-row>.dssm-tags,.dssm-row>.dssm-status{grid-column:1}.dssm-row-actions{grid-column:2;grid-row:1 / span 3}.dssm-path{display:none}}@media(max-width:760px){.dssm-summary{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:520px){.dssm-head{flex-direction:column}.dssm-actions{width:100%;margin-left:0}.dssm-actions .dssm-btn{flex:1}.dssm-filters{flex-direction:column}.dssm-source-filter{width:100%}.dssm-summary{grid-template-columns:1fr}}
79
+ @container(max-width:780px){.dssm-table-head{display:none}.dssm-row{grid-template-columns:minmax(0,1fr) max-content;gap:8px;padding:11px 13px}.dssm-row>.dssm-tags,.dssm-row>.dssm-status{grid-column:1}.dssm-row-actions{grid-column:2;grid-row:1 / span 3}.dssm-path{display:none}}@media(max-width:760px){.dssm-summary{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:720px){.dssm-title-row{flex-wrap:wrap}}@media(max-width:520px){.dssm-head{flex-direction:column}.dssm-actions{width:100%;margin-left:0}.dssm-actions .dssm-btn{flex:1}.dssm-filters{flex-direction:column}.dssm-source-filter{width:100%}.dssm-summary{grid-template-columns:1fr}}
79
80
  `;
80
81
 
81
82
  function translateOrFallback(t, key, fallback, params) { var value = t(key, params); return typeof value === "string" && value !== key ? value : fallback; }
@@ -150,6 +151,7 @@ window.__ModuleLoader__.load({
150
151
  return h("div", { className: "dssm-select", ref: ref }, h("button", { type: "button", className: "dssm-select-trigger", "aria-haspopup": "listbox", "aria-expanded": open, onClick: function () { setOpen(!open); } }, h("span", null, selected.label)), open ? h("div", { className: "dssm-select-menu", role: "listbox" }, props.options.map(function (o) { return h("button", { key: o.value || "all", type: "button", role: "option", className: "dssm-option", "aria-selected": o.value === props.value, onClick: function () { props.onChange(o.value); setOpen(false); } }, o.label); })) : null);
151
152
  }
152
153
  function Switch(props) { return h("button", { type: "button", className: "dssm-switch" + (props.on ? " dssm-switch-on" : ""), role: "switch", "aria-checked": props.on, "aria-label": props.label, disabled: props.disabled, onClick: props.onClick }); }
154
+ function GithubMark16() { return h("svg", { viewBox: "0 0 16 16", width: 16, height: 16, "aria-hidden": true, focusable: "false" }, h("path", { fill: "currentColor", d: "M8 0a8 8 0 0 0-2.53 15.59c.4.074.547-.173.547-.385 0-.19-.007-.693-.01-1.36-2.226.484-2.695-1.073-2.695-1.073-.364-.924-.89-1.17-.89-1.17-.726-.496.055-.486.055-.486.803.056 1.225.824 1.225.824.714 1.223 1.872.87 2.328.665.072-.517.28-.87.508-1.07-1.777-.202-3.645-.888-3.645-3.956 0-.874.31-1.588.823-2.148-.083-.202-.357-1.017.078-2.12 0 0 .672-.215 2.2.82A7.65 7.65 0 0 1 8 4.8c.68.003 1.365.092 2.004.27 1.527-1.035 2.197-.82 2.197-.82.437 1.103.162 1.918.08 2.12.513.56.822 1.274.822 2.148 0 3.076-1.872 3.752-3.654 3.95.288.248.544.735.544 1.482 0 1.07-.01 1.932-.01 2.195 0 .214.144.463.55.384A8.001 8.001 0 0 0 8 0Z" })); }
153
155
  function Modal(props) { var ref = react.useRef(null); react.useEffect(function () { if (ref.current) ref.current.focus(); }, []); return h("div", { className: "dssm-mask", onMouseDown: function (e) { if (e.target === e.currentTarget) props.onClose(); } }, h("div", { ref: ref, tabIndex: -1, className: "dssm-modal" + (props.wide ? " dssm-modal-wide" : "") + (props.className ? " " + props.className : ""), role: "dialog", "aria-modal": "true", onKeyDown: function (e) { if (!handleModalEscape(e, props.onClose)) trapModalFocus(e.currentTarget, e); } }, h("div", { className: "dssm-modal-head" }, h("h3", { className: "dssm-modal-title" }, props.title), h("button", { type: "button", className: "dssm-btn dssm-btn-secondary", onClick: props.onClose }, props.closeLabel)), props.children)); }
154
156
 
155
157
  function SkillManagerSection(props) {
@@ -195,7 +197,7 @@ window.__ModuleLoader__.load({
195
197
  }
196
198
 
197
199
  var summary = data.summary || { total: 0, enabled: 0, disabled: 0, issues: 0 };
198
- var content = [h("style", { key: "css" }, CSS), h("div", { key: "head", className: "dssm-head" }, h("div", null, h("h2", { className: "dssm-title" }, t("title")), h("p", { className: "dssm-desc" }, t("desc"))), h("div", { className: "dssm-actions" }, h("button", { type: "button", className: "dssm-btn dssm-btn-secondary", disabled: busy || snapshot.loading, onClick: function () { refresh(false); } }, t("btn.refresh")), h("button", { type: "button", className: "dssm-btn", onClick: openCreate }, t("btn.create")), h("button", { type: "button", className: "dssm-btn dssm-btn-secondary", onClick: function () { setResult(null); setUpload(null); setModal("import"); } }, t("btn.import")))), h("div", { key: "summary", className: "dssm-summary" }, [[summary.total, "summary.total"], [summary.enabled, "summary.enabled"], [summary.disabled, "summary.disabled"]].map(function (item) { return h("div", { key: item[1], className: "dssm-stat" }, h("strong", null, item[0]), t(countKey(item[1], item[0]), { count: item[0] }).replace(String(item[0]), "")); })), h("div", { key: "filters", className: "dssm-filters" }, h("input", { className: "dssm-control dssm-search", value: query, "aria-label": t("search"), placeholder: t("search.placeholder"), onChange: function (e) { setQuery(e.target.value); } }), h("div", { className: "dssm-source-filter" }, h(SourceSelect, { value: activeSource, options: options, onChange: setSource }))), result && modal !== "import" ? h("div", { key: "result", className: "dssm-feedback" + (result.warning ? " dssm-warning" : result.ok ? "" : " dssm-error") }, result.text) : null].concat((data.warnings || []).map(function (warning, index) { return h("div", { key: "warning-" + index, className: "dssm-feedback dssm-warning", role: "alert" }, translateError(t, warning)); }), [snapshot.error ? h("div", { key: "error", className: "dssm-feedback dssm-error" }, snapshot.error) : null, snapshot.loading && !snapshot.data ? h("div", { key: "loading", className: "dssm-empty" }, t("loading")) : h("div", { key: "sources", className: "dssm-sources" }, roots.map(renderRoot)), h("button", { key: "trash", type: "button", className: "dssm-trash-row", onClick: function () { setModal("trash"); } }, h("span", null, t("trash.title")), h("span", { className: "dssm-trash-count" }, (data.trash || []).length))]);
200
+ var content = [h("style", { key: "css" }, CSS), h("div", { key: "head", className: "dssm-head" }, h("div", { className: "dssm-title-block" }, h("div", { className: "dssm-title-row" }, h("h2", { className: "dssm-title" }, t("title")), h("div", { className: "dssm-feedback-links" }, h("a", { className: "dssm-feedback-link", href: "https://github.com/MichengAI/dsh-skills-manager", target: "_blank", rel: "noreferrer", "aria-label": t("link.project") }, h(GithubMark16, null), t("link.project")), h("a", { className: "dssm-feedback-link", href: "https://github.com/MichengAI/dsh-skills-manager/issues", target: "_blank", rel: "noreferrer", "aria-label": t("link.feedback") }, h(primitives.IconListPenOutline16, null), t("link.feedback")))), h("p", { className: "dssm-desc" }, t("desc"))), h("div", { className: "dssm-actions" }, h("button", { type: "button", className: "dssm-btn dssm-btn-secondary", disabled: busy || snapshot.loading, onClick: function () { refresh(false); } }, t("btn.refresh")), h("button", { type: "button", className: "dssm-btn", onClick: openCreate }, t("btn.create")), h("button", { type: "button", className: "dssm-btn dssm-btn-secondary", onClick: function () { setResult(null); setUpload(null); setModal("import"); } }, t("btn.import")))), h("div", { key: "summary", className: "dssm-summary" }, [[summary.total, "summary.total"], [summary.enabled, "summary.enabled"], [summary.disabled, "summary.disabled"]].map(function (item) { return h("div", { key: item[1], className: "dssm-stat" }, h("strong", null, item[0]), t(countKey(item[1], item[0]), { count: item[0] }).replace(String(item[0]), "")); })), h("div", { key: "filters", className: "dssm-filters" }, h("input", { className: "dssm-control dssm-search", value: query, "aria-label": t("search"), placeholder: t("search.placeholder"), onChange: function (e) { setQuery(e.target.value); } }), h("div", { className: "dssm-source-filter" }, h(SourceSelect, { value: activeSource, options: options, onChange: setSource }))), result && modal !== "import" ? h("div", { key: "result", className: "dssm-feedback" + (result.warning ? " dssm-warning" : result.ok ? "" : " dssm-error") }, result.text) : null].concat((data.warnings || []).map(function (warning, index) { return h("div", { key: "warning-" + index, className: "dssm-feedback dssm-warning", role: "alert" }, translateError(t, warning)); }), [snapshot.error ? h("div", { key: "error", className: "dssm-feedback dssm-error" }, snapshot.error) : null, snapshot.loading && !snapshot.data ? h("div", { key: "loading", className: "dssm-empty" }, t("loading")) : h("div", { key: "sources", className: "dssm-sources" }, roots.map(renderRoot)), h("button", { key: "trash", type: "button", className: "dssm-trash-row", onClick: function () { setModal("trash"); } }, h("span", null, t("trash.title")), h("span", { className: "dssm-trash-count" }, (data.trash || []).length))]);
199
201
 
200
202
  if (modal === "create") content.push(h(Modal, { key: "create", title: t("create.title"), closeLabel: t("btn.close"), onClose: function () { setModal(null); } }, h("div", { className: "dssm-form" }, h("label", { className: "dssm-field" }, h("span", { className: "dssm-label" }, t("create.target")), h(SourceSelect, { value: form.root, options: createOptions, onChange: function (value) { updateForm("root", value); } })), [["name", "create.name", "create.name.placeholder"], ["description", "create.description", "create.description.placeholder"]].map(function (field) { return h("label", { key: field[0], className: "dssm-field" }, h("span", { className: "dssm-label" }, t(field[1])), h("input", { className: "dssm-control", value: form[field[0]], placeholder: t(field[2]), onChange: function (e) { updateForm(field[0], e.target.value); } })); }), h("label", { className: "dssm-field" }, h("span", { className: "dssm-label" }, t("create.body")), h("textarea", { className: "dssm-control", value: form.body, placeholder: t("create.body.placeholder"), onChange: function (e) { updateForm("body", e.target.value); } })), h("p", { className: "dssm-help" }, t("create.chat.note"))), h("div", { className: "dssm-modal-actions" }, h("button", { className: "dssm-btn dssm-btn-secondary", onClick: function () { setModal(null); } }, t("btn.cancel")), h("button", { className: "dssm-btn", disabled: busy || !form.name.trim() || !form.description.trim() || !form.body.trim(), onClick: submitCreate }, t("btn.create.now")))));
201
203
  if (modal === "import") content.push(h(Modal, { key: "import", className: "dssm-modal-import", title: t("import.title"), closeLabel: t("btn.close"), onClose: function () { pickerOpenRef.current = false; setUpload(null); setResult(null); setModal(null); } },
package/lib/core.js CHANGED
@@ -77,7 +77,7 @@ export function userRoots() {
77
77
  }
78
78
 
79
79
  function projectIdentity(path) {
80
- const canonical = process.platform === "win32" ? resolve(path).toLowerCase() : resolve(path);
80
+ const canonical = pathIdentity(path);
81
81
  return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
82
82
  }
83
83
 
@@ -127,7 +127,7 @@ export async function projectRoots(projectCwds = [], diagnostics) {
127
127
  }
128
128
  continue;
129
129
  }
130
- const identity = process.platform === "win32" ? found.root.toLowerCase() : found.root;
130
+ const identity = pathIdentity(found.root);
131
131
  const existing = projects.get(identity) || { root: found.root, cwds: new Set() };
132
132
  existing.cwds.add(found.cwd);
133
133
  projects.set(identity, existing);
@@ -350,11 +350,26 @@ async function lstatOrNull(path) {
350
350
  }
351
351
  }
352
352
 
353
+ /** 预解析可作为用户级链接目标的只读根,供一次扫描中的所有链接复用。 */
354
+ async function resolvedReadonlyUserRoots() {
355
+ const roots = userRoots().filter((root) => !root.mutable && root.scope !== "project");
356
+ const resolvedRoots = await Promise.all(roots.map(async (root) => {
357
+ const stat = await lstatOrNull(root.path);
358
+ if (!stat || !stat.isDirectory() || stat.isSymbolicLink()) return null;
359
+ try {
360
+ return { root, realPath: await fs.realpath(root.path) };
361
+ } catch {
362
+ return null;
363
+ }
364
+ }));
365
+ return resolvedRoots.filter(Boolean);
366
+ }
367
+
353
368
  /**
354
369
  * 只接受用户级只读来源中的顶层目录链接,且目标必须是另一个已知只读 Skills 根的直接子目录。
355
370
  * 这样可以兼容 CC Switch 的分发链接,同时不把任意文件系统路径扩展成技能来源。
356
371
  */
357
- async function resolveTrustedLinkedBundle(root, bundlePath) {
372
+ async function resolveTrustedLinkedBundle(root, bundlePath, trustedReadonlyRoots = resolvedReadonlyUserRoots()) {
358
373
  const sourceRoot = rootDefinition(root);
359
374
  if (!sourceRoot || sourceRoot.mutable || sourceRoot.scope === "project") return null;
360
375
 
@@ -367,19 +382,13 @@ async function resolveTrustedLinkedBundle(root, bundlePath) {
367
382
  const realEntryStat = await lstatOrNull(realEntryPath);
368
383
  if (!realEntryStat || !realEntryStat.isDirectory() || realEntryStat.isSymbolicLink()) return null;
369
384
 
370
- let sourceRootReal;
371
- try {
372
- sourceRootReal = await fs.realpath(sourceRoot.path);
373
- } catch {
374
- return null;
375
- }
385
+ const resolvedRoots = await trustedReadonlyRoots;
386
+ const resolvedSourceRoot = resolvedRoots.find((candidate) => pathIdentity(candidate.root.path) === pathIdentity(sourceRoot.path));
387
+ if (!resolvedSourceRoot) return null;
376
388
  let trusted = false;
377
- for (const targetRoot of userRoots().filter((candidate) => !candidate.mutable && candidate.scope !== "project")) {
378
- const targetRootStat = await lstatOrNull(targetRoot.path);
379
- if (!targetRootStat || !targetRootStat.isDirectory() || targetRootStat.isSymbolicLink()) continue;
380
- const targetRootReal = await fs.realpath(targetRoot.path);
381
- if (pathIdentity(targetRootReal) === pathIdentity(sourceRootReal)) continue;
382
- const acceptedTarget = entryPath(targetRootReal, basename(realEntryPath));
389
+ for (const targetRoot of resolvedRoots) {
390
+ if (pathIdentity(targetRoot.realPath) === pathIdentity(resolvedSourceRoot.realPath)) continue;
391
+ const acceptedTarget = entryPath(targetRoot.realPath, basename(realEntryPath));
383
392
  if (acceptedTarget && pathIdentity(acceptedTarget) === pathIdentity(realEntryPath)) {
384
393
  trusted = true;
385
394
  break;
@@ -573,32 +582,37 @@ export function parseBoolValue(raw) {
573
582
 
574
583
  /** 按名称解析条目(bundle 优先,其次 flat)。找不到返回 null。 */
575
584
  export async function resolveEntry(root, name) {
576
- const bundlePath = entryPath(root, name);
577
- if (bundlePath === null) return null;
578
- const rootPath = resolve(root);
579
- const rootStat = await lstatOrNull(rootPath);
580
- if (!rootStat || !rootStat.isDirectory() || rootStat.isSymbolicLink()) return null;
581
- const rootReal = await resolvedPath(rootPath);
582
- const bundleStat = await lstatOrNull(bundlePath);
583
- if (bundleStat && bundleStat.isDirectory() && !bundleStat.isSymbolicLink()) {
584
- const bundleDoc = join(bundlePath, "SKILL.md");
585
- const docStat = await lstatOrNull(bundleDoc);
586
- if (docStat && docStat.isFile() && !docStat.isSymbolicLink() && await isInsideResolvedRoot(rootReal, bundleDoc)) {
587
- return { kind: "bundle", docPath: bundleDoc, entryPath: bundlePath, realDocPath: await fs.realpath(bundleDoc), realEntryPath: await fs.realpath(bundlePath), linked: false };
585
+ try {
586
+ const bundlePath = entryPath(root, name);
587
+ if (bundlePath === null) return null;
588
+ const rootPath = resolve(root);
589
+ const rootStat = await lstatOrNull(rootPath);
590
+ if (!rootStat || !rootStat.isDirectory() || rootStat.isSymbolicLink()) return null;
591
+ const rootReal = await resolvedPath(rootPath);
592
+ const bundleStat = await lstatOrNull(bundlePath);
593
+ if (bundleStat && bundleStat.isDirectory() && !bundleStat.isSymbolicLink()) {
594
+ const bundleDoc = join(bundlePath, "SKILL.md");
595
+ const docStat = await lstatOrNull(bundleDoc);
596
+ if (docStat && docStat.isFile() && !docStat.isSymbolicLink() && await isInsideResolvedRoot(rootReal, bundleDoc)) {
597
+ return { kind: "bundle", docPath: bundleDoc, entryPath: bundlePath, realDocPath: await fs.realpath(bundleDoc), realEntryPath: await fs.realpath(bundlePath), linked: false };
598
+ }
588
599
  }
600
+ if (bundleStat && bundleStat.isSymbolicLink()) {
601
+ const linked = await resolveTrustedLinkedBundle(rootPath, bundlePath);
602
+ if (linked) return { kind: "bundle", docPath: join(bundlePath, "SKILL.md"), entryPath: bundlePath, ...linked, linked: true };
603
+ }
604
+ const flatDoc = resolve(rootPath, `${name}.md`);
605
+ if (!isSameOrDescendant(rootPath, flatDoc) || rootPath === flatDoc) return null;
606
+ const flatStat = await lstatOrNull(flatDoc);
607
+ if (flatStat && flatStat.isFile() && !flatStat.isSymbolicLink() && await isInsideResolvedRoot(rootReal, flatDoc)) {
608
+ const realDocPath = await fs.realpath(flatDoc);
609
+ return { kind: "flat", docPath: flatDoc, entryPath: flatDoc, realDocPath, realEntryPath: realDocPath, linked: false };
610
+ }
611
+ return null;
612
+ } catch {
613
+ // lstat 与 realpath/readFile 之间允许来源变化,调用方统一按不存在处理。
614
+ return null;
589
615
  }
590
- if (bundleStat && bundleStat.isSymbolicLink()) {
591
- const linked = await resolveTrustedLinkedBundle(rootPath, bundlePath);
592
- if (linked) return { kind: "bundle", docPath: join(bundlePath, "SKILL.md"), entryPath: bundlePath, ...linked, linked: true };
593
- }
594
- const flatDoc = resolve(rootPath, `${name}.md`);
595
- if (!isSameOrDescendant(rootPath, flatDoc) || rootPath === flatDoc) return null;
596
- const flatStat = await lstatOrNull(flatDoc);
597
- if (flatStat && flatStat.isFile() && !flatStat.isSymbolicLink() && await isInsideResolvedRoot(rootReal, flatDoc)) {
598
- const realDocPath = await fs.realpath(flatDoc);
599
- return { kind: "flat", docPath: flatDoc, entryPath: flatDoc, realDocPath, realEntryPath: realDocPath, linked: false };
600
- }
601
- return null;
602
616
  }
603
617
 
604
618
  function entryOf(name, kind, docPath, doc) {
@@ -632,7 +646,7 @@ function entryOf(name, kind, docPath, doc) {
632
646
  }
633
647
 
634
648
  /** 扫描一个技能根(只扫一层)。返回 { exists, entries }。 */
635
- export async function scanEntries(root) {
649
+ export async function scanEntries(root, options = {}) {
636
650
  const rootStat = await lstatOrNull(resolve(root));
637
651
  if (!rootStat || !rootStat.isDirectory() || rootStat.isSymbolicLink()) return { exists: false, entries: [] };
638
652
  let items;
@@ -643,12 +657,14 @@ export async function scanEntries(root) {
643
657
  }
644
658
  const byName = new Map();
645
659
  const rootReal = await resolvedPath(root);
660
+ let trustedReadonlyRoots = options.trustedReadonlyRoots;
646
661
  for (const it of items) {
647
662
  try {
648
663
  if (it.isSymbolicLink()) {
649
664
  const bundlePath = entryPath(root, it.name);
650
665
  if (bundlePath === null) continue;
651
- const linked = await resolveTrustedLinkedBundle(root, bundlePath);
666
+ trustedReadonlyRoots ||= resolvedReadonlyUserRoots();
667
+ const linked = await resolveTrustedLinkedBundle(root, bundlePath, trustedReadonlyRoots);
652
668
  if (!linked) continue;
653
669
  const doc = parseSkillDoc(await fs.readFile(linked.realDocPath, "utf8"));
654
670
  byName.set(it.name, { ...entryOf(it.name, "bundle", join(bundlePath, "SKILL.md"), doc), entryPath: bundlePath, ...linked, linked: true });
@@ -677,23 +693,53 @@ export async function scanEntries(root) {
677
693
  return { exists: true, entries };
678
694
  }
679
695
 
680
- /** 同一真实技能可能被 CC Switch 分发到多个 Agent 根;按来源 rank 只保留一份。 */
696
+ /** 同一真实技能优先归属直接 SSOT;同类条目再按来源 rank 决胜。 */
681
697
  async function scanDeduplicatedUserRoots(roots = userRoots()) {
698
+ const trustedReadonlyRoots = resolvedReadonlyUserRoots();
699
+ const results = await Promise.all(roots.map(async (root) => [root, await scanEntries(root.path, { trustedReadonlyRoots })]));
682
700
  const scans = new Map();
683
- for (const root of roots) scans.set(root.key, await scanEntries(root.path));
684
- const claimed = new Set();
685
- for (const root of [...roots].sort((left, right) => left.rank - right.rank)) {
686
- const scan = scans.get(root.key);
687
- scan.entries = scan.entries.filter((entry) => {
701
+ const groups = new Map();
702
+ for (const [root, scan] of results) {
703
+ scans.set(root.key, scan);
704
+ for (const entry of scan.entries) {
688
705
  const identity = pathIdentity(entry.realEntryPath || entry.entryPath || entry.docPath);
689
- if (claimed.has(identity)) return false;
690
- claimed.add(identity);
691
- return true;
692
- });
706
+ const group = groups.get(identity) || [];
707
+ group.push({ root, entry });
708
+ groups.set(identity, group);
709
+ }
710
+ }
711
+ const winners = new Set();
712
+ for (const group of groups.values()) {
713
+ group.sort((left, right) => Number(left.entry.linked) - Number(right.entry.linked) || left.root.rank - right.root.rank);
714
+ const winner = group[0];
715
+ winner.entry.providerRank = Math.min(...group.map((item) => item.root.rank));
716
+ winner.entry.policyRootKeys = group.map((item) => item.root.key);
717
+ winners.add(winner.entry);
718
+ }
719
+ for (const scan of scans.values()) {
720
+ scan.entries = scan.entries.filter((entry) => winners.has(entry));
693
721
  }
694
722
  return scans;
695
723
  }
696
724
 
725
+ /**
726
+ * 详情只解析被请求的条目,再从物理路径确定其直接所有者。
727
+ * 受信链接只能指向已知只读根的顶层目录,故无需扫描并读取全部技能文档即可复现去重归属。
728
+ */
729
+ async function visibleEntryForRoot(root, name) {
730
+ if (root.scope === "project") return resolveEntry(root.path, name);
731
+ const entry = await resolveEntry(root.path, name);
732
+ if (!entry) return null;
733
+ const identity = pathIdentity(entry.realEntryPath || entry.entryPath || entry.docPath);
734
+ const owners = (await resolvedReadonlyUserRoots())
735
+ .filter((candidate) => {
736
+ const expected = entryPath(candidate.realPath, basename(entry.realEntryPath || entry.entryPath || entry.docPath));
737
+ return expected !== null && pathIdentity(expected) === identity;
738
+ })
739
+ .sort((left, right) => left.root.rank - right.root.rank);
740
+ return owners.length === 0 || owners[0].root.key === root.key ? entry : null;
741
+ }
742
+
697
743
  // ── Manager 本地策略(外部源只读,启停状态写入 DSH_HOME)────────────────────
698
744
 
699
745
  function defaultManagerState() {
@@ -747,7 +793,7 @@ function validManagerStateDocument(value) {
747
793
  return true;
748
794
  }
749
795
 
750
- /** v0.1.32 及更早状态尚无 CC Switch 键;只补充新增字段,其余损坏仍然失败关闭。 */
796
+ /** 仅对可识别的 version 1 状态补齐 CC Switch 键,其余结构仍交由严格校验失败关闭。 */
751
797
  function migrateManagerStateDocument(value) {
752
798
  if (!value || typeof value !== "object" || Array.isArray(value) || value.version !== 1) return value;
753
799
  if (value.sources && typeof value.sources === "object" && !Array.isArray(value.sources) && value.sources.ccswitch === undefined) value.sources.ccswitch = true;
@@ -799,14 +845,21 @@ async function writeManagerState(value) {
799
845
  await writeFileAtomically(managerStatePath(), `${JSON.stringify(normalizeManagerState(value), null, 2)}\n`);
800
846
  }
801
847
 
802
- function managerSkillOverride(policy, rootKey, name) {
848
+ function managerSkillOverride(policy, rootKey, name, policyRootKeys = []) {
803
849
  if ((policy.enabledSkills[rootKey] || []).includes(name)) return true;
804
850
  if ((policy.disabledSkills[rootKey] || []).includes(name)) return false;
851
+ let inheritedEnable = false;
852
+ for (const key of policyRootKeys) {
853
+ if (key === rootKey) continue;
854
+ if ((policy.disabledSkills[key] || []).includes(name)) return false;
855
+ if ((policy.enabledSkills[key] || []).includes(name)) inheritedEnable = true;
856
+ }
857
+ if (inheritedEnable) return true;
805
858
  return undefined;
806
859
  }
807
860
 
808
861
  function effectiveSkillPolicy(policyResult, root, entry) {
809
- const override = managerSkillOverride(policyResult.state, root.key, entry.name);
862
+ const override = managerSkillOverride(policyResult.state, root.key, entry.name, entry.policyRootKeys);
810
863
  const sourceEnabled = root.key === "dsh" || root.scope === "project" || policyResult.state.sources[root.key] !== false;
811
864
  if (policyResult.writable === false || !sourceEnabled || override === false) {
812
865
  return { override, sourceEnabled, modelInvocable: false, userInvocable: false, enabled: false };
@@ -865,7 +918,12 @@ async function setPolicySkillEnabled(root, name, enabled, log) {
865
918
  if (!definition) return readonlyError("toggle");
866
919
  const resolved = await resolveEntry(definition.path, name);
867
920
  if (resolved === null) return { ok: false, error: `技能不存在: ${name}`, code: "error.skill.notFound", params: { name } };
868
- const summary = entryOf(name, resolved.kind, resolved.docPath, parseSkillDoc(await fs.readFile(resolved.docPath, "utf8")));
921
+ let summary;
922
+ try {
923
+ summary = entryOf(name, resolved.kind, resolved.docPath, parseSkillDoc(await fs.readFile(resolved.realDocPath || resolved.docPath, "utf8")));
924
+ } catch {
925
+ return { ok: false, error: `技能不存在: ${name}`, code: "error.skill.notFound", params: { name } };
926
+ }
869
927
  if (!summary.hasFrontmatter) {
870
928
  return { ok: false, error: `技能缺少完整 frontmatter,无法${enabled ? "启用" : "停用"}: ${name}`, code: "error.skill.noFrontmatter", params: { name, action: enabled ? "enable" : "disable" } };
871
929
  }
@@ -982,8 +1040,8 @@ async function restoreRootDefinition(metadata, options = {}) {
982
1040
  || !isAbsolute(metadata.root.projectRoot)
983
1041
  ) return { ok: false, error: `回收站条目来源非法: ${metadata.id}`, code: "error.trash.invalid", params: { id: metadata.id } };
984
1042
  const roots = await projectRoots(options.projectCwds);
985
- const normalizedProjectRoot = process.platform === "win32" ? resolve(metadata.root.projectRoot).toLowerCase() : resolve(metadata.root.projectRoot);
986
- const root = roots.find((item) => item.key === metadata.root.key && item.kind === "project-dsh" && (process.platform === "win32" ? resolve(item.projectRoot).toLowerCase() : resolve(item.projectRoot)) === normalizedProjectRoot);
1043
+ const normalizedProjectRoot = pathIdentity(metadata.root.projectRoot);
1044
+ const root = roots.find((item) => item.key === metadata.root.key && item.kind === "project-dsh" && pathIdentity(item.projectRoot) === normalizedProjectRoot);
987
1045
  if (!root) {
988
1046
  return {
989
1047
  ok: false,
@@ -1536,9 +1594,14 @@ export async function skillDetail(key, name, options = {}) {
1536
1594
  const scopedRoots = await projectRoots(options.projectCwds);
1537
1595
  const root = rootByKey(key) || scopedRoots.find((item) => item.key === key);
1538
1596
  if (!root) return { ok: false, error: `未知技能来源: ${key}`, code: "error.root.unknown", params: { root: key } };
1539
- const entry = await resolveEntry(root.path, name);
1597
+ const entry = await visibleEntryForRoot(root, name);
1540
1598
  if (!entry) return { ok: false, error: `技能不存在: ${name}`, code: "error.skill.notFound", params: { name } };
1541
- const raw = await fs.readFile(entry.realDocPath || entry.docPath, "utf8");
1599
+ let raw;
1600
+ try {
1601
+ raw = await fs.readFile(entry.realDocPath || entry.docPath, "utf8");
1602
+ } catch {
1603
+ return { ok: false, error: `技能不存在: ${name}`, code: "error.skill.notFound", params: { name } };
1604
+ }
1542
1605
  const doc = parseSkillDoc(raw);
1543
1606
  const summary = entryOf(name, entry.kind, entry.docPath, doc);
1544
1607
  return {
@@ -1575,7 +1638,7 @@ export async function listProviderCandidates(options = {}) {
1575
1638
  invocation: { modelInvocable: policy.modelInvocable, userInvocable: policy.userInvocable },
1576
1639
  provider: "dsh-skills-manager-external",
1577
1640
  source: root.key === "dsh" ? "user-dsh" : `agent-${root.key}`,
1578
- rank: root.key === "dsh" ? USER_DSH_POLICY_RANK : root.rank,
1641
+ rank: root.key === "dsh" ? USER_DSH_POLICY_RANK : entry.providerRank ?? root.rank,
1579
1642
  locator: { rootKey: root.key, entryName: entry.name, path: entry.docPath, realEntryPath: entry.realEntryPath, realDocPath: entry.realDocPath },
1580
1643
  resourceBase: { kind: "directory", path: entry.kind === "bundle" ? entry.realEntryPath : root.path },
1581
1644
  path: entry.docPath,
@@ -1601,8 +1664,8 @@ export async function listProviderCandidates(options = {}) {
1601
1664
  provider: "dsh-skills-manager-external",
1602
1665
  source: root.kind,
1603
1666
  rank: root.rank - 1,
1604
- locator: { rootKey: root.key, entryName: entry.name, path: entry.docPath },
1605
- resourceBase: { kind: "directory", path: entry.kind === "bundle" ? dirname(entry.docPath) : root.path },
1667
+ locator: { rootKey: root.key, entryName: entry.name, path: entry.docPath, realEntryPath: entry.realEntryPath, realDocPath: entry.realDocPath },
1668
+ resourceBase: { kind: "directory", path: entry.kind === "bundle" ? entry.realEntryPath : root.path },
1606
1669
  path: entry.docPath,
1607
1670
  metadata: { dshSkillsManager: { root: root.key, readOnly: !root.mutable, sourceReadOnly: !root.mutable, policyOnly: true } },
1608
1671
  });
@@ -1614,30 +1677,34 @@ export async function listProviderCandidates(options = {}) {
1614
1677
 
1615
1678
  export async function getProviderSkill(candidate, options = {}) {
1616
1679
  const locator = candidate && candidate.locator;
1617
- if (!locator || typeof locator.path !== "string" || typeof locator.rootKey !== "string") return undefined;
1680
+ if (!locator || typeof locator.path !== "string" || typeof locator.rootKey !== "string" || typeof locator.realEntryPath !== "string" || typeof locator.realDocPath !== "string") return undefined;
1618
1681
  let root = rootByKey(locator.rootKey);
1619
1682
  if (!root && PROJECT_ROOT_KEY_RE.test(locator.rootKey) && typeof options.cwd === "string") {
1620
1683
  root = (await projectRoots([options.cwd])).find((item) => item.key === locator.rootKey);
1621
1684
  }
1622
1685
  if (!root) return undefined;
1623
- const entry = await resolveEntry(root.path, String(locator.entryName || ""));
1624
- if (!entry || resolve(entry.docPath) !== resolve(locator.path)) return undefined;
1625
- if (locator.realEntryPath && pathIdentity(entry.realEntryPath) !== pathIdentity(locator.realEntryPath)) return undefined;
1626
- if (locator.realDocPath && pathIdentity(entry.realDocPath) !== pathIdentity(locator.realDocPath)) return undefined;
1627
- const doc = parseSkillDoc(await fs.readFile(entry.realDocPath || entry.docPath, "utf8"));
1628
- const summary = entryOf(locator.entryName, entry.kind, entry.docPath, doc);
1629
- if (!summary.loadable || summary.declaredName !== candidate.name) return undefined;
1630
- return {
1631
- name: candidate.name,
1632
- description: candidate.description,
1633
- invocation: candidate.invocation,
1634
- provider: candidate.provider,
1635
- source: candidate.source,
1636
- resourceBase: candidate.resourceBase,
1637
- path: candidate.path,
1638
- metadata: candidate.metadata,
1639
- content: doc.body.trim(),
1640
- };
1686
+ try {
1687
+ const entry = await resolveEntry(root.path, String(locator.entryName || ""));
1688
+ if (!entry || resolve(entry.docPath) !== resolve(locator.path)) return undefined;
1689
+ if (pathIdentity(entry.realEntryPath) !== pathIdentity(locator.realEntryPath)) return undefined;
1690
+ if (pathIdentity(entry.realDocPath) !== pathIdentity(locator.realDocPath)) return undefined;
1691
+ const doc = parseSkillDoc(await fs.readFile(entry.realDocPath || entry.docPath, "utf8"));
1692
+ const summary = entryOf(locator.entryName, entry.kind, entry.docPath, doc);
1693
+ if (!summary.loadable || summary.declaredName !== candidate.name) return undefined;
1694
+ return {
1695
+ name: candidate.name,
1696
+ description: candidate.description,
1697
+ invocation: candidate.invocation,
1698
+ provider: candidate.provider,
1699
+ source: candidate.source,
1700
+ resourceBase: candidate.resourceBase,
1701
+ path: candidate.path,
1702
+ metadata: candidate.metadata,
1703
+ content: doc.body.trim(),
1704
+ };
1705
+ } catch {
1706
+ return undefined;
1707
+ }
1641
1708
  }
1642
1709
 
1643
1710
  // ── 状态快照 ────────────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michengai/dsh-skills-manager",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
4
4
  "description": "NPM-installable DSH Web plugin for safely loading and managing skills across DSH and common local Agents.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -50,14 +50,27 @@
50
50
  },
51
51
  "peerDependencies": {
52
52
  "@deepseek-ai/cordis": ">=4.0.1 <5.0.0",
53
- "@deepseek-ai/dsh-client-locale": ">=0.1.0-rc.0 <0.2.0",
54
- "@deepseek-ai/dsh-client-runtime": ">=0.1.0-rc.0 <0.2.0",
55
- "@deepseek-ai/dsh-client-ui-slots": ">=0.1.0-rc.0 <0.2.0",
53
+ "@deepseek-ai/dsh-client-locale": ">=0.1.0-rc.5 <0.2.0",
54
+ "@deepseek-ai/dsh-client-runtime": ">=0.1.0-rc.5 <0.2.0",
55
+ "@deepseek-ai/dsh-client-ui-primitives": ">=0.1.0-rc.5 <0.2.0",
56
+ "@deepseek-ai/dsh-client-ui-slots": ">=0.1.0-rc.5 <0.2.0",
56
57
  "@deepseek-ai/dsh-host-webserver": ">=0.1.0-rc.5 <0.2.0",
58
+ "@deepseek-ai/dsh-session": ">=0.1.0-rc.5 <0.2.0",
57
59
  "@deepseek-ai/dsh-skill": ">=0.1.0-rc.5 <0.2.0",
58
60
  "@deepseek-ai/dsh-tools": ">=0.1.0-rc.5 <0.2.0",
61
+ "@deepseek-ai/dsh-web-app": ">=0.1.0-rc.5 <0.2.0",
59
62
  "react": "^18.2.0"
60
63
  },
64
+ "devDependencies": {
65
+ "@deepseek-ai/dsh-client-locale": "0.1.2-alpha.5",
66
+ "@deepseek-ai/dsh-client-ui-primitives": "0.1.2-alpha.5",
67
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.2-alpha.5",
68
+ "@deepseek-ai/dsh-host-webserver": "0.1.2-alpha.5",
69
+ "@deepseek-ai/dsh-session": "0.1.2-alpha.5",
70
+ "@deepseek-ai/dsh-skill": "0.1.2-alpha.5",
71
+ "@deepseek-ai/dsh-tools": "0.1.2-alpha.5",
72
+ "@deepseek-ai/dsh-web-app": "0.1.2-alpha.5"
73
+ },
61
74
  "engines": {
62
75
  "node": ">=20"
63
76
  },