@michengai/dsh-skills-manager 0.1.33 → 0.1.34

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,16 @@ The five most recent published versions are listed below.
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## 0.1.34 — 2026-09-02
10
+
11
+ - 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.
12
+ - 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.
13
+ - Scanned user roots concurrently and reused one resolved set of trusted read-only roots per scan without caching external filesystem state across requests.
14
+ - 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.
15
+ - 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.
16
+
17
+ Published package: [`@michengai/dsh-skills-manager@0.1.34`](https://www.npmjs.com/package/@michengai/dsh-skills-manager/v/0.1.34).
18
+
9
19
  ## 0.1.33 — 2026-09-02
10
20
 
11
21
  - 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,16 @@
6
6
 
7
7
  ## 未发布
8
8
 
9
+ ## 0.1.34 — 2026-09-02
10
+
11
+ - 同一真实 Skill 同时存在直接条目和 Agent 分发链接时,改为由直接条目归属;规范候选继承所有分发入口中的最强 rank,停用 CC Switch 后不会回流到公共 Agent 链接副本。
12
+ - 用户级与项目级 Provider 候选必须携带扫描时发现的真实 bundle 和文档路径,加载时同时复验;启停读取也统一使用真实文档路径。
13
+ - 用户根改为并发扫描,并在单次扫描内复用已解析的受信只读根;不跨请求缓存外部文件系统状态。
14
+ - 当前分发条目归属迁移到规范来源时,保留原来源的逐 Skill 停用;规范来源自身的显式策略优先于继承的分发链接策略。
15
+ - 详情接口按被请求条目的物理所有者复现状态快照的用户根去重结果,避免读取无关 Skill 文档;文件在扫描后消失时,条目解析、详情读取和 Provider 读取均失败关闭。
16
+
17
+ 发布包:[`@michengai/dsh-skills-manager@0.1.34`](https://www.npmjs.com/package/@michengai/dsh-skills-manager/v/0.1.34)。
18
+
9
19
  ## 0.1.33 — 2026-09-02
10
20
 
11
21
  - 新增默认开启的只读 `CC Switch` 来源,固定读取 `~\.cc-switch\skills`,支持来源级与逐 Skill 本地启停;旧状态文件自动补齐新来源,不会因缺少新键失败关闭。
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.34",
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": {