feat(POSTV1-ASSET-ALL-16): 完整还原归档文字模板素材
Dada P0-A isolated Windows CI / validate-and-package (push) Waiting to run

This commit is contained in:
suyx
2026-08-06 16:20:24 +08:00
parent ef6950c5df
commit a70b9fc241
18 changed files with 28556 additions and 1147 deletions
+76 -14
View File
@@ -8,6 +8,7 @@ import {
readFileSync,
readdirSync,
realpathSync,
renameSync,
rmSync,
statSync,
writeFileSync,
@@ -15,6 +16,8 @@ import {
import { tmpdir } from "node:os";
import { basename, dirname, extname, isAbsolute, join, resolve, sep } from "node:path";
import { compileTextTemplateAssets } from "./text-template-assets.mjs";
export const P0A_RUNTIME_ASSET_ROOT_REF = "p0a_runtime_assets";
export const RUNTIME_ASSET_MANIFEST_SCHEMA = "DadaRuntimeAssets/v1";
@@ -64,6 +67,8 @@ function derivedCounts(entries) {
dynamic_images: entries.filter((entry) => entry.resourceVersion === "p0a-complex-v1" && /^DYN\d{3}-/.test(entry.assetId)).length,
font_panel_items: entries.filter((entry) => entry.resourceVersion === "p0a-complex-v1" && /^FONT\d{3}$/.test(entry.assetId)).length,
static_stickers: entries.filter((entry) => entry.resourceVersion === "p0a-static-v1" && /^STK\d{3,4}$/.test(entry.assetId)).length,
text_fonts: entries.filter((entry) => entry.resourceVersion === "p0a-complex-v1" && /^TEXT-FONT-/.test(entry.assetId)).length,
text_images: entries.filter((entry) => entry.resourceVersion === "p0a-complex-v1" && /^TEXT-IMAGE-/.test(entry.assetId)).length,
text_previews: entries.filter((entry) => entry.resourceVersion === "p0a-complex-v1" && /^TEXT-PREVIEW-/.test(entry.assetId)).length,
};
}
@@ -131,7 +136,42 @@ function sameFile(left, right) {
return leftStat.dev === rightStat.dev && leftStat.ino === rightStat.ino;
}
export function deployRuntimeAssetPlan({ assetRoot, manifest, resources }) {
function installRuntimeAsset(resource, targetPath, expectedSha) {
const generatedBytes = Buffer.isBuffer(resource.sourceBytes) ? resource.sourceBytes : undefined;
if (generatedBytes) writeFileSync(targetPath, generatedBytes, { flag: "wx" });
else {
try {
linkSync(resource.sourcePath, targetPath);
} catch (error) {
if (error && typeof error === "object" && "code" in error && error.code === "EXDEV") {
throw new Error("asset_hardlink_volume_mismatch");
}
throw error;
}
}
if (fileSha256(targetPath) !== expectedSha) throw new Error(generatedBytes ? "asset_generated_write_invalid" : "asset_hardlink_verification_failed");
if (!generatedBytes && !sameFile(resource.sourcePath, targetPath)) throw new Error("asset_hardlink_verification_failed");
}
function replaceManagedRuntimeAsset(resource, targetPath, expectedSha) {
const nextPath = `${targetPath}.dada-next`;
const previousPath = `${targetPath}.dada-previous`;
if (existsSync(nextPath) || existsSync(previousPath)) throw new Error("asset_update_staging_conflict");
installRuntimeAsset(resource, nextPath, expectedSha);
renameSync(targetPath, previousPath);
try {
renameSync(nextPath, targetPath);
if (fileSha256(targetPath) !== expectedSha) throw new Error("asset_update_verification_failed");
rmSync(previousPath, { force: true });
} catch (error) {
if (existsSync(targetPath)) renameSync(targetPath, nextPath);
renameSync(previousPath, targetPath);
rmSync(nextPath, { force: true });
throw error;
}
}
export function deployRuntimeAssetPlan({ allowManagedUpdate = false, assetRoot, manifest, resources }) {
if (!isAbsolute(assetRoot)) throw new Error("asset_root_must_be_absolute");
const normalizedManifest = createRuntimeAssetManifest({
counts: manifest.counts,
@@ -141,32 +181,35 @@ export function deployRuntimeAssetPlan({ assetRoot, manifest, resources }) {
const entries = new Map(normalizedManifest.entries.map((entry) => [`${entry.resourceVersion}\u0000${entry.assetId}`, entry]));
if (resources.length !== entries.size) throw new Error("asset_resource_plan_incomplete");
mkdirSync(assetRoot, { recursive: true });
const previousManifestPath = join(assetRoot, "manifest.json");
const previousEntries = allowManagedUpdate && existsSync(previousManifestPath)
? new Map(readRuntimeAssetManifest(previousManifestPath).entries.map((entry) => [entry.relativePath, entry]))
: new Map();
for (const resource of resources) {
const key = `${resource.entry.resourceVersion}\u0000${resource.entry.assetId}`;
const entry = entries.get(key);
if (!entry || JSON.stringify(entry) !== JSON.stringify({ ...resource.entry, sha256: resource.entry.sha256.toLowerCase() })) {
throw new Error("asset_resource_plan_mismatch");
}
if (!existsSync(resource.sourcePath) || !statSync(resource.sourcePath).isFile() || lstatSync(resource.sourcePath).isSymbolicLink()) {
const generatedBytes = Buffer.isBuffer(resource.sourceBytes) ? resource.sourceBytes : undefined;
if (!generatedBytes && (!existsSync(resource.sourcePath) || !statSync(resource.sourcePath).isFile() || lstatSync(resource.sourcePath).isSymbolicLink())) {
throw new Error("asset_source_invalid");
}
if (fileSha256(resource.sourcePath) !== entry.sha256) throw new Error("asset_source_hash_invalid");
if ((generatedBytes ? sha256(generatedBytes) : fileSha256(resource.sourcePath)) !== entry.sha256) throw new Error("asset_source_hash_invalid");
const targetPath = targetWithinRoot(assetRoot, entry.relativePath);
mkdirSync(dirname(targetPath), { recursive: true });
if (existsSync(targetPath)) {
if (fileSha256(targetPath) !== entry.sha256) throw new Error("asset_target_conflict");
if (!sameFile(resource.sourcePath, targetPath)) throw new Error("asset_target_not_hardlink");
const targetSha = fileSha256(targetPath);
if (targetSha !== entry.sha256) {
const previous = previousEntries.get(entry.relativePath);
if (!previous || targetSha !== previous.sha256) throw new Error("asset_target_conflict");
replaceManagedRuntimeAsset(resource, targetPath, entry.sha256);
continue;
}
if (!generatedBytes && !sameFile(resource.sourcePath, targetPath)) throw new Error("asset_target_not_hardlink");
continue;
}
try {
linkSync(resource.sourcePath, targetPath);
} catch (error) {
if (error && typeof error === "object" && "code" in error && error.code === "EXDEV") {
throw new Error("asset_hardlink_volume_mismatch");
}
throw error;
}
if (!sameFile(resource.sourcePath, targetPath)) throw new Error("asset_hardlink_verification_failed");
installRuntimeAsset(resource, targetPath, entry.sha256);
}
writeRuntimeAssetManifest(join(assetRoot, "manifest.json"), normalizedManifest);
return { linked_files: resources.length, manifest: normalizedManifest, status: "ready" };
@@ -197,6 +240,17 @@ function entryFor(sourcePath, assetId, resourceVersion, relativePath, mimeType)
};
}
function entryForBytes(sourceBytes, assetId, resourceVersion, relativePath, mimeType) {
return {
assetId,
mimeType,
relativePath,
resourceVersion,
rootRef: P0A_RUNTIME_ASSET_ROOT_REF,
sha256: sha256(sourceBytes),
};
}
function safeRuntimeComponent(value) {
const normalized = value.normalize("NFKD").replaceAll(/[^A-Za-z0-9_-]+/g, "-").replaceAll(/^-+|-+$/g, "");
if (normalized === value) return normalized;
@@ -325,6 +379,14 @@ export async function buildP0aRuntimeAssetPlan({ replicationRoot }) {
for (const templateId of registry.P0A_TEXT_TEMPLATE_IDS) {
const templateDirectory = textTemplateDirectory(textRoot, templateId);
const metadata = JSON.parse(readFileSync(join(templateDirectory, "metadata.json"), "utf8"));
const compiled = compileTextTemplateAssets({ templateDirectory, templateId });
for (const resource of compiled.resources) {
const relativePath = `${registry.P0A_COMPLEX_RELEASE_VERSION}/${resource.assetId}${resource.extension}`;
const entry = resource.sourceBytes
? entryForBytes(resource.sourceBytes, resource.assetId, registry.P0A_COMPLEX_RELEASE_VERSION, relativePath, resource.mimeType)
: entryFor(resource.sourcePath, resource.assetId, registry.P0A_COMPLEX_RELEASE_VERSION, relativePath, resource.mimeType);
addResource({ entry, ...(resource.sourceBytes ? { sourceBytes: resource.sourceBytes } : { sourcePath: resource.sourcePath }) });
}
const previewReference = metadata.files?.preview;
if (typeof previewReference !== "string" || previewReference.length === 0) continue;
const sourcePath = join(templateDirectory, ...previewReference.split("/"));