























1+import crypto from "node:crypto";
2+import fs from "node:fs/promises";
3+import path from "node:path";
4+import type { PluginDoctorStateMigration } from "openclaw/plugin-sdk/runtime-doctor";
5+6+type ActiveMemoryToggleEntry = {
7+sessionKey: string;
8+disabled: boolean;
9+updatedAt: number;
10+};
11+12+const TOGGLE_STATE_FILE = "session-toggles.json";
13+const SESSION_TOGGLES_NAMESPACE = "session-toggles";
14+const MAX_TOGGLE_ENTRIES = 10_000;
15+16+function resolveToggleStatePath(stateDir: string): string {
17+return path.join(stateDir, "plugins", "active-memory", TOGGLE_STATE_FILE);
18+}
19+20+function activeMemoryToggleKey(sessionKey: string): string {
21+return crypto.createHash("sha256").update(sessionKey, "utf8").digest("hex");
22+}
23+24+async function fileExists(filePath: string): Promise<boolean> {
25+try {
26+const stat = await fs.stat(filePath);
27+return stat.isFile();
28+} catch {
29+return false;
30+}
31+}
32+33+async function readLegacyToggleEntries(filePath: string): Promise<ActiveMemoryToggleEntry[]> {
34+try {
35+const parsed = JSON.parse(await fs.readFile(filePath, "utf8")) as unknown;
36+if (!parsed || typeof parsed !== "object") {
37+return [];
38+}
39+const sessions = (parsed as { sessions?: unknown }).sessions;
40+if (!sessions || typeof sessions !== "object" || Array.isArray(sessions)) {
41+return [];
42+}
43+const entries: ActiveMemoryToggleEntry[] = [];
44+for (const [sessionKey, value] of Object.entries(sessions)) {
45+if (!sessionKey.trim() || !value || typeof value !== "object" || Array.isArray(value)) {
46+continue;
47+}
48+if ((value as { disabled?: unknown }).disabled !== true) {
49+continue;
50+}
51+const updatedAt =
52+typeof (value as { updatedAt?: unknown }).updatedAt === "number"
53+ ? (value as { updatedAt: number }).updatedAt
54+ : Date.now();
55+entries.push({ sessionKey, disabled: true, updatedAt });
56+}
57+return entries;
58+} catch {
59+return [];
60+}
61+}
62+63+async function archiveLegacySource(params: {
64+filePath: string;
65+label: string;
66+changes: string[];
67+warnings: string[];
68+}): Promise<void> {
69+const archivedPath = `${params.filePath}.migrated`;
70+if (await fileExists(archivedPath)) {
71+params.warnings.push(
72+`Left migrated ${params.label} source in place because ${archivedPath} already exists`,
73+);
74+return;
75+}
76+try {
77+await fs.rename(params.filePath, archivedPath);
78+params.changes.push(`Archived ${params.label} legacy source -> ${archivedPath}`);
79+} catch (err) {
80+params.warnings.push(`Failed archiving ${params.label} legacy source: ${String(err)}`);
81+}
82+}
83+84+export const stateMigrations: PluginDoctorStateMigration[] = [
85+{
86+id: "active-memory-session-toggles-json-to-plugin-state",
87+label: "Active Memory session toggles",
88+async detectLegacyState(params) {
89+const filePath = resolveToggleStatePath(params.stateDir);
90+const entries = await readLegacyToggleEntries(filePath);
91+if (entries.length === 0) {
92+return null;
93+}
94+return {
95+preview: [
96+`- Active Memory session toggles: ${entries.length} ${entries.length === 1 ? "entry" : "entries"} -> plugin state (${SESSION_TOGGLES_NAMESPACE})`,
97+],
98+};
99+},
100+async migrateLegacyState(params) {
101+const changes: string[] = [];
102+const warnings: string[] = [];
103+const filePath = resolveToggleStatePath(params.stateDir);
104+const entries = await readLegacyToggleEntries(filePath);
105+if (entries.length === 0) {
106+return { changes, warnings };
107+}
108+const store = params.context.openPluginStateKeyedStore<ActiveMemoryToggleEntry>({
109+namespace: SESSION_TOGGLES_NAMESPACE,
110+maxEntries: MAX_TOGGLE_ENTRIES,
111+});
112+const existingKeys = new Set((await store.entries()).map((entry) => entry.key));
113+const missingEntries = entries.filter(
114+(entry) => !existingKeys.has(activeMemoryToggleKey(entry.sessionKey)),
115+);
116+if (missingEntries.length > MAX_TOGGLE_ENTRIES - existingKeys.size) {
117+warnings.push(
118+`Skipped Active Memory session toggle migration because plugin state has room for ${MAX_TOGGLE_ENTRIES - existingKeys.size} of ${missingEntries.length} missing entries; left legacy source in place`,
119+);
120+return { changes, warnings };
121+}
122+let imported = 0;
123+for (const entry of entries) {
124+const key = activeMemoryToggleKey(entry.sessionKey);
125+if (existingKeys.has(key)) {
126+continue;
127+}
128+await store.register(key, entry);
129+existingKeys.add(key);
130+imported++;
131+}
132+if (imported > 0) {
133+changes.push(
134+`Migrated ${imported} Active Memory session toggle ${imported === 1 ? "entry" : "entries"} -> plugin state`,
135+);
136+}
137+await archiveLegacySource({
138+ filePath,
139+label: "Active Memory session toggles",
140+ changes,
141+ warnings,
142+});
143+return { changes, warnings };
144+},
145+},
146+];
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。