



























@@ -1,6 +1,7 @@
11import path from "node:path";
22import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
33import { CHANNEL_IDS, normalizeChatChannelId } from "../channels/ids.js";
4+import { planManifestModelCatalogSuppressions } from "../model-catalog/index.js";
45import { withBundledPluginAllowlistCompat } from "../plugins/bundled-compat.js";
56import {
67normalizePluginsConfig,
@@ -49,6 +50,10 @@ type AllowedValuesCollection = {
4950hasValues: boolean;
5051};
5152type JsonSchemaLike = Record<string, unknown>;
53+type ConfiguredModelRef = {
54+path: string;
55+value: string;
56+};
52575358function stripDeprecatedValidationKeys(raw: unknown): unknown {
5459if (!isRecord(raw) || !isRecord(raw.commands) || !Object.hasOwn(raw.commands, "modelsWrite")) {
@@ -978,20 +983,29 @@ function validateConfigObjectWithPluginsBase(
978983return ensureInstalledPluginRecordIds().has(normalizedChannelId);
979984};
980985981-const collectKnownWebSearchProviderIds = (): string[] => {
986+const collectActiveWebSearchProviderIds = (): string[] => {
982987const { registry } = ensureRegistry();
983988return [
984989 ...new Set(
985-[
986- ...registry.plugins.flatMap((record) => record.contracts?.webSearchProviders ?? []),
987- ...resolveWebSearchInstallCatalogEntries().map((entry) => entry.provider.id),
988-]
990+registry.plugins
991+.flatMap((record) => record.contracts?.webSearchProviders ?? [])
989992.map((providerId) => providerId.trim())
990993.filter((providerId) => providerId.length > 0),
991994),
992995].toSorted((left, right) => left.localeCompare(right));
993996};
994997998+const collectKnownWebSearchProviderIds = (): string[] => {
999+return [
1000+ ...new Set([
1001+ ...collectActiveWebSearchProviderIds(),
1002+ ...resolveWebSearchInstallCatalogEntries()
1003+.map((entry) => entry.provider.id.trim())
1004+.filter((providerId) => providerId.length > 0),
1005+]),
1006+].toSorted((left, right) => left.localeCompare(right));
1007+};
1008+9951009const hasStalePluginEvidenceForUnknownWebSearchProvider = (providerId: string): boolean => {
9961010const normalizedProviderId = normalizePluginId(providerId);
9971011if (!normalizedProviderId || ensureKnownIds().has(normalizedProviderId)) {
@@ -1034,8 +1048,23 @@ function validateConfigObjectWithPluginsBase(
10341048issues.push({ path, message: "web_search provider must not be empty" });
10351049return;
10361050}
1051+const activeProviderIds = collectActiveWebSearchProviderIds();
1052+if (activeProviderIds.includes(trimmed)) {
1053+return;
1054+}
1055+const installCatalogEntry = resolveWebSearchInstallCatalogEntries().find(
1056+(entry) => entry.provider.id === trimmed,
1057+);
1058+if (installCatalogEntry) {
1059+issues.push({
1060+ path,
1061+message: `web_search provider is not available: ${trimmed} (install or enable plugin "${installCatalogEntry.pluginId}", then run openclaw doctor --fix)`,
1062+allowedValues: collectKnownWebSearchProviderIds(),
1063+});
1064+return;
1065+}
10371066const allowedValues = collectKnownWebSearchProviderIds();
1038-if (allowedValues.length === 0 || allowedValues.includes(trimmed)) {
1067+if (allowedValues.length === 0) {
10391068return;
10401069}
10411070const issue = {
@@ -1053,6 +1082,119 @@ function validateConfigObjectWithPluginsBase(
10531082issues.push(issue);
10541083};
105510841085+const collectConfiguredModelRefs = (): ConfiguredModelRef[] => {
1086+const refs: ConfiguredModelRef[] = [];
1087+const pushModelRef = (path: string, value: unknown) => {
1088+if (typeof value === "string" && value.trim()) {
1089+refs.push({ path, value: value.trim() });
1090+}
1091+};
1092+const collectModelConfig = (path: string, value: unknown) => {
1093+if (typeof value === "string") {
1094+pushModelRef(path, value);
1095+return;
1096+}
1097+if (!isRecord(value)) {
1098+return;
1099+}
1100+pushModelRef(`${path}.primary`, value.primary);
1101+if (Array.isArray(value.fallbacks)) {
1102+for (const [index, entry] of value.fallbacks.entries()) {
1103+pushModelRef(`${path}.fallbacks.${index}`, entry);
1104+}
1105+}
1106+};
1107+const collectFromAgent = (path: string, agent: unknown) => {
1108+if (!isRecord(agent)) {
1109+return;
1110+}
1111+for (const key of [
1112+"model",
1113+"imageModel",
1114+"imageGenerationModel",
1115+"videoGenerationModel",
1116+"musicGenerationModel",
1117+"pdfModel",
1118+]) {
1119+collectModelConfig(`${path}.${key}`, agent[key]);
1120+}
1121+if (isRecord(agent.models)) {
1122+for (const modelRef of Object.keys(agent.models)) {
1123+pushModelRef(`${path}.models.${modelRef}`, modelRef);
1124+}
1125+}
1126+};
1127+1128+collectFromAgent("agents.defaults", config.agents?.defaults);
1129+if (Array.isArray(config.agents?.list)) {
1130+for (const [index, entry] of config.agents.list.entries()) {
1131+collectFromAgent(`agents.list.${index}`, entry);
1132+}
1133+}
1134+return refs;
1135+};
1136+1137+const parseProviderModelRef = (value: string): { provider: string; model: string } | null => {
1138+const slashIndex = value.indexOf("/");
1139+if (slashIndex <= 0 || slashIndex >= value.length - 1) {
1140+return null;
1141+}
1142+const provider = normalizeLowercaseStringOrEmpty(value.slice(0, slashIndex));
1143+const model = normalizeLowercaseStringOrEmpty(value.slice(slashIndex + 1));
1144+return provider && model ? { provider, model } : null;
1145+};
1146+1147+const validateConfiguredModelRefs = () => {
1148+const configuredRefs = collectConfiguredModelRefs();
1149+if (configuredRefs.length === 0) {
1150+return;
1151+}
1152+const { registry } = ensureRegistry();
1153+const suppressedModels = new Map<
1154+string,
1155+{ provider: string; model: string; reason?: string }
1156+>();
1157+for (const suppression of planManifestModelCatalogSuppressions({ registry }).suppressions) {
1158+if (suppression.when) {
1159+continue;
1160+}
1161+const key = `${suppression.provider}/${suppression.model}`;
1162+if (!suppressedModels.has(key)) {
1163+suppressedModels.set(key, {
1164+provider: suppression.provider,
1165+model: suppression.model,
1166+ ...(suppression.reason ? { reason: suppression.reason } : {}),
1167+});
1168+}
1169+}
1170+if (suppressedModels.size === 0) {
1171+return;
1172+}
1173+const seen = new Set<string>();
1174+for (const ref of configuredRefs) {
1175+const parsed = parseProviderModelRef(ref.value);
1176+if (!parsed) {
1177+continue;
1178+}
1179+const suppression = suppressedModels.get(`${parsed.provider}/${parsed.model}`);
1180+if (!suppression) {
1181+continue;
1182+}
1183+const issueKey = `${ref.path}\0${parsed.provider}/${parsed.model}`;
1184+if (seen.has(issueKey)) {
1185+continue;
1186+}
1187+seen.add(issueKey);
1188+const modelRef = `${suppression.provider}/${suppression.model}`;
1189+issues.push({
1190+path: ref.path,
1191+message: suppression.reason
1192+ ? `Unknown model: ${modelRef}. ${suppression.reason}`
1193+ : `Unknown model: ${modelRef}.`,
1194+});
1195+}
1196+};
1197+10561198const replaceChannelConfig = (channelId: string, nextValue: unknown) => {
10571199if (!channelsCloned) {
10581200mutatedConfig = {
@@ -1200,6 +1342,7 @@ function validateConfigObjectWithPluginsBase(
12001342}
1201134312021344validateWebSearchProvider();
1345+validateConfiguredModelRefs();
1203134612041347if (!hasExplicitPluginsConfig) {
12051348if (issues.length > 0) {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。