

























@@ -7,7 +7,13 @@ import type { OpenClawConfig } from "../config/config.js";
77import { resolveChannelAllowFromPath } from "../pairing/pairing-store.js";
88import { openOpenClawStateDatabase } from "../state/openclaw-state-db.js";
99import { createTrackedTempDirs } from "../test-utils/tracked-temp-dirs.js";
10-import { detectLegacyStateMigrations, runLegacyStateMigrations } from "./state-migrations.js";
10+import {
11+autoMigrateLegacyState,
12+detectLegacyStateMigrations,
13+runLegacyStateMigrations,
14+} from "./state-migrations.js";
15+import { loadVoiceWakeRoutingConfig, setVoiceWakeRoutingConfig } from "./voicewake-routing.js";
16+import { loadVoiceWakeConfig, setVoiceWakeTriggers } from "./voicewake.js";
11171218vi.mock("../channels/plugins/bundled.js", () => {
1319function fileExists(filePath: string): boolean {
@@ -401,6 +407,117 @@ describe("state migrations", () => {
401407await expectMissingPath(path.join(stateDir, "session-delivery-queue"));
402408});
403409410+it("migrates legacy voice wake JSON settings into shared SQLite state", async () => {
411+const root = await createTempDir();
412+const stateDir = path.join(root, ".openclaw");
413+const env = createEnv(stateDir);
414+const cfg = createConfig();
415+const settingsDir = path.join(stateDir, "settings");
416+const triggersPath = path.join(settingsDir, "voicewake.json");
417+const routingPath = path.join(settingsDir, "voicewake-routing.json");
418+await fs.mkdir(settingsDir, { recursive: true });
419+await fs.writeFile(
420+triggersPath,
421+JSON.stringify({ triggers: [" wake ", "", "there"], updatedAtMs: -1 }),
422+"utf8",
423+);
424+await fs.writeFile(
425+routingPath,
426+JSON.stringify({
427+defaultTarget: { mode: "current" },
428+routes: [
429+{ trigger: " Robot Wake ", target: { agentId: "Main Agent" } },
430+{ trigger: "", target: { sessionKey: "agent:main:voice" } },
431+],
432+}),
433+"utf8",
434+);
435+436+const detected = await detectLegacyStateMigrations({ cfg, env, homedir: () => root });
437+expect(detected.voiceWake.hasLegacy).toBe(true);
438+expect(detected.preview).toContain(
439+"- Voice Wake settings: legacy JSON files → shared SQLite state",
440+);
441+442+const result = await runLegacyStateMigrations({ detected, config: cfg });
443+444+expect(result.warnings).toStrictEqual([]);
445+expect(result.changes).toContain("Migrated 2 voice wake triggers → shared SQLite state");
446+expect(result.changes).toContain(
447+"Migrated voice wake routing config with 1 route → shared SQLite state",
448+);
449+await expect(loadVoiceWakeConfig(stateDir)).resolves.toMatchObject({
450+triggers: ["wake", "there"],
451+});
452+await expect(loadVoiceWakeRoutingConfig(stateDir)).resolves.toMatchObject({
453+defaultTarget: { mode: "current" },
454+routes: [{ trigger: "robot wake", target: { agentId: "main-agent" } }],
455+});
456+await expectMissingPath(triggersPath);
457+await expectMissingPath(routingPath);
458+await expect(fs.readFile(`${triggersPath}.migrated`, "utf8")).resolves.toContain("wake");
459+await expect(fs.readFile(`${routingPath}.migrated`, "utf8")).resolves.toContain("Robot");
460+});
461+462+it("archives legacy voice wake JSON when shared SQLite already matches", async () => {
463+const root = await createTempDir();
464+const stateDir = path.join(root, ".openclaw");
465+const env = createEnv(stateDir);
466+const cfg = createConfig();
467+const settingsDir = path.join(stateDir, "settings");
468+const triggersPath = path.join(settingsDir, "voicewake.json");
469+const routingPath = path.join(settingsDir, "voicewake-routing.json");
470+await setVoiceWakeTriggers(["wake"], stateDir);
471+await setVoiceWakeRoutingConfig(
472+{
473+defaultTarget: { mode: "current" },
474+routes: [{ trigger: "robot wake", target: { agentId: "main" } }],
475+},
476+stateDir,
477+);
478+await fs.mkdir(settingsDir, { recursive: true });
479+await fs.writeFile(triggersPath, JSON.stringify({ triggers: ["wake"] }), "utf8");
480+await fs.writeFile(
481+routingPath,
482+JSON.stringify({
483+defaultTarget: { mode: "current" },
484+routes: [{ trigger: "robot wake", target: { agentId: "main" } }],
485+}),
486+"utf8",
487+);
488+489+const detected = await detectLegacyStateMigrations({ cfg, env, homedir: () => root });
490+const result = await runLegacyStateMigrations({ detected, config: cfg });
491+492+expect(result.warnings).toStrictEqual([]);
493+await expectMissingPath(triggersPath);
494+await expectMissingPath(routingPath);
495+await expect(fs.readFile(`${triggersPath}.migrated`, "utf8")).resolves.toContain("wake");
496+await expect(fs.readFile(`${routingPath}.migrated`, "utf8")).resolves.toContain("robot wake");
497+});
498+499+it("auto-migrates standalone legacy voice wake JSON settings", async () => {
500+const root = await createTempDir();
501+const stateDir = path.join(root, ".openclaw");
502+const env = createEnv(stateDir);
503+const cfg = createConfig();
504+const settingsDir = path.join(stateDir, "settings");
505+await fs.mkdir(settingsDir, { recursive: true });
506+await fs.writeFile(
507+path.join(settingsDir, "voicewake.json"),
508+JSON.stringify({ triggers: ["wake"] }),
509+"utf8",
510+);
511+512+const result = await autoMigrateLegacyState({ cfg, env, homedir: () => root });
513+514+expect(result.skipped).toBe(false);
515+expect(result.migrated).toBe(true);
516+expect(result.warnings).toStrictEqual([]);
517+await expect(loadVoiceWakeConfig(stateDir)).resolves.toMatchObject({ triggers: ["wake"] });
518+await expectMissingPath(path.join(settingsDir, "voicewake.json"));
519+});
520+404521it("keeps legacy delivery queue files when shared SQLite already has a conflicting row", async () => {
405522const root = await createTempDir();
406523const stateDir = path.join(root, ".openclaw");
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。