

























@@ -1,4 +1,4 @@
1-import fs from "node:fs";
1+import fs, { type Dirent } from "node:fs";
22import os from "node:os";
33import path from "node:path";
44import type { OpenClawConfig } from "../../config/types.openclaw.js";
@@ -146,6 +146,12 @@ type CandidateSkillDir = {
146146skillMdRealPath: string;
147147};
148148149+type ChildDirectoryScan = {
150+dirs: string[];
151+scannedEntryCount: number;
152+truncated: boolean;
153+};
154+149155function resolveSkillsLimits(config?: OpenClawConfig, agentId?: string): ResolvedSkillsLimits {
150156const limits = config?.skills?.limits;
151157const agentSkillsLimits = resolveEffectiveAgentSkillsLimits(config, agentId);
@@ -162,31 +168,53 @@ function resolveSkillsLimits(config?: OpenClawConfig, agentId?: string): Resolve
162168};
163169}
164170165-function listChildDirectories(dir: string): string[] {
171+function listChildDirectories(
172+dir: string,
173+opts?: {
174+maxEntriesToScan?: number;
175+},
176+): ChildDirectoryScan {
177+const maxEntriesToScan =
178+opts?.maxEntriesToScan === undefined
179+ ? Number.POSITIVE_INFINITY
180+ : Math.max(0, opts.maxEntriesToScan);
166181try {
167-const entries = fs.readdirSync(dir, { withFileTypes: true });
168182const dirs: string[] = [];
169-for (const entry of entries) {
170-if (entry.name.startsWith(".")) continue;
171-if (entry.name === "node_modules") continue;
172-const fullPath = path.join(dir, entry.name);
173-if (entry.isDirectory()) {
174-dirs.push(entry.name);
175-continue;
176-}
177-if (entry.isSymbolicLink()) {
178-try {
179-if (fs.statSync(fullPath).isDirectory()) {
180-dirs.push(entry.name);
183+let scannedEntryCount = 0;
184+let truncated = false;
185+const handle = fs.opendirSync(dir);
186+try {
187+let entry: Dirent | null;
188+while ((entry = handle.readSync()) !== null) {
189+if (scannedEntryCount >= maxEntriesToScan) {
190+truncated = true;
191+break;
192+}
193+scannedEntryCount += 1;
194+195+if (entry.name.startsWith(".")) continue;
196+if (entry.name === "node_modules") continue;
197+const fullPath = path.join(dir, entry.name);
198+if (entry.isDirectory()) {
199+dirs.push(entry.name);
200+continue;
201+}
202+if (entry.isSymbolicLink()) {
203+try {
204+if (fs.statSync(fullPath).isDirectory()) {
205+dirs.push(entry.name);
206+}
207+} catch {
208+// ignore broken symlinks
181209}
182-} catch {
183-// ignore broken symlinks
184210}
185211}
212+} finally {
213+handle.closeSync();
186214}
187-return dirs;
215+return { dirs, scannedEntryCount, truncated };
188216} catch {
189-return [];
217+return { dirs: [], scannedEntryCount: 0, truncated: false };
190218}
191219}
192220@@ -311,11 +339,10 @@ function resolveNestedSkillsRoot(
311339312340// Heuristic: if `dir/skills/*/SKILL.md` exists for any entry, treat `dir/skills` as the real root.
313341// Note: don't stop at 25, but keep a cap to avoid pathological scans.
314-const nestedDirs = listChildDirectories(nested);
315342const scanLimit = Math.max(0, opts?.maxEntriesToScan ?? 100);
316-const toScan = scanLimit === 0 ? [] : nestedDirs.slice(0, Math.min(nestedDirs.length, scanLimit));
343+const nestedDirs = listChildDirectories(nested, { maxEntriesToScan: scanLimit }).dirs;
317344318-for (const name of toScan) {
345+for (const name of nestedDirs) {
319346const skillMd = path.join(nested, name, "SKILL.md");
320347if (fs.existsSync(skillMd)) {
321348return { baseDir: nested, note: `Detected nested skills root at ${nested}` };
@@ -426,19 +453,23 @@ function loadSkillEntries(
426453});
427454}
428455429-const childDirs = listChildDirectories(baseDir);
430456const maxCandidatesPerRoot = Math.max(0, limits.maxCandidatesPerRoot);
431457const maxSkillsLoadedPerSource = Math.max(0, limits.maxSkillsLoadedPerSource);
432-const suspicious = childDirs.length > maxCandidatesPerRoot;
433-434458const maxCandidates = Math.min(maxCandidatesPerRoot, maxSkillsLoadedPerSource);
459+const childDirScan = listChildDirectories(baseDir, {
460+maxEntriesToScan: maxCandidatesPerRoot,
461+});
462+const childDirs = childDirScan.dirs;
463+const suspicious = childDirScan.truncated;
435464const limitedChildren = childDirs.toSorted().slice(0, maxCandidates);
436465437466if (suspicious) {
438467skillsLogger.warn("Skills root looks suspiciously large, truncating discovery.", {
439468dir: params.dir,
440469 baseDir,
441470childDirCount: childDirs.length,
471+scannedEntryCount: childDirScan.scannedEntryCount,
472+maxEntriesToScan: maxCandidatesPerRoot,
442473maxCandidatesPerRoot: limits.maxCandidatesPerRoot,
443474maxSkillsLoadedPerSource: limits.maxSkillsLoadedPerSource,
444475});
@@ -482,8 +513,11 @@ function loadSkillEntries(
482513} else {
483514// No SKILL.md here — check one level deeper for grouped skill directories.
484515// 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;
516+const nestedChildScan = listChildDirectories(skillDir, {
517+maxEntriesToScan: maxCandidatesPerRoot,
518+});
519+const nestedChildren = nestedChildScan.dirs;
520+const nestedSuspicious = nestedChildScan.truncated;
487521if (nestedSuspicious) {
488522skillsLogger.warn(
489523"Nested skills directory looks suspiciously large, truncating discovery.",
@@ -492,12 +526,14 @@ function loadSkillEntries(
492526 baseDir,
493527nestedDir: skillDir,
494528nestedChildDirCount: nestedChildren.length,
529+scannedEntryCount: nestedChildScan.scannedEntryCount,
530+maxEntriesToScan: maxCandidatesPerRoot,
495531maxCandidatesPerRoot: limits.maxCandidatesPerRoot,
496532maxSkillsLoadedPerSource: limits.maxSkillsLoadedPerSource,
497533},
498534);
499535}
500-const limitedNested = nestedChildren.toSorted().slice(0, maxCandidatesPerRoot);
536+const limitedNested = nestedChildren.toSorted();
501537for (const nestedName of limitedNested) {
502538const nestedDir = path.join(skillDir, nestedName);
503539const nestedSkillMd = path.join(nestedDir, "SKILL.md");
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。