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`);