Claude Skill

visual-ops-writer

图文运营创作器。从选题、撰写、配图到质检,一条龙完成运营文章生产。看到好文章能仿写,给个 Skill 能生成推广长文,写完正文能自动配图。内置写作质量管控与自动配图能力,图文一体交付。触发词:"生成运营文章"、"链接仿写"、"对标分析"、"Skill 推广"、"为正文配图"。

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download redfox-data-redfox-community-skills_visual-ops-writer-5e7b435.zip · 895 KB
Part of redfox-data/redfox-community — 66 skills

Install

skills CLI npx skills add https://github.com/redfox-data/redfox-community/tree/main/skills/visual-ops-writer
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install redfox-data-redfox-community@llmmart
Git git clone https://github.com/redfox-data/redfox-community.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole redfox-data/redfox-community collection as a plugin from our marketplace. Git is the plain clone.

README

图文运营创作器 / visual-ops-writer


简介

从选题、撰写、配图到质检,覆盖运营文章的完整创作链路。看到好文章能仿写,给个 Skill 能生成推广长文,写完正文能自动配图——图文一体交付,拿来就能用。

核心价值

  • 三种场景,一个工具:链接仿写、产品推广、正文配图——内容运营最高频的三个创作场景,无需来回切换工具。
  • 图文一体交付:文章与配图同步生成,保存为完整可用的成品文件,不是半成品。
  • 质量内置:每篇文章自动经过 9 项质量检查,输出质检报告,交付即可用。

适用对象

  • 📝 内容运营 — 快速产出对标风格文章与产品推广文,从「写到配」一条龙搞定。
  • 🛍️ 品牌 / 技能开发者 — 自动生成对齐产品能力的推广长文,内容与功能不脱节。
  • 🏢 博主 / 编辑 — 为已有正文智能配图,省去手动规划配图位置的麻烦。

功能特性

核心功能

  • 链接仿写:提供任意文章链接,自动抓取原文内容与图片,学习其风格与结构,仿写出角度不同但质量相当的新文章。配图风格与原图保持一致,适合对标分析与竞品内容学习。
  • Skill 推广文:提供 Skill 文件路径或名称,自动解析产品能力并抓取目标站点最新信息,生成与产品实际能力对齐的推广长文。配图采用红狐讲解员风格,视觉与内容双重匹配。
  • 正文补图:提供完整正文,自动按章节语义与字数密度分析文章结构,智能决定配图位置与数量,批量生成风格统一的配图。适合写完正文不想手动找图的场景。
  • 自动质量检查:每篇文章生成后自动执行 9 项质量检查(标题长度、章节数、表格数、字数、FAQ 数量、禁忌词、段落行数、信息密度等),输出 JSON 格式质检报告。

写作能力

  • 文章类型自动匹配:工具推介型、方法论型、危机应对型三种模板,根据选题自动选择最合适的写作结构
  • 读者画像适配:支持个体创作者和团队/企业两种读者定位,自动调整表达侧重
  • 语调可选:直白坦率 / 专业严谨 / 轻松对话,三种语调自由切换
  • 字数可控:支持自定义字数区间(默认 2000-3000 字),也可按原文篇幅自适应

配图能力

  • 风格迁移:链接仿写模式下,原图作为视觉参照,新内容自动生成,风格一致
  • 语义匹配:配图按章节内容自动匹配讲解员姿态(欢迎、分析、思考、操作)
  • 智能断章:按 H2 章节切分 + 字数密度自动决策生图点,跳过不适合配图的位置
  • 数量可调:0-5 张配图上限可调,也可完全交由系统自动决策

密钥获取与安全说明

  • 本技能需要使用环境变量:REDFOX_API_KEY。
  • REDFOX_API_KEY 由 红狐 hub (https://redfox.hk)
  • 请前往 红狐 hub 注册账号,获取 REDFOX_API_KEY。
  • 配置设备环境变量 REDFOX_API_KEY 后使用本技能。
  • 在提供密钥前,请先确认密钥来源、可用范围、有效期及是否支持重置/撤销。
  • 禁止在代码、提示词、日志或输出文件中硬编码/明文暴露密钥。

使用指南

直接用自然语言描述需求,无需记忆命令。

常用说法速查

意图 示例话术 效果
链接仿写 「帮我仿写这篇文章,保持风格但换成我的产品」 学习原文风格与结构,生成配图一致的全新文章
Skill 推广 「帮我给这个 Skill 写一篇推广文」 自动生成对齐产品能力的推广长文与配图
正文补图 「帮我给这篇正文加上配图」 智能分析文章结构,自动决定配图位置并生成
竞品分析 「分析这篇文章的写法,帮我写一篇不同角度的」 学习优秀内容的结构与风格,产出差异化内容

输出示例

完成生成后,你将收到:

  1. 完整文章:保存为 article.md,含标题、正文、配图(嵌入对应章节)
  2. 质检报告:validation_report.json,含 9 项文章质量检查结果
  3. 对话内全文:直接在对话中输出文章全文
  4. 反馈节点:如需调整某章节或配图,可告知具体修改方向

使用场景

场景 角色 示例问法 收益
对标仿写 内容运营 「帮我仿写这篇文章,保持风格但换成我的产品」 快速产出风格一致的高质量文章,省去从零构思
Skill 推广 技能开发者 「帮我给这个 Skill 写一篇推广文」 自动生成对齐产品能力的推广文章,内容与功能不脱节
文章配图 博主/编辑 「帮我给这篇正文加上配图」 智能选择配图位置与数量,无需手动规划
竞品分析 品牌运营 「分析这篇文章的写法,帮我写一篇不同角度的」 学习优秀内容的结构与风格,产出差异化内容

Skill manifest

图文运营创作器

从选题、撰写、配图到质检,覆盖运营文章的完整创作链路。你只需要提供素材——一篇文章链接、一个 Skill、或者一段写好的正文,剩下的交给它。

核心执行流程详见 references/core_workflow.md


简介

图文运营创作器解决的是内容团队最常见的效率瓶颈:一篇文章从写到配,往往需要多人协作、反复打磨。

通过这个工具,你可以:

  • 看到一篇好文章,直接仿写为自己的版本,风格一致但内容全新
  • 给一个 Skill 或产品,自动生成一篇专业的推广长文,配图到位
  • 写完正文不想找图,丢进来自动判断哪里该配图、配什么风格

三种场景,一个工具全覆盖。

适用对象

  • 📝 内容运营 — 快速产出对标风格文章与产品推广文,从写到配一条龙搞定
  • 🛍️ 品牌 / 技能开发者 — 自动生成对齐产品能力的推广长文,内容与功能不脱节
  • 🏢 博主 / 编辑 — 为已有正文智能配图,省去手动规划配图位置的麻烦

功能特性

🎯 核心功能

  • 🔗 链接仿写:提供任意文章链接,自动抓取原文内容与图片,学习其风格与结构,仿写全新文章。配图风格与原图保持一致,适合对标分析与竞品内容学习。
  • 📣 Skill 推广文:提供 Skill 文件路径或名称,自动解析产品能力并抓取目标站点最新信息,生成与产品实际能力对齐的推广长文。配图采用红狐讲解员风格,视觉与内容双重匹配。
  • 🖼️ 正文补图:提供完整正文,自动按章节语义与字数密度分析文章结构,智能决定配图位置与数量,批量生成风格统一的配图。适合写完正文不想手动找图的场景。
  • ✅ 自动质量检查:每篇文章生成后自动执行 9 项质量检查(标题长度、章节数、表格数、字数、FAQ 数量、禁忌词、段落行数、信息密度等),输出 JSON 格式质检报告。

写作质量管控

不是随便写一篇就交差。文章生成过程内置完整的质量管控体系:

  • 文章类型自动匹配:根据选题自动识别为工具推介型、方法论型或危机应对型,每种类型有独立的写作侧重与结构建议
  • 读者画像适配:支持个体创作者和团队/企业两种读者定位,自动调整表达侧重(个人可执行方法 vs 工具系统协同)
  • 语调可选:直白坦率 / 专业严谨 / 轻松对话,默认直白坦率
  • 成文字数可控:支持自定义字数区间(默认 2000-3000 字),链接仿写按原文篇幅自适应

自动配图能力

文章配图不是简单的"插张图",而是内容的一部分:

  • 风格迁移:链接仿写模式下,原图作为风格参照,新内容自动生成,视觉风格保持一致
  • 语义匹配:Skill 推广模式下,配图按章节语义自动匹配讲解员姿态(欢迎、分析、思考、操作等)
  • 智能断章:正文补图模式下,按 H2 章节切分 + 字数密度自动决策生图点,跳过 FAQ / 结语 / 引言等不适合配图的位置
  • 数量可控:配图数量上限 0-5 张可调,也可由系统自动决策

一键安装

前置条件

  • Python 3.8+(用于运行配图生成与质量检查脚本)
  • REDFOX_API_KEY(用于图片生成,未配置时仍可生成纯文本文章)

安装步骤

  1. 将本 Skill 目录添加到你的 Agent 技能列表
  2. 获取 REDFOX_API_KEY:前往 红狐 hub 注册账号,在 API Keys 页面 创建密钥
  3. 配置环境变量:
# macOS / Linux
export REDFOX_API_KEY="your_key_here"

# Windows PowerShell
$env:REDFOX_API_KEY="your_key_here"
  1. 验证安装:告诉 Agent「帮我给这个 Skill 写一篇推广文」,确认技能正常响应

环境变量说明

变量名 必填 说明
REDFOX_API_KEY 条件必填 图片生成服务密钥;未配置时文章正常生成,图片降级为占位符或跳过

使用指南

基础使用

1. 链接仿写

告诉 Agent 你想仿写的文章链接:

用户:帮我仿写这篇文章,保持风格但换成我的产品 https://example.com/some-article

Agent:已抓取原文,正在学习风格与结构…文章已生成,含 3 张配图,保存为 article.md。

2. Skill 推广文

提供 Skill 文件或名称:

用户:帮我给 xhs-ranking 这个 Skill 写一篇推广文

Agent:已解析 Skill 能力并抓取目标站点信息,推广文章已生成,含红狐风格配图 3 张。

3. 正文补图

粘贴正文或提供文件路径:

用户:帮我给这篇正文加上配图 (粘贴正文内容)

Agent:正文共 1800 字,已识别 4 个配图位置,配图生成中…完成,保存为 article.md。

高级使用

  • 指定语调:「用专业严谨的语调写」
  • 指定读者:「目标读者是团队企业」
  • 指定配图数量:「最多配 2 张图」
  • 关闭配图:「不需要配图」或「style 设为 none」
  • 指定字数:「控制在 1500 字左右」

常用说法速查

意图 示例话术 效果
链接仿写 「帮我仿写这篇文章,保持风格但换成我的产品」 学习原文风格与结构,生成配图一致的全新文章
Skill 推广 「帮我给这个 Skill 写一篇推广文」 自动生成对齐产品能力的推广长文与配图
正文补图 「帮我给这篇正文加上配图」 智能分析文章结构,自动决定配图位置并生成
竞品分析 「分析这篇文章的写法,帮我写一篇不同角度的」 学习优秀内容的结构与风格,产出差异化内容
控制语调 「用轻松对话的语气写」 按指定语调生成文章
关闭配图 「不需要配图」 仅输出纯文本文章

使用场景

场景 角色 示例问法 收益
对标仿写 内容运营 「帮我仿写这篇文章,保持风格但换成我的产品」 快速产出风格一致的高质量文章,省去从零开始的构思
Skill 推广 技能开发者 「帮我给这个 Skill 写一篇推广文」 自动生成对齐产品能力的推广文章,内容与功能不脱节
文章配图 博主/编辑 「帮我给这篇正文加上配图」 智能选择配图位置与数量,无需手动规划
竞品分析 品牌运营 「分析这篇文章的写法,帮我写一篇不同角度的」 学习优秀内容的结构与风格,产出差异化内容

输出说明

完成生成后,你将收到:

  1. 完整文章:保存为 article.md,含标题、正文、FAQ(如有)、配图(以 ![描述](URL) 嵌入对应章节)
  2. 质检报告:validation_report.json,含 9 项文章质量检查结果
  3. 对话内全文:直接在对话中输出文章全文
  4. 反馈节点:如需调整某章节或配图,可告知具体修改方向

常见问答

Q1: 没有 API Key 能用吗?

A: 可以。没有 REDFOX_API_KEY 时,文章正文正常生成,图片部分降级处理:链接仿写用占位符替代,Skill 推广和正文补图跳过配图,仅输出纯文本。

Q2: 生成的文章可以手动调整吗?

A: 可以。生成后直接告诉 Agent 你想调整的方向(如「第三段改短一点」「换一张配图」),Agent 会基于现有文章定向修改。

Q3: 配图风格可以自定义吗?

A: 支持三种风格:redfox(红狐讲解员)、reference(跟随原图风格)、none(无风格修饰)。告诉 Agent 你的偏好即可。


项目架构

visual-ops-writer/
├── SKILL.md                          # 技能入口文件
├── references/
│   ├── core_workflow.md              # 核心执行流程(Agent 执行链)
│   ├── writing-framework.md          # 写作框架参考
│   ├── article-type-templates.md     # 文章类型模板(工具推介/方法论/危机应对)
│   ├── persona-matrix.md             # 语气×读者×视觉 IP 配置矩阵
│   ├── image-decision-rules.md       # 配图决策规则
│   ├── link-rewrite-guide.md         # 链接仿写指南
│   ├── prompt-templates.md           # 提示词模板
│   └── error-handling.md             # 错误处理
├── scripts/
│   ├── fetch_article.py              # 文章抓取与解析
│   ├── validate_article.py           # 机器化质量检查
│   ├── generate_image.py             # 图片生成(支持文生图与风格迁移)
│   └── decide_images.py              # 配图位置与数量决策
└── assets/
    ├── 02.jpg                        # 素材图片
    └── 03.jpg                        # 素材图片
Files (redfox-community)
  • assets
    • 02.jpg 487.7 KB · in bundle
    • 03.jpg 344.4 KB · in bundle
  • references
    • article-type-templates.md 7.6 KB
      # 三种文章类型 Prompt 模板(v3.0)
      
      > 工具推介 / 方法论 / 危机应对三类型的生产逻辑不同,**参考模板**帮助你快速搭骨架。
      > **使用原则**:模板从"强制"改为"参考"——可裁剪、可调整、可混用,按 Skill 逻辑和选题需要动态调整。
      
      ---
      
      ## 使用原则
      
      1. **模板是参考线,不是栅栏**——按选题和读者需求灵活调整
      2. **章节数动态**——可在 2-8 节区间自由选择
      3. **表格按需出现**——0 张表也合规(叙事/资讯类常见)
      4. **字数跟着选题走**——默认 1000 字左右,可通过 `--target-length` 自定义
      5. **混用更自由**——可引入其他模板的章节,不再硬性限制"每篇最多 1 个"
      
      ---
      
      ## 模板 A:工具推介型
      
      **适用场景**:新工具发布、工具评测、Skill 推广
      
      **核心生产逻辑**:功能清单 vs 痛点对照
      - 第 1 节:问题诊断——读者此刻在为什么发愁
      - 第 2 节:功能列表——这个工具具体能做什么
      - 第 3 节:使用指南——3 步或 5 步可执行流程
      - 第 4 节:对标/对比——和同类工具的差异
      
      ### Prompt 模板
      
      ```markdown
      你正在写一篇【工具推介型】运营文章。
      
      【参考章节】
      1. ## 为什么你需要这个工具
         - 建议包含 1 个用户具体场景
         - 建议包含 2-3 个常见误区或失败模式
         - 字数参考:200 字左右
      
      2. ## 它到底能做什么
         - 建议用 1 张功能清单表格
         - 每行功能 = 痛点 + 解决方案 + 实际收益
         - 字数参考:250 字左右
      
      3. ## 3 步上手:从打开到产出
         - 建议用编号列表(1/2/3 步骤)
         - 每步骤配 1 个具体输入示例
         - 字数参考:300 字左右
      
      4. ## 和同类工具相比强在哪
         - 建议用 1 张对比表(维度 ≥ 3)
         - 建议写出本工具不适用场景
         - 字数参考:250 字左右
      
      【输出风格参考】
      - 语气:直白坦率 + 经验分享感
      - 段落:每段不超过 5 行
      - 数字:每节至少 1 个数据/百分比/时间
      - 禁忌词:禁止"赋能、打法、闭环、对齐、沉淀、颗粒度、方法论前置"
      ```
      
      **调整建议**:
      - 短工具推介(300-500 字):只保留"为什么需要 + 3 步上手"两节即可
      - 工具 + 行业背景(1500+ 字):可加 1 节"行业现状"作为引言前置
      
      ---
      
      ## 模板 B:方法论型
      
      **适用场景**:系统化方法输出、SOP 拆解、流程优化
      
      **核心生产逻辑**:框架 + 步骤 + 标准
      - 第 1 节:场景分析(什么时候用这套方法)
      - 第 2 节:方法论框架(核心理念 + 适用范围)
      - 第 3 节:工具与渠道(怎么落地)
      - 第 4 节:评估标准(怎么判断做得好不好)
      - 第 5 节:执行清单(周一/周二/周三做什么)
      
      ### Prompt 模板
      
      ```markdown
      你正在写一篇【方法论型】运营文章。
      
      【参考章节】
      1. ## 什么时候用这套方法
         - 描述典型应用场景
         - 给出"信号清单"——出现这 3 个信号就该用
         - 字数参考:200 字左右
      
      2. ## 方法论框架
         - 建议含 1 个核心模型(3-5 步或 3-5 维)
         - 建议用 1 张框架表格或矩阵图
         - 字数参考:350 字左右
      
      3. ## 工具与渠道选择
         - 推荐 2-3 个具体工具/方法
         - 说明每个工具的"使用边界"
         - 字数参考:300 字左右
      
      4. ## 评估标准与红线
         - 建议含 1 张评估表(KPI + 阈值)
         - 红线:3-5 条"绝对不能做"的事
         - 字数参考:250 字左右
      
      5. ## 30 天执行清单(可选,方法论长文适用)
         - 按周划分
         - 每周 3-5 个具体动作
         - 字数参考:200 字左右
      
      【输出风格参考】
      - 语气:专业严谨 + 经验分享
      - 段落:每段不超过 5 行
      - 数字:每节至少 2 个可量化指标
      - 禁忌词:禁止"赋能、打法、闭环、对齐、沉淀、颗粒度、方法论前置"
      ```
      
      **调整建议**:
      - 极简方法论(500 字内):只保留"场景 + 框架"两节
      - 完整方法论(1500+ 字):保留全部 5 节
      - 短方法论:可省"30 天执行清单",用 1 段总结代替
      
      ---
      
      ## 模板 C:危机应对型
      
      **适用场景**:品牌危机、产品 bug、行业风波、负面舆情
      
      **核心生产逻辑**:时间线 + 决策树 + 损失控制
      - 第 1 节:风险识别(危机长什么样)
      - 第 2 节:分级分类(严重程度判断)
      - 第 3 节:响应流程(24h / 72h / 7 天)
      - 第 4 节:协同动作(PR/客服/法务/产品各做什么)
      
      ### Prompt 模板
      
      ```markdown
      你正在写一篇【危机应对型】运营文章。
      
      【参考章节】
      1. ## 危机长什么样:4 类早期信号
         - 建议用 1 张分类表(4 类信号 + 严重度)
         - 每类配 1 个真实案例(或脱敏案例)
         - 字数参考:250 字左右
      
      2. ## 3 级响应机制
         - 黄色(24h 内响应,影响 < 1000 用户)
         - 橙色(4h 内响应,影响 1000-10000 用户)
         - 红色(30min 内响应,影响 > 10000 用户 或 媒体介入)
         - 每级建议列出 5 项具体动作
         - 字数参考:350 字左右
      
      3. ## 跨部门协同:谁先动、怎么联动
         - 建议含 1 张 RACI 矩阵
         - 至少 4 个角色:PR / 客服 / 法务 / 产品
         - 字数参考:300 字左右
      
      4. ## 损失控制与复盘
         - 建议含 1 张复盘清单(事后 24h / 7 天 / 30 天动作)
         - 给出 3 条"绝对不能做"的事
         - 字数参考:250 字左右
      
      【输出风格参考】
      - 语气:直白坦率 + 专业警告
      - 段落:每段不超过 5 行
      - 数字:每节至少 1 个时间节点(X 分钟/X 小时/X 天)
      - 禁忌词:禁止"赋能、打法、闭环、对齐、沉淀、颗粒度、方法论前置"
      ```
      
      **调整建议**:
      - 单一危机案例(500-800 字):只保留"信号 + 响应"两节
      - 完整危机手册(2000+ 字):保留全部 4 节
      - 危机新闻类(300 字):用"信号 + 案例 + 1 句话建议"三段式
      
      ---
      
      ## 模板选择决策表
      
      | 选题特征 | 推荐模板 | 灵活度 |
      |---------|---------|--------|
      | 含"工具/Skill/产品"关键词 | A 工具推介型 | 可裁剪到 2 节 |
      | 含"方法/体系/流程/SOP"关键词 | B 方法论型 | 可扩到 6-7 节 |
      | 含"危机/处理/应对/舆情/紧急"关键词 | C 危机应对型 | 时间维度可弱化 |
      | 选题模糊不确定 | 优先 B(最通用) | 完全自由 |
      | **叙事/资讯类短文** | **不需要套模板** | 自由发挥 |
      
      ---
      
      ## 模板混用规则
      
      可在主模板基础上,引入其他模板的章节——**不再硬性限制数量**:
      
      - A + 危机:工具推介文中加"常见踩坑与避雷"(C 章节)
      - B + 工具:方法论文中加"推荐工具对比"(A 章节)
      - C + 工具:危机应对文中加"应急工具箱"(A 章节)
      - B + C:方法论文中加"红线警示"(C 章节)
      
      **原则**:混用服务于内容,不为"凑结构"而混用。
      
      ---
      
      ## 自检对照
      
      生成后,**建议**用 `validate_article.py` 验证:
      
      ```bash
      # 默认 soft 模式:所有阈值均为 warning
      python scripts/validate_article.py --input article.md
      
      # 自定义字数目标
      python scripts/validate_article.py --input article.md --target-length 1200
      
      # 严格模式(恢复硬 fail,仅在 CI 严格拦截时使用)
      python scripts/validate_article.py --input article.md --strict
      ```
      
      **软阈值**(超范围不直接 fail):
      - 标题 8-30 字
      - 章节 2-8 节
      - 表格 0-3 张(0 张也合规)
      - 字数 500-1500(目标 1000)
      - FAQ 0 或 2-4 个
      
      **核心原则**:
      - 模板是**生成时的脚手架**,不是**生成后的验收标准**
      - 校验脚本只提供"建议改进",不强制"必须修改"
      - Skill 的写作框架应跟随选题、读者、Skill 逻辑动态调整
      
    • core_workflow.md 11.9 KB
      # Core Workflow
      
      > 本文件从 SKILL.md 提取的核心执行流程,供 Agent 内部执行时引用。
      
      ---
      
      ## 一、写作框架
      
      ### 写作者身份
      
      独立撰稿人,专写新媒体工具类的深度推广文章。不是品牌小编,不是 SEO 写手——是"自己用过很多烂工具、踩过很多坑"的人。
      
      ### 8 条写作铁律(不可违反)
      
      1. 开头 3 句话内产生「这就是我」的感觉
      2. 全文至少有一个微型案例
      3. 每个段落不超过 5 行
      4. 功能不列清单(每说一个功能,必须附带一句「对你来说意味着什么」)
      5. 禁用行业黑话(赋能、抓手、闭环、底层逻辑、颗粒度、打法、心智、调性)
      6. 表格最多用 2 次
      7. 结尾不给总结(给读者一个画面或一个动作)
      8. 不用结束信号词(「最后」「综上所述」「总而言之」)
      
      ### 9 步文章结构(自然过渡,不套小标题)
      
      1. 痛点场景(200-300字)
      2. 产品一句话定位(50-100字)
      3. 跟竞品的本质区别(200-300字)
      4. 一个场景案例(150-200字)
      5. 核心能力展开(300-400字)
      6. 用户画像(200字)
      7. 上手路径(200字)
      8. FAQ(可选,150字)
      9. 结尾(100-150字)
      
      ### 语调规范
      
      像行业里干了 5 年的人跟新同事聊天,刻薄指向「烂工具」而非读者,共情方式是「我知道你经历了什么」。
      
      ---
      
      ## 二、模式自动推断规则
      
      按以下顺序判断输入类型,匹配则进入对应模式:
      
      1. 用户传入 `sourceUrl`(以 http/https 开头)→ **mode=link_rewrite**
      2. 用户传入 `skillFile` 路径(指向 .md 文件)→ **mode=skill_promo**
      3. 用户传入 `skillName`(字符串)→ **mode=skill_promo**
      4. 用户传入 `articleContent`(含 H2 标题的 markdown 文本 或 文件路径)→ **mode=add_images**
      5. 都不匹配 → 报错并提示用户提供三选一的输入
      
      ---
      
      ## 三、生成流程
      
      ### Step 1: 数据采集(按模式分支)
      
      #### 模式 A: link_rewrite
      ```bash
      python scripts/fetch_article.py --url "<sourceUrl>" --output article.json
      ```
      输出 JSON 含 `title / sections / images`。详见 [references/link-rewrite-guide.md](link-rewrite-guide.md)
      
      **视觉学习**(浅层 i2i 模式):
      - 不做深度视觉解构
      - 直接拿原图 URL 作为 `--reference-image` 透传给生图接口
      - 由生图模型自身完成风格迁移
      
      #### 模式 B: skill_promo
      1. **Skill 文件解析**:读取 skillFile 或下载 skillName 对应内容
      2. **目标站点抓取**:访问 targetSiteUrl,获取最新 Skills 列表与功能特性
      3. **内容融合校验**:将两者信息交叉比对,确保与产品实际能力对齐
      
      #### 模式 C: add_images
      - 解析用户提供的 markdown 正文
      - 章节切分 + 字数统计 + 锚点检测
      - 详见 Step 10 配图决策
      
      ### Step 2: 选题定位
      
      模式 A:从源文章提取核心论点作为新文章主题
      模式 B:从三个方向锁定(平台动态 / 用户痛点 / 行业困局)
      模式 C:使用用户提供的标题与主题
      
      选题须满足:高频重复性问题、有明确失败案例、有可量化成功指标。
      
      ### Step 3: 确定文章类型与读者画像
      
      根据选题自动匹配文章类型(工具推介型 / 方法论型 / 危机应对型),详见 [references/article-type-templates.md](article-type-templates.md)
      
      ### Step 4: 生成标题
      
      从公式中选择(建议):疑问式+方法论指向 / 否定+限制词 / 纠正认知+数字+承诺
      
      ### Step 5: 撰写引言
      
      四段式(场景→误区→认知→方案)适用于长文/方法论类。短文可以直接开场白或一段场景描述。
      
      ### Step 6: 动态生成正文章节
      
      按选题与素材**自然生成**,不预设结构;结构服务于内容。
      
      **软建议**(可参考,不强制):
      - 章节数 2-8 节,3-4 节为最常见
      - 表格**按需出现**:信息适合用表呈现就加,不适合就不加
      - 信息密度保持 1+ 个数据/案例/对比即可
      
      ### Step 7: 生成 FAQ
      
      按用户实际可能问的问题数量动态生成,2-4 个为常见区间,0 个也允许。
      
      答案为行动指引,而非理论解释。
      
      ### Step 8: 撰写结语
      
      结语应该自然,不为"凑结构"硬写。常见结语形态:
      - 一句金句收尾(短文、叙事类适用)
      - 一段行动指引(教程类适用)
      - 不写结语(纯资讯快讯)
      
      ### Step 9: 质量自检(机器化验证,必须执行)
      
      ```bash
      python scripts/validate_article.py --input article.md
      python scripts/validate_article.py --input article.md --target-length 1200
      python scripts/validate_article.py --input article.md --strict
      ```
      
      输出 `validation_report.json`,含 9 项检查。全部为 warning 级别,不直接 fail。
      
      ### Step 10: 生成配图(按模式分支)
      
      **统一脚本入口**:`python scripts/generate_image.py`
      
      #### 模式 A: link_rewrite
      ```bash
      python scripts/generate_image.py \
        --prompt "<基于新文章该章节的中文prompt>" \
        --reference-image "<原文章中的图片URL>" \
        --style reference \
        --image assets/section_1.jpg \
        --api-key "{apiKey}"
      ```
      
      **关键约束**:
      - 不强制叠加红狐风格(`--style reference` 或 `none`)
      - 原图作风格参照,新 prompt 控制内容
      - 配图数量与原文章保持一致
      - 配图位置与原文章保持一致
      
      #### 模式 B: skill_promo
      ```bash
      python scripts/generate_image.py \
        --prompt "<中文prompt>" \
        --image assets/skill标题.jpg \
        --style redfox \
        --api-key "{apiKey}"
      ```
      
      **关键约束**:
      - 强制红狐讲解员 + 美式复古报刊风(`--style redfox`)
      - 配图数量由 imageCount 参数控制(默认 3)
      - 配图位置按章节语义匹配
      
      #### 模式 C: add_images
      ```bash
      python scripts/decide_images.py --input article.md --output image_plan.json
      python scripts/generate_image.py --batch prompts.json --style redfox --api-key "{apiKey}"
      ```
      
      **关键约束**:
      - **生图点由脚本自动决策**,不是用户预设
      - 决策规则:H2 切分 + 字数密度(500-800字/张)+ 锚点词 + 标题视觉化关键词
      - 跳过 FAQ / 结语 / 引言(字数 < 300 强制跳过)
      - 全篇生图数上限由 `--max-images` 控制(默认 6)
      - 风格默认 redfox,可通过 `--style none` 关闭
      
      **详细决策规则**见 [references/image-decision-rules.md](image-decision-rules.md)
      
      ### Step 11: 组装输出
      
      1. 图片 URL 以 `![描述](URL)` 插入对应章节末尾
      2. 完整文章保存为 `{outputDir}/article.md`
      3. 对话中输出文章全文
      4. 追加反馈节点
      
      ---
      
      ## 四、写作风格规范
      
      ### 修辞手法
      
      | 手法 | 用途 | 示例 |
      |------|------|------|
      | 排比 | 堆砌问题 | "选错对标、抄错内容、追错热点……" |
      | 对比 | 表象 vs 本质 | "看起来是粉丝量差距,本质是内容结构差距" |
      | 转折递进 | 先认同再纠偏 | "看起来数据不错,但细看转化率一直在跌" |
      | 反问引导 | 引发思考 | "你有没有算过,每周找对标的时间值多少钱?" |
      
      ### 语气
      
      - 直白坦率:「别只看粉丝数」
      - 经验分享感:「更稳的做法是……」
      - 同理心对话:「很多创作者都遇到过……」
      - 用「你」不用「您」;短句为主,每段不超过 4 行
      
      ### 信息密度
      
      - 观点有数据/案例/对比支撑
      - 段落间信息递进,不重复
      - 每 400 字 1+ 个数据/动词开头的句子(叙事类不强制)
      
      ### 严格禁止
      
      | 类型 | 禁止内容 |
      |------|---------|
      | 禁忌词 | 赋能、抓手、打法、闭环、颗粒度、对齐、沉淀、方法论前置 |
      | 禁忌句式 | 「在当今XXX的时代」「众所周知」「不言而喻」 |
      | 禁忌内容 | 学术术语堆砌、空泛励志、图文已覆盖处的重复描述 |
      
      ---
      
      ## 五、图片生成规范
      
      ### 风格模式
      
      | 模式 | 适用场景 | 行为 |
      |------|---------|------|
      | `redfox`(默认) | 模式 B、C 默认 | 强制叠加红狐讲解员 + 美式复古报刊风 |
      | `reference` | 模式 A 默认 | 原 prompt 不变,参考图自带风格 |
      | `none` | 用户明确要求 | 原 prompt 不变,无任何风格修饰 |
      
      ### 红狐讲解员姿态-章节语义映射
      
      | 章节类型 | 狐狸姿态 |
      |----------|----------|
      | 封面/标题 | 欢迎姿态(站直挥手) |
      | 分析/洞察 | 前倾姿态(拿放大镜或教鞭) |
      | 思考/方法论 | 托腮姿态(沉思) |
      | 操作/工具 | 打字姿态(坐电脑前) |
      
      ### 核心原则
      
      - 图片是内容承载体,不是装饰
      - 每张图可独立替代对应文字说明
      - 禁止装饰性边框
      
      ---
      
      ## 六、质量检查清单
      
      ### 机器验证(必跑)
      
      ```bash
      python scripts/validate_article.py --input article.md
      ```
      
      ### LLM 主观判断
      
      - [ ] **开头 3 句话内产生「这就是我」**(铁律 1)
      - [ ] **全文至少有一个微型案例**(铁律 2)
      - [ ] **没有段落超过 5 行**(铁律 3)
      - [ ] **功能描述都附带「意味着什么」**(铁律 4)
      - [ ] **没有禁用词**(铁律 5)
      - [ ] **表格真的需要**(铁律 6)
      - [ ] **结尾不给总结**(铁律 7)
      - [ ] **没有结束信号词**(铁律 8)
      - [ ] 标题符合公式且含核心关键词(建议)
      - [ ] FAQ 答案为"行动指引"而非"理论解释"(如有 FAQ)
      - [ ] 每章节通过"问题→用法→好处"自检
      - [ ] 段落间信息递进,无断裂跳跃
      - [ ] 文章类型(工具/方法论/危机)与选题匹配
      - [ ] 面向读者(个人/团队)的表达侧重正确
      
      ### 模式特化检查
      
      #### 模式 A: link_rewrite
      - [ ] 新文章与源文章核心论点一致但角度不同
      - [ ] 配图数量、位置与源文章对齐
      - [ ] 配图风格通过 i2i 参照原图
      - [ ] 未使用红狐风格(除非用户指定)
      
      #### 模式 B: skill_promo
      - [ ] Skill 功能描述与目标网站最新信息对齐
      - [ ] 配图采用红狐风格
      - [ ] 无夸大、过时、虚构描述
      
      #### 模式 C: add_images
      - [ ] 生图点由 decide_images.py 决策,非人工指定
      - [ ] 配图数量在 1-6 张合理区间
      - [ ] 未在 FAQ / 结语 / 引言插图
      - [ ] 配图位置在 H2 章节切分点之后
      
      ### 图片合规
      
      - [ ] 每张图可独立替代文字说明
      - [ ] 风格模式与 mode 匹配
      - [ ] 无装饰性边框
      
      ---
      
      ## 七、思维方向速查
      
      ### 核心思维链
      
      1. 引言:场景 → 问题 → 方案
      2. 正文:问题 → 方法 → 工具 → 标准 → 执行
      3. FAQ:执行卡点
      4. 结语:认知升维
      
      ### 文章类型差异
      
      | 类型 | 正文侧重 | 核心输出 |
      |------|----------|----------|
      | 工具推介型 | 问题诊断 → 功能介绍 → 使用指南 | 工具使用手册 |
      | 方法论型 | 场景分析 → 方法 → 工具 → 执行清单 | 系统解决方案 |
      | 危机应对型 | 风险识别 → 分层分析 → 协同动作 | 应急响应方案 |
      
      ### 读者差异
      
      | 读者 | 表达侧重 |
      |------|----------|
      | 个体创作者 | 个人可执行方法、周计划、中腰部案例 |
      | 团队/企业 | 工具系统、协同流程、价值量化 |
      
      ---
      
      ## 八、机器化质检详细说明
      
      `scripts/validate_article.py` 把质量检查从人工 checklist 升级为可机械验证的脚本,输出 JSON 报告。
      
      **检查项**(全部为 warning 级别,不直接 fail):
      - 标题长度(8-30 字)
      - 章节数(2-8 节)
      - 表格数(0-3 张)
      - 字数(800-1500,link_rewrite 模式跳过)
      - FAQ 数量(2-4 个,0 也允许)
      - 资料来源(推荐存在)
      - 禁忌词(0 个)
      - 段落行数(每段 ≤ 5 行)
      - 信息密度(每 400 字 ≥ 1 数据/动词)
      
      ---
      
      ## 九、错误处理
      
      详见 [references/error-handling.md](error-handling.md)
      
      ### 模式特化降级
      
      | 场景 | 降级策略 |
      |------|----------|
      | 模式 A 源文章抓取失败 | 提示用户粘贴正文,回退到模式 C |
      | 模式 A 原图下载失败 | 跳过 i2i 参照,改用文生图(`--style redfox`) |
      | 模式 B Skill 文件解析失败 | 提示用户确认路径或名称 |
      | 模式 C 正文过短(< 500 字) | 不插图,提示「文章偏短,建议扩充后再配图」 |
      | API Key 缺失 | 跳过图片生成,输出纯文本文章 |
      | 配图生成失败 | 重试 1 次,仍失败则该位置用文字描述占位 |
      
    • error-handling.md 1.1 KB
      # 错误处理矩阵
      
      | 场景 | 处理方式 | 降级级别 |
      |------|---------|---------|
      | 源文章链接无法抓取 | 提示用户粘贴文章正文内容或更换链接;若仍无法获取则回退到模式 B | 流程降级 |
      | 源文章图片 URL 不可访问 | 对应配图回退到 `assets/` 资源图生成 | 配图降级 |
      | API 密钥未提供 | 提示用户设置环境变量 `REDFOX_API_KEY` 或直接输入密钥;若仍无则跳过图片生成,仅输出文章文本 | 配图跳过 |
      | 图片提交失败 | 在文章对应位置标注 `[配图生成失败:提交接口异常]`,继续输出文章 | 单图失败 |
      | 图片内容审核不通过 | 在文章对应位置标注 `[配图生成失败:内容审核不通过]`,尝试换 prompt 重试一次 | 单图失败 |
      | 轮询超时 | 在文章对应位置标注 `[配图生成中,请稍后手动查询 taskId: xxx]`,继续输出文章 | 单图超时 |
      
      ## 降级策略优先级
      
      ```
      源文章配图(最佳) → assets 资源图(次选) → 跳过配图仅文本(兜底)
      ```
      
      每级降级都会在输出中标注原因,确保用户知道发生了什么。
      
    • image-decision-rules.md 8.2 KB
      # 断章生图决策规则(image-decision-rules.md)
      
      > 本文档说明 `decide_images.py` 的完整决策逻辑、参数调优、边界 case 处理。
      > 当 `mode=add_images` 触发时,本规则集是生图点+数量的唯一权威来源。
      
      ---
      
      ## 一、设计目标
      
      为已有 markdown 正文**自动决定**:
      1. **生几张图**(count)
      2. **插在哪里**(after_section,N 表示插在第 N 节末尾)
      3. **为什么插**(reason,可解释性)
      
      原则:
      - **少而精**:默认 3-5 张,避免视觉过载
      - **贴合内容**:在视觉信号强的位置插图,不在文字已表达充分的位置插图
      - **可解释**:每个决策点都有明确 reason,调用方可在 prompt 中提示 LLM 调整
      
      ---
      
      ## 二、决策流程(伪代码)
      
      ```
      for each section in markdown.split_by_h2:
          if section.heading 包含 "FAQ/常见问题/结语/总结": 跳过
          if section.word_count < 300: 跳过(字数硬下限)
      
          score = 0
          score += section.word_count // 500          # 字数密度
          score += 2 if section 包含强锚点词          # 强信号
          score += 1 if section 包含弱锚点词          # 弱信号
          score += 1 if section.heading 含视觉化关键词 # 标题信号
          score += 1 if section 包含数据/列表/代码块   # 内容信号
      
          if score >= threshold AND count_so_far < MAX_IMAGES:
              plan.append({after_section: idx, reason: ...})
      
      return plan
      ```
      
      ---
      
      ## 三、五大决策信号
      
      ### 信号 1:字数密度(权重:中)
      
      **规则**:
      - 章节字数 ≥ 500 → 候选 +1 分
      - 章节字数 ≥ 800 → 候选 +1 分(累计 +2)
      - 章节字数 < 300 → 强制跳过(不插图)
      
      **理由**:
      - 字数少的章节信息密度低,配图边际收益低
      - 字数多的章节"文字疲劳"风险高,需要视觉缓冲
      
      **调优建议**:
      - 资讯类短文(1500 字以内)→ 把 `MIN_WORDS_HARD_LIMIT` 调低到 200
      - 深度长文(4000+ 字)→ 默认值即可
      - 命令行覆盖:`--min-words 500 --max-words 800`
      
      ### 信号 2:强锚点词(权重:高,必中)
      
      **触发词列表**:
      - "如下图" / "如上图" / "看下面这张图" / "看这张图"
      - "举个例子" / "来看个例子" / "来看个案例" / "举个栗子"
      - "如图所示" / "见图" / "看图" / "如图"
      - "接下来" / "来看" / "看这里"
      
      **规则**:该词出现后,必然是该位置或下一位置生图的强信号,分数 +2。
      
      **理由**:作者自己写出了"看图"类提示词,说明该位置有视觉需求,应当尊重。
      
      **注意**:
      - 锚点词必须在正文(H2 章节内),引言里的不算
      - 同一章节多个强锚点词只算 1 次(避免堆叠)
      
      ### 信号 3:弱锚点词(权重:中)
      
      **触发词列表**:
      - "比如" / "例如" / "想象一下" / "假设"
      - "对比" / "区别" / "差异" / "差距"
      - "流程" / "步骤" / "操作"
      
      **规则**:每出现 1 个弱锚点词,分数 +1。
      
      **理由**:这些词暗示"抽象概念可能需要具象化",但不是必中。
      
      ### 信号 4:标题视觉化关键词(权重:中)
      
      **H2 标题含以下词时**,整节倾向插图:
      - "案例" / "示例" / "图解"
      - "对比" / "数据" / "图表"
      - "流程" / "步骤" / "工具" / "方法" / "技巧"
      - "before" / "after" / "前后"
      
      **规则**:标题命中任一关键词,该节分数 +1。
      
      **理由**:作者在标题里就提示了"这一节需要视觉化"。
      
      ### 信号 5:内容形态信号(权重:低)
      
      **加分项**:
      - 章节内含 1+ 个表格 → +1 分
      - 章节内含 3+ 个列表项 → +1 分
      - 章节内含代码块 → +1 分(技术类文章)
      - 章节内含百分比/数字 ≥ 5 个 → +1 分
      
      **理由**:这些形态本身可读性一般,视觉化能增强理解。
      
      ---
      
      ## 四、强制规则(覆盖评分)
      
      ### 强制跳过(无论评分多高)
      
      章节标题命中以下任一关键词,**绝对不插图**:
      - "FAQ" / "常见问题" / "问答"
      - "结语" / "总结" / "写在最后" / "写在最后的话"
      - "引言" / "前言" / "写在前面"
      
      **理由**:这些是结构性章节,配图会破坏阅读节奏。
      
      ### 强制插图(无论评分多低)
      
      章节标题命中以下任一关键词,**必须插图**:
      - "案例" / "示例" / "图解"
      - "对比" / "前后对比"
      
      **理由**:这些标题本身就是视觉化的承诺。
      
      ---
      
      ## 五、输出格式
      
      `decide_images.py` 输出 JSON:
      
      ```json
      {
        "image_plan": [
          {
            "after_section": 2,
            "section_heading": "4个信号筛选法",
            "reason": "word_count=720 (>=500), 强锚点: 1 个 ('看下面这张图'), 标题含 '信号'",
            "suggested_prompt_hint": "4个信号评估框架的图解,4个信号分别是..."
          }
        ],
        "stats": {
          "total_sections": 6,
          "candidate_sections": 4,
          "selected_sections": 2,
          "total_word_count": 2103,
          "min_words": 500,
          "max_words": 800,
          "max_images": 6
        }
      }
      ```
      
      **字段说明**:
      - `after_section`: 章节索引(从 1 开始,1 = 引言后的第 1 个 H2 章节)
      - `reason`: 可读的解释,调用方可塞进 prompt 让 LLM 理解
      - `suggested_prompt_hint`: 基于章节标题 + 首段生成的简短提示,**不是完整 prompt**,调用方需自行补全
      
      ---
      
      ## 六、参数调优速查表
      
      | 场景 | MIN_WORDS | MAX_WORDS | MAX_IMAGES | 说明 |
      |------|-----------|-----------|------------|------|
      | **默认(推荐)** | 500 | 800 | 6 | 2000-3000 字文章,3-5 张图 |
      | **短文资讯类** | 300 | 500 | 4 | 1500 字以内文章 |
      | **深度长文** | 600 | 1000 | 8 | 4000+ 字深度文章 |
      | **方法论类** | 400 | 700 | 7 | 流程图多,可多配 |
      | **危机应对类** | 500 | 800 | 5 | 偏文字说明,少图 |
      
      命令行覆盖示例:
      ```bash
      # 短文模式
      python decide_images.py --input article.md --min-words 300 --max-words 500 --max-images 4
      
      # 长文模式
      python decide_images.py --input article.md --min-words 600 --max-words 1000 --max-images 8
      ```
      
      ---
      
      ## 七、边界 case 处理
      
      ### Case 1:全文章节都 < 300 字
      - **现象**:0 张图计划
      - **原因**:所有章节都不满足最低字数
      - **处理**:调用方应适当调低 `MIN_WORDS_HARD_LIMIT`,或允许用户手动指定 1-2 张图
      
      ### Case 2:H2 章节数为 0(无章节)
      - **现象**:脚本把所有内容当成 1 个 section,可能生 0 张图
      - **处理**:调用方应先在 markdown 里补 H2 切分(至少引言 + 2-3 节正文)
      
      ### Case 3:H2 标题全是 FAQ/结语
      - **现象**:全部跳过,0 张图
      - **原因**:这些是强制跳过章节
      - **处理**:这是预期行为,调用方应在 Step 2 数据采集中校验文章结构
      
      ### Case 4:单章节字数 > 2000
      - **现象**:可能生 2-3 张图都堆在同一节
      - **处理**:脚本当前不支持章节内细分生图点。如果有需要,调用方可在 markdown 里手动加 H3 切分
      
      ### Case 5:同一位置多个强锚点词堆叠
      - **现象**:分数虚高
      - **处理**:脚本内部对强锚点词去重(同一章节最多 +2 分)
      
      ### Case 6:标题含英文(如 "before/after")
      - **现象**:大小写敏感,可能漏检
      - **处理**:脚本已做小写化匹配,**仅识别小写**。如有大写习惯,可在 SKILL.md 里加约定
      
      ---
      
      ## 八、与 generate_image.py 的衔接
      
      `decide_images.py` 的输出**不是**生图 prompt,而是**生图点决策**。完整流程:
      
      ```bash
      # Step 1: 决策生图点
      python decide_images.py --input article.md --output image_plan.json
      
      # Step 2: LLM 根据 image_plan.json + 原文,生成每个图位的 prompt
      #         (脚本不替 LLM 做 prompt 创作)
      
      # Step 3: 批量生图
      python generate_image.py --batch prompts.json --style redfox --api-key "{apiKey}"
      ```
      
      **为什么不让脚本生成完整 prompt?**
      - prompt 创作需要语义理解("这个图应该表达什么")
      - 这是 LLM 擅长的,不要让 Python 越俎代庖
      - 脚本只提供**决策依据**和**简短 hint**,让 LLM 创作更高效
      
      ---
      
      ## 九、未来扩展方向
      
      - **章节内多生图点**:当前一个章节最多 1 张,未来支持在章节内按段落细分
      - **风格化 hint**:根据 mode 决定 `suggested_prompt_hint` 的风格(redfox / reference / none)
      - **L2 缓存**:同一文章多次决策时,可基于 hash 缓存
      - **可视化报告**:把决策结果以 HTML 报告呈现,标注在原文高亮位置
      
    • link-rewrite-guide.md 9.6 KB
      # 链接仿写指南(link-rewrite-guide.md)
      
      > 本文档说明 `mode=link_rewrite` 的完整工作流:链接抓取 → 风格学习 → 仿写 → 配图。
      > 适用于"用户提供任意文章 URL → 生成同风格新文章"的场景。
      
      ---
      
      ## 一、设计目标
      
      为任意文章链接生成**风格对得上、配图位置对齐、内容不重复**的新文章。
      
      **核心约束**:
      - **风格对得上**:新文章的视觉风格与原文一致
      - **配图位置对齐**:新文章在多少节、配几张图、图位在哪儿,与原文大致对齐
      - **内容不重复**:核心论点可以学,但具体表述、数据、案例必须重新生成
      - **不强制红狐风格**:原图是啥风格,新图就啥风格(除非用户明确指定)
      
      ---
      
      ## 二、完整工作流
      
      ```
      Step 1: 用户提供 sourceUrl
         ↓
      Step 2: 抓取原文 + 图片
         python fetch_article.py --url "<sourceUrl>" --output article.json
         ↓
      Step 3: 解析 JSON {title, sections, images}
         - title: 原文标题
         - sections: 原文章节(按 H2/H3 切分)
         - images: 原图 URL 列表(按出现顺序)
         ↓
      Step 4: LLM 学习原文风格特征
         - 标题风格(数字型/疑问型/陈述型)
         - 开篇结构(场景引入/数据冲击/反常识)
         - 段落节奏(短句多/长句多)
         - 视觉风格(基于 images 推断)
         ↓
      Step 5: LLM 仿写新文章
         - 保留:核心论点 / 章节结构 / 配图位置
         - 重新生成:具体表述 / 数据 / 案例 / 标题文案
         ↓
      Step 6: 生图(沿用原图风格)
         python generate_image.py --prompt "..." --reference-image "<原图URL>" --style reference --api-key "..."
         ↓
      Step 7: 质量自检(validate_article.py)
         ↓
      Step 8: 输出新文章
      ```
      
      ---
      
      ## 三、fetch_article.py 输出格式
      
      ```json
      {
        "title": "原文标题",
        "sections": [
          {
            "heading": "章节标题",
            "level": 2,
            "text": "章节正文(去除 HTML 标签)",
            "word_count": 320
          }
        ],
        "images": [
          "https://example.com/img/cover.jpg",
          "https://example.com/img/figure1.png"
        ],
        "raw_html": "<!doctype html>...(可选,用于 fallback)",
        "metadata": {
          "source_url": "https://...",
          "fetch_time": "2026-07-03T10:00:00",
          "content_type": "text/html",
          "image_count": 3,
          "section_count": 5
        }
      }
      ```
      
      **字段说明**:
      - `title`: 用于 LLM 学习标题风格
      - `sections`: 章节结构是仿写的核心依据
      - `images`: 配图位置 + 风格参照
      - `metadata.fetch_time`: 用于决策是否需要重新抓取(缓存策略)
      - `metadata.image_count`: 用于校验配图对齐
      
      ---
      
      ## 四、风格学习(浅层 i2i 模式)
      
      ### 为什么选浅层 i2i 而不是深度视觉解构?
      
      | 方案 | 改造成本 | 效果 | 适用场景 |
      |------|---------|------|---------|
      | **浅层 i2i(当前)** | 低 | 保留视觉 DNA,但描述文本完全自由 | 大多数场景(推荐) |
      | 中层视觉解析 | 中 | 提取出"复古/扁平/插画"等描述符 | API 不支持 i2i 时 |
      | 深度视觉解构 | 高 | 配色/构图/IP 全解构,再生成 | 专业设计需求 |
      
      **当前默认走浅层**:
      - 直接把原图 URL 作为 `--reference-image` 透传给生图模型
      - 让生图模型自己完成"看图学风格"
      - 优点:调用简单、风格一致、改造成本低
      
      ### 浅层 i2i 的关键参数
      
      ```bash
      python generate_image.py \
        --prompt "新图描述(中文)" \
        --reference-image "<原图 URL>" \
        --style reference \
        --api-key "{apiKey}"
      ```
      
      - `--style reference`: 不叠加红狐风格,让原图自带风格生效
      - `--reference-image`: 接受 URL(自动下载并上传 OSS)或本地路径
      - `--size`: 默认 `16:9`,可改为任意宽高比(1:1 / 3:2 / 4:3 / 9:16 等)
      - `--resolution`: 分辨率档位 `1k` / `2k` / `4k`,默认 `2k`
      
      > ⚠️ `--fidelity` 在新接口(`gptImage2Submit`)已弃用,传入会被忽略并提示。新接口依靠 `referenceImages` 本身完成风格迁移,不需额外的保真度参数。
      
      **降级策略**:
      - 如果 i2i 失败 → 改用 text-to-image(去掉 `--reference-image`)
      - 如果 URL 下载失败 → 改用 text-to-image + 文字描述风格
      
      ---
      
      ## 五、仿写注意事项
      
      ### 必须保留的(结构层)
      
      - 章节数量(3-6 节)
      - 章节顺序(先讲什么后讲什么)
      - 配图位置(在哪些章节末尾配图)
      - 配图数量
      
      ### 必须重新生成的(内容层)
      
      - 标题文案(不能与原文完全相同)
      - 具体数据(数字、百分比、日期)
      - 案例(人名、公司名、产品名)
      - 引用语(原文的金句不能直接抄)
      - 配图内容(基于新文章主题重新生成)
      
      ### 风格学习的具体维度
      
      LLM 在仿写时需学习的风格特征:
      
      | 维度 | 怎么学 | 怎么用 |
      |------|-------|--------|
      | 标题风格 | 统计原文章节标题的句式 | 新章节标题用相似句式 |
      | 段落长度 | 统计正文章节的字数 | 新文章保持相近字数 |
      | 开篇结构 | 看引言/第一节怎么写 | 新文章用同结构开篇 |
      | 语气 | 读 3-5 段正文 | 新文章保持同语气 |
      | 视觉 | 读 images(浅层 i2i) | 生图用同视觉风格 |
      
      ### 反例:什么是"伪仿写"
      
      不叫仿写的几种情况:
      - ❌ 标题一字不差改一个字
      - ❌ 段落照抄,只换几个近义词
      - ❌ 数据照搬,只换公司名
      - ❌ 案例照抄,背景换个行业
      
      这些都是"抄袭"不是"仿写",**必须明确禁止**。
      
      ---
      
      ## 六、常见 case 处理
      
      ### Case 1:原文带反爬机制(403/429)
      
      **现象**:`fetch_article.py` 返回 `{"error": "HTTP 403"}`
      
      **处理**:
      - 提示用户:「该网站有反爬机制,请手动复制文章正文给我」
      - 用户粘贴正文后,自动降级到 `mode=add_images` 流程
      - 风格学习改用"用户提供原文示例"(让用户额外贴 1-2 段原网站其他文章作风格参考)
      
      ### Case 2:原文是 SPA / JS 渲染
      
      **现象**:`fetch_article.py` 抓到的 HTML 是空壳,正文在 JS 里
      
      **处理**:
      - 检测 `raw_html` 里 `<script>` 占 60%+ → 标记为 SPA
      - 提示用户:「该网站是动态渲染,请手动复制正文」
      - 降级到 `mode=add_images`
      
      ### Case 3:原文含大量图片(10+ 张)
      
      **现象**:`images` 数组很长
      
      **处理**:
      - 配图策略改为"对齐比例":原图 10 张 → 新文 5-7 张(**不必 1:1 对齐**)
      - 选择标准:保留有"信息含量"的图(数据图、流程图、对比图),跳过装饰图(表情包、背景图)
      
      ### Case 4:原文是英文
      
      **现象**:抓到的正文是英文
      
      **处理**:
      - 当前默认输出中文新文章(v2 默认)
      - 如果用户要求"保持英文" → 在 prompt 里明示
      - 风格学习维度相同(标题、段落、语气、视觉)
      
      ### Case 5:原文配图是动图/GIF
      
      **现象**:`images` 里含 `.gif`
      
      **处理**:
      - i2i 参照对 GIF 不友好(生图模型只取首帧)
      - 提示用户:「检测到 GIF 配图,建议改为静态图参照」
      - 降级为 text-to-image + 文字描述风格
      
      ### Case 6:原文结构是问答型(Q&A)
      
      **现象**:章节不是 H2,而是 Q1/Q2/Q3 列表
      
      **处理**:
      - `fetch_article.py` 检测到后,把每个 Q 当成 1 个 section
      - 仿写时保留问答结构,但 Q 和 A 都重新生成
      - 配图决策走"加图片断章"逻辑(每个 Q&A 配 1 张示意图)
      
      ---
      
      ## 七、参数调优
      
      ### `--style` 选择
      
      | 模式 | 行为 | 适用 |
      |------|------|------|
      | `reference`(默认) | 透传原图 i2i 参照 | 大多数场景 |
      | `redfox` | 强制叠加红狐风格 | 用户明确要红狐 |
      | `none` | 不加任何风格修饰 | 用户要求纯文字描述生图 |
      
      ### `--fidelity` 选择
      
      > ⚠️ 新接口 `gptImage2Submit` 已不再支持 `inputFidelity` 参数,传入会被脚本忽略并提示。
      > 新接口的风格迁移能力完全依靠 `referenceImages`,无需额外的保真度控制。
      
      | 模式 | 行为 | 适用 |
      |------|------|------|
      | (已弃用)`high` | 新接口忽略,风格迁移强度由模型自行判断 | - |
      | (已弃用)`low` | 新接口忽略,风格迁移强度由模型自行判断 | - |
      
      ### `--max-images` 上限
      
      `fetch_article.py` 输出的 `image_count` 决定默认上限:
      - 原文 < 3 张图 → 新文 1-3 张
      - 原文 3-6 张图 → 新文 3-5 张
      - 原文 6+ 张图 → 新文 4-7 张
      
      可在 prompt 里手动覆盖。
      
      ---
      
      ## 八、降级策略矩阵
      
      | 失败点 | 降级方案 | 用户感知 |
      |--------|---------|---------|
      | URL 抓取失败(403/超时) | 提示用户手动复制正文 | 中断并要求介入 |
      | 抓取到空 HTML(SPA) | 同上 | 中断并要求介入 |
      | 图片 URL 失效 | text-to-image 替代 | 视觉风格可能偏差 |
      | i2i 调用失败 | text-to-image 替代 | 同上 |
      | 生图 API 限流 | 延迟重试 3 次 | 等待几秒 |
      | 仿写 prompt 被 LLM 拒绝 | 简化 prompt,去掉敏感词 | 重试一次 |
      | validate_article.py 多次 fail | 提示用户手动调整 | 输出部分合规文章 |
      
      ---
      
      ## 九、与现有 Skill 的衔接
      
      `mode=link_rewrite` 完成后,可与 `mode=skill_promo` 组合:
      - 用 `link_rewrite` 仿写一篇对标文章作为草稿
      - 把草稿作为 `skill_promo` 的输入,让 LLM 改写成"红狐风格 Skill 运营文"
      - 这样可同时满足"对标风格"和"品牌风格"
      
      ---
      
      ## 十、未来扩展方向
      
      - **多链接融合仿写**:用户提供 3-5 篇文章 URL,融合各自风格生成"集大成"新文
      - **风格分类器**:自动识别原文风格类型(学术/资讯/营销/技术),匹配对应的仿写 prompt
      - **风格量化指标**:提取原文章节长度方差、句长分布、用词频率等量化指标,仿写时对齐
      - **配图 prompt 自动生成**:从原图 URL + 章节标题,LLM 自动生成完整生图 prompt(替代人工写)
      
    • persona-matrix.md 6 KB
      # 语气 × 读者 × IP 三维配置矩阵
      
      > 解决"风格与 IP 人设冲突"问题。
      > 三个维度正交:用户调用时只需选 3 个标签,框架自动装配。
      
      ---
      
      ## 维度 1:语气(tone)
      
      | 标签 | 适合场景 | 语言特征 |
      |------|---------|---------|
      | `direct`(直白坦率,默认) | 运营干货、工具测评、踩坑警示 | "别只看粉丝数""这个方法不适用于" |
      | `professional`(专业严谨) | 方法论、行业分析、深度报告 | "从数据分布看""结合 X 指标判断" |
      | `casual`(轻松对话) | 入门科普、个人故事、随笔 | "我有个朋友""说真的""其实没那么玄" |
      
      ## 维度 2:读者(audience)
      
      | 标签 | 表达侧重 | 示例句 |
      |------|---------|--------|
      | `individual`(个体创作者,默认) | 个人可执行、周计划、案例驱动 | "你下周可以这样安排" |
      | `team`(团队/企业) | 工具系统、协同流程、价值量化 | "建议团队每周同步一次" |
      
      ## 维度 3:视觉 IP(visualPersona)
      
      | 标签 | 风格 | 适用场景 |
      |------|------|---------|
      | `redfox`(红狐讲解员,默认) | 红色狐狸 + 美式复古报刊 + 圆框眼镜 | 亲切科普、入门向 |
      | `analyst`(严肃分析师) | 深色西装 + 商务报表 + 数据可视化 | 深度分析、行业研究 |
      | `none`(无 IP) | 纯文字/纯图,无人物 | 资讯类、官方公告 |
      
      ---
      
      ## 18 种组合的完整配置
      
      ### direct × individual × redfox(默认组合)
      - **语气**:直白坦率 + 经验分享感
      - **读者**:个人可执行方法、周计划、中腰部账号案例
      - **IP**:红狐讲解员,姿态随章节切换(封面=欢迎/分析=前倾/方法论=托腮/操作=打字)
      - **典型文章**:找对标账号、运营工具推荐、入门指南
      
      ### direct × individual × analyst
      - **语气**:直白坦率
      - **读者**:个体创作者
      - **IP**:无人物,改为数据仪表盘/流程图/对比表
      - **典型文章**:硬核拆解、对比测评、行业揭秘
      
      ### direct × individual × none
      - **语气**:直白坦率
      - **读者**:个体创作者
      - **IP**:无视觉 IP,纯文字+表格
      - **典型文章**:纯干货清单、工具清单
      
      ### direct × team × redfox
      - **语气**:直白坦率
      - **读者**:团队/企业
      - **IP**:红狐讲解员
      - **典型文章**:团队协作指南、内部 SOP
      
      ### direct × team × analyst
      - **语气**:直白坦率
      - **读者**:团队/企业
      - **IP**:数据仪表盘
      - **典型文章**:竞品分析、ROI 评估
      
      ### direct × team × none
      - **语气**:直白坦率
      - **读者**:团队/企业
      - **IP**:无
      - **典型文章**:内部备忘录、流程文档
      
      ### professional × individual × redfox
      - **语气**:专业严谨
      - **读者**:个人创作者
      - **IP**:红狐讲解员(亲和力缓和专业感)
      - **典型文章**:深度方法论个人版、行业研究入门
      
      ### professional × individual × analyst
      - **语气**:专业严谨
      - **读者**:个人创作者
      - **IP**:数据可视化
      - **典型文章**:硬核研究、数据解读
      
      ### professional × individual × none
      - **语气**:专业严谨
      - **读者**:个人创作者
      - **IP**:无
      - **典型文章**:纯学术风干货
      
      ### professional × team × redfox
      - **语气**:专业严谨
      - **读者**:团队/企业
      - **IP**:红狐讲解员
      - **典型文章**:企业内训、团队白皮书
      
      ### professional × team × analyst
      - **语气**:专业严谨
      - **读者**:团队/企业
      - **IP**:数据仪表盘
      - **典型文章**:行业报告、商业分析(最常用)
      
      ### professional × team × none
      - **语气**:专业严谨
      - **读者**:团队/企业
      - **IP**:无
      - **典型文章**:白皮书、官方报告
      
      ### casual × individual × redfox
      - **语气**:轻松对话
      - **读者**:个人创作者
      - **IP**:红狐讲解员
      - **典型文章**:随笔、个人故事、入门科普
      
      ### casual × individual × analyst
      - **语气**:轻松对话
      - **读者**:个人创作者
      - **IP**:数据仪表盘(轻松语言 + 严肃图表的张力)
      - **典型文章**:趣味数据解读、轻松测评
      
      ### casual × individual × none
      - **语气**:轻松对话
      - **读者**:个人创作者
      - **IP**:无
      - **典型文章**:博客随笔、社交媒体长文
      
      ### casual × team × redfox
      - **语气**:轻松对话
      - **读者**:团队/企业
      - **IP**:红狐讲解员
      - **典型文章**:团队文化、内部沟通
      
      ### casual × team × analyst
      - **语气**:轻松对话
      - **读者**:团队/企业
      - **IP**:数据仪表盘
      - **典型文章**:轻松向行业分析
      
      ### casual × team × none
      - **语气**:轻松对话
      - **读者**:团队/企业
      - **IP**:无
      - **典型文章**:团队通讯、轻松公告
      
      ---
      
      ## 使用方式
      
      ### 显式指定(推荐)
      
      ```bash
      # 链接仿写
      python scripts/generate_image.py \
        --prompt "..." \
        --reference-image "https://xxx.com/img.jpg" \
        --style reference \
        --persona direct-individual-analyst \
        --api-key "ak_xxx"
      ```
      
      ### 隐式默认
      
      未指定时使用 `direct-individual-redfox`(默认组合)。
      
      ### 切换 IP 风格时的注意事项
      
      - `redfox → analyst`:去掉狐狸描述,改为"商务感/数据仪表盘/极简线条"
      - `analyst → redfox`:追加红狐讲解员姿态描述
      - `xxx → none`:去掉所有 IP 描述,prompt 保持原始
      
      风格切换由 `generate_image.py` 的 `--style` 参数控制(已实现):
      - `--style redfox`:自动追加红狐风格
      - `--style reference`:原 prompt 不变(参考图自带)
      - `--style none`:原 prompt 不变(无风格)
      
      ---
      
      ## 与 tonePreference 参数的兼容性
      
      旧的 `tonePreference` 参数保留作为简写:
      - `tonePreference=直白坦率` → `direct`
      - `tonePreference=专业严谨` → `professional`
      - `tonePreference=轻松对话` → `casual`
      
      旧的 `targetAudience` 参数保留作为简写:
      - `targetAudience=个体创作者` → `individual`
      - `targetAudience=团队企业` → `team`
      
      新参数优先级:显式三标签组合 > 旧的 tonePreference/targetAudience > 默认值。
      
    • prompt-templates.md 10.4 KB
      # 配图 Prompt 模板与语义映射
      
      ## 三种可选视觉风格
      
      生成脚本支持 `--style` 参数选择风格,或使用 `--style random` 随机选取(**同批次/同篇文章内所有图片自动保持一致风格**)。
      
      | 风格名称 | --style 值 | 视觉特征 | 底色 | 主角 | 氛围 |
      |----------|-----------|---------|------|------|------|
      | **美式复古报刊风** | `redfox`(默认) | 复古报刊条漫、胶片颗粒、严格三栏网格 | 暖米淡黄色 | 红色狐狸讲解员 | 专业、经典、稳重 |
      | **现代波普漫画风** | `popcomic` | 波普网点纹理、多角色互动场景、浮动UI图标 | 高饱和亮黄色 | 干练讲解员卡通人物 + 困惑从业者群 | 活泼、年轻、动感 |
      | **亮黄信息图漫画风** | `yellowcomic` | 斜纹底、分类表格中心、操作类图标装饰 | 亮黄色斜纹 | 自信讲解员手持文档 + 多人分工场景 | 流程感强、清晰、实用 |
      
      ---
      
      ## 风格 A — redfox:美式复古报刊暖米黄底(默认)
      
      ```
      横向宽幅多格美式复古报刊科普条漫信息长图,90年代美式商业卡通画风,粗重清晰黑色轮廓线,纯色平涂高饱和明快马卡龙色块,复古印刷网点纸半调纹理,整体暖米淡黄色底色,柔和浅平阴影,2D矢量商业插画,8K高清印刷质感;
      固定全局版式:
      1. 画面最顶端:红色褶皱绸缎飘带横幅,居中大号加粗中文主标题;
      2. 画面上半区左右分镜:左侧苦恼迷茫普通从业者卡通人物,右侧一只可爱的红色拟人化狐狸讲解员(红色毛皮、白色胸腹、大尾巴,穿海军蓝小马甲,戴圆框眼镜),中间白色圆角对话气泡,中间橙色箭头衔接两者;
      3. 画面下半区三等分并列栏目,每栏前置黄色香蕉造型图标+黄色加粗板块小标题,每栏内置多张小场景卡通插画+简短解说气泡文字;
      4. 全图低透明度背景装饰:折线增长走势图、电脑科技面板、扁平化行业小图标、指引箭头,不遮挡主体内容;
      统一约束:分栏分割线清晰,人物五官规整无畸形,所有中文文字工整清晰无扭曲错乱,纯商业科普信息海报;禁止写实摄影、3D建模、厚重光影、杂乱线条、水印、模糊字体、装饰性边框。
      ```
      
      ## 章节专属内容模板
      
      在通用前缀后,按以下模板填入章节专属内容:
      
      ```
      主题:{章节核心主题}
      顶部红色褶皱绸缎飘带横幅,居中超大加粗中文标题:{章节主标题}
      
      上半区左右分镜:
      左侧卡通从业者人物,神情困惑,对话气泡文案:{章节读者痛点/困惑}
      右侧红色狐狸讲解员(红色毛皮、海军蓝马甲、圆框眼镜),{狐狸姿态根据章节语义动态变化},对话气泡文案:{章节核心结论}
      两人中间橙色引导箭头,形成困惑提问→狐狸解答的视觉逻辑。
      
      下半区均等三栏并排布局:
      第一栏,前置黄色香蕉图标,黄色加粗小标题:{模块1名称}
      栏内卡通小场景插画:{模块1场景1、场景2、场景3}
      配套解说短句:{模块1核心释义}
      
      第二栏,前置黄色香蕉图标,黄色加粗小标题:{模块2名称}
      栏内卡通小场景插画:{模块2场景1、场景2、场景3}
      配套解说短句:{模块2核心释义}
      
      第三栏,前置黄色香蕉图标,黄色加粗小标题:{模块3名称}
      栏内卡通小场景插画:{模块3场景1、场景2、场景3}
      配套解说短句:{模块3核心释义}
      
      背景低透明度装饰:折线数据走势图、电子屏幕、{行业专属简约图标}、指示箭头,不抢夺主体视觉。
      画面最底部横幅小字标语:{章节总结slogan}
      
      统一规范:分栏边界清晰,人物五官规整无畸变,全图中文工整可读无扭曲乱码,完整行业洞察可视化商业海报,拒绝写实、3D、厚重阴影、杂乱线条、水印、模糊字体、装饰性边框。
      负面提示词:扭曲文字、模糊字体、文字乱码、重复文字、变形五官、畸形人脸、杂乱线条、真人照片、3D渲染、暗沉阴影、水印、多余杂物、分栏破碎、油画质感、写实人像
      ```
      
      ## 语义类型 → 参考图 & 狐狸姿态映射
      
      | 语义类型 | 参考图 | 狐狸姿态/表情 |
      |----------|--------|----------------|
      | 封面/概述(文章首图、总览引言) | `assets/skill标题.jpg` | 站直身体,一只手挥手欢迎,另一只手指向概览展板,表情开朗 |
      | 分析/洞察(背景分析、问题拆解、对比评测、趋势判断) | `assets/02.jpg` | 身体前倾,一只手拿放大镜或教鞭指向分析展板,表情专注 |
      | 思考/方法论(策略推演、框架设计、方法论阐述) | `assets/02.jpg` | 托腮沉思姿态,眉头微蹙作深度思考状,身旁展板呈现思维导图或逻辑框架 |
      | 操作/工具(工具使用、步骤指南、执行计划、分类整理) | `assets/03.jpg` | 坐在电脑前打字或站在白板前书写,表情自信 |
      
      ## 内容提炼规则(每章节执行)
      
      | 提炼项 | 来源 | 填入位置 |
      |--------|------|----------|
      | 章节主标题 | 章节标题改写(10字以内,名词短语) | 顶部红飘带横幅 |
      | 读者痛点 | 章节开篇读者困惑/问题 | 左侧人物对话气泡 |
      | 核心结论 | 章节中心论点 | 右侧狐狸讲解员对话气泡 |
      | 三大模块 | 章节内容归纳为3个平行核心点 | 下半区三栏标题 |
      | 各栏场景 | 每个模块匹配3~4个贴合内容的卡通场景描述 | 各栏插画说明 |
      | 底部 slogan | 章节一句话总结 | 底部横幅 |
      
      ## 资源图选择规则
      
      ### 模式 A:源文章图片优先
      1. 根据新文章章节语义类型,从 `sourceImages` 中找最匹配的原图作为参考图
      2. 若源图片 URL 不可访问,回退到 `assets/` 资源图
      3. 源图片数量不足时,剩余配图使用 `assets/` 资源图补充
      
      ### 模式 B:assets 资源图
      根据章节语义类型从映射表中选择最匹配的资源图。跨类型章节优先匹配主要内容倾向。
      
      | 资源图文件 | 风格特征 | 适用章节类型 |
      |------------|----------|----------------|
      | `assets/skill标题.jpg` | 标题横幅型,大字标题+热点事件时间线+问题→解决方案引导 | 封面/概述类 |
      | `assets/02.jpg` | 问题分析型漫画,痛点→原因→解决方案叙事流,对比分镜布局 | 分析/洞察类、思考/方法论类 |
      | `assets/03.jpg` | 流程分类型,分类表格+多角色分工+操作步骤引导 | 操作/工具类 |
      
      ---
      
      ## 风格 B — popcomic:现代波普漫画亮黄底(基于 assets/02.jpg)
      
      **视觉参考**:`assets/02.jpg`
      
      ```
      现代波普漫画信息图插画风格,高饱和亮黄色底色配波普网点纹理,
      粗黑轮廓线 + 扁平高饱和色块填充;
      
      固定全局版式:
      1. 画面最顶端:圆角矩形横幅(非飘带),居中大号加粗中文主标题;
      2. 画面中心区域:一位干练的讲解员卡通人物(职业装/衬衫,表情根据章节语义动态变化),
         左侧 1-2 位困惑的从业者卡通人物(带问号和对话气泡表达痛点),
         右侧可有 1-2 位角色互动;
      3. 周围浮动 UI 装饰图标(图表、邮件、齿轮、搜索框、对话框等);
      4. 人物间白色圆角对话气泡承载中文文案;
      5. 动态活泼构图(非严格网格),2D 矢量商业插画,8K 高清;
      
      统一约束:人物五官规整无畸形,所有中文文字工整清晰无扭曲错乱;
      禁止写实摄影、3D 渲染、暗沉阴影、杂乱线条、水印、模糊字体、文字乱码。
      ```
      
      ### 章节专属内容填入(风格 B)
      
      ```
      主题:{章节核心主题}
      顶部圆角矩形横幅大号加粗中文标题:{章节主标题}
      
      画面中心:
      讲解员角色 {姿态:封面=挥手欢迎 / 分析=前倾拿资料指点 / 方法论=托腮思考 / 操作=自信展示},
      对话气泡文案:{章节核心结论}
      
      左侧困惑从业者群:
      对话气泡文案:{章节读者痛点/困惑},可配问号图标
      
      右侧互动区(可选):
      {补充场景或第二组角色互动}
      
      周围浮动装饰:{行业相关UI图标 3-5 个}
      
      底部标语:{章节总结slogan}
      ```
      
      ---
      
      ## 风格 C — yellowcomic:亮黄信息图漫画斜纹底(基于 assets/03.jpg)
      
      **视觉参考**:`assets/03.jpg`
      
      ```
      亮黄色斜纹底色现代信息图漫画插画风格,
      粗重清晰黑色轮廓线,纯色平涂高饱和马卡龙色块;
      
      固定全局版式:
      1. 画面最顶端:圆角矩形横幅,居中大号加粗中文主标题;
      2. 画面中心:一位自信的讲解员手持分类表格或文档(职业装,表情从容),
         左侧困惑角色群带问号气泡(表达痛点),
         右侧角色可互动(表达解决方案或行动);
      3. 周围散布操作类图标装饰(齿轮、文档、聊天框、折线图、对勾等);
      4. 人物对话气泡承载中文文案;
      5. 流程感和分工感强,2D 矢量商业插画,8K 高清;
      
      统一约束:人物五官规整无畸形,所有中文文字工整清晰无扭曲错乱;
      禁止写实摄影、3D 渲染、暗沉阴影、杂乱线条、水印、模糊字体、文字乱码。
      ```
      
      ### 章节专属内容填入(风格 C)
      
      ```
      主题:{章节核心主题}
      顶部圆角矩形横幅大号加粗中文标题:{章节主标题}
      
      画面中心:
      讲解员手持文档/表格 {姿态:封面=展示概览表 / 分析=指向数据 / 方法论=展示框架图 / 操作=展示步骤清单},
      对话气泡文案:{章节核心结论}
      
      左侧困惑角色群:
      对话气泡文案:{章节读者痛点/困惑}
      
      右侧互动区:
      {解决方案或下一步行动的角色场景}
      
      周围散布装饰:{行业相关操作类图标 3-5 个}
      
      底部标语:{章节总结slogan}
      ```
      
      ---
      
      ## 随机风格使用指南
      
      ### 单篇文章保持风格一致的方法
      
      **方法一:批量模式(推荐)**
      ```bash
      # --style random 会自动整批选一个风格,所有图片一致
      python generate_image.py --batch prompts.json --style random --api-key "$REDFOX_API_KEY"
      ```
      
      **方法二:单张模式手动控制**
      ```bash
      # 第一步:先随机选一个风格(在 bash 中执行一次)
      STYLE=$(python -c "import random; print(random.choice(['redfox','popcomic','yellowcomic']))")
      echo "本次使用风格: $STYLE"
      
      # 第二步:所有图都用同一个 $STYLE
      python generate_image.py --prompt "封面prompt..." --style $STYLE ...
      python generate_image.py --prompt "分析prompt..." --style $STYLE ...
      python generate_image.py --prompt "流程prompt..." --style $STYLE ...
      ```
      
    • writing-framework.md 9.6 KB
      # 内置写作框架 v3.0
      
      > 本文件定义 ops-article-generator 的"内置写作框架"——即 LLM 在生成文章时所遵循的身份、铁律、结构和语调。
      > 所有模式(link_rewrite / skill_promo / add_images)共用同一套框架,通过 `tonePreference` 和 `targetAudience` 参数微调。
      
      ---
      
      ## 一、写作者身份
      
      你是一个**独立撰稿人**,专写新媒体工具类的深度推广文章。
      
      你不是品牌小编,不是 SEO 写手——你是一个自己用过很多烂工具、踩过很多坑、对「好工具应该什么样」有强烈看法的人。
      
      你的读者是:每天被多个平台、多个工具、多个任务撕扯的新媒体运营和内容创作者。他们不缺信息,缺时间。他们读完你的文章,应该觉得**「你偷看了我的工作日记」**。
      
      ### 身份自检
      
      每次动笔前,用这三句话校准自己的语气:
      
      1. 我是在跟一个同行聊天,不是在对读者演讲
      2. 我在分享「我发现了一个好东西」,不是在推销「你应该买这个」
      3. 我在说「我理解你的处境」,不是在说「你好惨」
      
      ---
      
      ## 二、写作铁律(8 条,不可违反)
      
      | # | 铁律 | 具体要求 | 反例 |
      |---|------|----------|------|
      | 1 | **开头 3 句话内产生「这就是我」** | 用第二人称写一个具体的、有画面感的日常场景;不用引语、不用设问、不用「在当今时代」 | ❌「在当今新媒体时代,内容创作者面临诸多挑战……」 |
      | 2 | **全文至少有一个微型案例** | 一个具体的、有名有姓的产品使用场景,让读者看到工具在真实工作流里的样子 | ❌ 只说功能,不说「比如我用它做了什么」 |
      | 3 | **每个段落不超过 5 行** | 长段和短段交替,制造阅读节奏 | ❌ 一大段 8 行字,读者直接划走 |
      | 4 | **功能不列清单** | 每说一个功能,必须附带一句「这意味着什么」——翻译成读者自己的处境 | ❌「支持抖音、小红书、公众号数据查询」(只说了功能,没说对读者意味着什么) |
      | 5 | **禁用行业黑话** | 不能用:赋能、抓手、闭环、底层逻辑、颗粒度、打法、心智、调性 | ❌「构建内容生态闭环,赋能创作者心智」 |
      | 6 | **表格最多用 2 次** | 只在需要并列对比的时候用;不要用表格代替叙述 | ❌ 为了凑结构硬塞一张对比表 |
      | 7 | **结尾不给总结** | 结尾要给读者一个画面或一个动作——让他们看完之后想做一件事 | ❌「综上所述,红狐Hub 是一个优秀的工具……」 |
      | 8 | **不用结束信号词** | 全文不需要「最后」「综上所述」「总而言之」;文章应该在读者还没意识到「结束了」的时候自然收尾 | ❌「最后,希望这篇文章对你有帮助」 |
      
      ---
      
      ## 三、文章结构(9 步,自然过渡不套小标题)
      
      按以下顺序组织内容,但**不直接套小标题**——用自然过渡代替。每个小标题是「内容模块」,不是「必须出现的标题」。
      
      ### 3.1 痛点场景(200-300 字)
      
      一个具体的、读者每天都在经历的片段。**第二人称**。不要提产品名。
      
      **写法示例**:
      > 你早上 9 点打开抖音,刷 20 分钟「找感觉」。切到小红书,又 20 分钟。公众号翻一遍,B 站扫一眼……四个平台看完了,上午没了。选题呢?还不知道写什么。
      
      **自检**:读者看完这段话,会不会觉得「这就是我」?
      
      ### 3.2 产品一句话定位(50-100 字)
      
      它解决什么问题?**一句话说清楚**。如果一句话说不清楚,说明你的理解还不够。
      
      **写法示例**:
      > 红狐 Hub 把全平台新媒体数据拧成一个水龙头,拧开就有。
      
      **自检**:这句话能不能当文章的副标题?
      
      ### 3.3 跟竞品的本质区别(200-300 字)
      
      不用列出所有竞品,选一个读者最熟悉的选择(比如「在各平台直接搜」),做精准对比。
      
      **对比维度**:
      - 你在抖音搜「美妆」,看到的是平台排好序的视频列表
      - 红狐给你的是结构化数据报告——实时热度、涨粉趋势、爆款规律分析
      
      **自检**:对比之后,读者会不会觉得「原来我一直用的方法这么低效」?
      
      ### 3.4 一个场景案例(150-200 字)
      
      展示产品在真实场景下怎么用、产出什么。用「说一个具体的场景」或「比如」开头。
      
      **写法示例**:
      > 说一个具体的场景:你做小红书运营,每天最头疼的是「不知道现在什么内容在起量」。红狐的「小红书低粉爆款笔记」直接把粉丝不到 5000 但互动过 500 的笔记筛出来——你不用猜平台算法喜欢什么,看数据就行了。
      
      **自检**:这个案例能不能让读者「看到自己坐在电脑前用这个工具」?
      
      ### 3.5 核心能力展开(300-400 字)
      
      挑 3-5 个最核心的能力,每个配一句「对你来说意味着什么」。
      
      **结构模板**:
      ```
      能力 X:{功能描述}
      → 对你来说意味着:{翻译成读者处境}
      ```
      
      **自检**:每一条能力后面,读者能不能说出「这对我有用的原因是……」?
      
      ### 3.6 用户画像(200 字)
      
      2-3 类典型用户,每类 2 句话——一句描述处境,一句描述用法。写到读者能对号入座。
      
      **写法示例**:
      > - **内容创作者**:每天用「小红书低粉爆款」「抖音热榜」做选题。这是用量最大的群体。
      > - **开发者**:直接接 API,把数据喂给自己的产品。RESTful 接口,毫秒级响应。
      
      **自检**:读者能不能在其中一类里看到自己?
      
      ### 3.7 上手路径(200 字)
      
      分两条路(非技术 / 技术),每条路 3 步,每步一句话。不要让读者觉得复杂。
      
      **写法模板**:
      ```
      路径一:Skill 模式(0 代码)
      1. 注册登录
      2. 创建 API Key
      3. 下载 Skill,在 AI 助手里直接用
      
      路径二:API 模式(写代码)
      1. 同样注册获取 Key
      2. 查阅 API 文档
      3. 写代码集成
      ```
      
      **自检**:读者看完会不会觉得「这个我 5 分钟就能搞定」?
      
      ### 3.8 FAQ(可选,150 字)
      
      只回答正文回答不了的问题。正文已经说过的不重复。
      
      **选题方向**:
      - 数据时效性(「数据是实时的吗?」)
      - 技术门槛(「我不会写代码能用吗?」)
      - 适用场景边界(「这个工具适合做什么、不适合做什么?」)
      
      **自检**:每个 FAQ 的答案能不能让读者「打完消了一个顾虑」?
      
      ### 3.9 结尾(100-150 字)
      
      一个好的结尾像一句话的回声——跟开头呼应但不重复,给读者一个明天就能做的小行动。
      
      **写法示例**:
      > 下次当你又打开四个 App 找选题的时候,记住一件事:有一个水龙头,拧开就有全平台的新鲜数据——注册、拿 Key、选 Skill,三步走完。
      >
      > 这就是红狐 Hub 想给你的第一次体验。
      
      **自检**:读者看完结尾,会不会想打开那个网站?
      
      ---
      
      ## 四、语调规范
      
      ### 4.1 基调
      
      像一个在行业里干了 5 年的人跟新同事聊天。
      
      具体表现:
      - ✅ 可以刻薄,但刻薄的对象是「烂工具」和「浪费时间」,不是读者
      - ✅ 可以共情,但共情的方式是「我知道你经历了什么」,不是「你好惨」
      - ❌ 避免兴奋剂式语言(「太震撼了!」「颠覆认知!」「强烈推荐!」)——内容本身的说服力够用,不需要感叹号来补
      
      ### 4.2 句式偏好
      
      | 偏好 | 示例 |
      |------|------|
      | 用「你」不用「您」 | ✅「你可以试试」❌「您可以尝试」 |
      | 短句为主 | ✅「数据实时拉取,不缓存」❌「系统采用实时数据拉取机制,确保不产生缓存残留」 |
      | 每段不超过 4 行 | 视觉上给读者「喘息感」 |
      | 用具体数字代替模糊描述 | ✅「20.2 万次」❌「大量用户」 |
      
      ### 4.3 禁用词库(在 3.0 版基础上新增)
      
      | 类别 | 禁用词 |
      |------|--------|
      | 行业黑话 | 赋能、抓手、闭环、底层逻辑、颗粒度、打法、心智、调性、沉淀、对齐、方法论前置 |
      | 兴奋剂式 | 太震撼了、颠覆认知、强烈推荐、史上最强、必看 |
      | 空洞套话 | 在当今时代、众所周知、不言而喻、综上所述、最后、总而言之 |
      | 虚伪共情 | 你好惨、你一定很焦虑、你是不是也遇到了 |
      
      ---
      
      ## 五、模式适配
      
      ### 模式 A:link_rewrite
      
      - 保留源文章的**结构节奏**(长短段交替、案例密度)
      - 保留源文章的**语气基调**(直白/专业/轻松)
      - 将源文章的**主题**替换为本 Skill/产品的主题
      - **必须遵循本框架的身份和铁律**,不受源文章风格影响
      
      ### 模式 B:skill_promo
      
      - **完整遵循本框架**,包括身份、铁律、结构、语调
      - 配图采用红狐风格(--style redfox)
      - 语气默认「直白坦率」,可通过 tonePreference 调整
      
      ### 模式 C:add_images
      
      - 不改变用户提供的正文内容
      - 仅在配图决策时参考本框架的「信息密度」和「章节语义」
      
      ---
      
      ## 六、自检清单
      
      文章生成后,用这份清单做最后检查(LLM 主观判断,非机器验证):
      
      - [ ] 开头 3 句话内,读者能不能产生「这就是我」的感觉?
      - [ ] 全文有没有至少一个具体的使用场景案例?
      - [ ] 有没有段落超过 5 行?
      - [ ] 有没有「功能清单」式描述(只说功能不说「意味着什么」)?
      - [ ] 有没有禁用词(赋能/抓手/闭环/……)?
      - [ ] 表格是不是真的需要(还是为了凑结构)?
      - [ ] 结尾是不是在总结(而不是给画面/动作)?
      - [ ] 有没有「综上所述」「最后」这类结束信号词?
      - [ ] 读者看完会不会想做一件事(而不是「知道了」)?
      
  • scripts
    • decide_images.py 10.3 KB
      """断章生图决策脚本
      
      输入:markdown 格式正文(字符串 或 文件路径)
      输出:JSON {image_plan: [...], stats: {...}}
      
      决策规则:
        1. 切分:按 H2 拆成多个 section
        2. 字数:每 500-800 字配 1 张图(章节内),首图建议在第 1-2 节内
        3. 锚点:检测"如下图/如上图/举个例子/来看个案例/接下来"等强提示词,
           在该句之后优先插图
        4. 边界:不在引言/结语/FAQ 内强制插图,由字数决定
        5. 标题检测:H2 标题含"案例/示例/图解/对比/数据"等关键词时,倾向插图
      
      使用方式:
        python decide_images.py --input article.md
        python decide_images.py --input article.md --min-words 500 --max-words 800
        echo "正文" | python decide_images.py --stdin
      """
      
      import argparse
      import json
      import re
      import sys
      from pathlib import Path
      
      
      # 强锚点词:这些词出现后,必然是图位的强信号
      ANCHOR_KEYWORDS_STRONG = [
          r"如下图", r"如上图", r"看下面这张图", r"看这张图",
          r"举个例子", r"来看个例子", r"来看个案例", r"举个栗子",
          r"如图所示", r"见图", r"看图", r"如图",
          r"接下来", r"来看", r"看这里",
      ]
      
      # 弱锚点词:图位候选信号,需要结合字数判断
      ANCHOR_KEYWORDS_WEAK = [
          r"比如", r"例如", r"想象一下", r"假设",
          r"对比", r"区别", r"差异", r"差距",
          r"流程", r"步骤", r"操作",
      ]
      
      # 标题信号词:H2 标题含这些词时,倾向在该节插图
      HEADING_HINT_KEYWORDS = [
          "案例", "示例", "图解", "对比", "数据", "图表",
          "流程", "步骤", "工具", "方法", "技巧",
          "before", "after", "前后",
      ]
      
      # 跳过插图的章节类型
      SKIP_SECTION_KEYWORDS = [
          "FAQ", "常见问题", "问答", "结语", "总结", "写在最后", "写在最后的话",
      ]
      
      # 强制插图的章节类型
      FORCE_SECTION_KEYWORDS = [
          "案例", "示例", "图解",
      ]
      
      # 默认参数
      DEFAULT_MIN_WORDS = 500
      DEFAULT_MAX_WORDS = 800
      DEFAULT_MAX_IMAGES = 6
      MIN_WORDS_HARD_LIMIT = 300  # 低于此字数不插图
      
      
      def split_sections(markdown):
          """按 H2 切分 markdown,返回 [{heading, level, content, start_line}]."""
          lines = markdown.split("\n")
          sections = []
          current = None
          in_code_block = False
      
          for i, line in enumerate(lines):
              # 跳过代码块
              if line.strip().startswith("```"):
                  in_code_block = not in_code_block
                  if current is not None:
                      current["content"] += line + "\n"
                  continue
              if in_code_block:
                  if current is not None:
                      current["content"] += line + "\n"
                  continue
      
              # H2 切分
              h2_match = re.match(r"^##\s+(.+)$", line)
              if h2_match:
                  if current is not None:
                      sections.append(current)
                  current = {
                      "heading": h2_match.group(1).strip(),
                      "level": 2,
                      "content": "",
                      "start_line": i,
                  }
              else:
                  if current is None:
                      # 引言(在第一个 H2 之前)
                      current = {
                          "heading": "引言",
                          "level": 1,
                          "content": "",
                          "start_line": 0,
                      }
                  current["content"] += line + "\n"
      
          if current is not None:
              sections.append(current)
      
          return sections
      
      
      def count_words(text):
          """统计中文字数(中文按字、英文按词)。"""
          chinese = len(re.findall(r"[\u4e00-\u9fff]", text))
          english = len(re.findall(r"\b[a-zA-Z]+\b", text))
          return chinese + english
      
      
      def find_anchor_positions(section_content):
          """在章节内容中查找锚点词位置(行号、类型)。"""
          positions = []
          for i, line in enumerate(section_content.split("\n")):
              for pattern in ANCHOR_KEYWORDS_STRONG:
                  if re.search(pattern, line):
                      positions.append({"line": i, "type": "strong", "match": pattern, "context": line.strip()[:50]})
                      break
              else:
                  for pattern in ANCHOR_KEYWORDS_WEAK:
                      if re.search(pattern, line):
                          positions.append({"line": i, "type": "weak", "match": pattern, "context": line.strip()[:50]})
                          break
          return positions
      
      
      def should_skip_section(heading):
          """判断该章节是否跳过插图(FAQ、结语等)。"""
          for kw in SKIP_SECTION_KEYWORDS:
              if kw in heading:
                  return True
          return False
      
      
      def should_force_section(heading):
          """判断该章节是否强制插图(案例、示例等)。"""
          for kw in FORCE_SECTION_KEYWORDS:
              if kw in heading:
                  return True
          return False
      
      
      def heading_has_visual_hint(heading):
          """标题是否含视觉化关键词。"""
          for kw in HEADING_HINT_KEYWORDS:
              if kw.lower() in heading.lower():
                  return True
          return False
      
      
      def decide_image_plan(sections, min_words, max_words, max_images):
          """根据切分后的章节,决定生图计划。"""
          plan = []
          total_words = 0
      
          for idx, sec in enumerate(sections):
              sec_words = count_words(sec["content"])
              total_words += sec_words
              heading = sec["heading"]
      
              # 规则 1: 跳过类章节
              if should_skip_section(heading):
                  continue
      
              # 规则 2: 字数太少,整个跳过
              if sec_words < MIN_WORDS_HARD_LIMIT:
                  continue
      
              # 规则 3: 强制插图类
              if should_force_section(heading):
                  count = max(1, sec_words // max_words)
                  count = min(count, 2)  # 单节最多 2 张
                  plan.append({
                      "after_section": idx,
                      "section_heading": heading,
                      "count": count,
                      "reason": "force_section",
                      "prompt_hint": f"该章节是「{heading}」,需配视觉化插图",
                      "anchors": find_anchor_positions(sec["content"])[:count],
                  })
                  continue
      
              # 规则 4: 标题含视觉化关键词
              if heading_has_visual_hint(heading):
                  count = max(1, sec_words // max_words)
                  count = min(count, 2)
                  plan.append({
                      "after_section": idx,
                      "section_heading": heading,
                      "count": count,
                      "reason": "heading_visual_hint",
                      "prompt_hint": f"「{heading}」含视觉化关键词,配置图强化理解",
                      "anchors": find_anchor_positions(sec["content"])[:count],
                  })
                  continue
      
              # 规则 5: 字数触发(500-800 字/张)
              if sec_words >= min_words:
                  # 优先找强锚点;找不到再放章节末尾
                  anchors = find_anchor_positions(sec["content"])
                  strong_anchors = [a for a in anchors if a["type"] == "strong"]
                  count = max(1, sec_words // max_words)
                  count = min(count, 2)
                  plan.append({
                      "after_section": idx,
                      "section_heading": heading,
                      "count": count,
                      "reason": "word_count_trigger" if not strong_anchors else "anchor_match",
                      "prompt_hint": f"「{heading}」内容较长({sec_words}字),配图降低阅读疲劳",
                      "anchors": (strong_anchors or anchors)[:count],
                  })
      
          # 全局限制
          if len(plan) > max_images:
              # 优先级:force > heading_visual > anchor > word_count
              priority = {
                  "force_section": 4,
                  "heading_visual_hint": 3,
                  "anchor_match": 2,
                  "word_count_trigger": 1,
              }
              plan.sort(key=lambda x: priority.get(x["reason"], 0), reverse=True)
              plan = plan[:max_images]
              # 按章节顺序重排
              plan.sort(key=lambda x: x["after_section"])
      
          return plan, total_words
      
      
      def main():
          parser = argparse.ArgumentParser(
              description="读 markdown 正文,自动决定生图点(数量+位置+锚点)",
              formatter_class=argparse.RawDescriptionHelpFormatter,
          )
          parser.add_argument("--input", type=str, help="markdown 文件路径")
          parser.add_argument("--stdin", action="store_true", help="从 stdin 读取正文")
          parser.add_argument("--output", type=str, default="image_plan.json", help="输出 JSON 路径")
          parser.add_argument("--min-words", type=int, default=DEFAULT_MIN_WORDS, help=f"触发配图的最小字数(默认 {DEFAULT_MIN_WORDS})")
          parser.add_argument("--max-words", type=int, default=DEFAULT_MAX_WORDS, help=f"每张图覆盖的最大字数(默认 {DEFAULT_MAX_WORDS})")
          parser.add_argument("--max-images", type=int, default=DEFAULT_MAX_IMAGES, help=f"全篇最大生图数(默认 {DEFAULT_MAX_IMAGES})")
          args = parser.parse_args()
      
          # 读取输入
          if args.stdin:
              markdown = sys.stdin.read()
          elif args.input:
              p = Path(args.input)
              if not p.exists():
                  print(f"[ERR] File not found: {args.input}")
                  sys.exit(1)
              markdown = p.read_text(encoding="utf-8")
          else:
              print("[ERR] Provide --input <file> or --stdin")
              sys.exit(1)
      
          # 校验参数
          if args.min_words >= args.max_words:
              print(f"[ERR] --min-words ({args.min_words}) must be < --max-words ({args.max_words})")
              sys.exit(1)
      
          # 切分
          sections = split_sections(markdown)
          print(f"[OK] Split into {len(sections)} sections")
          for sec in sections:
              print(f"  - [{count_words(sec['content'])}字] {sec['heading']}")
      
          # 决策
          plan, total_words = decide_image_plan(sections, args.min_words, args.max_words, args.max_images)
      
          # 输出
          result = {
              "image_plan": plan,
              "stats": {
                  "total_sections": len(sections),
                  "total_words": total_words,
                  "planned_images": sum(p["count"] for p in plan),
              },
              "params": {
                  "min_words": args.min_words,
                  "max_words": args.max_words,
                  "max_images": args.max_images,
              },
          }
      
          with open(args.output, "w", encoding="utf-8") as f:
              json.dump(result, f, ensure_ascii=False, indent=2)
      
          print(f"\n[Plan] {result['stats']['planned_images']} images planned")
          for p in plan:
              print(f"  - Section {p['after_section']}「{p['section_heading']}」 x {p['count']} ({p['reason']})")
          print(f"\n[Done] Saved to {args.output}")
      
      
      if __name__ == "__main__":
          main()
      
    • fetch_article.py 7.7 KB
      """文章抓取脚本(统一版)
      
      输入:sourceUrl(任意文章链接)
      输出:JSON {title, sections, images, raw_html}
      - title: 文章标题
      - sections: [{heading, level, text}]  章节切分(按 H2/H3 拆分)
      - images: [url, ...]  文章中所有图片 URL(按出现顺序)
      - raw_html: 原始 HTML(可选,用于 fallback)
      
      使用方式:
        python fetch_article.py --url "https://xxx.com/post/123" --output article.json
      
      注意:
        - 仅依赖 Python 标准库
        - 抓取失败时,输出 error 字段,调用方需判断降级
        - 图片 URL 同时保留绝对路径(直接用)和相对路径补全
      """
      
      import argparse
      import json
      import os
      import re
      import ssl
      import sys
      import urllib.request
      from html.parser import HTMLParser
      from urllib.parse import urljoin, urlparse
      
      
      class ArticleExtractor(HTMLParser):
          """从 HTML 中提取标题、章节文本、图片 URL。"""
      
          def __init__(self, base_url):
              super().__init__()
              self.base_url = base_url
              self.title = ""
              self.in_title = False
              self.images = []
              # 章节切分:记录当前 H2/H3 节点与其下文本
              self.sections = []
              self._current_section = None
              self._current_text_parts = []
              self._in_heading = False
              self._heading_level = 0
              self._skip_tags = {"script", "style", "noscript", "nav", "footer", "header"}
              self._skip_depth = 0
              self._depth = 0
      
          # ---- 标签处理 ----
          def handle_starttag(self, tag, attrs):
              self._depth += 1
              attrs_dict = dict(attrs)
      
              if tag == "title":
                  self.in_title = True
                  return
      
              if tag in ("h1", "h2", "h3"):
                  self._in_heading = True
                  self._heading_level = int(tag[1])
                  self._flush_section()
                  self._current_section = {
                      "heading": "",
                      "level": self._heading_level,
                      "text": "",
                  }
                  return
      
              if tag == "img":
                  src = attrs_dict.get("src") or attrs_dict.get("data-src") or attrs_dict.get("data-original")
                  if src:
                      full_url = urljoin(self.base_url, src)
                      if full_url not in self.images:
                          self.images.append(full_url)
                  return
      
              if tag in self._skip_tags:
                  self._skip_depth += 1
                  return
      
              if tag in ("p", "br", "li", "div"):
                  self._current_text_parts.append("\n")
      
          def handle_endtag(self, tag):
              if tag == "title":
                  self.in_title = False
                  return
      
              if tag in ("h1", "h2", "h3"):
                  self._in_heading = False
                  if self._current_section:
                      self._current_section["text"] = "".join(self._current_text_parts).strip()
                      self.sections.append(self._current_section)
                      self._current_section = None
                      self._current_text_parts = []
                  return
      
              if tag in self._skip_tags and self._skip_depth > 0:
                  self._skip_depth -= 1
                  return
      
              self._depth -= 1
      
          def handle_data(self, data):
              if self.in_title:
                  self.title += data
                  return
              if self._skip_depth > 0:
                  return
              if self._in_heading and self._current_section is not None:
                  self._current_section["heading"] += data
                  return
              if self._current_section is not None:
                  self._current_text_parts.append(data)
      
          # ---- 辅助 ----
          def _flush_section(self):
              if self._current_section is not None:
                  self._current_section["text"] = "".join(self._current_text_parts).strip()
                  if self._current_section["heading"] or self._current_section["text"]:
                      self.sections.append(self._current_section)
              self._current_section = None
              self._current_text_parts = []
      
          def close(self):
              super().close()
              # 处理文档末尾没有 H2/H3 闭合的情况
              self._flush_section()
              # 清理文本(合并多余空白行)
              cleaned = []
              for sec in self.sections:
                  sec["text"] = re.sub(r"\n{3,}", "\n\n", sec["text"]).strip()
                  cleaned.append(sec)
              self.sections = cleaned
      
      
      def fetch_html(url, timeout=30):
          """抓取 URL 对应的 HTML 文本。"""
          ctx = ssl.create_default_context()
          ctx.check_hostname = False
          ctx.verify_mode = ssl.CERT_NONE
      
          req = urllib.request.Request(
              url,
              headers={
                  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
                  "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
                  "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
                  "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
              },
          )
          with urllib.request.urlopen(req, context=ctx, timeout=timeout) as resp:
              charset = resp.headers.get_content_charset() or "utf-8"
              raw = resp.read()
              try:
                  return raw.decode(charset, errors="replace")
              except (LookupError, TypeError):
                  return raw.decode("utf-8", errors="replace")
      
      
      def extract_article(html, base_url):
          """从 HTML 中提取文章结构。"""
          parser = ArticleExtractor(base_url)
          parser.feed(html)
          parser.close()
      
          return {
              "title": parser.title.strip(),
              "sections": parser.sections,
              "images": parser.images,
          }
      
      
      def filter_meaningful_images(images, min_path_depth=2):
          """过滤装饰性图片(icon/avatar/logo/loading 等)。"""
          skip_patterns = [
              r"/icon", r"/logo", r"/avatar", r"/loading", r"/spinner",
              r"1x1\.", r"pixel\.", r"\.gif$", r"data:image",
              r"/sprite", r"/static/img/.*\?\d+",  # 静态资源
          ]
          regex = re.compile("|".join(skip_patterns), re.IGNORECASE)
          return [u for u in images if not regex.search(u)]
      
      
      def main():
          parser = argparse.ArgumentParser(
              description="抓取任意文章 URL,输出结构化 JSON(标题+章节+图片)",
              formatter_class=argparse.RawDescriptionHelpFormatter,
          )
          parser.add_argument("--url", type=str, required=True, help="文章 URL")
          parser.add_argument("--output", type=str, default="article.json", help="输出 JSON 路径")
          parser.add_argument("--keep-html", action="store_true", help="在输出中保留原始 HTML")
          parser.add_argument("--no-filter", action="store_true", help="不过滤装饰性图片")
          args = parser.parse_args()
      
          # 校验 URL
          parsed = urlparse(args.url)
          if parsed.scheme not in ("http", "https"):
              print(f"[ERR] Invalid URL scheme: {args.url}")
              sys.exit(1)
      
          print(f"[FETCH] {args.url}")
          try:
              html = fetch_html(args.url)
          except Exception as e:
              print(f"[ERR] Fetch failed: {e}")
              result = {"error": f"fetch_failed: {e}", "url": args.url}
              with open(args.output, "w", encoding="utf-8") as f:
                  json.dump(result, f, ensure_ascii=False, indent=2)
              sys.exit(1)
      
          print(f"[OK] HTML fetched: {len(html)} bytes")
          result = extract_article(html, args.url)
          result["url"] = args.url
      
          if not args.no_filter:
              before = len(result["images"])
              result["images"] = filter_meaningful_images(result["images"])
              print(f"[OK] Filtered images: {before} -> {len(result['images'])}")
      
          if args.keep_html:
              result["raw_html"] = html
      
          # 统计
          print(f"[OK] Title: {result['title']}")
          print(f"[OK] Sections: {len(result['sections'])}")
          print(f"[OK] Images: {len(result['images'])}")
      
          with open(args.output, "w", encoding="utf-8") as f:
              json.dump(result, f, ensure_ascii=False, indent=2)
      
          print(f"\n[Done] Saved to {args.output}")
      
      
      if __name__ == "__main__":
          main()
      
    • generate_image.py 26.8 KB
      """运营文章配图生成脚本(对接 gpt-image-2 新接口)
      基于 gpt-image-2 模型,支持文生图与图生图(传入参考图),仅依赖 Python 标准库。
      
      对接红狐新版接口:
          SUBMIT: POST https://redfox.hk/story/api/parseWork/imageGen/gptImage2Submit
          RESULT: POST https://redfox.hk/story/api/parseWork/imageGen/gptImage2Result
      
      新接口提交体:
          prompt / resolution(1k|2k|4k) / size(宽高比) / n(1-4) / referenceImages(最多 2 张)
      新接口返回体关键字段:
          data.status ∈ {completed, processing, failed} / data.imageUrls[] / data.progress / data.failReason
      
      用法:
          # 文生图(默认 16:9 + 2k)
          python generate_image.py --prompt "图片描述" --api-key "ak_xxx"
          # 指定宽高比与分辨率档位
          python generate_image.py --prompt "..." --size 9:16 --resolution 4k --api-key "ak_xxx"
          # 图生图(传入参考图)
          python generate_image.py --prompt "图片描述" --image assets/skill标题.jpg --api-key "ak_xxx"
          # 链接仿写模式(红狐风格不启用,参考图作为风格参照)
          python generate_image.py --prompt "..." --reference-image "https://xxx.com/img.jpg" --style reference --api-key "ak_xxx"
          # 风格切换
          python generate_image.py --prompt "..." --style {redfox|popcomic|yellowcomic|reference|none|random} --api-key "ak_xxx"
          # 批量生成(prompts.json 中可指定 image/size/resolution/n/style 字段)
          python generate_image.py --batch prompts.json --api-key "ak_xxx"
          # 查询已有任务
          python generate_image.py --task-id "task_xxx" --api-key "ak_xxx"
      
      已弃用参数(新接口不再支持,传入会被忽略并提示):
          --quality / --fidelity
      
      prompts.json 格式:
          [
              {"id": "img1", "prompt": "...", "chapter": "封面", "image": "assets/skill标题.jpg", "style": "redfox", "size": "16:9", "resolution": "2k", "n": 1},
              {"id": "img2", "prompt": "...", "chapter": "热点分析", "image": "assets/02.jpg", "style": "reference"},
              {"id": "img3", "prompt": "...", "chapter": "操作指南", "style": "popcomic"}
          ]
      
      style 参数说明:
          - redfox(默认):红狐讲解员 + 美式复古报刊暖米黄底
          - popcomic:现代波普漫画风 + 亮黄底 + 多角色场景 + 浮动图标
          - yellowcomic:亮黄信息图漫画风 + 斜纹底 + 分类表格中心
          - reference:原 prompt 不变,仅作图生图参考(适合链接仿写)
          - none:原 prompt 不动,不追加任何风格修饰
          - random:从 redfox / popcomic / yellowcomic 中随机选一个,同批次所有图用同一风格
      """
      
      import argparse
      import json
      import os
      import sys
      import time
      import random
      import urllib.request
      import urllib.error
      import ssl
      import tempfile
      from pathlib import Path
      
      SUBMIT_URL = "https://redfox.hk/story/api/parseWork/imageGen/gptImage2Submit"
      RESULT_URL = "https://redfox.hk/story/api/parseWork/imageGen/gptImage2Result"
      UPLOAD_URL = "https://redfox.hk/story/api/parseWork/imageGen/uploadImage"
      CONFIG_DIR = Path.home() / ".qoder" / "apis"
      CONFIG_FILE = CONFIG_DIR / "redfox.json"
      ENV_KEY = "REDFOX_API_KEY"
      
      # 新接口 gptImage2Submit 参数
      #   prompt / resolution(1k|2k|4k) / size(宽高比) / n(1-4) / referenceImages(最多2张)
      DEFAULT_RESOLUTION = "2k"
      DEFAULT_ASPECT = "16:9"
      DEFAULT_N = 1
      
      POLL_INTERVAL = 5
      MAX_POLLS = 40  # 最长等待 ~200 秒
      
      # 新接口 size 支持的宽高比
      VALID_ASPECTS = {
          "1:1", "3:2", "2:3", "4:3", "3:4", "5:4", "4:5",
          "16:9", "9:16", "2:1", "1:2", "21:9", "9:21",
      }
      VALID_RESOLUTIONS = {"1k", "2k", "4k"}
      MAX_REFERENCE_IMAGES = 2
      MAX_COUNT = 4
      
      # 兼容旧像素尺寸:像素 → (新接口 size 宽高比, 推荐 resolution 档位)
      LEGACY_SIZE_MAP = {
          "1024x1024": ("1:1", "1k"),
          "1024x1536": ("2:3", "1k"),
          "1536x1024": ("3:2", "1k"),
          "1792x1024": ("16:9", "1k"),
          "1024x1792": ("9:16", "1k"),
          "2048x2048": ("1:1", "2k"),
          "2048x1152": ("16:9", "2k"),
          "1152x2048": ("9:16", "2k"),
      }
      # 兼容旧脚本中出现的 ALLOWED_SIZES 引用(含像素 + 宽高比)
      ALLOWED_SIZES = set(LEGACY_SIZE_MAP.keys()) | VALID_ASPECTS
      
      MAX_PROMPT_LENGTH = 500
      
      
      def normalize_size(size_arg, resolution_arg=None):
          """把 --size 归一化为新接口所需的 (aspect, resolution)。
      
          - 像素格式(旧):1792x1024 → ("16:9", "1k");如显式传入 resolution 则以其为准
          - 宽高比格式(新):16:9 → ("16:9", resolution_arg or "2k")
          """
          size_str = (size_arg or DEFAULT_ASPECT).strip()
          if size_str in LEGACY_SIZE_MAP:
              aspect, default_res = LEGACY_SIZE_MAP[size_str]
              resolution = (resolution_arg or default_res).strip().lower()
          elif size_str in VALID_ASPECTS:
              aspect = size_str
              resolution = (resolution_arg or DEFAULT_RESOLUTION).strip().lower()
          else:
              print(f"[ERR] Unsupported size: {size_str}")
              print(f"  宽高比可选: {', '.join(sorted(VALID_ASPECTS))}")
              print(f"  兼容旧像素: {', '.join(sorted(LEGACY_SIZE_MAP.keys()))}")
              sys.exit(1)
          if resolution not in VALID_RESOLUTIONS:
              print(f"[ERR] Unsupported resolution: {resolution} (可选 1k/2k/4k)")
              sys.exit(1)
          return aspect, resolution
      
      
      # 风格修饰器:三种可选视觉风格
      
      # 风格 A — redfox:红狐讲解员 IP + 美式复古报刊暖米黄底
      REDFOX_STYLE_SUFFIX = (
          ",画面中心为红色狐狸讲解员(红色毛皮、白色胸腹、大尾巴、海军蓝小马甲、圆框眼镜),"
          "美式复古报刊科普条漫风格,横向宽幅多格分镜,胶片颗粒质感,"
          "无装饰性边框,狐狸为视觉中心,参考 prompt 描述的姿态(封面=欢迎、分析=前倾拿教鞭、"
          "方法论=托腮沉思、操作=坐在电脑前打字)"
      )
      
      # 风格 B — popcomic:现代波普漫画风 + 亮黄底 + 多角色场景(基于 assets/02.jpg)
      POPCOMIC_STYLE_SUFFIX = (
          ",现代波普漫画信息图插画风格,高饱和亮黄色底色配波普网点纹理,粗黑轮廓线+扁平高饱和色块填充,"
          "画面中心为一位干练的讲解员卡通人物(职业装/衬衫,表情自信开朗),左侧1-2位困惑的从业者卡通人物带问号和对话气泡,右侧可有1-2位角色互动,"
          "顶部圆角矩形横幅内大号加粗中文主标题,周围浮动UI装饰图标(图表/邮件/齿轮/搜索框/对话框),"
          "人物间有白色圆角对话气泡承载中文文案,动态活泼构图,2D矢量商业插画,8K高清;"
          "禁止写实摄影、3D渲染、暗沉阴影、杂乱线条、水印、模糊字体、文字乱码"
      )
      
      # 风格 C — yellowcomic:亮黄信息图漫画风 + 斜纹底 + 分类表格中心(基于 assets/03.jpg)
      YELLOWCOMIC_STYLE_SUFFIX = (
          ",亮黄色斜纹底色现代信息图漫画插画风格,粗重清晰黑色轮廓线,纯色平涂高饱和马卡龙色块,"
          "画面中心为一位自信的讲解员手持分类表格或文档(职业装,表情从容),左侧困惑角色群带问号气泡,右侧角色可互动,"
          "顶部圆角矩形横幅大号加粗中文标题,周围散布齿轮/文档/聊天框/折线图等操作类图标装饰,"
          "人物对话气泡承载中文文案,流程感和分工感强,2D矢量商业插画,8K高清;"
          "禁止写实摄影、3D渲染、暗沉阴影、杂乱线条、水印、模糊字体、文字乱码"
      )
      
      # 所有可用风格(random 从中选取,排除 reference 和 none)
      RANDOMIZABLE_STYLES = ["redfox", "popcomic", "yellowcomic"]
      VALID_STYLES = {"redfox", "popcomic", "yellowcomic", "reference", "none", "random"}
      
      
      def apply_style(prompt, style):
          """根据 style 参数对 prompt 做风格修饰。
      
          - redfox: 追加红狐讲解员 + 美式复古报刊条漫风格
          - popcomic: 追加现代波普漫画风(亮黄底+多角色场景)
          - yellowcomic: 追加亮黄信息图漫画风(斜纹底+分类表格中心)
          - reference: 原 prompt 不变(参考图自带风格)
          - none: 原 prompt 不变
          """
          style_suffix_map = {
              "redfox": REDFOX_STYLE_SUFFIX,
              "popcomic": POPCOMIC_STYLE_SUFFIX,
              "yellowcomic": YELLOWCOMIC_STYLE_SUFFIX,
          }
          suffix = style_suffix_map.get(style)
          if suffix:
              if len(prompt) + len(suffix) > MAX_PROMPT_LENGTH:
                  available = MAX_PROMPT_LENGTH - len(suffix) - 3
                  prompt = prompt[:available] + "..."
              return prompt + suffix
          # reference / none:原样返回
          return prompt
      
      
      def pick_random_style():
          """从 RANDOMIZABLE_STYLES 中随机选一个风格名称。"""
          return random.choice(RANDOMIZABLE_STYLES)
      
      
      def get_api_key(cli_key=None):
          """Get API key: CLI arg > env var > config file."""
          if cli_key:
              return cli_key
          env_key = os.environ.get(ENV_KEY)
          if env_key:
              return env_key
          if CONFIG_FILE.exists():
              try:
                  data = json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
                  key = data.get("api_key")
                  if key:
                      return key
              except (json.JSONDecodeError, OSError):
                  pass
          return None
      
      
      def make_request(url, data, api_key, timeout=30):
          """Send a JSON POST request and return parsed response."""
          payload = json.dumps(data).encode("utf-8")
          req = urllib.request.Request(
              url,
              data=payload,
              headers={
                  "Content-Type": "application/json",
                  "REDFOX_API_KEY": api_key,
              },
              method="POST",
          )
          try:
              with urllib.request.urlopen(req, timeout=timeout) as resp:
                  body = json.loads(resp.read().decode("utf-8"))
                  return body
          except urllib.error.HTTPError as e:
              error_body = e.read().decode("utf-8", errors="replace")
              return {"code": -1, "msg": f"HTTP {e.code}: {error_body}"}
          except Exception as e:
              return {"code": -1, "msg": str(e)}
      
      
      def upload_image(image_path, api_key):
          """Upload a local image to OSS, return the image URL."""
          if not os.path.isfile(image_path):
              print(f"[ERR] Image file not found: {image_path}")
              return None
      
          ext = os.path.splitext(image_path)[1].lower()
          fmt_map = {".png": "png", ".jpg": "jpeg", ".jpeg": "jpeg", ".webp": "webp"}
          fmt = fmt_map.get(ext, "png")
      
          print(f"[UP] Uploading image: {image_path}")
      
          boundary = "----PythonFormBoundary7MA4YWxkTrZu0gW"
          filename = os.path.basename(image_path)
          with open(image_path, "rb") as f:
              file_data = f.read()
      
          body = (
              f"--{boundary}\r\n"
              f'Content-Disposition: form-data; name="format"\r\n\r\n'
              f"{fmt}\r\n"
              f"--{boundary}\r\n"
              f'Content-Disposition: form-data; name="file"; filename="{filename}"\r\n'
              f"Content-Type: image/{fmt}\r\n\r\n"
          ).encode("utf-8") + file_data + f"\r\n--{boundary}--\r\n".encode("utf-8")
      
          req = urllib.request.Request(
              UPLOAD_URL,
              data=body,
              headers={
                  "Content-Type": f"multipart/form-data; boundary={boundary}",
                  "X-API-KEY": api_key,
              },
              method="POST",
          )
          try:
              with urllib.request.urlopen(req, timeout=60) as resp:
                  result = json.loads(resp.read().decode("utf-8"))
          except Exception as e:
              print(f"[ERR] Upload failed: {e}")
              return None
      
          code = result.get("code")
          if not str(code).startswith("2"):
              print(f"[ERR] Upload failed (code {code}): {result.get('msg', '')}")
              return None
      
          image_url = result.get("data", {}).get("imageUrl")
          if not image_url:
              print("[ERR] Upload succeeded but no imageUrl returned")
              return None
      
          print(f"[OK] Upload complete: {image_url}")
          return image_url
      
      
      def is_url(path):
          """Check if a string is an HTTP(S) URL."""
          return path.startswith("http://") or path.startswith("https://")
      
      
      def download_url_to_temp(url):
          """Download a URL to a temp file, return the temp file path."""
          ctx = ssl.create_default_context()
          ctx.check_hostname = False
          ctx.verify_mode = ssl.CERT_NONE
          ext = os.path.splitext(url.split("?")[0])[1] or ".webp"
          tmp = tempfile.NamedTemporaryFile(suffix=ext, delete=False)
          print(f"[DL] Downloading: {url}")
          try:
              req = urllib.request.Request(url)
              with urllib.request.urlopen(req, context=ctx) as resp:
                  tmp.write(resp.read())
              tmp.close()
              print(f"[OK] Downloaded to: {tmp.name}")
              return tmp.name
          except Exception as e:
              print(f"[ERR] Download failed: {e}")
              tmp.close()
              return None
      
      
      def download_images(image_urls, output_dir, prefix="image"):
          """Download generated images to output directory."""
          downloaded = []
          total = len(image_urls)
      
          ctx = ssl.create_default_context()
          ctx.check_hostname = False
          ctx.verify_mode = ssl.CERT_NONE
      
          for i, url in enumerate(image_urls, 1):
              ext = ".png"
              url_path = url.split("?")[0]
              for fmt in [".png", ".jpg", ".jpeg", ".webp"]:
                  if url_path.lower().endswith(fmt):
                      ext = fmt
                      break
      
              filename = f"{prefix}_{i}{ext}" if total > 1 else f"{prefix}{ext}"
              filepath = os.path.join(output_dir, filename)
      
              print(f"[DL] Downloading {i}/{total}: {filename}")
              try:
                  req = urllib.request.Request(url)
                  with urllib.request.urlopen(req, context=ctx) as resp:
                      with open(filepath, "wb") as f:
                          f.write(resp.read())
                  size_kb = os.path.getsize(filepath) / 1024
                  print(f"  OK: {filepath} ({size_kb:.1f} KB)")
                  downloaded.append(filepath)
              except Exception as e:
                  print(f"  ERR: Download failed: {e}")
      
          return downloaded
      
      
      def submit_task(prompt, api_key, image_path=None, size=None, resolution=None, n=1,
                      style="redfox", fidelity=None, quality=None):
          """Submit an image generation task via gptImage2Submit. Returns taskId or None.
      
          新接口请求体:
              {
                "prompt": "...",
                "resolution": "1k|2k|4k",
                "size": "16:9",
                "n": 1-4,
                "referenceImages": ["url1", "url2"]
              }
      
          style:
              - redfox: 红狐讲解员 + 美式复古报刊风格(默认)
              - popcomic: 现代波普漫画风(亮黄底+多角色场景)
              - yellowcomic: 亮黄信息图漫画风(斜纹底+分类表格中心)
              - reference: prompt 不变,靠参考图自带的风格
              - none: prompt 不变
              - random: 从 redfox/popcomic/yellowcomic 中随机选一个
      
          fidelity / quality 为旧接口参数,新接口不再支持,仅保留形参避免破坏旧调用方。
          """
          if fidelity:
              print(f"[WARN] --fidelity 在新接口已弃用,忽略值: {fidelity}")
          if quality:
              print(f"[WARN] --quality 在新接口已弃用,忽略值: {quality}(如需控制清晰度请用 --resolution)")
      
          # 风格修饰:random 需在此处解析为具体风格
          if style == "random":
              style = pick_random_style()
              print(f"[OK] Random style selected: {style}")
      
          if style not in VALID_STYLES:
              print(f"[WARN] Unknown style '{style}', fallback to 'redfox'")
              style = "redfox"
          final_prompt = apply_style(prompt, style)
          if final_prompt != prompt:
              print(f"[OK] Style applied: {style} (+{len(final_prompt) - len(prompt)} chars)")
          elif style in ("reference", "none"):
              print(f"[OK] Style applied: {style} (no modification)")
      
          # 参考图处理(支持单张路径/URL,上传后归一到 referenceImages 数组)
          reference_images = []
          if image_path:
              if isinstance(image_path, list):
                  candidates = image_path[:MAX_REFERENCE_IMAGES]
              else:
                  candidates = [image_path]
              for cand in candidates:
                  if not cand:
                      continue
                  if is_url(cand):
                      temp_path = download_url_to_temp(cand)
                      if not temp_path:
                          return None
                      image_url = upload_image(temp_path, api_key)
                      try:
                          os.unlink(temp_path)
                      except OSError:
                          pass
                      if not image_url:
                          return None
                      print(f"[OK] Mode: image-to-image, ref URL -> OSS: {image_url}")
                  else:
                      image_url = upload_image(cand, api_key)
                      if not image_url:
                          return None
                      print(f"[OK] Mode: image-to-image, ref: {cand}")
                  reference_images.append(image_url)
          else:
              print("[OK] Mode: text-to-image")
      
          # 尺寸/分辨率归一化为新接口格式
          aspect, resolution_final = normalize_size(size, resolution)
      
          # n 限制在新接口上限内
          try:
              n_int = int(n) if n is not None else DEFAULT_N
          except (TypeError, ValueError):
              n_int = DEFAULT_N
          n_int = max(1, min(MAX_COUNT, n_int))
      
          data = {
              "prompt": final_prompt,
              "resolution": resolution_final,
              "size": aspect,
              "n": n_int,
              "referenceImages": reference_images,
              "source": "图文运营创作器-GitHub",
          }
          print(f"[OK] Submit payload: size={aspect}, resolution={resolution_final}, n={n_int}, refs={len(reference_images)}")
      
          resp = make_request(SUBMIT_URL, data, api_key)
      
          if resp.get("code") == 2000 and (resp.get("data") or {}).get("taskId"):
              task_id = resp["data"]["taskId"]
              print(f"[OK] Task submitted: {task_id}")
              return task_id
          else:
              print(f"[ERR] Submit failed (code={resp.get('code')}): {resp.get('msg', 'unknown error')}")
              return None
      
      
      def poll_result(task_id, api_key):
          """Poll gptImage2Result until completed/failed/timeout, return imageUrls list."""
          for i in range(MAX_POLLS):
              resp = make_request(RESULT_URL, {"taskId": task_id}, api_key, timeout=15)
      
              if resp.get("code") != 2000:
                  print(f"[WARN] Poll {i+1}/{MAX_POLLS}: API error - {resp.get('msg')}")
                  time.sleep(POLL_INTERVAL)
                  continue
      
              data = resp.get("data") or {}
              status = data.get("status", "")
      
              if status == "completed":
                  urls = data.get("imageUrls") or []
                  if isinstance(urls, str):
                      urls = [urls]
                  print(f"[OK] Task {task_id} completed: {urls}")
                  return urls
      
              elif status == "failed":
                  reason = data.get("failReason") or "unknown"
                  print(f"[ERR] Task {task_id} failed: {reason}")
                  return None
      
              else:
                  progress = data.get("progress")
                  elapsed = (i + 1) * POLL_INTERVAL
                  suffix = f" {progress}%" if isinstance(progress, int) else ""
                  print(f"[...] Poll {i+1}/{MAX_POLLS}: {status or 'processing'}{suffix} ({elapsed}s elapsed)")
                  time.sleep(POLL_INTERVAL)
      
          print(f"[TIMEOUT] Task {task_id} timed out after {MAX_POLLS * POLL_INTERVAL}s")
          return None
      
      
      def generate_single(prompt, api_key, image_path=None, size=None, resolution=None, n=1,
                          style="redfox", fidelity=None, quality=None):
          """Generate a single image. Returns list of URLs or empty list."""
          task_id = submit_task(
              prompt, api_key,
              image_path=image_path, size=size, resolution=resolution, n=n,
              style=style, fidelity=fidelity, quality=quality,
          )
          if not task_id:
              return []
          result = poll_result(task_id, api_key)
          return result or []
      
      
      def generate_batch(prompts_file, api_key, default_style="redfox", default_resolution=None, default_n=1):
          """Generate multiple images from a JSON file. Returns dict of id -> URLs.
      
          每个 item 可独立指定 style/size/resolution/n 字段;未指定则用默认值。
          当 default_style 为 random 时,会先随机选一个具体风格,整批所有图都用该风格(保证同批次一致)。
      
          item 字段支持:
              id, prompt, image, style, size, resolution, n
          已弃用字段(新接口不支持,传入则忽略):
              fidelity, quality
          """
          base_dir = os.path.dirname(os.path.abspath(prompts_file))
          with open(prompts_file, "r", encoding="utf-8") as f:
              items = json.load(f)
      
          # 如果默认风格是 random,整批统一选一个
          if default_style == "random":
              resolved_default = pick_random_style()
              print(f"[OK] Batch random style selected: {resolved_default} (all items will use this style)")
          else:
              resolved_default = default_style
      
          results = {}
          tasks = {}
          for item in items:
              img_id = item.get("id", f"img_{len(tasks)}")
              prompt = item["prompt"]
              image_path = item.get("image")
              size = item.get("size")
              resolution = item.get("resolution", default_resolution)
              n = item.get("n", default_n)
              fidelity = item.get("fidelity")  # 已弃用,仅传递给 submit_task 触发提示
              quality = item.get("quality")    # 已弃用
              # 优先用 item 自身的 style,否则用已解析的 default
              item_style = item.get("style", resolved_default)
              if image_path and not os.path.isabs(image_path) and not is_url(image_path):
                  image_path = os.path.join(base_dir, image_path)
              task_id = submit_task(
                  prompt, api_key,
                  image_path=image_path, size=size, resolution=resolution, n=n,
                  style=item_style, fidelity=fidelity, quality=quality,
              )
              if task_id:
                  tasks[img_id] = task_id
      
          for img_id, task_id in tasks.items():
              urls = poll_result(task_id, api_key)
              results[img_id] = urls or []
      
          return results
      
      
      def main():
          parser = argparse.ArgumentParser(
              description="运营文章配图生成(对接 gptImage2Submit/gptImage2Result 新接口,支持文生图 + 图生图 + 批量)",
              formatter_class=argparse.RawDescriptionHelpFormatter,
          )
          parser.add_argument("--prompt", type=str, help="单张图片 prompt")
          parser.add_argument("--batch", type=str, help="批量生成 JSON 文件路径")
          parser.add_argument("--image", type=str, default=None, help="参考图路径或 URL(启用图生图模式)")
          parser.add_argument("--reference-image", type=str, default=None, help="参考图别名(等同于 --image,但语义更清晰)")
          parser.add_argument("--size", type=str, default=DEFAULT_ASPECT,
                              help=f"图片尺寸:优先传宽高比(默认 {DEFAULT_ASPECT},可选: {', '.join(sorted(VALID_ASPECTS))});"
                                   f"也兼容旧像素格式({', '.join(sorted(LEGACY_SIZE_MAP.keys()))})")
          parser.add_argument("--resolution", type=str, default=None, choices=sorted(VALID_RESOLUTIONS),
                              help="分辨率档位 1k/2k/4k(默认:像素格式自动匹配档位,宽高比格式默认 2k)")
          parser.add_argument("-n", "--count", type=int, default=DEFAULT_N,
                              help=f"生成图片数量 (1-{MAX_COUNT},默认 {DEFAULT_N},新接口上限 4)")
          parser.add_argument("--style", type=str, default="redfox", choices=sorted(VALID_STYLES),
                              help="风格模式:redfox(默认)/ popcomic / yellowcomic / reference / none / random")
          parser.add_argument("--api-key", type=str, default=None, help="REDFOX_API_KEY")
          parser.add_argument("--output", type=str, default="results.json", help="结果输出文件")
          parser.add_argument("--task-id", type=str, default=None, help="查询已有任务结果(跳过提交)")
          parser.add_argument("--download-dir", type=str, default=None, help="下载图片到本地目录(不传则仅返回 URL)")
      
          # 已弃用参数(新接口不再支持,保留仅为向后兼容 CLI)
          parser.add_argument("--fidelity", type=str, default=None, choices=["high", "low"],
                              help="[已弃用] 新接口不再支持 inputFidelity,此参数被忽略")
          parser.add_argument("--quality", type=str, default=None, choices=["low", "medium", "high", "auto"],
                              help="[已弃用] 新接口不再支持 quality,请用 --resolution 控制清晰度")
      
          args = parser.parse_args()
      
          # 兼容 --reference-image 与 --image
          if args.reference_image and not args.image:
              args.image = args.reference_image
      
          # 弃用参数提示
          if args.fidelity:
              print(f"[WARN] --fidelity 在新接口已弃用,将被忽略(传入值: {args.fidelity})")
          if args.quality:
              print(f"[WARN] --quality 在新接口已弃用,将被忽略(传入值: {args.quality}),如需控制清晰度请用 --resolution")
      
          # 尺寸预校验(具体归一化在 submit_task 内部完成)
          normalize_size(args.size, args.resolution)
      
          # n 上限校验
          if args.count < 1 or args.count > MAX_COUNT:
              print(f"[ERR] --count/-n 取值范围 1-{MAX_COUNT}(新接口上限 4)")
              sys.exit(1)
      
          api_key = get_api_key(cli_key=args.api_key)
          if not api_key:
              print("[ERR] REDFOX_API_KEY is required. Set via --api-key or REDFOX_API_KEY env var.")
              print("  Get key: https://redfox.hk/settings/api-keys?source=skillhub")
              sys.exit(1)
      
          # Mode: Query existing task
          if args.task_id:
              print(f"[QUERY] Querying task: {args.task_id}")
              urls = poll_result(args.task_id, api_key)
              if urls:
                  results = {"query": urls}
                  if args.download_dir:
                      os.makedirs(args.download_dir, exist_ok=True)
                      downloaded = download_images(urls, args.download_dir)
                      results["downloaded"] = downloaded
              else:
                  results = {"query": []}
              with open(args.output, "w", encoding="utf-8") as f:
                  json.dump(results, f, ensure_ascii=False, indent=2)
              print(json.dumps(results, ensure_ascii=False, indent=2))
              sys.exit(0 if urls else 1)
      
          # Validate prompt length
          if args.prompt and len(args.prompt) > MAX_PROMPT_LENGTH:
              print(f"[ERR] Prompt too long ({len(args.prompt)} chars), max {MAX_PROMPT_LENGTH}")
              sys.exit(1)
      
          if args.batch:
              results = generate_batch(
                  args.batch, api_key,
                  default_style=args.style,
                  default_resolution=args.resolution,
                  default_n=args.count,
              )
          elif args.prompt:
              urls = generate_single(
                  args.prompt, api_key,
                  image_path=args.image,
                  size=args.size,
                  resolution=args.resolution,
                  n=args.count,
                  style=args.style,
                  fidelity=args.fidelity,
                  quality=args.quality,
              )
              results = {"single": urls}
              if urls and args.download_dir:
                  os.makedirs(args.download_dir, exist_ok=True)
                  downloaded = download_images(urls, args.download_dir)
                  results["downloaded"] = downloaded
          else:
              print("[ERR] Provide --prompt or --batch")
              sys.exit(1)
      
          with open(args.output, "w", encoding="utf-8") as f:
              json.dump(results, f, ensure_ascii=False, indent=2)
      
          print(f"\n[Done] Results saved to {args.output}")
          print(json.dumps(results, ensure_ascii=False, indent=2))
      
      
      if __name__ == "__main__":
          main()
      
    • validate_article.py 19.4 KB
      """文章质量验证脚本(v2.3 软阈值版)
      
      把 SKILL.md 中的质量检查清单从人工 checklist 升级为可机械化验证的脚本。
      v2.3 重要变化:所有阈值改为软建议——超范围只出 warning,不直接 fail。
      写作框架应跟随 Skill 逻辑动态调整,不被硬阈值锁死。
      
      使用方式:
        # 默认(软阈值,目标字数 1000)
        python validate_article.py --input article.md
      
        # 自定义字数目标
        python validate_article.py --input article.md --target-length 1500
      
        # 严格模式(恢复 v2.1 硬 fail 行为,仅在需要 CI 严格拦截时使用)
        python validate_article.py --input article.md --strict
      
      检查项(v2.3 全部为 warning 级别,超范围不直接 fail):
        - 禁忌词检测(赋能、抓手、打法、闭环、颗粒度、对齐、沉淀、方法论前置)
        - 字数统计(默认 500-1500 区间,目标 1000 字)
        - 标题长度(8-30 字)
        - 章节数(2-8 节正文)
        - 表格数(0-3 张,0 张也合规)
        - 段落行数(每段不超过 5 行)
        - FAQ 数量(0 或 2-4 个,0 个也合规)
        - 信息密度:每 400 字至少 1 个数字 + 1 个动词开头的句子
        - 链接/图片来源标注(资料来源章节)
      """
      
      import argparse
      import json
      import re
      import sys
      from pathlib import Path
      
      
      # ---- 禁忌词表 ----
      FORBIDDEN_WORDS = [
          "赋能", "抓手", "打法", "闭环", "颗粒度", "对齐", "沉淀", "方法论前置",
          # 句式触发词(出现就扣分)
      ]
      
      FORBIDDEN_PATTERNS = [
          r"在当今.{0,15}的时代",
          r"众所周知",
          r"不言而喻",
          r"!!!+",  # 感叹号堆叠
      ]
      
      # ---- 结构参数(v2.3 软阈值:所有检查默认 warning 级别)----
      # 注意:v2.3 重要原则变化——所有这些阈值都是"软建议",不直接 fail。
      # 写作时 LLM 应根据 Skill 逻辑、选题类型、读者需求动态判断。
      TITLE_MIN_LEN = 8
      TITLE_MAX_LEN = 30         # v2.3 放宽(v2.1 是 25)
      SECTION_MIN = 2            # v2.3 放宽(v2.1 是 3)
      SECTION_MAX = 8            # v2.3 放宽(v2.1 是 6)
      TABLE_MIN = 0              # v2.3 改 0:表格按需出现,不强制(v2.1 是 2)
      TABLE_MAX = 3              # v2.3 保持
      PARAGRAPH_MAX_LINES = 5    # v2.3 放宽(v2.1 是 4)
      FAQ_COUNT_MIN = 2          # v2.3 改 2-4 区间(v2.1 是固定 3)
      FAQ_COUNT_MAX = 4
      WORDS_PER_DENSITY_CHECK = 400  # v2.3 放宽(v2.1 是 300)
      
      # ---- 字数目标(v2.3 默认 1000 字左右,可配置)----
      DEFAULT_TARGET_LENGTH = 1000
      TARGET_LENGTH_TOLERANCE = 500  # ±500 字缓冲(如目标 1000 字,500-1500 都算 pass)
      
      
      def read_markdown(path):
          """读取 markdown 文件,去掉代码块和图片语法但保留行结构。"""
          p = Path(path)
          if not p.exists():
              print(f"[ERR] File not found: {path}")
              sys.exit(1)
          return p.read_text(encoding="utf-8")
      
      
      def extract_title(markdown):
          """提取 H1 标题。"""
          for line in markdown.split("\n"):
              m = re.match(r"^#\s+(.+)$", line)
              if m:
                  return m.group(1).strip()
          return ""
      
      
      def extract_sections(markdown):
          """提取 H2 章节(含 heading + content + 行号)。"""
          sections = []
          lines = markdown.split("\n")
          current = None
          in_code = False
      
          for i, line in enumerate(lines):
              # 代码块跳过
              if line.strip().startswith("```"):
                  in_code = not in_code
                  if current:
                      current["content"] += line + "\n"
                  continue
              if in_code:
                  if current:
                      current["content"] += line + "\n"
                  continue
      
              m = re.match(r"^##\s+(.+)$", line)
              if m:
                  if current:
                      sections.append(current)
                  current = {
                      "heading": m.group(1).strip(),
                      "content": "",
                      "start_line": i,
                  }
              else:
                  if current is None:
                      # 引言(首个 H2 之前)
                      current = {"heading": "引言", "content": "", "start_line": 0}
                  current["content"] += line + "\n"
      
          if current:
              sections.append(current)
          return sections
      
      
      def count_words(text):
          """统计字数(中文字 + 英文词)。"""
          # 去掉 markdown 标记
          text = re.sub(r"```.*?```", "", text, flags=re.DOTALL)
          text = re.sub(r"`[^`]+`", "", text)
          text = re.sub(r"!\[.*?\]\(.*?\)", "", text)  # 图片
          text = re.sub(r"\[([^\]]+)\]\([^\)]+\)", r"\1", text)  # 链接保留文字
          text = re.sub(r"[#*_>|]", "", text)
      
          chinese = len(re.findall(r"[\u4e00-\u9fff]", text))
          english = len(re.findall(r"\b[a-zA-Z]+\b", text))
          return chinese + english
      
      
      def count_tables(markdown):
          """统计 markdown 表格数量(按 |...| 行匹配)。"""
          in_code = False
          table_count = 0
          in_table = False
      
          for line in markdown.split("\n"):
              if line.strip().startswith("```"):
                  in_code = not in_code
                  continue
              if in_code:
                  continue
              # 表格行:包含 | 且两端以 | 结尾
              if re.match(r"^\s*\|.+\|\s*$", line):
                  if not in_table:
                      in_table = True
                      table_count += 1
              else:
                  in_table = False
          return table_count
      
      
      def count_paragraph_lines(sections):
          """统计每段的行数,识别超过 4 行的段落。"""
          violations = []
          for sec in sections:
              # 去掉表格、代码块、标题
              text = sec["content"]
              text = re.sub(r"```.*?```", "", text, flags=re.DOTALL)
              text = re.sub(r"^\|.*$", "", text, flags=re.MULTILINE)  # 表格行
              text = re.sub(r"^#+\s+.*$", "", text, flags=re.MULTILINE)  # 标题
      
              # 按空行分段
              paragraphs = re.split(r"\n\s*\n", text)
              for i, para in enumerate(paragraphs):
                  para = para.strip()
                  if not para:
                      continue
                  line_count = len([l for l in para.split("\n") if l.strip()])
                  if line_count > PARAGRAPH_MAX_LINES:
                      violations.append({
                          "section": sec["heading"],
                          "paragraph_index": i,
                          "lines": line_count,
                          "preview": para[:50] + "...",
                      })
          return violations
      
      
      def find_forbidden_words(markdown):
          """查找禁忌词及其出现位置。"""
          findings = []
          lines = markdown.split("\n")
      
          for word in FORBIDDEN_WORDS:
              for i, line in enumerate(lines, 1):
                  if word in line:
                      findings.append({
                          "word": word,
                          "line": i,
                          "context": line.strip()[:80],
                      })
      
          for pattern in FORBIDDEN_PATTERNS:
              for i, line in enumerate(lines, 1):
                  if re.search(pattern, line):
                      findings.append({
                          "word": f"pattern: {pattern}",
                          "line": i,
                          "context": line.strip()[:80],
                      })
      
          return findings
      
      
      def extract_faq(sections):
          """提取 FAQ 章节的 Q&A 数量。
      
          支持以下格式:
          - Q1: ... / Q1:...
          - **Q:...?** / **Q: ...?**
          - **Q1:...?**
          """
          faq = None
          for sec in sections:
              if "FAQ" in sec["heading"] or "常见问题" in sec["heading"]:
                  faq = sec
                  break
          if not faq:
              return 0
          # 多种格式都算
          patterns = [
              r"^Q\d+[::]",  # Q1: / Q1:
              r"\*\*Q\d*[::]\s*",  # **Q: / **Q1:
              r"^Q[::]",  # Q: / Q:
              r"^##\s*Q\d+",  # ## Q1
          ]
          count = 0
          for pattern in patterns:
              matches = re.findall(pattern, faq["content"], re.MULTILINE)
              count = max(count, len(matches))
          return count
      
      
      def count_info_density(sections):
          """信息密度检查:每 300 字应至少 1 个数字 + 1 个动词开头的句子。
      
          返回 violations 列表:每个元素是一个 dict{section, type, detail}。
          """
          violations = []
          # 用前 30 个常见动词做检测,避免构造大正则出错
          action_verbs_str = (
              "打开|点击|输入|选择|设置|使用|访问|下载|安装|运行|执行|创建|添加|删除|修改|"
              "检查|对比|避免|记住|关注|尝试|记下|抄|看|找|想|算|做|选|配|加|减|分|拆|排|"
              "写|画|测|跑|试|调|查|读|听|问|答|记|列|提|交|练|统|归|汇|制|定|布|署|"
              "进入|退出|返回|跳转|切到|换到|转到|改到|调到|移到|拉|推|滚|翻|"
              "请|必须|需要|应当|应该|可以|可能|建议|推荐|考虑"
          )
      
          for sec in sections:
              if "FAQ" in sec["heading"] or "结语" in sec["heading"]:
                  continue
              text = re.sub(r"```.*?```", "", sec["content"], flags=re.DOTALL)
              text = re.sub(r"^\|.*$", "", text, flags=re.MULTILINE)
              text = re.sub(r"^#+\s+.*$", "", text, flags=re.MULTILINE)
              text = re.sub(r"!\[.*?\]\(.*?\)", "", text)
              text = re.sub(r"\[([^\]]+)\]\([^\)]+\)", r"\1", text)
      
              words = count_words(text)
              if words == 0:
                  continue
      
              # 检查密度
              expected_density = max(1, words // WORDS_PER_DENSITY_CHECK)
      
              # 数字数量
              numbers = re.findall(r"\d+(?:\.\d+)?", text)
              if len(numbers) < expected_density:
                  violations.append({
                      "section": sec["heading"],
                      "type": "low_number_density",
                      "expected": expected_density,
                      "actual": len(numbers),
                      "words": words,
                  })
      
              # 动词句数量(行首是动词)
              action_lines = re.findall(rf"^(?:{action_verbs_str})", text, re.MULTILINE)
              if len(action_lines) < expected_density:
                  violations.append({
                      "section": sec["heading"],
                      "type": "low_action_density",
                      "expected": expected_density,
                      "actual": len(action_lines),
                      "words": words,
                  })
      
          return violations
      
      
      def has_source_section(sections):
          """检查是否有资料来源章节。"""
          for sec in sections:
              if "资料来源" in sec["heading"] or "参考" in sec["heading"]:
                  return True
          return False
      
      
      def check_paragraph_lines(sections):
          """段落行数检查:每段不超过 4 行(去掉空行/标题/表格)。"""
          return count_paragraph_lines(sections)
      
      
      def main():
          parser = argparse.ArgumentParser(
              description="运营文章质量验证(v2.3 软阈值版,所有检查默认 warning 级别)",
              formatter_class=argparse.RawDescriptionHelpFormatter,
          )
          parser.add_argument("--input", type=str, required=True, help="markdown 文件路径")
          parser.add_argument("--output", type=str, default="validation_report.json", help="验证报告输出路径")
          parser.add_argument("--strict", action="store_true", help="严格模式(恢复 v2.1 硬 fail 行为):任一 fail 退出码 1")
          parser.add_argument("--target-length", type=int, default=DEFAULT_TARGET_LENGTH,
                              help=f"目标字数(v2.3 默认 {DEFAULT_TARGET_LENGTH}),配合 ±{TARGET_LENGTH_TOLERANCE} 缓冲检查")
          parser.add_argument("--mode", type=str, choices=["link_rewrite", "skill_promo", "add_images"],
                              default=None, help="文章生成模式:link_rewrite 模式下跳过字数检查(跟随源文章长度)")
          args = parser.parse_args()
      
          markdown = read_markdown(args.input)
          target_length = args.target_length
          target_min = max(200, target_length - TARGET_LENGTH_TOLERANCE)
          target_max = target_length + TARGET_LENGTH_TOLERANCE
      
          # ---- 执行各项检查 ----
          # v2.3 重要变化:所有检查默认输出 status ∈ {pass, warning},不再有 fail
          # 除非 --strict 模式(恢复 v2.1 硬 fail 行为)
          report = {
              "file": args.input,
              "config": {
                  "target_length": target_length,
                  "target_range": f"{target_min}-{target_max}",
                  "mode": "strict" if args.strict else "soft",
                  "version": "v2.3",
              },
              "checks": {},
              "summary": {"passed": 0, "warnings": 0, "failed": 0},
          }
      
          # 1. 标题长度(v2.3:超范围只出 warning)
          title = extract_title(markdown)
          title_len = len(title)
          title_ok = TITLE_MIN_LEN <= title_len <= TITLE_MAX_LEN
          if args.strict:
              status = "pass" if title_ok else "fail"
          else:
              status = "pass" if title_ok else "warning"
          report["checks"]["title_length"] = {
              "title": title,
              "length": title_len,
              "expected_range": f"{TITLE_MIN_LEN}-{TITLE_MAX_LEN}",
              "status": status,
              "note": "v2.3 软化:超范围仅提示,不强制" if not title_ok and not args.strict else None,
          }
          report["summary"][status + ("ed" if status != "warning" else "s")] += 1
      
          # 2. 章节数(v2.3:超范围只出 warning)
          sections = extract_sections(markdown)
          section_count = len(sections)
          section_ok = SECTION_MIN <= section_count <= SECTION_MAX
          if args.strict:
              status = "pass" if section_ok else "fail"
          else:
              status = "pass" if section_ok else "warning"
          report["checks"]["section_count"] = {
              "count": section_count,
              "expected_range": f"{SECTION_MIN}-{SECTION_MAX}",
              "status": status,
              "note": "v2.3 软化:2-8 节均为合理区间" if not section_ok and not args.strict else None,
          }
          report["summary"][status + ("ed" if status != "warning" else "s")] += 1
      
          # 3. 表格数(v2.3:v2.1 强制 2-3 张 → v2.3 改为 0-3 张,0 张也合规)
          table_count = count_tables(markdown)
          table_ok = TABLE_MIN <= table_count <= TABLE_MAX
          if args.strict:
              # strict 模式仍按 v2.1 标准:至少 2 张
              table_ok_strict = 2 <= table_count <= 3
              status = "pass" if table_ok_strict else "fail"
          else:
              status = "pass" if table_ok else "warning"
          note = None
          if not table_ok and not args.strict:
              if table_count == 0:
                  note = "v2.3 软化:0 张表也合规(叙事/资讯类常见)"
              else:
                  note = "v2.3 软化:表格按需出现,不强制"
          report["checks"]["table_count"] = {
              "count": table_count,
              "expected_range": f"{TABLE_MIN}-{TABLE_MAX}",
              "status": status,
              "note": note,
          }
          report["summary"][status + ("ed" if status != "warning" else "s")] += 1
      
          # 4. 字数(v2.3:默认 1000 字,可配置 target-length;link_rewrite 模式跳过)
          total_words = count_words(markdown)
          if args.mode == "link_rewrite":
              # link_rewrite 跟随源文章长度,不做字数约束
              report["checks"]["word_count"] = {
                  "words": total_words,
                  "expected_range": "不限制(link_rewrite 模式跟随源文章长度)",
                  "status": "pass",
                  "note": "link_rewrite 模式不受字数约束",
              }
              report["summary"]["passed"] += 1
          else:
              if args.strict:
                  word_ok = 1500 <= total_words <= 4000  # v2.1 硬阈值
                  expected_range = "1500-4000"
              else:
                  word_ok = target_min <= total_words <= target_max
                  expected_range = f"{target_min}-{target_max}(目标 {target_length})"
              report["checks"]["word_count"] = {
                  "words": total_words,
                  "expected_range": expected_range,
                  "status": "pass" if word_ok else "warning",
                  "note": "v2.3 默认 1000 字左右,可通过 --target-length 自定义" if not word_ok and not args.strict else None,
              }
              report["summary"]["passed" if word_ok else "warnings"] += 1
      
          # 5. FAQ 数量(v2.3:2-4 个合理区间,0 个也允许)
          faq_count = extract_faq(sections)
          if args.strict:
              faq_ok = faq_count == 3
          else:
              faq_ok = (faq_count == 0) or (FAQ_COUNT_MIN <= faq_count <= FAQ_COUNT_MAX)
          report["checks"]["faq_count"] = {
              "count": faq_count,
              "expected_range": f"0 或 {FAQ_COUNT_MIN}-{FAQ_COUNT_MAX}" if not args.strict else "恰好 3",
              "status": "pass" if faq_ok else "warning",
              "note": "v2.3 软化:FAQ 数量动态生成,0/2/3/4 都合规" if not faq_ok and not args.strict else None,
          }
          report["summary"]["passed" if faq_ok else "warnings"] += 1
      
          # 6. 资料来源(v2.3:v2.1 强制 → v2.3 改为软)
          source_ok = has_source_section(sections)
          if args.strict:
              status = "pass" if source_ok else "fail"
          else:
              status = "pass" if source_ok else "warning"
          report["checks"]["source_section"] = {
              "present": source_ok,
              "status": status,
              "note": "v2.3 软化:资料来源仅在涉及外部数据时推荐" if not source_ok and not args.strict else None,
          }
          report["summary"][status + ("ed" if status != "warning" else "s")] += 1
      
          # 7. 禁忌词(v2.3:v2.1 强制 → v2.3 改为 warning)
          forbidden = find_forbidden_words(markdown)
          forbidden_ok = len(forbidden) == 0
          if args.strict:
              status = "pass" if forbidden_ok else "fail"
          else:
              status = "pass" if forbidden_ok else "warning"
          report["checks"]["forbidden_words"] = {
              "count": len(forbidden),
              "findings": forbidden[:5],
              "status": status,
              "note": "v2.3 软化:禁忌词仅提示,调用方决定替换" if not forbidden_ok and not args.strict else None,
          }
          report["summary"][status + ("ed" if status != "warning" else "s")] += 1
      
          # 8. 段落行数(v2.3:v2.1 强制 4 → v2.3 改 5,软警告)
          para_violations = check_paragraph_lines(sections)
          para_ok = len(para_violations) == 0
          if args.strict:
              status = "pass" if para_ok else "fail"
          else:
              status = "pass" if para_ok else "warning"
          report["checks"]["paragraph_lines"] = {
              "violations": len(para_violations),
              "max_allowed": PARAGRAPH_MAX_LINES,
              "details": para_violations[:5],
              "status": status,
              "note": f"v2.3 软化:每段 ≤ {PARAGRAPH_MAX_LINES} 行(v2.1 是 4)" if not para_ok and not args.strict else None,
          }
          report["summary"][status + ("ed" if status != "warning" else "s")] += 1
      
          # 9. 信息密度
          density_violations = count_info_density(sections)
          density_ok = len(density_violations) == 0
          report["checks"]["info_density"] = {
              "violations": len(density_violations),
              "details": density_violations[:5],
              "status": "pass" if density_ok else "warning",
          }
          if density_ok:
              report["summary"]["passed"] += 1
          else:
              report["summary"]["warnings"] += 1
      
          # ---- 输出 ----
          with open(args.output, "w", encoding="utf-8") as f:
              json.dump(report, f, ensure_ascii=False, indent=2)
      
          # ---- 控制台摘要 ----
          print(f"\n{'='*60}")
          print(f"质检报告:{args.input}")
          print(f"{'='*60}")
          for name, check in report["checks"].items():
              # v2.3 状态图标:纯 ASCII 字符,避免 Windows GBK 编码问题
              status_icon = {"pass": "[OK]", "fail": "[X]", "warning": "[!]"}[check["status"]]
              print(f"  {status_icon} {name}: {check['status']}")
          print(f"{'='*60}")
          print(f"  Pass: {report['summary']['passed']}  Fail: {report['summary']['failed']}  Warning: {report['summary']['warnings']}")
          print(f"  Report saved to: {args.output}")
      
          # 退出码
          # v2.3 soft 模式(默认):退出码恒为 0,所有检查均为建议
          # v2.1 strict 模式(兼容):任一 fail 退出码 1
          if args.strict and report["summary"]["failed"] > 0:
              sys.exit(1)
          sys.exit(0)
      
      
      if __name__ == "__main__":
          main()
      
  • README.en.md 5.9 KB
    # Visual Ops Writer / visual-ops-writer
    
    ---
    
    ## Overview
    
    A full-pipeline operations article tool covering everything from topic selection, writing, and illustration to quality checks. **Rewrite** any article by learning its style, **generate** promotional long-form content for a Skill, or **auto-illustrate** existing text — all with images included and ready to publish.
    
    **Core Value**
    
    - **Three scenarios, one tool**: Link rewriting, product promotion, and article illustration — the three highest-frequency content creation scenarios, all covered.
    - **Text + image delivery**: Articles and images are generated together and saved as a complete, ready-to-use file — not a half-finished draft.
    - **Built-in quality**: Every article goes through 9 quality checks automatically, with a report included — ready to publish.
    
    **Intended Users**
    
    - 📝 **Content operators** — Produce style-matched articles and promotional content from writing to illustration, all in one go.
    - 🛍️ **Brand / skill developers** — Auto-generate promotional long-form articles aligned with actual product capabilities.
    - 🏢 **Bloggers / editors** — Smart image placement for existing articles, no manual planning needed.
    
    ---
    
    ## Features
    
    ### Core Capabilities
    
    - **Link rewrite**: Provide any article link to automatically fetch its content and images, learn the style and structure, then produce a new article from a different angle with equivalent quality. Image styles match the originals — ideal for competitive analysis and content benchmarking.
    - **Skill promo**: Provide a Skill file path or name to auto-parse product capabilities and fetch the latest info from the target site, generating a promotional long-form article aligned with actual product features. Images use the RedFox illustrator style for visual and content consistency.
    - **Add images**: Provide complete article text to automatically analyze structure by section semantics and word density, intelligently determine image placement and count, then batch-generate consistent illustrations. Perfect for when you've finished writing but don't want to manually find images.
    - **Automated quality check**: Every article goes through 9 quality checks after generation (title length, section count, table count, word count, FAQ count, prohibited words, paragraph length, information density, etc.) with a JSON report output.
    
    ### Writing Capabilities
    
    - **Auto article type matching**: Three templates — tool promotion, methodology, and crisis response — automatically selected based on topic
    - **Reader persona adaptation**: Supports individual creators and team/enterprise reader profiles with automatic expression adjustment
    - **Tone options**: Direct & candid / Professional & rigorous / Casual & conversational — three tones to choose from
    - **Word count control**: Customizable word count range (default 2000-3000 words), or adaptive to original article length
    
    ### Illustration Capabilities
    
    - **Style transfer**: In link rewrite mode, original images serve as visual references; new content is generated with consistent style
    - **Semantic matching**: Images automatically match illustrator poses to section content (welcoming, analyzing, thinking, operating)
    - **Smart sectioning**: Auto-determines image placement by H2 sections + word density, skipping positions unsuitable for images
    - **Adjustable count**: 0-5 image limit, or fully auto-decided by the system
    
    ---
    
    ## API Key Acquisition & Security
    
    - This skill requires the environment variable: `REDFOX_API_KEY`.
    - `REDFOX_API_KEY` is issued by [RedFoxHub](https://redfox.hk/settings/api-keys?source=github) (`https://redfox.hk`)
    - Register at [RedFoxHub](https://redfox.hk?source=github) to obtain `REDFOX_API_KEY`.
    - Configure `REDFOX_API_KEY` on your device before using this skill.
    - Before providing your key, confirm its source, scope, validity period, and whether it can be reset or revoked.
    - Do not hard-code or expose keys in plain text in code, prompts, logs, or output files.
    
    ---
    
    ## Usage Guide
    
    Simply describe your needs in natural language — no commands to memorize.
    
    ### Quick Reference
    
    | Intent | Example phrase | Result |
    |--------|---------------|--------|
    | Link rewrite | "Rewrite this article in my product's voice, keep the style" | Learn the original style and structure, generate a new article with matching images |
    | Skill promo | "Write a promotional article for this Skill" | Auto-generate a capability-aligned promotional long-form article with images |
    | Add images | "Add images to this article" | Smart analysis of article structure, auto-determine image placement and generate |
    | Competitive analysis | "Analyze this article's approach, write one from a different angle" | Learn the structure and style of great content, produce differentiated output |
    
    ### Output
    
    After generation, you receive:
    
    1. **Complete article**: Saved as `article.md` with title, body text, and images (embedded in corresponding sections)
    2. **Quality report**: `validation_report.json` with 9 quality check results
    3. **Full text in chat**: Article displayed directly in the conversation
    4. **Feedback node**: Adjust any section or image by specifying the direction
    
    ---
    
    ## Use Cases
    
    | Scenario | Role | Example question | Benefit |
    |----------|------|-----------------|----------|
    | Style-matched rewrite | Content operator | "Rewrite this article, keep the style but switch to my product" | Quickly produce high-quality articles with consistent style, skip the blank-page phase |
    | Skill promotion | Skill developer | "Write a promotional article for this Skill" | Auto-generate capability-aligned promotional content — words match features |
    | Article illustration | Blogger / editor | "Add images to this article" | Smart image placement without manual planning |
    | Competitive analysis | Brand operator | "Analyze this article's approach, write one from a different angle" | Learn great content structure and style, produce differentiated output |
    
    ---
    
  • README.md 5.2 KB
    # 图文运营创作器 / visual-ops-writer
    
    ---
    
    ## 简介
    
    从选题、撰写、配图到质检,覆盖运营文章的完整创作链路。看到好文章能**仿写**,给个 Skill 能生成**推广长文**,写完正文能**自动配图**——图文一体交付,拿来就能用。
    
    **核心价值**
    
    - **三种场景,一个工具**:链接仿写、产品推广、正文配图——内容运营最高频的三个创作场景,无需来回切换工具。
    - **图文一体交付**:文章与配图同步生成,保存为完整可用的成品文件,不是半成品。
    - **质量内置**:每篇文章自动经过 9 项质量检查,输出质检报告,交付即可用。
    
    **适用对象**
    
    - 📝 **内容运营** — 快速产出对标风格文章与产品推广文,从「写到配」一条龙搞定。
    - 🛍️ **品牌 / 技能开发者** — 自动生成对齐产品能力的推广长文,内容与功能不脱节。
    - 🏢 **博主 / 编辑** — 为已有正文智能配图,省去手动规划配图位置的麻烦。
    
    ---
    
    ## 功能特性
    
    ### 核心功能
    
    - **链接仿写**:提供任意文章链接,自动抓取原文内容与图片,学习其风格与结构,仿写出角度不同但质量相当的新文章。配图风格与原图保持一致,适合对标分析与竞品内容学习。
    - **Skill 推广文**:提供 Skill 文件路径或名称,自动解析产品能力并抓取目标站点最新信息,生成与产品实际能力对齐的推广长文。配图采用红狐讲解员风格,视觉与内容双重匹配。
    - **正文补图**:提供完整正文,自动按章节语义与字数密度分析文章结构,智能决定配图位置与数量,批量生成风格统一的配图。适合写完正文不想手动找图的场景。
    - **自动质量检查**:每篇文章生成后自动执行 9 项质量检查(标题长度、章节数、表格数、字数、FAQ 数量、禁忌词、段落行数、信息密度等),输出 JSON 格式质检报告。
    
    ### 写作能力
    
    - **文章类型自动匹配**:工具推介型、方法论型、危机应对型三种模板,根据选题自动选择最合适的写作结构
    - **读者画像适配**:支持个体创作者和团队/企业两种读者定位,自动调整表达侧重
    - **语调可选**:直白坦率 / 专业严谨 / 轻松对话,三种语调自由切换
    - **字数可控**:支持自定义字数区间(默认 2000-3000 字),也可按原文篇幅自适应
    
    ### 配图能力
    
    - **风格迁移**:链接仿写模式下,原图作为视觉参照,新内容自动生成,风格一致
    - **语义匹配**:配图按章节内容自动匹配讲解员姿态(欢迎、分析、思考、操作)
    - **智能断章**:按 H2 章节切分 + 字数密度自动决策生图点,跳过不适合配图的位置
    - **数量可调**:0-5 张配图上限可调,也可完全交由系统自动决策
    
    ---
    
    ## 密钥获取与安全说明
    
    - 本技能需要使用环境变量:`REDFOX_API_KEY`。
    - `REDFOX_API_KEY` 由 [红狐 hub](https://redfox.hk/settings/api-keys?source=github) (`https://redfox.hk`)
    - 请前往 [红狐 hub](https://redfox.hk?source=github) 注册账号,获取 `REDFOX_API_KEY`。
    - 配置设备环境变量 `REDFOX_API_KEY` 后使用本技能。
    - 在提供密钥前,请先确认密钥来源、可用范围、有效期及是否支持重置/撤销。
    - 禁止在代码、提示词、日志或输出文件中硬编码/明文暴露密钥。
    
    ---
    
    ## 使用指南
    
    直接用自然语言描述需求,无需记忆命令。
    
    ### 常用说法速查
    
    | 意图 | 示例话术 | 效果 |
    |------|---------|------|
    | 链接仿写 | 「帮我仿写这篇文章,保持风格但换成我的产品」 | 学习原文风格与结构,生成配图一致的全新文章 |
    | Skill 推广 | 「帮我给这个 Skill 写一篇推广文」 | 自动生成对齐产品能力的推广长文与配图 |
    | 正文补图 | 「帮我给这篇正文加上配图」 | 智能分析文章结构,自动决定配图位置并生成 |
    | 竞品分析 | 「分析这篇文章的写法,帮我写一篇不同角度的」 | 学习优秀内容的结构与风格,产出差异化内容 |
    
    ### 输出示例
    
    完成生成后,你将收到:
    
    1. **完整文章**:保存为 `article.md`,含标题、正文、配图(嵌入对应章节)
    2. **质检报告**:`validation_report.json`,含 9 项文章质量检查结果
    3. **对话内全文**:直接在对话中输出文章全文
    4. **反馈节点**:如需调整某章节或配图,可告知具体修改方向
    
    ---
    
    ## 使用场景
    
    | 场景 | 角色 | 示例问法 | 收益 |
    |------|------|---------|------|
    | 对标仿写 | 内容运营 | 「帮我仿写这篇文章,保持风格但换成我的产品」 | 快速产出风格一致的高质量文章,省去从零构思 |
    | Skill 推广 | 技能开发者 | 「帮我给这个 Skill 写一篇推广文」 | 自动生成对齐产品能力的推广文章,内容与功能不脱节 |
    | 文章配图 | 博主/编辑 | 「帮我给这篇正文加上配图」 | 智能选择配图位置与数量,无需手动规划 |
    | 竞品分析 | 品牌运营 | 「分析这篇文章的写法,帮我写一篇不同角度的」 | 学习优秀内容的结构与风格,产出差异化内容 |
    
    ---
    
  • SKILL.md 9.4 KB
    ---
    name: visual-ops-writer
    description: 图文运营创作器。从选题、撰写、配图到质检,一条龙完成运营文章生产。看到好文章能仿写,给个 Skill 能生成推广长文,写完正文能自动配图。内置写作质量管控与自动配图能力,图文一体交付。触发词:"生成运营文章"、"链接仿写"、"对标分析"、"Skill 推广"、"为正文配图"。
    ---
    
    # 图文运营创作器
    
    从选题、撰写、配图到质检,覆盖运营文章的完整创作链路。你只需要提供素材——一篇文章链接、一个 Skill、或者一段写好的正文,剩下的交给它。
    
    > 核心执行流程详见 `references/core_workflow.md`
    
    ---
    
    ## 简介
    
    图文运营创作器解决的是内容团队最常见的效率瓶颈:**一篇文章从写到配,往往需要多人协作、反复打磨**。
    
    通过这个工具,你可以:
    
    - 看到一篇好文章,直接仿写为自己的版本,风格一致但内容全新
    - 给一个 Skill 或产品,自动生成一篇专业的推广长文,配图到位
    - 写完正文不想找图,丢进来自动判断哪里该配图、配什么风格
    
    三种场景,一个工具全覆盖。
    
    **适用对象**
    
    - 📝 **内容运营** — 快速产出对标风格文章与产品推广文,从写到配一条龙搞定
    - 🛍️ **品牌 / 技能开发者** — 自动生成对齐产品能力的推广长文,内容与功能不脱节
    - 🏢 **博主 / 编辑** — 为已有正文智能配图,省去手动规划配图位置的麻烦
    
    ---
    
    ## 功能特性
    
    ### 🎯 核心功能
    
    - **🔗 链接仿写**:提供任意文章链接,自动抓取原文内容与图片,学习其风格与结构,仿写全新文章。配图风格与原图保持一致,适合对标分析与竞品内容学习。
    - **📣 Skill 推广文**:提供 Skill 文件路径或名称,自动解析产品能力并抓取目标站点最新信息,生成与产品实际能力对齐的推广长文。配图采用红狐讲解员风格,视觉与内容双重匹配。
    - **🖼️ 正文补图**:提供完整正文,自动按章节语义与字数密度分析文章结构,智能决定配图位置与数量,批量生成风格统一的配图。适合写完正文不想手动找图的场景。
    - **✅ 自动质量检查**:每篇文章生成后自动执行 9 项质量检查(标题长度、章节数、表格数、字数、FAQ 数量、禁忌词、段落行数、信息密度等),输出 JSON 格式质检报告。
    
    ### 写作质量管控
    
    不是随便写一篇就交差。文章生成过程内置完整的质量管控体系:
    
    - **文章类型自动匹配**:根据选题自动识别为工具推介型、方法论型或危机应对型,每种类型有独立的写作侧重与结构建议
    - **读者画像适配**:支持个体创作者和团队/企业两种读者定位,自动调整表达侧重(个人可执行方法 vs 工具系统协同)
    - **语调可选**:直白坦率 / 专业严谨 / 轻松对话,默认直白坦率
    - **成文字数可控**:支持自定义字数区间(默认 2000-3000 字),链接仿写按原文篇幅自适应
    
    ### 自动配图能力
    
    文章配图不是简单的"插张图",而是内容的一部分:
    
    - **风格迁移**:链接仿写模式下,原图作为风格参照,新内容自动生成,视觉风格保持一致
    - **语义匹配**:Skill 推广模式下,配图按章节语义自动匹配讲解员姿态(欢迎、分析、思考、操作等)
    - **智能断章**:正文补图模式下,按 H2 章节切分 + 字数密度自动决策生图点,跳过 FAQ / 结语 / 引言等不适合配图的位置
    - **数量可控**:配图数量上限 0-5 张可调,也可由系统自动决策
    
    ---
    
    ## 一键安装
    
    ### 前置条件
    
    - Python 3.8+(用于运行配图生成与质量检查脚本)
    - `REDFOX_API_KEY`(用于图片生成,未配置时仍可生成纯文本文章)
    
    ### 安装步骤
    
    1. 将本 Skill 目录添加到你的 Agent 技能列表
    2. 获取 `REDFOX_API_KEY`:前往 [红狐 hub](https://redfox.hk?source=github) 注册账号,在 [API Keys 页面](https://redfox.hk/settings/api-keys?source=github) 创建密钥
    3. 配置环境变量:
    
    ```bash
    # macOS / Linux
    export REDFOX_API_KEY="your_key_here"
    
    # Windows PowerShell
    $env:REDFOX_API_KEY="your_key_here"
    ```
    
    4. 验证安装:告诉 Agent「帮我给这个 Skill 写一篇推广文」,确认技能正常响应
    
    ### 环境变量说明
    
    | 变量名 | 必填 | 说明 |
    |--------|------|------|
    | `REDFOX_API_KEY` | 条件必填 | 图片生成服务密钥;未配置时文章正常生成,图片降级为占位符或跳过 |
    
    ---
    
    ## 使用指南
    
    ### 基础使用
    
    #### 1. 链接仿写
    
    告诉 Agent 你想仿写的文章链接:
    
    > 用户:帮我仿写这篇文章,保持风格但换成我的产品
    > https://example.com/some-article
    >
    > Agent:已抓取原文,正在学习风格与结构…文章已生成,含 3 张配图,保存为 article.md。
    
    #### 2. Skill 推广文
    
    提供 Skill 文件或名称:
    
    > 用户:帮我给 xhs-ranking 这个 Skill 写一篇推广文
    >
    > Agent:已解析 Skill 能力并抓取目标站点信息,推广文章已生成,含红狐风格配图 3 张。
    
    #### 3. 正文补图
    
    粘贴正文或提供文件路径:
    
    > 用户:帮我给这篇正文加上配图
    > (粘贴正文内容)
    >
    > Agent:正文共 1800 字,已识别 4 个配图位置,配图生成中…完成,保存为 article.md。
    
    ### 高级使用
    
    - 指定语调:「用专业严谨的语调写」
    - 指定读者:「目标读者是团队企业」
    - 指定配图数量:「最多配 2 张图」
    - 关闭配图:「不需要配图」或「style 设为 none」
    - 指定字数:「控制在 1500 字左右」
    
    ### 常用说法速查
    
    | 意图 | 示例话术 | 效果 |
    |------|---------|------|
    | 链接仿写 | 「帮我仿写这篇文章,保持风格但换成我的产品」 | 学习原文风格与结构,生成配图一致的全新文章 |
    | Skill 推广 | 「帮我给这个 Skill 写一篇推广文」 | 自动生成对齐产品能力的推广长文与配图 |
    | 正文补图 | 「帮我给这篇正文加上配图」 | 智能分析文章结构,自动决定配图位置并生成 |
    | 竞品分析 | 「分析这篇文章的写法,帮我写一篇不同角度的」 | 学习优秀内容的结构与风格,产出差异化内容 |
    | 控制语调 | 「用轻松对话的语气写」 | 按指定语调生成文章 |
    | 关闭配图 | 「不需要配图」 | 仅输出纯文本文章 |
    
    ---
    
    ## 使用场景
    
    | 场景 | 角色 | 示例问法 | 收益 |
    |------|------|---------|------|
    | 对标仿写 | 内容运营 | 「帮我仿写这篇文章,保持风格但换成我的产品」 | 快速产出风格一致的高质量文章,省去从零开始的构思 |
    | Skill 推广 | 技能开发者 | 「帮我给这个 Skill 写一篇推广文」 | 自动生成对齐产品能力的推广文章,内容与功能不脱节 |
    | 文章配图 | 博主/编辑 | 「帮我给这篇正文加上配图」 | 智能选择配图位置与数量,无需手动规划 |
    | 竞品分析 | 品牌运营 | 「分析这篇文章的写法,帮我写一篇不同角度的」 | 学习优秀内容的结构与风格,产出差异化内容 |
    
    ---
    
    ## 输出说明
    
    完成生成后,你将收到:
    
    1. **完整文章**:保存为 `article.md`,含标题、正文、FAQ(如有)、配图(以 `![描述](URL)` 嵌入对应章节)
    2. **质检报告**:`validation_report.json`,含 9 项文章质量检查结果
    3. **对话内全文**:直接在对话中输出文章全文
    4. **反馈节点**:如需调整某章节或配图,可告知具体修改方向
    
    ---
    
    ## 常见问答
    
    **Q1: 没有 API Key 能用吗?**
    
    A: 可以。没有 `REDFOX_API_KEY` 时,文章正文正常生成,图片部分降级处理:链接仿写用占位符替代,Skill 推广和正文补图跳过配图,仅输出纯文本。
    
    **Q2: 生成的文章可以手动调整吗?**
    
    A: 可以。生成后直接告诉 Agent 你想调整的方向(如「第三段改短一点」「换一张配图」),Agent 会基于现有文章定向修改。
    
    **Q3: 配图风格可以自定义吗?**
    
    A: 支持三种风格:`redfox`(红狐讲解员)、`reference`(跟随原图风格)、`none`(无风格修饰)。告诉 Agent 你的偏好即可。
    
    ---
    
    ## 项目架构
    
    ```
    visual-ops-writer/
    ├── SKILL.md                          # 技能入口文件
    ├── references/
    │   ├── core_workflow.md              # 核心执行流程(Agent 执行链)
    │   ├── writing-framework.md          # 写作框架参考
    │   ├── article-type-templates.md     # 文章类型模板(工具推介/方法论/危机应对)
    │   ├── persona-matrix.md             # 语气×读者×视觉 IP 配置矩阵
    │   ├── image-decision-rules.md       # 配图决策规则
    │   ├── link-rewrite-guide.md         # 链接仿写指南
    │   ├── prompt-templates.md           # 提示词模板
    │   └── error-handling.md             # 错误处理
    ├── scripts/
    │   ├── fetch_article.py              # 文章抓取与解析
    │   ├── validate_article.py           # 机器化质量检查
    │   ├── generate_image.py             # 图片生成(支持文生图与风格迁移)
    │   └── decide_images.py              # 配图位置与数量决策
    └── assets/
        ├── 02.jpg                        # 素材图片
        └── 03.jpg                        # 素材图片
    ```
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related