chore: 今日工作总结复盘-发布流程/Skill/内容规范沉淀

This commit is contained in:
小橙
2026-05-09 15:33:28 +00:00
parent 64afc0ec5f
commit d2ff5db8f2
6 changed files with 365 additions and 0 deletions

BIN
docx-publisher.skill Normal file

Binary file not shown.

View File

@@ -83,3 +83,67 @@ YYYY-MM-DD | 场景 | 反馈原文 | 调整动作
- 每次生成图文发布版前,先读取母版完整正文,不用草稿里的缩略版 - 每次生成图文发布版前,先读取母版完整正文,不用草稿里的缩略版
- 配图按段落生成:封面图、场景图、政策图、数据图、案例图、总结图 - 配图按段落生成:封面图、场景图、政策图、数据图、案例图、总结图
- 草稿里的 md 文件如果内容偏薄,必须用母版正文替换后再生成 docx - 草稿里的 md 文件如果内容偏薄,必须用母版正文替换后再生成 docx
## 2026-05-09 | 平台月度发布限额(重要)
**化工仪器网 (chem17.com)**:每月最多发布 10 条内容,超出将被限制。
- 建议:优先发布质量高的文章,合理规划发布节奏,避免月内用完额度。
## 2026-05-09 | 平台发布节奏规范
**有限额平台**:月度/周度发布限额如化工仪器网每月10条只发经过筛选的高质量选题不随意批量发布。
**无限制平台**:可高频发布,保证每周 2-3 个选题稳定输出。
当前有限额平台清单:
- 化工仪器网每月最多10条
## 2026-05-09 | 今日工作总结与复盘
### 一、发布流程问题
**问题1B站 browserless 无法登录**
- 原因browserless IP被B站识别为机器人创作中心直接报错
- 解决:改用 Tyrone 自操方式发布
- 教训视频平台B站/抖音/快手浏览器反爬严格browserless 难以绕过,优先准备 docx 让用户自操发布
**问题2百家号登录百度安全验证拦截**
- 原因browserless 环境频繁登录触发百度安全机制,要求手机短信二次验证
- 解决:同样改为 docx 自操
- 教训:百度系平台(百家号/搜狐号登录安全机制严browserless 极易触发验证,建议优先 docx 自操
**问题3Control UI 文件查看器 md 中文乱码**
- 原因Control UI 内置 markdown 渲染器对 UTF-8 中文支持有 bug
- 解决:不用 MEDIA: 路径,改直接在聊天里贴文本内容
- 教训md 文件不要期望在 Control UI 里预览,直接给用户内容
### 二、Skill 沉淀
**bilibili-publisher**生成B站视频发布版 docx自动嵌入配图图片永久存储
- 触发词发B站/生成B站发布版/生成docx
- 注意media 目录图片会被清理,必须立即 cp 到 drafts/assets/ 永久目录
**docx-publisher**:通用图文平台发布版 docx 生成器,内置平台合规规则
- 化工仪器网禁用词已沉淀:卓越/先进/工信部/领航/首次/第一/国家级/行业标杆
- 替代表达已记录
### 三、内容质量
**反馈**:图文平台(百家号/搜狐号文章内容过薄不及B站脚本丰富
- 原因:用了草稿的缩略版,而不是母版全文
- 规范图文平台必须用母版完整正文4-6张配图/篇
### 四、工作流优化
**发布前必做**
1. 读取平台合规规则docx-publisher/references/平台合规规则.md
2. 用母版全文替换缩略版
3. 生成配图立即 cp 到永久目录
4. docx 脚本中的内容直接内嵌,不用 --content-json 传参(避免中文引号 JSON 解析失败)
### 五、平台分类
**开绿灯平台(一次授权可自动发布)**CSDN、博客园、搜狐号、百家号、好看视频
**用户自操平台browserless 无法绕过登录/验证)**B站、百家号百度验证、抖音、快手、视频号、微信公众号
**有限额平台**化工仪器网每月最多10条只发高质量选题

View File

@@ -0,0 +1,69 @@
---
name: docx-publisher
description: 生成图文平台发布版docx文档支持多平台合规过滤。当用户说"生成发布版"、"生成docx"时触发。工作流读取drafts/对应草稿 → 注入平台合规规则 → 生成配图 → Node.js脚本生成带图docx → 发给用户。
---
# 图文平台发布版生成器
将图文草稿md格式转换为**发布版 docx文档**,图片直接嵌入,可导入各平台后台直接发布。
## 触发词
"生成发布版"、"生成docx"、"发XX平台"
## 平台合规规则
详见 `references/平台合规规则.md`。生成前必须读取对应平台的规则。
## 工作流
### Step 1读取草稿 + 合规规则
1. 读取 `drafts/` 下对应 md 文件(优先读母版 `master` 版,内容更完整)
2. 读取 `references/平台合规规则.md`,确认目标平台的**禁用词列表**和**特殊规则**
3. 将草稿正文中的禁用词替换为合规替代表述
### Step 2生成配图
根据平台类型决定配图数量:
- **图文平台(百家号/搜狐号/公众号等)**:封面图 + 每段落1张共4-6张
- **化工仪器网**:封面图 + 3张段落图政策/技术/案例)
**关键**:每张图生成后**立即用 `cp`** 复制到 `published/<日期>_<主题>/assets/` 永久目录,再生成下一张
### Step 3生成 docx
```bash
node scripts/gen_docx.js \
--title "标题" \
--output "output.docx" \
--cover ./assets/cover.png \
--image2 ./assets/img2.png \
--image3 ./assets/img3.png \
--image4 ./assets/img4.png \
--content-json '<JSON>'
```
### Step 4发给用户
发 MEDIA: 路径给用户,告知可直接导入平台发布。
## docx 格式规范
| 元素 | 样式 |
|------|------|
| 标题 | 24pt+,加粗,居中 |
| 副标题/摘要 | 灰色,斜体,居中 |
| 小标题H2 | 加粗,段落前后间距 |
| 核心观点(加粗句)| 加粗,可带底边线 |
| 正文 | 标准字 |
| 结束语/来源 | 灰色,小字,居中 |
| 图片 | 宽度480-580px居中 |
## 禁止出现在发布版的内容
- 所有平台禁用词(见 `references/平台合规规则.md`
- 联系方式(电话/QQ/微信/网址)
- 绝对化用语(最好/第一/国家级/唯一等)
- 未通过合规扫描的内容
- `[时间] 旁白类型` 格式的分镜指令

View File

@@ -0,0 +1,92 @@
# 平台合规规则
> 生成各平台发布版 docx 前,必须读取本文件并对目标平台应用对应规则。
---
## 化工仪器网 (chem17.com)
**审核严格程度:高**
### 禁用词(直接替换,不得出现)
| 禁用词 | 替代表达 |
|--------|---------|
| 卓越 | 更高级别 |
| 先进 | 高等级 |
| 领航 | 最高级别 |
| 工信部 | 六部门 |
| 首次 | 首次 → "这一次" |
| 第一 | 首位/领先 |
| 国家级 | 全国性 |
| 行业标杆 | 行业示范 |
| 保证/确保/100% | 通常可达到/通常情况下 |
| 国家级 | 全国性标准 |
| 官方 | 权威 |
### 特殊规则
- **禁止插入联系方式**:电话/QQ/微信/网址均不可出现
- **图片中不得含联系方式水印**
- **来源处不得写网址**,只写公司简称
- **数据须注明来源**:补贴金额后加"(以各地政策为准)";案例成效加"(模拟参考值,以实际为准)"
### 替代表达库
```
"详细方案" → "完整方案"
"完美解决" → "有效改善"
"彻底解决" → "大幅改善"
"遥遥领先" → "具备优势"
"绝对可靠" → "稳定可靠"
"唯一" → "少数具备"
"独家" → "自主研发"
```
---
## 百家号
**审核严格程度:中**
- 禁用词参照广告法(参见 `brand/banned-words.md`
- 联系方式:可在文末注明公司名,但不可留具体手机号/微信号
- 不得出现竞品负面表述
---
## 搜狐号
**审核严格程度:中**
- 基本同百家号规则
- 图片中不得含水印联系方式
---
## 微信公众号
**审核严格程度:中**
- 关注自动回复/外部链接需合规
- 不得诱导关注/分享
- 图片须有版权
---
## CSDN / 博客园
**审核严格程度:低**
- 技术文章为主,相对宽松
- 代码块须格式正确
- 可保留技术参考链接
---
## 通用规则(所有平台)
1. **数据必须有来源**:补贴金额注明政策依据;案例数据注明"模拟参考值"
2. **禁用绝对化用语**:最好/最佳/第一/唯一/顶级/完美
3. **竞品中性原则**:不得出现"碾压/完爆/吊打"等贬低竞品词汇
4. **联系方式处理**:原则上不发手机号/微信号,平台要求除外

View File

@@ -0,0 +1,81 @@
#!/usr/bin/env node
/**
* gen_docx.js
* 通用图文平台发布版docx生成器
*
* 用法:
* node gen_docx.js --title "标题" --subtitle "副标题" --output output.docx \
* --cover ./assets/cover.png \
* --content-json '<JSON_STRING>'
*/
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 = '', output = '', coverImg = '';
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] === '--output' && args[i+1]) output = args[++i];
else if (args[i] === '--cover' && args[i+1]) coverImg = args[++i];
else if (args[i] === '--content-json' && args[i+1]) contentJson = args[++i];
}
if (!output) { console.error('Usage: --output required'); process.exit(1); }
const loadImg = (p) => (p && fs.existsSync(p)) ? fs.readFileSync(p) : null;
const cover = coverImg ? loadImg(coverImg) : null;
const E = () => new Paragraph({ text: '' });
const TITLE = (t) => new Paragraph({ children: [new TextRun({ text: t, bold: true, size: 48 })], alignment: 'center', spacing: { before: 0, after: 160 } });
const SUB = (t) => new Paragraph({ children: [new TextRun({ text: t, size: 24, color: '888888', italics: true })], alignment: 'center', spacing: { before: 0, after: 200 } });
const H2 = (t) => new Paragraph({ children: [new TextRun({ text: t, bold: true, size: 36 })], spacing: { before: 320, after: 160 } });
const H3 = (t) => new Paragraph({ children: [new TextRun({ text: t, bold: true, size: 28 })], spacing: { before: 200, after: 80 } });
const BODY = (t) => new Paragraph({ children: [new TextRun({ text: t, size: 28 })], spacing: { before: 60, after: 80 } });
const EMP = (t) => new Paragraph({ children: [new TextRun({ text: t, bold: true, size: 30 })], spacing: { before: 120, after: 100 } });
const END = (t) => new Paragraph({ children: [new TextRun({ text: t, size: 22, color: 'AAAAAA', italics: true })], alignment: 'center', spacing: { before: 200, after: 0 } });
const IMGP = (d, w, h) => d ? new Paragraph({ children: [new ImageRun({ data: d, transformation: { width: w, height: h }, type: 'png' })], alignment: 'center', spacing: { before: 100, after: 100 } }) : E();
let config = {};
try { config = contentJson ? JSON.parse(contentJson) : {}; } catch(e) { console.error('JSON parse error:', e.message); }
const children = [];
// Cover
if (cover) { children.push(IMGP(cover, 580, 326)); children.push(E()); }
children.push(TITLE(title));
if (subtitle) children.push(SUB(subtitle));
children.push(E());
// Sections
const sections = config.sections || [];
for (const sec of sections) {
if (sec.heading) children.push(H2(sec.heading));
if (sec.image) 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.match(/^[①②③④]/)) {
children.push(BODY(clean));
} else if (clean.length < 80 && !clean.includes('。')) {
children.push(BODY(clean));
} else {
children.push(BODY(clean));
}
}
}
children.push(E());
}
// Source
if (config.source) children.push(END(config.source));
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); });

59
tmp_gen_docx.js Normal file
View File

@@ -0,0 +1,59 @@
#!/usr/bin/env node
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 = '', output = '', coverImg = '';
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] === '--output' && args[i+1]) output = args[++i];
else if (args[i] === '--cover' && args[i+1]) coverImg = args[++i];
else if (args[i] === '--image2' && args[i+1]) coverImg = args[++i]; // reuse
else if (args[i] === '--content-json' && args[i+1]) contentJson = args[++i];
}
const loadImg = (p) => (p && fs.existsSync(p)) ? fs.readFileSync(p) : null;
const cover = coverImg ? loadImg(coverImg) : null;
const E = () => new Paragraph({ text: '' });
const TITLE = (t) => new Paragraph({ children: [new TextRun({ text: t, bold: true, size: 48 })], alignment: 'center', spacing: { before: 0, after: 160 } });
const SUB = (t) => new Paragraph({ children: [new TextRun({ text: t, size: 24, color: '888888', italics: true })], alignment: 'center', spacing: { before: 0, after: 200 } });
const H2 = (t) => new Paragraph({ children: [new TextRun({ text: t, bold: true, size: 36 })], spacing: { before: 320, after: 160 } });
const H3 = (t) => new Paragraph({ children: [new TextRun({ text: t, bold: true, size: 28 })], spacing: { before: 200, after: 80 } });
const BODY = (t) => new Paragraph({ children: [new TextRun({ text: t, size: 28 })], spacing: { before: 60, after: 80 } });
const EMP = (t) => new Paragraph({ children: [new TextRun({ text: t, bold: true, size: 30 })], spacing: { before: 120, after: 100 } });
const END = (t) => new Paragraph({ children: [new TextRun({ text: t, size: 22, color: 'AAAAAA', italics: true })], alignment: 'center', spacing: { before: 200, after: 0 } });
const IMGP = (d, w, h) => d ? new Paragraph({ children: [new ImageRun({ data: d, transformation: { width: w, height: h }, type: 'png' })], alignment: 'center', spacing: { before: 100, after: 100 } }) : E();
let config = {};
try { config = contentJson ? JSON.parse(contentJson) : {}; } catch(e) { console.error('JSON parse error:', e.message); }
const children = [];
if (cover) { children.push(IMGP(cover, 580, 326)); children.push(E()); }
children.push(TITLE(title));
if (subtitle) children.push(SUB(subtitle));
children.push(E());
const sections = config.sections || [];
for (const sec of sections) {
if (sec.heading) children.push(H2(sec.heading));
if (sec.image) children.push(E());
if (sec.paragraphs && Array.isArray(sec.paragraphs)) {
for (const p of sec.paragraphs) {
if (!p || p.trim() === '') { children.push(E()); continue; }
children.push(BODY(p.trim()));
}
}
children.push(E());
}
if (config.source) children.push(END(config.source));
const doc = new Document({ sections: [{ children }] });
Packer.toBuffer(doc).then(buf => {
fs.writeFileSync(output, buf);
console.log('done: ' + output);
}).catch(e => { console.error(e); process.exit(1); });