




















@@ -126,6 +126,8 @@ const DEFAULT_MAX_SKILLS_LOADED_PER_SOURCE = 200;
126126const DEFAULT_MAX_SKILLS_IN_PROMPT = 150;
127127const DEFAULT_MAX_SKILLS_PROMPT_CHARS = 18_000;
128128const DEFAULT_MAX_SKILL_FILE_BYTES = 256_000;
129+const DEFAULT_MIN_RAW_ENTRIES_PER_DIRECTORY_SCAN = 1_000;
130+const DEFAULT_MAX_RAW_ENTRIES_PER_DIRECTORY_SCAN = 10_000;
129131130132type ResolvedSkillsLimits = {
131133maxCandidatesPerRoot: number;
@@ -171,13 +173,14 @@ function resolveSkillsLimits(config?: OpenClawConfig, agentId?: string): Resolve
171173function listChildDirectories(
172174dir: string,
173175opts?: {
174-maxEntriesToScan?: number;
176+maxCandidateDirs?: number;
177+maxRawEntriesToScan?: number;
175178},
176179): ChildDirectoryScan {
177-const maxEntriesToScan =
178-opts?.maxEntriesToScan === undefined
179- ? Number.POSITIVE_INFINITY
180- : Math.max(0, opts.maxEntriesToScan);
180+const maxRawEntriesToScan =
181+opts?.maxRawEntriesToScan === undefined
182+ ? resolveRawEntryScanLimit(opts?.maxCandidateDirs)
183+ : Math.max(0, opts.maxRawEntriesToScan);
181184try {
182185const dirs: string[] = [];
183186let scannedEntryCount = 0;
@@ -186,7 +189,7 @@ function listChildDirectories(
186189try {
187190let entry: Dirent | null;
188191while ((entry = handle.readSync()) !== null) {
189-if (scannedEntryCount >= maxEntriesToScan) {
192+if (scannedEntryCount >= maxRawEntriesToScan) {
190193truncated = true;
191194break;
192195}
@@ -218,6 +221,20 @@ function listChildDirectories(
218221}
219222}
220223224+function resolveRawEntryScanLimit(maxCandidateDirs: number | undefined): number {
225+if (maxCandidateDirs === undefined) {
226+return Number.POSITIVE_INFINITY;
227+}
228+const normalized = Math.max(0, maxCandidateDirs);
229+if (normalized === 0) {
230+return 0;
231+}
232+return Math.min(
233+DEFAULT_MAX_RAW_ENTRIES_PER_DIRECTORY_SCAN,
234+Math.max(DEFAULT_MIN_RAW_ENTRIES_PER_DIRECTORY_SCAN, normalized * 10),
235+);
236+}
237+221238function tryRealpath(filePath: string): string | null {
222239try {
223240return fs.realpathSync(filePath);
@@ -340,7 +357,7 @@ function resolveNestedSkillsRoot(
340357// Heuristic: if `dir/skills/*/SKILL.md` exists for any entry, treat `dir/skills` as the real root.
341358// Note: don't stop at 25, but keep a cap to avoid pathological scans.
342359const scanLimit = Math.max(0, opts?.maxEntriesToScan ?? 100);
343-const nestedDirs = listChildDirectories(nested, { maxEntriesToScan: scanLimit }).dirs;
360+const nestedDirs = listChildDirectories(nested, { maxCandidateDirs: scanLimit }).dirs;
344361345362for (const name of nestedDirs) {
346363const skillMd = path.join(nested, name, "SKILL.md");
@@ -455,39 +472,63 @@ function loadSkillEntries(
455472456473const maxCandidatesPerRoot = Math.max(0, limits.maxCandidatesPerRoot);
457474const maxSkillsLoadedPerSource = Math.max(0, limits.maxSkillsLoadedPerSource);
458-const maxCandidates = Math.min(maxCandidatesPerRoot, maxSkillsLoadedPerSource);
459475const childDirScan = listChildDirectories(baseDir, {
460-maxEntriesToScan: maxCandidatesPerRoot,
476+maxCandidateDirs: maxCandidatesPerRoot,
461477});
462478const childDirs = childDirScan.dirs;
463479const suspicious = childDirScan.truncated;
464-const limitedChildren = childDirs.toSorted().slice(0, maxCandidates);
480+const limitedChildren =
481+maxSkillsLoadedPerSource === 0 ? [] : childDirs.toSorted().slice(0, maxCandidatesPerRoot);
465482466483if (suspicious) {
467484skillsLogger.warn("Skills root looks suspiciously large, truncating discovery.", {
468485dir: params.dir,
469486 baseDir,
470487childDirCount: childDirs.length,
471488scannedEntryCount: childDirScan.scannedEntryCount,
472-maxEntriesToScan: maxCandidatesPerRoot,
489+maxEntriesToScan: resolveRawEntryScanLimit(maxCandidatesPerRoot),
473490maxCandidatesPerRoot: limits.maxCandidatesPerRoot,
474491maxSkillsLoadedPerSource: limits.maxSkillsLoadedPerSource,
475492});
476-} else if (childDirs.length > maxCandidates) {
493+} else if (childDirs.length > maxCandidatesPerRoot) {
477494skillsLogger.warn("Skills root has many entries, truncating discovery.", {
478495dir: params.dir,
479496 baseDir,
480497childDirCount: childDirs.length,
498+maxCandidatesPerRoot: limits.maxCandidatesPerRoot,
481499maxSkillsLoadedPerSource: limits.maxSkillsLoadedPerSource,
482500});
483501}
484502485503const loadedSkills: LoadedSkillRecord[] = [];
504+const loadCandidateSkill = ({ skillDir, name, skillMdRealPath }: CandidateSkillDir) => {
505+try {
506+const size = fs.statSync(skillMdRealPath).size;
507+if (size > limits.maxSkillFileBytes) {
508+skillsLogger.warn("Skipping skill due to oversized SKILL.md.", {
509+skill: name,
510+filePath: path.join(skillDir, "SKILL.md"),
511+ size,
512+maxSkillFileBytes: limits.maxSkillFileBytes,
513+});
514+return;
515+}
516+} catch {
517+return;
518+}
519+520+loadedSkills.push(
521+ ...loadContainedSkillRecords({
522+ skillDir,
523+source: params.source,
524+maxSkillFileBytes: limits.maxSkillFileBytes,
525+}),
526+);
527+};
486528487529// Consider immediate subfolders that look like skills (have SKILL.md) and are under size cap.
488530// When an immediate subfolder does NOT have a SKILL.md, check one level deeper for grouped
489531// skill directories (e.g. ~/.openclaw/skills/coze/koze-retrieval/SKILL.md).
490-const candidateDirs: CandidateSkillDir[] = [];
491532for (const name of limitedChildren) {
492533const skillDir = path.join(baseDir, name);
493534const skillDirRealPath = resolveContainedSkillPath({
@@ -508,13 +549,13 @@ function loadSkillEntries(
508549candidatePath: skillMd,
509550});
510551if (skillMdRealPath) {
511-candidateDirs.push({ skillDir, name, skillMdRealPath });
552+loadCandidateSkill({ skillDir, name, skillMdRealPath });
512553}
513554} else {
514555// No SKILL.md here — check one level deeper for grouped skill directories.
515556// Apply the same per-root cap as the outer scan to avoid scanning huge nested trees.
516557const nestedChildScan = listChildDirectories(skillDir, {
517-maxEntriesToScan: maxCandidatesPerRoot,
558+maxCandidateDirs: maxCandidatesPerRoot,
518559});
519560const nestedChildren = nestedChildScan.dirs;
520561const nestedSuspicious = nestedChildScan.truncated;
@@ -527,13 +568,22 @@ function loadSkillEntries(
527568nestedDir: skillDir,
528569nestedChildDirCount: nestedChildren.length,
529570scannedEntryCount: nestedChildScan.scannedEntryCount,
530-maxEntriesToScan: maxCandidatesPerRoot,
571+maxEntriesToScan: resolveRawEntryScanLimit(maxCandidatesPerRoot),
531572maxCandidatesPerRoot: limits.maxCandidatesPerRoot,
532573maxSkillsLoadedPerSource: limits.maxSkillsLoadedPerSource,
533574},
534575);
576+} else if (nestedChildren.length > maxCandidatesPerRoot) {
577+skillsLogger.warn("Nested skills directory has many entries, truncating discovery.", {
578+dir: params.dir,
579+ baseDir,
580+nestedDir: skillDir,
581+nestedChildDirCount: nestedChildren.length,
582+maxCandidatesPerRoot: limits.maxCandidatesPerRoot,
583+maxSkillsLoadedPerSource: limits.maxSkillsLoadedPerSource,
584+});
535585}
536-const limitedNested = nestedChildren.toSorted();
586+const limitedNested = nestedChildren.toSorted().slice(0, maxCandidatesPerRoot);
537587for (const nestedName of limitedNested) {
538588const nestedDir = path.join(skillDir, nestedName);
539589const nestedSkillMd = path.join(nestedDir, "SKILL.md");
@@ -551,47 +601,18 @@ function loadSkillEntries(
551601candidatePath: nestedSkillMd,
552602});
553603if (nestedDirRealPath && nestedSkillMdRealPath) {
554-candidateDirs.push({
604+loadCandidateSkill({
555605skillDir: nestedDir,
556606name: `${name}/${nestedName}`,
557607skillMdRealPath: nestedSkillMdRealPath,
558608});
559609}
560610}
561-if (candidateDirs.length >= maxSkillsLoadedPerSource) {
611+if (loadedSkills.length >= maxSkillsLoadedPerSource) {
562612break;
563613}
564614}
565615}
566-if (candidateDirs.length >= maxSkillsLoadedPerSource) {
567-break;
568-}
569-}
570-571-for (const { skillDir, name, skillMdRealPath } of candidateDirs) {
572-try {
573-const size = fs.statSync(skillMdRealPath).size;
574-if (size > limits.maxSkillFileBytes) {
575-skillsLogger.warn("Skipping skill due to oversized SKILL.md.", {
576-skill: name,
577-filePath: path.join(skillDir, "SKILL.md"),
578- size,
579-maxSkillFileBytes: limits.maxSkillFileBytes,
580-});
581-continue;
582-}
583-} catch {
584-continue;
585-}
586-587-loadedSkills.push(
588- ...loadContainedSkillRecords({
589- skillDir,
590-source: params.source,
591-maxSkillFileBytes: limits.maxSkillFileBytes,
592-}),
593-);
594-595616if (loadedSkills.length >= maxSkillsLoadedPerSource) {
596617break;
597618}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。