mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-11 03:46:39 +02:00
fix(config): apply filtered doctor compat at read time
This commit is contained in:
1
extensions/discord/doctor-contract-api.ts
Normal file
1
extensions/discord/doctor-contract-api.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { normalizeCompatibilityConfig, legacyConfigRules } from "./src/doctor-contract.js";
|
||||
@@ -1,12 +1,32 @@
|
||||
import { resolveChannelPreviewStreamMode } from "openclaw/plugin-sdk/channel-streaming";
|
||||
|
||||
export type DiscordPreviewStreamMode = "off" | "partial" | "block";
|
||||
|
||||
function parsePreviewStreamingMode(value: unknown): DiscordPreviewStreamMode | undefined {
|
||||
return value === "off" || value === "partial" || value === "block" ? value : undefined;
|
||||
}
|
||||
|
||||
export function resolveDiscordPreviewStreamMode(
|
||||
params: {
|
||||
streamMode?: unknown;
|
||||
streaming?: unknown;
|
||||
} = {},
|
||||
): DiscordPreviewStreamMode {
|
||||
return resolveChannelPreviewStreamMode(params, "off");
|
||||
const parsedStreaming =
|
||||
params.streaming && typeof params.streaming === "object" && !Array.isArray(params.streaming)
|
||||
? parsePreviewStreamingMode(
|
||||
(params.streaming as Record<string, unknown>).mode ??
|
||||
(params.streaming as Record<string, unknown>).streaming,
|
||||
)
|
||||
: parsePreviewStreamingMode(params.streaming);
|
||||
if (parsedStreaming) {
|
||||
return parsedStreaming;
|
||||
}
|
||||
|
||||
const legacy = parsePreviewStreamingMode(params.streamMode);
|
||||
if (legacy) {
|
||||
return legacy;
|
||||
}
|
||||
if (typeof params.streaming === "boolean") {
|
||||
return params.streaming ? "partial" : "off";
|
||||
}
|
||||
return "off";
|
||||
}
|
||||
|
||||
@@ -1,9 +1,27 @@
|
||||
import type { LegacyConfigRule } from "../../config/legacy.shared.js";
|
||||
import { iterateBootstrapChannelPlugins } from "./bootstrap-registry.js";
|
||||
import { getBootstrapChannelPlugin } from "./bootstrap-registry.js";
|
||||
import type { ChannelId } from "./types.js";
|
||||
|
||||
export function collectChannelLegacyConfigRules(): LegacyConfigRule[] {
|
||||
function collectConfiguredChannelIds(raw: unknown): ChannelId[] {
|
||||
if (!raw || typeof raw !== "object") {
|
||||
return [];
|
||||
}
|
||||
const channels = (raw as { channels?: unknown }).channels;
|
||||
if (!channels || typeof channels !== "object" || Array.isArray(channels)) {
|
||||
return [];
|
||||
}
|
||||
return Object.keys(channels)
|
||||
.filter((channelId) => channelId !== "defaults")
|
||||
.map((channelId) => channelId as ChannelId);
|
||||
}
|
||||
|
||||
export function collectChannelLegacyConfigRules(raw?: unknown): LegacyConfigRule[] {
|
||||
const rules: LegacyConfigRule[] = [];
|
||||
for (const plugin of iterateBootstrapChannelPlugins()) {
|
||||
for (const channelId of collectConfiguredChannelIds(raw)) {
|
||||
const plugin = getBootstrapChannelPlugin(channelId);
|
||||
if (!plugin) {
|
||||
continue;
|
||||
}
|
||||
rules.push(...(plugin.doctor?.legacyConfigRules ?? []));
|
||||
}
|
||||
return rules;
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import { iterateBootstrapChannelPlugins } from "../../../channels/plugins/bootstrap-registry.js";
|
||||
import type { OpenClawConfig } from "../../../config/types.js";
|
||||
import {
|
||||
applyPluginDoctorCompatibilityMigrations,
|
||||
collectRelevantDoctorPluginIds,
|
||||
} from "../../../plugins/doctor-contract-registry.js";
|
||||
|
||||
export function applyChannelDoctorCompatibilityMigrations(cfg: Record<string, unknown>): {
|
||||
next: Record<string, unknown>;
|
||||
changes: string[];
|
||||
} {
|
||||
let nextCfg = cfg as OpenClawConfig & Record<string, unknown>;
|
||||
const changes: string[] = [];
|
||||
for (const plugin of iterateBootstrapChannelPlugins()) {
|
||||
const mutation = plugin.doctor?.normalizeCompatibilityConfig?.({ cfg: nextCfg });
|
||||
if (!mutation || mutation.changes.length === 0) {
|
||||
continue;
|
||||
}
|
||||
nextCfg = mutation.config as OpenClawConfig & Record<string, unknown>;
|
||||
changes.push(...mutation.changes);
|
||||
}
|
||||
return { next: nextCfg, changes };
|
||||
const compat = applyPluginDoctorCompatibilityMigrations(cfg as OpenClawConfig, {
|
||||
pluginIds: collectRelevantDoctorPluginIds(cfg),
|
||||
});
|
||||
return {
|
||||
next: compat.config as OpenClawConfig & Record<string, unknown>,
|
||||
changes: compat.changes,
|
||||
};
|
||||
}
|
||||
|
||||
8
src/commands/doctor/shared/runtime-compat-api.ts
Normal file
8
src/commands/doctor/shared/runtime-compat-api.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { applyLegacyDoctorMigrations } from "./legacy-config-migrate.js";
|
||||
|
||||
export function applyRuntimeLegacyConfigMigrations(raw: unknown): {
|
||||
next: Record<string, unknown> | null;
|
||||
changes: string[];
|
||||
} {
|
||||
return applyLegacyDoctorMigrations(raw);
|
||||
}
|
||||
@@ -36,7 +36,7 @@ describe("legacy provider-shaped config snapshots", () => {
|
||||
expect(res.ok).toBe(false);
|
||||
});
|
||||
|
||||
it("detects legacy messages.tts provider keys and reports legacyIssues", async () => {
|
||||
it("accepts legacy messages.tts provider keys via auto-migration and reports legacyIssues", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
await writeOpenClawConfig(home, {
|
||||
messages: {
|
||||
@@ -52,19 +52,24 @@ describe("legacy provider-shaped config snapshots", () => {
|
||||
|
||||
const snap = await readConfigFileSnapshot();
|
||||
|
||||
expect(snap.valid).toBe(false);
|
||||
expect(snap.valid).toBe(true);
|
||||
expect(snap.legacyIssues.some((issue) => issue.path === "messages.tts")).toBe(true);
|
||||
expect(snap.sourceConfig.messages?.tts).toEqual({
|
||||
provider: "elevenlabs",
|
||||
elevenlabs: {
|
||||
apiKey: "test-key",
|
||||
voiceId: "voice-1",
|
||||
providers: {
|
||||
elevenlabs: {
|
||||
apiKey: "test-key",
|
||||
voiceId: "voice-1",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(
|
||||
(snap.sourceConfig.messages?.tts as Record<string, unknown> | undefined)?.elevenlabs,
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("reports legacy talk flat fields without auto-migrating them at config load", async () => {
|
||||
it("accepts legacy talk flat fields via auto-migration and reports legacyIssues", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
await writeOpenClawConfig(home, {
|
||||
talk: {
|
||||
@@ -76,17 +81,26 @@ describe("legacy provider-shaped config snapshots", () => {
|
||||
|
||||
const snap = await readConfigFileSnapshot();
|
||||
|
||||
expect(snap.valid).toBe(false);
|
||||
expect(snap.valid).toBe(true);
|
||||
expect(snap.legacyIssues.some((issue) => issue.path === "talk")).toBe(true);
|
||||
expect(snap.sourceConfig.talk).toEqual({
|
||||
expect(snap.sourceConfig.talk?.providers?.elevenlabs).toEqual({
|
||||
voiceId: "voice-1",
|
||||
modelId: "eleven_v3",
|
||||
apiKey: "test-key",
|
||||
});
|
||||
expect(
|
||||
(snap.sourceConfig.talk as Record<string, unknown> | undefined)?.voiceId,
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
(snap.sourceConfig.talk as Record<string, unknown> | undefined)?.modelId,
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
(snap.sourceConfig.talk as Record<string, unknown> | undefined)?.apiKey,
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("detects legacy plugins.entries.*.config.tts provider keys", async () => {
|
||||
it("accepts legacy plugins.entries.*.config.tts provider keys via auto-migration", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
await writeOpenClawConfig(home, {
|
||||
plugins: {
|
||||
@@ -108,7 +122,7 @@ describe("legacy provider-shaped config snapshots", () => {
|
||||
|
||||
const snap = await readConfigFileSnapshot();
|
||||
|
||||
expect(snap.valid).toBe(false);
|
||||
expect(snap.valid).toBe(true);
|
||||
expect(snap.legacyIssues.some((issue) => issue.path === "plugins.entries")).toBe(true);
|
||||
const voiceCallTts = (
|
||||
snap.sourceConfig.plugins?.entries as
|
||||
@@ -127,15 +141,18 @@ describe("legacy provider-shaped config snapshots", () => {
|
||||
)?.["voice-call"]?.config?.tts;
|
||||
expect(voiceCallTts).toEqual({
|
||||
provider: "openai",
|
||||
openai: {
|
||||
model: "gpt-4o-mini-tts",
|
||||
voice: "alloy",
|
||||
providers: {
|
||||
openai: {
|
||||
model: "gpt-4o-mini-tts",
|
||||
voice: "alloy",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(voiceCallTts?.openai).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("detects legacy discord voice tts provider keys and reports legacyIssues", async () => {
|
||||
it("accepts legacy discord voice tts provider keys via auto-migration and reports legacyIssues", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
await writeOpenClawConfig(home, {
|
||||
channels: {
|
||||
@@ -165,7 +182,7 @@ describe("legacy provider-shaped config snapshots", () => {
|
||||
|
||||
const snap = await readConfigFileSnapshot();
|
||||
|
||||
expect(snap.valid).toBe(false);
|
||||
expect(snap.valid).toBe(true);
|
||||
expect(snap.legacyIssues.some((issue) => issue.path === "channels.discord.voice.tts")).toBe(
|
||||
true,
|
||||
);
|
||||
@@ -174,13 +191,17 @@ describe("legacy provider-shaped config snapshots", () => {
|
||||
);
|
||||
expect(snap.sourceConfig.channels?.discord?.voice?.tts).toEqual({
|
||||
provider: "elevenlabs",
|
||||
elevenlabs: {
|
||||
voiceId: "voice-1",
|
||||
providers: {
|
||||
elevenlabs: {
|
||||
voiceId: "voice-1",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(snap.sourceConfig.channels?.discord?.accounts?.main?.voice?.tts).toEqual({
|
||||
edge: {
|
||||
voice: "en-US-AvaNeural",
|
||||
providers: {
|
||||
microsoft: {
|
||||
voice: "en-US-AvaNeural",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import path from "node:path";
|
||||
import { isDeepStrictEqual } from "node:util";
|
||||
import JSON5 from "json5";
|
||||
import { ensureOwnerDisplaySecret } from "../agents/owner-display.js";
|
||||
import { applyRuntimeLegacyConfigMigrations } from "../commands/doctor/shared/runtime-compat-api.js";
|
||||
import { loadDotEnv } from "../infra/dotenv.js";
|
||||
import { resolveRequiredHomeDir } from "../infra/home-dir.js";
|
||||
import {
|
||||
@@ -13,7 +14,10 @@ import {
|
||||
shouldDeferShellEnvFallback,
|
||||
shouldEnableShellEnvFallback,
|
||||
} from "../infra/shell-env.js";
|
||||
import { listPluginDoctorLegacyConfigRules } from "../plugins/doctor-contract-registry.js";
|
||||
import {
|
||||
collectRelevantDoctorPluginIds,
|
||||
listPluginDoctorLegacyConfigRules,
|
||||
} from "../plugins/doctor-contract-registry.js";
|
||||
import { sanitizeTerminalText } from "../terminal/safe-text.js";
|
||||
import { VERSION } from "../version.js";
|
||||
import { DuplicateAgentDirError, findDuplicateAgentDirs } from "./agent-dirs.js";
|
||||
@@ -1619,12 +1623,20 @@ function resolveLegacyConfigForRead(
|
||||
resolvedConfigRaw: unknown,
|
||||
sourceRaw: unknown,
|
||||
): LegacyMigrationResolution {
|
||||
const pluginIds = collectRelevantDoctorPluginIds(resolvedConfigRaw);
|
||||
const sourceLegacyIssues = findLegacyConfigIssues(
|
||||
resolvedConfigRaw,
|
||||
sourceRaw,
|
||||
listPluginDoctorLegacyConfigRules(),
|
||||
listPluginDoctorLegacyConfigRules({ pluginIds }),
|
||||
);
|
||||
return { effectiveConfigRaw: resolvedConfigRaw, sourceLegacyIssues };
|
||||
if (!resolvedConfigRaw || typeof resolvedConfigRaw !== "object") {
|
||||
return { effectiveConfigRaw: resolvedConfigRaw, sourceLegacyIssues };
|
||||
}
|
||||
const compat = applyRuntimeLegacyConfigMigrations(resolvedConfigRaw);
|
||||
return {
|
||||
effectiveConfigRaw: compat.next ?? resolvedConfigRaw,
|
||||
sourceLegacyIssues,
|
||||
};
|
||||
}
|
||||
|
||||
type ReadConfigFileSnapshotInternalResult = {
|
||||
|
||||
@@ -28,7 +28,7 @@ export function findLegacyConfigIssues(
|
||||
const issues: LegacyConfigIssue[] = [];
|
||||
for (const rule of [
|
||||
...LEGACY_CONFIG_RULES,
|
||||
...collectChannelLegacyConfigRules(),
|
||||
...collectChannelLegacyConfigRules(raw),
|
||||
...extraRules,
|
||||
]) {
|
||||
const cursor = getPathValue(root, rule.path);
|
||||
|
||||
@@ -7,7 +7,10 @@ import {
|
||||
resolveEffectivePluginActivationState,
|
||||
resolveMemorySlotDecision,
|
||||
} from "../plugins/config-state.js";
|
||||
import { listPluginDoctorLegacyConfigRules } from "../plugins/doctor-contract-registry.js";
|
||||
import {
|
||||
collectRelevantDoctorPluginIds,
|
||||
listPluginDoctorLegacyConfigRules,
|
||||
} from "../plugins/doctor-contract-registry.js";
|
||||
import {
|
||||
loadPluginManifestRegistry,
|
||||
resolveManifestContractPluginIds,
|
||||
@@ -455,7 +458,11 @@ export function validateConfigObjectRaw(
|
||||
raw: unknown,
|
||||
): { ok: true; config: OpenClawConfig } | { ok: false; issues: ConfigValidationIssue[] } {
|
||||
const policyIssues = collectUnsupportedSecretRefPolicyIssues(raw);
|
||||
const legacyIssues = findLegacyConfigIssues(raw, raw, listPluginDoctorLegacyConfigRules());
|
||||
const legacyIssues = findLegacyConfigIssues(
|
||||
raw,
|
||||
raw,
|
||||
listPluginDoctorLegacyConfigRules({ pluginIds: collectRelevantDoctorPluginIds(raw) }),
|
||||
);
|
||||
if (legacyIssues.length > 0) {
|
||||
return {
|
||||
ok: false,
|
||||
|
||||
@@ -25,10 +25,8 @@ vi.mock("./manifest-registry.js", () => ({
|
||||
mocks.loadPluginManifestRegistry(...args),
|
||||
}));
|
||||
|
||||
import {
|
||||
clearPluginDoctorContractRegistryCache,
|
||||
listPluginDoctorLegacyConfigRules,
|
||||
} from "./doctor-contract-registry.js";
|
||||
let clearPluginDoctorContractRegistryCache: typeof import("./doctor-contract-registry.js").clearPluginDoctorContractRegistryCache;
|
||||
let listPluginDoctorLegacyConfigRules: typeof import("./doctor-contract-registry.js").listPluginDoctorLegacyConfigRules;
|
||||
|
||||
function makeTempDir(): string {
|
||||
return makeTrackedTempDir("openclaw-doctor-contract-registry", tempDirs);
|
||||
@@ -39,8 +37,7 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("doctor-contract-registry getJiti", () => {
|
||||
beforeEach(() => {
|
||||
clearPluginDoctorContractRegistryCache();
|
||||
beforeEach(async () => {
|
||||
mocks.createJiti.mockReset();
|
||||
mocks.discoverOpenClawPlugins.mockReset();
|
||||
mocks.loadPluginManifestRegistry.mockReset();
|
||||
@@ -53,6 +50,10 @@ describe("doctor-contract-registry getJiti", () => {
|
||||
return () => ({ default: {} });
|
||||
},
|
||||
);
|
||||
vi.resetModules();
|
||||
({ clearPluginDoctorContractRegistryCache, listPluginDoctorLegacyConfigRules } =
|
||||
await import("./doctor-contract-registry.js"));
|
||||
clearPluginDoctorContractRegistryCache();
|
||||
});
|
||||
|
||||
it("disables native jiti loading on Windows for contract-api modules", () => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createJiti } from "jiti";
|
||||
import type { LegacyConfigRule } from "../config/legacy.shared.js";
|
||||
import type { OpenClawConfig } from "../config/types.js";
|
||||
import { discoverOpenClawPlugins } from "./discovery.js";
|
||||
import { loadPluginManifestRegistry } from "./manifest-registry.js";
|
||||
import { resolvePluginCacheInputs } from "./roots.js";
|
||||
@@ -20,11 +21,22 @@ const RUNNING_FROM_BUILT_ARTIFACT =
|
||||
|
||||
type PluginDoctorContractModule = {
|
||||
legacyConfigRules?: unknown;
|
||||
normalizeCompatibilityConfig?: unknown;
|
||||
};
|
||||
|
||||
type PluginDoctorCompatibilityMutation = {
|
||||
config: OpenClawConfig;
|
||||
changes: string[];
|
||||
};
|
||||
|
||||
type PluginDoctorCompatibilityNormalizer = (params: {
|
||||
cfg: OpenClawConfig;
|
||||
}) => PluginDoctorCompatibilityMutation;
|
||||
|
||||
type PluginDoctorContractEntry = {
|
||||
pluginId: string;
|
||||
rules: LegacyConfigRule[];
|
||||
normalizeCompatibilityConfig?: PluginDoctorCompatibilityNormalizer;
|
||||
};
|
||||
|
||||
const jitiLoaders = new Map<string, ReturnType<typeof createJiti>>();
|
||||
@@ -52,6 +64,7 @@ function getJiti(modulePath: string) {
|
||||
function buildDoctorContractCacheKey(params: {
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
pluginIds?: readonly string[];
|
||||
}): string {
|
||||
const { roots, loadPaths } = resolvePluginCacheInputs({
|
||||
workspaceDir: params.workspaceDir,
|
||||
@@ -60,6 +73,7 @@ function buildDoctorContractCacheKey(params: {
|
||||
return JSON.stringify({
|
||||
roots,
|
||||
loadPaths,
|
||||
pluginIds: [...(params.pluginIds ?? [])].toSorted(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -67,6 +81,12 @@ function resolveContractApiPath(rootDir: string): string | null {
|
||||
const orderedExtensions = RUNNING_FROM_BUILT_ARTIFACT
|
||||
? CONTRACT_API_EXTENSIONS
|
||||
: ([...CONTRACT_API_EXTENSIONS.slice(3), ...CONTRACT_API_EXTENSIONS.slice(0, 3)] as const);
|
||||
for (const extension of orderedExtensions) {
|
||||
const candidate = path.join(rootDir, `doctor-contract-api${extension}`);
|
||||
if (fs.existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
for (const extension of orderedExtensions) {
|
||||
const candidate = path.join(rootDir, `contract-api${extension}`);
|
||||
if (fs.existsSync(candidate)) {
|
||||
@@ -89,14 +109,68 @@ function coerceLegacyConfigRules(value: unknown): LegacyConfigRule[] {
|
||||
}) as LegacyConfigRule[];
|
||||
}
|
||||
|
||||
function coerceNormalizeCompatibilityConfig(
|
||||
value: unknown,
|
||||
): PluginDoctorCompatibilityNormalizer | undefined {
|
||||
return typeof value === "function" ? (value as PluginDoctorCompatibilityNormalizer) : undefined;
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: null;
|
||||
}
|
||||
|
||||
function hasLegacyElevenLabsTalkFields(raw: unknown): boolean {
|
||||
const talk = asRecord(asRecord(raw)?.talk);
|
||||
if (!talk) {
|
||||
return false;
|
||||
}
|
||||
return ["voiceId", "voiceAliases", "modelId", "outputFormat", "apiKey"].some((key) =>
|
||||
Object.prototype.hasOwnProperty.call(talk, key),
|
||||
);
|
||||
}
|
||||
|
||||
export function collectRelevantDoctorPluginIds(raw: unknown): string[] {
|
||||
const ids = new Set<string>();
|
||||
const root = asRecord(raw);
|
||||
if (!root) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const channels = asRecord(root.channels);
|
||||
if (channels) {
|
||||
for (const channelId of Object.keys(channels)) {
|
||||
if (channelId !== "defaults") {
|
||||
ids.add(channelId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const pluginsEntries = asRecord(asRecord(root.plugins)?.entries);
|
||||
if (pluginsEntries) {
|
||||
for (const pluginId of Object.keys(pluginsEntries)) {
|
||||
ids.add(pluginId);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasLegacyElevenLabsTalkFields(root)) {
|
||||
ids.add("elevenlabs");
|
||||
}
|
||||
|
||||
return [...ids].toSorted();
|
||||
}
|
||||
|
||||
function resolvePluginDoctorContracts(params?: {
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
pluginIds?: readonly string[];
|
||||
}): PluginDoctorContractEntry[] {
|
||||
const env = params?.env ?? process.env;
|
||||
const cacheKey = buildDoctorContractCacheKey({
|
||||
workspaceDir: params?.workspaceDir,
|
||||
env,
|
||||
pluginIds: params?.pluginIds,
|
||||
});
|
||||
const cached = doctorContractCache.get(cacheKey);
|
||||
if (cached) {
|
||||
@@ -117,7 +191,17 @@ function resolvePluginDoctorContracts(params?: {
|
||||
});
|
||||
|
||||
const entries: PluginDoctorContractEntry[] = [];
|
||||
const selectedPluginIds =
|
||||
params?.pluginIds && params.pluginIds.length > 0 ? new Set(params.pluginIds) : null;
|
||||
for (const record of manifestRegistry.plugins) {
|
||||
if (
|
||||
selectedPluginIds &&
|
||||
!selectedPluginIds.has(record.id) &&
|
||||
!record.channels.some((channelId) => selectedPluginIds.has(channelId)) &&
|
||||
!record.providers.some((providerId) => selectedPluginIds.has(providerId))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const contractSource = resolveContractApiPath(record.rootDir);
|
||||
if (!contractSource) {
|
||||
continue;
|
||||
@@ -132,12 +216,17 @@ function resolvePluginDoctorContracts(params?: {
|
||||
(mod as { default?: PluginDoctorContractModule }).default?.legacyConfigRules ??
|
||||
mod.legacyConfigRules,
|
||||
);
|
||||
if (rules.length === 0) {
|
||||
const normalizeCompatibilityConfig = coerceNormalizeCompatibilityConfig(
|
||||
mod.normalizeCompatibilityConfig ??
|
||||
(mod as { default?: PluginDoctorContractModule }).default?.normalizeCompatibilityConfig,
|
||||
);
|
||||
if (rules.length === 0 && !normalizeCompatibilityConfig) {
|
||||
continue;
|
||||
}
|
||||
entries.push({
|
||||
pluginId: record.id,
|
||||
rules,
|
||||
normalizeCompatibilityConfig,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -147,11 +236,37 @@ function resolvePluginDoctorContracts(params?: {
|
||||
|
||||
export function clearPluginDoctorContractRegistryCache(): void {
|
||||
doctorContractCache.clear();
|
||||
jitiLoaders.clear();
|
||||
}
|
||||
|
||||
export function listPluginDoctorLegacyConfigRules(params?: {
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
pluginIds?: readonly string[];
|
||||
}): LegacyConfigRule[] {
|
||||
return resolvePluginDoctorContracts(params).flatMap((entry) => entry.rules);
|
||||
}
|
||||
|
||||
export function applyPluginDoctorCompatibilityMigrations(
|
||||
cfg: OpenClawConfig,
|
||||
params?: {
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
pluginIds?: readonly string[];
|
||||
},
|
||||
): {
|
||||
config: OpenClawConfig;
|
||||
changes: string[];
|
||||
} {
|
||||
let nextCfg = cfg;
|
||||
const changes: string[] = [];
|
||||
for (const entry of resolvePluginDoctorContracts(params)) {
|
||||
const mutation = entry.normalizeCompatibilityConfig?.({ cfg: nextCfg });
|
||||
if (!mutation || mutation.changes.length === 0) {
|
||||
continue;
|
||||
}
|
||||
nextCfg = mutation.config;
|
||||
changes.push(...mutation.changes);
|
||||
}
|
||||
return { config: nextCfg, changes };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user