openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
204 lines (203 loc) • 6.76 kB
JavaScript
//#region extensions/workboard/doctor-contract-api.ts
const MAX_CARDS = 2e3;
function migrationEnv(params) {
return {
...params.env,
OPENCLAW_STATE_DIR: params.stateDir
};
}
function openLegacyStore(params) {
return params.context.openPluginStateKeyedStore({
namespace: params.namespace,
maxEntries: params.maxEntries,
env: params.env
});
}
function isPersistedCard(value) {
return Boolean(value && typeof value === "object" && value.version === 1);
}
function isPersistedBoard(value) {
return Boolean(value && typeof value === "object" && value.version === 1);
}
function isPersistedSubscription(value) {
return Boolean(value && typeof value === "object" && value.version === 1);
}
function isPersistedAttachment(value) {
if (!value || typeof value !== "object") return false;
const candidate = value;
const attachment = candidate.attachment;
return candidate.version === 1 && attachment !== void 0 && typeof attachment === "object" && typeof attachment.id === "string" && typeof attachment.cardId === "string" && typeof attachment.fileName === "string" && typeof attachment.byteSize === "number" && typeof attachment.createdAt === "number" && typeof candidate.contentBase64 === "string";
}
async function migrateNamespace(params) {
const warnings = [];
let imported = 0;
for (const entry of await params.legacy.entries()) {
if (!params.isValid(entry.value)) {
warnings.push(`Skipped malformed legacy Workboard ${params.label} entry ${entry.key}`);
continue;
}
try {
const targetEntry = await params.target.lookup(entry.key);
if (targetEntry) {
if (JSON.stringify(targetEntry) === JSON.stringify(entry.value)) {
await params.legacy.delete(entry.key);
imported++;
continue;
}
warnings.push(`Skipped legacy Workboard ${params.label} entry ${entry.key} because the SQLite target already exists`);
continue;
}
await params.target.register(entry.key, entry.value);
await params.legacy.delete(entry.key);
imported++;
} catch (err) {
warnings.push(`Failed migrating legacy Workboard ${params.label} entry ${entry.key}: ${String(err)}`);
}
}
return {
imported,
warnings
};
}
async function targetCardReferencesAttachment(cards, attachment) {
const card = await cards.lookup(attachment.attachment.cardId);
return Boolean(card?.version === 1 && card.card.metadata?.attachments?.some((entry) => entry.id === attachment.attachment.id && entry.cardId === attachment.attachment.cardId));
}
async function migrateAttachments(params) {
const warnings = [];
let imported = 0;
for (const entry of await params.legacy.entries()) {
if (!isPersistedAttachment(entry.value)) {
warnings.push(`Skipped malformed legacy Workboard attachment entry ${entry.key}`);
continue;
}
if (!await targetCardReferencesAttachment(params.cards, entry.value)) {
warnings.push(`Skipped legacy Workboard attachment entry ${entry.key} because its owning card was not migrated or does not reference the attachment`);
continue;
}
const targetEntry = await params.target.lookup(entry.key);
if (targetEntry) {
if (JSON.stringify(targetEntry) === JSON.stringify(entry.value)) {
await params.legacy.delete(entry.key);
imported++;
continue;
}
warnings.push(`Skipped legacy Workboard attachment entry ${entry.key} because the SQLite target already exists`);
continue;
}
try {
await params.target.register(entry.key, entry.value);
await params.legacy.delete(entry.key);
imported++;
} catch (err) {
warnings.push(`Failed migrating legacy Workboard attachment entry ${entry.key}: ${String(err)}`);
}
}
return {
imported,
warnings
};
}
const stateMigrations = [{
id: "workboard-28-kv-to-sqlite",
label: "Workboard .28 plugin-state KV",
async detectLegacyState(params) {
const { resolveWorkboardSqlitePath } = await import("../../sqlite-store-DYFYXj5g.js");
const env = migrationEnv(params);
const cards = await openLegacyStore({
context: params.context,
env,
namespace: "workboard.cards",
maxEntries: MAX_CARDS
}).entries();
const boards = await openLegacyStore({
context: params.context,
env,
namespace: "workboard.boards",
maxEntries: 200
}).entries();
const subscriptions = await openLegacyStore({
context: params.context,
env,
namespace: "workboard.notify",
maxEntries: 2e3
}).entries();
const attachments = await openLegacyStore({
context: params.context,
env,
namespace: "workboard.attachments",
maxEntries: MAX_CARDS * 21
}).entries();
const count = cards.length + boards.length + subscriptions.length + attachments.length;
if (count === 0) return null;
return { preview: [`- Workboard: ${count} legacy .28 plugin-state KV ${count === 1 ? "entry" : "entries"} → ${resolveWorkboardSqlitePath(env)}`] };
},
async migrateLegacyState(params) {
const { createWorkboardSqliteStores } = await import("../../sqlite-store-DYFYXj5g.js");
const env = migrationEnv(params);
const cards = openLegacyStore({
context: params.context,
env,
namespace: "workboard.cards",
maxEntries: MAX_CARDS
});
const boards = openLegacyStore({
context: params.context,
env,
namespace: "workboard.boards",
maxEntries: 200
});
const subscriptions = openLegacyStore({
context: params.context,
env,
namespace: "workboard.notify",
maxEntries: 2e3
});
const attachments = openLegacyStore({
context: params.context,
env,
namespace: "workboard.attachments",
maxEntries: MAX_CARDS * 21
});
const sqlite = createWorkboardSqliteStores({ env });
try {
const cardResult = await migrateNamespace({
label: "card",
legacy: cards,
target: sqlite.cards,
isValid: isPersistedCard
});
const boardResult = await migrateNamespace({
label: "board",
legacy: boards,
target: sqlite.boards,
isValid: isPersistedBoard
});
const subscriptionResult = await migrateNamespace({
label: "notification subscription",
legacy: subscriptions,
target: sqlite.subscriptions,
isValid: isPersistedSubscription
});
const attachmentResult = await migrateAttachments({
legacy: attachments,
cards: sqlite.cards,
target: sqlite.attachments
});
const imported = cardResult.imported + boardResult.imported + subscriptionResult.imported + attachmentResult.imported;
return {
changes: imported > 0 ? [`Migrated ${imported} Workboard .28 plugin-state KV ${imported === 1 ? "entry" : "entries"} → relational SQLite`] : [],
warnings: [
...cardResult.warnings,
...boardResult.warnings,
...subscriptionResult.warnings,
...attachmentResult.warnings
]
};
} finally {
sqlite.close();
}
}
}];
//#endregion
export { stateMigrations };