mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:20:43 +00:00
fix(doctor): restore group config drift migrations (#77465)
This commit is contained in:
@@ -182,7 +182,56 @@ describe("legacy migrate audio transcription", () => {
|
||||
});
|
||||
|
||||
describe("legacy migrate mention routing", () => {
|
||||
it("does not rewrite removed routing.groupChat.requireMention migrations", () => {
|
||||
it("moves legacy routing group chat settings into current channel and message config", () => {
|
||||
const res = migrateLegacyConfigForTest({
|
||||
routing: {
|
||||
allowFrom: ["+15550001111"],
|
||||
groupChat: {
|
||||
requireMention: false,
|
||||
historyLimit: 12,
|
||||
mentionPatterns: ["@openclaw"],
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
whatsapp: {},
|
||||
telegram: {
|
||||
groups: {
|
||||
"*": { requireMention: true },
|
||||
},
|
||||
},
|
||||
imessage: {},
|
||||
},
|
||||
});
|
||||
|
||||
const migratedConfig = res.config as Record<string, unknown> | null;
|
||||
expect(migratedConfig?.routing).toBeUndefined();
|
||||
expect(res.config?.channels?.whatsapp?.allowFrom).toEqual(["+15550001111"]);
|
||||
expect(res.config?.channels?.whatsapp?.groups).toEqual({
|
||||
"*": { requireMention: false },
|
||||
});
|
||||
expect(res.config?.channels?.telegram?.groups).toEqual({
|
||||
"*": { requireMention: true },
|
||||
});
|
||||
expect(res.config?.channels?.imessage?.groups).toEqual({
|
||||
"*": { requireMention: false },
|
||||
});
|
||||
expect(res.config?.messages?.groupChat).toEqual({
|
||||
historyLimit: 12,
|
||||
mentionPatterns: ["@openclaw"],
|
||||
});
|
||||
expect(res.changes).toEqual(
|
||||
expect.arrayContaining([
|
||||
"Moved routing.allowFrom → channels.whatsapp.allowFrom.",
|
||||
'Moved routing.groupChat.requireMention → channels.whatsapp.groups."*".requireMention.',
|
||||
'Removed routing.groupChat.requireMention (channels.telegram.groups."*" already set).',
|
||||
'Moved routing.groupChat.requireMention → channels.imessage.groups."*".requireMention.',
|
||||
"Moved routing.groupChat.historyLimit → messages.groupChat.historyLimit.",
|
||||
"Moved routing.groupChat.mentionPatterns → messages.groupChat.mentionPatterns.",
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("removes legacy routing requireMention when no compatible channel exists", () => {
|
||||
const res = migrateLegacyConfigForTest({
|
||||
routing: {
|
||||
groupChat: {
|
||||
@@ -191,11 +240,14 @@ describe("legacy migrate mention routing", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.changes).toEqual([]);
|
||||
expect(res.config).toBeNull();
|
||||
const migratedConfig = res.config as Record<string, unknown> | null;
|
||||
expect(migratedConfig?.routing).toBeUndefined();
|
||||
expect(res.changes).toEqual([
|
||||
"Removed routing.groupChat.requireMention (no configured WhatsApp, Telegram, or iMessage channel found).",
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not rewrite removed channels.telegram.requireMention migrations", () => {
|
||||
it("moves channels.telegram.requireMention into the wildcard group default", () => {
|
||||
const res = migrateLegacyConfigForTest({
|
||||
channels: {
|
||||
telegram: {
|
||||
@@ -204,8 +256,14 @@ describe("legacy migrate mention routing", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.changes).toEqual([]);
|
||||
expect(res.config).toBeNull();
|
||||
expect(res.config?.channels?.telegram).toEqual({
|
||||
groups: {
|
||||
"*": { requireMention: false },
|
||||
},
|
||||
});
|
||||
expect(res.changes).toContain(
|
||||
'Moved channels.telegram.requireMention → channels.telegram.groups."*".requireMention.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,41 @@ import { describe, expect, it } from "vitest";
|
||||
import { migrateLegacyConfig } from "./legacy-config-migrate.js";
|
||||
|
||||
describe("legacy config migrate validation", () => {
|
||||
it("returns valid migrated config for legacy group chat routing drift", () => {
|
||||
const res = migrateLegacyConfig({
|
||||
routing: {
|
||||
allowFrom: ["+15550001111"],
|
||||
groupChat: {
|
||||
requireMention: false,
|
||||
historyLimit: 8,
|
||||
mentionPatterns: ["@openclaw"],
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
whatsapp: {},
|
||||
telegram: {},
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.partiallyValid).toBeUndefined();
|
||||
const migratedConfig = res.config as Record<string, unknown> | null;
|
||||
expect(migratedConfig?.routing).toBeUndefined();
|
||||
expect(res.config?.channels?.whatsapp?.allowFrom).toEqual(["+15550001111"]);
|
||||
expect(res.config?.channels?.whatsapp?.groups).toEqual({
|
||||
"*": { requireMention: false },
|
||||
});
|
||||
expect(res.config?.channels?.telegram?.groups).toEqual({
|
||||
"*": { requireMention: false },
|
||||
});
|
||||
expect(res.config?.messages?.groupChat).toEqual({
|
||||
historyLimit: 8,
|
||||
mentionPatterns: ["@openclaw"],
|
||||
});
|
||||
expect(res.changes).toContain(
|
||||
'Moved routing.groupChat.requireMention → channels.telegram.groups."*".requireMention.',
|
||||
);
|
||||
});
|
||||
|
||||
it("returns migrated config when unrelated plugin validation issues remain (#76798)", () => {
|
||||
const res = migrateLegacyConfig({
|
||||
agents: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
defineLegacyConfigMigration,
|
||||
ensureRecord,
|
||||
getRecord,
|
||||
type LegacyConfigMigrationSpec,
|
||||
type LegacyConfigRule,
|
||||
@@ -9,6 +10,196 @@ function hasOwnKey(target: Record<string, unknown>, key: string): boolean {
|
||||
return Object.prototype.hasOwnProperty.call(target, key);
|
||||
}
|
||||
|
||||
function cleanupEmptyRecord(parent: Record<string, unknown>, key: string): void {
|
||||
const value = getRecord(parent[key]);
|
||||
if (value && Object.keys(value).length === 0) {
|
||||
delete parent[key];
|
||||
}
|
||||
}
|
||||
|
||||
function resolveCompatibleDefaultGroupEntry(section: Record<string, unknown>): {
|
||||
groups: Record<string, unknown>;
|
||||
entry: Record<string, unknown>;
|
||||
} | null {
|
||||
const existingGroups = section.groups;
|
||||
if (existingGroups !== undefined && !getRecord(existingGroups)) {
|
||||
return null;
|
||||
}
|
||||
const groups = getRecord(existingGroups) ?? {};
|
||||
const defaultKey = "*";
|
||||
const existingEntry = groups[defaultKey];
|
||||
if (existingEntry !== undefined && !getRecord(existingEntry)) {
|
||||
return null;
|
||||
}
|
||||
const entry = getRecord(existingEntry) ?? {};
|
||||
return { groups, entry };
|
||||
}
|
||||
|
||||
function migrateChannelDefaultRequireMention(params: {
|
||||
section: Record<string, unknown>;
|
||||
channelId: string;
|
||||
legacyPath: string;
|
||||
requireMention: unknown;
|
||||
changes: string[];
|
||||
}): boolean {
|
||||
const defaultGroupEntry = resolveCompatibleDefaultGroupEntry(params.section);
|
||||
if (!defaultGroupEntry) {
|
||||
params.changes.push(
|
||||
`Removed ${params.legacyPath} (channels.${params.channelId}.groups has an incompatible shape; fix remaining issues manually).`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const { groups, entry } = defaultGroupEntry;
|
||||
if (entry.requireMention === undefined) {
|
||||
entry.requireMention = params.requireMention;
|
||||
groups["*"] = entry;
|
||||
params.section.groups = groups;
|
||||
params.changes.push(
|
||||
`Moved ${params.legacyPath} → channels.${params.channelId}.groups."*".requireMention.`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
params.changes.push(
|
||||
`Removed ${params.legacyPath} (channels.${params.channelId}.groups."*" already set).`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
function migrateRoutingAllowFrom(raw: Record<string, unknown>, changes: string[]): void {
|
||||
const routing = getRecord(raw.routing);
|
||||
if (!routing || routing.allowFrom === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const channels = getRecord(raw.channels);
|
||||
const whatsapp = getRecord(channels?.whatsapp);
|
||||
if (!channels || !whatsapp) {
|
||||
delete routing.allowFrom;
|
||||
cleanupEmptyRecord(raw, "routing");
|
||||
changes.push("Removed routing.allowFrom (channels.whatsapp not configured).");
|
||||
return;
|
||||
}
|
||||
|
||||
if (whatsapp.allowFrom === undefined) {
|
||||
whatsapp.allowFrom = routing.allowFrom;
|
||||
changes.push("Moved routing.allowFrom → channels.whatsapp.allowFrom.");
|
||||
} else {
|
||||
changes.push("Removed routing.allowFrom (channels.whatsapp.allowFrom already set).");
|
||||
}
|
||||
|
||||
delete routing.allowFrom;
|
||||
channels.whatsapp = whatsapp;
|
||||
raw.channels = channels;
|
||||
cleanupEmptyRecord(raw, "routing");
|
||||
}
|
||||
|
||||
function migrateRoutingGroupChatMessages(params: {
|
||||
raw: Record<string, unknown>;
|
||||
routing: Record<string, unknown>;
|
||||
groupChat: Record<string, unknown>;
|
||||
changes: string[];
|
||||
}): void {
|
||||
const migrateMessageGroupField = (field: "historyLimit" | "mentionPatterns") => {
|
||||
const value = params.groupChat[field];
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const messages = ensureRecord(params.raw, "messages");
|
||||
const messagesGroup = ensureRecord(messages, "groupChat");
|
||||
if (messagesGroup[field] === undefined) {
|
||||
messagesGroup[field] = value;
|
||||
params.changes.push(`Moved routing.groupChat.${field} → messages.groupChat.${field}.`);
|
||||
} else {
|
||||
params.changes.push(
|
||||
`Removed routing.groupChat.${field} (messages.groupChat.${field} already set).`,
|
||||
);
|
||||
}
|
||||
delete params.groupChat[field];
|
||||
};
|
||||
|
||||
migrateMessageGroupField("historyLimit");
|
||||
migrateMessageGroupField("mentionPatterns");
|
||||
|
||||
if (Object.keys(params.groupChat).length === 0) {
|
||||
delete params.routing.groupChat;
|
||||
} else {
|
||||
params.routing.groupChat = params.groupChat;
|
||||
}
|
||||
}
|
||||
|
||||
function migrateRoutingGroupChatRequireMention(params: {
|
||||
raw: Record<string, unknown>;
|
||||
groupChat: Record<string, unknown>;
|
||||
changes: string[];
|
||||
}): void {
|
||||
const requireMention = params.groupChat.requireMention;
|
||||
if (requireMention === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const channels = getRecord(params.raw.channels);
|
||||
let matchedChannel = false;
|
||||
if (channels) {
|
||||
for (const channelId of ["whatsapp", "telegram", "imessage"]) {
|
||||
const section = getRecord(channels[channelId]);
|
||||
if (!section) {
|
||||
continue;
|
||||
}
|
||||
matchedChannel = true;
|
||||
migrateChannelDefaultRequireMention({
|
||||
section,
|
||||
channelId,
|
||||
legacyPath: "routing.groupChat.requireMention",
|
||||
requireMention,
|
||||
changes: params.changes,
|
||||
});
|
||||
channels[channelId] = section;
|
||||
}
|
||||
params.raw.channels = channels;
|
||||
}
|
||||
|
||||
if (!matchedChannel) {
|
||||
params.changes.push(
|
||||
"Removed routing.groupChat.requireMention (no configured WhatsApp, Telegram, or iMessage channel found).",
|
||||
);
|
||||
}
|
||||
delete params.groupChat.requireMention;
|
||||
}
|
||||
|
||||
function migrateRoutingGroupChat(raw: Record<string, unknown>, changes: string[]): void {
|
||||
const routing = getRecord(raw.routing);
|
||||
const groupChat = getRecord(routing?.groupChat);
|
||||
if (!routing || !groupChat) {
|
||||
return;
|
||||
}
|
||||
|
||||
migrateRoutingGroupChatRequireMention({ raw, groupChat, changes });
|
||||
migrateRoutingGroupChatMessages({ raw, routing, groupChat, changes });
|
||||
cleanupEmptyRecord(raw, "routing");
|
||||
}
|
||||
|
||||
function migrateTelegramRequireMention(raw: Record<string, unknown>, changes: string[]): void {
|
||||
const channels = getRecord(raw.channels);
|
||||
const telegram = getRecord(channels?.telegram);
|
||||
if (!channels || !telegram || telegram.requireMention === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
migrateChannelDefaultRequireMention({
|
||||
section: telegram,
|
||||
channelId: "telegram",
|
||||
legacyPath: "channels.telegram.requireMention",
|
||||
requireMention: telegram.requireMention,
|
||||
changes,
|
||||
});
|
||||
delete telegram.requireMention;
|
||||
channels.telegram = telegram;
|
||||
raw.channels = channels;
|
||||
}
|
||||
|
||||
function hasLegacyThreadBindingTtl(value: unknown): boolean {
|
||||
const threadBindings = getRecord(value);
|
||||
return Boolean(threadBindings && hasOwnKey(threadBindings, "ttlHours"));
|
||||
@@ -190,7 +381,46 @@ const THREAD_BINDING_RULES: LegacyConfigRule[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const GROUP_ROUTING_RULES: LegacyConfigRule[] = [
|
||||
{
|
||||
path: ["routing", "allowFrom"],
|
||||
message:
|
||||
'routing.allowFrom was removed; use channels.whatsapp.allowFrom instead. Run "openclaw doctor --fix".',
|
||||
},
|
||||
{
|
||||
path: ["routing", "groupChat", "requireMention"],
|
||||
message:
|
||||
'routing.groupChat.requireMention was removed; use channels.<channel>.groups."*".requireMention instead. Run "openclaw doctor --fix".',
|
||||
},
|
||||
{
|
||||
path: ["routing", "groupChat", "historyLimit"],
|
||||
message:
|
||||
'routing.groupChat.historyLimit was moved; use messages.groupChat.historyLimit instead. Run "openclaw doctor --fix".',
|
||||
},
|
||||
{
|
||||
path: ["routing", "groupChat", "mentionPatterns"],
|
||||
message:
|
||||
'routing.groupChat.mentionPatterns was moved; use messages.groupChat.mentionPatterns instead. Run "openclaw doctor --fix".',
|
||||
},
|
||||
{
|
||||
path: ["channels", "telegram", "requireMention"],
|
||||
message:
|
||||
'channels.telegram.requireMention was removed; use channels.telegram.groups."*".requireMention instead. Run "openclaw doctor --fix".',
|
||||
},
|
||||
];
|
||||
|
||||
export const LEGACY_CONFIG_MIGRATIONS_CHANNELS: LegacyConfigMigrationSpec[] = [
|
||||
defineLegacyConfigMigration({
|
||||
id: "legacy-group-routing->channel-groups",
|
||||
describe:
|
||||
"Move legacy routing group chat settings to current channel group and messages config",
|
||||
legacyRules: GROUP_ROUTING_RULES,
|
||||
apply: (raw, changes) => {
|
||||
migrateRoutingAllowFrom(raw, changes);
|
||||
migrateRoutingGroupChat(raw, changes);
|
||||
migrateTelegramRequireMention(raw, changes);
|
||||
},
|
||||
}),
|
||||
defineLegacyConfigMigration({
|
||||
id: "thread-bindings.ttlHours->idleHours",
|
||||
describe:
|
||||
|
||||
Reference in New Issue
Block a user