feat(publish): B站-智能工厂四级补贴首发归档
- drafts/ 按日期+名称分类重整 - 2026-05-09_B站首发归档至published/ - 配图6张永久存档
This commit is contained in:
102
skills/bilibili-publisher/SKILL.md
Normal file
102
skills/bilibili-publisher/SKILL.md
Normal file
@@ -0,0 +1,102 @@
|
||||
---
|
||||
name: bilibili-publisher
|
||||
description: 生成B站视频发布版docx文档,自动嵌入配图。当用户说"生成B站发布版"、"帮我生成docx"、"发B站"时触发。工作流:读取drafts/下对应草稿 → AI生成6张配图存assets/ → Node.js脚本生成带图的docx → 发给用户。
|
||||
---
|
||||
|
||||
# B站发布版生成器
|
||||
|
||||
将 B站 视频草稿(md格式)转换为**发布版 docx文档**,图片直接嵌入,可直接导入B站创作页面发布。
|
||||
|
||||
## 输入
|
||||
|
||||
- drafts/ 下对应的主题 md 文件(如 `2026-05-08_B站_智能工厂四级补贴.md`)
|
||||
- 内容形态:视频脚本,包含时间戳旁白(如 `[开场 0:00–0:30]`)
|
||||
|
||||
## 输出
|
||||
|
||||
- `drafts/assets/` 下生成6张配图(png,永久存储)
|
||||
- `drafts/<日期>_<主题>_发布版.docx`(1.1MB左右,含嵌入图片)
|
||||
- 直接发 MEDIA: 路径给用户下载
|
||||
|
||||
## 工作流
|
||||
|
||||
### Step 1:读取草稿
|
||||
|
||||
读取 `drafts/` 下对应 md 文件,提取:
|
||||
- 标题(从文件名或文件内 H1 提取)
|
||||
- 封面建议文字
|
||||
- 视频简介 / 标签
|
||||
- 脚本正文(去掉 `[时间] 旁白指令` 行)
|
||||
- 分段结构(H2 标题)
|
||||
|
||||
### Step 2:生成配图
|
||||
|
||||
根据脚本6个段落生成6张配图:
|
||||
|
||||
| 段落 | 图片主题 | 文件名 |
|
||||
|------|---------|--------|
|
||||
| 封面 | 工厂+数字仪表盘全息投影 | `bilibili_cover.png` |
|
||||
| 开场 | 工厂车间暗光设备剪影 | `bilibili_opening.png` |
|
||||
| 政策全景 | 四级政策图解信息图 | `bilibili_policy.png` |
|
||||
| 补贴数字 | 金额对比信息图 | `bilibili_subsidy.png` |
|
||||
| 四项指标 | SCADA dashboard截图 | `bilibili_indicators.png` |
|
||||
| 真实案例 | 工厂中控室数据大屏 | `bilibili_case.png` |
|
||||
|
||||
**关键**:每张图生成后**立即用 `cp`** 复制到 `drafts/assets/`,再生成下一张(media 目录会被定时清理,不能存那里)
|
||||
|
||||
### Step 3:生成 docx
|
||||
|
||||
使用 `scripts/gen_bilibili_docx.js`:
|
||||
|
||||
```bash
|
||||
node ~/.openclaw/workspace/skills/bilibili-publisher/scripts/gen_bilibili_docx.js \
|
||||
--title "标题" \
|
||||
--subtitle "封面建议文字" \
|
||||
--intro "视频简介" \
|
||||
--cover ./drafts/assets/bilibili_cover.png \
|
||||
--opening ./drafts/assets/bilibili_opening.png \
|
||||
--policy ./drafts/assets/bilibili_policy.png \
|
||||
--subsidy ./drafts/assets/bilibili_subsidy.png \
|
||||
--indicators ./drafts/assets/bilibili_indicators.png \
|
||||
--case ./drafts/assets/bilibili_case.png \
|
||||
--output ./drafts/<日期>_<主题>_发布版.docx \
|
||||
--content "./drafts/assets/content.json"
|
||||
```
|
||||
|
||||
内容通过 `content.json` 传入(避免命令行参数转义问题):
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "标题",
|
||||
"subtitle": "副标题",
|
||||
"intro": "视频简介",
|
||||
"sections": [
|
||||
{
|
||||
"heading": "段落小标题",
|
||||
"image": "配图路径(可选)",
|
||||
"paragraphs": ["段落1", "段落2"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4:发送给用户
|
||||
|
||||
docx 生成后,发 MEDIA: 路径给用户,告知可以直接导入B站。
|
||||
|
||||
## docx 格式规范
|
||||
|
||||
- **标题**:28pt,加粗,居中
|
||||
- **副标题**:14pt,灰色,居中
|
||||
- **小标题(H2)**:18pt,加粗
|
||||
- **核心观点**:加粗,带下划线或底边线
|
||||
- **普通正文**:14pt
|
||||
- **结束语**:斜体,灰色
|
||||
- **来源**:12pt,灰色,居中
|
||||
- **图片**:宽度500px,居中,段前段后间距80
|
||||
|
||||
## 禁止出现的内容(发布版必须删除)
|
||||
|
||||
- 所有 `[时间节点] 旁白类型` 格式的行(如 `[开场 0:00–0:30] 悬念引入`)
|
||||
- 任何分镜备注、拍摄指导
|
||||
- 未通过合规扫描的内容
|
||||
131
skills/bilibili-publisher/scripts/gen_bilibili_docx.js
Normal file
131
skills/bilibili-publisher/scripts/gen_bilibili_docx.js
Normal file
@@ -0,0 +1,131 @@
|
||||
#!/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); });
|
||||
Reference in New Issue
Block a user