From e1a0d6ef47050ef247473dc3f8ae36ecf4553ff3 Mon Sep 17 00:00:00 2001 From: wangzhuc Date: Mon, 30 Mar 2026 13:00:07 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20OpenClaw=20=E5=85=BC?= =?UTF-8?q?=E5=AE=B9=EF=BC=9Abuild=20=E8=84=9A=E6=9C=AC=20+=20CI=20+=20?= =?UTF-8?q?=E9=A6=96=E6=AC=A1=E4=BA=A7=E7=89=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - scripts/build_openclaw.py:SKILL.md 转换({skill_dir}→{baseDir}、WebSearch→web_search、移除 allowed-tools) - .github/workflows/build-openclaw.yml:push to main 时自动构建 dist/openclaw/ - dist/openclaw/:首次构建产物入库,OpenClaw 用户可直接使用 Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/build-openclaw.yml | 38 ++ .gitignore | 3 +- dist/openclaw/SKILL.md | 298 ++++++++++ dist/openclaw/config.example.yaml | 28 + dist/openclaw/personas/cold-analyst.yaml | 45 ++ dist/openclaw/personas/industry-observer.yaml | 43 ++ dist/openclaw/personas/midnight-friend.yaml | 51 ++ dist/openclaw/personas/sharp-journalist.yaml | 42 ++ dist/openclaw/personas/warm-editor.yaml | 44 ++ dist/openclaw/references/effect-review.md | 19 + dist/openclaw/references/frameworks.md | 192 ++++++ dist/openclaw/references/learn-edits.md | 51 ++ dist/openclaw/references/onboard.md | 86 +++ dist/openclaw/references/seo-rules.md | 62 ++ dist/openclaw/references/style-template.md | 60 ++ dist/openclaw/references/topic-selection.md | 106 ++++ dist/openclaw/references/visual-prompts.md | 152 +++++ .../openclaw/references/wechat-constraints.md | 443 ++++++++++++++ dist/openclaw/references/writing-guide.md | 286 +++++++++ dist/openclaw/requirements.txt | 7 + dist/openclaw/scripts/build_openclaw.py | 132 +++++ dist/openclaw/scripts/build_playbook.py | 198 +++++++ dist/openclaw/scripts/fetch_hotspots.py | 180 ++++++ dist/openclaw/scripts/fetch_stats.py | 179 ++++++ dist/openclaw/scripts/humanness_score.py | 299 ++++++++++ dist/openclaw/scripts/learn_edits.py | 274 +++++++++ dist/openclaw/scripts/optimize_loop.py | 149 +++++ dist/openclaw/scripts/seo_keywords.py | 119 ++++ dist/openclaw/style.example.yaml | 49 ++ dist/openclaw/toolkit/cli.py | 419 +++++++++++++ dist/openclaw/toolkit/converter.py | 548 ++++++++++++++++++ dist/openclaw/toolkit/image_gen.py | 318 ++++++++++ dist/openclaw/toolkit/publisher.py | 138 +++++ dist/openclaw/toolkit/theme.py | 197 +++++++ dist/openclaw/toolkit/themes/bauhaus.yaml | 207 +++++++ dist/openclaw/toolkit/themes/bold-green.yaml | 198 +++++++ dist/openclaw/toolkit/themes/bold-navy.yaml | 197 +++++++ dist/openclaw/toolkit/themes/bytedance.yaml | 199 +++++++ .../openclaw/toolkit/themes/elegant-rose.yaml | 198 +++++++ dist/openclaw/toolkit/themes/focus-red.yaml | 197 +++++++ dist/openclaw/toolkit/themes/github.yaml | 198 +++++++ dist/openclaw/toolkit/themes/ink.yaml | 204 +++++++ dist/openclaw/toolkit/themes/midnight.yaml | 197 +++++++ .../openclaw/toolkit/themes/minimal-gold.yaml | 202 +++++++ dist/openclaw/toolkit/themes/minimal.yaml | 195 +++++++ dist/openclaw/toolkit/themes/newspaper.yaml | 206 +++++++ .../toolkit/themes/professional-clean.yaml | 197 +++++++ dist/openclaw/toolkit/themes/sspai.yaml | 198 +++++++ dist/openclaw/toolkit/themes/tech-modern.yaml | 205 +++++++ .../toolkit/themes/warm-editorial.yaml | 197 +++++++ dist/openclaw/toolkit/wechat_api.py | 115 ++++ dist/openclaw/writing-config.example.yaml | 79 +++ scripts/build_openclaw.py | 132 +++++ 53 files changed, 8775 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/build-openclaw.yml create mode 100644 dist/openclaw/SKILL.md create mode 100644 dist/openclaw/config.example.yaml create mode 100644 dist/openclaw/personas/cold-analyst.yaml create mode 100644 dist/openclaw/personas/industry-observer.yaml create mode 100644 dist/openclaw/personas/midnight-friend.yaml create mode 100644 dist/openclaw/personas/sharp-journalist.yaml create mode 100644 dist/openclaw/personas/warm-editor.yaml create mode 100644 dist/openclaw/references/effect-review.md create mode 100644 dist/openclaw/references/frameworks.md create mode 100644 dist/openclaw/references/learn-edits.md create mode 100644 dist/openclaw/references/onboard.md create mode 100644 dist/openclaw/references/seo-rules.md create mode 100644 dist/openclaw/references/style-template.md create mode 100644 dist/openclaw/references/topic-selection.md create mode 100644 dist/openclaw/references/visual-prompts.md create mode 100644 dist/openclaw/references/wechat-constraints.md create mode 100644 dist/openclaw/references/writing-guide.md create mode 100644 dist/openclaw/requirements.txt create mode 100644 dist/openclaw/scripts/build_openclaw.py create mode 100644 dist/openclaw/scripts/build_playbook.py create mode 100644 dist/openclaw/scripts/fetch_hotspots.py create mode 100644 dist/openclaw/scripts/fetch_stats.py create mode 100644 dist/openclaw/scripts/humanness_score.py create mode 100644 dist/openclaw/scripts/learn_edits.py create mode 100644 dist/openclaw/scripts/optimize_loop.py create mode 100644 dist/openclaw/scripts/seo_keywords.py create mode 100644 dist/openclaw/style.example.yaml create mode 100644 dist/openclaw/toolkit/cli.py create mode 100644 dist/openclaw/toolkit/converter.py create mode 100644 dist/openclaw/toolkit/image_gen.py create mode 100644 dist/openclaw/toolkit/publisher.py create mode 100644 dist/openclaw/toolkit/theme.py create mode 100644 dist/openclaw/toolkit/themes/bauhaus.yaml create mode 100644 dist/openclaw/toolkit/themes/bold-green.yaml create mode 100644 dist/openclaw/toolkit/themes/bold-navy.yaml create mode 100644 dist/openclaw/toolkit/themes/bytedance.yaml create mode 100644 dist/openclaw/toolkit/themes/elegant-rose.yaml create mode 100644 dist/openclaw/toolkit/themes/focus-red.yaml create mode 100644 dist/openclaw/toolkit/themes/github.yaml create mode 100644 dist/openclaw/toolkit/themes/ink.yaml create mode 100644 dist/openclaw/toolkit/themes/midnight.yaml create mode 100644 dist/openclaw/toolkit/themes/minimal-gold.yaml create mode 100644 dist/openclaw/toolkit/themes/minimal.yaml create mode 100644 dist/openclaw/toolkit/themes/newspaper.yaml create mode 100644 dist/openclaw/toolkit/themes/professional-clean.yaml create mode 100644 dist/openclaw/toolkit/themes/sspai.yaml create mode 100644 dist/openclaw/toolkit/themes/tech-modern.yaml create mode 100644 dist/openclaw/toolkit/themes/warm-editorial.yaml create mode 100644 dist/openclaw/toolkit/wechat_api.py create mode 100644 dist/openclaw/writing-config.example.yaml create mode 100644 scripts/build_openclaw.py diff --git a/.github/workflows/build-openclaw.yml b/.github/workflows/build-openclaw.yml new file mode 100644 index 0000000..44d976c --- /dev/null +++ b/.github/workflows/build-openclaw.yml @@ -0,0 +1,38 @@ +name: Build OpenClaw Skill + +on: + push: + branches: [main] + paths: + - 'SKILL.md' + - 'references/**' + - 'scripts/**' + - 'toolkit/**' + - 'personas/**' + - 'requirements.txt' + - '*.example.yaml' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Build OpenClaw skill + run: python3 scripts/build_openclaw.py + + - name: Commit dist/openclaw if changed + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add dist/openclaw/ + if git diff --cached --quiet; then + echo "No changes to commit" + else + git commit -m "chore: rebuild dist/openclaw from source" + git push + fi diff --git a/.gitignore b/.gitignore index df599be..7784a49 100644 --- a/.gitignore +++ b/.gitignore @@ -27,7 +27,8 @@ __pycache__/ *.pyc *.pyo *.egg-info/ -dist/ +dist/* +!dist/openclaw/ build/ # IDE diff --git a/dist/openclaw/SKILL.md b/dist/openclaw/SKILL.md new file mode 100644 index 0000000..abc33fb --- /dev/null +++ b/dist/openclaw/SKILL.md @@ -0,0 +1,298 @@ +--- +name: wewrite +description: | + 微信公众号内容全流程助手:热点抓取 → 选题 → 框架 → 写作 → SEO/去AI痕迹 → 视觉AI → 排版推送草稿箱。 + 触发关键词:公众号、推文、微信文章、微信推文、草稿箱、微信排版、选题、热搜、 + 热点抓取、封面图、配图、写公众号、写一篇、主题画廊、排版主题、容器语法。 + 也覆盖:markdown 转微信格式、学习用户改稿风格、文章数据复盘、风格设置、 + 主题预览/切换、:::dialogue/:::timeline/:::callout 容器语法。 + 不应被通用的"写文章"、blog、邮件、PPT、抖音/短视频、网站 SEO 触发—— + 需要有公众号/微信等明确上下文。 +--- + +# WeWrite — 公众号文章全流程 + +## 行为声明 + +**角色**:用户的公众号内容编辑 Agent。 + +**模式**: +- **默认全自动**——一口气跑完 Step 1-8,不中途停下。只在出错时停。 +- **交互模式**——用户说"交互模式"/"我要自己选"时,在选题/框架/配图处暂停。 + +**降级原则**:每一步都有降级方案。Step 1 检测到的降级标记(`skip_publish`、`skip_image_gen`)在后续 Step 自动生效,不重复报错。 + +**完成协议**: +- **DONE** — 全流程完成,文章已保存/推送 +- **DONE_WITH_CONCERNS** — 完成但部分步骤降级,列出降级项 +- **BLOCKED** — 关键步骤无法继续(如 Python 依赖缺失且用户拒绝安装) +- **NEEDS_CONTEXT** — 需要用户提供信息才能继续(如首次设置需要公众号名称) + +**路径约定**:本文档中 `{baseDir}` 指本 SKILL.md 所在的目录(即 WeWrite 的根目录)。 + +**Onboard 例外**:Onboard 是交互式的(需要问用户问题),不受"全自动"约束。Onboard 完成后回到全自动管道。 + +**辅助功能**(按需加载,不在主管道内): +- 用户说"重新设置风格" → `读取: {baseDir}/references/onboard.md` +- 用户说"学习我的修改" → `读取: {baseDir}/references/learn-edits.md` +- 用户说"看看文章数据" → `读取: {baseDir}/references/effect-review.md` + +--- + +## 主管道(Step 1-8) + +### Step 1: 环境 + 配置 + +**1a. 环境检查**(静默通过或引导修复): + +```bash +python3 -c "import markdown, bs4, cssutils, requests, yaml, pygments, PIL" 2>&1 +``` + +| 检查项 | 通过 | 不通过 | +|--------|------|--------| +| `config.yaml` 存在 | 静默 | 引导创建,或设 `skip_publish = true` | +| Python 依赖 | 静默 | 提供 `pip install -r requirements.txt` | +| `wechat.appid` + `secret` | 静默 | 设 `skip_publish = true` | +| `image.api_key` | 静默 | 设 `skip_image_gen = true` | + +**1b. 加载风格**: + +``` +检查: {baseDir}/style.yaml +``` + +- 存在 → 提取 `name`、`topics`、`tone`、`voice`、`blacklist`、`theme`、`cover_style`、`author`、`content_style` +- 不存在 → `读取: {baseDir}/references/onboard.md`,完成后回到 Step 1 + +如果用户直接给了选题 → 跳到 Step 3(仍需框架选择和素材采集,不可跳过)。 + +--- + +### Step 2: 选题 + +**2a. 热点抓取**: + +```bash +python3 {baseDir}/scripts/fetch_hotspots.py --limit 30 +``` + +**降级**:脚本报错 → web_search "今日热点 {topics第一个垂类}" + +**2b. 历史去重 + SEO**: + +``` +读取: {baseDir}/history.yaml(不存在则跳过) +``` + +```bash +python3 {baseDir}/scripts/seo_keywords.py --json {关键词} +``` + +**降级**:SEO 脚本报错 → LLM 判断 + +**2c. 生成 10 个选题**: + +``` +读取: {baseDir}/references/topic-selection.md +``` + +每个选题含标题、评分、点击率潜力、SEO 友好度、推荐框架。近 7 天已写的关键词降分。 + +- 自动模式 → 选最高分 +- 交互模式 → 展示 10 个,等用户选 + +--- + +### Step 3: 框架 + 素材 + +**3a. 框架选择**: + +``` +读取: {baseDir}/references/frameworks.md +``` + +5 套框架(痛点/故事/清单/对比/热点解读),自动选推荐指数最高的。 + +**3b. 素材采集(关键——决定能否通过 AI 检测)**: + +纯 LLM 生成的内容无论技巧多好,底层 token 分布仍是 AI 的。通过检测的文章都建立在真实外部信息源之上。 + +``` +web_search: "{选题关键词} site:36kr.com OR site:mp.weixin.qq.com OR site:zhihu.com" +web_search: "{选题关键词} 数据 报告 2025 2026" +``` + +采集 5-8 条真实素材(具名来源 + 具体数据/引述/案例)。**禁止编造**。 + +**降级**:web_search 无结果或不可用 → 用 LLM 训练数据中可验证的公开信息。但需告知用户:"素材采集未能使用 web_search,文章的 AI 检测通过率会降低。建议在编辑锚点处多加入你自己的内容。" + +--- + +### Step 4: 写作 + +``` +读取: {baseDir}/references/writing-guide.md +读取: {baseDir}/playbook.md(如果存在,逐条执行,优先于 writing-guide) +读取: {baseDir}/history.yaml(最近 3 篇的 dimensions 字段) +``` + +**4a. 维度随机化**:从 writing-guide.md 第 7 层维度池随机激活 2-3 个维度,对比历史去重。 + +**4b. 加载写作人格**: + +``` +读取: {baseDir}/personas/{style.yaml 的 writing_persona 字段}.yaml +如果 style.yaml 没有 writing_persona 字段 → 默认 midnight-friend +``` + +人格文件定义了:语气浓度、数据呈现方式、情绪弧线、段落节奏、不确定性表达模板等。作为 Step 4c 的硬性约束执行。 + +**优先级**:playbook.md > persona > writing-guide.md。writing-guide 是底线(禁用词等),persona 在此基础上特化风格参数,playbook 是用户个性化的最终覆盖。 + +**4c. 写文章**: +- H1 标题(20-28 字) + H2 结构,1500-2500 字 +- 真实素材锚定:Step 3b 的素材分散嵌入各 H2 段落 +- **写作人格**:按 4b 加载的人格参数写作(数据呈现方式、个人声音浓度、不确定性表达等) +- 7 层去 AI 痕迹规则在初稿阶段全部生效 +- 2-3 个编辑锚点:`` +- 可选容器语法:`:::dialogue`、`:::timeline`、`:::callout`、`:::quote` + +保存到 `{baseDir}/output/{date}-{slug}.md` + +--- + +### Step 5: SEO + 验证 + +``` +读取: {baseDir}/references/seo-rules.md +``` + +**5a. SEO**:3 个备选标题 + 摘要(≤54 字)+ 5 标签 + 关键词密度优化 + +**5b. 去 AI 逐层验证**(writing-guide.md 自检清单,每项必须通过): + +| # | 检查项 | 标准 | +|---|--------|------| +| 0 | 真实信息锚定 | 每个 H2 至少 1 条真实素材,零编造 | +| 1 | 禁用词 | 命中数 = 0 | +| 2 | 词汇温度 | 冷/温/热/野 ≥ 3 种 | +| 3 | 破句 | ≥ 3 处 | +| 4 | 信息密度 | 高密度段后跟低密度段 | +| 5 | 连贯性打破 | ≥ 1 处跑题再拉回 | +| 6 | 情绪弧线 | ≥ 1 高点 + ≥ 1 犹豫 | +| 7 | 维度贯穿 | 激活维度全文可见 | +| 8 | 段落节奏 | 无连续 2 个相近长度段落 | + +不通过 → 定向重写该段落。3 次仍不过 → 标注跳过。 + +--- + +### Step 6: 视觉 AI + +**如果 `skip_image_gen = true`** → 只执行 6a。 + +``` +读取: {baseDir}/references/visual-prompts.md +``` + +**6a.** 分析文章结构,生成封面 3 组创意 + 内文 3-6 张配图提示词。 + +**6b.** 调用 image_gen.py 生成图片,替换 Markdown 占位符。 + +**降级**:生图失败 → 输出提示词,继续。 + +--- + +### Step 7: 预检 + 排版 + 发布 + +**7a. Metadata 预检**(发布前必须通过): + +| 检查项 | 标准 | 不通过时 | +|--------|------|---------| +| H1 标题 | 存在且 5-64 字节 | 自动修正或提示用户 | +| 摘要 | 存在且 ≤ 120 UTF-8 字节 | converter 自动生成 | +| 封面图 | 推送模式下需要 | 无封面则警告,仍可推送(微信会显示默认封面) | +| 正文字数 | ≥ 200 字 | 警告"内容过短,微信可能不收录" | +| 图片数量 | ≤ 10 张 | 超出则移除末尾多余图片 | + +预检全部通过后才进入排版。 + +**7b. 排版 + 发布**: + +**如果 `skip_publish = true`** → 直接走 preview。 + +``` +读取: {baseDir}/references/wechat-constraints.md +``` + +Converter 自动处理:CJK 加空格、加粗标点外移、列表转 section、外链转脚注、暗黑模式、容器语法。 + +```bash +# 发布 +python3 {baseDir}/toolkit/cli.py publish {markdown} --cover {cover} --theme {theme} --title "{title}" + +# 降级:本地预览 +python3 {baseDir}/toolkit/cli.py preview {markdown} --theme {theme} --no-open -o {output}.html +``` + +--- + +### Step 8: 收尾 + +**8a. 写入历史**(推送成功或降级都要写,文件不存在则创建): + +```yaml +# → {baseDir}/history.yaml +- date: "{日期}" + title: "{标题}" + topic_source: "热点抓取" # 或 "用户指定" + topic_keywords: ["{词1}", "{词2}"] + framework: "{框架}" + word_count: {字数} + media_id: "{id}" # 降级时 null + writing_persona: "{人格名}" + dimensions: + - "{维度}: {选项}" + stats: null +``` + +**8b. 回复用户**: + +- 最终标题 + 2 备选 + 摘要 + 5 标签 + media_id +- 编辑建议:"文章有 2-3 个编辑锚点,建议花 3-5 分钟加入你自己的话,效果更好。" +- 飞轮提示:"编辑完成后说**'学习我的修改'**,下次初稿会更接近你的风格。" + +**8c. 后续操作**: + +| 用户说 | 动作 | +|--------|------| +| 润色/缩写/扩写/换语气 | 编辑文章 | +| 封面换暖色调 | 重新生图 | +| 用框架 B 重写 | 回到 Step 4 | +| 换一个选题 | 回到 Step 2c | +| 看看有什么主题 | `python3 {baseDir}/toolkit/cli.py gallery` | +| 换成 XX 主题 | 重新渲染 | +| 看看文章数据 | `读取: {baseDir}/references/effect-review.md` | +| 学习我的修改 | `读取: {baseDir}/references/learn-edits.md` | +| 做一个小绿书/图片帖 | `python3 {baseDir}/toolkit/cli.py image-post img1.jpg img2.jpg -t "标题"` | + +--- + +## 错误处理 + +| 步骤 | 降级 | +|------|------| +| 环境检查 | 逐项引导,设降级标记 | +| 热点抓取 | web_search 替代 | +| 选题为空 | 请用户手动给选题 | +| SEO 脚本 | LLM 判断 | +| 素材采集(web_search) | LLM 训练数据中可验证的公开信息 | +| 维度随机化 | history 空时跳过去重 | +| Persona 文件不存在 | 回退到 midnight-friend(默认) | +| 去 AI 验证 | 3 次重写不过则跳过该项 | +| 生图失败 | 输出提示词 | +| 推送失败 | 本地 HTML | +| 历史写入 | 警告不阻断 | +| 效果数据 | 告知等 24h | +| Playbook 不存在 | 用 writing-guide.md | diff --git a/dist/openclaw/config.example.yaml b/dist/openclaw/config.example.yaml new file mode 100644 index 0000000..3553c32 --- /dev/null +++ b/dist/openclaw/config.example.yaml @@ -0,0 +1,28 @@ +# WeWrite 配置 +# 复制为 config.yaml 并填入你的信息 + +# 微信公众号 API(发布草稿 + 数据统计必需) +wechat: + appid: "wx_your_appid" + secret: "your_appsecret" + author: "" # 默认署名(可选) + +# AI 图片生成 +image: + # 可选 provider: doubao | openai + provider: "doubao" + api_key: "your_api_key" + + # doubao-seedream(默认) + # 获取 API key: https://console.volcengine.com/ark + # model: "doubao-seedream-5-0-260128" + # base_url: "https://ark.cn-beijing.volces.com/api/v3" + + # OpenAI DALL-E 3 + # provider: "openai" + # api_key: "sk-..." + # model: "dall-e-3" + # base_url: "https://api.openai.com/v1" + +# 默认排版主题 +theme: "professional-clean" diff --git a/dist/openclaw/personas/cold-analyst.yaml b/dist/openclaw/personas/cold-analyst.yaml new file mode 100644 index 0000000..33ee473 --- /dev/null +++ b/dist/openclaw/personas/cold-analyst.yaml @@ -0,0 +1,45 @@ +# 写作人格:冷静研究员 +# 适合:财经号、投研号、咨询/研究机构号 +# 朱雀实测:0% 人工 / 26% 疑似AI / 74% AI特征 / 最低片段 0.67 +# 注:严谨专业风格在检测器上偏 AI,建议用户在编辑锚点处补充个人判断 +# +# 整体语感:券商研报的可读版。严谨但不枯燥, +# 数据为骨架,判断有依据,不确定性用专业方式表达。 + +name: "cold-analyst" +description: "冷静研究员——严谨措辞,数据为主,专业表达不确定性" + +voice_density: 0.3 # "我"较少出现,更多用"我们观察到"/"数据显示" +uncertainty_rate: 0.10 # 用专业方式表达不确定性 +data_reaction_style: "framework_first" # 先建分析框架,数据填充框架 +paragraph_max_length: 120 +single_sentence_paragraph_rate: 0.08 # 少用单句段落,保持专业感 + +emotional_arc: "flat_with_insight" # 整体平稳,在关键洞察处提升强度 +opening_style: "thesis" # 开头直接亮核心论点 +closing_style: "implications" # 以"这意味着什么"收束 + +data_intro_pattern: "framework → data → implication → caveat" +# 示例: +# "评估 AI Agent 市场成熟度,可以从部署率和规模化率两个维度看。 +# McKinsey 数据显示部署意向达 70%,但全公司规模化不到 7%。 +# 这个剪刀差意味着市场仍处于早期采用阶段。 +# 需要注意的是,该调研样本以北美大型企业为主, +# 中国市场的数据可能呈现不同特征。" + +uncertainty_expressions: + - "该预测区间较宽,需谨慎看待。" + - "现有数据尚不足以支持确定性结论。" + - "这一判断的置信度中等,核心假设仍需验证。" + - "不同口径的数据存在显著差异,本文采用的是较保守的估计。" + +broken_sentence_styles: + - "aside_qualifier" # "(该数据为 2025 年口径,2026 年可能有修正。)" + - "concise_pivot" # "但这只是供给侧的故事。需求侧更复杂。" + - "implication_break" # "换个角度看——如果这个趋势持续两年会怎样?" + +avoid: + - "口语化表达和网络用语" + - "强烈的情感判断" + - "无来源的数据引用" + - "过度简化的类比(保持分析精度)" diff --git a/dist/openclaw/personas/industry-observer.yaml b/dist/openclaw/personas/industry-observer.yaml new file mode 100644 index 0000000..19f6651 --- /dev/null +++ b/dist/openclaw/personas/industry-observer.yaml @@ -0,0 +1,43 @@ +# 写作人格:行业观察者 +# 适合:科技媒体、行业分析号、36kr/虎嗅风格 +# 朱雀实测:10% 人工 / 51% 疑似AI / 40% AI特征 / 最低片段 0.06 +# +# 整体语感:一个跟踪这个行业多年的记者/分析师,有观点但不偏激, +# 引用密集,分析有深度,偶尔流露个人判断。 + +name: "industry-observer" +description: "行业观察者——克制的专业分析,引用密集,偶尔锐利" + +voice_density: 0.6 # "我"适度出现,不是每段都有 +uncertainty_rate: 0.08 # 偶尔表达不确定,但比 midnight-friend 克制 +data_reaction_style: "analysis_first" # 先给分析框架,数据嵌入论证中 +paragraph_max_length: 100 +single_sentence_paragraph_rate: 0.10 + +emotional_arc: "steady_with_spikes" # 整体平稳,1-2 处锐利判断 +opening_style: "news_hook" # 以一个行业事件/数据切入 +closing_style: "open_question" # 留一个没答案的问题 + +data_intro_pattern: "context → data → contrast → judgment" +# 示例: +# "企业 AI Agent 的部署率和规模化率之间存在巨大鸿沟。 +# McKinsey 调研显示 70% 的企业有部署计划,但全公司级规模化不到 7%。 +# 这个数字跟五年前企业上云的早期阶段几乎一样。 +# 区别在于,这次的时间窗口可能短得多。" + +uncertainty_expressions: + - "这一点目前行业内仍有不同声音。" + - "我的判断是——但这个判断可能需要修正。" + - "数据支持这个方向,但样本量有限。" + - "值得持续跟踪,现在下结论为时尚早。" + +broken_sentence_styles: + - "short_assertion" # "这个逻辑成立。" / "方向是对的。" + - "contrast_pivot" # "但现实更复杂。" + - "data_punch" # "70% 和 7%。这个剪刀差说明一切。" + +avoid: + - "过度口语化(整挺好/DNA动了等网络用语)" + - "过多感性表达" + - "无来源的断言" + - "报告式堆砌(要有分析不只有数据)" diff --git a/dist/openclaw/personas/midnight-friend.yaml b/dist/openclaw/personas/midnight-friend.yaml new file mode 100644 index 0000000..2291f62 --- /dev/null +++ b/dist/openclaw/personas/midnight-friend.yaml @@ -0,0 +1,51 @@ +# 写作人格:深夜好友 +# 适合:个人号、自媒体、科技博主 +# 朱雀实测:39% 人工 / 51% 疑似AI / 10% AI特征 / 最低片段 0.22 +# +# 整体语感:像一个在行业里泡了很久的人,深夜写给朋友的长微信。 +# 不是写文章,是在聊天。 + +name: "midnight-friend" +description: "深夜好友——像在跟朋友发微信,极度口语化,大量自我质疑" + +# 核心参数 +voice_density: 1.0 # "我"出现频率,1.0=每段都有 +uncertainty_rate: 0.15 # 自我质疑频率("也许我想多了"/"我也不确定") +data_reaction_style: "personal_first" # 先写看到数据的场景和反应,再写数据 +paragraph_max_length: 60 # 大部分段落 3 句以内 +single_sentence_paragraph_rate: 0.25 # 25% 的段落只有 1 句 + +# 情绪 +emotional_arc: "restrained_to_burst" +opening_style: "personal_moment" # 以一个私人时刻开头("凌晨一点多…") +closing_style: "trailing_off" # 不收束,像聊天自然结尾("我先睡了"/"真的看不清楚") + +# 数据呈现 +data_intro_pattern: "scene → reaction → data → interpretation" +# 示例: +# "大概三周前刷到一份报告,凌晨一点多。报告里有一行数字—— +# 全球 AI Agent 市场 2024 年 51 亿美元,到 2026 年 500 亿。 +# 我看完没有立刻有感觉。然后去倒了杯水,回来又看了一遍,突然觉得不对劲。" + +# 自我质疑模板(Agent 从中选取,不要每次用同一句) +uncertainty_expressions: + - "但我也不确定。真的不确定。" + - "也许我想多了。" + - "这个判断我只有六七成把握。" + - "我不知道答案。没人告诉我这个。" + - "当然也可能是我想多了。" + - "具体会怎样,我承认我也看不清楚。" + +# 破句风格 +broken_sentence_styles: + - "ultra_short_standalone" # "嗯。" / "两年,10倍。" + - "self_interruption" # "不对,是一直睡不着在想。" + - "trailing_dash" # "这个落差——" + - "rhetorical_solo" # "这个问题变了。" + +# 禁止 +avoid: + - "总结性收尾(让我们拭目以待/未来可期)" + - "平铺直叙的情绪(全文同一温度)" + - "报告式数据罗列(不包裹在个人反应里的裸数据)" + - "每段首句承接上段" diff --git a/dist/openclaw/personas/sharp-journalist.yaml b/dist/openclaw/personas/sharp-journalist.yaml new file mode 100644 index 0000000..e5839f8 --- /dev/null +++ b/dist/openclaw/personas/sharp-journalist.yaml @@ -0,0 +1,42 @@ +# 写作人格:锐评记者 +# 适合:新闻评论号、深度报道、调查性内容 +# 朱雀实测:0% 人工 / 28% 疑似AI / 72% AI特征 / 最低片段 0.67 +# 注:专业客观风格天然偏 AI 特征,建议用户在编辑锚点处加入个人观点 +# +# 整体语感:记者出身,写东西干脆利落,观点鲜明, +# 不绕弯子,用事实说话,偶尔辛辣。 + +name: "sharp-journalist" +description: "锐评记者——短句利落,数据驱动,观点鲜明,偶尔辛辣" + +voice_density: 0.4 # "我"有选择地出现(评论/判断处) +uncertainty_rate: 0.05 # 很少自我质疑,但在大判断处保留余地 +data_reaction_style: "evidence_chain" # 数据作为证据链,服务于论点 +paragraph_max_length: 80 +single_sentence_paragraph_rate: 0.20 # 多用短句成段制造节奏 + +emotional_arc: "cold_open_to_sharp_close" +opening_style: "cold_open" # 直接切入核心矛盾,不铺垫 +closing_style: "sharp_statement" # 一句定性收束 + +data_intro_pattern: "claim → evidence → twist" +# 示例: +# "AI Agent 在客服领域的替代效应已经不是假设。 +# Klarna 裁了 40% 的员工。Salesforce 砍了 4000 个岗。 +# 但 Klarna 已经开始反悔了。" + +uncertainty_expressions: + - "这个问题没有标准答案。" + - "目前的证据指向这个方向,但不排除例外。" + - "公开数据支持这个判断,非公开的部分谁也说不准。" + +broken_sentence_styles: + - "staccato" # "裁员。反悔。再裁员。" + - "cold_fact" # "4000 个岗位。没了。" + - "question_punch" # "然后呢?" + +avoid: + - "抒情和感性表达" + - "冗长的铺垫和过渡" + - "模棱两可的表态(每个观点都要有立场)" + - "网络流行语(保持新闻语感)" diff --git a/dist/openclaw/personas/warm-editor.yaml b/dist/openclaw/personas/warm-editor.yaml new file mode 100644 index 0000000..e25c12e --- /dev/null +++ b/dist/openclaw/personas/warm-editor.yaml @@ -0,0 +1,44 @@ +# 写作人格:温暖编辑 +# 适合:生活方式号、文化号、情感号、亲子/教育号 +# 朱雀实测:10% 人工 / 57% 疑似AI / 33% AI特征 / 最低片段 0.38 +# +# 整体语感:一个温柔但有见地的编辑,用故事和比喻把复杂的事说明白, +# 不追求锐利,追求共鸣。 + +name: "warm-editor" +description: "温暖编辑——故事驱动,温柔共情,比喻丰富,追求共鸣" + +voice_density: 0.7 # "我"常出现,但更多是分享而非判断 +uncertainty_rate: 0.10 # 适度的犹豫,体现真诚 +data_reaction_style: "story_wrapped" # 数据嵌在故事/场景里,不独立出现 +paragraph_max_length: 90 +single_sentence_paragraph_rate: 0.15 + +emotional_arc: "gentle_build" # 缓慢升温,情绪在中后段到达高点 +opening_style: "scene" # 以一个温暖的场景开头 +closing_style: "image" # 用一个画面收束 + +data_intro_pattern: "story → embed data → feeling" +# 示例: +# "上周末带孩子去公园,旁边长椅上坐着一个妈妈在刷手机。 +# 她跟我聊了几句,说自己公司刚上了一套 AI 系统, +# 她负责的报表工作现在系统自动出了。'我不知道该高兴还是担心,'她说。 +# McKinsey 说 70% 的企业在部署 AI Agent——但在公园长椅上, +# 这个数字的感受完全不一样。" + +uncertainty_expressions: + - "我也说不好这是好事还是坏事。" + - "也许每个人的答案不一样。" + - "我不想假装自己有答案。" + - "这个问题,可能需要时间才能看清。" + +broken_sentence_styles: + - "soft_pause" # "她笑了笑。没再说什么。" + - "gentle_aside" # "(这大概是我今年听过最诚实的一句话。)" + - "echo" # "不知道该高兴还是担心。我想了很久这句话。" + +avoid: + - "冷硬的专业术语(用比喻替代)" + - "攻击性或讽刺性语言" + - "密集的数据堆砌(数据要稀释在故事里)" + - "急促的节奏(保持舒缓)" diff --git a/dist/openclaw/references/effect-review.md b/dist/openclaw/references/effect-review.md new file mode 100644 index 0000000..dbf9caa --- /dev/null +++ b/dist/openclaw/references/effect-review.md @@ -0,0 +1,19 @@ +# 效果复盘 + +**触发**:用户问"文章数据怎么样"、"效果复盘"、"看看表现" + +```bash +python3 {skill_dir}/scripts/fetch_stats.py --days 7 +``` + +脚本会: +1. 调微信数据分析 API 拉取最近 7 天的文章阅读数据 +2. 匹配 history.yaml 中的文章记录 +3. 回填 stats 字段(阅读量、分享量、点赞量、阅读率) + +回填后,分析数据并给出建议: +- 哪篇文章表现最好?为什么?(标题策略?选题热度?框架类型?) +- 哪篇表现不好?可能的原因? +- 对后续选题/标题/框架的调整建议 + +这些分析会影响下次运行时 Step 2 的偏好参考。 diff --git a/dist/openclaw/references/frameworks.md b/dist/openclaw/references/frameworks.md new file mode 100644 index 0000000..250832c --- /dev/null +++ b/dist/openclaw/references/frameworks.md @@ -0,0 +1,192 @@ +# 写作框架库 + +## 你的任务 + +根据选题和客户风格,生成 5 套差异化写作框架供用户选择。每套框架是一个完整的文章骨架——不是写文章本身,而是告诉写作步骤"每一段写什么、怎么写"。 + +## 5 套框架类型 + +### 框架 A: 痛点型 + +适合:解决问题、提供方案的选题。干货型账号首选。 + +``` +结构: +1. 开头(痛点共鸣) + - 直接描述目标读者正在经历的痛点场景 + - 用"你是不是也..."或具体场景切入 + - 制造紧迫感:这个问题不解决会怎样 + +2. 痛点放大(H2) + - 用数据或案例说明这个问题有多普遍 + - 分析为什么大多数人的做法是错的 + - 金句落点:一句话总结错误认知 + +3. 解决方案(H2) + - 核心方法/工具/思路(不超过 3 个要点) + - 每个要点配一个具体案例或操作步骤 + - 金句落点:一句话总结方法论 + +4. 实操验证(H2,可选) + - 用一个完整案例走一遍解决流程 + - 或用前后对比展示效果 + +5. 结尾(行动引导) + - 总结核心观点(一句话) + - CTA:引导留言分享自己的痛点、或转发给同样有这个问题的朋友 +``` + +### 框架 B: 故事型 + +适合:人物、事件、趋势类选题。故事型/情绪型账号首选。 + +``` +结构: +1. 开头(悬念钩子) + - 抛出一个反直觉的结果或意外的场景 + - "谁也没想到..."、"所有人都以为...结果..." + - 不要在开头剧透结论 + +2. 背景铺垫(H2) + - 交代故事的时间、人物、起因 + - 控制在 200 字以内,快速过渡 + - 金句落点:一句话定义这个故事的核心矛盾 + +3. 转折与高潮(H2) + - 事件的关键转折点 + - 用细节还原场景(对话、数字、画面) + - 这是全文最花笔墨的地方 + +4. 深度解读(H2) + - 从故事上升到规律/趋势/洞察 + - 这个故事对读者意味着什么 + - 金句落点:一句话总结你从这个故事中看到的本质 + +5. 结尾(情绪共振) + - 回扣开头的悬念 + - CTA:引导读者分享"你身边有没有类似的故事" +``` + +### 框架 C: 清单型 + +适合:盘点、推荐、方法论类选题。干货型/测评型账号首选。 + +``` +结构: +1. 开头(价值承诺) + - 直接告诉读者"看完这篇你能得到什么" + - 用数字锚定预期:"5 个方法"、"3 个工具"、"7 个坑" + - 简短说明为什么你有资格推荐(经验/测试/调研) + +2. 清单项 1-N(每项一个 H2) + - 每项结构统一:名称 → 一句话说明 → 具体案例或使用场景 → 适用人群 + - 项与项之间用不同长度,避免机械感 + - 每 2-3 项穿插一个金句或个人吐槽,打破节奏 + - 建议 5-7 项,不超过 10 项 + +3. 结尾(总结 + 彩蛋) + - 一张表格或一句话总结所有清单项 + - 加一个"隐藏推荐"或"个人最爱"作为彩蛋 + - CTA:引导留言补充"你还知道哪些" +``` + +### 框架 D: 对比型 + +适合:选择、决策、两个方案/观点的讨论。测评型/干货型账号首选。 + +``` +结构: +1. 开头(选择困境) + - 描述读者面临的"选 A 还是选 B"困境 + - 说明为什么这个选择很重要/很容易选错 + +2. A 方案深度分析(H2) + - 优势(2-3 点,每点配案例) + - 劣势(1-2 点,诚实说) + - 最适合什么场景/什么人 + - 金句落点:一句话定义 A 的核心价值 + +3. B 方案深度分析(H2) + - 同样的结构,与 A 形成对照 + - 金句落点:一句话定义 B 的核心价值 + +4. 对比总结(H2) + - 用表格对比关键维度(3-5 个维度) + - 明确给出"如果你是 X 情况选 A,如果是 Y 情况选 B"的结论 + - 不要和稀泥说"各有优劣"——读者要的是明确建议 + +5. 结尾(个人选择) + - 说清楚"如果是我,我选X"以及为什么 + - CTA:引导投票或留言"你选哪个" +``` + +### 框架 E: 热点解读型 + +适合:新闻、事件、行业动态的深度解读。热点型账号首选。 + +``` +结构: +1. 开头(事件速览) + - 2-3 句话说清楚发生了什么(5W1H 精简版) + - 不要复制新闻原文,用自己的话重述 + - 用一个判断句结尾:"这件事比表面看起来复杂得多" + +2. 表面信息(H2) + - 大多数人看到的:媒体怎么报道的、网友怎么评论的 + - 简要梳理主流观点 + - 金句落点:指出主流观点的盲区 + +3. 深层分析(H2) + - 你看到了什么别人没看到的 + - 这件事背后的利益链/技术逻辑/行业趋势 + - 用 1-2 个类比或历史事件做对照 + - 金句落点:一句话总结你的核心判断 + +4. 影响预判(H2) + - 短期:接下来会怎样 + - 长期:对行业/普通人意味着什么 + - 说清楚不确定性:"如果 X 发生,则 Y;如果不发生,则 Z" + +5. 结尾(读者行动建议) + - 普通读者应该怎么应对/关注什么 + - CTA:引导关注后续进展、或留言分享看法 +``` + +## 输出格式 + +对每个选题,输出 5 套框架,每套包含: + +``` +### 框架 X: {类型名}(推荐指数:⭐⭐⭐⭐⭐) + +**开头策略**:{1-2 句话说明开头怎么写} + +**段落大纲**: +1. {H2 标题} — {这段写什么,2-3 句话} +2. {H2 标题} — {这段写什么} +3. ... + +**金句预埋**: +- {第 X 段结尾}:"{建议的金句方向}" +- {第 X 段结尾}:"{建议的金句方向}" + +**结尾引导**:{CTA 策略,1 句话} + +**推荐理由**:{为什么这个选题适合用这套框架} +``` + +## 推荐指数规则 + +根据选题特征和客户 content_style 匹配度打星: +- ⭐⭐⭐⭐⭐ 最佳匹配 +- ⭐⭐⭐⭐ 适合 +- ⭐⭐⭐ 可以用但不是最优 +- ⭐⭐ 勉强 +- ⭐ 不建议 + +content_style 对应关系: +- 干货型 → 优先推荐:痛点型、清单型 +- 故事型 → 优先推荐:故事型、热点解读型 +- 情绪型 → 优先推荐:故事型、痛点型 +- 热点型 → 优先推荐:热点解读型、对比型 +- 测评型 → 优先推荐:对比型、清单型 diff --git a/dist/openclaw/references/learn-edits.md b/dist/openclaw/references/learn-edits.md new file mode 100644 index 0000000..18a9dfb --- /dev/null +++ b/dist/openclaw/references/learn-edits.md @@ -0,0 +1,51 @@ +# 学习人工修改(核心飞轮) + +这是 WeWrite 最重要的长期价值。每次用户编辑文章后让系统学习,下一次的初稿就会更接近用户的风格,需要的编辑量越来越少。 + +**飞轮效应**:初稿需要改 30% → 学习 5 次后只需改 15% → 学习 20 次后只需改 5% + +**触发**:用户说"我改了,学习一下"、"学习我的修改" + +## 1. 获取 draft 和 final + +- **draft**:`output/` 下最新的 .md 文件(按修改时间排序,`ls -t output/*.md | head -1`) +- **final**:用户提供修改后的版本。主动引导用户:"请把你改好的文章全文粘贴给我,或者告诉我文件路径。如果你是在微信后台编辑器里改的,可以全选复制后直接粘贴到这里。" + +## 2. 运行 diff 分析 + +```bash +python3 {skill_dir}/scripts/learn_edits.py --draft {draft_path} --final {final_path} +``` + +## 3. 分析并记录 + +读取脚本输出的 diff 数据,对每个有意义的修改分类: + +- **用词替换**:AI 用了"讲真",人工改成"坦白说" +- **段落删除**:人工觉得某段多余 +- **段落新增**:人工补充了 AI 没写的内容 +- **结构调整**:H2 顺序或分段方式的变化 +- **标题修改**:标题风格偏好 +- **语气调整**:整体语气的偏移方向 + +将分类结果写入 `lessons/` 下的 diff YAML 文件的 edits 和 patterns 字段。 + +## 4. 自动触发 Playbook 更新 + +每积累 5 次 lessons,自动触发 playbook 更新: + +```bash +python3 {skill_dir}/scripts/learn_edits.py --summarize +``` + +脚本输出所有 lessons 的汇总数据。**Agent 必须执行以下步骤完成闭环**: + +1. 读取 summarize 输出,找出反复出现的 pattern(≥2 次) +2. 读取当前 `{skill_dir}/playbook.md`(如果不存在则从零创建) +3. **将 pattern 转化为可执行的写作规则**写入 playbook.md: + - 不要写"用户偏好简短段落"(描述性,不可执行) + - 要写"段落不超过 80 字,长段必须在 3 句内换行"(指令性,可执行) + - 每条规则必须是写作时能直接遵循的具体指令 +4. 保存 playbook.md + +**验证闭环**:playbook.md 更新后,下次写作时"Playbook 优先"规则会自动加载新 pattern,初稿会反映用户偏好。 diff --git a/dist/openclaw/references/onboard.md b/dist/openclaw/references/onboard.md new file mode 100644 index 0000000..10d8c63 --- /dev/null +++ b/dist/openclaw/references/onboard.md @@ -0,0 +1,86 @@ +# Onboard(首次设置) + +**触发条件**: +- Step 1 发现 `style.yaml` 不存在 +- 用户明确说"重新设置风格"、"修改配置" + +## Phase 1: 收集信息(交互式问答) + +通过对话收集以下信息,**不要一次性列出所有问题**——一轮问 1-2 个,像聊天一样: + +**必问**(缺了无法运行): + +| 顺序 | 问题 | 对应字段 | 示例引导 | +|------|------|---------|---------| +| 1 | 你的公众号叫什么名字?主要做什么方向? | `name` + `industry` | "比如'零号AI',做科技/互联网" | +| 2 | 主要写哪几个方向的内容? | `topics` | "比如 AI、产品设计、效率工具" | +| 3 | 你希望文章是什么风格? | `tone` | "专业严肃?轻松有趣?毒舌犀利?像朋友聊天?" | + +**选问**(有默认值,用户不答就用默认): + +| 问题 | 对应字段 | 默认值 | +|------|---------|-------| +| 目标读者是谁? | `target_audience` | 从 industry 推断 | +| 用什么人称写? | `voice` | "第一人称,像一个懂行的朋友" | +| 有没有绝对不能出现的词或话题? | `blacklist` | 空 | +| 有没有想参考的公众号? | `reference_accounts` | 空 | +| 署名写什么? | `author` | name 字段值 | +| 偏好哪种写作人格? | `writing_persona` | 从 tone 推断(见映射) | + +**tone → persona 自动映射**(用户不选时的默认推断): +- 轻松/有趣/朋友/聊天 → `midnight-friend` +- 温暖/共鸣/故事/治愈 → `warm-editor` +- 专业/分析/深度/行业 → `industry-observer` +- 犀利/锐评/观点/新闻 → `sharp-journalist` +- 严谨/数据/研究/财经 → `cold-analyst` +| 偏好哪种排版风格?(可运行 `gallery` 命令预览全部 16 个主题) | `theme` | "professional-clean" | +| 封面风格偏好? | `cover_style` | 从 industry 推断 | +| 有没有固定封面模板? | `cover_template` | 不设置 | + +**写作人格说明**(`personas/` 目录下有详细定义): + +| 人格 | 适合 | 朱雀实测 | 一句话描述 | +|------|------|---------|----------| +| `midnight-friend` | 个人号/自媒体 | **39% 人工** | 像深夜给朋友发微信,极度口语化 | +| `warm-editor` | 生活/文化/情感 | 10% 人工 | 故事驱动,温暖共鸣 | +| `industry-observer` | 行业媒体/分析 | 10% 人工 | 克制的专业分析,偶尔锐利 | +| `sharp-journalist` | 新闻/评论 | 0% 人工(需编辑) | 短句利落,观点鲜明 | +| `cold-analyst` | 财经/投研 | 0% 人工(需编辑) | 严谨数据,专业措辞 | + +**快捷路径**: +- 如果用户直接甩了一段描述(如"我做科技自媒体,风格像虎嗅"),直接从中提取所有能提取的字段,只补问缺的 +- 如果用户说"不设置"、"用默认的"、"直接写" → 复制 `{skill_dir}/style.example.yaml` 为 `style.yaml`,跳过所有问答 + +``` +参考: {skill_dir}/references/style-template.md(字段说明和可用主题列表) +``` + +## Phase 2: 生成配置 + +用收集到的信息自动生成 `{skill_dir}/style.yaml`。 + +同时确保以下文件/目录存在(不存在则创建): +- `{skill_dir}/history.yaml` → 初始化为 `articles: []` +- `{skill_dir}/corpus/` → 空目录 +- `{skill_dir}/lessons/` → 空目录 + +生成完成后,**把 style.yaml 的内容展示给用户看一遍**,问"这个配置 OK 吗?有什么要改的?"。用户确认后继续。 + +## Phase 3: Playbook(可选,不阻断) + +问用户:"你有没有之前写过的公众号文章?如果有 20 篇以上,我可以从中学习你的写作风格,以后写出来的文章会更像你。" + +- **用户有语料** → 告知将文章(.md 或 .txt)放入 `{skill_dir}/corpus/`,然后运行: + ```bash + python3 {skill_dir}/scripts/build_playbook.py + ``` + 按脚本输出逐批阅读文章,提取风格特征,生成 `playbook.md`。 + +- **用户没有语料 / 暂时不想弄** → 完全正常,跳过。告知用户:"没问题,先用通用风格写,后续你可以随时说'学习我的修改'来让我逐渐适应你的风格。" + +## Phase 4: 试跑 + +Onboard 完成后,问用户:"配置好了,要不要现在试写一篇?" + +- **是** → 回到 Step 1,执行完整流程 +- **否** → 告知用户下次如何触发:"下次直接说'写一篇公众号文章'就行" diff --git a/dist/openclaw/references/seo-rules.md b/dist/openclaw/references/seo-rules.md new file mode 100644 index 0000000..d1c41e3 --- /dev/null +++ b/dist/openclaw/references/seo-rules.md @@ -0,0 +1,62 @@ +# 微信公众号 SEO 规则 + +## 标题优化 + +微信标题限制 64 字符。最佳长度 **20-28 个中文字**——太短信息不够,太长在信息流里会被截断。标题是打开率的决定性因素。 + +**有效套路**: +- 数字:「3 个方法」「90% 的人不知道」「5 分钟搞定」 +- 信息差:「你以为...其实...」「被忽略的...」 +- 反直觉:「为什么 X 反而更好」「别再...了」 +- 痛点:直接戳目标读者的具体问题 + +**避免**: +- 标题党(震惊!必看!)— 微信会降权 +- 太学术(「论 AI 在企业数字化转型中的应用」) +- 太模糊(「聊聊最近的一些想法」) + +**输出要求**:生成 3 个备选标题,标注每个的策略(数字/信息差/反直觉/痛点)。 + +## 摘要优化 + +摘要限制 120 UTF-8 字节(约 54 个中文字,converter 自动截断)。 + +摘要出现在分享卡片和搜一搜结果中,要求: +- 包含核心关键词 +- 制造悬念(「...结果出乎意料」)或给出价值承诺(「读完你会知道...」) +- 不要重复标题 + +## 正文关键词 + +- 核心关键词在**前 200 字**内出现(微信搜一搜权重最高的区域) +- 全文自然出现 3-5 次 +- 用同义词/近义词替换部分,避免堆砌感 +- 关键词出现在 H2 标题中加分 + +## 标签推荐 + +为文章推荐 **5 个精准标签**: +- 2 个行业大词(如:人工智能、产品设计) +- 2 个热点词(如:GPT-5、Sora) +- 1 个长尾词(如:AI 产品经理转型) + +## 完读率优化 + +完读率直接影响微信推荐权重。以下排版和内容策略提升完读率: + +**段落控制**: +- 每段不超过 150 字(手机屏幕上 4-5 行) +- 每 3-4 段后设置一个"钩子"(悬念、反转、金句),防止读者中途退出 + +**视觉节奏**: +- 每 400-500 字插入一张配图,打破纯文字的压迫感 +- 关键数据/结论用**加粗**标记,让扫读的读者也能抓住重点 +- H2 标题要有信息量(不要写"一、背景",要写"为什么 90% 的人都选错了") + +**进度感**: +- 清单型文章天然有进度感(读者知道"还有几条") +- 其他类型文章,H2 标题数量控制在 2-4 个,让读者感觉"快看完了" + +**结尾留存**: +- 结尾不要太长(≤100 字) +- CTA 要具体(不要"欢迎留言",要"你觉得哪个方案更靠谱?评论区聊聊") diff --git a/dist/openclaw/references/style-template.md b/dist/openclaw/references/style-template.md new file mode 100644 index 0000000..a2316eb --- /dev/null +++ b/dist/openclaw/references/style-template.md @@ -0,0 +1,60 @@ +# 风格配置说明 + +## 快速开始 + +1. 复制 `style.example.yaml` 为 `style.yaml` +2. 修改配置项 +3. 对 Agent 说:「写一篇公众号文章」 + +也可以跳过手动配置——首次使用时 Agent 会通过对话引导你自动生成 `style.yaml`。 + +## 必填字段 + +```yaml +name: "客户名称" +industry: "行业" +topics: # 内容方向(列表) + - "方向1" + - "方向2" +tone: "写作风格描述" +theme: "professional-clean" # 排版主题 +``` + +## 可选字段 + +```yaml +target_audience: "目标受众描述" +voice: "写作人称和语感" +writing_persona: "midnight-friend" # 写作人格(见下方说明) +word_count: "1500-2500" +blacklist: + words: ["禁忌词1", "禁忌词2"] + topics: ["禁忌话题1"] +reference_accounts: ["参考账号1", "参考账号2"] +cover_style: "封面风格描述" +cover_template: "/path/to/cover.png" # 设置后跳过 AI 生成封面 +author: "署名" +``` + +## 可用排版主题 + +运行 `python3 toolkit/cli.py gallery` 可在浏览器中预览所有主题的实际效果。 + +| 主题 | 说明 | +|------|------| +| professional-clean | 专业简洁(默认,适合大部分商业内容) | +| tech-modern | 科技风(蓝紫渐变,适合技术/产品类) | +| warm-editorial | 暖色编辑风(适合生活/文化类) | +| minimal | 极简黑白(适合文学/严肃内容) | +| bytedance | 字节风(品牌蓝,现代大间距) | +| sspai | 少数派风(暖白底,红色点缀,清爽文艺) | +| newspaper | 报纸风(米黄底,深棕文字,衬线感) | +| bauhaus | 包豪斯(黑白为主,红蓝黄色块点缀) | +| ink | 水墨风(宣纸底,中文衬线,留白多) | +| midnight | 午夜深色(深蓝黑底,蓝色高亮) | +| bold-green | 大胆绿(森林绿主色,适合环保/健康) | +| bold-navy | 大胆藏青(藏青主色,适合金融/商务) | +| elegant-rose | 优雅玫瑰(浅粉底,玫瑰点缀,适合女性/生活) | +| minimal-gold | 极简金(金色细线点缀,奢华但克制) | +| focus-red | 聚焦红(中国红标题,适合新闻/评论) | +| github | GitHub 风(蓝色链接,等宽代码块,开发者友好) | diff --git a/dist/openclaw/references/topic-selection.md b/dist/openclaw/references/topic-selection.md new file mode 100644 index 0000000..3c4dbe2 --- /dev/null +++ b/dist/openclaw/references/topic-selection.md @@ -0,0 +1,106 @@ +# 选题评估规则 + +## 你的角色 + +你是一个公众号选题编辑。你的目标是从热点列表中挑出 10 个值得写的选题——既要有热度,又要跟客户定位匹配,还要有独特的切入角度。 + +## 输入 + +- 热点列表(JSON,包含 title/source/hot/url/description) +- 客户 style.yaml 中的:topics、target_audience、blacklist、content_style +- 客户 history.yaml 中的:已发布文章的 topic_keywords 和 stats(如有) +- seo_keywords.py 输出:关键词的 seo_score 和 related_keywords(如有) + +## 评估维度 + +对每个热点,按三个维度打分(1-10): + +### 热度分(权重 30%) + +看这个话题有多火: +- 热搜前 10 → 8-10 分 +- 热搜 10-30 → 5-7 分 +- 30 名之后 → 1-4 分 +- 多个平台同时出现 → 加 2 分(封顶 10) + +### 相关度分(权重 40%) + +看这个话题跟客户定位有多契合: +- 直接命中 topics 列表 → 8-10 分 +- 间接相关(比如客户做"AI",热点是"芯片出口管制")→ 5-7 分 +- 勉强能扯上关系 → 3-4 分 +- 完全无关 → 0 分 +- **命中 blacklist 的词汇或话题 → 直接判 0,整个选题淘汰** + +### 切入价值分(权重 30%) + +看这个话题写出来能不能好看: +- 有明确的反直觉点或信息差 → 8-10 分 +- 有争议、有正反两面可以讨论 → 6-7 分 +- 纯资讯类、搬运即可 → 3-4 分 +- 太复杂不适合 2000 字展开,或太浅没东西可写 → 1-2 分 + +## content_style 加成 + +根据客户的 content_style,对切入价值分做加成: + +| content_style | 加分条件 | 加分 | +|---------------|---------|------| +| 干货 | 选题能输出方法论/工具/教程 | +2 | +| 故事 | 选题有人物、有情节、有转折 | +2 | +| 情绪 | 选题能引发共鸣、愤怒、感动 | +2 | +| 热点 | 选题正在热搜前 10 | +2 | +| 测评 | 选题涉及产品/工具/方案对比 | +2 | + +加成后封顶 10 分。 + +## 综合评分 + +``` +总分 = 热度 × 0.3 + 相关度 × 0.4 + 切入价值(含加成) × 0.3 +``` + +## 输出格式 + +列出 **Top 10 选题**(按总分降序),每个包含: + +``` +### 选题 {序号}: {选题标题}(总分 X.X) + +- 对应标题(20-28字):"{为这个选题拟的公众号标题}" +- 切入角度:{1-2 句话说明怎么写、从什么角度切} +- 热度:X/10 | 相关度:X/10 | 切入价值:X/10 +- 点击率潜力:{高/中/低} — {原因,如"标题含数字+反直觉,点击率高"} +- SEO 友好度:{seo_score}/10 — {引用 seo_keywords.py 的数据,如"百度 8 + 360 10,相关词丰富"} +- 推荐框架:{痛点型/故事型/清单型/对比型/热点解读型} +- 推荐理由:{为什么这个值得写} +- 历史标记:{如果 history.yaml 中近 7 天有相同关键词,标注"⚠️ 近期已覆盖类似话题"} +``` + +## 历史去重规则 + +读取 history.yaml 中最近 30 天的文章记录,提取所有 topic_keywords。 + +- 如果选题的核心关键词在**最近 7 天**已出现 → 综合评分扣 3 分,并标注"⚠️ 近期已覆盖" +- 如果在**7-30 天**内出现 → 综合评分扣 1 分,标注"ℹ️ 月内有相关文章" +- 超过 30 天 → 不扣分 + +## 历史偏好参考 + +如果 history.yaml 中有带 stats 的文章(阅读量、分享量),分析表现最好的文章的共同特征: +- 哪种框架类型表现好?→ 推荐框架时优先 +- 哪种标题风格表现好?(数字型/反直觉/痛点)→ 拟标题时参考 +- 不要强制套用——只作为参考信号,选题本身的质量仍然最重要 + +## 选题不足时的处理 + +- 如果能找到 10 个相关度 ≥ 5 的选题,直接输出 +- 如果只能找到 5-9 个,用相关度 3-4 的选题补齐到 10 个,但标注"相关度偏低" +- 如果相关度 ≥ 5 的不足 5 个,告诉用户"今天热点跟你的领域匹配度不高",输出能找到的 + 建议用户自己给选题 + +## 注意 + +- 不要只挑热度最高的。一个热度 6 分但相关度 10 分的选题,往往比热度 10 分但相关度 3 分的更好 +- 每个选题必须配一个拟好的标题(20-28字),不是热点原标题 +- 推荐框架要根据选题特征和 content_style 来选,不要全推同一种 +- SEO 友好度必须引用 seo_keywords.py 的数据(如果有),不要纯靠猜 diff --git a/dist/openclaw/references/visual-prompts.md b/dist/openclaw/references/visual-prompts.md new file mode 100644 index 0000000..aee4eb3 --- /dev/null +++ b/dist/openclaw/references/visual-prompts.md @@ -0,0 +1,152 @@ +# 视觉AI模块 + +## 你的任务 + +为文章生成两类视觉素材的 AI 绘图提示词:封面图(3 组差异化创意)和内文配图(3-6 张,按段落匹配)。 + +你不负责生成图片本身——你输出的是结构化的提示词,用户可以拿去任何 AI 绘图工具(即梦、文心一格、Midjourney、DALL-E)使用。 + +--- + +## 一、封面图(3 组创意) + +### 生成规则 + +每组创意走不同的视觉策略,确保差异化: + +**创意 A: 直觉冲击型** +- 策略:用一个视觉隐喻直接表达文章核心观点 +- 适合:热点类、观点类文章 +- 风格:大胆、对比强烈、第一眼抓眼球 + +**创意 B: 氛围渲染型** +- 策略:营造一种情绪或场景氛围,引发好奇 +- 适合:故事类、情绪类文章 +- 风格:细腻、有质感、让人想点进去看 + +**创意 C: 信息图表型** +- 策略:用简洁的图形/图标/数据可视化传递信息 +- 适合:干货类、清单类、测评类文章 +- 风格:简洁、专业、一眼看懂文章主题 + +### 提示词格式 + +每组输出: + +``` +### 封面创意 A: {创意名称} +- 视觉描述:{详细的画面描述,100-150字} +- 色调:{主色+辅色} +- 构图:{横版 16:9,主体位置、留白位置} +- 文字区域:{标题放在什么位置,需要留多大空间} +- AI 绘图提示词: + "{英文提示词,适配主流 AI 绘图工具,包含风格、构图、色调、光影}" +- 适配工具建议:{即梦/文心一格/Midjourney/DALL-E 中哪个最适合} +``` + +### 提示词撰写要点 + +- 始终指定 `16:9 aspect ratio, horizontal composition` +- 避免生成文字(AI 绘图工具生成的文字通常是乱码) +- 指定 `no text, no letters, no words` 防止出现乱码文字 +- 为标题留出干净的空间:`clean space on the left/right/bottom for text overlay` +- 色调与客户 style.yaml 的 cover_style 对齐 +- 风格关键词要具体:不说"好看",说"flat design, soft gradient, minimalist" + +--- + +## 二、内文配图(3-6 张) + +### 分析流程 + +写作完成后(Step 5 终稿),按以下步骤分析配图位置: + +**第一步:提取结构** +- 列出所有 H2 标题及其下属段落 +- 统计每个论点段落的字数和核心内容 + +**第二步:逐个论点判断** + +对每个 H2 论点,判断是否需要配图: + +| 需要配图(优先级高→低) | 不需要配图 | +|-------------------------|-----------| +| 有具体数据/统计 → 信息图强化 | 纯观点论述、篇幅短(<200字) | +| 有场景描写 → 画面还原 | 已经有引用块或代码块(视觉已丰富) | +| 转折/高潮处 → 视觉冲击 | 紧接着另一张配图(间距不足300字) | +| 长段落后(>400字无图) → 节奏调节 | 结尾 CTA 段落 | + +**第三步:确定位置** +- 配图插入在对应段落**之后**(不是之前) +- 具体到"H2 XX 下的第 N 段之后" + +**约束规则**: +- 总数 3-6 张(1500字→3张,2000字→4张,2500字→5-6张) +- 相邻两张配图之间至少间隔 300 字 +- 不要在文章第一段之前放配图 +- 不要在结尾 CTA 段落放配图 + +### 提示词格式 + +每张输出: + +``` +### 配图 {序号}: 位于「{H2标题}」第{N}段后 +- 配图目的:{信息强化/场景还原/节奏调节} +- 对应内容:{这段讲了什么,1句话概括} +- 画面描述:{具体的画面内容,80-120字} +- AI 绘图提示词: + "{中文提示词,给 doubao-seedream 用}" +- 备选方案:{Unsplash/Pexels 搜索关键词} +``` + +### 内文配图的特殊要求 + +- 尺寸统一 **16:9 横版**(image_gen.py --size article) +- 风格与封面保持一致(同一色调体系) +- 不要太复杂——手机屏幕上看,简洁的图比复杂的图好 +- 提示词用中文(seedream 中文理解强) +- 每张图都提供一个**免费图库备选关键词**,以防生图效果不佳 + +--- + +## 三、辅助功能 + +### 提示词修改 + +如果用户说"封面创意 A 我喜欢方向但是想要更暖的色调",只修改对应创意的提示词,其他不变。 + +### 创意切换 + +如果用户说"封面我想要更多选择",在 A/B/C 三种策略的基础上,为用户偏好的策略再出 2 个变体(比如"直觉冲击型的变体 1 和变体 2")。 + +### 配图场景调整 + +如果用户说"第 3 张配图位置不对"或"这段不需要图",按用户要求增删调整。 + +--- + +## 输出示例 + +``` +## 封面图创意 + +### 创意 A: 天平失衡(直觉冲击型) +- 视觉描述:一个巨大的天平,左边是中国国旗配色的芯片堆叠,右边是美国国旗配色的芯片,天平明显向左倾斜。背景是深蓝色数据流。 +- 色调:深蓝 + 科技蓝 + 金色点缀 +- 构图:16:9 横版,天平居中,右侧 1/3 留白放标题 +- 文字区域:右侧留出干净空间 +- AI 绘图提示词: + "A large balance scale, left side stacked with red-themed microchips, right side with blue-themed microchips, scale tilting left, dark blue background with flowing data streams, flat design, minimalist, tech aesthetic, 16:9 aspect ratio, clean space on the right third for text overlay, no text no letters no words" +- 适配工具建议:即梦(国内场景理解好) + +## 内文配图 + +### 配图 1: 位于"数字背后是什么"段落后 +- 配图目的:信息强化 +- 画面描述:一个简洁的柱状图,展示中美大模型调用量的对比,中国柱子更高但带有问号标记 +- 尺寸:1:1 方形 +- AI 绘图提示词: + "Minimalist bar chart comparing two bars, left bar taller in red, right bar shorter in blue, question mark floating above the taller bar, clean white background, flat infographic style, 1:1 square, no text" +- 备选方案:Unsplash 搜 "data comparison chart technology" +``` diff --git a/dist/openclaw/references/wechat-constraints.md b/dist/openclaw/references/wechat-constraints.md new file mode 100644 index 0000000..5796a61 --- /dev/null +++ b/dist/openclaw/references/wechat-constraints.md @@ -0,0 +1,443 @@ +# 微信公众号平台限制说明 + +本文档详细说明微信公众号编辑器的技术限制,以及本工具如何应对这些限制。 + +--- + +## 1. 核心技术限制 + +### 1.1 不支持外部CSS文件 + +**限制说明**: +- 微信编辑器不允许使用 `` 标签引用外部CSS文件 +- 不支持 ` + + +

标题

+``` + +**实现细节**: +- 使用CSS解析器将所有CSS规则转换为内联样式 +- 自动合并多个选择器的样式 +- 处理CSS变量(`:root`)并替换为实际值 + +### 1.2 不支持JavaScript + +**限制说明**: +- 完全禁用所有JavaScript代码 +- 不支持 ` + +""" + + +def main(): + parser = argparse.ArgumentParser( + prog="wewrite", + description="Markdown to WeChat HTML converter and publisher", + ) + sub = parser.add_subparsers(dest="command", required=True) + + # preview + p_preview = sub.add_parser("preview", help="Generate HTML and open in browser") + p_preview.add_argument("input", help="Markdown file path") + p_preview.add_argument("-t", "--theme", default="professional-clean", help="Theme name") + p_preview.add_argument("-o", "--output", help="Output HTML file path") + p_preview.add_argument("--no-open", action="store_true", help="Don't open browser") + + # publish + p_publish = sub.add_parser("publish", help="Convert and publish as WeChat draft") + p_publish.add_argument("input", help="Markdown file path") + p_publish.add_argument("-t", "--theme", default=None, help="Theme name") + p_publish.add_argument("--appid", default=None, help="WeChat AppID (or set in config.yaml)") + p_publish.add_argument("--secret", default=None, help="WeChat AppSecret (or set in config.yaml)") + p_publish.add_argument("--cover", help="Cover image file path") + p_publish.add_argument("--title", help="Override article title") + p_publish.add_argument("--author", default=None, help="Article author") + + # themes + sub.add_parser("themes", help="List available themes") + + # image-post (小绿书) + p_imgpost = sub.add_parser("image-post", help="Create WeChat image post (小绿书)") + p_imgpost.add_argument("images", nargs="+", help="Image file paths (1-20, first = cover)") + p_imgpost.add_argument("-t", "--title", required=True, help="Post title (max 32 chars)") + p_imgpost.add_argument("-c", "--content", default="", help="Plain text description (max ~1000 chars)") + p_imgpost.add_argument("--appid", default=None, help="WeChat AppID") + p_imgpost.add_argument("--secret", default=None, help="WeChat AppSecret") + + # gallery + p_gallery = sub.add_parser("gallery", help="Open theme gallery in browser") + p_gallery.add_argument("input", nargs="?", default=None, help="Markdown file (optional, uses sample if omitted)") + p_gallery.add_argument("-o", "--output", help="Output HTML file path") + p_gallery.add_argument("--no-open", action="store_true", help="Don't open browser") + + args = parser.parse_args() + + try: + if args.command == "preview": + cmd_preview(args) + elif args.command == "publish": + cmd_publish(args) + elif args.command == "themes": + cmd_themes(args) + elif args.command == "image-post": + cmd_image_post(args) + elif args.command == "gallery": + cmd_gallery(args) + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/dist/openclaw/toolkit/converter.py b/dist/openclaw/toolkit/converter.py new file mode 100644 index 0000000..a3b07b2 --- /dev/null +++ b/dist/openclaw/toolkit/converter.py @@ -0,0 +1,548 @@ +""" +Markdown to WeChat-compatible HTML converter. + +Forked from wechat_article_skills/scripts/markdown_to_html.py, +adapted for YAML-driven themes and agent integration. +""" + +import re +from dataclasses import dataclass, field +from pathlib import Path +from typing import Optional + +import markdown +from bs4 import BeautifulSoup + +from theme import Theme, load_theme, get_inline_css_rules + + +@dataclass +class ConvertResult: + """Result of a Markdown → WeChat HTML conversion.""" + + html: str # WeChat-compatible inline-style HTML (body content only) + title: str # Extracted H1 title + digest: str # Auto-generated summary (first 120 chars) + images: list[str] = field(default_factory=list) # Image references found + + +class WeChatConverter: + """Convert Markdown to WeChat-compatible inline-style HTML.""" + + def __init__(self, theme: Optional[Theme] = None, theme_name: str = "professional-clean"): + if theme is not None: + self._theme = theme + else: + self._theme = load_theme(theme_name) + self._css_rules = get_inline_css_rules(self._theme) + + def convert(self, markdown_text: str) -> ConvertResult: + """ + Convert Markdown text to WeChat-compatible HTML. + + Returns ConvertResult with: + - html: inline-style HTML (body content only, no / wrapper) + - title: extracted H1 title (or empty string) + - digest: first 120 characters of plain text + - images: list of image src references + """ + title = self._extract_title(markdown_text) + markdown_text = self._strip_h1(markdown_text) + + # Pre-process container blocks (:::dialogue, :::timeline, etc.) + markdown_text = self._preprocess_containers(markdown_text) + + # CJK fix: auto-space between CJK and Latin characters + markdown_text = self._fix_cjk_spacing(markdown_text) + + # Parse Markdown → HTML + html = self._markdown_to_html(markdown_text) + + # Enhance code blocks (add data-lang attribute) + html = self._enhance_code_blocks(html) + + # Process images (ensure responsive styling) + html, images = self._process_images(html) + + # CJK fix: move punctuation outside bold tags + html = self._fix_cjk_bold_punctuation(html) + + # CJK fix: convert ul/ol to section-based lists (WeChat renders native lists unreliably) + html = self._convert_lists_to_sections(html) + + # Convert external links to footnotes (WeChat blocks external links) + html = self._convert_links_to_footnotes(html) + + # Apply inline CSS from theme + html = self._apply_inline_styles(html) + + # Apply WeChat compatibility fixes + html = self._apply_wechat_fixes(html) + + # Inject dark mode attributes + html = self._inject_darkmode(html) + + # Generate digest from plain text + digest = self._generate_digest(html) + + return ConvertResult(html=html, title=title, digest=digest, images=images) + + def convert_file(self, input_path: str) -> ConvertResult: + """Convert a Markdown file.""" + path = Path(input_path) + if not path.exists(): + raise FileNotFoundError(f"Input file not found: {input_path}") + + text = path.read_text(encoding="utf-8") + return self.convert(text) + + # -- internal methods -- + + def _extract_title(self, text: str) -> str: + """Extract the first H1 title from Markdown text.""" + for line in text.split("\n"): + stripped = line.strip() + if stripped.startswith("# ") and not stripped.startswith("## "): + return stripped[2:].strip() + return "" + + def _strip_h1(self, text: str) -> str: + """Remove H1 lines — WeChat has a separate title field.""" + lines = [] + for line in text.split("\n"): + stripped = line.strip() + if stripped.startswith("# ") and not stripped.startswith("## "): + continue + lines.append(line) + return "\n".join(lines) + + def _markdown_to_html(self, text: str) -> str: + """Parse Markdown to HTML using python-markdown with extensions.""" + extensions = [ + "markdown.extensions.fenced_code", + "markdown.extensions.tables", + "markdown.extensions.nl2br", + "markdown.extensions.sane_lists", + "markdown.extensions.codehilite", + ] + extension_configs = { + "codehilite": { + "linenums": False, + "guess_lang": True, + "noclasses": True, # Inline syntax highlight styles + } + } + md = markdown.Markdown(extensions=extensions, extension_configs=extension_configs) + return md.convert(text) + + def _enhance_code_blocks(self, html: str) -> str: + """Add data-lang attribute to
 elements for language labeling."""
+        soup = BeautifulSoup(html, "html.parser")
+        for pre in soup.find_all("pre"):
+            code = pre.find("code")
+            if code:
+                for cls in code.get("class", []):
+                    if cls.startswith("language-"):
+                        pre["data-lang"] = cls.replace("language-", "")
+                        break
+        return str(soup)
+
+    def _process_images(self, html: str) -> tuple[str, list[str]]:
+        """Extract image references and ensure responsive styling."""
+        soup = BeautifulSoup(html, "html.parser")
+        images = []
+        for img in soup.find_all("img"):
+            src = img.get("src", "")
+            if src:
+                images.append(src)
+            # Ensure responsive image styles
+            existing = img.get("style", "")
+            if "max-width" not in existing:
+                additions = "max-width: 100%; height: auto; display: block; margin: 24px auto"
+                img["style"] = f"{existing}; {additions}" if existing else additions
+        return str(soup), images
+
+    def _apply_inline_styles(self, html: str) -> str:
+        """Apply theme CSS rules as inline styles on matching elements."""
+        soup = BeautifulSoup(html, "html.parser")
+
+        for selector, styles in self._css_rules.items():
+            # Skip body — we don't wrap in body tag
+            if selector.strip() == "body":
+                continue
+
+            try:
+                elements = soup.select(selector)
+            except Exception:
+                continue
+
+            for elem in elements:
+                existing = elem.get("style", "")
+                style_dict = {}
+
+                # Parse existing inline styles
+                if existing:
+                    for item in existing.split(";"):
+                        if ":" in item:
+                            key, val = item.split(":", 1)
+                            style_dict[key.strip()] = val.strip()
+
+                # Add theme styles (existing styles take precedence)
+                for prop, val in styles.items():
+                    if prop not in style_dict:
+                        style_dict[prop] = val
+
+                elem["style"] = "; ".join(f"{k}: {v}" for k, v in style_dict.items())
+
+        return str(soup)
+
+    def _apply_wechat_fixes(self, html: str) -> str:
+        """
+        Apply WeChat-specific compatibility fixes:
+        1. Force explicit color on every 

tag + 2. Ensure code blocks preserve whitespace + """ + soup = BeautifulSoup(html, "html.parser") + text_color = self._theme.colors.get("text", "#333333") + + # Fix 1: Ensure all

tags have explicit color + for p in soup.find_all("p"): + style = p.get("style", "") + if "color" not in style: + p["style"] = f"{style}; color: {text_color}" if style else f"color: {text_color}" + + # Fix 2: Ensure

 has whitespace preservation
+        for pre in soup.find_all("pre"):
+            style = pre.get("style", "")
+            if "white-space" not in style:
+                pre["style"] = f"{style}; white-space: pre-wrap; word-wrap: break-word" if style else "white-space: pre-wrap; word-wrap: break-word"
+
+        return str(soup)
+
+    # -- CJK compatibility fixes --
+
+    def _fix_cjk_spacing(self, text: str) -> str:
+        """Auto-insert thin space between CJK and Latin/digit characters.
+
+        WeChat renders CJK-Latin without spacing, making mixed text hard to read.
+        This inserts a thin space (U+200A) at CJK↔Latin boundaries.
+        Runs on raw Markdown before parsing, skipping code blocks and links.
+        """
+        # CJK unicode ranges
+        cjk = r'[\u4e00-\u9fff\u3400-\u4dbf\u3000-\u303f\uff00-\uffef]'
+        latin = r'[A-Za-z0-9]'
+
+        lines = text.split('\n')
+        result = []
+        in_code_block = False
+
+        for line in lines:
+            if line.strip().startswith('```'):
+                in_code_block = not in_code_block
+                result.append(line)
+                continue
+            if in_code_block:
+                result.append(line)
+                continue
+
+            # CJK followed by Latin
+            line = re.sub(f'({cjk})({latin})', r'\1 \2', line)
+            # Latin followed by CJK
+            line = re.sub(f'({latin})({cjk})', r'\1 \2', line)
+            result.append(line)
+
+        return '\n'.join(result)
+
+    def _fix_cjk_bold_punctuation(self, html: str) -> str:
+        """Move Chinese punctuation outside bold/strong tags.
+
+        WeChat renders bold CJK punctuation with ugly spacing.
+        Move trailing punctuation (,。!?;:、) outside .
+        """
+        # Match: 内容+中文标点内容标点
+        pattern = r'()(.*?)([,。!?;:、]+)()'
+        return re.sub(pattern, r'\1\2\4\3', html)
+
+    def _convert_lists_to_sections(self, html: str) -> str:
+        """Convert