mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-05 17:30:21 +00:00
264 lines
7.8 KiB
TypeScript
264 lines
7.8 KiB
TypeScript
import { BUNDLED_WEB_SEARCH_PROVIDER_PLUGIN_IDS } from "../plugins/bundled-capability-metadata.js";
|
|
import type { OpenClawConfig } from "./config.js";
|
|
import { mergeMissing } from "./legacy.shared.js";
|
|
|
|
type JsonRecord = Record<string, unknown>;
|
|
|
|
const MODERN_SCOPED_WEB_SEARCH_KEYS = new Set(["openaiCodex"]);
|
|
|
|
// Tavily only ever used the plugin-owned config path, so there is no legacy
|
|
// `tools.web.search.tavily.*` shape to migrate.
|
|
const NON_MIGRATED_LEGACY_WEB_SEARCH_PROVIDER_IDS = new Set(["tavily"]);
|
|
const LEGACY_WEB_SEARCH_PROVIDER_PLUGIN_IDS = Object.fromEntries(
|
|
Object.entries(BUNDLED_WEB_SEARCH_PROVIDER_PLUGIN_IDS).filter(
|
|
([providerId]) => !NON_MIGRATED_LEGACY_WEB_SEARCH_PROVIDER_IDS.has(providerId),
|
|
),
|
|
);
|
|
const LEGACY_WEB_SEARCH_PROVIDER_IDS = Object.keys(LEGACY_WEB_SEARCH_PROVIDER_PLUGIN_IDS);
|
|
const LEGACY_WEB_SEARCH_PROVIDER_ID_SET = new Set(LEGACY_WEB_SEARCH_PROVIDER_IDS);
|
|
const LEGACY_GLOBAL_WEB_SEARCH_PROVIDER_ID = "brave";
|
|
|
|
function isRecord(value: unknown): value is JsonRecord {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
function cloneRecord<T extends JsonRecord>(value: T | undefined): T {
|
|
return { ...value } as T;
|
|
}
|
|
|
|
function ensureRecord(target: JsonRecord, key: string): JsonRecord {
|
|
const current = target[key];
|
|
if (isRecord(current)) {
|
|
return current;
|
|
}
|
|
const next: JsonRecord = {};
|
|
target[key] = next;
|
|
return next;
|
|
}
|
|
|
|
function resolveLegacySearchConfig(raw: unknown): JsonRecord | undefined {
|
|
if (!isRecord(raw)) {
|
|
return undefined;
|
|
}
|
|
const tools = isRecord(raw.tools) ? raw.tools : undefined;
|
|
const web = isRecord(tools?.web) ? tools.web : undefined;
|
|
return isRecord(web?.search) ? web.search : undefined;
|
|
}
|
|
|
|
function copyLegacyProviderConfig(search: JsonRecord, providerKey: string): JsonRecord | undefined {
|
|
const current = search[providerKey];
|
|
return isRecord(current) ? cloneRecord(current) : undefined;
|
|
}
|
|
|
|
function hasOwnKey(target: JsonRecord, key: string): boolean {
|
|
return Object.prototype.hasOwnProperty.call(target, key);
|
|
}
|
|
|
|
function hasMappedLegacyWebSearchConfig(raw: unknown): boolean {
|
|
const search = resolveLegacySearchConfig(raw);
|
|
if (!search) {
|
|
return false;
|
|
}
|
|
if (hasOwnKey(search, "apiKey")) {
|
|
return true;
|
|
}
|
|
return LEGACY_WEB_SEARCH_PROVIDER_IDS.some((providerId) => isRecord(search[providerId]));
|
|
}
|
|
|
|
function resolveLegacyGlobalWebSearchMigration(search: JsonRecord): {
|
|
pluginId: string;
|
|
payload: JsonRecord;
|
|
legacyPath: string;
|
|
targetPath: string;
|
|
} | null {
|
|
const legacyProviderConfig = copyLegacyProviderConfig(
|
|
search,
|
|
LEGACY_GLOBAL_WEB_SEARCH_PROVIDER_ID,
|
|
);
|
|
const payload = legacyProviderConfig ?? {};
|
|
const hasLegacyApiKey = hasOwnKey(search, "apiKey");
|
|
if (hasLegacyApiKey) {
|
|
payload.apiKey = search.apiKey;
|
|
}
|
|
if (Object.keys(payload).length === 0) {
|
|
return null;
|
|
}
|
|
const pluginId =
|
|
LEGACY_WEB_SEARCH_PROVIDER_PLUGIN_IDS[LEGACY_GLOBAL_WEB_SEARCH_PROVIDER_ID] ??
|
|
LEGACY_GLOBAL_WEB_SEARCH_PROVIDER_ID;
|
|
return {
|
|
pluginId,
|
|
payload,
|
|
legacyPath: hasLegacyApiKey
|
|
? "tools.web.search.apiKey"
|
|
: `tools.web.search.${LEGACY_GLOBAL_WEB_SEARCH_PROVIDER_ID}`,
|
|
targetPath:
|
|
hasLegacyApiKey && !legacyProviderConfig
|
|
? `plugins.entries.${pluginId}.config.webSearch.apiKey`
|
|
: `plugins.entries.${pluginId}.config.webSearch`,
|
|
};
|
|
}
|
|
|
|
function migratePluginWebSearchConfig(params: {
|
|
root: JsonRecord;
|
|
legacyPath: string;
|
|
targetPath: string;
|
|
pluginId: string;
|
|
payload: JsonRecord;
|
|
changes: string[];
|
|
}) {
|
|
const plugins = ensureRecord(params.root, "plugins");
|
|
const entries = ensureRecord(plugins, "entries");
|
|
const entry = ensureRecord(entries, params.pluginId);
|
|
const config = ensureRecord(entry, "config");
|
|
const hadEnabled = entry.enabled !== undefined;
|
|
const existing = isRecord(config.webSearch) ? cloneRecord(config.webSearch) : undefined;
|
|
|
|
if (!hadEnabled) {
|
|
entry.enabled = true;
|
|
}
|
|
|
|
if (!existing) {
|
|
config.webSearch = cloneRecord(params.payload);
|
|
params.changes.push(`Moved ${params.legacyPath} → ${params.targetPath}.`);
|
|
return;
|
|
}
|
|
|
|
const merged = cloneRecord(existing);
|
|
mergeMissing(merged, params.payload);
|
|
const changed = JSON.stringify(merged) !== JSON.stringify(existing) || !hadEnabled;
|
|
config.webSearch = merged;
|
|
if (changed) {
|
|
params.changes.push(
|
|
`Merged ${params.legacyPath} → ${params.targetPath} (filled missing fields from legacy; kept explicit plugin config values).`,
|
|
);
|
|
return;
|
|
}
|
|
|
|
params.changes.push(`Removed ${params.legacyPath} (${params.targetPath} already set).`);
|
|
}
|
|
|
|
export function listLegacyWebSearchConfigPaths(raw: unknown): string[] {
|
|
const search = resolveLegacySearchConfig(raw);
|
|
if (!search) {
|
|
return [];
|
|
}
|
|
const paths: string[] = [];
|
|
|
|
if ("apiKey" in search) {
|
|
paths.push("tools.web.search.apiKey");
|
|
}
|
|
for (const providerId of LEGACY_WEB_SEARCH_PROVIDER_IDS) {
|
|
const scoped = search[providerId];
|
|
if (isRecord(scoped)) {
|
|
for (const key of Object.keys(scoped)) {
|
|
paths.push(`tools.web.search.${providerId}.${key}`);
|
|
}
|
|
}
|
|
}
|
|
return paths;
|
|
}
|
|
|
|
export function normalizeLegacyWebSearchConfig<T>(raw: T): T {
|
|
if (!isRecord(raw)) {
|
|
return raw;
|
|
}
|
|
|
|
const search = resolveLegacySearchConfig(raw);
|
|
if (!search) {
|
|
return raw;
|
|
}
|
|
|
|
return normalizeLegacyWebSearchConfigRecord(raw).config;
|
|
}
|
|
|
|
export function migrateLegacyWebSearchConfig<T>(raw: T): { config: T; changes: string[] } {
|
|
if (!isRecord(raw)) {
|
|
return { config: raw, changes: [] };
|
|
}
|
|
|
|
if (!hasMappedLegacyWebSearchConfig(raw)) {
|
|
return { config: raw, changes: [] };
|
|
}
|
|
|
|
return normalizeLegacyWebSearchConfigRecord(raw);
|
|
}
|
|
|
|
function normalizeLegacyWebSearchConfigRecord<T extends JsonRecord>(
|
|
raw: T,
|
|
): {
|
|
config: T;
|
|
changes: string[];
|
|
} {
|
|
const nextRoot = cloneRecord(raw);
|
|
const tools = ensureRecord(nextRoot, "tools");
|
|
const web = ensureRecord(tools, "web");
|
|
const search = resolveLegacySearchConfig(nextRoot);
|
|
if (!search) {
|
|
return { config: raw, changes: [] };
|
|
}
|
|
const nextSearch: JsonRecord = {};
|
|
const changes: string[] = [];
|
|
|
|
for (const [key, value] of Object.entries(search)) {
|
|
if (key === "apiKey") {
|
|
continue;
|
|
}
|
|
if (LEGACY_WEB_SEARCH_PROVIDER_ID_SET.has(key) && isRecord(value)) {
|
|
continue;
|
|
}
|
|
if (MODERN_SCOPED_WEB_SEARCH_KEYS.has(key) || !isRecord(value)) {
|
|
nextSearch[key] = value;
|
|
}
|
|
}
|
|
web.search = nextSearch;
|
|
|
|
const globalSearchMigration = resolveLegacyGlobalWebSearchMigration(search);
|
|
if (globalSearchMigration) {
|
|
migratePluginWebSearchConfig({
|
|
root: nextRoot,
|
|
legacyPath: globalSearchMigration.legacyPath,
|
|
targetPath: globalSearchMigration.targetPath,
|
|
pluginId: globalSearchMigration.pluginId,
|
|
payload: globalSearchMigration.payload,
|
|
changes,
|
|
});
|
|
}
|
|
|
|
for (const providerId of LEGACY_WEB_SEARCH_PROVIDER_IDS) {
|
|
if (providerId === LEGACY_GLOBAL_WEB_SEARCH_PROVIDER_ID) {
|
|
continue;
|
|
}
|
|
const scoped = copyLegacyProviderConfig(search, providerId);
|
|
if (!scoped || Object.keys(scoped).length === 0) {
|
|
continue;
|
|
}
|
|
const pluginId = LEGACY_WEB_SEARCH_PROVIDER_PLUGIN_IDS[providerId];
|
|
if (!pluginId) {
|
|
continue;
|
|
}
|
|
migratePluginWebSearchConfig({
|
|
root: nextRoot,
|
|
legacyPath: `tools.web.search.${providerId}`,
|
|
targetPath: `plugins.entries.${pluginId}.config.webSearch`,
|
|
pluginId,
|
|
payload: scoped,
|
|
changes,
|
|
});
|
|
}
|
|
|
|
return { config: nextRoot, changes };
|
|
}
|
|
|
|
export function resolvePluginWebSearchConfig(
|
|
config: OpenClawConfig | undefined,
|
|
pluginId: string,
|
|
): Record<string, unknown> | undefined {
|
|
const pluginConfig = config?.plugins?.entries?.[pluginId]?.config;
|
|
if (!isRecord(pluginConfig)) {
|
|
return undefined;
|
|
}
|
|
const webSearch = pluginConfig.webSearch;
|
|
return isRecord(webSearch) ? webSearch : undefined;
|
|
}
|