#!/usr/bin/env node /** * gen_bilibili_docx.js * 生成B站视频发布版docx文档 * * 用法: * node gen_bilibili_docx.js \ * --title "视频标题" \ * --subtitle "封面副标题" \ * --intro "视频简介和标签" \ * --output "output.docx" \ * --cover "cover.png" \ * --opening "opening.png" \ * --policy "policy.png" \ * --subsidy "subsidy.png" \ * --indicators "indicators.png" \ * --case "case.png" \ * --content-json '' * * JSON_STRING 格式: * { * "sections": [ * { "heading": "小标题", "img": "对应图片key(cover/opening/policy/subsidy/indicators/case)", "paragraphs": ["正文1", "正文2"] } * ] * } */ const { Document, Packer, Paragraph, TextRun, ImageRun } = require('/home/node/.openclaw/node_modules/docx'); const fs = require('fs'); const args = process.argv.slice(2); let title = '', subtitle = '', intro = '', output = ''; let coverImg = '', openingImg = '', policyImg = '', subsidyImg = '', indicatorsImg = '', caseImg = ''; let contentJson = ''; for (let i = 0; i < args.length; i++) { if (args[i] === '--title' && args[i+1]) title = args[++i]; else if (args[i] === '--subtitle' && args[i+1]) subtitle = args[++i]; else if (args[i] === '--intro' && args[i+1]) intro = args[++i]; else if (args[i] === '--output' && args[i+1]) output = args[++i]; else if (args[i] === '--cover' && args[i+1]) coverImg = args[++i]; else if (args[i] === '--opening' && args[i+1]) openingImg = args[++i]; else if (args[i] === '--policy' && args[i+1]) policyImg = args[++i]; else if (args[i] === '--subsidy' && args[i+1]) subsidyImg = args[++i]; else if (args[i] === '--indicators' && args[i+1]) indicatorsImg = args[++i]; else if (args[i] === '--case' && args[i+1]) caseImg = args[++i]; else if (args[i] === '--content-json' && args[i+1]) contentJson = args[++i]; } if (!output) { console.error('Error: --output required'); process.exit(1); } const imgMap = { cover: coverImg, opening: openingImg, policy: policyImg, subsidy: subsidyImg, indicators: indicatorsImg, case: caseImg }; const loadImg = (k) => { const p = imgMap[k]; return (p && fs.existsSync(p)) ? fs.readFileSync(p) : null; }; const imgs = { cover: loadImg('cover'), opening: loadImg('opening'), policy: loadImg('policy'), subsidy: loadImg('subsidy'), indicators: loadImg('indicators'), case: loadImg('case') }; const E = () => new Paragraph({ text: '' }); const TITLE = (t) => new Paragraph({ children: [new TextRun({ text: t, bold: true, size: 56 })], alignment: 'center', spacing: { before: 0, after: 160 } }); const SUB = (t) => new Paragraph({ children: [new TextRun({ text: t, size: 28, color: '666666' })], alignment: 'center', spacing: { before: 0, after: 200 } }); const H2 = (t) => new Paragraph({ children: [new TextRun({ text: t, bold: true, size: 36 })], spacing: { before: 240, after: 120 } }); const BODY = (t) => new Paragraph({ children: [new TextRun({ text: t, size: 28 })], spacing: { before: 60, after: 60 } }); const HL = (t) => new Paragraph({ children: [new TextRun({ text: t, bold: true, size: 28, color: '1A1A1A' })], spacing: { before: 120, after: 80 } }); const EMP = (t) => new Paragraph({ children: [new TextRun({ text: t, bold: true, size: 28 })], spacing: { before: 160, after: 160 }, border: { bottom: { color: 'CCCCCC', space: 1, style: 'single', size: 4 } } }); const END = (t) => new Paragraph({ children: [new TextRun({ text: t, size: 28, italics: true, color: '555555' })], spacing: { before: 80, after: 80 } }); const IMGP = (d, w, h) => d ? new Paragraph({ children: [new ImageRun({ data: d, transformation: { width: w, height: h }, type: 'png' })], alignment: 'center', spacing: { before: 80, after: 80 } }) : E(); let sections = []; try { sections = contentJson ? JSON.parse(contentJson) : {}; } catch(e) { console.error('JSON parse error:', e.message); } // Auto-detect which image matches a heading keyword const headingImgMap = { '开场': 'opening', '悬念': 'opening', '引入': 'opening', '政策': 'policy', '全景': 'policy', '四级': 'policy', '补贴': 'subsidy', '金额': 'subsidy', '数字': 'subsidy', '指标': 'indicators', '门槛': 'indicators', '硬指标': 'indicators', '案例': 'case', '真实': 'case', 'SCADA': 'case', }; const detectImg = (heading) => { if (!heading) return null; for (const [kw, imgKey] of Object.entries(headingImgMap)) { if (heading.includes(kw)) return imgs[imgKey]; } return null; }; const children = []; // Cover children.push(IMGP(imgs.cover, 560, 315)); children.push(E()); children.push(TITLE(title)); if (subtitle) children.push(SUB(subtitle)); if (intro) { const introPara = new Paragraph({ children: [new TextRun({ text: intro, size: 22, color: '0077CC' })], alignment: 'center', spacing: { before: 0, after: 200 } }); children.push(introPara); } children.push(E()); // Sections if (sections.sections && Array.isArray(sections.sections)) { for (const sec of sections.sections) { if (sec.heading) children.push(H2(sec.heading)); const secImg = sec.img ? imgs[sec.img] : detectImg(sec.heading); if (secImg) { children.push(IMGP(secImg, 480, 270)); children.push(E()); } if (sec.paragraphs && Array.isArray(sec.paragraphs)) { for (const p of sec.paragraphs) { if (!p || p.trim() === '') { children.push(E()); continue; } const clean = p.trim(); if (clean.length < 60 && (clean.includes('——') || clean.match(/^[①②③④]/))) { children.push(BODY(clean)); } else if (clean.length < 80 && !clean.includes('。')) { children.push(BODY(clean)); } else if (clean.startsWith('!') || clean.startsWith('?')) { children.push(HL(clean)); } else { children.push(BODY(clean)); } } } children.push(E()); } } // Source children.push(new Paragraph({ children: [new TextRun({ text: '内容来源:上海橙轩智能(Orpaon)· 制造业数字化解决方案 | 官网:www.orpaon.com', size: 20, color: '999999' })], alignment: 'center', spacing: { before: 200, after: 0 }, })); const doc = new Document({ sections: [{ children }] }); Packer.toBuffer(doc).then(buf => { fs.writeFileSync(output, buf); console.log('done'); }).catch(e => { console.error(e); process.exit(1); });