diff --git a/src/commands/doctor-state-migrations.test.ts b/src/commands/doctor-state-migrations.test.ts index 24bbb4e8e39..4116a6fca6e 100644 --- a/src/commands/doctor-state-migrations.test.ts +++ b/src/commands/doctor-state-migrations.test.ts @@ -296,6 +296,9 @@ describe("doctor legacy state migrations", () => { env: { OPENCLAW_STATE_DIR: root } as NodeJS.ProcessEnv, }); expect(detected.pairingAllowFrom.hasLegacyTelegram).toBe(true); + expect( + detected.pairingAllowFrom.copyPlans.map((plan) => path.basename(plan.targetPath)), + ).toEqual(["telegram-default-allowFrom.json"]); const result = await runLegacyStateMigrations({ detected, now: () => 123 }); expect(result.warnings).toEqual([]); @@ -308,6 +311,59 @@ describe("doctor legacy state migrations", () => { }); }); + it("fans out legacy Telegram pairing allowFrom store to configured named accounts", async () => { + const root = await makeTempRoot(); + const cfg: OpenClawConfig = { + channels: { + telegram: { + accounts: { + bot1: {}, + bot2: {}, + }, + }, + }, + }; + const oauthDir = ensureCredentialsDir(root); + fs.writeFileSync( + path.join(oauthDir, "telegram-allowFrom.json"), + JSON.stringify( + { + version: 1, + allowFrom: ["123456"], + }, + null, + 2, + ) + "\n", + "utf-8", + ); + + const detected = await detectLegacyStateMigrations({ + cfg, + env: { OPENCLAW_STATE_DIR: root } as NodeJS.ProcessEnv, + }); + expect(detected.pairingAllowFrom.hasLegacyTelegram).toBe(true); + expect( + detected.pairingAllowFrom.copyPlans.map((plan) => path.basename(plan.targetPath)).toSorted(), + ).toEqual(["telegram-bot1-allowFrom.json", "telegram-bot2-allowFrom.json"]); + + const result = await runLegacyStateMigrations({ detected, now: () => 123 }); + expect(result.warnings).toEqual([]); + + const bot1Target = path.join(oauthDir, "telegram-bot1-allowFrom.json"); + const bot2Target = path.join(oauthDir, "telegram-bot2-allowFrom.json"); + expect(fs.existsSync(bot1Target)).toBe(true); + expect(fs.existsSync(bot2Target)).toBe(true); + expect(fs.existsSync(path.join(oauthDir, "telegram-default-allowFrom.json"))).toBe(false); + expect(JSON.parse(fs.readFileSync(bot1Target, "utf-8"))).toEqual({ + version: 1, + allowFrom: ["123456"], + }); + expect(JSON.parse(fs.readFileSync(bot2Target, "utf-8"))).toEqual({ + version: 1, + allowFrom: ["123456"], + }); + }); + it("no-ops when nothing detected", async () => { const root = await makeTempRoot(); const cfg: OpenClawConfig = {}; diff --git a/src/infra/state-migrations.ts b/src/infra/state-migrations.ts index 155bc6289f6..2aa50037e0c 100644 --- a/src/infra/state-migrations.ts +++ b/src/infra/state-migrations.ts @@ -21,6 +21,7 @@ import { DEFAULT_MAIN_KEY, normalizeAgentId, } from "../routing/session-key.js"; +import { listTelegramAccountIds } from "../telegram/accounts.js"; import { isWithinDir } from "./path-safety.js"; import { ensureDir, @@ -57,13 +58,18 @@ export type LegacyStateDetection = { hasLegacy: boolean; }; pairingAllowFrom: { - legacyTelegramPath: string; - targetTelegramPath: string; hasLegacyTelegram: boolean; + copyPlans: FileCopyPlan[]; }; preview: string[]; }; +type FileCopyPlan = { + label: string; + sourcePath: string; + targetPath: string; +}; + type MigrationLogger = { info: (message: string) => void; warn: (message: string) => void; @@ -98,6 +104,30 @@ function isLegacyGroupKey(key: string): boolean { return false; } +function buildFileCopyPreview(plan: FileCopyPlan): string { + return `- ${plan.label}: ${plan.sourcePath} → ${plan.targetPath}`; +} + +async function runFileCopyPlans( + plans: FileCopyPlan[], +): Promise<{ changes: string[]; warnings: string[] }> { + const changes: string[] = []; + const warnings: string[] = []; + for (const plan of plans) { + if (fileExists(plan.targetPath)) { + continue; + } + try { + ensureDir(path.dirname(plan.targetPath)); + fs.copyFileSync(plan.sourcePath, plan.targetPath); + changes.push(`Copied ${plan.label} → ${plan.targetPath}`); + } catch (err) { + warnings.push(`Failed migrating ${plan.label} (${plan.sourcePath}): ${String(err)}`); + } + } + return { changes, warnings }; +} + function canonicalizeSessionKeyForAgent(params: { key: string; agentId: string; @@ -619,13 +649,24 @@ export async function detectLegacyStateMigrations(params: { fileExists(path.join(oauthDir, "creds.json")) && !fileExists(path.join(targetWhatsAppAuthDir, "creds.json")); const legacyTelegramAllowFromPath = resolveChannelAllowFromPath("telegram", env); - const targetTelegramAllowFromPath = resolveChannelAllowFromPath( - "telegram", - env, - DEFAULT_ACCOUNT_ID, - ); - const hasLegacyTelegramAllowFrom = - fileExists(legacyTelegramAllowFromPath) && !fileExists(targetTelegramAllowFromPath); + const telegramPairingAllowFromPlans = fileExists(legacyTelegramAllowFromPath) + ? Array.from( + new Set( + listTelegramAccountIds(params.cfg).map((accountId) => + resolveChannelAllowFromPath("telegram", env, accountId), + ), + ), + ) + .filter((targetPath) => !fileExists(targetPath)) + .map( + (targetPath): FileCopyPlan => ({ + label: "Telegram pairing allowFrom", + sourcePath: legacyTelegramAllowFromPath, + targetPath, + }), + ) + : []; + const hasLegacyTelegramAllowFrom = telegramPairingAllowFromPlans.length > 0; const preview: string[] = []; if (hasLegacySessions) { @@ -641,9 +682,7 @@ export async function detectLegacyStateMigrations(params: { preview.push(`- WhatsApp auth: ${oauthDir} → ${targetWhatsAppAuthDir} (keep oauth.json)`); } if (hasLegacyTelegramAllowFrom) { - preview.push( - `- Telegram pairing allowFrom: ${legacyTelegramAllowFromPath} → ${targetTelegramAllowFromPath}`, - ); + preview.push(...telegramPairingAllowFromPlans.map(buildFileCopyPreview)); } return { @@ -671,9 +710,8 @@ export async function detectLegacyStateMigrations(params: { hasLegacy: hasLegacyWhatsAppAuth, }, pairingAllowFrom: { - legacyTelegramPath: legacyTelegramAllowFromPath, - targetTelegramPath: targetTelegramAllowFromPath, hasLegacyTelegram: hasLegacyTelegramAllowFrom, + copyPlans: telegramPairingAllowFromPlans, }, preview, }; @@ -899,18 +937,7 @@ async function migrateLegacyTelegramPairingAllowFrom( if (!detected.pairingAllowFrom.hasLegacyTelegram) { return { changes, warnings }; } - - const legacyPath = detected.pairingAllowFrom.legacyTelegramPath; - const targetPath = detected.pairingAllowFrom.targetTelegramPath; - try { - ensureDir(path.dirname(targetPath)); - fs.copyFileSync(legacyPath, targetPath); - changes.push(`Copied Telegram pairing allowFrom → ${targetPath}`); - } catch (err) { - warnings.push(`Failed migrating Telegram pairing allowFrom (${legacyPath}): ${String(err)}`); - } - - return { changes, warnings }; + return await runFileCopyPlans(detected.pairingAllowFrom.copyPlans); } export async function runLegacyStateMigrations(params: {