




















@@ -140,6 +140,12 @@ type LoadedSkillRecord = {
140140frontmatter?: ParsedSkillFrontmatter;
141141};
142142143+type CandidateSkillDir = {
144+skillDir: string;
145+name: string;
146+skillMdRealPath: string;
147+};
148+143149function resolveSkillsLimits(config?: OpenClawConfig, agentId?: string): ResolvedSkillsLimits {
144150const limits = config?.skills?.limits;
145151const agentSkillsLimits = resolveEffectiveAgentSkillsLimits(config, agentId);
@@ -288,32 +294,6 @@ function resolveContainedSkillPath(params: {
288294return null;
289295}
290296291-function filterLoadedSkillRecordsInsideRoot(params: {
292-records: LoadedSkillRecord[];
293-source: string;
294-rootDir: string;
295-rootRealPath: string;
296-}): LoadedSkillRecord[] {
297-return params.records.filter(({ skill }) => {
298-const baseDirRealPath = resolveContainedSkillPath({
299-source: params.source,
300-rootDir: params.rootDir,
301-rootRealPath: params.rootRealPath,
302-candidatePath: skill.baseDir,
303-});
304-if (!baseDirRealPath) {
305-return false;
306-}
307-const skillFileRealPath = resolveContainedSkillPath({
308-source: params.source,
309-rootDir: params.rootDir,
310-rootRealPath: params.rootRealPath,
311-candidatePath: skill.filePath,
312-});
313-return Boolean(skillFileRealPath);
314-});
315-}
316-317297function resolveNestedSkillsRoot(
318298dir: string,
319299opts?: {
@@ -365,6 +345,22 @@ function unwrapLoadedSkillRecords(loaded: unknown): LoadedSkillRecord[] {
365345return [];
366346}
367347348+function loadContainedSkillRecords(params: {
349+skillDir: string;
350+source: string;
351+maxSkillFileBytes: number;
352+}): LoadedSkillRecord[] {
353+const expectedBaseDir = path.resolve(params.skillDir);
354+const loaded = loadSkillsFromDirSafe({
355+dir: params.skillDir,
356+source: params.source,
357+maxBytes: params.maxSkillFileBytes,
358+});
359+return unwrapLoadedSkillRecords(loaded).filter(
360+(record) => path.resolve(record.skill.baseDir) === expectedBaseDir,
361+);
362+}
363+368364function loadSkillEntries(
369365workspaceDir: string,
370366opts?: {
@@ -423,23 +419,19 @@ function loadSkillEntries(
423419return [];
424420}
425421426-const loaded = loadSkillsFromDirSafe({
427-dir: baseDir,
428-source: params.source,
429-maxBytes: limits.maxSkillFileBytes,
430-});
431-return filterLoadedSkillRecordsInsideRoot({
432-records: unwrapLoadedSkillRecords(loaded),
422+return loadContainedSkillRecords({
423+skillDir: baseDir,
433424source: params.source,
434- rootDir,
435-rootRealPath: baseDirRealPath,
425+maxSkillFileBytes: limits.maxSkillFileBytes,
436426});
437427}
438428439429const childDirs = listChildDirectories(baseDir);
440-const suspicious = childDirs.length > limits.maxCandidatesPerRoot;
430+const maxCandidatesPerRoot = Math.max(0, limits.maxCandidatesPerRoot);
431+const maxSkillsLoadedPerSource = Math.max(0, limits.maxSkillsLoadedPerSource);
432+const suspicious = childDirs.length > maxCandidatesPerRoot;
441433442-const maxCandidates = Math.max(0, limits.maxSkillsLoadedPerSource);
434+const maxCandidates = Math.min(maxCandidatesPerRoot, maxSkillsLoadedPerSource);
443435const limitedChildren = childDirs.toSorted().slice(0, maxCandidates);
444436445437if (suspicious) {
@@ -461,7 +453,10 @@ function loadSkillEntries(
461453462454const loadedSkills: LoadedSkillRecord[] = [];
463455464-// Only consider immediate subfolders that look like skills (have SKILL.md) and are under size cap.
456+// Consider immediate subfolders that look like skills (have SKILL.md) and are under size cap.
457+// When an immediate subfolder does NOT have a SKILL.md, check one level deeper for grouped
458+// skill directories (e.g. ~/.openclaw/skills/coze/koze-retrieval/SKILL.md).
459+const candidateDirs: CandidateSkillDir[] = [];
465460for (const name of limitedChildren) {
466461const skillDir = path.join(baseDir, name);
467462const skillDirRealPath = resolveContainedSkillPath({
@@ -474,24 +469,76 @@ function loadSkillEntries(
474469continue;
475470}
476471const skillMd = path.join(skillDir, "SKILL.md");
477-if (!fs.existsSync(skillMd)) {
478-continue;
472+if (fs.existsSync(skillMd)) {
473+const skillMdRealPath = resolveContainedSkillPath({
474+source: params.source,
475+ rootDir,
476+rootRealPath: baseDirRealPath,
477+candidatePath: skillMd,
478+});
479+if (skillMdRealPath) {
480+candidateDirs.push({ skillDir, name, skillMdRealPath });
481+}
482+} else {
483+// No SKILL.md here — check one level deeper for grouped skill directories.
484+// Apply the same per-root cap as the outer scan to avoid scanning huge nested trees.
485+const nestedChildren = listChildDirectories(skillDir);
486+const nestedSuspicious = nestedChildren.length > maxCandidatesPerRoot;
487+if (nestedSuspicious) {
488+skillsLogger.warn(
489+"Nested skills directory looks suspiciously large, truncating discovery.",
490+{
491+dir: params.dir,
492+ baseDir,
493+nestedDir: skillDir,
494+nestedChildDirCount: nestedChildren.length,
495+maxCandidatesPerRoot: limits.maxCandidatesPerRoot,
496+maxSkillsLoadedPerSource: limits.maxSkillsLoadedPerSource,
497+},
498+);
499+}
500+const limitedNested = nestedChildren.toSorted().slice(0, maxCandidatesPerRoot);
501+for (const nestedName of limitedNested) {
502+const nestedDir = path.join(skillDir, nestedName);
503+const nestedSkillMd = path.join(nestedDir, "SKILL.md");
504+if (fs.existsSync(nestedSkillMd)) {
505+const nestedDirRealPath = resolveContainedSkillPath({
506+source: params.source,
507+ rootDir,
508+rootRealPath: baseDirRealPath,
509+candidatePath: nestedDir,
510+});
511+const nestedSkillMdRealPath = resolveContainedSkillPath({
512+source: params.source,
513+ rootDir,
514+rootRealPath: baseDirRealPath,
515+candidatePath: nestedSkillMd,
516+});
517+if (nestedDirRealPath && nestedSkillMdRealPath) {
518+candidateDirs.push({
519+skillDir: nestedDir,
520+name: `${name}/${nestedName}`,
521+skillMdRealPath: nestedSkillMdRealPath,
522+});
523+}
524+}
525+if (candidateDirs.length >= maxSkillsLoadedPerSource) {
526+break;
527+}
528+}
479529}
480-const skillMdRealPath = resolveContainedSkillPath({
481-source: params.source,
482- rootDir,
483-rootRealPath: baseDirRealPath,
484-candidatePath: skillMd,
485-});
486-if (!skillMdRealPath) {
487-continue;
530+if (candidateDirs.length >= maxSkillsLoadedPerSource) {
531+break;
488532}
533+}
534+535+for (const { skillDir, name, skillMdRealPath } of candidateDirs) {
489536try {
490537const size = fs.statSync(skillMdRealPath).size;
491538if (size > limits.maxSkillFileBytes) {
492539skillsLogger.warn("Skipping skill due to oversized SKILL.md.", {
493540skill: name,
494-filePath: skillMd,
541+filePath: path.join(skillDir, "SKILL.md"),
495542 size,
496543maxSkillFileBytes: limits.maxSkillFileBytes,
497544});
@@ -501,30 +548,24 @@ function loadSkillEntries(
501548continue;
502549}
503550504-const loaded = loadSkillsFromDirSafe({
505-dir: skillDir,
506-source: params.source,
507-maxBytes: limits.maxSkillFileBytes,
508-});
509551loadedSkills.push(
510- ...filterLoadedSkillRecordsInsideRoot({
511-records: unwrapLoadedSkillRecords(loaded),
552+ ...loadContainedSkillRecords({
553+skillDir,
512554source: params.source,
513- rootDir,
514-rootRealPath: baseDirRealPath,
555+maxSkillFileBytes: limits.maxSkillFileBytes,
515556}),
516557);
517558518-if (loadedSkills.length >= limits.maxSkillsLoadedPerSource) {
559+if (loadedSkills.length >= maxSkillsLoadedPerSource) {
519560break;
520561}
521562}
522563523-if (loadedSkills.length > limits.maxSkillsLoadedPerSource) {
564+if (loadedSkills.length > maxSkillsLoadedPerSource) {
524565return loadedSkills
525566.slice()
526567.sort((a, b) => a.skill.name.localeCompare(b.skill.name, "en"))
527-.slice(0, limits.maxSkillsLoadedPerSource);
568+.slice(0, maxSkillsLoadedPerSource);
528569}
529570530571return loadedSkills;
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。