mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 13:20:43 +00:00
fix(doctor): restore group config drift migrations (#77465)
This commit is contained in:
@@ -47,6 +47,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
|
- Doctor/config: restore legacy group chat config migrations for `routing.allowFrom`, `routing.groupChat.*`, and `channels.telegram.requireMention` so upgrades keep WhatsApp, Telegram, and iMessage group mention gates and history settings instead of leaving configs invalid or silently blocked.
|
||||||
- fix(gateway): clamp unbound websocket auth scopes [AI]. (#77413) Thanks @pgondhi987.
|
- fix(gateway): clamp unbound websocket auth scopes [AI]. (#77413) Thanks @pgondhi987.
|
||||||
- Gate zalouser startup name matching [AI]. (#77411) Thanks @pgondhi987.
|
- Gate zalouser startup name matching [AI]. (#77411) Thanks @pgondhi987.
|
||||||
- fix(device-pair): require pairing scope for pair command [AI]. (#76377) Thanks @pgondhi987.
|
- fix(device-pair): require pairing scope for pair command [AI]. (#76377) Thanks @pgondhi987.
|
||||||
|
|||||||
@@ -189,6 +189,7 @@ That stages grounded durable candidates into the short-term dreaming store while
|
|||||||
- `routing.groupChat.requireMention` → `channels.whatsapp/telegram/imessage.groups."*".requireMention`
|
- `routing.groupChat.requireMention` → `channels.whatsapp/telegram/imessage.groups."*".requireMention`
|
||||||
- `routing.groupChat.historyLimit` → `messages.groupChat.historyLimit`
|
- `routing.groupChat.historyLimit` → `messages.groupChat.historyLimit`
|
||||||
- `routing.groupChat.mentionPatterns` → `messages.groupChat.mentionPatterns`
|
- `routing.groupChat.mentionPatterns` → `messages.groupChat.mentionPatterns`
|
||||||
|
- `channels.telegram.requireMention` → `channels.telegram.groups."*".requireMention`
|
||||||
- configured-channel configs missing visible reply policy → `messages.groupChat.visibleReplies: "message_tool"`
|
- configured-channel configs missing visible reply policy → `messages.groupChat.visibleReplies: "message_tool"`
|
||||||
- `routing.queue` → `messages.queue`
|
- `routing.queue` → `messages.queue`
|
||||||
- `routing.bindings` → top-level `bindings`
|
- `routing.bindings` → top-level `bindings`
|
||||||
|
|||||||
@@ -182,7 +182,56 @@ describe("legacy migrate audio transcription", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("legacy migrate mention routing", () => {
|
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({
|
const res = migrateLegacyConfigForTest({
|
||||||
routing: {
|
routing: {
|
||||||
groupChat: {
|
groupChat: {
|
||||||
@@ -191,11 +240,14 @@ describe("legacy migrate mention routing", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(res.changes).toEqual([]);
|
const migratedConfig = res.config as Record<string, unknown> | null;
|
||||||
expect(res.config).toBeNull();
|
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({
|
const res = migrateLegacyConfigForTest({
|
||||||
channels: {
|
channels: {
|
||||||
telegram: {
|
telegram: {
|
||||||
@@ -204,8 +256,14 @@ describe("legacy migrate mention routing", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(res.changes).toEqual([]);
|
expect(res.config?.channels?.telegram).toEqual({
|
||||||
expect(res.config).toBeNull();
|
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";
|
import { migrateLegacyConfig } from "./legacy-config-migrate.js";
|
||||||
|
|
||||||
describe("legacy config migrate validation", () => {
|
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)", () => {
|
it("returns migrated config when unrelated plugin validation issues remain (#76798)", () => {
|
||||||
const res = migrateLegacyConfig({
|
const res = migrateLegacyConfig({
|
||||||
agents: {
|
agents: {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
defineLegacyConfigMigration,
|
defineLegacyConfigMigration,
|
||||||
|
ensureRecord,
|
||||||
getRecord,
|
getRecord,
|
||||||
type LegacyConfigMigrationSpec,
|
type LegacyConfigMigrationSpec,
|
||||||
type LegacyConfigRule,
|
type LegacyConfigRule,
|
||||||
@@ -9,6 +10,196 @@ function hasOwnKey(target: Record<string, unknown>, key: string): boolean {
|
|||||||
return Object.prototype.hasOwnProperty.call(target, key);
|
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 {
|
function hasLegacyThreadBindingTtl(value: unknown): boolean {
|
||||||
const threadBindings = getRecord(value);
|
const threadBindings = getRecord(value);
|
||||||
return Boolean(threadBindings && hasOwnKey(threadBindings, "ttlHours"));
|
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[] = [
|
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({
|
defineLegacyConfigMigration({
|
||||||
id: "thread-bindings.ttlHours->idleHours",
|
id: "thread-bindings.ttlHours->idleHours",
|
||||||
describe:
|
describe:
|
||||||
|
|||||||
Reference in New Issue
Block a user