
























@@ -0,0 +1,325 @@
1+import { existsSync, readFileSync, readdirSync } from "node:fs";
2+import { dirname, relative, resolve, sep } from "node:path";
3+import { fileURLToPath } from "node:url";
4+5+const DEFAULT_REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
6+7+const COMPAT_CONFIG_API_FILES = new Set([
8+"src/config/config.ts",
9+"src/config/io.ts",
10+"src/config/mutate.ts",
11+"src/memory-host-sdk/runtime-core.ts",
12+"src/plugin-sdk/browser-config-runtime.ts",
13+"src/plugin-sdk/config-runtime.ts",
14+"src/plugin-sdk/memory-core.ts",
15+"src/plugin-sdk/memory-core-host-runtime-core.ts",
16+"src/plugins/contracts/deprecated-internal-config-api.test.ts",
17+"src/plugins/runtime/runtime-config.test.ts",
18+"src/plugins/runtime/runtime-config.ts",
19+"src/plugins/runtime/types-core.ts",
20+]);
21+22+const AMBIENT_RUNTIME_LOAD_CONFIG_COMPAT_FILES = new Set([
23+"src/plugins/runtime/load-context.ts",
24+"src/plugins/runtime/runtime-config.ts",
25+"src/plugins/runtime/runtime-plugin-boundary.ts",
26+]);
27+28+const PROCESS_BOUNDARY_DIRECT_CONFIG_LOAD_FILES = new Set([
29+"src/cli/banner-config-lite.ts",
30+"src/cli/daemon-cli/status.gather.ts",
31+]);
32+33+function collectTypeScriptFiles(dir) {
34+if (!existsSync(dir)) {
35+return [];
36+}
37+const entries = readdirSync(dir, { withFileTypes: true });
38+const files = [];
39+for (const entry of entries) {
40+const fullPath = resolve(dir, entry.name);
41+if (entry.isDirectory()) {
42+if (entry.name === "dist" || entry.name === "node_modules") {
43+continue;
44+}
45+files.push(...collectTypeScriptFiles(fullPath));
46+continue;
47+}
48+if (entry.isFile() && entry.name.endsWith(".ts")) {
49+files.push(fullPath);
50+}
51+}
52+return files;
53+}
54+55+function repoRelative(repoRoot, filePath) {
56+return relative(repoRoot, filePath).split(sep).join("/");
57+}
58+59+function isProductionExtensionFile(relPath) {
60+if (
61+relPath.includes("/test-support/") ||
62+relPath.includes(".test.") ||
63+relPath.includes(".live.test.") ||
64+relPath.includes(".test-d.") ||
65+relPath.includes(".test-harness.") ||
66+relPath.includes(".test-shared.") ||
67+relPath.endsWith("-test-helpers.ts") ||
68+relPath.endsWith("-test-support.ts")
69+) {
70+return false;
71+}
72+return true;
73+}
74+75+function isTestOrHarnessFile(relPath) {
76+return (
77+relPath.includes("test-support") ||
78+relPath.includes("/test-support/") ||
79+relPath.includes("/test-helpers/") ||
80+relPath.includes(".test.") ||
81+relPath.includes(".live.test.") ||
82+relPath.includes(".test-d.") ||
83+relPath.includes(".test-harness.") ||
84+relPath.includes(".test-shared.") ||
85+relPath.endsWith(".test-helpers.ts") ||
86+relPath.endsWith(".test-support.ts") ||
87+relPath.endsWith("-test-helpers.ts") ||
88+relPath.endsWith("-test-support.ts")
89+);
90+}
91+92+function isCompatConfigApiFile(relPath) {
93+return COMPAT_CONFIG_API_FILES.has(relPath);
94+}
95+96+function isAmbientRuntimeConfigCompatFile(relPath) {
97+return AMBIENT_RUNTIME_LOAD_CONFIG_COMPAT_FILES.has(relPath);
98+}
99+100+function findLineNumbers(source, pattern) {
101+const lines = source.split(/\r?\n/);
102+return lines.flatMap((line, index) => (pattern.test(line) ? [index + 1] : []));
103+}
104+105+function findMatchLineNumbers(source, pattern) {
106+const flags = pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`;
107+const regex = new RegExp(pattern.source, flags);
108+const lines = [];
109+for (let match = regex.exec(source); match; match = regex.exec(source)) {
110+lines.push(source.slice(0, match.index).split(/\r?\n/).length);
111+}
112+return lines;
113+}
114+115+function findNonCommentLineNumbers(source, pattern) {
116+return source.split(/\r?\n/).flatMap((line, index) => {
117+const trimmed = line.trimStart();
118+if (trimmed.startsWith("//") || trimmed.startsWith("*")) {
119+return [];
120+}
121+return pattern.test(line) ? [index + 1] : [];
122+});
123+}
124+125+function repoCodeRoots(repoRoot) {
126+return ["src", "extensions", "packages", "test", "scripts"].map((entry) =>
127+resolve(repoRoot, entry),
128+);
129+}
130+131+function pushDeprecatedRuntimeApiViolations(violations, files) {
132+const guards = [
133+{
134+pattern:
135+/(?:api\.runtime\.config|core\.config|runtime\.config|get[A-Za-z0-9]+Runtime\(\)\.config|rt\.config|configApi)\??\.loadConfig\b/,
136+replacement: "use runtime.config.current() or pass the already loaded config",
137+},
138+{
139+pattern:
140+/(?:api\.runtime\.config|core\.config|runtime\.config|get[A-Za-z0-9]+Runtime\(\)\.config|rt\.config|configApi)\??\.writeConfigFile\b/,
141+replacement:
142+"use runtime.config.mutateConfigFile(...) or replaceConfigFile(...) with afterWrite",
143+},
144+];
145+146+for (const { filePath, relPath } of files) {
147+const source = readFileSync(filePath, "utf8");
148+for (const guard of guards) {
149+for (const line of findMatchLineNumbers(source, guard.pattern)) {
150+violations.push(`${relPath}:${line} ${guard.replacement}`);
151+}
152+}
153+}
154+}
155+156+export function collectDeprecatedInternalConfigApiViolations({
157+ repoRoot = DEFAULT_REPO_ROOT,
158+} = {}) {
159+const srcRoot = resolve(repoRoot, "src");
160+const extensionsRoot = resolve(repoRoot, "extensions");
161+const gatewayServerMethodsRoot = resolve(srcRoot, "gateway/server-methods");
162+const ambientRuntimeConfigRoots = [
163+"src/gateway",
164+"src/auto-reply",
165+"src/agents",
166+"src/infra",
167+"src/mcp",
168+"src/plugins/runtime",
169+"src/config/sessions",
170+].map((entry) => resolve(repoRoot, entry));
171+172+const violations = [];
173+174+const productionExtensionFiles = collectTypeScriptFiles(extensionsRoot)
175+.map((filePath) => ({ filePath, relPath: repoRelative(repoRoot, filePath) }))
176+.filter(({ relPath }) => isProductionExtensionFile(relPath));
177+pushDeprecatedRuntimeApiViolations(violations, productionExtensionFiles);
178+179+for (const { filePath, relPath } of productionExtensionFiles) {
180+const source = readFileSync(filePath, "utf8");
181+const guards = [
182+{
183+pattern:
184+/\b(?:import|export)\s+(?:type\s+)?\{[^}]*\bloadConfig\b[^}]*\}\s+from\s+["']openclaw\/plugin-sdk\/(?:browser-config-runtime|config-runtime|memory-core-host-runtime-core)["']/,
185+replacement:
186+"use getRuntimeConfig(), runtime.config.current(), or pass the already loaded config",
187+},
188+{
189+pattern: /(?<!\.)\bloadConfig\s*\(/,
190+replacement: "use getRuntimeConfig(), runtime.config.current(), or passed config",
191+},
192+{
193+pattern: /\bcreateConfigIO\b|\.\s*loadConfig\s*\(/,
194+replacement: "use runtime.config.current(), getRuntimeConfig(), or passed config",
195+},
196+{
197+pattern: /\bwriteConfigFile\s*\(/,
198+replacement: "use mutateConfigFile(...) or replaceConfigFile(...) with afterWrite",
199+},
200+];
201+for (const guard of guards) {
202+for (const line of findLineNumbers(source, guard.pattern)) {
203+violations.push(`${relPath}:${line} ${guard.replacement}`);
204+}
205+}
206+}
207+208+const repoFiles = repoCodeRoots(repoRoot)
209+.flatMap(collectTypeScriptFiles)
210+.map((filePath) => ({ filePath, relPath: repoRelative(repoRoot, filePath) }));
211+212+pushDeprecatedRuntimeApiViolations(
213+violations,
214+repoFiles.filter(({ relPath }) => !isCompatConfigApiFile(relPath)),
215+);
216+217+for (const { filePath, relPath } of repoFiles.filter(
218+({ relPath }) => !isCompatConfigApiFile(relPath),
219+)) {
220+const source = readFileSync(filePath, "utf8");
221+const guards = [
222+{
223+pattern:
224+/\b(?:import|export)\s+(?:type\s+)?\{[\s\S]*?\b(?:loadConfig|writeConfigFile)\b[\s\S]*?\}\s+from\s+["']openclaw\/plugin-sdk\/(?:browser-config-runtime|config-runtime|memory-core-host-runtime-core|memory-core)["']/,
225+replacement:
226+"use getRuntimeConfig(), runtime.config.current(), or mutation helpers with afterWrite",
227+},
228+{
229+pattern:
230+/ReturnType<typeof import\(["']openclaw\/plugin-sdk\/(?:browser-config-runtime|config-runtime|memory-core-host-runtime-core|memory-core)["']\)\.(?:loadConfig|writeConfigFile)>/,
231+replacement: "use OpenClawConfig or the explicit mutation helper type",
232+},
233+];
234+for (const guard of guards) {
235+for (const line of findMatchLineNumbers(source, guard.pattern)) {
236+violations.push(`${relPath}:${line} ${guard.replacement}`);
237+}
238+}
239+}
240+241+for (const { filePath, relPath } of repoFiles.filter(
242+({ relPath }) =>
243+!isTestOrHarnessFile(relPath) &&
244+!isCompatConfigApiFile(relPath) &&
245+!relPath.startsWith("test/"),
246+)) {
247+const source = readFileSync(filePath, "utf8");
248+const importPattern =
249+/\bimport\s+\{[\s\S]*?\bwriteConfigFile\b[\s\S]*?\}\s+from\s+["'][^"']*(?:config\/config|config\/io)\.js["']/;
250+const dynamicImportPattern =
251+/\bconst\s+\{[\s\S]*?\bwriteConfigFile\b[\s\S]*?\}\s*=\s*await\s+import\(["'][^"']*(?:config\/config|config\/io)\.js["']\)/;
252+const directMethodPattern = /\.\s*writeConfigFile\s*\(/;
253+for (const pattern of [importPattern, dynamicImportPattern]) {
254+for (const line of findMatchLineNumbers(source, pattern)) {
255+violations.push(
256+`${relPath}:${line} use replaceConfigFile(...) or mutateConfigFile(...) with afterWrite`,
257+);
258+}
259+}
260+for (const line of findNonCommentLineNumbers(source, directMethodPattern)) {
261+violations.push(
262+`${relPath}:${line} use replaceConfigFile(...) or mutateConfigFile(...) with afterWrite`,
263+);
264+}
265+}
266+267+for (const { filePath, relPath } of repoFiles.filter(
268+({ relPath }) =>
269+!isTestOrHarnessFile(relPath) &&
270+!isCompatConfigApiFile(relPath) &&
271+!PROCESS_BOUNDARY_DIRECT_CONFIG_LOAD_FILES.has(relPath) &&
272+!relPath.startsWith("test/"),
273+)) {
274+const source = readFileSync(filePath, "utf8");
275+for (const line of findNonCommentLineNumbers(source, /(?<!\.)\bloadConfig\s*\(/)) {
276+violations.push(
277+`${relPath}:${line} use a passed cfg, context.getRuntimeConfig(), or getRuntimeConfig() at an explicit process boundary`,
278+);
279+}
280+for (const line of findNonCommentLineNumbers(source, /\.\s*loadConfig\s*\(/)) {
281+violations.push(
282+`${relPath}:${line} use a passed cfg, context.getRuntimeConfig(), or getRuntimeConfig() at an explicit process boundary`,
283+);
284+}
285+}
286+287+for (const { filePath, relPath } of collectTypeScriptFiles(gatewayServerMethodsRoot)
288+.map((filePath) => ({ filePath, relPath: repoRelative(repoRoot, filePath) }))
289+.filter(({ relPath }) => !isTestOrHarnessFile(relPath))) {
290+const source = readFileSync(filePath, "utf8");
291+const importPattern =
292+/\bimport\s+\{[\s\S]*?\bloadConfig\b[\s\S]*?\}\s+from\s+["'][^"']*(?:config\/config|config\/io)\.js["']/;
293+for (const line of findMatchLineNumbers(source, importPattern)) {
294+violations.push(
295+`${relPath}:${line} use context.getRuntimeConfig() in gateway request handlers`,
296+);
297+}
298+for (const line of findNonCommentLineNumbers(source, /(?<!\.)\bloadConfig\s*\(/)) {
299+violations.push(
300+`${relPath}:${line} use context.getRuntimeConfig() in gateway request handlers`,
301+);
302+}
303+}
304+305+for (const { filePath, relPath } of ambientRuntimeConfigRoots
306+.flatMap(collectTypeScriptFiles)
307+.map((filePath) => ({ filePath, relPath: repoRelative(repoRoot, filePath) }))
308+.filter(
309+({ relPath }) =>
310+!isTestOrHarnessFile(relPath) &&
311+!isCompatConfigApiFile(relPath) &&
312+!isAmbientRuntimeConfigCompatFile(relPath),
313+)) {
314+const source = readFileSync(filePath, "utf8");
315+const loadConfigLines = findNonCommentLineNumbers(source, /(?<!\.)\bloadConfig\s*\(/);
316+if (loadConfigLines.length === 0) {
317+continue;
318+}
319+violations.push(
320+`${relPath}:${loadConfigLines.join(",")} has ${loadConfigLines.length} ambient loadConfig() calls. Pass cfg through the call path, use context.getRuntimeConfig(), or use getRuntimeConfig() at a process boundary.`,
321+);
322+}
323+324+return violations;
325+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。