diff --git a/docx-publisher.skill b/docx-publisher.skill new file mode 100644 index 0000000..1a56186 Binary files /dev/null and b/docx-publisher.skill differ diff --git a/insights.md b/insights.md index 92ecb78..36b040a 100755 --- a/insights.md +++ b/insights.md @@ -83,3 +83,67 @@ YYYY-MM-DD | 场景 | 反馈原文 | 调整动作 - 每次生成图文发布版前,先读取母版完整正文,不用草稿里的缩略版 - 配图按段落生成:封面图、场景图、政策图、数据图、案例图、总结图 - 草稿里的 md 文件如果内容偏薄,必须用母版正文替换后再生成 docx + +## 2026-05-09 | 平台月度发布限额(重要) + +**化工仪器网 (chem17.com)**:每月最多发布 10 条内容,超出将被限制。 +- 建议:优先发布质量高的文章,合理规划发布节奏,避免月内用完额度。 + +## 2026-05-09 | 平台发布节奏规范 + +**有限额平台**:月度/周度发布限额(如化工仪器网每月10条),只发经过筛选的高质量选题,不随意批量发布。 +**无限制平台**:可高频发布,保证每周 2-3 个选题稳定输出。 + +当前有限额平台清单: +- 化工仪器网:每月最多10条 + + +## 2026-05-09 | 今日工作总结与复盘 + +### 一、发布流程问题 + +**问题1:B站 browserless 无法登录** +- 原因:browserless IP被B站识别为机器人,创作中心直接报错 +- 解决:改用 Tyrone 自操方式发布 +- 教训:视频平台(B站/抖音/快手)浏览器反爬严格,browserless 难以绕过,优先准备 docx 让用户自操发布 + +**问题2:百家号登录百度安全验证拦截** +- 原因:browserless 环境频繁登录触发百度安全机制,要求手机短信二次验证 +- 解决:同样改为 docx 自操 +- 教训:百度系平台(百家号/搜狐号)登录安全机制严,browserless 极易触发验证,建议优先 docx 自操 + +**问题3:Control 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条,只发高质量选题 + diff --git a/skills/docx-publisher/SKILL.md b/skills/docx-publisher/SKILL.md new file mode 100644 index 0000000..8ab6003 --- /dev/null +++ b/skills/docx-publisher/SKILL.md @@ -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 '' +``` + +### Step 4:发给用户 + +发 MEDIA: 路径给用户,告知可直接导入平台发布。 + +## docx 格式规范 + +| 元素 | 样式 | +|------|------| +| 标题 | 24pt+,加粗,居中 | +| 副标题/摘要 | 灰色,斜体,居中 | +| 小标题(H2) | 加粗,段落前后间距 | +| 核心观点(加粗句)| 加粗,可带底边线 | +| 正文 | 标准字 | +| 结束语/来源 | 灰色,小字,居中 | +| 图片 | 宽度480-580px,居中 | + +## 禁止出现在发布版的内容 + +- 所有平台禁用词(见 `references/平台合规规则.md`) +- 联系方式(电话/QQ/微信/网址) +- 绝对化用语(最好/第一/国家级/唯一等) +- 未通过合规扫描的内容 +- `[时间] 旁白类型` 格式的分镜指令 diff --git a/skills/docx-publisher/references/平台合规规则.md b/skills/docx-publisher/references/平台合规规则.md new file mode 100644 index 0000000..b763c58 --- /dev/null +++ b/skills/docx-publisher/references/平台合规规则.md @@ -0,0 +1,92 @@ +# 平台合规规则 + +> 生成各平台发布版 docx 前,必须读取本文件并对目标平台应用对应规则。 + +--- + +## 化工仪器网 (chem17.com) + +**审核严格程度:高** + +### 禁用词(直接替换,不得出现) + +| 禁用词 | 替代表达 | +|--------|---------| +| 卓越 | 更高级别 | +| 先进 | 高等级 | +| 领航 | 最高级别 | +| 工信部 | 六部门 | +| 首次 | 首次 → "这一次" | +| 第一 | 首位/领先 | +| 国家级 | 全国性 | +| 行业标杆 | 行业示范 | +| 保证/确保/100% | 通常可达到/通常情况下 | +| 国家级 | 全国性标准 | +| 官方 | 权威 | + +### 特殊规则 + +- **禁止插入联系方式**:电话/QQ/微信/网址均不可出现 +- **图片中不得含联系方式水印** +- **来源处不得写网址**,只写公司简称 +- **数据须注明来源**:补贴金额后加"(以各地政策为准)";案例成效加"(模拟参考值,以实际为准)" + +### 替代表达库 + +``` +"详细方案" → "完整方案" +"完美解决" → "有效改善" +"彻底解决" → "大幅改善" +"遥遥领先" → "具备优势" +"绝对可靠" → "稳定可靠" +"唯一" → "少数具备" +"独家" → "自主研发" +``` + +--- + +## 百家号 + +**审核严格程度:中** + +- 禁用词参照广告法(参见 `brand/banned-words.md`) +- 联系方式:可在文末注明公司名,但不可留具体手机号/微信号 +- 不得出现竞品负面表述 + +--- + +## 搜狐号 + +**审核严格程度:中** + +- 基本同百家号规则 +- 图片中不得含水印联系方式 + +--- + +## 微信公众号 + +**审核严格程度:中** + +- 关注自动回复/外部链接需合规 +- 不得诱导关注/分享 +- 图片须有版权 + +--- + +## CSDN / 博客园 + +**审核严格程度:低** + +- 技术文章为主,相对宽松 +- 代码块须格式正确 +- 可保留技术参考链接 + +--- + +## 通用规则(所有平台) + +1. **数据必须有来源**:补贴金额注明政策依据;案例数据注明"模拟参考值" +2. **禁用绝对化用语**:最好/最佳/第一/唯一/顶级/完美 +3. **竞品中性原则**:不得出现"碾压/完爆/吊打"等贬低竞品词汇 +4. **联系方式处理**:原则上不发手机号/微信号,平台要求除外 diff --git a/skills/docx-publisher/scripts/gen_docx.js b/skills/docx-publisher/scripts/gen_docx.js new file mode 100644 index 0000000..84f3612 --- /dev/null +++ b/skills/docx-publisher/scripts/gen_docx.js @@ -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 '' + */ + +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); }); \ No newline at end of file diff --git a/tmp_gen_docx.js b/tmp_gen_docx.js new file mode 100644 index 0000000..11b322e --- /dev/null +++ b/tmp_gen_docx.js @@ -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); });