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