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 11:39:46 +08:00
parent fd4cd277cc
commit 9de0d2a63c
9 changed files with 8644 additions and 46 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1
View File
@@ -24,6 +24,7 @@
"package:portable": "node scripts/build-portable.mjs", "package:portable": "node scripts/build-portable.mjs",
"assets:manifest": "pnpm build:workspace-packages && node scripts/generate-runtime-asset-manifest.mjs", "assets:manifest": "pnpm build:workspace-packages && node scripts/generate-runtime-asset-manifest.mjs",
"assets:deploy": "pnpm build:workspace-packages && node scripts/deploy-runtime-assets.mjs", "assets:deploy": "pnpm build:workspace-packages && node scripts/deploy-runtime-assets.mjs",
"assets:browser-catalog": "pnpm build:workspace-packages && node scripts/generate-complex-browser-catalog.mjs",
"generate:openapi": "node scripts/generate-openapi.mjs", "generate:openapi": "node scripts/generate-openapi.mjs",
"check:openapi": "node scripts/check-openapi.mjs", "check:openapi": "node scripts/check-openapi.mjs",
"validate:tdd-trace": "node scripts/validate-tdd-trace.mjs", "validate:tdd-trace": "node scripts/validate-tdd-trace.mjs",
@@ -0,0 +1,131 @@
import { createHash } from "node:crypto";
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
import {
P0A_DYNAMIC_STICKER_IDS,
P0A_REQUIRED_FONT_PANEL_IDS,
P0A_TEXT_TEMPLATE_IDS,
} from "../packages/template-registry/dist/index.js";
function option(name) {
const index = process.argv.indexOf(name);
return index >= 0 ? process.argv[index + 1] : undefined;
}
function readJson(path) {
return JSON.parse(readFileSync(path, "utf8"));
}
function oneDirectoryWithPrefix(root, prefix) {
const matches = readdirSync(root, { withFileTypes: true })
.filter((entry) => entry.isDirectory() && entry.name.startsWith(`${prefix}_`));
if (matches.length !== 1) throw new Error(`complex_catalog_directory_invalid:${prefix}`);
return join(root, matches[0].name);
}
function sha256(path) {
return createHash("sha256").update(readFileSync(path)).digest("hex").toUpperCase();
}
function textTemplateDirectory(root, templateId) {
const family = templateId.startsWith("FLOWER") ? "花字"
: templateId.startsWith("SIMPLE") ? "简约"
: templateId.startsWith("TAG") ? "标签"
: "标题";
return join(root, family, "templates", templateId);
}
function normalizedTextCategory(value) {
if (value === "花字") return "flower";
if (value === "简约") return "simple";
if (value === "标签") return "tag";
return "title";
}
function normalizedDynamicCategory(value) {
if (value === "user") return "identity";
if (value === "location" || value === "time") return value;
return "other";
}
const replicationRoot = resolve(option("--replication-root") ?? join(homedir(), "Desktop", "sticker_web_replication_assets"));
if (!isAbsolute(replicationRoot) || !existsSync(replicationRoot)) throw new Error("replication_asset_root_unavailable");
const outputPath = resolve(option("--output") ?? "apps/web/src/generated/complex-assets.json");
const fontPackagesRoot = join(replicationRoot, "sticker_text", "字体", "面板全量采集", "font_panel_full_20260722", "resources", "font_packages");
const textRoot = join(replicationRoot, "sticker_text", "模板", "单模板归档");
const dynamicRoot = join(replicationRoot, "sticker_interactive", "单模板归档", "templates");
const fontHashes = new Map();
const fontPanelItems = P0A_REQUIRED_FONT_PANEL_IDS.map((fontId, displayOrder) => {
const metadata = readJson(join(oneDirectoryWithPrefix(fontPackagesRoot, fontId), "metadata.json"));
if (typeof metadata.local_sha256 !== "string") throw new Error(`complex_catalog_font_hash_missing:${fontId}`);
fontHashes.set(metadata.local_sha256.toUpperCase(), fontId);
return {
display_name: String(metadata.display_name ?? fontId),
display_order: displayOrder,
font_id: fontId,
};
});
const textTemplates = P0A_TEXT_TEMPLATE_IDS.map((templateId, catalogOrder) => {
const directory = textTemplateDirectory(textRoot, templateId);
const metadata = readJson(join(directory, "metadata.json"));
const fontReferences = Array.isArray(metadata.files?.fonts) ? metadata.files.fonts : [];
const matchedFontIds = [...new Set(fontReferences
.map((reference) => fontHashes.get(sha256(join(directory, ...reference.split("/")))))
.filter(Boolean))];
const previewReference = typeof metadata.files?.preview === "string" ? metadata.files.preview : undefined;
const hasPreview = previewReference ? existsSync(join(directory, ...previewReference.split("/"))) : false;
return {
available: true,
catalog_order: catalogOrder,
category: normalizedTextCategory(metadata.category),
default_font_id: matchedFontIds[0] ?? "FONT081",
default_font_size: 48,
default_text: String(metadata.default_text ?? metadata.display_name ?? templateId),
display_name: String(metadata.display_name || metadata.default_text || templateId),
font_match_status: matchedFontIds.length > 0 ? "exact_panel_hash" : "catalog_fallback",
...(hasPreview ? { preview_asset_id: `TEXT-PREVIEW-${templateId}` } : {}),
resource_class: metadata.resource_class === "parameter_only" ? "parameter_only" : "zip_template",
template_id: templateId,
};
});
const dynamicStickers = P0A_DYNAMIC_STICKER_IDS.map((templateId, catalogOrder) => {
const metadata = readJson(join(dynamicRoot, templateId, "metadata.json"));
const requiredFields = Array.isArray(metadata.dynamic_keys) ? metadata.dynamic_keys.map(String) : [];
const fontIds = Array.isArray(metadata.files?.fonts)
? metadata.files.fonts.map((reference) => basename(reference))
: [];
return {
catalog_order: catalogOrder,
category: normalizedDynamicCategory(metadata.category),
display_name: String(metadata.display_name ?? templateId),
font_ids: fontIds.length > 0 ? fontIds : ["FONT081"],
required_fields: requiredFields,
requires_location_consent: requiredFields.includes("latitude") || requiredFields.includes("longitude"),
source_candidate_id: String(metadata.source_candidate_id ?? metadata.display_name ?? templateId),
template_id: templateId,
};
});
const catalog = {
dynamic_stickers: dynamicStickers,
font_panel_items: fontPanelItems,
schema_version: "DadaComplexBrowserCatalog/v1",
text_templates: textTemplates,
};
const serialized = `${JSON.stringify(catalog, null, 2)}\n`;
if (/[A-Z]:[\\/]/i.test(serialized)) throw new Error("complex_catalog_absolute_path_detected");
mkdirSync(dirname(outputPath), { recursive: true });
writeFileSync(outputPath, serialized);
process.stdout.write(`${JSON.stringify({
dynamic_stickers: dynamicStickers.length,
font_panel_items: fontPanelItems.length,
output: outputPath,
text_previews: textTemplates.filter((item) => item.preview_asset_id).length,
text_templates: textTemplates.length,
})}\n`);
+71 -22
View File
@@ -64,6 +64,7 @@ function derivedCounts(entries) {
dynamic_images: entries.filter((entry) => entry.resourceVersion === "p0a-complex-v1" && /^DYN\d{3}-/.test(entry.assetId)).length, 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, 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, static_stickers: entries.filter((entry) => entry.resourceVersion === "p0a-static-v1" && /^STK\d{3,4}$/.test(entry.assetId)).length,
text_previews: entries.filter((entry) => entry.resourceVersion === "p0a-complex-v1" && /^TEXT-PREVIEW-/.test(entry.assetId)).length,
}; };
} }
@@ -196,6 +197,12 @@ function entryFor(sourcePath, assetId, resourceVersion, relativePath, mimeType)
}; };
} }
function safeRuntimeComponent(value) {
const normalized = value.normalize("NFKD").replaceAll(/[^A-Za-z0-9_-]+/g, "-").replaceAll(/^-+|-+$/g, "");
if (normalized === value) return normalized;
return `${normalized || "asset"}-${sha256(value).slice(0, 8).toLowerCase()}`;
}
function dynamicMetadata(templateRoot, descriptor, field) { function dynamicMetadata(templateRoot, descriptor, field) {
const templateDirectory = join(templateRoot, descriptor.templateId); const templateDirectory = join(templateRoot, descriptor.templateId);
const metadata = JSON.parse(readFileSync(join(templateDirectory, "metadata.json"), "utf8")); const metadata = JSON.parse(readFileSync(join(templateDirectory, "metadata.json"), "utf8"));
@@ -205,6 +212,14 @@ function dynamicMetadata(templateRoot, descriptor, field) {
return templateDirectory; return templateDirectory;
} }
function textTemplateDirectory(root, templateId) {
const family = templateId.startsWith("FLOWER") ? "花字"
: templateId.startsWith("SIMPLE") ? "简约"
: templateId.startsWith("TAG") ? "标签"
: "标题";
return join(root, family, "templates", templateId);
}
export async function buildP0aRuntimeAssetPlan({ replicationRoot }) { export async function buildP0aRuntimeAssetPlan({ replicationRoot }) {
const [{ compileStaticStickerCatalog }, registry] = await Promise.all([ const [{ compileStaticStickerCatalog }, registry] = await Promise.all([
import("../../packages/asset-compiler/dist/index.js"), import("../../packages/asset-compiler/dist/index.js"),
@@ -230,6 +245,19 @@ export async function buildP0aRuntimeAssetPlan({ replicationRoot }) {
if (entry.sha256 !== item.sha256.toLowerCase()) throw new Error(`static_sticker_hash_invalid:${item.stable_id}`); if (entry.sha256 !== item.sha256.toLowerCase()) throw new Error(`static_sticker_hash_invalid:${item.stable_id}`);
return { entry, sourcePath }; return { entry, sourcePath };
}); });
const resourceKeys = new Map(resources.map((resource) => [`${resource.entry.resourceVersion}\u0000${resource.entry.assetId}`, resource]));
const addResource = (resource) => {
const key = `${resource.entry.resourceVersion}\u0000${resource.entry.assetId}`;
const existing = resourceKeys.get(key);
if (existing) {
if (existing.entry.sha256 !== resource.entry.sha256 || existing.entry.mimeType !== resource.entry.mimeType) {
throw new Error(`runtime_asset_duplicate_conflict:${resource.entry.assetId}`);
}
return;
}
resourceKeys.set(key, resource);
resources.push(resource);
};
const fontPackagesRoot = join( const fontPackagesRoot = join(
replicationRoot, replicationRoot,
@@ -244,7 +272,7 @@ export async function buildP0aRuntimeAssetPlan({ replicationRoot }) {
const packageDirectory = oneDirectoryWithPrefix(fontPackagesRoot, assetId); const packageDirectory = oneDirectoryWithPrefix(fontPackagesRoot, assetId);
const sourcePath = oneSupportedFont(join(packageDirectory, "font_files")); const sourcePath = oneSupportedFont(join(packageDirectory, "font_files"));
const extension = extname(sourcePath).toLowerCase(); const extension = extname(sourcePath).toLowerCase();
resources.push({ addResource({
entry: entryFor( entry: entryFor(
sourcePath, sourcePath,
assetId, assetId,
@@ -257,33 +285,59 @@ export async function buildP0aRuntimeAssetPlan({ replicationRoot }) {
} }
const templateRoot = join(replicationRoot, "sticker_interactive", "单模板归档", "templates"); const templateRoot = join(replicationRoot, "sticker_interactive", "单模板归档", "templates");
for (const descriptor of registry.P0A_DYNAMIC_RUNTIME_FONT_SOURCES) { for (const templateId of registry.P0A_DYNAMIC_STICKER_IDS) {
const templateDirectory = dynamicMetadata(templateRoot, descriptor, "fonts"); const templateDirectory = join(templateRoot, templateId);
const sourcePath = oneSupportedFont(join(templateDirectory, ...descriptor.sourceReference.split("/"))); const metadata = JSON.parse(readFileSync(join(templateDirectory, "metadata.json"), "utf8"));
for (const sourceReference of metadata.files?.fonts ?? []) {
const sourcePath = oneSupportedFont(join(templateDirectory, ...sourceReference.split("/")));
const extension = extname(sourcePath).toLowerCase(); const extension = extname(sourcePath).toLowerCase();
resources.push({ const assetId = basename(sourceReference);
addResource({
entry: entryFor( entry: entryFor(
sourcePath, sourcePath,
descriptor.assetId, assetId,
registry.P0A_COMPLEX_RELEASE_VERSION, registry.P0A_COMPLEX_RELEASE_VERSION,
`${registry.P0A_COMPLEX_RELEASE_VERSION}/${descriptor.assetId}${extension}`, `${registry.P0A_COMPLEX_RELEASE_VERSION}/${assetId}${extension}`,
fontMimeTypes.get(extension), fontMimeTypes.get(extension),
), ),
sourcePath, sourcePath,
}); });
} }
for (const descriptor of registry.P0A_DYNAMIC_RUNTIME_IMAGE_SOURCES) { for (const sourceReference of metadata.files?.images ?? []) {
const templateDirectory = dynamicMetadata(templateRoot, descriptor, "images"); const sourcePath = join(templateDirectory, ...sourceReference.split("/"));
const sourcePath = join(templateDirectory, ...descriptor.sourceReference.split("/")); const extension = extname(sourcePath).toLowerCase();
if (!existsSync(sourcePath) || extname(sourcePath).toLowerCase() !== ".png") { const assetId = `${templateId}-${safeRuntimeComponent(basename(sourceReference, extension))}`;
throw new Error(`runtime_dynamic_image_invalid:${descriptor.assetId}`); if (!existsSync(sourcePath) || extension !== ".png") throw new Error(`runtime_dynamic_image_invalid:${assetId}`);
} addResource({
resources.push({
entry: entryFor( entry: entryFor(
sourcePath, sourcePath,
descriptor.assetId, assetId,
registry.P0A_COMPLEX_RELEASE_VERSION, registry.P0A_COMPLEX_RELEASE_VERSION,
`${registry.P0A_COMPLEX_RELEASE_VERSION}/${descriptor.assetId}.png`, `${registry.P0A_COMPLEX_RELEASE_VERSION}/${assetId}.png`,
"image/png",
),
sourcePath,
});
}
}
const textRoot = join(replicationRoot, "sticker_text", "模板", "单模板归档");
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 previewReference = metadata.files?.preview;
if (typeof previewReference !== "string" || previewReference.length === 0) continue;
const sourcePath = join(templateDirectory, ...previewReference.split("/"));
if (!existsSync(sourcePath) || extname(sourcePath).toLowerCase() !== ".png") {
throw new Error(`runtime_text_preview_invalid:${templateId}`);
}
const assetId = `TEXT-PREVIEW-${templateId}`;
addResource({
entry: entryFor(
sourcePath,
assetId,
registry.P0A_COMPLEX_RELEASE_VERSION,
`${registry.P0A_COMPLEX_RELEASE_VERSION}/${assetId}.png`,
"image/png", "image/png",
), ),
sourcePath, sourcePath,
@@ -292,12 +346,7 @@ export async function buildP0aRuntimeAssetPlan({ replicationRoot }) {
const manifestPath = join(replicationRoot, "sticker_web_handoff", "sticker_web_catalog_manifest.json"); const manifestPath = join(replicationRoot, "sticker_web_handoff", "sticker_web_catalog_manifest.json");
const manifest = createRuntimeAssetManifest({ const manifest = createRuntimeAssetManifest({
counts: { counts: derivedCounts(resources.map((resource) => resource.entry)),
dynamic_fonts: registry.P0A_DYNAMIC_RUNTIME_FONT_SOURCES.length,
dynamic_images: registry.P0A_DYNAMIC_RUNTIME_IMAGE_SOURCES.length,
font_panel_items: registry.P0A_REQUIRED_FONT_PANEL_IDS.length,
static_stickers: staticResult.catalog.count,
},
entries: resources.map((resource) => resource.entry), entries: resources.map((resource) => resource.entry),
sourceManifestSha256: fileSha256(manifestPath), sourceManifestSha256: fileSha256(manifestPath),
}); });
+1 -1
View File
@@ -32,7 +32,7 @@ function fixture() {
sha256: createHash("sha256").update(bytes).digest("hex"), sha256: createHash("sha256").update(bytes).digest("hex"),
}; };
const manifest = { const manifest = {
counts: { dynamic_fonts: 0, dynamic_images: 0, font_panel_items: 0, static_stickers: 1 }, counts: { dynamic_fonts: 0, dynamic_images: 0, font_panel_items: 0, static_stickers: 1, text_previews: 0 },
entries: [entry], entries: [entry],
root_ref: "p0a_runtime_assets", root_ref: "p0a_runtime_assets",
schema_version: "DadaRuntimeAssets/v1", schema_version: "DadaRuntimeAssets/v1",
+7 -6
View File
@@ -15,12 +15,13 @@ import {
test("committed P0-A runtime manifest covers the frozen first-version binary assets", () => { test("committed P0-A runtime manifest covers the frozen first-version binary assets", () => {
const manifest = readRuntimeAssetManifest("config/runtime-assets-manifest.json"); const manifest = readRuntimeAssetManifest("config/runtime-assets-manifest.json");
assert.deepEqual(manifest.counts, { assert.deepEqual(manifest.counts, {
dynamic_fonts: 7, dynamic_fonts: 18,
dynamic_images: 8, dynamic_images: 43,
font_panel_items: 11, font_panel_items: 86,
static_stickers: 1407, static_stickers: 1407,
text_previews: 261,
}); });
assert.equal(manifest.entries.length, 1433); assert.equal(manifest.entries.length, 1815);
assert.doesNotMatch(serializeRuntimeAssetManifest(manifest), /[A-Za-z]:[\\/]/); assert.doesNotMatch(serializeRuntimeAssetManifest(manifest), /[A-Za-z]:[\\/]/);
}); });
@@ -42,7 +43,7 @@ test("runtime asset deployment creates verified hardlinks and a path-free manife
sha256: createHash("sha256").update(bytes).digest("hex"), sha256: createHash("sha256").update(bytes).digest("hex"),
}; };
const manifest = createRuntimeAssetManifest({ const manifest = createRuntimeAssetManifest({
counts: { dynamic_fonts: 0, dynamic_images: 0, font_panel_items: 0, static_stickers: 1 }, counts: { dynamic_fonts: 0, dynamic_images: 0, font_panel_items: 0, static_stickers: 1, text_previews: 0 },
entries: [entry], entries: [entry],
}); });
@@ -75,7 +76,7 @@ test("runtime asset deployment refuses a mismatched existing target", async (t)
sha256: createHash("sha256").update("expected").digest("hex"), sha256: createHash("sha256").update("expected").digest("hex"),
}; };
const manifest = createRuntimeAssetManifest({ const manifest = createRuntimeAssetManifest({
counts: { dynamic_fonts: 0, dynamic_images: 0, font_panel_items: 0, static_stickers: 1 }, counts: { dynamic_fonts: 0, dynamic_images: 0, font_panel_items: 0, static_stickers: 1, text_previews: 0 },
entries: [entry], entries: [entry],
}); });
+5 -4
View File
@@ -114,12 +114,13 @@ test("portable package serves the product and keeps SQLite data across API resta
assert.ok(existsSync(join(packageRoot, "web", "index.html"))); assert.ok(existsSync(join(packageRoot, "web", "index.html")));
const runtimeAssetManifest = JSON.parse(await readFile(join(packageRoot, "asset-metadata", "manifest.json"), "utf8")); const runtimeAssetManifest = JSON.parse(await readFile(join(packageRoot, "asset-metadata", "manifest.json"), "utf8"));
assert.deepEqual(runtimeAssetManifest.counts, { assert.deepEqual(runtimeAssetManifest.counts, {
dynamic_fonts: 7, dynamic_fonts: 18,
dynamic_images: 8, dynamic_images: 43,
font_panel_items: 11, font_panel_items: 86,
static_stickers: 1407, static_stickers: 1407,
text_previews: 261,
}); });
assert.equal(runtimeAssetManifest.entries.length, 1433); assert.equal(runtimeAssetManifest.entries.length, 1815);
const packagedWorker = await readFile(join(packageRoot, "server", "worker", "dist", "worker.js"), "utf8"); const packagedWorker = await readFile(join(packageRoot, "server", "worker", "dist", "worker.js"), "utf8");
const packagedOneApiAdapter = await readFile(join(packageRoot, "server", "worker", "dist", "oneapi-generation-adapter.js"), "utf8"); const packagedOneApiAdapter = await readFile(join(packageRoot, "server", "worker", "dist", "oneapi-generation-adapter.js"), "utf8");
assert.match(packagedWorker, /GenerationProcessor/); assert.match(packagedWorker, /GenerationProcessor/);
+11
View File
@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import complexAssetCatalog from "../../apps/web/src/generated/complex-assets.json";
import { import {
P0A_COLOR_CARD_IDS, P0A_COLOR_CARD_IDS,
P0A_DYNAMIC_STICKER_IDS, P0A_DYNAMIC_STICKER_IDS,
@@ -25,4 +26,14 @@ describe("POSTV1-ASSET-ALL-16 complete asset catalog", () => {
expect(P0A_DYNAMIC_STICKER_IDS[0]).toBe("DYN001"); expect(P0A_DYNAMIC_STICKER_IDS[0]).toBe("DYN001");
expect(P0A_DYNAMIC_STICKER_IDS.at(-1)).toBe("DYN035"); expect(P0A_DYNAMIC_STICKER_IDS.at(-1)).toBe("DYN035");
}); });
it("generates a sanitized browser catalog for every text, font, and dynamic definition", () => {
expect(complexAssetCatalog.schema_version).toBe("DadaComplexBrowserCatalog/v1");
expect(complexAssetCatalog.text_templates).toHaveLength(332);
expect(complexAssetCatalog.font_panel_items).toHaveLength(86);
expect(complexAssetCatalog.dynamic_stickers).toHaveLength(35);
expect(complexAssetCatalog.text_templates.filter((item) => item.preview_asset_id)).toHaveLength(261);
expect(complexAssetCatalog.text_templates.every((item) => item.available)).toBe(true);
expect(JSON.stringify(complexAssetCatalog)).not.toMatch(/[A-Z]:[\\/]/i);
});
}); });