@clawhub-xingkongqy-9f3fcc6a3c
微信公众号发布工具 - 安全版 v2.8,支持 Knowledge-Base 主题、分步流程、一键发布,优化表格和 Markdown 渲染
---
name: wechat-mp-xk
description: 微信公众号发布工具 - 安全版 v2.8,支持 Knowledge-Base 主题、分步流程、一键发布,优化表格和 Markdown 渲染
version: 2.8.0
author: 九章快手团队 / JVS Claw 团队
license: MIT
tags:
- wechat
- mp
- publish
- markdown
- knowledge-base
- security
metadata:
openclaw:
emoji: "📱"
category: social
---
# 微信公众号发布工具 - wechat-mp-xk v2.8
**安全版 - 一键将 Markdown 文章发布到微信公众号草稿箱**
## 🎉 v2.8 更新(2026-03-31)
- ✅ **链接可点击修复** - 链接语法优先处理,添加移动端点击优化
- ✅ **代码块结构优化** - 使用 `section+div` 替代`pre+code`,微信编辑器更稳定
- ✅ **编辑后格式保持** - 每行独立 `div` 包裹,不依赖 `white-space` 属性
- ✅ **HTML 转义保护** - 自动转义 `&`、`<`、`>` 防止解析错误
- ✅ **左侧边框标识** - 代码块添加左侧红色边框,视觉更清晰
## 🎉 v2.6 更新(2026-03-31)
- ✅ **代码块处理优化** - 确保代码块优先处理,避免被表格逻辑误判
- ✅ **空单元格自动继承** - 表格空单元格自动填充上一行对应列的值
- ✅ **支持 Markdown 简写** - `| | 内容 |` 自动继承前一列的值
## 🎉 v2.5 更新(2026-03-31)
- ✅ **代码块换行修复** - 添加 `white-space: pre-wrap` 保留换行符
- ✅ **代码块自动换行** - 添加 `word-break: break-all` 防止长代码溢出
- ✅ **保留原始格式** - 代码块内容使用原始 line 而非 stripped
## 🎉 v2.3 更新(2026-03-28)
- ✅ **文字自动换行** - 添加 `word-break: break-word` 防止文字溢出
- ✅ **白色空间正常** - 添加 `white-space: normal` 确保换行生效
- ✅ **颜色保护** - 添加 `!important` 防止微信样式覆盖
- ✅ **表格优化** - 所有表格添加自动换行支持
## 🎉 v2.2 更新(2026-03-28)
- ✅ **流程图文字居中** - 单列表格/流程图文字自动居中
- ✅ **边框对齐优化** - 右侧边框线对齐,形成整体文本框
- ✅ **连接线一致** - 表格间连接线对齐统一
- ✅ **垂直居中对齐** - 多行内容垂直居中显示
## 🎉 v2.1 更新(2026-03-28)
- ✅ **移除 `---` 分隔符** - 段落不再显示水平线
- ✅ **四级标题支持** - `####` 正确渲染为 h4 标题
- ✅ **表格加粗无背景色** - 表格第一列加粗仅加粗,无黄色背景
- ✅ **表格格式对齐优化** - 表格内容垂直顶部对齐
## 🎉 v2.0 更新
- ✅ **表格 Markdown 渲染修复** - 表格内加粗、斜体、链接正常显示
- ✅ **行内格式优化** - 加粗、斜体、代码、链接全面支持
- ✅ **推广链接统一** - 使用 JVS Claw 官方推广链接
- ✅ **自我优化机制** - 持续改进,不断进步
## ⚠️ 配置提示
**重要:** 本工具使用环境变量管理敏感信息,请勿在代码中硬编码 AppID/Secret!
## ✨ 功能特点
- 🔒 **安全配置** - 环境变量管理敏感信息
- 📱 **一键发布** - Markdown → 公众号草稿箱
- 🎨 **Knowledge-Base 主题** - 简约专业排版
- 🔧 **分步流程** - 灵活控制每个环节
- 🖼️ **自动图片** - 自动上传封面图
- 📝 **Front Matter** - 支持元数据配置
- ✨ **Markdown 渲染** - 表格、加粗、斜体、链接全面支持(v2.0)
## 🚀 快速开始
### 安装
```bash
# 通过 ClawHub 安装
clawhub install wechat-mp-xk
# 或从 GitHub 克隆
git clone https://github.com/xingkongqy/wechat-mp-xk.git
cd wechat-mp-xk
```
### 配置(重要!)
**方式 1:环境变量(推荐)**
```bash
# 临时配置(当前终端有效)
export WX_APPID="your_appid"
export WX_SECRET="your_secret"
# 永久配置(添加到 ~/.bashrc)
echo 'export WX_APPID="your_appid"' >> ~/.bashrc
echo 'export WX_SECRET="your_secret"' >> ~/.bashrc
source ~/.bashrc
```
**方式 2:.env 文件**
```bash
# 复制示例文件
cp .env.example .env
# 编辑 .env 文件,填入真实值
# ⚠️ 不要将 .env 提交到 Git!
```
### 一键发布
```bash
python3 wechat_mp_xk.py article article.md \
--cover cover.jpg \
--title "文章标题" \
--author "作者名"
```
## 🔗 JVS Claw 推广
**统一推广链接**:https://www.aliyun.com/activity/ecs/clawdbot?userCode=d8ptsfvb
在文章内容中引用 JVS Claw 相关产品时,请使用以上统一推广链接。
## 📋 分步流程
### Step 1: Markdown 转 HTML
```bash
python3 wechat_mp_xk.py md2html article.md --output-dir .wxgzh
```
### Step 2: 修复 HTML
```bash
python3 wxgzh_step_by_step.py fix .wxgzh/article.html
```
### Step 3: 上传封面图
```bash
python3 wechat_mp_xk.py cover \
--cover cover.jpg \
--output .wxgzh/cover.json
```
### Step 4: 发布到草稿箱
```bash
python3 wechat_mp_xk.py publish \
--article .wxgzh/article.html \
--cover cover.jpg \
--title "文章标题"
```
## 🎨 Knowledge-Base 主题
| 元素 | 样式 |
|------|------|
| **一级标题** | 28px,底部细线分割 |
| **二级标题** | 22px,浅灰背景条 |
| **三级标题** | 18px,底部奶黄色高亮 |
| **正文** | 16px,行距 1.75 |
| **加粗** | 黄色高光笔效果 |
| **引用块** | 浅灰背景,左侧边框 |
| **表格** | 数据库风格 |
## 🔒 安全最佳实践
### ❌ 不要
- 在代码中硬编码 AppID/Secret
- 将 .env 文件提交到 Git
- 在日志中打印敏感信息
- 通过 URL 传递敏感参数
### ✅ 要
- 使用环境变量
- 使用密钥管理服务
- 定期轮换密钥(90 天)
- 限制文件权限(chmod 600)
## 📁 文件结构
```
wechat-mp-xk/
├── wechat_mp_xk.py # 主程序(分步流程)
├── publish_kb_theme.py # Knowledge-Base 主题版
├── wechat_mp.py # 核心 API 模块
├── wechat_style_template.py # 排版模板
├── README.md # 使用文档
├── SECURITY.md # 安全说明
├── .env.example # 环境变量示例
├── .gitignore # Git 忽略配置
└── tests/
└── test_publish.py # 测试用例
```
## ⚠️ 注意事项
1. **IP 白名单** - 服务器 IP 需在公众号后台配置
2. **作者名限制** - 最多 20 字节(中文约 6-7 字)
3. **标题限制** - 最多 64 字节
4. **Token 缓存** - 自动缓存到 /tmp/wechat_token.json
## 📝 使用示例
### 示例 1:一键发布
```bash
# 配置环境变量
export WX_APPID="your_appid"
export WX_SECRET="your_secret"
# 发布文章
python3 wxgzh_step_by_step.py article article.md \
--cover cover.jpg \
--title "文章标题"
```
### 示例 2:分步发布
```bash
# Step 1: 转换
python3 wxgzh_step_by_step.py md2html article.md -o .wxgzh
# Step 2: 修复
python3 wxgzh_step_by_step.py fix .wxgzh/article.html
# Step 3: 封面
python3 wxgzh_step_by_step.py cover --cover cover.jpg -o .wxgzh/cover.json
# Step 4: 发布
python3 wxgzh_step_by_step.py publish --article .wxgzh/article.html --cover cover.jpg
```
## 🧪 测试
```bash
# 运行测试
python3 -m pytest tests/
```
## 📄 License
MIT License
Copyright (c) 2026 九章快手团队
---
**版本:** v1.1.0
**创建时间:** 2026-03-20
**作者:** 九章快手团队
FILE:CHANGELOG-v2.1.md
# wechat-mp-xk v2.1 更新日志
**发布日期**: 2026 年 3 月 28 日
---
## 🎉 重大更新
### v2.1.0 - 格式优化与问题修复
#### 核心修复(根据用户反馈)
1. **移除 `---` 分隔符** ✅
- **问题**: 段落后面出现 `---` 水平线,影响阅读体验
- **修复**: 忽略 Markdown 水平线语法(`---`、`***`、`___`)
- **影响**: 所有文章不再显示分隔线
2. **四级标题支持** ✅
- **问题**: `#### 2.1.1 核心特征` 直接显示原文,未渲染
- **修复**: 添加四级标题(h4)样式支持
- **样式**: 16px 字体,600 字重,无背景色
- **代码**:
```python
if line_stripped.startswith('#### '):
title_text = process_markdown_inline(line_stripped[5:])
html_parts.append(f'<h4>...</h4>')
```
3. **表格加粗无背景色** ✅
- **问题**: 表格第一列加粗有黄色背景色,与正文风格不统一
- **修复**: 表格内加粗仅保留加粗样式,移除背景色
- **实现**: `process_markdown_inline(text, for_table=True)` 参数控制
- **对比**:
```python
# 表格内:无背景色
<strong style="color: #37352F; font-weight: 600;">实践导向</strong>
# 普通段落:有背景色
<strong style="...; background-color: #FDECC8; ...;">实践导向</strong>
```
4. **表格格式对齐优化** ✅
- **新增**: `vertical-align: top;` 单元格垂直顶部对齐
- **效果**: 多行内容表格更整齐
---
## 📊 效果对比
### 问题 1: `---` 分隔符
**修复前**(v2.0):
```
3. AI 代理赋能科研的边界在哪里?
---
二、理论基础与文献综述
```
**修复后**(v2.1):
```
3. AI 代理赋能科研的边界在哪里?
二、理论基础与文献综述
```
### 问题 2: 四级标题
**修复前**(v2.0):
```
## 2.1 行动研究方法论
### 2.1.1 核心特征 ← 三级标题
#### 2.1.1 核心特征 ← 显示原文!
```
**修复后**(v2.1):
```
## 2.1 行动研究方法论
### 2.1.1 核心特征 ← 三级标题(18px,底部黄线)
#### 2.1.1 核心特征 ← 四级标题(16px,无装饰)
```
### 问题 3: 表格加粗背景色
**修复前**(v2.0):
```
| **实践导向** | 以解决组织实际问题为目标 |
```
显示:**实践导向**(黄色背景)
**修复后**(v2.1):
```
| **实践导向** | 以解决组织实际问题为目标 |
```
显示:**实践导向**(仅加粗,无背景色)
---
## 🔧 技术实现
### 1. 水平线移除
```python
# v2.1 新增:在标题处理前检查
if line_stripped.startswith('---') or \
line_stripped.startswith('***') or \
line_stripped.startswith('___'):
# 忽略水平线
continue
```
### 2. 四级标题支持
```python
# v2.1 新增:四级标题
if line_stripped.startswith('#### '):
title_text = process_markdown_inline(line_stripped[5:])
html_parts.append(
f'<h4 style="margin-top: 24px; margin-bottom: 10px;">'
f'<span class="content" style="font-size: 16px; '
f'font-weight: 600; color: #37352F;">{title_text}'
f'</span></h4>'
)
```
### 3. 表格加粗无背景色
```python
# 新增 for_table 参数
def process_markdown_inline(text, for_table=False):
if for_table:
# 表格内:无背景色
text = re.sub(
r'\*\*(.*?)\*\*',
r'<strong style="color: #37352F; font-weight: 600;">\1</strong>',
text
)
else:
# 普通段落:有背景色
text = re.sub(
r'\*\*(.*?)\*\*',
r'<strong style="...; background-color: #FDECC8; ...">\1</strong>',
text
)
# 表格调用时传入 for_table=True
cells_html = ''.join(
f'<{row_tag}>{process_markdown_inline(c, for_table=True)}</{row_tag}>'
for c in cells
)
```
---
## 📝 使用示例
### 学术论文发布(推荐)
```bash
python3 wechat_mp_xk.py article paper.md \
--cover cover.jpg \
--title "学术论文标题" \
--author "研究团队"
```
### 技术文档发布
```bash
python3 wechat_mp_xk.py article docs.md \
--cover tech.png \
--title "技术文档" \
--author "技术团队"
```
---
## ✅ 测试清单
发布前请确认:
- [ ] 段落无 `---` 分隔线
- [ ] 四级标题正确渲染
- [ ] 表格加粗无背景色
- [ ] 表格内容顶部对齐
- [ ] 封面图上传成功
- [ ] 发布到草稿箱成功
---
## 🐛 已知问题
暂无
---
## 📚 相关资源
- **GitHub**: https://github.com/xingkongqy/wechat-mp-xk
- **JVS Claw 推广**: https://www.aliyun.com/activity/ecs/clawdbot?userCode=d8ptsfvb
- **OpenClaw 文档**: https://docs.openclaw.ai
---
## 🎯 未来规划
### v2.2(计划中)
- [ ] 五级标题支持(`#####`)
- [ ] 表格跨页优化
- [ ] 图片自动压缩
- [ ] 发布历史记录
### v3.0(规划中)
- [ ] 可视化编辑器
- [ ] 模板系统
- [ ] 定时发布
- [ ] 数据分析
---
## 📈 版本演进
| 版本 | 发布日期 | 核心更新 |
|------|----------|----------|
| v1.3 | 2026-03-25 | 基础功能、Knowledge-Base 主题 |
| v2.0 | 2026-03-28 | Markdown 渲染全面优化 |
| v2.1 | 2026-03-28 | 格式问题修复(用户反馈驱动) |
---
**持续改进,不断进步!** 🚀
wechat-mp-xk 团队
2026 年 3 月 28 日
FILE:CHANGELOG-v2.2.md
# wechat-mp-xk v2.2 更新日志
**发布日期**: 2026 年 3 月 28 日
---
## 🎉 重大更新
### v2.2.0 - 流程图样式全面优化
#### 核心修复(根据用户反馈)
1. **流程图文字居中** ✅
- **问题**: 流程框中的文字描述左对齐,不美观
- **修复**: 单列表格/流程图表格文字自动居中
- **样式**: `text-align: center;`
- **检测**: 自动识别包含"阶段"、"步骤"、"流程"、"Diagnosing"等关键词的表格
2. **边框对齐优化** ✅
- **问题**: 框的右侧框竖线不对齐,无法形成整体文本框
- **修复**: 统一使用 `border: 2px solid #E3E2E0;`
- **效果**: 所有流程框边框宽度一致,右侧对齐
3. **连接线一致** ✅
- **问题**: 框之间的连接线不对齐
- **修复**: 统一单元格高度和内边距 `padding: 16px 12px;`
- **效果**: 表格行高一致,连接线自然对齐
4. **垂直居中对齐** ✅
- **问题**: 多行内容在框内顶部对齐,不美观
- **修复**: 垂直居中 `vertical-align: middle;`
- **效果**: 内容在框内垂直居中显示
---
## 📊 效果对比
### 问题 1: 文字居中
**修复前**(v2.1):
```
┌─────────────────────────────┐
│ 诊断(Diagnosing): │
│ 协作识别核心问题 │
│ 并提出理论假设 │
└─────────────────────────────┘
```
文字左对齐,参差不齐
**修复后**(v2.2):
```
┌─────────────────────────────┐
│ 诊断(Diagnosing): │
│ 协作识别核心问题 │
│ 并提出理论假设 │
└─────────────────────────────┘
```
文字居中,整齐美观
### 问题 2: 边框对齐
**修复前**(v2.1):
```
┌──────────────────────────┐
│ 内容 1 │ ← 右边框位置不一致
┌─────────────────────────────┐
│ 内容 2 │
└─────────────────────────────┘
```
**修复后**(v2.2):
```
┌─────────────────────────────┐
│ 内容 1 │ ← 右边框对齐
├─────────────────────────────┤
│ 内容 2 │
└─────────────────────────────┘
```
### 问题 3: 垂直对齐
**修复前**(v2.1):
```
┌─────────────────────────────┐
│ 内容 1 │ ← 顶部对齐
│ │
└─────────────────────────────┘
┌─────────────────────────────┐
│ 内容 2 │ ← 高度不一致
└─────────────────────────────┘
```
**修复后**(v2.2):
```
┌─────────────────────────────┐
│ │
│ 内容 1 │ ← 垂直居中
│ │
├─────────────────────────────┤
│ │
│ 内容 2 │ ← 高度一致
│ │
└─────────────────────────────┘
```
---
## 🔧 技术实现
### 1. 流程图表格自动识别
```python
def is_flowchart_table(cells):
"""判断是否为流程图表格"""
# 单列表格通常是流程图
if len(cells) == 1:
return True
# 检查流程关键词
flowchart_keywords = [
'阶段', '步骤', '流程', '循环', '模型',
'Diagnosing', 'Action', 'Planning', 'Taking', 'Evaluating',
'诊断', '行动', '规划', '执行', '评估'
]
return any(keyword.lower() in cells[0].lower()
for keyword in flowchart_keywords)
```
### 2. 流程图样式
```python
if current_table_is_flowchart:
# 流程图表格样式
html_parts.append(
'<table style="width: 100%; border-collapse: collapse; '
'margin: 30px 0; font-size: 15px; '
'border: 2px solid #E3E2E0; border-radius: 4px;">'
)
# 单元格样式:居中 + 垂直居中 + 统一边框
cell_style = (
'color: #37352F; background: #fff; '
'text-align: center; vertical-align: middle; '
'border: 2px solid #E3E2E0; padding: 16px 12px;'
)
else:
# 普通表格样式
cell_style = (
'border: 1px solid #E3E2E0; '
'padding: 10px 12px; '
'text-align: left; vertical-align: top;'
)
```
### 3. 样式对比
| 属性 | 流程图表格 | 普通表格 |
|------|-----------|----------|
| **边框宽度** | 2px | 1px |
| **文字对齐** | center(居中) | left(左对齐) |
| **垂直对齐** | middle(居中) | top(顶部) |
| **内边距** | 16px 12px | 10px 12px |
| **圆角** | 4px | 0 |
---
## 📝 使用示例
### 学术论文发布(含流程图)
```bash
python3 wechat_mp_xk.py article paper.md \
--cover cover.jpg \
--title "学术论文标题" \
--author "研究团队"
```
### 技术文档发布(含步骤说明)
```bash
python3 wechat_mp_xk.py article docs.md \
--cover tech.png \
--title "技术文档" \
--author "技术团队"
```
---
## ✅ 测试清单
发布前请确认:
- [ ] 流程图文字居中
- [ ] 边框右侧对齐
- [ ] 连接线一致
- [ ] 多行内容垂直居中
- [ ] 段落无 `---` 分隔线
- [ ] 四级标题正确渲染
- [ ] 表格加粗无背景色
- [ ] 封面图上传成功
- [ ] 发布到草稿箱成功
---
## 🐛 已知问题
暂无
---
## 📚 相关资源
- **GitHub**: https://github.com/xingkongqy/wechat-mp-xk
- **JVS Claw 推广**: https://www.aliyun.com/activity/ecs/clawdbot?userCode=d8ptsfvb
- **OpenClaw 文档**: https://docs.openclaw.ai
---
## 🎯 未来规划
### v2.3(计划中)
- [ ] 流程图箭头连接线
- [ ] 自定义流程图样式
- [ ] 多列流程图支持
- [ ] 图片自动压缩
### v3.0(规划中)
- [ ] 可视化编辑器
- [ ] 模板系统
- [ ] 定时发布
- [ ] 数据分析
---
## 📈 版本演进
| 版本 | 发布日期 | 核心更新 |
|------|----------|----------|
| v1.3 | 2026-03-25 | 基础功能、Knowledge-Base 主题 |
| v2.0 | 2026-03-28 | Markdown 渲染全面优化 |
| v2.1 | 2026-03-28 | 格式问题修复(用户反馈驱动) |
| v2.2 | 2026-03-28 | 流程图样式优化(边框/对齐/居中) |
---
**持续改进,不断进步!** 🚀
wechat-mp-xk 团队
2026 年 3 月 28 日
FILE:CHANGELOG-v2.3.md
# wechat-mp-xk v2.3 更新日志
**发布日期**: 2026 年 3 月 28 日
---
## 🎉 重大更新
### v2.3.0 - 微信预览问题全面修复
#### 问题背景
用户反馈最新发布的文章在微信预览中出现以下问题:
1. 表格文字显示不全,被截断
2. 表格边框线显示为红色(异常)
3. 文字没有自动换行,溢出单元格
#### 根本原因分析
**可能的原因:**
1. **微信样式覆盖**
- 微信公众号编辑器会应用自己的默认样式
- 某些 CSS 属性(如 `color`)可能被微信样式覆盖
- **解决**: 使用 `!important` 强制应用样式
2. **文字不换行**
- 长文本(如"在本研究中的体现"列)超出单元格宽度
- 默认 `white-space: normal` 可能不够
- **解决**: 显式添加 `word-break: break-word` 和 `white-space: normal`
3. **表格宽度问题**
- 表格宽度 100%,但微信预览区域有限
- 列宽没有合理分配
- **解决**: 添加 `word-break` 确保文字自动换行
4. **颜色显示异常**
- 截图显示边框线为红色(应该是灰色#E3E2E0)
- 可能是微信编辑器的临时预览 bug
- **解决**: 使用更明确的颜色值和 `!important`
#### 核心修复
1. **文字自动换行** ✅
```css
word-break: break-word;
white-space: normal;
```
2. **颜色保护** ✅
```css
color: #37352F !important;
```
3. **表格样式优化** ✅
```css
table {
word-break: break-word;
}
td, th {
word-break: break-word;
white-space: normal;
}
```
---
## 📊 效果对比
### 问题 1: 文字溢出
**修复前**(v2.2):
```
┌─────────────────────────────────────┐
│ 特征 │ 内涵 │ 在本研究中的体现 │
│------│------│----------------------│
│ 实践导向 │ 以解决组织实际问题为目...│ ← 文字被截断
```
**修复后**(v2.3):
```
┌─────────────────────────────────────┐
│ 特征 │ 内涵 │ 在本研究中的体现 │
│------│------│----------------------│
│ 实践导向 │ 以解决组织实际问题为目 │
│ │ 标,同时在过程中生成科学 │ ← 自动换行
│ │ 知识 │
```
### 问题 2: 颜色异常
**修复前**(v2.2):
```css
color: #37352F; /* 可能被微信覆盖 */
```
**修复后**(v2.3):
```css
color: #37352F !important; /* 强制应用 */
```
---
## 🔧 技术实现
### 1. 表格样式优化
```python
# v2.3 关键修复:添加 word-break 和 white-space
cell_style = (
row_style +
' border: 1px solid #E3E2E0; '
' padding: 10px 12px; '
' text-align: left; '
' vertical-align: top; '
' word-break: break-word; ' # 新增
' white-space: normal;' # 新增
)
```
### 2. 颜色保护
```python
# v2.3 新增:使用 !important 防止覆盖
row_style = 'color: #37352F !important; background: #fff;'
```
### 3. 表格容器优化
```python
# v2.3 新增:表格容器也添加 word-break
html_parts.append(
'<table style="width: 100%; border-collapse: collapse; '
'margin: 30px 0; font-size: 14px; '
'border: 1px solid #E3E2E0; border-radius: 0; '
'word-break: break-word;">' # 新增
)
```
---
## ✅ 测试清单
发布前请确认:
- [ ] 表格文字自动换行
- [ ] 长文本不溢出单元格
- [ ] 边框颜色正常(灰色#E3E2E0)
- [ ] 文字颜色正常(深灰#37352F)
- [ ] 流程图文字居中
- [ ] 边框右侧对齐
- [ ] 连接线一致
- [ ] 多行内容垂直居中
- [ ] 段落无 `---` 分隔线
- [ ] 四级标题正确渲染
- [ ] 表格加粗无背景色
- [ ] 封面图上传成功
- [ ] 发布到草稿箱成功
---
## 🐛 已知问题
暂无
---
## 📚 相关资源
- **GitHub**: https://github.com/xingkongqy/wechat-mp-xk
- **JVS Claw 推广**: https://www.aliyun.com/activity/ecs/clawdbot?userCode=d8ptsfvb
- **OpenClaw 文档**: https://docs.openclaw.ai
---
## 🎯 未来规划
### v2.4(计划中)
- [ ] 表格列宽自适应
- [ ] 流程图箭头连接线
- [ ] 自定义表格样式
- [ ] 图片自动压缩
### v3.0(规划中)
- [ ] 可视化编辑器
- [ ] 模板系统
- [ ] 定时发布
- [ ] 数据分析
---
## 📈 版本演进
| 版本 | 发布日期 | 核心更新 |
|------|----------|----------|
| v1.3 | 2026-03-25 | 基础功能、Knowledge-Base 主题 |
| v2.0 | 2026-03-28 | Markdown 渲染全面优化 |
| v2.1 | 2026-03-28 | 格式问题修复(用户反馈驱动) |
| v2.2 | 2026-03-28 | 流程图样式优化(边框/对齐/居中) |
| v2.3 | 2026-03-28 | 微信预览问题修复(换行/颜色保护) |
---
## 🔄 自我提升机制
wechat-mp-xk skill 持续改进机制:
```
用户反馈 → 问题分析 → 快速修复 → 测试验证 → 文档更新 → 版本迭代
↓
不断进步
```
**v2.3 特点**:
- 快速响应用户反馈
- 深入分析根本原因
- 全面修复所有相关问题
- 文档同步更新
---
**持续改进,不断进步!** 🚀
wechat-mp-xk 团队
2026 年 3 月 28 日
FILE:CHANGELOG-v2.4.md
# wechat-mp-xk v2.4 更新日志
**发布日期**: 2026 年 3 月 28 日
---
## 🎉 重大更新
### v2.4.0 - Markdown 表格对齐语法支持
#### 功能来源
学习自 IMA 知识库 AI 专题库中的《笔记|Markdown 进阶语法》
#### 核心新增
1. **Markdown 对齐语法解析** ✅
- `|:---|` - 左对齐(默认)
- `|:---:|` - 居中对齐
- `|---:|` - 右对齐
2. **表头居中优化** ✅
- 表头内容上下左右居中
- `text-align: center; vertical-align: middle;`
3. **列对齐继承** ✅
- 解析分隔线语法
- 自动应用到每列单元格
---
## 📊 效果对比
### Markdown 语法示例
**输入**:
```markdown
| 语法 | 描述 | 呈现 |
|:---|:---:|---:|
| 页眉 | 标题 | 句子 |
| 段 | 主旨 | 单词 |
```
**修复前**(v2.3):
```
所有列都左对齐,表头也左对齐
```
**修复后**(v2.4):
```
| 语法(左) | 描述(中) | 呈现(右) |
|------|------|------|
| 页眉 | 标题 | 句子 |
| 段 | 主旨 | 单词 |
```
### 表头样式对比
**修复前**(v2.3):
```
特征(左对齐,顶部对齐)
```
**修复后**(v2.4):
```
特征
(居中对齐,垂直居中)
```
---
## 🔧 技术实现
### 1. 对齐语法解析函数
```python
def parse_table_alignment(separator_line):
"""
解析 Markdown 表格分隔线的对齐语法
|:-| 左对齐,|-:| 右对齐,|:-:| 居中对齐
"""
alignments = []
cells = [c.strip() for c in separator_line.split('|') if c.strip()]
for cell in cells:
if cell.startswith(':') and cell.endswith(':'):
alignments.append('center') # :---: 居中
elif cell.endswith(':'):
alignments.append('right') # ---: 右对齐
else:
alignments.append('left') # --- 或 :--- 左对齐
return alignments
```
### 2. 表格处理逻辑优化
```python
# 检查分隔线行
if line_stripped.startswith('|---') or \
line_stripped.startswith('|:--') or \
line_stripped.startswith('|--:'):
# 解析对齐语法
table_column_alignments = parse_table_alignment(line_stripped)
continue
# 应用对齐样式
if table_column_alignments:
align = table_column_alignments[col_index]
align_style = f'text-align: {align}; vertical-align: middle;'
else:
align_style = 'text-align: left; vertical-align: top;'
```
### 3. 表头样式优化
```python
# v2.4 新增:表头内容上下左右居中
if is_header:
row_style = 'background: #F7F6F3; color: #37352F; font-weight: 600; ' + \
'text-align: center; vertical-align: middle;'
```
---
## 📝 使用示例
### 左对齐表格(默认)
```markdown
| 左对齐 | 内容 |
|:---|:---|
| 项目 1 | 描述 1 |
| 项目 2 | 描述 2 |
```
### 居中对齐表格
```markdown
| 居中 | 内容 |
|:---:|:---:|
| 项目 1 | 描述 1 |
| 项目 2 | 描述 2 |
```
### 右对齐表格
```markdown
| 右对齐 | 数值 |
|---:|---:|
| 项目 1 | 100 |
| 项目 2 | 200 |
```
### 混合对齐表格
```markdown
| 左对齐 | 居中 | 右对齐 |
|:---|:---:|---:|
| 项目 1 | 描述 | 100 |
| 项目 2 | 说明 | 200 |
```
---
## ✅ 测试清单
发布前请确认:
- [ ] Markdown 对齐语法正确解析
- [ ] 表头内容上下左右居中
- [ ] 列对齐方式正确应用
- [ ] 文字自动换行
- [ ] 边框颜色正常
- [ ] 流程图文字居中
- [ ] 边框右侧对齐
- [ ] 连接线一致
- [ ] 多行内容垂直居中
- [ ] 段落无 `---` 分隔线
- [ ] 四级标题正确渲染
- [ ] 表格加粗无背景色
- [ ] 封面图上传成功
- [ ] 发布到草稿箱成功
---
## 🐛 已知问题
暂无
---
## 📚 相关资源
- **GitHub**: https://github.com/xingkongqy/wechat-mp-xk
- **JVS Claw 推广**: https://www.aliyun.com/activity/ecs/clawdbot?userCode=d8ptsfvb
- **OpenClaw 文档**: https://docs.openclaw.ai
- **IMA 知识库**: https://ima.qq.com
---
## 🎯 未来规划
### v2.5(计划中)
- [ ] 脚注支持(`[^1]`)
- [ ] 高亮支持(`==text==`)
- [ ] 下划线支持(`~text~`)
- [ ] 删除线支持(`~~text~~`)
### v3.0(规划中)
- [ ] 可视化编辑器
- [ ] 模板系统
- [ ] 定时发布
- [ ] 数据分析
---
## 📈 版本演进
| 版本 | 发布日期 | 核心更新 |
|------|----------|----------|
| v1.3 | 2026-03-25 | 基础功能、Knowledge-Base 主题 |
| v2.0 | 2026-03-28 | Markdown 渲染全面优化 |
| v2.1 | 2026-03-28 | 格式问题修复(3 项) |
| v2.2 | 2026-03-28 | 流程图样式优化(4 项) |
| v2.3 | 2026-03-28 | 微信预览问题修复(3 项) |
| v2.4 | 2026-03-28 | Markdown 对齐语法支持 |
---
## 🔄 自我提升机制
wechat-mp-xk skill 持续改进机制:
```
IMA 知识库学习 → 技术分析 → 功能实现 → 测试验证 → 文档更新
↓
不断进步
```
**v2.4 特点**:
- 学习 IMA 知识库 Markdown 进阶语法
- 支持标准 Markdown 对齐语法
- 表头样式优化(居中显示)
- 持续改进表格排版能力
---
**持续改进,不断进步!** 🚀
wechat-mp-xk 团队
2026 年 3 月 28 日
FILE:CHANGELOG-v2.5.md
# wechat-mp-xk v2.5 更新日志
**发布日期**: 2026 年 3 月 31 日
---
## 🎉 重大更新
### v2.5.0 - 代码块换行修复
#### 问题背景
用户反馈在微信预览中,代码块内容显示为单行,没有正确换行:
**问题截图**:
- 代码块中的多行命令显示为单行
- 长代码溢出容器边界
- 无法区分不同的命令
#### 根因分析
1. **`white-space` 属性缺失**
- `<pre>` 标签默认 `white-space: pre`
- 但微信可能覆盖此样式
- 导致换行符被忽略
2. **长代码无换行**
- 长代码行超出容器宽度
- 没有 `word-break` 或 `overflow` 处理
- 导致横向滚动或溢出
3. **使用 `line_stripped`**
- 代码块内容使用了 `line_stripped`
- 移除了前导和尾随空格
- 可能破坏代码格式
---
## 🔧 技术修复
### 1. 添加 `white-space: pre-wrap`
```python
# v2.5 修复前
<pre style="background: #F7F6F3; padding: 20px; border-radius: 4px; overflow-x: auto;">
# v2.5 修复后
<pre style="background: #F7F6F3; padding: 20px; border-radius: 4px; overflow-x: auto; white-space: pre-wrap; word-break: break-all; word-wrap: break-word;">
```
**样式说明**:
- `white-space: pre-wrap` - 保留换行符和空格,同时允许自动换行
- `word-break: break-all` - 在任意字符间断行(防止长代码溢出)
- `word-wrap: break-word` - 长单词自动换行
### 2. 保留原始行
```python
# v2.5 修复前
if in_code_block:
html_parts.append(line_stripped) # 移除了空格和换行
continue
# v2.5 修复后
if in_code_block:
html_parts.append(line) # 保留原始换行和空格
continue
```
---
## 📊 效果对比
### 问题 1: 代码块不换行
**修复前**(v2.4):
```
cd ~/.openclaw/workspace/skills/minimax-docx bash scripts/setup.sh
```
(单行显示,无法区分命令)
**修复后**(v2.5):
```bash
cd ~/.openclaw/workspace/skills/minimax-docx
bash scripts/setup.sh
```
(正确换行,清晰可读)
### 问题 2: 长代码溢出
**修复前**(v2.4):
```
bash scripts/create-report.sh --title "项目报告" --sections background,objectives,plan,risks,outcomes --output report.docx
```
(超出容器边界)
**修复后**(v2.5):
```bash
bash scripts/create-report.sh \
--title "项目报告" \
--sections background,objectives,plan,risks,outcomes \
--output report.docx
```
(自动换行,完整显示)
---
## ✅ 测试清单
发布前请确认:
- [ ] 代码块正确换行
- [ ] 多行命令清晰可读
- [ ] 长代码自动换行
- [ ] 代码缩进保留
- [ ] Markdown 对齐语法正确
- [ ] 表头内容居中
- [ ] 文字自动换行
- [ ] 边框颜色正常
- [ ] 流程图文字居中
- [ ] 边框右侧对齐
- [ ] 连接线一致
- [ ] 封面图上传成功
- [ ] 发布到草稿箱成功
---
## 🐛 已知问题
暂无
---
## 📚 相关资源
- **GitHub**: https://github.com/xingkongqy/wechat-mp-xk
- **JVS Claw 推广**: https://www.aliyun.com/activity/ecs/clawdbot?userCode=d8ptsfvb
- **OpenClaw 文档**: https://docs.openclaw.ai
- **IMA 知识库**: https://ima.qq.com
---
## 🎯 未来规划
### v2.6(计划中)
- [ ] 代码高亮优化
- [ ] 行号显示
- [ ] 复制按钮
### v3.0(规划中)
- [ ] 可视化编辑器
- [ ] 模板系统
- [ ] 定时发布
- [ ] 数据分析
---
## 📈 版本演进
| 版本 | 发布日期 | 核心更新 |
|------|----------|----------|
| v1.3 | 2026-03-25 | 基础功能、Knowledge-Base 主题 |
| v2.0 | 2026-03-28 | Markdown 渲染全面优化 |
| v2.1 | 2026-03-28 | 格式问题修复(3 项) |
| v2.2 | 2026-03-28 | 流程图样式优化(4 项) |
| v2.3 | 2026-03-28 | 微信预览问题修复(3 项) |
| v2.4 | 2026-03-28 | Markdown 对齐语法支持 |
| v2.5 | 2026-03-31 | 代码块换行修复 |
---
## 🔄 自我提升机制
wechat-mp-xk skill 持续改进机制:
```
用户反馈 → 问题分析 → 技术修复 → 测试验证 → 文档更新
↓
不断进步
```
**v2.5 特点**:
- 快速响应用户反馈
- 修复代码块换行问题
- 保留代码原始格式
- 持续改进排版能力
---
**持续改进,不断进步!** 🚀
wechat-mp-xk 团队
2026 年 3 月 31 日
FILE:CHANGELOG-v2.6-fixed.md
# wechat-mp-xk v2.6 综合修复日志
**发布日期**: 2026 年 3 月 31 日
---
## 🎉 重大更新
### v2.6.0 - 代码块与表格综合修复
#### 问题背景
**v2.6 第一次发布后用户反馈**:
- 代码块内容显示为单行,没有正确换行(v2.5 问题复发)
- 表格列错位,空单元格未正确对齐
#### 根因分析
**问题 1:代码块不换行(v2.5 问题复发)**
**原因**:代码块处理逻辑顺序问题
- `in_code_block` 检查在表格处理之后
- 代码块内的 `|` 字符被表格逻辑误判
- 导致代码块内容被错误处理
**问题 2:表格列错位**
**原因**:Markdown 空列语法不支持
- 用户使用 `| | 内容 |` 简写
- 标准 Markdown 不支持空列
- 导致列数不匹配
---
## 🔧 技术修复
### 修复 1:代码块处理优先级
**v2.6 修复前**:
```python
for line in lines:
line_stripped = line.strip()
if line_stripped.startswith('```'):
# 处理代码块开始/结束
...
if in_code_block:
html_parts.append(line)
continue
# 表格处理
if line_stripped.startswith('|'):
# 可能被误判
```
**v2.6 修复后**:
```python
for line in lines:
line_stripped = line.strip()
# v2.6 修复:代码块优先处理
if in_code_block:
if line_stripped.startswith('```'):
html_parts.append('</code></pre>')
in_code_block = False
else:
html_parts.append(line) # 保留原始换行
continue # 立即跳出,避免误判
if line_stripped.startswith('```'):
html_parts.append('<pre ...>')
in_code_block = True
continue
# 表格处理(不会误判代码块内容)
if line_stripped.startswith('|'):
...
```
**关键点**:
1. `in_code_block` 检查放在最前面
2. 代码块内使用 `continue` 立即跳出
3. 避免表格逻辑误判代码块内容
---
### 修复 2:空单元格自动继承
**v2.6 新增逻辑**:
```python
# 解析原始单元格(保留空单元格)
raw_cells = line_stripped.split('|')
raw_cells = [c.strip() for c in raw_cells]
# 移除首尾空元素
if raw_cells and raw_cells[0] == '':
raw_cells = raw_cells[1:]
if raw_cells and raw_cells[-1] == '':
raw_cells = raw_cells[:-1]
# 自动填充空单元格(继承上一行的值)
if 'table_prev_row_cells' not in locals():
table_prev_row_cells = []
cells = []
for i, cell in enumerate(raw_cells):
if cell == '' and i < len(table_prev_row_cells):
cells.append(table_prev_row_cells[i]) # 继承
else:
cells.append(cell)
# 更新记录
if cells:
table_prev_row_cells = cells
```
---
## 📊 效果对比
### 问题 1:代码块不换行
**修复前**(v2.6 初版):
```
# Word 文档处理 cd ~/.openclaw/workspace/skills/minimax-docx bash scripts/setup.sh # Excel 表格处理...
```
(单行显示,无法阅读)
**修复后**(v2.6 终版):
```bash
# Word 文档处理
cd ~/.openclaw/workspace/skills/minimax-docx
bash scripts/setup.sh
# Excel 表格处理
cd ~/.openclaw/workspace/skills/minimax-xlsx
bash scripts/setup.sh
```
(正确换行,清晰可读)
### 问题 2:表格列错位
**修复前**(v2.5):
```
技能 测试项目 通过率
minimax-docx 创建文档 100%
编辑文档 100% ← 列错位
格式调整 100% ← 列错位
```
**修复后**(v2.6):
```
技能 测试项目 通过率
minimax-docx 创建文档 100%
minimax-docx 编辑文档 100% ← 自动继承
minimax-docx 格式调整 100% ← 自动继承
```
(列正确对齐)
---
## ✅ 综合测试清单
发布前必须确认所有项目:
### 代码块测试
- [ ] 代码块正确换行
- [ ] 多行命令清晰可读
- [ ] 长代码自动换行
- [ ] 代码缩进保留
- [ ] 代码块内 `|` 字符不误判
### 表格测试
- [ ] 空单元格自动继承
- [ ] 表格列正确对齐
- [ ] Markdown 对齐语法正确
- [ ] 表头内容居中
- [ ] 文字自动换行
### 其他测试
- [ ] 边框颜色正常
- [ ] 流程图文字居中
- [ ] 边框右侧对齐
- [ ] 连接线一致
- [ ] 封面图上传成功
- [ ] 发布到草稿箱成功
---
## 🐛 已知问题
暂无
---
## 📚 相关资源
- **GitHub**: https://github.com/xingkongqy/wechat-mp-xk
- **JVS Claw 推广**: https://www.aliyun.com/activity/ecs/clawdbot?userCode=d8ptsfvb
- **OpenClaw 文档**: https://docs.openclaw.ai
- **IMA 知识库**: https://ima.qq.com
---
## 🎯 未来规划
### v2.7(计划中)
- [ ] 代码高亮优化
- [ ] 行号显示
- [ ] 复制按钮
### v3.0(规划中)
- [ ] 可视化编辑器
- [ ] 模板系统
- [ ] 定时发布
- [ ] 数据分析
---
## 📈 版本演进
| 版本 | 发布日期 | 核心更新 |
|------|----------|----------|
| v1.3 | 2026-03-25 | 基础功能、Knowledge-Base 主题 |
| v2.0 | 2026-03-28 | Markdown 渲染全面优化 |
| v2.1 | 2026-03-28 | 格式问题修复(3 项) |
| v2.2 | 2026-03-28 | 流程图样式优化(4 项) |
| v2.3 | 2026-03-28 | 微信预览问题修复(3 项) |
| v2.4 | 2026-03-28 | Markdown 对齐语法支持 |
| v2.5 | 2026-03-31 | 代码块换行修复 |
| v2.6 | 2026-03-31 | 代码块优先级 + 空单元格继承 |
---
## 🔄 自我提升机制
wechat-mp-xk skill 持续改进机制:
```
用户反馈 → 截图分析 → 根因分析 → 综合修复 → 测试验证 → 文档更新
↓
双问题并行修复(代码块 + 表格)
↓
不断进步
```
**v2.6 特点**:
- 快速响应用户反馈
- 综合修复多个问题
- 代码块优先级优化
- 空单元格自动继承
- 持续改进排版能力
---
## 💡 经验教训
### 教训 1:处理顺序很重要
**问题**:代码块处理在表格之后,导致误判
**解决**:
- 将 `in_code_block` 检查放在最前面
- 使用 `continue` 立即跳出循环
- 避免后续逻辑误判
### 教训 2:回归测试很重要
**问题**:v2.6 引入了 v2.5 的问题
**解决**:
- 建立回归测试清单
- 每次更新前测试所有历史问题
- 确保不引入旧 bug
### 教训 3:双重修复策略
**问题**:单一修复可能不够
**解决**:
- 方案 1:修复源文件(立即可用)
- 方案 2:优化 skill(长期解决)
- 双管齐下,确保效果
---
**持续改进,不断进步!** 🚀
wechat-mp-xk 团队
2026 年 3 月 31 日
FILE:CHANGELOG-v2.6.md
# wechat-mp-xk v2.6 更新日志
**发布日期**: 2026 年 3 月 31 日
---
## 🎉 重大更新
### v2.6.0 - 表格空单元格自动继承
#### 问题背景
用户反馈表格内容未对齐,从截图可以看到:
- 第一列的内容(如"编辑文档"、"格式调整")没有正确显示在单元格内
- 表格的列没有正确对齐
- 看起来像是跨行了
#### 根因分析
**Markdown 表格格式问题**:
用户使用了空列语法(`| |`)来表示"同上":
```markdown
| 技能 | 测试项目 | 通过率 | 备注 |
|------|----------|--------|------|
| **minimax-docx** | 创建文档 | 100% | ✅ |
| | 编辑文档 | 100% | ✅ | ← 第一列为空
| | 格式调整 | 100% | ✅ | ← 第一列为空
```
**问题**:
- 标准 Markdown 不支持空列语法
- 空列导致列数不匹配
- 表格渲染错位
---
## 🔧 技术修复
### 方案 1:修复源文件(已完成)
将空列填充为实际内容:
```markdown
| 技能 | 测试项目 | 通过率 | 备注 |
|------|----------|--------|------|
| **minimax-docx** | 创建文档 | 100% | ✅ |
| **minimax-docx** | 编辑文档 | 100% | ✅ | ← 填充内容
| **minimax-docx** | 格式调整 | 100% | ✅ | ← 填充内容
```
### 方案 2:优化 skill 自动处理(v2.6 新增)
**核心逻辑**:
```python
# v2.6 新增:解析原始单元格(保留空单元格)
raw_cells = line_stripped.split('|')
raw_cells = [c.strip() for c in raw_cells] # 保留空字符串
# v2.6 关键修复:自动填充空单元格(继承上一行对应列的值)
if 'table_prev_row_cells' not in locals():
table_prev_row_cells = []
cells = []
for i, cell in enumerate(raw_cells):
if cell == '' and i < len(table_prev_row_cells):
# 空单元格,继承上一行的值
cells.append(table_prev_row_cells[i])
else:
cells.append(cell)
# 更新上一行单元格记录
if cells:
table_prev_row_cells = cells
```
**效果**:
- 用户可以使用简写格式 `| | 内容 |`
- skill 自动填充为空单元格为上一行对应列的值
- 表格正确对齐
---
## 📊 效果对比
### 修复前(v2.5)
**输入**:
```markdown
| 技能 | 测试项目 | 通过率 | 备注 |
|------|----------|--------|------|
| **minimax-docx** | 创建文档 | 100% | ✅ |
| | 编辑文档 | 100% | ✅ |
| | 格式调整 | 100% | ✅ |
```
**输出**:
```
技能 测试项目 通过率 备注
minimax-docx 创建文档 100% ✅
编辑文档 100% ✅
格式调整 100% ✅
```
(列错位,第一列内容跑到第二列)
### 修复后(v2.6)
**输入**(相同):
```markdown
| 技能 | 测试项目 | 通过率 | 备注 |
|------|----------|--------|------|
| **minimax-docx** | 创建文档 | 100% | ✅ |
| | 编辑文档 | 100% | ✅ |
| | 格式调整 | 100% | ✅ |
```
**输出**:
```
技能 测试项目 通过率 备注
minimax-docx 创建文档 100% ✅
minimax-docx 编辑文档 100% ✅
minimax-docx 格式调整 100% ✅
```
(列正确对齐,空单元格自动继承)
---
## ✅ 测试清单
发布前请确认:
- [ ] 空单元格自动继承功能正常
- [ ] 表格列正确对齐
- [ ] 代码块正确换行
- [ ] 多行命令清晰可读
- [ ] 长代码自动换行
- [ ] 代码缩进保留
- [ ] Markdown 对齐语法正确
- [ ] 表头内容居中
- [ ] 文字自动换行
- [ ] 边框颜色正常
- [ ] 流程图文字居中
- [ ] 边框右侧对齐
- [ ] 连接线一致
- [ ] 封面图上传成功
- [ ] 发布到草稿箱成功
---
## 🐛 已知问题
暂无
---
## 📚 相关资源
- **GitHub**: https://github.com/xingkongqy/wechat-mp-xk
- **JVS Claw 推广**: https://www.aliyun.com/activity/ecs/clawdbot?userCode=d8ptsfvb
- **OpenClaw 文档**: https://docs.openclaw.ai
- **IMA 知识库**: https://ima.qq.com
---
## 🎯 未来规划
### v2.7(计划中)
- [ ] 代码高亮优化
- [ ] 行号显示
- [ ] 复制按钮
### v3.0(规划中)
- [ ] 可视化编辑器
- [ ] 模板系统
- [ ] 定时发布
- [ ] 数据分析
---
## 📈 版本演进
| 版本 | 发布日期 | 核心更新 |
|------|----------|----------|
| v1.3 | 2026-03-25 | 基础功能、Knowledge-Base 主题 |
| v2.0 | 2026-03-28 | Markdown 渲染全面优化 |
| v2.1 | 2026-03-28 | 格式问题修复(3 项) |
| v2.2 | 2026-03-28 | 流程图样式优化(4 项) |
| v2.3 | 2026-03-28 | 微信预览问题修复(3 项) |
| v2.4 | 2026-03-28 | Markdown 对齐语法支持 |
| v2.5 | 2026-03-31 | 代码块换行修复 |
| v2.6 | 2026-03-31 | 空单元格自动继承 |
---
## 🔄 自我提升机制
wechat-mp-xk skill 持续改进机制:
```
用户反馈 → 截图分析 → 根因分析 → 技术修复 → 测试验证 → 文档更新
↓
双方案并行(源文件修复 + skill 优化)
↓
不断进步
```
**v2.6 特点**:
- 快速响应用户反馈
- 双方案解决问题(源文件 + skill)
- 自动继承空单元格值
- 支持 Markdown 简写格式
---
**持续改进,不断进步!** 🚀
wechat-mp-xk 团队
2026 年 3 月 31 日
FILE:CHANGELOG-v2.7.md
# wechat-mp-xk v2.7 代码块稳定性优化
**发布日期**: 2026 年 3 月 31 日
---
## 🎉 重大更新
### v2.7.0 - 微信编辑器兼容性优化
#### 问题背景
**用户反馈**:
> "在公众号后台首次预览正确,对文章做修改后脚本格式就出现错误"
**问题分析**:
- 首次粘贴/预览时,HTML 样式正确
- 但在微信编辑器中修改后,样式被重置
- 代码块显示为单行,失去换行
#### 根因分析
**微信公众号编辑器特点**:
1. **HTML 净化机制**
- 微信编辑器会"净化"粘贴的 HTML
- 某些标签和样式会被重置或移除
- 特别是 `<pre>`和`<code>` 标签
2. **样式重置**
- `white-space` 属性可能被忽略
- `word-break` 属性可能被重置
- 编辑后重新渲染时丢失格式
3. **标签兼容性**
- `<pre>` 标签在编辑后容易失去样式
- `<code>` 标签的字体可能被重置
- 嵌套结构可能被简化
---
## 🔧 技术解决方案
### v2.6 及之前方案(不稳定)
```html
<pre style="white-space: pre-wrap; word-break: break-all;">
<code style="font-family: Consolas, monospace;">
第一行代码
第二行代码
第三行代码
</code>
</pre>
```
**问题**:
- 依赖 `white-space` 属性
- 编辑后 `<pre>` 样式被重置
- 所有行合并为单行
---
### v2.7 新方案(稳定)
```html
<section style="background: #F7F6F3; padding: 20px; border-radius: 4px; border-left: 4px solid #EB5757;">
<div style="white-space: pre; font-family: Consolas, monospace;">第一行代码</div>
<div style="white-space: pre; font-family: Consolas, monospace;">第二行代码</div>
<div style="white-space: pre; font-family: Consolas, monospace;">第三行代码</div>
</section>
```
**优势**:
- 每行独立 `<div>` 包裹
- 不依赖 `white-space` 跨行继承
- 编辑后格式保持
- 使用 `<section>` 替代 `<pre>`,微信支持更好
---
## 📊 效果对比
### 首次预览
**v2.6**(`<pre>+<code>`):
```
✅ 正确显示
cd ~/.openclaw/workspace/skills/minimax-docx
bash scripts/setup.sh
```
**v2.7**(`<section>+<div>`):
```
✅ 正确显示
cd ~/.openclaw/workspace/skills/minimax-docx
bash scripts/setup.sh
```
### 编辑后
**v2.6**(`<pre>+<code>`):
```
❌ 格式丢失
cd ~/.openclaw/workspace/skills/minimax-docx bash scripts/setup.sh
```
(单行显示)
**v2.7**(`<section>+<div>`):
```
✅ 格式保持
cd ~/.openclaw/workspace/skills/minimax-docx
bash scripts/setup.sh
```
(保持换行)
---
## 🔧 核心代码实现
### v2.7 代码块处理
```python
# 代码块(v2.7 优化:使用 section+div 结构,微信编辑器更稳定)
if in_code_block:
if line_stripped.startswith('```'):
html_parts.append('</section>')
in_code_block = False
else:
# v2.7 关键修复:每行使用 div 包裹,不依赖 white-space
# 转义 HTML 特殊字符
safe_line = line.replace('&', '&').replace('<', '<').replace('>', '>')
html_parts.append(
f'<div style="white-space: pre; '
f'font-family: Consolas, \'Courier New\', monospace; '
f'font-size: 13px; line-height: 1.6; '
f'color: #EB5757;">{safe_line}</div>'
)
continue
if line_stripped.startswith('```'):
# v2.7 使用 section 替代 pre,微信编辑器更稳定
html_parts.append(
'<section style="background: #F7F6F3; '
'padding: 20px; border-radius: 4px; '
'overflow-x: auto; margin: 30px 0; '
'border-left: 4px solid #EB5757;">'
)
in_code_block = True
continue
```
---
## ✅ 测试清单
### 基础测试
- [ ] 首次预览代码块正确显示
- [ ] 编辑后代码块保持换行
- [ ] 多行命令清晰可读
- [ ] 代码缩进保留
- [ ] 特殊字符正确转义(`&`、`<`、`>`)
### 编辑测试
- [ ] 在代码块前添加文字
- [ ] 在代码块后添加文字
- [ ] 修改代码块内容
- [ ] 删除部分代码行
- [ ] 复制粘贴代码块
### 兼容性测试
- [ ] 微信 PC 端编辑器
- [ ] 微信移动端编辑器
- [ ] 草稿箱预览
- [ ] 手机预览
---
## 🎯 最佳实践
### 代码块使用建议
1. **简洁优先**
- 代码块不宜过长(建议<50 行)
- 复杂代码考虑分多个代码块
2. **转义处理**
- 包含 HTML 字符时自动转义
- 包含 Markdown 语法时注意转义
3. **视觉标识**
- 左侧红色边框标识代码块
- 灰色背景区分正文
### 编辑建议
1. **首次预览后**
- 确认格式正确再编辑
- 避免大幅修改代码块
2. **需要修改时**
- 在源码 Markdown 中修改
- 重新发布而非编辑器修改
3. **紧急修改**
- 小修改可在编辑器进行
- v2.7 格式编辑后保持稳定
---
## 🐛 已知问题
暂无
---
## 📚 相关资源
- **GitHub**: https://github.com/xingkongqy/wechat-mp-xk
- **JVS Claw 推广**: https://www.aliyun.com/activity/ecs/clawdbot?userCode=d8ptsfvb
- **OpenClaw 文档**: https://docs.openclaw.ai
- **微信编辑器规范**: https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Overview.html
---
## 📈 版本演进
| 版本 | 发布日期 | 核心更新 |
|------|----------|----------|
| v2.5 | 2026-03-31 | 代码块换行修复(`white-space: pre-wrap`) |
| v2.6 | 2026-03-31 | 代码块优先级 + 空单元格继承 |
| v2.7 | 2026-03-31 | 微信编辑器兼容性优化(`section+div`) |
---
## 🔄 自我提升机制
```
用户反馈(编辑后格式丢失)
↓
根因分析(微信编辑器 HTML 净化)
↓
技术方案(section+div 替代 pre+code)
↓
实现验证(重新发布测试)
↓
文档更新(SKILL.md + CHANGELOG)
↓
版本迭代(v2.7)
↓
持续改进
```
---
## 💡 经验教训
### 教训 1:了解目标平台特性
**问题**:通用 HTML 在微信编辑器表现不佳
**解决**:
- 深入了解微信编辑器的 HTML 处理机制
- 使用微信友好的标签和结构
- 避免依赖可能被重置的样式
### 教训 2:测试场景要全面
**问题**:只测试了首次预览,未测试编辑后
**解决**:
- 建立完整的测试清单
- 包括编辑后场景
- 模拟真实使用流程
### 教训 3:结构优于样式
**问题**:依赖 CSS 属性保持格式
**解决**:
- 使用 HTML 结构保证格式
- 每行独立 `<div>` 包裹
- 不依赖跨行样式继承
---
**持续改进,不断进步!** 🚀
wechat-mp-xk 团队
2026 年 3 月 31 日
FILE:CHANGELOG-v2.8.md
# wechat-mp-xk v2.8 链接可点击修复
**发布日期**: 2026 年 3 月 31 日
---
## 🎉 重大更新
### v2.8.0 - 链接可点击修复
#### 问题背景
**用户反馈**:
> "文章中所有的链接,无法直接点击"
**问题截图**:
- 链接显示为普通文本
- 没有蓝色下划线样式
- 点击无反应
#### 根因分析
**Markdown 链接语法**:
```markdown
[GitHub 仓库](https://github.com/MiniMax-AI/skills)
```
**问题原因**:
1. **处理顺序问题** - 链接处理在加粗/斜体之后
2. **样式被破坏** - 如果链接文本包含 `**` 等语法,先被处理成 HTML
3. **正则匹配失败** - `[**text**](url)` 无法匹配标准链接正则
**错误处理流程**(v2.7 及之前):
```python
# 1. 先处理加粗
[**GitHub** 仓库](url) → [<strong>GitHub</strong> 仓库](url)
# 2. 再处理链接(失败!)
[<strong>GitHub</strong> 仓库](url) → 无法匹配,保持原样
```
**正确处理流程**(v2.8):
```python
# 1. 先处理链接
[**GitHub** 仓库](url) → <a href="url">**GitHub** 仓库</a>
# 2. 再处理加粗(在链接内部)
<a href="url">**GitHub** 仓库</a> → <a href="url"><strong>GitHub</strong> 仓库</a>
```
---
## 🔧 技术修复
### v2.8 关键修复
**修复 1:链接优先处理**
```python
def process_markdown_inline(text, for_table=False):
if not text:
return text
# v2.8 关键修复:链接优先处理(第一位)
text = re.sub(
r'\[([^\]]+)\]\(([^)]+)\)',
r'<a href="\2" style="color: #1E80FF; text-decoration: none; word-break: break-all; -webkit-tap-highlight-color: rgba(0,0,0,0);">\1</a>',
text
)
# 然后处理加粗、斜体、代码
# ...
```
**修复 2:移动端点击优化**
```html
<a href="url" style="
color: #1E80FF;
text-decoration: none;
word-break: break-all;
-webkit-tap-highlight-color: rgba(0,0,0,0);
">text</a>
```
**关键样式**:
- `-webkit-tap-highlight-color: rgba(0,0,0,0)` - 移除点击高亮
- `word-break: break-all` - 长链接自动换行
- `color: #1E80FF` - 微信标准链接蓝色
---
## 📊 效果对比
### 修复前(v2.7)
**输入**:
```markdown
- **GitHub 仓库**: https://github.com/MiniMax-AI/skills
- **JVS Claw 官网**: [https://www.aliyun.com/activity/ecs/clawdbot?userCode=d8ptsfvb](https://www.aliyun.com/activity/ecs/clawdbot?userCode=d8ptsfvb)
```
**输出**:
```html
<li><strong>GitHub 仓库</strong>: https://github.com/MiniMax-AI/skills</li>
<li><strong>JVS Claw 官网</strong>: [https://...](https://...)</li>
```
(链接显示为纯文本,无法点击)
### 修复后(v2.8)
**输入**(相同):
```markdown
- **GitHub 仓库**: https://github.com/MiniMax-AI/skills
- **JVS Claw 官网**: [https://www.aliyun.com/activity/ecs/clawdbot?userCode=d8ptsfvb](https://www.aliyun.com/activity/ecs/clawdbot?userCode=d8ptsfvb)
```
**输出**:
```html
<li><strong>GitHub 仓库</strong>: <a href="https://github.com/MiniMax-AI/skills" style="...">https://github.com/MiniMax-AI/skills</a></li>
<li><strong>JVS Claw 官网</strong>: <a href="https://..." style="...">https://...</a></li>
```
(链接可点击,蓝色显示)
---
## ✅ 测试清单
### 链接测试
- [ ] 标准链接可点击 `[text](url)`
- [ ] 纯 URL 自动转换(可选功能)
- [ ] 长链接自动换行
- [ ] 移动端点击响应正常
- [ ] 点击无多余高亮
### 组合语法测试
- [ ] 链接 + 加粗 `[**text**](url)`
- [ ] 链接 + 斜体 `[*text*](url)`
- [ ] 链接 + 代码 `` [`code`](url) ``
- [ ] 多个链接连续出现
### 样式测试
- [ ] 链接蓝色显示(#1E80FF)
- [ ] 无下划线(text-decoration: none)
- [ ] 点击高亮移除
- [ ] 长链接换行正常
---
## 🎯 最佳实践
### Markdown 链接写法
**推荐**:
```markdown
[GitHub 仓库](https://github.com/MiniMax-AI/skills)
```
**自动 URL**(需要额外支持):
```markdown
https://github.com/MiniMax-AI/skills
```
**列表中的链接**:
```markdown
- **资源 1**: [链接文本](url)
- **资源 2**: [链接文本](url)
```
### 避免的写法
**不推荐**(可能兼容性问题):
```markdown
[链接](url "标题") # 带标题属性
[链接][ref] # 引用式链接
```
---
## 🐛 已知问题
暂无
---
## 📚 相关资源
- **GitHub**: https://github.com/xingkongqy/wechat-mp-xk
- **JVS Claw 推广**: https://www.aliyun.com/activity/ecs/clawdbot?userCode=d8ptsfvb
- **OpenClaw 文档**: https://docs.openclaw.ai
- **微信编辑器规范**: https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Overview.html
---
## 📈 版本演进
| 版本 | 发布日期 | 核心更新 |
|------|----------|----------|
| v2.6 | 2026-03-31 | 代码块优先级 + 空单元格继承 |
| v2.7 | 2026-03-31 | 微信编辑器兼容性优化 |
| v2.8 | 2026-03-31 | 链接可点击修复 |
---
## 🔄 自我提升机制
```
用户反馈(链接无法点击)
↓
根因分析(处理顺序问题)
↓
技术方案(链接优先处理)
↓
实现验证(重新发布测试)
↓
文档更新(SKILL.md + CHANGELOG)
↓
版本迭代(v2.8)
↓
持续改进
```
---
## 💡 经验教训
### 教训 1:处理顺序很重要
**问题**:链接处理在加粗之后,导致包含加粗语法的链接无法识别
**解决**:
- 链接处理放在第一位
- 先包裹成 `<a>` 标签
- 内部语法后续处理
### 教训 2:移动端优化
**问题**:PC 端正常,移动端点击体验不佳
**解决**:
- 添加 `-webkit-tap-highlight-color`
- 移除点击高亮
- 优化触摸反馈
### 教训 3:测试场景要全面
**问题**:只测试了简单链接,未测试组合语法
**解决**:
- 测试链接 + 加粗
- 测试链接 + 斜体
- 测试链接 + 代码
- 测试多个链接
---
**持续改进,不断进步!** 🚀
wechat-mp-xk 团队
2026 年 3 月 31 日
FILE:CHANGELOG.md
# wechat-mp-xk v2.0 更新日志
**发布日期**: 2026 年 3 月 28 日
---
## 🎉 重大更新
### v2.0.0 - Markdown 渲染全面优化
#### 核心修复
1. **表格内 Markdown 语法渲染** ✅
- 修复前:表格单元格内的 `**加粗**` 直接显示原文
- 修复后:正确渲染为 HTML 加粗样式
- 影响范围:所有包含表格的文章
2. **行内格式全面支持** ✅
- **加粗** `**text**` → 黄色高亮背景
- *斜体* `*text*` → 斜体样式
- `代码` `` `code` `` → 红色等宽字体
- [链接](url) `[text](url)` → 蓝色可点击链接
3. **标题 Markdown 处理** ✅
- 一级、二级、三级标题均支持行内格式
- 标题中的加粗、链接正确渲染
4. **引用块优化** ✅
- 引用块内的 Markdown 语法正确处理
#### 技术改进
```python
# 新增加粗函数 process_markdown_inline()
def process_markdown_inline(text):
"""处理行内 Markdown 语法"""
# 加粗
text = re.sub(r'\*\*(.*?)\*\*', r'<strong>...</strong>', text)
# 斜体
text = re.sub(r'\*(?!\*)(.*?)(?<!\*)\*', r'<em>...</em>', text)
# 代码
text = re.sub(r'`([^`]+)`', r'<code>...</code>', text)
# 链接
text = re.sub(r'\[([^\]]+)\]\(([^)]+)\)', r'<a href="...">...</a>', text)
return text
```
#### 推广链接统一
- 新增 `JVS_CLAW_PROMO_LINK` 常量
- 统一使用链接:https://www.aliyun.com/activity/ecs/clawdbot?userCode=d8ptsfvb
---
## 📊 效果对比
### 表格渲染对比
**修复前**(v1.x):
```
| **实践导向** | 以解决组织实际问题为目标 |
```
显示为:`**实践导向**`(原文)
**修复后**(v2.0):
```
| <strong>实践导向</strong> | 以解决组织实际问题为目标 |
```
显示为:**实践导向**(正确渲染)
### 内容长度对比
| 版本 | 学术文章长度 | 提升 |
|------|-------------|------|
| v1.3 | 87,262 字符 | - |
| v2.0 | 91,863 字符 | +5.3% |
---
## 🔧 升级指南
### 方式 1:覆盖升级
```bash
cd ~/.openclaw/workspace/skills/wechat-mp-xk
# 备份旧版本
cp wechat_mp_xk.py wechat_mp_xk.py.bak
# 下载新版本
curl -L https://github.com/xingkongqy/wechat-mp-xk/raw/main/wechat_mp_xk.py \
-o wechat_mp_xk.py
# 验证版本
python3 wechat_mp_xk.py --version
```
### 方式 2:重新安装
```bash
# 卸载旧版本
rm -rf ~/.openclaw/workspace/skills/wechat-mp-xk
# 重新克隆
git clone https://github.com/xingkongqy/wechat-mp-xk.git \
~/.openclaw/workspace/skills/wechat-mp-xk
# 配置凭证
cp .env.example .env
# 编辑 .env 填入 AppID 和 Secret
```
---
## ✅ 测试清单
发布前请确认以下功能正常:
- [ ] 表格内加粗正确渲染
- [ ] 表格内链接正确显示
- [ ] 标题中的 Markdown 格式正常
- [ ] 列表项加粗正常
- [ ] 引用块格式正常
- [ ] 封面图上传成功
- [ ] 发布到草稿箱成功
---
## 📝 使用示例
### 学术论文发布
```bash
python3 wechat_mp_xk.py article paper.md \
--cover cover.jpg \
--title "学术论文标题" \
--author "研究团队"
```
### 技术文档发布
```bash
python3 wechat_mp_xk.py article docs.md \
--cover tech.png \
--title "技术文档" \
--author "技术团队"
```
---
## 🐛 已知问题
暂无
---
## 📚 相关资源
- **GitHub**: https://github.com/xingkongqy/wechat-mp-xk
- **JVS Claw**: https://www.aliyun.com/activity/ecs/clawdbot?userCode=d8ptsfvb
- **OpenClaw 文档**: https://docs.openclaw.ai
---
## 🎯 未来规划
### v2.1(计划中)
- [ ] 支持更多 Markdown 语法(删除线、下划线等)
- [ ] 图片自动压缩
- [ ] 多文章批量发布
- [ ] 发布历史记录
### v3.0(规划中)
- [ ] 可视化编辑器
- [ ] 模板系统
- [ ] 定时发布
- [ ] 数据分析
---
**持续改进,不断进步!** 🚀
wechat-mp-xk 团队
2026 年 3 月 28 日
FILE:README.md
# 📱 微信公众号发布工具 - wechat-mp-xk
[](https://opensource.org/licenses/MIT)
[](https://www.python.org/downloads/)
[](https://clawhub.ai)
[](https://github.com/xingkongqy/wechat-mp-xk)
**一键将 Markdown 文章发布到微信公众号草稿箱**
---
## ⚠️ 配置提示
**重要:** 本工具使用环境变量管理敏感信息,请勿在代码中硬编码 AppID/Secret!
---
## ✨ 功能特点
- 🔒 **安全配置** - 环境变量管理敏感信息
- 📱 **一键发布** - Markdown → 公众号草稿箱
- 🎨 **Knowledge-Base 主题** - 简约专业排版
- 🔧 **分步流程** - 灵活控制每个环节
- 🖼️ **自动图片** - 自动上传封面图
- 📝 **Front Matter** - 支持元数据配置
---
## 🚀 快速开始
### 安装
#### 方式 1:通过 ClawHub 安装
```bash
clawhub install wechat-mp-xk
```
#### 方式 2:从 GitHub 克隆
```bash
git clone https://github.com/xingkongqy/wechat-mp-xk.git
cd wechat-mp-xk
```
### 配置(重要!)
**方式 1:环境变量(推荐)**
```bash
# 临时配置(当前终端会话有效)
export WX_APPID="your_appid"
export WX_SECRET="your_secret"
# 永久配置(添加到 ~/.bashrc)
echo 'export WX_APPID="your_appid"' >> ~/.bashrc
echo 'export WX_SECRET="your_secret"' >> ~/.bashrc
source ~/.bashrc
```
**方式 2:.env 文件**
```bash
# 复制示例文件
cp .env.example .env
# 编辑 .env 文件,填入真实值
# ⚠️ 不要将 .env 提交到 Git!
# 设置文件权限
chmod 600 .env
```
### 一键发布
```bash
python3 wechat_mp_xk.py article article.md \
--cover cover.jpg \
--title "文章标题" \
--author "作者名"
```
---
## 📋 分步流程
### Step 1: Markdown 转 HTML
```bash
python3 wechat_mp_xk.py md2html article.md --output-dir .wxgzh
```
### Step 2: 修复 HTML
```bash
python3 wechat_mp_xk.py fix .wxgzh/article.html
```
### Step 3: 上传封面图
```bash
python3 wechat_mp_xk.py cover \
--cover cover.jpg \
--output .wxgzh/cover.json
```
### Step 4: 发布到草稿箱
```bash
python3 wechat_mp_xk.py publish \
--article .wxgzh/article.html \
--cover cover.jpg \
--title "文章标题"
```
---
## 🎨 Knowledge-Base 主题
| 元素 | 样式 |
|------|------|
| **一级标题** | 28px,底部细线分割 |
| **二级标题** | 22px,浅灰背景条 |
| **三级标题** | 18px,底部奶黄色高亮 |
| **正文** | 16px,行距 1.75 |
| **加粗** | 黄色高光笔效果 |
| **引用块** | 浅灰背景,左侧边框 |
| **表格** | 数据库风格 |
---
## 📁 文件结构
```
wechat-mp-xk/
├── wechat_mp_xk.py # 主程序(分步流程)
├── publish_kb_theme.py # Knowledge-Base 主题版
├── wechat_mp.py # 核心 API 模块
├── wechat_style_template.py # 排版模板
├── README.md # 使用文档
├── .env.example # 环境变量示例
├── .gitignore # Git 忽略配置
├── package.json # 包配置
└── tests/
└── test_publish.py # 测试用例
```
---
## ⚠️ 注意事项
1. **IP 白名单** - 服务器 IP 需在公众号后台配置
2. **作者名限制** - 最多 20 字节(中文约 6-7 字)
3. **标题限制** - 最多 64 字节
4. **Token 缓存** - 自动缓存到 /tmp/wechat_token.json
---
## 🧪 测试
```bash
# 运行测试
python3 -m pytest tests/
```
---
## 📄 License
MIT License
Copyright (c) 2026 九章快手团队
---
## 🔗 相关链接
- **GitHub:** https://github.com/xingkongqy/wechat-mp-xk
- **ClawHub:** 待发布
- **安全说明:** [SECURITY.md](SECURITY.md)
---
**版本:** v1.1.0
**创建时间:** 2026-03-20
**作者:** 九章快手团队
**团队口号:** 高效执行,持续优化,自我提升,永不止步!
FILE:_meta.json
{
"ownerId": "kn78z8cfad0jqt2nxjr2phx4ss835a7k",
"slug": "wechat-mp-xk",
"version": "2.8.0",
"publishedAt": 1773938446301,
"updatedAt": 1776064380000
}
FILE:package.json
{
"name": "wechat-mp-xk",
"version": "2.8.0",
"description": "微信公众号发布工具 - 安全版 v2.8,支持 Knowledge-Base 主题、分步流程、一键发布,优化表格和 Markdown 渲染",
"main": "wechat_mp_xk.py",
"scripts": {
"test": "python3 -m pytest tests/",
"article": "python3 wechat_mp_xk.py article",
"md2html": "python3 wechat_mp_xk.py md2html"
},
"keywords": [
"wechat",
"mp",
"publish",
"markdown",
"knowledge-base",
"security",
"openclaw",
"skill"
],
"author": "九章快手团队",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/xingkongqy/wechat-mp-xk.git"
},
"homepage": "https://github.com/xingkongqy/wechat-mp-xk#readme",
"bugs": {
"url": "https://github.com/xingkongqy/wechat-mp-xk/issues"
},
"engines": {
"python": ">=3.8"
},
"dependencies": {
"requests": ">=2.28.0"
}
}
FILE:wechat_mp_xk.py
#!/usr/bin/env python3
"""
微信公众号文章发布工具 - 分步流程版 v2.2
参考:https://github.com/lyhue1991/wxgzh
支持分步执行:
1. md2html - Markdown 转 HTML(应用 Knowledge-Base 主题)
2. fix - 修复 HTML(处理图片、移除不适合结构)
3. cover - 生成/上传封面图
4. publish - 发布到草稿箱
也可一键执行:article - 完成全部流程
v2.2 更新:
- 优化流程图表格样式(文字居中、边框对齐、连接线一致)
- 单列表格自动居中显示
- 表格内多行内容垂直居中
"""
import sys
import requests
import json
import time
import os
import argparse
import tempfile
import re
from datetime import datetime
# 配置
env_file = os.path.join(os.path.dirname(__file__), '.env')
if os.path.exists(env_file):
with open(env_file, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#') and '=' in line:
key, value = line.split('=', 1)
os.environ[key.strip()] = value.strip()
APPID = os.environ.get('WX_APPID')
SECRET = os.environ.get('WX_SECRET')
TOKEN_FILE = '/tmp/wechat_token.json'
DEFAULT_OUTPUT_DIR = './.wxgzh'
# 统一推广链接
JVS_CLAW_PROMO_LINK = "https://www.aliyun.com/activity/ecs/clawdbot?userCode=d8ptsfvb"
# 验证配置
if not APPID or not SECRET:
print("❌ 错误:缺少 WX_APPID 或 WX_SECRET 配置")
sys.exit(1)
# ==================== 工具函数 ====================
def get_access_token():
"""获取 stable_access_token"""
if os.path.exists(TOKEN_FILE):
try:
with open(TOKEN_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
if time.time() < data.get('expires_at', 0):
return data['access_token']
except:
pass
url = 'https://api.weixin.qq.com/cgi-bin/stable_token'
data = {
'grant_type': 'client_credential',
'appid': APPID,
'secret': SECRET,
'force_refresh': False
}
resp = requests.post(
url,
data=json.dumps(data, ensure_ascii=False).encode('utf-8'),
timeout=10,
headers={'Content-Type': 'application/json; charset=utf-8'}
)
result = resp.json()
if 'access_token' in result:
result['expires_at'] = time.time() + 7000
with open(TOKEN_FILE, 'w', encoding='utf-8') as f:
json.dump(result, f, ensure_ascii=False)
return result['access_token']
else:
raise Exception(f"Token 获取失败:{result}")
def extract_front_matter(md_content):
"""提取 Front Matter 元数据"""
front_matter = {}
content = md_content
if md_content.startswith('---'):
parts = md_content.split('---', 2)
if len(parts) >= 3:
fm_text = parts[1].strip()
content = parts[2].strip()
for line in fm_text.split('\n'):
if ':' in line:
key, value = line.split(':', 1)
front_matter[key.strip()] = value.strip().strip('"\'')
return front_matter, content
def process_markdown_inline(text, for_table=False):
"""
处理行内 Markdown 语法
v2.8 优化:链接优先处理,避免被其他语法破坏
Args:
text: 待处理的文本
for_table: 是否为表格内容(表格内加粗无背景色)
"""
if not text:
return text
# v2.8 关键修复:链接优先处理(第一位),避免被加粗/斜体破坏
# 处理链接 [text](url)
text = re.sub(
r'\[([^\]]+)\]\(([^)]+)\)',
r'<a href="\2" style="color: #1E80FF; text-decoration: none; word-break: break-all; -webkit-tap-highlight-color: rgba(0,0,0,0);">\1</a>',
text
)
# 处理加粗 **text**
if for_table:
text = re.sub(
r'\*\*(.*?)\*\*',
r'<strong style="color: #37352F; font-weight: 600;">\1</strong>',
text
)
else:
text = re.sub(
r'\*\*(.*?)\*\*',
r'<strong style="color: #37352F; font-weight: 600; background-color: #FDECC8; padding: 2px 4px; margin: 0 2px; border-radius: 3px;">\1</strong>',
text
)
# 处理斜体 *text*
text = re.sub(
r'\*(?!\*)(.*?)(?<!\*)\*',
r'<em style="font-style: italic;">\1</em>',
text
)
# 处理行内代码 `code`
text = re.sub(
r'`([^`]+)`',
r'<code style="background: #F7F6F3; padding: 2px 6px; border-radius: 3px; font-family: \'SFMono-Regular\', Consolas, monospace; font-size: 13px; color: #EB5757;">\1</code>',
text
)
return text
def is_flowchart_table(cells):
"""
判断是否为流程图表格
特征:单列或多列,但内容包含流程步骤关键词
"""
if not cells:
return False
# 单列表格通常是流程图或步骤说明
if len(cells) == 1:
return True
# 检查是否包含流程关键词
flowchart_keywords = [
'阶段', '步骤', '流程', '循环', '模型',
'Diagnosing', 'Action', 'Planning', 'Taking', 'Evaluating',
'诊断', '行动', '规划', '执行', '评估',
'基础设施', '建立', '研究范围'
]
first_cell = cells[0].lower()
return any(keyword.lower() in first_cell for keyword in flowchart_keywords)
def parse_table_alignment(separator_line):
"""
解析 Markdown 表格分隔线的对齐语法
|:-| 左对齐,|-:| 右对齐,|:-:| 居中对齐
Args:
separator_line: 分隔线,如 "|:---|:---:|---:|"
Returns:
list: 每列的对齐方式 ['left', 'center', 'right']
"""
alignments = []
cells = [c.strip() for c in separator_line.split('|') if c.strip()]
for cell in cells:
if cell.startswith(':') and cell.endswith(':'):
alignments.append('center') # :---: 居中
elif cell.endswith(':'):
alignments.append('right') # ---: 右对齐
else:
alignments.append('left') # --- 或 :--- 左对齐
return alignments
def markdown_to_kb_html(md_content):
"""Markdown 转 Knowledge-Base 风格 HTML(v2.2 优化版)"""
front_matter, content = extract_front_matter(md_content)
lines = content.split('\n')
html_parts = []
html_parts.append('<section id="wemd" style="padding: 30px 24px; max-width: 677px; margin: 0 auto; font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', \'Helvetica Neue\', \'PingFang SC\', sans-serif; color: #37352F; word-break: break-word;">')
in_code_block = False
in_table = False
last_line_was_table = False
current_table_is_flowchart = False
for line in lines:
line_stripped = line.strip()
# 代码块(v2.7 优化:使用 section+div 结构,微信编辑器更稳定)
# 微信编辑器特点:首次预览正确,但编辑后会重置 pre/code 样式
# 解决方案:使用 section 包裹 + div 每行 + !important 强制样式
if in_code_block:
if line_stripped.startswith('```'):
html_parts.append('</section>')
in_code_block = False
last_line_was_table = False
else:
# v2.7 关键修复:每行使用 div 包裹,不依赖 white-space
# 转义 HTML 特殊字符
safe_line = line.replace('&', '&').replace('<', '<').replace('>', '>')
html_parts.append(f'<div style="white-space: pre; font-family: Consolas, \'Courier New\', monospace; font-size: 13px; line-height: 1.7; color: #EB5757;">{safe_line}</div>')
continue
if line_stripped.startswith('```'):
# v2.7 使用 section 替代 pre,微信编辑器更稳定
html_parts.append('<section style="background: #F7F6F3; padding: 20px; border-radius: 4px; overflow-x: auto; margin: 30px 0; border-left: 4px solid #EB5757;">')
in_code_block = True
last_line_was_table = False
continue
# 空行
if not line_stripped:
if in_table:
html_parts.append('</table></section></section>')
in_table = False
last_line_was_table = False
continue
# v2.1 修复:移除水平线 `---` 或 `***`
if line_stripped.startswith('---') or line_stripped.startswith('***') or line_stripped.startswith('___'):
last_line_was_table = False
continue
# 一级标题
if line_stripped.startswith('# '):
title_text = process_markdown_inline(line_stripped[2:])
html_parts.append(f'<h1 style="margin-top: 50px; margin-bottom: 40px; text-align: left; border-bottom: 1px solid #E3E2E0; padding-bottom: 20px;"><span class="content" style="font-size: 28px; font-weight: 700; color: #37352F; display: inline-block; line-height: 1.2;">{title_text}</span></h1>')
last_line_was_table = False
continue
# 二级标题
if line_stripped.startswith('## '):
title_text = process_markdown_inline(line_stripped[3:])
html_parts.append(f'<h2 style="margin-top: 40px; margin-bottom: 20px; text-align: left;"><span class="content" style="display: block; font-size: 22px; font-weight: 600; color: #37352F; padding: 8px 12px; background-color: #F7F6F3; border-radius: 4px; line-height: 1.3;">{title_text}</span></h2>')
last_line_was_table = False
continue
# 三级标题
if line_stripped.startswith('### '):
title_text = process_markdown_inline(line_stripped[4:])
html_parts.append(f'<h3 style="margin-top: 30px; margin-bottom: 12px;"><span class="content" style="font-size: 18px; font-weight: 600; color: #37352F; display: inline-block; border-bottom: 3px solid #FDECC8; padding-bottom: 2px;">{title_text}</span></h3>')
last_line_was_table = False
continue
# v2.1 新增:四级标题支持
if line_stripped.startswith('#### '):
title_text = process_markdown_inline(line_stripped[5:])
html_parts.append(f'<h4 style="margin-top: 24px; margin-bottom: 10px;"><span class="content" style="font-size: 16px; font-weight: 600; color: #37352F; display: inline-block;">{title_text}</span></h4>')
last_line_was_table = False
continue
# 引用块
if line_stripped.startswith('> '):
quote_text = process_markdown_inline(line_stripped[2:])
html_parts.append(f'<div class="multiquote-1" style="margin: 24px 0; padding: 16px 16px 16px 20px; background-color: #F1F1EF; border: none; border-radius: 4px; border-left: 4px solid #37352F; overflow: visible !important;"><p style="margin: 0; color: #37352F; font-size: 15px; line-height: 1.6;">{quote_text}</p></div>')
last_line_was_table = False
continue
# 表格处理(v2.6 优化:支持空单元格自动继承)
if line_stripped.startswith('|'):
if not in_table:
# 判断是否为流程图表格
test_cells = [c.strip() for c in line_stripped.split('|') if c.strip()]
current_table_is_flowchart = is_flowchart_table(test_cells)
if current_table_is_flowchart:
# 流程图表格样式:居中对齐,统一边框
html_parts.append('<section><section><table style="width: 100%; border-collapse: collapse; margin: 30px 0; font-size: 15px; border: 2px solid #E3E2E0; border-radius: 4px; word-break: break-word;">')
else:
# 普通表格样式(v2.4 优化:添加 word-break)
html_parts.append('<section><section><table style="width: 100%; border-collapse: collapse; margin: 30px 0; font-size: 14px; border: 1px solid #E3E2E0; border-radius: 0; word-break: break-word;">')
in_table = True
table_column_alignments = [] # 存储每列的对齐方式
table_prev_row_cells = [] # v2.6 新增:存储上一行单元格用于继承
# 检查是否为分隔线行(包含 : 和 -)
if line_stripped.startswith('|---') or line_stripped.startswith('|:--') or line_stripped.startswith('|--:'):
# 解析对齐语法
table_column_alignments = parse_table_alignment(line_stripped)
last_line_was_table = True
continue
# 跳过分隔线行(已处理)
if not (line_stripped.startswith('|---') or line_stripped.startswith('|:--') or line_stripped.startswith('|--:')):
# v2.6 新增:解析原始单元格(保留空单元格)
raw_cells = line_stripped.split('|')
raw_cells = [c.strip() for c in raw_cells] # 保留空字符串
# 移除首尾空元素(Markdown 表格以 | 开头和结尾)
if raw_cells and raw_cells[0] == '':
raw_cells = raw_cells[1:]
if raw_cells and raw_cells[-1] == '':
raw_cells = raw_cells[:-1]
# v2.6 关键修复:自动填充空单元格(继承上一行对应列的值)
if 'table_prev_row_cells' not in locals():
table_prev_row_cells = []
cells = []
for i, cell in enumerate(raw_cells):
if cell == '' and i < len(table_prev_row_cells):
# 空单元格,继承上一行的值
cells.append(table_prev_row_cells[i])
else:
cells.append(cell)
# 更新上一行单元格记录
if cells:
table_prev_row_cells = cells
if cells:
# 判断是否为表头
is_header = any(word in cells[0] for word in ['角色', '指标', '任务', '时间', '使用', '检查', '步骤', '文件', '项目', '状态', '版本', '特征', '内涵', '体现', '阶段', '对比', '类型', '权益', '说明', '适用场景', '首发优惠', '本地接入', '云端', '跨平台', '多任务', 'AI 技能', '专属文件', '新功能', '高峰优先', '月度积分'])
row_tag = 'th' if is_header else 'td'
if current_table_is_flowchart:
# v2.2 流程图表格样式优化(v2.3 添加 word-break)
if is_header:
row_style = 'background: #F7F6F3; color: #37352F; font-weight: 600;'
else:
# 流程图内容:文字居中,垂直居中,统一边框
row_style = 'color: #37352F !important; background: #fff; text-align: center; vertical-align: middle;'
cell_style = row_style + ' border: 2px solid #E3E2E0; padding: 16px 12px; word-break: break-word; white-space: normal;'
else:
# v2.4 普通表格样式优化:支持列对齐和表头居中
if is_header:
# v2.4 新增:表头内容上下左右居中
row_style = 'background: #F7F6F3; color: #37352F; font-weight: 600; text-align: center; vertical-align: middle;'
else:
row_style = 'color: #37352F !important; background: #fff;'
# v2.4 关键优化:根据分隔线语法设置对齐方式
if table_column_alignments:
col_index = len([c for c in html_parts if '<tr>' in c]) % len(table_column_alignments)
align = table_column_alignments[col_index] if col_index < len(table_column_alignments) else 'left'
align_style = f'text-align: {align}; vertical-align: middle;'
else:
align_style = 'text-align: left; vertical-align: top;'
# v2.3 关键修复:添加 word-break 和 white-space 确保文字换行
cell_style = row_style + ' border: 1px solid #E3E2E0; padding: 10px 12px; ' + align_style + ' word-break: break-word; white-space: normal;'
# v2.1 关键修复:表格内容使用 for_table=True,加粗无背景色
cells_html = ''.join(f'<{row_tag} style="{cell_style}">{process_markdown_inline(c, for_table=True)}</{row_tag}>' for c in cells)
row = f'<tr>{cells_html}</tr>'
html_parts.append(row)
last_line_was_table = True
continue
elif in_table and not last_line_was_table:
html_parts.append('</table></section></section>')
in_table = False
current_table_is_flowchart = False
table_column_alignments = []
table_prev_row_cells = [] # v2.6 新增:重置单元格继承
last_line_was_table = False
# 列表
if line_stripped.startswith('- ') or line_stripped.startswith('* '):
list_text = process_markdown_inline(line_stripped[2:])
html_parts.append(f'<ul style="list-style-type: disc; padding-left: 24px; margin: 20px 0; color: #37352F;"><li style="margin-bottom: 8px; line-height: 1.0;"><section style="color: #37352F; font-size: 16px;">{list_text}</section></li></ul>')
last_line_was_table = False
continue
# 普通段落(v2.1 优化)
paragraph = process_markdown_inline(line_stripped)
html_parts.append(f'<section><section><p style="margin-top: 16px; margin-bottom: 16px; line-height: 1.25; letter-spacing: 0.2px; text-align: justify; color: #37352F; font-size: 16px;">{paragraph}</p></section></section>')
last_line_was_table = False
# 关闭未闭合标签
if in_code_block:
html_parts.append('</code></pre>')
if in_table:
html_parts.append('</table></section></section>')
html_parts.append('</section>')
html_content = '\n'.join(html_parts)
return html_content, front_matter
def upload_image(image_path, token=None):
"""上传图片获取 media_id"""
if token is None:
token = get_access_token()
if not os.path.exists(image_path):
return None
url = f'https://api.weixin.qq.com/cgi-bin/material/add_material?access_token={token}&type=image'
with open(image_path, 'rb') as f:
files = {'media': f}
resp = requests.post(url, files=files, timeout=30)
result = json.loads(resp.text, strict=False)
return result.get('media_id')
def fix_html(html_content, upload_images=True):
"""修复 HTML"""
html_content = re.sub(r'<script[^>]*>.*?</script>', '', html_content, flags=re.DOTALL)
html_content = re.sub(r'<iframe[^>]*>.*?</iframe>', '', html_content, flags=re.DOTALL)
html_content = re.sub(r'<style[^>]*>.*?</style>', '', html_content, flags=re.DOTALL)
return html_content
def create_draft(title, content, summary, thumb_media_id, author='黑白'):
"""创建草稿"""
token = get_access_token()
url = f'https://api.weixin.qq.com/cgi-bin/draft/add?access_token={token}'
data = {
'articles': [{
'title': title,
'author': author,
'digest': summary,
'content': content,
'thumb_media_id': thumb_media_id,
'show_cover_pic': 1
}]
}
resp = requests.post(
url,
data=json.dumps(data, ensure_ascii=False).encode('utf-8'),
timeout=30,
headers={'Content-Type': 'application/json; charset=utf-8'}
)
result = resp.json()
if result.get('errcode', 0) == 0:
return result.get('media_id'), result
else:
raise Exception(f"发布失败:{result.get('errmsg', '未知错误')}")
# ==================== 分步命令 ====================
def cmd_md2html(args):
"""Step 1: Markdown 转 HTML"""
print("=" * 60)
print("Step 1: Markdown 转 HTML(Knowledge-Base 主题 v2.2)")
print("=" * 60)
if not os.path.exists(args.input):
print(f"❌ 文件不存在:{args.input}")
sys.exit(1)
with open(args.input, 'r', encoding='utf-8') as f:
md_content = f.read()
html_content, front_matter = markdown_to_kb_html(md_content)
output_file = args.output
if not output_file:
output_dir = args.output_dir or DEFAULT_OUTPUT_DIR
os.makedirs(output_dir, exist_ok=True)
base_name = os.path.splitext(os.path.basename(args.input))[0]
output_file = os.path.join(output_dir, f'{base_name}.html')
with open(output_file, 'w', encoding='utf-8') as f:
f.write(html_content)
print(f"✅ HTML 已生成:{output_file}")
print(f" 内容长度:{len(html_content)}")
print(f" v2.2 优化:流程图表格居中、边框对齐、连接线一致")
if front_matter:
print(f" 元数据:{front_matter}")
return output_file, front_matter
def cmd_fix(args):
"""Step 2: 修复 HTML"""
print("=" * 60)
print("Step 2: 修复 HTML")
print("=" * 60)
if not os.path.exists(args.input):
print(f"❌ 文件不存在:{args.input}")
sys.exit(1)
with open(args.input, 'r', encoding='utf-8') as f:
html_content = f.read()
html_content = fix_html(html_content, upload_images=not args.no_upload)
output_file = args.output or args.input
with open(output_file, 'w', encoding='utf-8') as f:
f.write(html_content)
print(f"✅ HTML 已修复:{output_file}")
print(f" 内容长度:{len(html_content)}")
return output_file
def cmd_cover(args):
"""Step 3: 上传封面图"""
print("=" * 60)
print("Step 3: 上传封面图")
print("=" * 60)
if not args.cover:
print("❌ 请指定封面图路径:--cover <path>")
sys.exit(1)
media_id = upload_image(args.cover)
if media_id:
print(f"✅ 封面图上传成功")
print(f" media_id: {media_id}")
if args.output:
with open(args.output, 'w', encoding='utf-8') as f:
json.dump({'media_id': media_id}, f, ensure_ascii=False)
print(f" 已保存:{args.output}")
return media_id
else:
print("❌ 封面图上传失败")
sys.exit(1)
def cmd_publish(args):
"""Step 4: 发布到草稿箱"""
print("=" * 60)
print("Step 4: 发布到草稿箱")
print("=" * 60)
if not os.path.exists(args.article):
print(f"❌ 文章文件不存在:{args.article}")
sys.exit(1)
with open(args.article, 'r', encoding='utf-8') as f:
content = f.read()
thumb_media_id = None
if args.cover:
if os.path.exists(args.cover):
thumb_media_id = upload_image(args.cover)
else:
thumb_media_id = args.cover
if not thumb_media_id:
print("❌ 请指定封面图:--cover <path>")
sys.exit(1)
title = args.title or os.path.basename(args.article)
author = args.author or '黑白'
summary = args.summary or ''
try:
media_id, result = create_draft(title, content, summary, thumb_media_id, author)
print(f"✅ 发布成功!")
print(f" 草稿 ID: {media_id}")
print(f" 标题:{title}")
print(f" 作者:{author}")
print(f" 请前往公众号后台查看:https://mp.weixin.qq.com")
return media_id
except Exception as e:
print(f"❌ 发布失败:{e}")
sys.exit(1)
def cmd_article(args):
"""一键发布"""
print("=" * 60)
print("一键发布:Markdown → 公众号草稿箱 (v2.2)")
print("=" * 60)
print()
html_file, front_matter = cmd_md2html(args)
print()
args.input = html_file
args.output = html_file
if not hasattr(args, 'no_upload'):
args.no_upload = False
cmd_fix(args)
print()
if args.cover:
cover_file = os.path.join(os.path.dirname(html_file), 'cover.json')
args.output = cover_file
media_id = cmd_cover(args)
print()
else:
media_id = None
print("⚠️ 未指定封面图")
print()
args.article = html_file
args.title = args.title or front_matter.get('title')
args.author = args.author or front_matter.get('author', '黑白')
args.summary = args.summary or front_matter.get('digest', '')
args.cover = media_id or args.cover
cmd_publish(args)
# ==================== 主函数 ====================
def main():
parser = argparse.ArgumentParser(description='微信公众号文章发布工具 - 分步流程版 v2.2')
subparsers = parser.add_subparsers(dest='command', help='子命令')
p_md2html = subparsers.add_parser('md2html', help='Markdown 转 HTML')
p_md2html.add_argument('input', help='输入 Markdown 文件')
p_md2html.add_argument('-o', '--output', help='输出 HTML 文件')
p_md2html.add_argument('--output-dir', default=DEFAULT_OUTPUT_DIR, help='输出目录')
p_md2html.set_defaults(func=cmd_md2html)
p_fix = subparsers.add_parser('fix', help='修复 HTML')
p_fix.add_argument('input', help='输入 HTML 文件')
p_fix.add_argument('-o', '--output', help='输出 HTML 文件')
p_fix.add_argument('--no-upload', action='store_true', help='不上传图片')
p_fix.set_defaults(func=cmd_fix)
p_cover = subparsers.add_parser('cover', help='上传封面图')
p_cover.add_argument('--cover', required=True, help='封面图路径')
p_cover.add_argument('-o', '--output', help='保存 media_id 的文件')
p_cover.set_defaults(func=cmd_cover)
p_publish = subparsers.add_parser('publish', help='发布到草稿箱')
p_publish.add_argument('--article', required=True, help='HTML 文章文件')
p_publish.add_argument('--cover', required=True, help='封面图路径或 media_id')
p_publish.add_argument('--title', help='文章标题')
p_publish.add_argument('--author', default='黑白', help='作者名')
p_publish.add_argument('--summary', default='', help='文章摘要')
p_publish.set_defaults(func=cmd_publish)
p_article = subparsers.add_parser('article', help='一键发布')
p_article.add_argument('input', help='输入 Markdown 文件')
p_article.add_argument('-o', '--output', help='输出 HTML 文件')
p_article.add_argument('--output-dir', default=DEFAULT_OUTPUT_DIR, help='输出目录')
p_article.add_argument('--cover', help='封面图路径')
p_article.add_argument('--title', help='文章标题')
p_article.add_argument('--author', default='黑白', help='作者名')
p_article.add_argument('--summary', default='', help='文章摘要')
p_article.set_defaults(func=cmd_article)
args = parser.parse_args()
if args.command is None:
parser.print_help()
sys.exit(1)
args.func(args)
if __name__ == '__main__':
main()
FILE:wechat_mp_xk_v2.1.py
#!/usr/bin/env python3
"""
微信公众号文章发布工具 - 分步流程版 v2.1
参考:https://github.com/lyhue1991/wxgzh
支持分步执行:
1. md2html - Markdown 转 HTML(应用 Knowledge-Base 主题)
2. fix - 修复 HTML(处理图片、移除不适合结构)
3. cover - 生成/上传封面图
4. publish - 发布到草稿箱
也可一键执行:article - 完成全部流程
v2.1 更新:
- 修复段落后的 `---` 分隔符问题
- 添加四级标题支持(####)
- 表格第一列加粗移除背景色
- 优化表格内容对齐和格式
"""
import sys
import requests
import json
import time
import os
import argparse
import tempfile
import re
from datetime import datetime
# 配置
env_file = os.path.join(os.path.dirname(__file__), '.env')
if os.path.exists(env_file):
with open(env_file, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#') and '=' in line:
key, value = line.split('=', 1)
os.environ[key.strip()] = value.strip()
APPID = os.environ.get('WX_APPID')
SECRET = os.environ.get('WX_SECRET')
TOKEN_FILE = '/tmp/wechat_token.json'
DEFAULT_OUTPUT_DIR = './.wxgzh'
# 统一推广链接
JVS_CLAW_PROMO_LINK = "https://www.aliyun.com/activity/ecs/clawdbot?userCode=d8ptsfvb"
# 验证配置
if not APPID or not SECRET:
print("❌ 错误:缺少 WX_APPID 或 WX_SECRET 配置")
sys.exit(1)
# ==================== 工具函数 ====================
def get_access_token():
"""获取 stable_access_token"""
if os.path.exists(TOKEN_FILE):
try:
with open(TOKEN_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
if time.time() < data.get('expires_at', 0):
return data['access_token']
except:
pass
url = 'https://api.weixin.qq.com/cgi-bin/stable_token'
data = {
'grant_type': 'client_credential',
'appid': APPID,
'secret': SECRET,
'force_refresh': False
}
resp = requests.post(
url,
data=json.dumps(data, ensure_ascii=False).encode('utf-8'),
timeout=10,
headers={'Content-Type': 'application/json; charset=utf-8'}
)
result = resp.json()
if 'access_token' in result:
result['expires_at'] = time.time() + 7000
with open(TOKEN_FILE, 'w', encoding='utf-8') as f:
json.dump(result, f, ensure_ascii=False)
return result['access_token']
else:
raise Exception(f"Token 获取失败:{result}")
def extract_front_matter(md_content):
"""提取 Front Matter 元数据"""
front_matter = {}
content = md_content
if md_content.startswith('---'):
parts = md_content.split('---', 2)
if len(parts) >= 3:
fm_text = parts[1].strip()
content = parts[2].strip()
for line in fm_text.split('\n'):
if ':' in line:
key, value = line.split(':', 1)
front_matter[key.strip()] = value.strip().strip('"\'')
return front_matter, content
def process_markdown_inline(text, for_table=False):
"""
处理行内 Markdown 语法
Args:
text: 待处理的文本
for_table: 是否为表格内容(表格内加粗无背景色)
"""
if not text:
return text
# 处理加粗 **text**
if for_table:
# 表格内:加粗无背景色
text = re.sub(
r'\*\*(.*?)\*\*',
r'<strong style="color: #37352F; font-weight: 600;">\1</strong>',
text
)
else:
# 普通段落:加粗有背景色
text = re.sub(
r'\*\*(.*?)\*\*',
r'<strong style="color: #37352F; font-weight: 600; background-color: #FDECC8; padding: 2px 4px; margin: 0 2px; border-radius: 3px;">\1</strong>',
text
)
# 处理斜体 *text*
text = re.sub(
r'\*(?!\*)(.*?)(?<!\*)\*',
r'<em style="font-style: italic;">\1</em>',
text
)
# 处理行内代码 `code`
text = re.sub(
r'`([^`]+)`',
r'<code style="background: #F7F6F3; padding: 2px 6px; border-radius: 3px; font-family: \'SFMono-Regular\', Consolas, monospace; font-size: 13px; color: #EB5757;">\1</code>',
text
)
# 处理链接 [text](url)
text = re.sub(
r'\[([^\]]+)\]\(([^)]+)\)',
r'<a href="\2" style="color: #1E80FF; text-decoration: none; word-break: break-all;">\1</a>',
text
)
return text
def markdown_to_kb_html(md_content):
"""Markdown 转 Knowledge-Base 风格 HTML(v2.1 优化版)"""
front_matter, content = extract_front_matter(md_content)
lines = content.split('\n')
html_parts = []
html_parts.append('<section id="wemd" style="padding: 30px 24px; max-width: 677px; margin: 0 auto; font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', \'Helvetica Neue\', \'PingFang SC\', sans-serif; color: #37352F; word-break: break-word;">')
in_code_block = False
in_table = False
last_line_was_table = False
for line in lines:
line_stripped = line.strip()
# 代码块
if line_stripped.startswith('```'):
if in_code_block:
html_parts.append('</code></pre>')
in_code_block = False
else:
html_parts.append('<pre style="background: #F7F6F3; padding: 20px; border-radius: 4px; overflow-x: auto;"><code style="color: #EB5757; font-family: \'SFMono-Regular\', Consolas, monospace; font-size: 13px; line-height: 1.6;">')
in_code_block = True
last_line_was_table = False
continue
if in_code_block:
html_parts.append(line_stripped)
continue
# 空行
if not line_stripped:
if in_table:
html_parts.append('</table></section></section>')
in_table = False
last_line_was_table = False
continue
# v2.1 修复:移除水平线 `---` 或 `***`
if line_stripped.startswith('---') or line_stripped.startswith('***') or line_stripped.startswith('___'):
# 忽略水平线,不输出任何内容
last_line_was_table = False
continue
# 一级标题
if line_stripped.startswith('# '):
title_text = process_markdown_inline(line_stripped[2:])
html_parts.append(f'<h1 style="margin-top: 50px; margin-bottom: 40px; text-align: left; border-bottom: 1px solid #E3E2E0; padding-bottom: 20px;"><span class="content" style="font-size: 28px; font-weight: 700; color: #37352F; display: inline-block; line-height: 1.2;">{title_text}</span></h1>')
last_line_was_table = False
continue
# 二级标题
if line_stripped.startswith('## '):
title_text = process_markdown_inline(line_stripped[3:])
html_parts.append(f'<h2 style="margin-top: 40px; margin-bottom: 20px; text-align: left;"><span class="content" style="display: block; font-size: 22px; font-weight: 600; color: #37352F; padding: 8px 12px; background-color: #F7F6F3; border-radius: 4px; line-height: 1.3;">{title_text}</span></h2>')
last_line_was_table = False
continue
# 三级标题
if line_stripped.startswith('### '):
title_text = process_markdown_inline(line_stripped[4:])
html_parts.append(f'<h3 style="margin-top: 30px; margin-bottom: 12px;"><span class="content" style="font-size: 18px; font-weight: 600; color: #37352F; display: inline-block; border-bottom: 3px solid #FDECC8; padding-bottom: 2px;">{title_text}</span></h3>')
last_line_was_table = False
continue
# v2.1 新增:四级标题支持
if line_stripped.startswith('#### '):
title_text = process_markdown_inline(line_stripped[5:])
html_parts.append(f'<h4 style="margin-top: 24px; margin-bottom: 10px;"><span class="content" style="font-size: 16px; font-weight: 600; color: #37352F; display: inline-block;">{title_text}</span></h4>')
last_line_was_table = False
continue
# 引用块
if line_stripped.startswith('> '):
quote_text = process_markdown_inline(line_stripped[2:])
html_parts.append(f'<div class="multiquote-1" style="margin: 24px 0; padding: 16px 16px 16px 20px; background-color: #F1F1EF; border: none; border-radius: 4px; border-left: 4px solid #37352F; overflow: visible !important;"><p style="margin: 0; color: #37352F; font-size: 15px; line-height: 1.6;">{quote_text}</p></div>')
last_line_was_table = False
continue
# 表格处理(v2.1 优化)
if line_stripped.startswith('|'):
if not in_table:
html_parts.append('<section><section><table style="width: 100%; border-collapse: collapse; margin: 30px 0; font-size: 14px; border: 1px solid #E3E2E0; border-radius: 0;">')
in_table = True
# 跳过分隔线行
if not line_stripped.startswith('|---'):
cells = [c.strip() for c in line_stripped.split('|') if c.strip()]
if cells:
# 判断是否为表头
is_header = any(word in cells[0] for word in ['角色', '指标', '任务', '时间', '使用', '检查', '步骤', '文件', '项目', '状态', '版本', '特征', '内涵', '体现', '阶段', '对比', '类型', '权益', '说明', '适用场景', '首发优惠', '本地接入', '云端', '跨平台', '多任务', 'AI 技能', '专属文件', '新功能', '高峰优先', '月度积分'])
row_tag = 'th' if is_header else 'td'
if is_header:
row_style = 'background: #F7F6F3; color: #37352F; font-weight: 600;'
else:
row_style = 'color: #37352F; background: #fff;'
cell_style = row_style + ' border: 1px solid #E3E2E0; padding: 10px 12px; text-align: left; vertical-align: top;'
# v2.1 关键修复:表格内容使用 for_table=True,加粗无背景色
cells_html = ''.join(f'<{row_tag} style="{cell_style}">{process_markdown_inline(c, for_table=True)}</{row_tag}>' for c in cells)
row = f'<tr>{cells_html}</tr>'
html_parts.append(row)
last_line_was_table = True
continue
elif in_table and not last_line_was_table:
html_parts.append('</table></section></section>')
in_table = False
last_line_was_table = False
# 列表
if line_stripped.startswith('- ') or line_stripped.startswith('* '):
list_text = process_markdown_inline(line_stripped[2:])
html_parts.append(f'<ul style="list-style-type: disc; padding-left: 24px; margin: 16px 0; color: #37352F;"><li style="margin-bottom: 8px; line-height: 1.7;"><section style="color: #37352F; font-size: 16px;">{list_text}</section></li></ul>')
last_line_was_table = False
continue
# 普通段落(v2.1 优化)
paragraph = process_markdown_inline(line_stripped)
html_parts.append(f'<section><section><p style="margin-top: 16px; margin-bottom: 16px; line-height: 1.75; letter-spacing: 0.2px; text-align: justify; color: #37352F; font-size: 16px;">{paragraph}</p></section></section>')
last_line_was_table = False
# 关闭未闭合标签
if in_code_block:
html_parts.append('</code></pre>')
if in_table:
html_parts.append('</table></section></section>')
html_parts.append('</section>')
html_content = '\n'.join(html_parts)
return html_content, front_matter
def upload_image(image_path, token=None):
"""上传图片获取 media_id"""
if token is None:
token = get_access_token()
if not os.path.exists(image_path):
return None
url = f'https://api.weixin.qq.com/cgi-bin/material/add_material?access_token={token}&type=image'
with open(image_path, 'rb') as f:
files = {'media': f}
resp = requests.post(url, files=files, timeout=30)
result = json.loads(resp.text, strict=False)
return result.get('media_id')
def fix_html(html_content, upload_images=True):
"""修复 HTML"""
html_content = re.sub(r'<script[^>]*>.*?</script>', '', html_content, flags=re.DOTALL)
html_content = re.sub(r'<iframe[^>]*>.*?</iframe>', '', html_content, flags=re.DOTALL)
html_content = re.sub(r'<style[^>]*>.*?</style>', '', html_content, flags=re.DOTALL)
return html_content
def create_draft(title, content, summary, thumb_media_id, author='黑白'):
"""创建草稿"""
token = get_access_token()
url = f'https://api.weixin.qq.com/cgi-bin/draft/add?access_token={token}'
data = {
'articles': [{
'title': title,
'author': author,
'digest': summary,
'content': content,
'thumb_media_id': thumb_media_id,
'show_cover_pic': 1
}]
}
resp = requests.post(
url,
data=json.dumps(data, ensure_ascii=False).encode('utf-8'),
timeout=30,
headers={'Content-Type': 'application/json; charset=utf-8'}
)
result = resp.json()
if result.get('errcode', 0) == 0:
return result.get('media_id'), result
else:
raise Exception(f"发布失败:{result.get('errmsg', '未知错误')}")
# ==================== 分步命令 ====================
def cmd_md2html(args):
"""Step 1: Markdown 转 HTML"""
print("=" * 60)
print("Step 1: Markdown 转 HTML(Knowledge-Base 主题 v2.1)")
print("=" * 60)
if not os.path.exists(args.input):
print(f"❌ 文件不存在:{args.input}")
sys.exit(1)
with open(args.input, 'r', encoding='utf-8') as f:
md_content = f.read()
html_content, front_matter = markdown_to_kb_html(md_content)
output_file = args.output
if not output_file:
output_dir = args.output_dir or DEFAULT_OUTPUT_DIR
os.makedirs(output_dir, exist_ok=True)
base_name = os.path.splitext(os.path.basename(args.input))[0]
output_file = os.path.join(output_dir, f'{base_name}.html')
with open(output_file, 'w', encoding='utf-8') as f:
f.write(html_content)
print(f"✅ HTML 已生成:{output_file}")
print(f" 内容长度:{len(html_content)}")
print(f" v2.1 优化:移除 `---` 分隔符、四级标题支持、表格加粗无背景色")
if front_matter:
print(f" 元数据:{front_matter}")
return output_file, front_matter
def cmd_fix(args):
"""Step 2: 修复 HTML"""
print("=" * 60)
print("Step 2: 修复 HTML")
print("=" * 60)
if not os.path.exists(args.input):
print(f"❌ 文件不存在:{args.input}")
sys.exit(1)
with open(args.input, 'r', encoding='utf-8') as f:
html_content = f.read()
html_content = fix_html(html_content, upload_images=not args.no_upload)
output_file = args.output or args.input
with open(output_file, 'w', encoding='utf-8') as f:
f.write(html_content)
print(f"✅ HTML 已修复:{output_file}")
print(f" 内容长度:{len(html_content)}")
return output_file
def cmd_cover(args):
"""Step 3: 上传封面图"""
print("=" * 60)
print("Step 3: 上传封面图")
print("=" * 60)
if not args.cover:
print("❌ 请指定封面图路径:--cover <path>")
sys.exit(1)
media_id = upload_image(args.cover)
if media_id:
print(f"✅ 封面图上传成功")
print(f" media_id: {media_id}")
if args.output:
with open(args.output, 'w', encoding='utf-8') as f:
json.dump({'media_id': media_id}, f, ensure_ascii=False)
print(f" 已保存:{args.output}")
return media_id
else:
print("❌ 封面图上传失败")
sys.exit(1)
def cmd_publish(args):
"""Step 4: 发布到草稿箱"""
print("=" * 60)
print("Step 4: 发布到草稿箱")
print("=" * 60)
if not os.path.exists(args.article):
print(f"❌ 文章文件不存在:{args.article}")
sys.exit(1)
with open(args.article, 'r', encoding='utf-8') as f:
content = f.read()
thumb_media_id = None
if args.cover:
if os.path.exists(args.cover):
thumb_media_id = upload_image(args.cover)
else:
thumb_media_id = args.cover
if not thumb_media_id:
print("❌ 请指定封面图:--cover <path>")
sys.exit(1)
title = args.title or os.path.basename(args.article)
author = args.author or '黑白'
summary = args.summary or ''
try:
media_id, result = create_draft(title, content, summary, thumb_media_id, author)
print(f"✅ 发布成功!")
print(f" 草稿 ID: {media_id}")
print(f" 标题:{title}")
print(f" 作者:{author}")
print(f" 请前往公众号后台查看:https://mp.weixin.qq.com")
return media_id
except Exception as e:
print(f"❌ 发布失败:{e}")
sys.exit(1)
def cmd_article(args):
"""一键发布"""
print("=" * 60)
print("一键发布:Markdown → 公众号草稿箱 (v2.1)")
print("=" * 60)
print()
html_file, front_matter = cmd_md2html(args)
print()
args.input = html_file
args.output = html_file
if not hasattr(args, 'no_upload'):
args.no_upload = False
cmd_fix(args)
print()
if args.cover:
cover_file = os.path.join(os.path.dirname(html_file), 'cover.json')
args.output = cover_file
media_id = cmd_cover(args)
print()
else:
media_id = None
print("⚠️ 未指定封面图")
print()
args.article = html_file
args.title = args.title or front_matter.get('title')
args.author = args.author or front_matter.get('author', '黑白')
args.summary = args.summary or front_matter.get('digest', '')
args.cover = media_id or args.cover
cmd_publish(args)
# ==================== 主函数 ====================
def main():
parser = argparse.ArgumentParser(description='微信公众号文章发布工具 - 分步流程版 v2.1')
subparsers = parser.add_subparsers(dest='command', help='子命令')
p_md2html = subparsers.add_parser('md2html', help='Markdown 转 HTML')
p_md2html.add_argument('input', help='输入 Markdown 文件')
p_md2html.add_argument('-o', '--output', help='输出 HTML 文件')
p_md2html.add_argument('--output-dir', default=DEFAULT_OUTPUT_DIR, help='输出目录')
p_md2html.set_defaults(func=cmd_md2html)
p_fix = subparsers.add_parser('fix', help='修复 HTML')
p_fix.add_argument('input', help='输入 HTML 文件')
p_fix.add_argument('-o', '--output', help='输出 HTML 文件')
p_fix.add_argument('--no-upload', action='store_true', help='不上传图片')
p_fix.set_defaults(func=cmd_fix)
p_cover = subparsers.add_parser('cover', help='上传封面图')
p_cover.add_argument('--cover', required=True, help='封面图路径')
p_cover.add_argument('-o', '--output', help='保存 media_id 的文件')
p_cover.set_defaults(func=cmd_cover)
p_publish = subparsers.add_parser('publish', help='发布到草稿箱')
p_publish.add_argument('--article', required=True, help='HTML 文章文件')
p_publish.add_argument('--cover', required=True, help='封面图路径或 media_id')
p_publish.add_argument('--title', help='文章标题')
p_publish.add_argument('--author', default='黑白', help='作者名')
p_publish.add_argument('--summary', default='', help='文章摘要')
p_publish.set_defaults(func=cmd_publish)
p_article = subparsers.add_parser('article', help='一键发布')
p_article.add_argument('input', help='输入 Markdown 文件')
p_article.add_argument('-o', '--output', help='输出 HTML 文件')
p_article.add_argument('--output-dir', default=DEFAULT_OUTPUT_DIR, help='输出目录')
p_article.add_argument('--cover', help='封面图路径')
p_article.add_argument('--title', help='文章标题')
p_article.add_argument('--author', default='黑白', help='作者名')
p_article.add_argument('--summary', default='', help='文章摘要')
p_article.set_defaults(func=cmd_article)
args = parser.parse_args()
if args.command is None:
parser.print_help()
sys.exit(1)
args.func(args)
if __name__ == '__main__':
main()
FILE:wechat_mp_xk_v2.2.py
#!/usr/bin/env python3
"""
微信公众号文章发布工具 - 分步流程版 v2.2
参考:https://github.com/lyhue1991/wxgzh
支持分步执行:
1. md2html - Markdown 转 HTML(应用 Knowledge-Base 主题)
2. fix - 修复 HTML(处理图片、移除不适合结构)
3. cover - 生成/上传封面图
4. publish - 发布到草稿箱
也可一键执行:article - 完成全部流程
v2.2 更新:
- 优化流程图表格样式(文字居中、边框对齐、连接线一致)
- 单列表格自动居中显示
- 表格内多行内容垂直居中
"""
import sys
import requests
import json
import time
import os
import argparse
import tempfile
import re
from datetime import datetime
# 配置
env_file = os.path.join(os.path.dirname(__file__), '.env')
if os.path.exists(env_file):
with open(env_file, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#') and '=' in line:
key, value = line.split('=', 1)
os.environ[key.strip()] = value.strip()
APPID = os.environ.get('WX_APPID')
SECRET = os.environ.get('WX_SECRET')
TOKEN_FILE = '/tmp/wechat_token.json'
DEFAULT_OUTPUT_DIR = './.wxgzh'
# 统一推广链接
JVS_CLAW_PROMO_LINK = "https://www.aliyun.com/activity/ecs/clawdbot?userCode=d8ptsfvb"
# 验证配置
if not APPID or not SECRET:
print("❌ 错误:缺少 WX_APPID 或 WX_SECRET 配置")
sys.exit(1)
# ==================== 工具函数 ====================
def get_access_token():
"""获取 stable_access_token"""
if os.path.exists(TOKEN_FILE):
try:
with open(TOKEN_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
if time.time() < data.get('expires_at', 0):
return data['access_token']
except:
pass
url = 'https://api.weixin.qq.com/cgi-bin/stable_token'
data = {
'grant_type': 'client_credential',
'appid': APPID,
'secret': SECRET,
'force_refresh': False
}
resp = requests.post(
url,
data=json.dumps(data, ensure_ascii=False).encode('utf-8'),
timeout=10,
headers={'Content-Type': 'application/json; charset=utf-8'}
)
result = resp.json()
if 'access_token' in result:
result['expires_at'] = time.time() + 7000
with open(TOKEN_FILE, 'w', encoding='utf-8') as f:
json.dump(result, f, ensure_ascii=False)
return result['access_token']
else:
raise Exception(f"Token 获取失败:{result}")
def extract_front_matter(md_content):
"""提取 Front Matter 元数据"""
front_matter = {}
content = md_content
if md_content.startswith('---'):
parts = md_content.split('---', 2)
if len(parts) >= 3:
fm_text = parts[1].strip()
content = parts[2].strip()
for line in fm_text.split('\n'):
if ':' in line:
key, value = line.split(':', 1)
front_matter[key.strip()] = value.strip().strip('"\'')
return front_matter, content
def process_markdown_inline(text, for_table=False):
"""
处理行内 Markdown 语法
Args:
text: 待处理的文本
for_table: 是否为表格内容(表格内加粗无背景色)
"""
if not text:
return text
# 处理加粗 **text**
if for_table:
text = re.sub(
r'\*\*(.*?)\*\*',
r'<strong style="color: #37352F; font-weight: 600;">\1</strong>',
text
)
else:
text = re.sub(
r'\*\*(.*?)\*\*',
r'<strong style="color: #37352F; font-weight: 600; background-color: #FDECC8; padding: 2px 4px; margin: 0 2px; border-radius: 3px;">\1</strong>',
text
)
# 处理斜体 *text*
text = re.sub(
r'\*(?!\*)(.*?)(?<!\*)\*',
r'<em style="font-style: italic;">\1</em>',
text
)
# 处理行内代码 `code`
text = re.sub(
r'`([^`]+)`',
r'<code style="background: #F7F6F3; padding: 2px 6px; border-radius: 3px; font-family: \'SFMono-Regular\', Consolas, monospace; font-size: 13px; color: #EB5757;">\1</code>',
text
)
# 处理链接 [text](url)
text = re.sub(
r'\[([^\]]+)\]\(([^)]+)\)',
r'<a href="\2" style="color: #1E80FF; text-decoration: none; word-break: break-all;">\1</a>',
text
)
return text
def is_flowchart_table(cells):
"""
判断是否为流程图表格
特征:单列或多列,但内容包含流程步骤关键词
"""
if not cells:
return False
# 单列表格通常是流程图或步骤说明
if len(cells) == 1:
return True
# 检查是否包含流程关键词
flowchart_keywords = [
'阶段', '步骤', '流程', '循环', '模型',
'Diagnosing', 'Action', 'Planning', 'Taking', 'Evaluating',
'诊断', '行动', '规划', '执行', '评估',
'基础设施', '建立', '研究范围'
]
first_cell = cells[0].lower()
return any(keyword.lower() in first_cell for keyword in flowchart_keywords)
def markdown_to_kb_html(md_content):
"""Markdown 转 Knowledge-Base 风格 HTML(v2.2 优化版)"""
front_matter, content = extract_front_matter(md_content)
lines = content.split('\n')
html_parts = []
html_parts.append('<section id="wemd" style="padding: 30px 24px; max-width: 677px; margin: 0 auto; font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', \'Helvetica Neue\', \'PingFang SC\', sans-serif; color: #37352F; word-break: break-word;">')
in_code_block = False
in_table = False
last_line_was_table = False
current_table_is_flowchart = False
for line in lines:
line_stripped = line.strip()
# 代码块
if line_stripped.startswith('```'):
if in_code_block:
html_parts.append('</code></pre>')
in_code_block = False
else:
html_parts.append('<pre style="background: #F7F6F3; padding: 20px; border-radius: 4px; overflow-x: auto;"><code style="color: #EB5757; font-family: \'SFMono-Regular\', Consolas, monospace; font-size: 13px; line-height: 1.6;">')
in_code_block = True
last_line_was_table = False
continue
if in_code_block:
html_parts.append(line_stripped)
continue
# 空行
if not line_stripped:
if in_table:
html_parts.append('</table></section></section>')
in_table = False
last_line_was_table = False
continue
# v2.1 修复:移除水平线 `---` 或 `***`
if line_stripped.startswith('---') or line_stripped.startswith('***') or line_stripped.startswith('___'):
last_line_was_table = False
continue
# 一级标题
if line_stripped.startswith('# '):
title_text = process_markdown_inline(line_stripped[2:])
html_parts.append(f'<h1 style="margin-top: 50px; margin-bottom: 40px; text-align: left; border-bottom: 1px solid #E3E2E0; padding-bottom: 20px;"><span class="content" style="font-size: 28px; font-weight: 700; color: #37352F; display: inline-block; line-height: 1.2;">{title_text}</span></h1>')
last_line_was_table = False
continue
# 二级标题
if line_stripped.startswith('## '):
title_text = process_markdown_inline(line_stripped[3:])
html_parts.append(f'<h2 style="margin-top: 40px; margin-bottom: 20px; text-align: left;"><span class="content" style="display: block; font-size: 22px; font-weight: 600; color: #37352F; padding: 8px 12px; background-color: #F7F6F3; border-radius: 4px; line-height: 1.3;">{title_text}</span></h2>')
last_line_was_table = False
continue
# 三级标题
if line_stripped.startswith('### '):
title_text = process_markdown_inline(line_stripped[4:])
html_parts.append(f'<h3 style="margin-top: 30px; margin-bottom: 12px;"><span class="content" style="font-size: 18px; font-weight: 600; color: #37352F; display: inline-block; border-bottom: 3px solid #FDECC8; padding-bottom: 2px;">{title_text}</span></h3>')
last_line_was_table = False
continue
# v2.1 新增:四级标题支持
if line_stripped.startswith('#### '):
title_text = process_markdown_inline(line_stripped[5:])
html_parts.append(f'<h4 style="margin-top: 24px; margin-bottom: 10px;"><span class="content" style="font-size: 16px; font-weight: 600; color: #37352F; display: inline-block;">{title_text}</span></h4>')
last_line_was_table = False
continue
# 引用块
if line_stripped.startswith('> '):
quote_text = process_markdown_inline(line_stripped[2:])
html_parts.append(f'<div class="multiquote-1" style="margin: 24px 0; padding: 16px 16px 16px 20px; background-color: #F1F1EF; border: none; border-radius: 4px; border-left: 4px solid #37352F; overflow: visible !important;"><p style="margin: 0; color: #37352F; font-size: 15px; line-height: 1.6;">{quote_text}</p></div>')
last_line_was_table = False
continue
# 表格处理(v2.2 优化)
if line_stripped.startswith('|'):
if not in_table:
# 判断是否为流程图表格
test_cells = [c.strip() for c in line_stripped.split('|') if c.strip()]
current_table_is_flowchart = is_flowchart_table(test_cells)
if current_table_is_flowchart:
# 流程图表格样式:居中对齐,统一边框
html_parts.append('<section><section><table style="width: 100%; border-collapse: collapse; margin: 30px 0; font-size: 15px; border: 2px solid #E3E2E0; border-radius: 4px;">')
else:
# 普通表格样式
html_parts.append('<section><section><table style="width: 100%; border-collapse: collapse; margin: 30px 0; font-size: 14px; border: 1px solid #E3E2E0; border-radius: 0;">')
in_table = True
# 跳过分隔线行
if not line_stripped.startswith('|---'):
cells = [c.strip() for c in line_stripped.split('|') if c.strip()]
if cells:
# 判断是否为表头
is_header = any(word in cells[0] for word in ['角色', '指标', '任务', '时间', '使用', '检查', '步骤', '文件', '项目', '状态', '版本', '特征', '内涵', '体现', '阶段', '对比', '类型', '权益', '说明', '适用场景', '首发优惠', '本地接入', '云端', '跨平台', '多任务', 'AI 技能', '专属文件', '新功能', '高峰优先', '月度积分'])
row_tag = 'th' if is_header else 'td'
if current_table_is_flowchart:
# v2.2 流程图表格样式优化
if is_header:
row_style = 'background: #F7F6F3; color: #37352F; font-weight: 600;'
else:
# 流程图内容:文字居中,垂直居中,统一边框
row_style = 'color: #37352F; background: #fff; text-align: center; vertical-align: middle;'
cell_style = row_style + ' border: 2px solid #E3E2E0; padding: 16px 12px;'
else:
# 普通表格样式
if is_header:
row_style = 'background: #F7F6F3; color: #37352F; font-weight: 600;'
else:
row_style = 'color: #37352F; background: #fff;'
cell_style = row_style + ' border: 1px solid #E3E2E0; padding: 10px 12px; text-align: left; vertical-align: top;'
# v2.1 关键修复:表格内容使用 for_table=True,加粗无背景色
cells_html = ''.join(f'<{row_tag} style="{cell_style}">{process_markdown_inline(c, for_table=True)}</{row_tag}>' for c in cells)
row = f'<tr>{cells_html}</tr>'
html_parts.append(row)
last_line_was_table = True
continue
elif in_table and not last_line_was_table:
html_parts.append('</table></section></section>')
in_table = False
current_table_is_flowchart = False
last_line_was_table = False
# 列表
if line_stripped.startswith('- ') or line_stripped.startswith('* '):
list_text = process_markdown_inline(line_stripped[2:])
html_parts.append(f'<ul style="list-style-type: disc; padding-left: 24px; margin: 16px 0; color: #37352F;"><li style="margin-bottom: 8px; line-height: 1.7;"><section style="color: #37352F; font-size: 16px;">{list_text}</section></li></ul>')
last_line_was_table = False
continue
# 普通段落(v2.1 优化)
paragraph = process_markdown_inline(line_stripped)
html_parts.append(f'<section><section><p style="margin-top: 16px; margin-bottom: 16px; line-height: 1.75; letter-spacing: 0.2px; text-align: justify; color: #37352F; font-size: 16px;">{paragraph}</p></section></section>')
last_line_was_table = False
# 关闭未闭合标签
if in_code_block:
html_parts.append('</code></pre>')
if in_table:
html_parts.append('</table></section></section>')
html_parts.append('</section>')
html_content = '\n'.join(html_parts)
return html_content, front_matter
def upload_image(image_path, token=None):
"""上传图片获取 media_id"""
if token is None:
token = get_access_token()
if not os.path.exists(image_path):
return None
url = f'https://api.weixin.qq.com/cgi-bin/material/add_material?access_token={token}&type=image'
with open(image_path, 'rb') as f:
files = {'media': f}
resp = requests.post(url, files=files, timeout=30)
result = json.loads(resp.text, strict=False)
return result.get('media_id')
def fix_html(html_content, upload_images=True):
"""修复 HTML"""
html_content = re.sub(r'<script[^>]*>.*?</script>', '', html_content, flags=re.DOTALL)
html_content = re.sub(r'<iframe[^>]*>.*?</iframe>', '', html_content, flags=re.DOTALL)
html_content = re.sub(r'<style[^>]*>.*?</style>', '', html_content, flags=re.DOTALL)
return html_content
def create_draft(title, content, summary, thumb_media_id, author='黑白'):
"""创建草稿"""
token = get_access_token()
url = f'https://api.weixin.qq.com/cgi-bin/draft/add?access_token={token}'
data = {
'articles': [{
'title': title,
'author': author,
'digest': summary,
'content': content,
'thumb_media_id': thumb_media_id,
'show_cover_pic': 1
}]
}
resp = requests.post(
url,
data=json.dumps(data, ensure_ascii=False).encode('utf-8'),
timeout=30,
headers={'Content-Type': 'application/json; charset=utf-8'}
)
result = resp.json()
if result.get('errcode', 0) == 0:
return result.get('media_id'), result
else:
raise Exception(f"发布失败:{result.get('errmsg', '未知错误')}")
# ==================== 分步命令 ====================
def cmd_md2html(args):
"""Step 1: Markdown 转 HTML"""
print("=" * 60)
print("Step 1: Markdown 转 HTML(Knowledge-Base 主题 v2.2)")
print("=" * 60)
if not os.path.exists(args.input):
print(f"❌ 文件不存在:{args.input}")
sys.exit(1)
with open(args.input, 'r', encoding='utf-8') as f:
md_content = f.read()
html_content, front_matter = markdown_to_kb_html(md_content)
output_file = args.output
if not output_file:
output_dir = args.output_dir or DEFAULT_OUTPUT_DIR
os.makedirs(output_dir, exist_ok=True)
base_name = os.path.splitext(os.path.basename(args.input))[0]
output_file = os.path.join(output_dir, f'{base_name}.html')
with open(output_file, 'w', encoding='utf-8') as f:
f.write(html_content)
print(f"✅ HTML 已生成:{output_file}")
print(f" 内容长度:{len(html_content)}")
print(f" v2.2 优化:流程图表格居中、边框对齐、连接线一致")
if front_matter:
print(f" 元数据:{front_matter}")
return output_file, front_matter
def cmd_fix(args):
"""Step 2: 修复 HTML"""
print("=" * 60)
print("Step 2: 修复 HTML")
print("=" * 60)
if not os.path.exists(args.input):
print(f"❌ 文件不存在:{args.input}")
sys.exit(1)
with open(args.input, 'r', encoding='utf-8') as f:
html_content = f.read()
html_content = fix_html(html_content, upload_images=not args.no_upload)
output_file = args.output or args.input
with open(output_file, 'w', encoding='utf-8') as f:
f.write(html_content)
print(f"✅ HTML 已修复:{output_file}")
print(f" 内容长度:{len(html_content)}")
return output_file
def cmd_cover(args):
"""Step 3: 上传封面图"""
print("=" * 60)
print("Step 3: 上传封面图")
print("=" * 60)
if not args.cover:
print("❌ 请指定封面图路径:--cover <path>")
sys.exit(1)
media_id = upload_image(args.cover)
if media_id:
print(f"✅ 封面图上传成功")
print(f" media_id: {media_id}")
if args.output:
with open(args.output, 'w', encoding='utf-8') as f:
json.dump({'media_id': media_id}, f, ensure_ascii=False)
print(f" 已保存:{args.output}")
return media_id
else:
print("❌ 封面图上传失败")
sys.exit(1)
def cmd_publish(args):
"""Step 4: 发布到草稿箱"""
print("=" * 60)
print("Step 4: 发布到草稿箱")
print("=" * 60)
if not os.path.exists(args.article):
print(f"❌ 文章文件不存在:{args.article}")
sys.exit(1)
with open(args.article, 'r', encoding='utf-8') as f:
content = f.read()
thumb_media_id = None
if args.cover:
if os.path.exists(args.cover):
thumb_media_id = upload_image(args.cover)
else:
thumb_media_id = args.cover
if not thumb_media_id:
print("❌ 请指定封面图:--cover <path>")
sys.exit(1)
title = args.title or os.path.basename(args.article)
author = args.author or '黑白'
summary = args.summary or ''
try:
media_id, result = create_draft(title, content, summary, thumb_media_id, author)
print(f"✅ 发布成功!")
print(f" 草稿 ID: {media_id}")
print(f" 标题:{title}")
print(f" 作者:{author}")
print(f" 请前往公众号后台查看:https://mp.weixin.qq.com")
return media_id
except Exception as e:
print(f"❌ 发布失败:{e}")
sys.exit(1)
def cmd_article(args):
"""一键发布"""
print("=" * 60)
print("一键发布:Markdown → 公众号草稿箱 (v2.2)")
print("=" * 60)
print()
html_file, front_matter = cmd_md2html(args)
print()
args.input = html_file
args.output = html_file
if not hasattr(args, 'no_upload'):
args.no_upload = False
cmd_fix(args)
print()
if args.cover:
cover_file = os.path.join(os.path.dirname(html_file), 'cover.json')
args.output = cover_file
media_id = cmd_cover(args)
print()
else:
media_id = None
print("⚠️ 未指定封面图")
print()
args.article = html_file
args.title = args.title or front_matter.get('title')
args.author = args.author or front_matter.get('author', '黑白')
args.summary = args.summary or front_matter.get('digest', '')
args.cover = media_id or args.cover
cmd_publish(args)
# ==================== 主函数 ====================
def main():
parser = argparse.ArgumentParser(description='微信公众号文章发布工具 - 分步流程版 v2.2')
subparsers = parser.add_subparsers(dest='command', help='子命令')
p_md2html = subparsers.add_parser('md2html', help='Markdown 转 HTML')
p_md2html.add_argument('input', help='输入 Markdown 文件')
p_md2html.add_argument('-o', '--output', help='输出 HTML 文件')
p_md2html.add_argument('--output-dir', default=DEFAULT_OUTPUT_DIR, help='输出目录')
p_md2html.set_defaults(func=cmd_md2html)
p_fix = subparsers.add_parser('fix', help='修复 HTML')
p_fix.add_argument('input', help='输入 HTML 文件')
p_fix.add_argument('-o', '--output', help='输出 HTML 文件')
p_fix.add_argument('--no-upload', action='store_true', help='不上传图片')
p_fix.set_defaults(func=cmd_fix)
p_cover = subparsers.add_parser('cover', help='上传封面图')
p_cover.add_argument('--cover', required=True, help='封面图路径')
p_cover.add_argument('-o', '--output', help='保存 media_id 的文件')
p_cover.set_defaults(func=cmd_cover)
p_publish = subparsers.add_parser('publish', help='发布到草稿箱')
p_publish.add_argument('--article', required=True, help='HTML 文章文件')
p_publish.add_argument('--cover', required=True, help='封面图路径或 media_id')
p_publish.add_argument('--title', help='文章标题')
p_publish.add_argument('--author', default='黑白', help='作者名')
p_publish.add_argument('--summary', default='', help='文章摘要')
p_publish.set_defaults(func=cmd_publish)
p_article = subparsers.add_parser('article', help='一键发布')
p_article.add_argument('input', help='输入 Markdown 文件')
p_article.add_argument('-o', '--output', help='输出 HTML 文件')
p_article.add_argument('--output-dir', default=DEFAULT_OUTPUT_DIR, help='输出目录')
p_article.add_argument('--cover', help='封面图路径')
p_article.add_argument('--title', help='文章标题')
p_article.add_argument('--author', default='黑白', help='作者名')
p_article.add_argument('--summary', default='', help='文章摘要')
p_article.set_defaults(func=cmd_article)
args = parser.parse_args()
if args.command is None:
parser.print_help()
sys.exit(1)
args.func(args)
if __name__ == '__main__':
main()
FILE:wechat_mp_xk_v2.py
#!/usr/bin/env python3
"""
微信公众号文章发布工具 - 分步流程版 v2.0
参考:https://github.com/lyhue1991/wxgzh
支持分步执行:
1. md2html - Markdown 转 HTML(应用 Knowledge-Base 主题)
2. fix - 修复 HTML(处理图片、移除不适合结构)
3. cover - 生成/上传封面图
4. publish - 发布到草稿箱
也可一键执行:article - 完成全部流程
v2.0 更新:
- 修复表格内 Markdown 语法渲染问题
- 优化加粗、斜体、链接等格式处理
- 统一使用 JVS Claw 推广链接
"""
import sys
import requests
import json
import time
import os
import argparse
import tempfile
import re
from datetime import datetime
# 配置
# 尝试从 .env 文件加载(如果存在)
env_file = os.path.join(os.path.dirname(__file__), '.env')
if os.path.exists(env_file):
with open(env_file, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#') and '=' in line:
key, value = line.split('=', 1)
os.environ[key.strip()] = value.strip()
APPID = os.environ.get('WX_APPID')
SECRET = os.environ.get('WX_SECRET')
TOKEN_FILE = '/tmp/wechat_token.json'
DEFAULT_OUTPUT_DIR = './.wxgzh'
# 统一推广链接
JVS_CLAW_PROMO_LINK = "https://www.aliyun.com/activity/ecs/clawdbot?userCode=d8ptsfvb"
# 验证配置
if not APPID or not SECRET:
print("❌ 错误:缺少 WX_APPID 或 WX_SECRET 配置")
print(f" APPID: {APPID}")
print(f" SECRET: {SECRET}")
print(f" .env 文件:{env_file}")
sys.exit(1)
# ==================== 工具函数 ====================
def get_access_token():
"""获取 stable_access_token"""
if os.path.exists(TOKEN_FILE):
try:
with open(TOKEN_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
if time.time() < data.get('expires_at', 0):
return data['access_token']
except:
pass
url = 'https://api.weixin.qq.com/cgi-bin/stable_token'
data = {
'grant_type': 'client_credential',
'appid': APPID,
'secret': SECRET,
'force_refresh': False
}
resp = requests.post(
url,
data=json.dumps(data, ensure_ascii=False).encode('utf-8'),
timeout=10,
headers={'Content-Type': 'application/json; charset=utf-8'}
)
result = resp.json()
if 'access_token' in result:
result['expires_at'] = time.time() + 7000
with open(TOKEN_FILE, 'w', encoding='utf-8') as f:
json.dump(result, f, ensure_ascii=False)
return result['access_token']
else:
raise Exception(f"Token 获取失败:{result}")
def extract_front_matter(md_content):
"""提取 Front Matter 元数据"""
front_matter = {}
content = md_content
if md_content.startswith('---'):
parts = md_content.split('---', 2)
if len(parts) >= 3:
fm_text = parts[1].strip()
content = parts[2].strip()
for line in fm_text.split('\n'):
if ':' in line:
key, value = line.split(':', 1)
front_matter[key.strip()] = value.strip().strip('"\'')
return front_matter, content
def process_markdown_inline(text):
"""
处理行内 Markdown 语法
支持:加粗、斜体、链接、代码
"""
if not text:
return text
# 处理加粗 **text**
text = re.sub(
r'\*\*(.*?)\*\*',
r'<strong style="color: #37352F; font-weight: 600; background-color: #FDECC8; padding: 2px 4px; margin: 0 2px; border-radius: 3px;">\1</strong>',
text
)
# 处理斜体 *text*
text = re.sub(
r'\*(?!\*)(.*?)(?<!\*)\*',
r'<em style="font-style: italic;">\1</em>',
text
)
# 处理行内代码 `code`
text = re.sub(
r'`([^`]+)`',
r'<code style="background: #F7F6F3; padding: 2px 6px; border-radius: 3px; font-family: \'SFMono-Regular\', Consolas, monospace; font-size: 13px; color: #EB5757;">\1</code>',
text
)
# 处理链接 [text](url)
text = re.sub(
r'\[([^\]]+)\]\(([^)]+)\)',
r'<a href="\2" style="color: #1E80FF; text-decoration: none; word-break: break-all;">\1</a>',
text
)
return text
def markdown_to_kb_html(md_content):
"""Markdown 转 Knowledge-Base 风格 HTML(v2.0 优化版)"""
front_matter, content = extract_front_matter(md_content)
lines = content.split('\n')
html_parts = []
html_parts.append('<section id="wemd" style="padding: 30px 24px; max-width: 677px; margin: 0 auto; font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', \'Helvetica Neue\', \'PingFang SC\', sans-serif; color: #37352F; word-break: break-word;">')
in_code_block = False
in_table = False
last_line_was_table = False
for line in lines:
line_stripped = line.strip()
# 代码块
if line_stripped.startswith('```'):
if in_code_block:
html_parts.append('</code></pre>')
in_code_block = False
else:
html_parts.append('<pre style="background: #F7F6F3; padding: 20px; border-radius: 4px; overflow-x: auto;"><code style="color: #EB5757; font-family: \'SFMono-Regular\', Consolas, monospace; font-size: 13px; line-height: 1.6;">')
in_code_block = True
last_line_was_table = False
continue
if in_code_block:
html_parts.append(line_stripped)
continue
# 空行
if not line_stripped:
if in_table:
html_parts.append('</table></section></section>')
in_table = False
last_line_was_table = False
continue
# 一级标题
if line_stripped.startswith('# '):
title_text = process_markdown_inline(line_stripped[2:])
html_parts.append(f'<h1 style="margin-top: 50px; margin-bottom: 40px; text-align: left; border-bottom: 1px solid #E3E2E0; padding-bottom: 20px;"><span class="content" style="font-size: 28px; font-weight: 700; color: #37352F; display: inline-block; line-height: 1.2;">{title_text}</span></h1>')
last_line_was_table = False
continue
# 二级标题
if line_stripped.startswith('## '):
title_text = process_markdown_inline(line_stripped[3:])
html_parts.append(f'<h2 style="margin-top: 40px; margin-bottom: 20px; text-align: left;"><span class="content" style="display: block; font-size: 22px; font-weight: 600; color: #37352F; padding: 8px 12px; background-color: #F7F6F3; border-radius: 4px; line-height: 1.3;">{title_text}</span></h2>')
last_line_was_table = False
continue
# 三级标题
if line_stripped.startswith('### '):
title_text = process_markdown_inline(line_stripped[4:])
html_parts.append(f'<h3 style="margin-top: 30px; margin-bottom: 12px;"><span class="content" style="font-size: 18px; font-weight: 600; color: #37352F; display: inline-block; border-bottom: 3px solid #FDECC8; padding-bottom: 2px;">{title_text}</span></h3>')
last_line_was_table = False
continue
# 引用块
if line_stripped.startswith('> '):
quote_text = process_markdown_inline(line_stripped[2:])
html_parts.append(f'<div class="multiquote-1" style="margin: 24px 0; padding: 16px 16px 16px 20px; background-color: #F1F1EF; border: none; border-radius: 4px; border-left: 4px solid #37352F; overflow: visible !important;"><p style="margin: 0; color: #37352F; font-size: 15px; line-height: 1.6;">{quote_text}</p></div>')
last_line_was_table = False
continue
# 表格处理(v2.0 优化)
if line_stripped.startswith('|'):
if not in_table:
html_parts.append('<section><section><table style="width: 100%; border-collapse: collapse; margin: 30px 0; font-size: 14px; border: 1px solid #E3E2E0; border-radius: 0;">')
in_table = True
# 跳过分隔线行
if not line_stripped.startswith('|---'):
cells = [c.strip() for c in line_stripped.split('|') if c.strip()]
if cells:
# 判断是否为表头
is_header = any(word in cells[0] for word in ['角色', '指标', '任务', '时间', '使用', '检查', '步骤', '文件', '项目', '状态', '版本', '特征', '内涵', '体现', '阶段', '对比', '类型'])
row_tag = 'th' if is_header else 'td'
row_style = 'background: #F7F6F3; color: #37352F; font-weight: 600;' if is_header else 'color: #37352F; background: #fff;'
cell_style = row_style + ' border: 1px solid #E3E2E0; padding: 10px 12px; text-align: left; vertical-align: top;'
# v2.0 关键修复:对单元格内容应用 Markdown 处理
cells_html = ''.join(f'<{row_tag} style="{cell_style}">{process_markdown_inline(c)}</{row_tag}>' for c in cells)
row = f'<tr>{cells_html}</tr>'
html_parts.append(row)
last_line_was_table = True
continue
elif in_table and not last_line_was_table:
html_parts.append('</table></section></section>')
in_table = False
last_line_was_table = False
# 列表
if line_stripped.startswith('- ') or line_stripped.startswith('* '):
list_text = process_markdown_inline(line_stripped[2:])
html_parts.append(f'<ul style="list-style-type: disc; padding-left: 24px; margin: 16px 0; color: #37352F;"><li style="margin-bottom: 8px; line-height: 1.7;"><section style="color: #37352F; font-size: 16px;">{list_text}</section></li></ul>')
last_line_was_table = False
continue
# 普通段落(v2.0 优化:应用 Markdown 处理)
paragraph = process_markdown_inline(line_stripped)
html_parts.append(f'<section><section><p style="margin-top: 16px; margin-bottom: 16px; line-height: 1.75; letter-spacing: 0.2px; text-align: justify; color: #37352F; font-size: 16px;">{paragraph}</p></section></section>')
last_line_was_table = False
# 关闭未闭合标签
if in_code_block:
html_parts.append('</code></pre>')
if in_table:
html_parts.append('</table></section></section>')
# 容器结束
html_parts.append('</section>')
html_content = '\n'.join(html_parts)
return html_content, front_matter
def upload_image(image_path, token=None):
"""上传图片获取 media_id"""
if token is None:
token = get_access_token()
if not os.path.exists(image_path):
return None
url = f'https://api.weixin.qq.com/cgi-bin/material/add_material?access_token={token}&type=image'
with open(image_path, 'rb') as f:
files = {'media': f}
resp = requests.post(url, files=files, timeout=30)
result = json.loads(resp.text, strict=False)
return result.get('media_id')
def fix_html(html_content, upload_images=True):
"""修复 HTML(移除不适合结构,处理图片)"""
# 移除 script 标签
html_content = re.sub(r'<script[^>]*>.*?</script>', '', html_content, flags=re.DOTALL)
# 移除 iframe
html_content = re.sub(r'<iframe[^>]*>.*?</iframe>', '', html_content, flags=re.DOTALL)
# 移除 style 标签
html_content = re.sub(r'<style[^>]*>.*?</style>', '', html_content, flags=re.DOTALL)
return html_content
def create_draft(title, content, summary, thumb_media_id, author='黑白'):
"""创建草稿"""
token = get_access_token()
url = f'https://api.weixin.qq.com/cgi-bin/draft/add?access_token={token}'
data = {
'articles': [{
'title': title,
'author': author,
'digest': summary,
'content': content,
'thumb_media_id': thumb_media_id,
'show_cover_pic': 1
}]
}
resp = requests.post(
url,
data=json.dumps(data, ensure_ascii=False).encode('utf-8'),
timeout=30,
headers={'Content-Type': 'application/json; charset=utf-8'}
)
result = resp.json()
if result.get('errcode', 0) == 0:
return result.get('media_id'), result
else:
raise Exception(f"发布失败:{result.get('errmsg', '未知错误')}")
# ==================== 分步命令 ====================
def cmd_md2html(args):
"""Step 1: Markdown 转 HTML"""
print("=" * 60)
print("Step 1: Markdown 转 HTML(Knowledge-Base 主题 v2.0)")
print("=" * 60)
if not os.path.exists(args.input):
print(f"❌ 文件不存在:{args.input}")
sys.exit(1)
with open(args.input, 'r', encoding='utf-8') as f:
md_content = f.read()
html_content, front_matter = markdown_to_kb_html(md_content)
output_file = args.output
if not output_file:
output_dir = args.output_dir or DEFAULT_OUTPUT_DIR
os.makedirs(output_dir, exist_ok=True)
base_name = os.path.splitext(os.path.basename(args.input))[0]
output_file = os.path.join(output_dir, f'{base_name}.html')
with open(output_file, 'w', encoding='utf-8') as f:
f.write(html_content)
print(f"✅ HTML 已生成:{output_file}")
print(f" 内容长度:{len(html_content)}")
print(f" v2.0 优化:表格 Markdown 语法渲染、加粗/斜体/链接处理")
if front_matter:
print(f" 元数据:{front_matter}")
return output_file, front_matter
def cmd_fix(args):
"""Step 2: 修复 HTML"""
print("=" * 60)
print("Step 2: 修复 HTML")
print("=" * 60)
if not os.path.exists(args.input):
print(f"❌ 文件不存在:{args.input}")
sys.exit(1)
with open(args.input, 'r', encoding='utf-8') as f:
html_content = f.read()
html_content = fix_html(html_content, upload_images=not args.no_upload)
output_file = args.output or args.input
with open(output_file, 'w', encoding='utf-8') as f:
f.write(html_content)
print(f"✅ HTML 已修复:{output_file}")
print(f" 内容长度:{len(html_content)}")
return output_file
def cmd_cover(args):
"""Step 3: 上传封面图"""
print("=" * 60)
print("Step 3: 上传封面图")
print("=" * 60)
if not args.cover:
print("❌ 请指定封面图路径:--cover <path>")
sys.exit(1)
media_id = upload_image(args.cover)
if media_id:
print(f"✅ 封面图上传成功")
print(f" media_id: {media_id}")
if args.output:
with open(args.output, 'w', encoding='utf-8') as f:
json.dump({'media_id': media_id}, f, ensure_ascii=False)
print(f" 已保存:{args.output}")
return media_id
else:
print("❌ 封面图上传失败")
sys.exit(1)
def cmd_publish(args):
"""Step 4: 发布到草稿箱"""
print("=" * 60)
print("Step 4: 发布到草稿箱")
print("=" * 60)
if not os.path.exists(args.article):
print(f"❌ 文章文件不存在:{args.article}")
sys.exit(1)
with open(args.article, 'r', encoding='utf-8') as f:
content = f.read()
thumb_media_id = None
if args.cover:
if os.path.exists(args.cover):
thumb_media_id = upload_image(args.cover)
else:
thumb_media_id = args.cover
if not thumb_media_id:
print("❌ 请指定封面图:--cover <path>")
sys.exit(1)
title = args.title or os.path.basename(args.article)
author = args.author or '黑白'
summary = args.summary or ''
try:
media_id, result = create_draft(title, content, summary, thumb_media_id, author)
print(f"✅ 发布成功!")
print(f" 草稿 ID: {media_id}")
print(f" 标题:{title}")
print(f" 作者:{author}")
print(f" 请前往公众号后台查看:https://mp.weixin.qq.com")
return media_id
except Exception as e:
print(f"❌ 发布失败:{e}")
sys.exit(1)
def cmd_article(args):
"""一键发布:完成全部流程"""
print("=" * 60)
print("一键发布:Markdown → 公众号草稿箱 (v2.0)")
print("=" * 60)
print()
html_file, front_matter = cmd_md2html(args)
print()
args.input = html_file
args.output = html_file
if not hasattr(args, 'no_upload'):
args.no_upload = False
cmd_fix(args)
print()
if args.cover:
cover_file = os.path.join(os.path.dirname(html_file), 'cover.json')
args.output = cover_file
media_id = cmd_cover(args)
print()
else:
media_id = None
print("⚠️ 未指定封面图")
print()
args.article = html_file
args.title = args.title or front_matter.get('title')
args.author = args.author or front_matter.get('author', '黑白')
args.summary = args.summary or front_matter.get('digest', '')
args.cover = media_id or args.cover
cmd_publish(args)
# ==================== 主函数 ====================
def main():
parser = argparse.ArgumentParser(description='微信公众号文章发布工具 - 分步流程版 v2.0')
subparsers = parser.add_subparsers(dest='command', help='子命令')
p_md2html = subparsers.add_parser('md2html', help='Markdown 转 HTML')
p_md2html.add_argument('input', help='输入 Markdown 文件')
p_md2html.add_argument('-o', '--output', help='输出 HTML 文件')
p_md2html.add_argument('--output-dir', default=DEFAULT_OUTPUT_DIR, help='输出目录')
p_md2html.set_defaults(func=cmd_md2html)
p_fix = subparsers.add_parser('fix', help='修复 HTML')
p_fix.add_argument('input', help='输入 HTML 文件')
p_fix.add_argument('-o', '--output', help='输出 HTML 文件')
p_fix.add_argument('--no-upload', action='store_true', help='不上传图片')
p_fix.set_defaults(func=cmd_fix)
p_cover = subparsers.add_parser('cover', help='上传封面图')
p_cover.add_argument('--cover', required=True, help='封面图路径')
p_cover.add_argument('-o', '--output', help='保存 media_id 的文件')
p_cover.set_defaults(func=cmd_cover)
p_publish = subparsers.add_parser('publish', help='发布到草稿箱')
p_publish.add_argument('--article', required=True, help='HTML 文章文件')
p_publish.add_argument('--cover', required=True, help='封面图路径或 media_id')
p_publish.add_argument('--title', help='文章标题')
p_publish.add_argument('--author', default='黑白', help='作者名')
p_publish.add_argument('--summary', default='', help='文章摘要')
p_publish.set_defaults(func=cmd_publish)
p_article = subparsers.add_parser('article', help='一键发布')
p_article.add_argument('input', help='输入 Markdown 文件')
p_article.add_argument('-o', '--output', help='输出 HTML 文件')
p_article.add_argument('--output-dir', default=DEFAULT_OUTPUT_DIR, help='输出目录')
p_article.add_argument('--cover', help='封面图路径')
p_article.add_argument('--title', help='文章标题')
p_article.add_argument('--author', default='黑白', help='作者名')
p_article.add_argument('--summary', default='', help='文章摘要')
p_article.set_defaults(func=cmd_article)
args = parser.parse_args()
if args.command is None:
parser.print_help()
sys.exit(1)
args.func(args)
if __name__ == '__main__':
main()