Files
NASOpenClawRunTime/skills/bilibili-publisher/scripts/gen_bilibili_docx.js
小橙 7edb53c43c feat(publish): B站-智能工厂四级补贴首发归档
- drafts/ 按日期+名称分类重整
- 2026-05-09_B站首发归档至published/
- 配图6张永久存档
2026-05-09 13:20:00 +00:00

132 lines
6.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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>'
*
* 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); });