

























@@ -97,6 +97,7 @@ vi.mock("../channels/plugins/bundled.js", () => {
9797const tempDirs = createTrackedTempDirs();
98989999type UpdateCheckStateDatabase = Pick<OpenClawStateKyselyDatabase, "update_check_state">;
100+type PluginBindingApprovalsDatabase = Pick<OpenClawStateKyselyDatabase, "plugin_binding_approvals">;
100101type CurrentConversationBindingsDatabase = Pick<
101102OpenClawStateKyselyDatabase,
102103"current_conversation_bindings"
@@ -168,6 +169,25 @@ function readCurrentConversationBindingRows(env: NodeJS.ProcessEnv): Array<{
168169).rows;
169170}
170171172+function readPluginBindingApprovalRows(env: NodeJS.ProcessEnv): Array<{
173+plugin_root: string;
174+channel: string;
175+account_id: string;
176+plugin_id: string;
177+plugin_name: string | null;
178+approved_at: number;
179+}> {
180+const { db } = openOpenClawStateDatabase({ env });
181+const stateDb = getNodeSqliteKysely<PluginBindingApprovalsDatabase>(db);
182+return executeSqliteQuerySync(
183+db,
184+stateDb
185+.selectFrom("plugin_binding_approvals")
186+.select(["plugin_root", "channel", "account_id", "plugin_id", "plugin_name", "approved_at"])
187+.orderBy("plugin_root", "asc"),
188+).rows;
189+}
190+171191function insertCurrentConversationBindingRow(
172192env: NodeJS.ProcessEnv,
173193params: {
@@ -229,6 +249,7 @@ function createConfig(): OpenClawConfig {
229249function createEnv(stateDir: string): NodeJS.ProcessEnv {
230250return {
231251 ...process.env,
252+HOME: path.dirname(stateDir),
232253OPENCLAW_STATE_DIR: stateDir,
233254};
234255}
@@ -723,6 +744,104 @@ describe("state migrations", () => {
723744await expect(fs.readFile(`${sourcePath}.migrated`, "utf8")).resolves.toContain("workspace-dm");
724745});
725746747+it("migrates legacy plugin binding approvals JSON into shared SQLite state", async () => {
748+const root = await createTempDir();
749+const stateDir = path.join(root, ".openclaw");
750+const env = createEnv(stateDir);
751+const cfg = createConfig();
752+const sourcePath = path.join(stateDir, "plugin-binding-approvals.json");
753+await fs.mkdir(stateDir, { recursive: true });
754+await fs.writeFile(
755+sourcePath,
756+JSON.stringify({
757+version: 1,
758+approvals: [
759+{
760+pluginRoot: "/plugins/codex-a",
761+pluginId: "codex",
762+pluginName: "Codex App Server",
763+channel: "Discord",
764+accountId: "default",
765+approvedAt: 1234,
766+},
767+],
768+}),
769+"utf8",
770+);
771+772+const detected = await detectLegacyStateMigrations({ cfg, env, homedir: () => root });
773+expect(detected.pluginBindingApprovals.hasLegacy).toBe(true);
774+expect(detected.preview).toContain(
775+"- Plugin binding approvals: legacy JSON file → shared SQLite state",
776+);
777+778+const result = await runLegacyStateMigrations({ detected, config: cfg });
779+780+expect(result.warnings).toStrictEqual([]);
781+expect(result.changes).toContain("Migrated 1 plugin binding approval → shared SQLite state");
782+expect(readPluginBindingApprovalRows(env)).toEqual([
783+{
784+plugin_root: "/plugins/codex-a",
785+channel: "discord",
786+account_id: "default",
787+plugin_id: "codex",
788+plugin_name: "Codex App Server",
789+approved_at: 1234,
790+},
791+]);
792+await expectMissingPath(sourcePath);
793+await expect(fs.readFile(`${sourcePath}.migrated`, "utf8")).resolves.toContain(
794+"Codex App Server",
795+);
796+});
797+798+it("migrates legacy plugin binding approvals from the home state dir when using a custom state dir", async () => {
799+const root = await createTempDir();
800+const stateDir = path.join(root, "custom-state");
801+const env = createEnv(stateDir);
802+const cfg = createConfig();
803+const sourcePath = path.join(root, ".openclaw", "plugin-binding-approvals.json");
804+await fs.mkdir(path.dirname(sourcePath), { recursive: true });
805+await fs.writeFile(
806+sourcePath,
807+JSON.stringify({
808+version: 1,
809+approvals: [
810+{
811+pluginRoot: "/plugins/codex-a",
812+pluginId: "codex",
813+channel: "telegram",
814+accountId: "default",
815+approvedAt: 2345,
816+},
817+],
818+}),
819+"utf8",
820+);
821+822+const detected = await detectLegacyStateMigrations({ cfg, env, homedir: () => root });
823+expect(detected.pluginBindingApprovals).toMatchObject({
824+ sourcePath,
825+hasLegacy: true,
826+});
827+828+const result = await runLegacyStateMigrations({ detected, config: cfg });
829+830+expect(result.warnings).toStrictEqual([]);
831+expect(result.changes).toContain("Migrated 1 plugin binding approval → shared SQLite state");
832+expect(readPluginBindingApprovalRows(env)).toEqual([
833+{
834+plugin_root: "/plugins/codex-a",
835+channel: "telegram",
836+account_id: "default",
837+plugin_id: "codex",
838+plugin_name: null,
839+approved_at: 2345,
840+},
841+]);
842+await expectMissingPath(sourcePath);
843+});
844+726845it("imports non-conflicting legacy current-conversation bindings when SQLite has a conflict", async () => {
727846const root = await createTempDir();
728847const stateDir = path.join(root, ".openclaw");
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。