mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-29 10:50:58 +00:00
refactor(config): dedupe legacy migration metadata
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import {
|
||||
defineLegacyConfigMigration,
|
||||
ensureRecord,
|
||||
getRecord,
|
||||
type LegacyConfigMigration,
|
||||
mapLegacyAudioTranscription,
|
||||
type LegacyConfigMigrationSpec,
|
||||
} from "./legacy.shared.js";
|
||||
|
||||
function applyLegacyAudioTranscriptionModel(params: {
|
||||
@@ -31,8 +32,8 @@ function applyLegacyAudioTranscriptionModel(params: {
|
||||
params.changes.push(params.alreadySetMessage);
|
||||
}
|
||||
|
||||
export const LEGACY_CONFIG_MIGRATIONS_PART_2: LegacyConfigMigration[] = [
|
||||
{
|
||||
export const LEGACY_CONFIG_MIGRATIONS_AUDIO: LegacyConfigMigrationSpec[] = [
|
||||
defineLegacyConfigMigration({
|
||||
id: "audio.transcription-v2",
|
||||
describe: "Move audio.transcription to tools.media.audio.models",
|
||||
apply: (raw, changes) => {
|
||||
@@ -56,5 +57,5 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_2: LegacyConfigMigration[] = [
|
||||
raw.audio = audio;
|
||||
}
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
@@ -6,14 +6,30 @@ import {
|
||||
resolveSlackStreamingMode,
|
||||
resolveTelegramPreviewStreamMode,
|
||||
} from "./discord-preview-streaming.js";
|
||||
import { getRecord, type LegacyConfigMigration } from "./legacy.shared.js";
|
||||
import {
|
||||
defineLegacyConfigMigration,
|
||||
getRecord,
|
||||
type LegacyConfigMigrationSpec,
|
||||
type LegacyConfigRule,
|
||||
} from "./legacy.shared.js";
|
||||
|
||||
function hasOwnKey(target: Record<string, unknown>, key: string): boolean {
|
||||
return Object.prototype.hasOwnProperty.call(target, key);
|
||||
}
|
||||
|
||||
function escapeControlForLog(value: string): string {
|
||||
return value.replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\t/g, "\\t");
|
||||
function hasLegacyThreadBindingTtl(value: unknown): boolean {
|
||||
const threadBindings = getRecord(value);
|
||||
return Boolean(threadBindings && hasOwnKey(threadBindings, "ttlHours"));
|
||||
}
|
||||
|
||||
function hasLegacyThreadBindingTtlInAccounts(value: unknown): boolean {
|
||||
const accounts = getRecord(value);
|
||||
if (!accounts) {
|
||||
return false;
|
||||
}
|
||||
return Object.values(accounts).some((entry) =>
|
||||
hasLegacyThreadBindingTtl(getRecord(entry)?.threadBindings),
|
||||
);
|
||||
}
|
||||
|
||||
function migrateThreadBindingsTtlHoursForPath(params: {
|
||||
@@ -45,11 +61,33 @@ function migrateThreadBindingsTtlHoursForPath(params: {
|
||||
return true;
|
||||
}
|
||||
|
||||
export const LEGACY_CONFIG_MIGRATIONS_PART_1: LegacyConfigMigration[] = [
|
||||
const THREAD_BINDING_RULES: LegacyConfigRule[] = [
|
||||
{
|
||||
path: ["session", "threadBindings"],
|
||||
message:
|
||||
"session.threadBindings.ttlHours was renamed to session.threadBindings.idleHours (auto-migrated on load).",
|
||||
match: (value) => hasLegacyThreadBindingTtl(value),
|
||||
},
|
||||
{
|
||||
path: ["channels", "discord", "threadBindings"],
|
||||
message:
|
||||
"channels.discord.threadBindings.ttlHours was renamed to channels.discord.threadBindings.idleHours (auto-migrated on load).",
|
||||
match: (value) => hasLegacyThreadBindingTtl(value),
|
||||
},
|
||||
{
|
||||
path: ["channels", "discord", "accounts"],
|
||||
message:
|
||||
"channels.discord.accounts.<id>.threadBindings.ttlHours was renamed to channels.discord.accounts.<id>.threadBindings.idleHours (auto-migrated on load).",
|
||||
match: (value) => hasLegacyThreadBindingTtlInAccounts(value),
|
||||
},
|
||||
];
|
||||
|
||||
export const LEGACY_CONFIG_MIGRATIONS_CHANNELS: LegacyConfigMigrationSpec[] = [
|
||||
defineLegacyConfigMigration({
|
||||
id: "thread-bindings.ttlHours->idleHours",
|
||||
describe:
|
||||
"Move legacy threadBindings.ttlHours keys to threadBindings.idleHours (session + channels.discord)",
|
||||
legacyRules: THREAD_BINDING_RULES,
|
||||
apply: (raw, changes) => {
|
||||
const session = getRecord(raw.session);
|
||||
if (session) {
|
||||
@@ -93,8 +131,8 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_1: LegacyConfigMigration[] = [
|
||||
channels.discord = discord;
|
||||
raw.channels = channels;
|
||||
},
|
||||
},
|
||||
{
|
||||
}),
|
||||
defineLegacyConfigMigration({
|
||||
id: "channels.streaming-keys->channels.streaming",
|
||||
describe:
|
||||
"Normalize legacy streaming keys to channels.<provider>.streaming (Telegram/Discord/Slack)",
|
||||
@@ -196,45 +234,5 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_1: LegacyConfigMigration[] = [
|
||||
migrateProvider("discord");
|
||||
migrateProvider("slack");
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "gateway.bind.host-alias->bind-mode",
|
||||
describe: "Normalize gateway.bind host aliases to supported bind modes",
|
||||
apply: (raw, changes) => {
|
||||
const gateway = getRecord(raw.gateway);
|
||||
if (!gateway) {
|
||||
return;
|
||||
}
|
||||
const bindRaw = gateway.bind;
|
||||
if (typeof bindRaw !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalized = bindRaw.trim().toLowerCase();
|
||||
let mapped: "lan" | "loopback" | undefined;
|
||||
if (
|
||||
normalized === "0.0.0.0" ||
|
||||
normalized === "::" ||
|
||||
normalized === "[::]" ||
|
||||
normalized === "*"
|
||||
) {
|
||||
mapped = "lan";
|
||||
} else if (
|
||||
normalized === "127.0.0.1" ||
|
||||
normalized === "localhost" ||
|
||||
normalized === "::1" ||
|
||||
normalized === "[::1]"
|
||||
) {
|
||||
mapped = "loopback";
|
||||
}
|
||||
|
||||
if (!mapped || normalized === mapped) {
|
||||
return;
|
||||
}
|
||||
|
||||
gateway.bind = mapped;
|
||||
raw.gateway = gateway;
|
||||
changes.push(`Normalized gateway.bind "${escapeControlForLog(bindRaw)}" → "${mapped}".`);
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
@@ -5,10 +5,12 @@ import {
|
||||
resolveGatewayPortWithDefault,
|
||||
} from "./gateway-control-ui-origins.js";
|
||||
import {
|
||||
defineLegacyConfigMigration,
|
||||
ensureRecord,
|
||||
getRecord,
|
||||
type LegacyConfigMigration,
|
||||
mergeMissing,
|
||||
type LegacyConfigMigrationSpec,
|
||||
type LegacyConfigRule,
|
||||
} from "./legacy.shared.js";
|
||||
import { DEFAULT_GATEWAY_PORT } from "./paths.js";
|
||||
import { isBlockedObjectKey } from "./prototype-keys.js";
|
||||
@@ -32,6 +34,39 @@ const AGENT_HEARTBEAT_KEYS = new Set([
|
||||
|
||||
const CHANNEL_HEARTBEAT_KEYS = new Set(["showOk", "showAlerts", "useIndicator"]);
|
||||
|
||||
function isLegacyGatewayBindHostAlias(value: unknown): boolean {
|
||||
if (typeof value !== "string") {
|
||||
return false;
|
||||
}
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
normalized === "auto" ||
|
||||
normalized === "loopback" ||
|
||||
normalized === "lan" ||
|
||||
normalized === "tailnet" ||
|
||||
normalized === "custom"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
normalized === "0.0.0.0" ||
|
||||
normalized === "::" ||
|
||||
normalized === "[::]" ||
|
||||
normalized === "*" ||
|
||||
normalized === "127.0.0.1" ||
|
||||
normalized === "localhost" ||
|
||||
normalized === "::1" ||
|
||||
normalized === "[::1]"
|
||||
);
|
||||
}
|
||||
|
||||
function escapeControlForLog(value: string): string {
|
||||
return value.replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\t/g, "\\t");
|
||||
}
|
||||
|
||||
function splitLegacyHeartbeat(legacyHeartbeat: Record<string, unknown>): {
|
||||
agentHeartbeat: Record<string, unknown> | null;
|
||||
channelHeartbeat: Record<string, unknown> | null;
|
||||
@@ -140,12 +175,28 @@ function migrateLegacyTtsConfig(
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: tools.alsoAllow was introduced after legacy migrations; no legacy migration needed.
|
||||
const MEMORY_SEARCH_RULE: LegacyConfigRule = {
|
||||
path: ["memorySearch"],
|
||||
message:
|
||||
"top-level memorySearch was moved; use agents.defaults.memorySearch instead (auto-migrated on load).",
|
||||
};
|
||||
|
||||
// tools.alsoAllow legacy migration intentionally omitted (field not shipped in prod).
|
||||
const GATEWAY_BIND_RULE: LegacyConfigRule = {
|
||||
path: ["gateway", "bind"],
|
||||
message:
|
||||
"gateway.bind host aliases (for example 0.0.0.0/localhost) are legacy; use bind modes (lan/loopback/custom/tailnet/auto) instead (auto-migrated on load).",
|
||||
match: (value) => isLegacyGatewayBindHostAlias(value),
|
||||
requireSourceLiteral: true,
|
||||
};
|
||||
|
||||
export const LEGACY_CONFIG_MIGRATIONS_PART_3: LegacyConfigMigration[] = [
|
||||
{
|
||||
const HEARTBEAT_RULE: LegacyConfigRule = {
|
||||
path: ["heartbeat"],
|
||||
message:
|
||||
"top-level heartbeat is not a valid config path; use agents.defaults.heartbeat (cadence/target/model settings) or channels.defaults.heartbeat (showOk/showAlerts/useIndicator).",
|
||||
};
|
||||
|
||||
export const LEGACY_CONFIG_MIGRATIONS_RUNTIME: LegacyConfigMigrationSpec[] = [
|
||||
defineLegacyConfigMigration({
|
||||
// v2026.2.26 added a startup guard requiring gateway.controlUi.allowedOrigins (or the
|
||||
// host-header fallback flag) for any non-loopback bind. The setup wizard was updated
|
||||
// to seed this for new installs, but existing bind=lan/bind=custom installs that upgrade
|
||||
@@ -188,10 +239,11 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_3: LegacyConfigMigration[] = [
|
||||
"Required since v2026.2.26. Add other machine origins to gateway.controlUi.allowedOrigins if needed.",
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
}),
|
||||
defineLegacyConfigMigration({
|
||||
id: "memorySearch->agents.defaults.memorySearch",
|
||||
describe: "Move top-level memorySearch to agents.defaults.memorySearch",
|
||||
legacyRules: [MEMORY_SEARCH_RULE],
|
||||
apply: (raw, changes) => {
|
||||
const legacyMemorySearch = getRecord(raw.memorySearch);
|
||||
if (!legacyMemorySearch) {
|
||||
@@ -210,8 +262,49 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_3: LegacyConfigMigration[] = [
|
||||
});
|
||||
delete raw.memorySearch;
|
||||
},
|
||||
},
|
||||
{
|
||||
}),
|
||||
defineLegacyConfigMigration({
|
||||
id: "gateway.bind.host-alias->bind-mode",
|
||||
describe: "Normalize gateway.bind host aliases to supported bind modes",
|
||||
legacyRules: [GATEWAY_BIND_RULE],
|
||||
apply: (raw, changes) => {
|
||||
const gateway = getRecord(raw.gateway);
|
||||
if (!gateway) {
|
||||
return;
|
||||
}
|
||||
const bindRaw = gateway.bind;
|
||||
if (typeof bindRaw !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalized = bindRaw.trim().toLowerCase();
|
||||
let mapped: "lan" | "loopback" | undefined;
|
||||
if (
|
||||
normalized === "0.0.0.0" ||
|
||||
normalized === "::" ||
|
||||
normalized === "[::]" ||
|
||||
normalized === "*"
|
||||
) {
|
||||
mapped = "lan";
|
||||
} else if (
|
||||
normalized === "127.0.0.1" ||
|
||||
normalized === "localhost" ||
|
||||
normalized === "::1" ||
|
||||
normalized === "[::1]"
|
||||
) {
|
||||
mapped = "loopback";
|
||||
}
|
||||
|
||||
if (!mapped || normalized === mapped) {
|
||||
return;
|
||||
}
|
||||
|
||||
gateway.bind = mapped;
|
||||
raw.gateway = gateway;
|
||||
changes.push(`Normalized gateway.bind "${escapeControlForLog(bindRaw)}" → "${mapped}".`);
|
||||
},
|
||||
}),
|
||||
defineLegacyConfigMigration({
|
||||
id: "tts.providers-generic-shape",
|
||||
describe: "Move legacy bundled TTS config keys into messages.tts.providers",
|
||||
apply: (raw, changes) => {
|
||||
@@ -240,10 +333,11 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_3: LegacyConfigMigration[] = [
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
}),
|
||||
defineLegacyConfigMigration({
|
||||
id: "heartbeat->agents.defaults.heartbeat",
|
||||
describe: "Move top-level heartbeat to agents.defaults.heartbeat/channels.defaults.heartbeat",
|
||||
legacyRules: [HEARTBEAT_RULE],
|
||||
apply: (raw, changes) => {
|
||||
const legacyHeartbeat = getRecord(raw.heartbeat);
|
||||
if (!legacyHeartbeat) {
|
||||
@@ -283,5 +377,5 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_3: LegacyConfigMigration[] = [
|
||||
}
|
||||
delete raw.heartbeat;
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
@@ -1,9 +1,17 @@
|
||||
import { LEGACY_CONFIG_MIGRATIONS_PART_1 } from "./legacy.migrations.part-1.js";
|
||||
import { LEGACY_CONFIG_MIGRATIONS_PART_2 } from "./legacy.migrations.part-2.js";
|
||||
import { LEGACY_CONFIG_MIGRATIONS_PART_3 } from "./legacy.migrations.part-3.js";
|
||||
import { LEGACY_CONFIG_MIGRATIONS_AUDIO } from "./legacy.migrations.audio.js";
|
||||
import { LEGACY_CONFIG_MIGRATIONS_CHANNELS } from "./legacy.migrations.channels.js";
|
||||
import { LEGACY_CONFIG_MIGRATIONS_RUNTIME } from "./legacy.migrations.runtime.js";
|
||||
|
||||
export const LEGACY_CONFIG_MIGRATIONS = [
|
||||
...LEGACY_CONFIG_MIGRATIONS_PART_1,
|
||||
...LEGACY_CONFIG_MIGRATIONS_PART_2,
|
||||
...LEGACY_CONFIG_MIGRATIONS_PART_3,
|
||||
const LEGACY_CONFIG_MIGRATION_SPECS = [
|
||||
...LEGACY_CONFIG_MIGRATIONS_CHANNELS,
|
||||
...LEGACY_CONFIG_MIGRATIONS_AUDIO,
|
||||
...LEGACY_CONFIG_MIGRATIONS_RUNTIME,
|
||||
];
|
||||
|
||||
export const LEGACY_CONFIG_MIGRATIONS = LEGACY_CONFIG_MIGRATION_SPECS.map(
|
||||
({ legacyRules: _legacyRules, ...migration }) => migration,
|
||||
);
|
||||
|
||||
export const LEGACY_CONFIG_MIGRATION_RULES = LEGACY_CONFIG_MIGRATION_SPECS.flatMap(
|
||||
(migration) => migration.legacyRules ?? [],
|
||||
);
|
||||
|
||||
@@ -1,85 +1 @@
|
||||
import type { LegacyConfigRule } from "./legacy.shared.js";
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function hasLegacyThreadBindingTtl(value: unknown): boolean {
|
||||
return isRecord(value) && Object.prototype.hasOwnProperty.call(value, "ttlHours");
|
||||
}
|
||||
|
||||
function hasLegacyThreadBindingTtlInAccounts(value: unknown): boolean {
|
||||
if (!isRecord(value)) {
|
||||
return false;
|
||||
}
|
||||
return Object.values(value).some((entry) =>
|
||||
hasLegacyThreadBindingTtl(isRecord(entry) ? entry.threadBindings : undefined),
|
||||
);
|
||||
}
|
||||
|
||||
function isLegacyGatewayBindHostAlias(value: unknown): boolean {
|
||||
if (typeof value !== "string") {
|
||||
return false;
|
||||
}
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
normalized === "auto" ||
|
||||
normalized === "loopback" ||
|
||||
normalized === "lan" ||
|
||||
normalized === "tailnet" ||
|
||||
normalized === "custom"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
normalized === "0.0.0.0" ||
|
||||
normalized === "::" ||
|
||||
normalized === "[::]" ||
|
||||
normalized === "*" ||
|
||||
normalized === "127.0.0.1" ||
|
||||
normalized === "localhost" ||
|
||||
normalized === "::1" ||
|
||||
normalized === "[::1]"
|
||||
);
|
||||
}
|
||||
|
||||
export const LEGACY_CONFIG_RULES: LegacyConfigRule[] = [
|
||||
{
|
||||
path: ["session", "threadBindings"],
|
||||
message:
|
||||
"session.threadBindings.ttlHours was renamed to session.threadBindings.idleHours (auto-migrated on load).",
|
||||
match: (value) => hasLegacyThreadBindingTtl(value),
|
||||
},
|
||||
{
|
||||
path: ["channels", "discord", "threadBindings"],
|
||||
message:
|
||||
"channels.discord.threadBindings.ttlHours was renamed to channels.discord.threadBindings.idleHours (auto-migrated on load).",
|
||||
match: (value) => hasLegacyThreadBindingTtl(value),
|
||||
},
|
||||
{
|
||||
path: ["channels", "discord", "accounts"],
|
||||
message:
|
||||
"channels.discord.accounts.<id>.threadBindings.ttlHours was renamed to channels.discord.accounts.<id>.threadBindings.idleHours (auto-migrated on load).",
|
||||
match: (value) => hasLegacyThreadBindingTtlInAccounts(value),
|
||||
},
|
||||
{
|
||||
path: ["memorySearch"],
|
||||
message:
|
||||
"top-level memorySearch was moved; use agents.defaults.memorySearch instead (auto-migrated on load).",
|
||||
},
|
||||
{
|
||||
path: ["gateway", "bind"],
|
||||
message:
|
||||
"gateway.bind host aliases (for example 0.0.0.0/localhost) are legacy; use bind modes (lan/loopback/custom/tailnet/auto) instead (auto-migrated on load).",
|
||||
match: (value) => isLegacyGatewayBindHostAlias(value),
|
||||
requireSourceLiteral: true,
|
||||
},
|
||||
{
|
||||
path: ["heartbeat"],
|
||||
message:
|
||||
"top-level heartbeat is not a valid config path; use agents.defaults.heartbeat (cadence/target/model settings) or channels.defaults.heartbeat (showOk/showAlerts/useIndicator).",
|
||||
},
|
||||
];
|
||||
export { LEGACY_CONFIG_MIGRATION_RULES as LEGACY_CONFIG_RULES } from "./legacy.migrations.js";
|
||||
|
||||
@@ -13,6 +13,10 @@ export type LegacyConfigMigration = {
|
||||
apply: (raw: Record<string, unknown>, changes: string[]) => void;
|
||||
};
|
||||
|
||||
export type LegacyConfigMigrationSpec = LegacyConfigMigration & {
|
||||
legacyRules?: LegacyConfigRule[];
|
||||
};
|
||||
|
||||
import { isSafeExecutableValue } from "../infra/exec-safety.js";
|
||||
import { isRecord } from "../utils.js";
|
||||
import { isBlockedObjectKey } from "./prototype-keys.js";
|
||||
@@ -131,3 +135,7 @@ export const ensureAgentEntry = (list: unknown[], id: string): Record<string, un
|
||||
list.push(created);
|
||||
return created;
|
||||
};
|
||||
|
||||
export const defineLegacyConfigMigration = (
|
||||
migration: LegacyConfigMigrationSpec,
|
||||
): LegacyConfigMigrationSpec => migration;
|
||||
|
||||
Reference in New Issue
Block a user