import fs from "node:fs/promises"; import path from "node:path"; import { pathToFileURL } from "node:url"; import { createHash } from "node:crypto"; import { checkContent, normalize } from "./ppt-acceptance.mjs"; const [action, dir, reference, runtime, skill] = process.argv.slice(2); process.env.RUNTIME_NODE_MODULES = path.join(runtime, "node/node_modules"); process.env.RUNTIME_PYTHON = path.join(runtime, "python/bin/python3"); const { Presentation, PresentationFile, FileBlob } = await import(pathToFileURL(path.join(runtime, "node/node_modules/@oai/artifact-tool/dist/artifact_tool.mjs"))); const { finalizePresentation, resolvePresentationFont } = await import(pathToFileURL(path.join(skill, "container_tools/artifact_tool_utils.mjs"))); const input = JSON.parse(await fs.readFile(path.join(dir, "input.json"), "utf8")); const task = await fs.readFile(path.join(dir, "task.json"), "utf8").then(JSON.parse).catch(e => { if (e.code === "ENOENT") return null; throw e; }); const purpose = input.purpose ?? "debug"; let originalTexts = []; const templated = action === "inspect" || input.template === "swads"; const deck = templated ? await PresentationFile.importPptx(await FileBlob.load(reference)) : Presentation.create({ slideSize: { width: 1280, height: 720 } }); if (action === "inspect") { const snapshot = await deck.inspect({ kind: "slide,textbox,table", maxChars: 120000 }); await fs.writeFile(path.join(dir, "result.json"), JSON.stringify({ template: "swads", snapshot: snapshot.ndjson })); process.exit(0); } if (templated) { if (!input.replacements?.length && !input.tableCells?.length) throw new Error("PPT_REPLACEMENTS_REQUIRED"); const snapshot = await deck.inspect({ kind: "textbox,table", maxChars: 1000000 }); const records = snapshot.ndjson.split("\n").filter(Boolean).map(line => JSON.parse(line)); originalTexts = records.filter(r => r.kind === "textbox").map(r => r.text ?? r.textPreview ?? "").filter(t => normalize(t).length > 12); for (const change of input.replacements ?? []) { const record = records.find(r => r.id === change.id); if (!record || !(record.text ?? record.textPreview ?? "").includes(change.oldText)) throw new Error(`PPT_TEMPLATE_ANCHOR_MISMATCH: ${change.id}. Reinspect this object; do not generate subset test files as final.`); deck.resolve(change.id).text.replace(change.oldText, change.newText); } for (const cell of input.tableCells ?? []) { const record = records.find(r => r.id === cell.id && r.kind === "table"); if (!record) throw new Error("PPT_TABLE_ANCHOR_MISMATCH"); deck.resolve(cell.id).cells.set(cell.row, cell.column, cell.text); } } else { if (!input.slides?.length) throw new Error("PPT_SLIDES_REQUIRED"); const font = resolvePresentationFont(); for (const data of input.slides) { const slide = deck.slides.add(); slide.background.fill = "#FAFAF7"; for (const [text, top, height, size, bold] of [[data.title, 60, 100, 44, true], [data.body, 190, 440, 28, false]]) { const shape = slide.shapes.add({ geometry: "textbox", position: { left: 80, top, width: 1120, height }, fill: "none", line: { fill: "none", width: 0 } }); shape.text = text; shape.text.style = { typeface: font, fontSize: size, color: "#17372B", bold, autoFit: "none" }; } slide.speakerNotes.textFrame.setText(data.sources ?? "来源由调用方提供;未提供外部来源。"); } } await fs.mkdir(path.join(dir, "output")); const candidate = path.join(dir, "candidate.pptx"); await (await PresentationFile.exportPptx(deck)).save(candidate); const finalPath = path.join(dir, "output/deck.pptx"); await finalizePresentation({ workspaceDir: dir, candidatePath: candidate, finalPath, pythonExecutable: path.join(runtime, "python/bin/python3"), integrityValidatorPath: path.join(skill, "container_tools/inspect_presentation_package_integrity.py"), layoutValidatorPath: path.join(skill, "container_tools/inspect_presentation_layout_geometry.py"), layoutArgs: ["--validate-heading-fit"], verifyArtifactToolImport: true, receiptPath: path.join(dir, "validation.json"), }); const finalDeck = await PresentationFile.importPptx(await FileBlob.load(finalPath)); let count = 0; for (const slide of finalDeck.slides.items) { const png = await finalDeck.export({ slide, format: "png", scale: 1 }); await fs.writeFile(path.join(dir, `slide-${++count}.png`), new Uint8Array(await png.arrayBuffer())); } if (!count) throw new Error("PPT_EMPTY"); const bytes = await fs.readFile(finalPath); const finalSnapshot = await finalDeck.inspect({ kind: "textbox,table", maxChars: 1000000 }); await fs.writeFile(path.join(dir, "content.ndjson"), finalSnapshot.ndjson); const finalText = normalize(finalSnapshot.ndjson.split("\n").filter(Boolean).map(line => { const r = JSON.parse(line); return r.text ?? r.textPreview ?? JSON.stringify(r); }).join("\n")); const failures = checkContent({ purpose, task, template: input.template, originalTexts, finalText }); await fs.writeFile(path.join(dir, "acceptance.json"), JSON.stringify({ purpose, runId: task?.runId, taskRevision: task?.revision, passed: purpose === "final" && failures.length === 0, failures, limits: "Text coverage and explicit task terms only; embedded images and factual accuracy are not semantically verified." }, null, 2)); if (failures.length) throw new Error(`PPT_CONTENT_REJECTED: ${failures.slice(0,8).join("; ")}`); await fs.writeFile(path.join(dir, "result.json"), JSON.stringify({ title: input.title, slideCount: count, validated: true, purpose, deliveryAccepted: false, runId: task?.runId, taskRevision: task?.revision, sha256: createHash("sha256").update(bytes).digest("hex"), template: input.template, note: "Package, layout checks and rendering passed. Business facts and visual quality require review." }));