



























@@ -154,10 +154,13 @@ type ResolvedSkillsLimits = {
154154type LoadedSkillRecord = {
155155skill: Skill;
156156frontmatter?: ParsedSkillFrontmatter;
157+syncSourceDir?: string;
158+syncDirName?: string;
157159};
158160159161type CandidateSkillDir = {
160162skillDir: string;
163+skillDirRealPath: string;
161164name: string;
162165skillMdRealPath: string;
163166};
@@ -388,16 +391,53 @@ function loadContainedSkillRecords(params: {
388391skillDir: string;
389392source: string;
390393maxSkillFileBytes: number;
394+canonicalSkillDir?: string;
391395}): LoadedSkillRecord[] {
392396const expectedBaseDir = path.resolve(params.skillDir);
393397const loaded = loadSkillsFromDirSafe({
394398dir: params.skillDir,
395399source: params.source,
396400maxBytes: params.maxSkillFileBytes,
397401});
398-return unwrapLoadedSkillRecords(loaded).filter(
402+const records = unwrapLoadedSkillRecords(loaded).filter(
399403(record) => path.resolve(record.skill.baseDir) === expectedBaseDir,
400404);
405+const canonicalSkillDir = params.canonicalSkillDir;
406+return canonicalSkillDir
407+ ? records.map((record) => canonicalizeLoadedSkillRecord(record, canonicalSkillDir))
408+ : records;
409+}
410+411+function canonicalizeLoadedSkillRecord(
412+record: LoadedSkillRecord,
413+canonicalSkillDir: string,
414+): LoadedSkillRecord {
415+const originalBaseDir = path.resolve(record.skill.baseDir);
416+const canonicalBaseDir = path.resolve(canonicalSkillDir);
417+if (originalBaseDir === canonicalBaseDir) {
418+return record;
419+}
420+const filePath = path.join(
421+canonicalBaseDir,
422+path.relative(originalBaseDir, record.skill.filePath),
423+);
424+return {
425+ ...record,
426+syncSourceDir: canonicalBaseDir,
427+syncDirName: path.basename(originalBaseDir),
428+skill: {
429+ ...record.skill,
430+ filePath,
431+baseDir: canonicalBaseDir,
432+sourceInfo: record.skill.sourceInfo
433+ ? {
434+ ...record.skill.sourceInfo,
435+path: filePath,
436+baseDir: canonicalBaseDir,
437+}
438+ : record.skill.sourceInfo,
439+},
440+};
401441}
402442403443function isPathInsideAnyRoot(rootRealPaths: readonly string[], candidateRealPath: string): boolean {
@@ -437,6 +477,10 @@ function resolveSkillRootCandidatePath(params: {
437477});
438478}
439479480+function canonicalSkillDirForSource(source: string, skillDirRealPath: string): string | undefined {
481+return shouldEnforceConfiguredSkillRootContainment(source) ? undefined : skillDirRealPath;
482+}
483+440484function resolveSkillFilePath(params: {
441485source: string;
442486skillDir: string;
@@ -624,6 +668,7 @@ function loadSkillEntries(
624668skillDir: baseDir,
625669source: params.source,
626670maxSkillFileBytes: limits.maxSkillFileBytes,
671+canonicalSkillDir: canonicalSkillDirForSource(params.source, baseDirRealPath),
627672});
628673}
629674@@ -658,7 +703,12 @@ function loadSkillEntries(
658703}
659704660705const loadedSkills: LoadedSkillRecord[] = [];
661-const loadCandidateSkill = ({ skillDir, name, skillMdRealPath }: CandidateSkillDir) => {
706+const loadCandidateSkill = ({
707+ skillDir,
708+ skillDirRealPath,
709+ name,
710+ skillMdRealPath,
711+}: CandidateSkillDir) => {
662712try {
663713const size = fs.statSync(skillMdRealPath).size;
664714if (size > limits.maxSkillFileBytes) {
@@ -679,6 +729,7 @@ function loadSkillEntries(
679729 skillDir,
680730source: params.source,
681731maxSkillFileBytes: limits.maxSkillFileBytes,
732+canonicalSkillDir: canonicalSkillDirForSource(params.source, skillDirRealPath),
682733}),
683734);
684735};
@@ -707,7 +758,7 @@ function loadSkillEntries(
707758candidatePath: skillMd,
708759});
709760if (skillMdRealPath) {
710-loadCandidateSkill({ skillDir, name, skillMdRealPath });
761+loadCandidateSkill({ skillDir, skillDirRealPath, name, skillMdRealPath });
711762}
712763} else {
713764// No SKILL.md here — check one level deeper for grouped skill directories.
@@ -764,6 +815,7 @@ function loadSkillEntries(
764815if (nestedDirRealPath && nestedSkillMdRealPath) {
765816loadCandidateSkill({
766817skillDir: nestedDir,
818+skillDirRealPath: nestedDirRealPath,
767819name: `${name}/${nestedName}`,
768820skillMdRealPath: nestedSkillMdRealPath,
769821});
@@ -884,6 +936,8 @@ function loadSkillEntries(
884936 frontmatter,
885937metadata: resolveOpenClawMetadata(frontmatter),
886938 invocation,
939+ ...(record.syncSourceDir !== undefined ? { syncSourceDir: record.syncSourceDir } : {}),
940+ ...(record.syncDirName !== undefined ? { syncDirName: record.syncDirName } : {}),
887941exposure: {
888942includeInRuntimeRegistry: true,
889943// Freshly loaded entries preserve the documented disable-model-invocation
@@ -1169,7 +1223,9 @@ function resolveSyncedSkillDestinationPath(params: {
11691223entry: SkillEntry;
11701224usedDirNames: Set<string>;
11711225}): string | null {
1172-const sourceDirName = path.basename(params.entry.skill.baseDir).trim();
1226+const sourceDirName = (
1227+params.entry.syncDirName ?? path.basename(params.entry.skill.baseDir)
1228+).trim();
11731229if (!sourceDirName || sourceDirName === "." || sourceDirName === "..") {
11741230return null;
11751231}
@@ -1233,7 +1289,7 @@ export async function syncSkillsToWorkspace(params: {
12331289continue;
12341290}
12351291try {
1236-await fsp.cp(entry.skill.baseDir, dest, {
1292+await fsp.cp(entry.syncSourceDir ?? entry.skill.baseDir, dest, {
12371293recursive: true,
12381294force: true,
12391295filter: (src) => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。