Files
openclaw/extensions/canvas/doctor-contract-api.ts
Peter Steinberger 8cb53c7b55 perf(doctor): keep bundled doctor contract closures dependency-light (#120698)
* perf(doctor): keep bundled doctor contract closures dependency-light

Doctor contract enumeration cold-loads each plugin's doctor-contract-api
closure via jiti, so a static value import of openclaw/plugin-sdk/runtime-doctor
pulled the state-db/kysely graph (~4.3s per closure) into
listPluginDoctorLegacyConfigRules / listPluginDoctorStateMigrationEntries.

- migrate all light doctor-contract closures (66 files) to the
  dependency-light openclaw/plugin-sdk/runtime-doctor-migrations subpath
- voice-call: load detect/repairOpenClawStateDatabaseSchema* lazily inside
  the migration bodies; keep only a type-only static runtime-doctor import
- matrix: split pure credential record shapes/normalizers into
  credentials-state.ts so the doctor closure no longer imports the sync
  plugin-state store through credentials-read
- guard: doctor-contract-closure-guard.test.ts now forbids static value
  imports of runtime-doctor in closures alongside agent-runtime

* fix(matrix): keep credential revocation record type module-local

Knip production scan flags the export as consumer-less; the type is only
referenced by the exported union and revocation guard signature.
2026-08-08 17:51:31 -07:00

121 lines
4.4 KiB
TypeScript

// Canvas doctor contract migrates documents from configured host roots into core storage.
import fs from "node:fs/promises";
import path from "node:path";
import { resolvePluginConfigObject } from "openclaw/plugin-sdk/plugin-config-runtime";
import type { PluginDoctorStateMigration } from "openclaw/plugin-sdk/runtime-doctor-migrations";
import { pathExists } from "openclaw/plugin-sdk/security-runtime";
import {
asOptionalRecord as readRecord,
readStringValue as readString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { resolveUserPath } from "openclaw/plugin-sdk/text-utility-runtime";
type StateMigrationParams = Parameters<PluginDoctorStateMigration["detectLegacyState"]>[0];
function resolveLegacyDocumentsDir(params: StateMigrationParams): string | null {
const pluginConfig = resolvePluginConfigObject(params.config, "canvas");
const configuredRoot = readString(readRecord(pluginConfig?.host)?.root)?.trim();
if (!configuredRoot) {
return null;
}
const legacyDir = path.join(
path.resolve(resolveUserPath(configuredRoot, params.env)),
"documents",
);
const coreDir = path.resolve(params.stateDir, "canvas", "documents");
return legacyDir === coreDir ? null : legacyDir;
}
async function listDocumentIds(documentsDir: string): Promise<string[]> {
try {
return (await fs.readdir(documentsDir, { withFileTypes: true }))
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.toSorted();
} catch {
return [];
}
}
export const stateMigrations: PluginDoctorStateMigration[] = [
{
id: "canvas-custom-root-documents-to-core",
label: "Canvas documents in a custom host root",
async detectLegacyState(params) {
const legacyDir = resolveLegacyDocumentsDir(params);
if (!legacyDir) {
return null;
}
const documentIds = await listDocumentIds(legacyDir);
if (documentIds.length === 0) {
return null;
}
const coreDir = path.resolve(params.stateDir, "canvas", "documents");
return {
preview: [
`- Canvas documents: ${legacyDir} -> ${coreDir} (${documentIds.length} document(s))`,
],
};
},
async migrateLegacyState(params) {
const changes: string[] = [];
const warnings: string[] = [];
const legacyDir = resolveLegacyDocumentsDir(params);
if (!legacyDir) {
return { changes, warnings };
}
const documentIds = await listDocumentIds(legacyDir);
if (documentIds.length === 0) {
return { changes, warnings };
}
const coreDir = path.resolve(params.stateDir, "canvas", "documents");
await fs.mkdir(coreDir, { recursive: true });
let migrated = 0;
for (const documentId of documentIds) {
const sourceDir = path.join(legacyDir, documentId);
const targetDir = path.join(coreDir, documentId);
let tempParent: string | undefined;
try {
if (await pathExists(targetDir)) {
throw new Error("core target already exists");
}
tempParent = await fs.mkdtemp(path.join(coreDir, ".canvas-migrate-"));
const tempDocumentDir = path.join(tempParent, documentId);
await fs.cp(sourceDir, tempDocumentDir, {
recursive: true,
errorOnExist: true,
force: false,
});
if (await pathExists(targetDir)) {
throw new Error("core target was created during migration");
}
// Publish only a complete same-filesystem copy; interrupted copies stay invisible.
await fs.rename(tempDocumentDir, targetDir);
await fs.rm(sourceDir, { recursive: true, force: true });
migrated += 1;
} catch (error) {
warnings.push(
`Skipped Canvas document ${documentId}; core target may already exist: ${String(error)}`,
);
} finally {
if (tempParent) {
await fs.rm(tempParent, { recursive: true, force: true }).catch(() => undefined);
}
}
}
if (migrated > 0) {
changes.push(`Migrated ${migrated} Canvas document(s) into core storage`);
}
try {
if ((await fs.readdir(legacyDir)).length === 0) {
await fs.rmdir(legacyDir);
}
} catch {
// A retained or concurrently created document keeps the legacy directory in place.
}
return { changes, warnings };
},
},
];