mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-06 03:51:43 +00:00
* feat(sqlite): migrate persisted media to canonical facts and stop legacy writes PR 3 of the media legacy retirement program — the operator-approved canonical cutover. - openclaw doctor --fix owns one idempotent migration: active transcript_events rows canonicalize to __openclaw.media (facts-first gap-fill, bare legacy kinds to fact.kind, transcribed indexes and workspace dirs onto per-fact fields) via the transcript replacement owner; cold plain/.zst archives rewrite through temp-file + codec readback + event/id verification + atomic replace; trajectory runtime snapshots canonicalize IN PLACE (telemetry preserved, never row deletion). Invalid JSON, genuinely ambiguous legacy-only sparse alignment, or a changed source aborts that owner without partial work; reruns are no-ops. - Per-agent schema advances to v16 as a pure downgrade guard (main independently took v15 for board/session-sharing tables; no columns/tables/indexes change here, shared-state DB untouched). v15 databases repair canonical indexes before the version assertion so repairable installations never strand. - The user-turn builder stops writing top-level legacy Media* fields; shouldPersistStructuredMediaEntries and the aligned projection mode are deleted; the generic transcript append boundary canonicalizes every message role so SDK/mirror writers cannot mint new legacy rows. - Internal persisted-reader legacy fallbacks are removed; the public SDK projection stays until retirement PR 4's window expires. Hardening from three adversarial review rounds, each with fixture regressions: in-place trajectory canonicalization instead of row deletion; repair-before-assert on the v15 path; all-roles append canonicalization; duplicate-preserving exact row rewrites; v0-v15 reopen guards; complete canonical facts bypass compact legacy projections (PR-1 dual-write rows migrate cleanly); SQLite LIKE underscore escaped so populated foreign databases are never claimed. * fix(sqlite): align schema-support metadata and gates with the v16 cutover package.json agent schema support advances to 16; verifier and board parity fixtures run doctor migration before steady-state access (the production guards were correct); two test-only exports removed; the migration module registered in the doctor raw-SQLite allowlist.
65 lines
2.5 KiB
TypeScript
65 lines
2.5 KiB
TypeScript
import path from "node:path";
|
|
import { mimeTypeFromFilePath } from "@openclaw/media-core/mime";
|
|
import type { MediaFactInput } from "../media/media-facts.js";
|
|
import type { PersistedUserTurnMediaInput } from "./user-turn-transcript.types.js";
|
|
|
|
const URL_LIKE_MEDIA_PATH_PATTERN = /^[a-z][a-z0-9+.-]*:/i;
|
|
const STRUCTURED_MEDIA_KINDS = new Set<NonNullable<MediaFactInput["kind"]>>([
|
|
"image",
|
|
"audio",
|
|
"video",
|
|
"document",
|
|
"sticker",
|
|
"unknown",
|
|
]);
|
|
const MIME_TYPE_PATTERN = /^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/iu;
|
|
|
|
function normalizeOptionalText(value: string | null | undefined): string | undefined {
|
|
const normalized = value?.trim();
|
|
return normalized ? normalized : undefined;
|
|
}
|
|
|
|
function normalizeStructuredMediaKind(value: string | null | undefined): MediaFactInput["kind"] {
|
|
const kind = normalizeOptionalText(value);
|
|
return kind && STRUCTURED_MEDIA_KINDS.has(kind as NonNullable<MediaFactInput["kind"]>)
|
|
? (kind as NonNullable<MediaFactInput["kind"]>)
|
|
: undefined;
|
|
}
|
|
|
|
export function resolveTranscriptMediaPath(
|
|
pathValue: string,
|
|
workspaceDir: string | undefined,
|
|
): string {
|
|
// Relative staged media paths are anchored to the media workspace; absolute
|
|
// paths and URL-like refs are already stable transcript references.
|
|
if (!workspaceDir || path.isAbsolute(pathValue) || URL_LIKE_MEDIA_PATH_PATTERN.test(pathValue)) {
|
|
return pathValue;
|
|
}
|
|
return path.join(workspaceDir, pathValue);
|
|
}
|
|
|
|
export function normalizeStructuredMediaEntryForTranscript(
|
|
media: PersistedUserTurnMediaInput,
|
|
): MediaFactInput {
|
|
const mediaPath = normalizeOptionalText(media.path);
|
|
const mediaUrl = normalizeOptionalText(media.url);
|
|
const kind = normalizeStructuredMediaKind(media.kind);
|
|
const legacyKind = normalizeOptionalText(media.kind);
|
|
const messageId = normalizeOptionalText(media.messageId);
|
|
const workspaceDir = normalizeOptionalText(media.workspaceDir);
|
|
const contentType =
|
|
normalizeOptionalText(media.contentType) ??
|
|
(kind || !legacyKind || !MIME_TYPE_PATTERN.test(legacyKind) ? undefined : legacyKind) ??
|
|
mimeTypeFromFilePath(mediaPath ?? mediaUrl);
|
|
return {
|
|
...(mediaPath ? { path: mediaPath } : {}),
|
|
...(mediaUrl ? { url: mediaUrl } : {}),
|
|
...(contentType ? { contentType } : {}),
|
|
...(kind ? { kind } : {}),
|
|
...(media.transcribed === true ? { transcribed: true } : {}),
|
|
...(messageId ? { messageId } : {}),
|
|
...(workspaceDir ? { workspaceDir } : {}),
|
|
...(media.hydrationSuppressed === true ? { hydrationSuppressed: true } : {}),
|
|
};
|
|
}
|