134 lines
6.1 KiB
JavaScript
134 lines
6.1 KiB
JavaScript
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
import { createHash } from "node:crypto";
|
|
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";
|
|
import { compileTextTemplateAssets } from "./lib/text-template-assets.mjs";
|
|
|
|
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 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 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}`);
|
|
return {
|
|
display_name: String(metadata.display_name ?? fontId),
|
|
display_order: displayOrder,
|
|
font_id: fontId,
|
|
};
|
|
});
|
|
|
|
const textResourceRevision = createHash("sha256");
|
|
const textTemplates = P0A_TEXT_TEMPLATE_IDS.map((templateId, catalogOrder) => {
|
|
const directory = textTemplateDirectory(textRoot, templateId);
|
|
const metadata = readJson(join(directory, "metadata.json"));
|
|
const compiled = compileTextTemplateAssets({ templateDirectory: directory, templateId });
|
|
if (compiled.diagnostics.unresolved_images !== 0) throw new Error(`text_template_image_reference_unresolved:${templateId}`);
|
|
for (const resource of compiled.resources) {
|
|
const bytes = resource.sourceBytes ?? readFileSync(resource.sourcePath);
|
|
textResourceRevision.update(resource.assetId).update(createHash("sha256").update(bytes).digest());
|
|
}
|
|
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),
|
|
...compiled.catalog,
|
|
display_name: String(metadata.display_name || metadata.default_text || compiled.catalog.default_text || templateId),
|
|
...(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/v2",
|
|
text_resource_revision: textResourceRevision.digest("hex").slice(0, 16),
|
|
text_templates: textTemplates,
|
|
};
|
|
|
|
const serialized = `${JSON.stringify(catalog, null, 2)}\n`;
|
|
function containsAbsolutePath(value) {
|
|
if (typeof value === "string") return /^[A-Z]:[\\/]/i.test(value);
|
|
if (Array.isArray(value)) return value.some(containsAbsolutePath);
|
|
return value && typeof value === "object" ? Object.values(value).some(containsAbsolutePath) : false;
|
|
}
|
|
const unsafeTemplateIds = textTemplates.filter(containsAbsolutePath).map((item) => item.template_id);
|
|
if (unsafeTemplateIds.length > 0) throw new Error(`complex_catalog_absolute_path_detected:${unsafeTemplateIds.join(",")}`);
|
|
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`);
|