微信公众号双风格发布 Skill
功能概述
本 Skill 实现:
1. 两种写作风格:原风格(接地气)+ 卡斯克风格(数字生命卡兹克风格)
2. 两个公众号:西安广源光电(公众号1)+ 西安手机配件报价(公众号2)
3. 交叉发布逻辑:文章风格1 → 公众号2,文章风格2 → 公众号1
4. 素材来源:从 IMA 知识库抓取
5. IP白名单检查:发布前自动检查服务器IP是否已加白名单
使用流程
第一步:确认 IP 白名单
在开始写作前,必须先确认服务器IP已加入公众号后台白名单!
# 检查 IP 白名单(获取 access_token)
def check_ip_whitelist(app_id, app_secret):
url = f"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={app_id}&secret={app_secret}"
r = requests.get(url, timeout=10)
data = r.json()
if "access_token" in data:
print(f"✅ IP白名单通过!")
return data["access_token"]
else:
print(f"❌ IP白名单未通过!错误:{data}")
print(f" 请登录公众号后台,将服务器IP加入白名单")
print(f" 服务器IP:117.36.8.124")
return None
⚠️ 如果用户还未添加IP白名单,停止后续操作,提示用户先添加!
第二步:从 IMA 知识库抓取素材
素材来源:IMA 知识库「工作大脑」或「龙虾大脑」
def fetch_material_from_ima(keyword):
"""
从 IMA 知识库搜索素材
keyword: 搜索关键词(如"手机配件趋势"、"AI工具")
"""
# 使用 IMA skill 搜索
# 具体实现参考 ~/.qclaw/skills/ima/SKILL.md
# 返回相关笔记内容
pass
搜索策略:
1. 优先搜索「工作大脑」知识库(行业洞察)
2. 其次搜索「龙虾大脑」知识库(写作技巧)
3. 提取前3篇最相关的笔记作为素材
第三步:生成两种风格的文章
风格1:原风格(接地气、口语化)
特点:
- 面向手机店老板
- 口语化、接地气
- 强调利润、好卖、质量稳
- 标题格式:「西安鑫源光电为您推荐 XXX」
Prompt 模板:
你是一个手机配件批发行业的老炮儿,给手机店老板们写篇干货。
要求:
1. 语气像隔壁老王聊天,别整那些虚的
2. 突出这款产品怎么帮店老板赚钱(利润高、好卖、质量稳)
3. 用短句,别写长句,手机上看着累
4. 加点感情,比如"我跟你讲"、"说实话"、"你信我"
5. 结尾引导:"微信搜索:西安膜世界 或 西安贴膜送彩票"
素材:
{从IMA抓取的素材}
产品信息:
{产品名称、型号、卖点}
风格2:卡斯克风格(数字生命卡兹克风格)
特点(严格遵守 khazix-writer skill):
- ❌ 禁用:冒号、破折号、感叹号
- ✅ 使用:「」替代引号
- 短句分段
- 蓝色强调色(#1a73e8)
- 标题左侧蓝色边框 + 浅蓝背景
- 不带情绪,客观陈述
Prompt 模板:
你是一个理性、客观的行业观察者,用卡斯克的风格写文章。
严格遵守以下排版规范(来自 khazix-writer skill):
1. 不使用冒号、破折号、感叹号
2. 用「」替代所有引号
3. 短句分段,每段不超过3行
4. 标题用 ## 或 ###,不加粗
5. 加粗用 **文字**,表示强调
HTML排版要求:
- 一级标题:蓝色(#1a73e8) + 左侧4px蓝色边框 + 浅蓝背景(#f8f9fa)
- 二级标题:黑色(#333) + 加粗
- 正文:黑色(#333) + 15px + 行高1.8
- 加粗:蓝色(#1a73e8)强调
素材:
{从IMA抓取的素材}
产品信息:
{产品名称、型号、卖点}
第四步:交叉发布到两个公众号
核心逻辑:
- 风格1文章 → 用公众号2的凭证上传
- 风格2文章 → 用公众号1的凭证上传
# 公众号凭证配置
ACCOUNTS = {
"公众号1": {
"name": "西安广源光电",
"app_id": "wx63d9199fe9053f19",
"app_secret": "0138f21ddc3bbee57370c694c28b6540",
"upload_target": "风格2文章" # 风格2的文章上传到这里
},
"公众号2": {
"name": "西安手机配件报价",
"app_id": "wxdfba84c57701414f",
"app_secret": "cb311d40f1d06e221b343a2f58d904a0",
"upload_target": "风格1文章" # 风格1的文章上传到这里
}
}
发布函数:
def upload_article(token, article, thumb_media_id, account_name):
"""
上传文章到草稿箱
token: access_token
article: 文章数据(title, author, digest, content)
thumb_media_id: 封面图 media_id
account_name: 目标公众号名称(用于日志)
"""
# 构造草稿
draft = {
"articles": [{
"title": article["title"],
"author": article["author"],
"digest": article["digest"],
"content": article["content"],
"content_source_url": "",
"thumb_media_id": thumb_media_id,
"need_open_comment": 0,
"only_fans_can_comment": 0
}]
}
# 🔥 关键:显式指定 UTF-8 编码(避免乱码)
url = f"https://api.weixin.qq.com/cgi-bin/draft/add?access_token={token}"
headers = {'Content-Type': 'application/json; charset=utf-8'}
json_data = json.dumps(draft, ensure_ascii=False).encode('utf-8')
r = requests.post(url, data=json_data, headers=headers, timeout=30)
data = r.json()
if data.get("media_id"):
print(f"✅ 上传成功!目标:{account_name}")
print(f" media_id: {data['media_id']}")
return True
else:
print(f"❌ 上传失败!错误:{data}")
return False
完整工作流代码框架
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
微信公众号双风格发布 Skill
- 两种写作风格(原风格 + 卡斯克风格)
- 两个公众号(西安广源光电 + 西安手机配件报价)
- 交叉发布(风格1→公众号2,风格2→公众号1)
"""
import requests
import json
import re
from PIL import Image, ImageDraw, ImageFont
import io
# ========== 配置区 ==========
ACCOUNTS = {
"公众号1": {
"name": "西安广源光电",
"app_id": "wx63d9199fe9053f19",
"app_secret": "0138f21ddc3bbee57370c694c28b6540",
"upload_target": "风格2" # 风格2的文章上传到这里
},
"公众号2": {
"name": "西安手机配件报价",
"app_id": "wxdfba84c57701414f",
"app_secret": "cb311d40f1d06e221b343a2f58d904a0",
"upload_target": "风格1" # 风格1的文章上传到这里
}
}
SERVER_IP = "117.36.8.124"
# ========== 配置区结束 ==========
def check_ip_whitelist():
"""检查 IP 白名单(两个公众号都检查)"""
print("🔍 检查 IP 白名单...")
for account_name, config in ACCOUNTS.items():
print(f"\n检查 {account_name} ({config['name']})...")
token = get_token(config["app_id"], config["app_secret"])
if not token:
print(f"❌ {account_name} IP白名单未通过!")
print(f" 请登录 https://mp.weixin.qq.com")
print(f" 进入 设置与开发 → 基本配置 → IP白名单")
print(f" 添加IP:{SERVER_IP}")
return False
else:
print(f"✅ {account_name} IP白名单通过!")
return True
def get_token(app_id, app_secret):
"""获取 access_token"""
url = f"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={app_id}&secret={app_secret}"
r = requests.get(url, timeout=10)
data = r.json()
if "access_token" in data:
return data["access_token"]
else:
return None
def fetch_material_from_ima(keyword):
"""从 IMA 知识库抓取素材"""
# TODO: 调用 IMA skill 搜索
# 参考 ~/.qclaw/skills/ima/SKILL.md
print(f"📚 从 IMA 知识库搜索素材:{keyword}")
# 模拟返回素材
return [
"素材1:手机配件行业趋势...",
"素材2:AI工具使用方法...",
"素材3:门店经营技巧..."
]
def generate_article_style1(keyword, materials):
"""生成风格1文章(原风格)"""
print(f"✍️ 生成风格1文章(原风格):{keyword}")
# TODO: 调用 LLM 生成
# Prompt 模板见上文
content = f"这是风格1文章(原风格)的内容...\n\n素材:{materials[0]}"
return content
def generate_article_style2(keyword, materials):
"""生成风格2文章(卡斯克风格)"""
print(f"✍️ 生成风格2文章(卡斯克风格):{keyword}")
# TODO: 调用 LLM 生成
# Prompt 模板见上文
content = f"这是风格2文章(卡斯克风格)的内容...\n\n素材:{materials[0]}"
return content
def md_to_html_style1(md_content):
"""Markdown 转 HTML(风格1:原风格)"""
# 简化版,只用基础标签
lines = md_content.split('\n')
html_parts = []
current_paragraph = []
def flush_paragraph():
if current_paragraph:
text = ' '.join(current_paragraph)
text = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', text)
html_parts.append(f'<p style="margin:10px 0;line-height:1.8;color:#333;font-size:15px;">{text}</p>')
current_paragraph.clear()
for line in lines:
line = line.rstrip()
if not line:
flush_paragraph()
continue
if line.startswith('# '):
flush_paragraph()
html_parts.append(f'<h2 style="color:#1a73e8;font-size:20px;margin:20px 0 10px;">{line[2:]}</h2>')
elif line.startswith('## '):
flush_paragraph()
html_parts.append(f'<h3 style="color:#333;font-size:17px;margin:15px 0 8px;font-weight:bold;">{line[3:]}</h3>')
elif line.startswith('- '):
flush_paragraph()
html_parts.append(f'<p style="margin:5px 0;line-height:1.8;color:#333;font-size:15px;padding-left:20px;">• {line[2:]}</p>')
else:
current_paragraph.append(line)
flush_paragraph()
html = '\n'.join(html_parts)
html = f'<section style="padding:15px;background:#fff;">\n{html}\n</section>'
return html
def md_to_html_style2(md_content):
"""Markdown 转 HTML(风格2:卡斯克风格)"""
# 卡斯克排版:标题左侧蓝色边框 + 背景色
lines = md_content.split('\n')
html_parts = []
current_paragraph = []
def flush_paragraph():
if current_paragraph:
text = ' '.join(current_paragraph)
text = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', text)
html_parts.append(f'<p style="margin:10px 0;line-height:1.8;color:#333;font-size:15px;">{text}</p>')
current_paragraph.clear()
for line in lines:
line = line.rstrip()
if not line:
flush_paragraph()
continue
if line.startswith('# '):
flush_paragraph()
title = line[2:]
html_parts.append(f'<h2 style="color:#1a73e8;font-size:20px;margin:20px 0 10px;padding-left:12px;border-left:4px solid #1a73e8;background:#f8f9fa;padding:8px 0 8px 12px;">{title}</h2>')
elif line.startswith('## '):
flush_paragraph()
title = line[3:]
html_parts.append(f'<h3 style="color:#333;font-size:17px;margin:15px 0 8px;font-weight:bold;">{title}</h3>')
elif line.startswith('- '):
flush_paragraph()
html_parts.append(f'<p style="margin:5px 0;line-height:1.8;color:#333;font-size:15px;padding-left:20px;">• {line[2:]}</p>')
else:
current_paragraph.append(line)
flush_paragraph()
html = '\n'.join(html_parts)
html = f'<section style="padding:15px;background:#fff;font-family:-apple-system,BlinkMacSystemFont,\'Segoe UI\',Roboto,sans-serif;font-size:15px;line-height:1.8;color:#333;">\n{html}\n</section>'
return html
def create_cover(title, output_path):
"""生成封面图"""
width, height = 900, 500
img = Image.new('RGB', (width, height), color='#1a73e8')
draw = ImageDraw.Draw(img)
try:
font_title = ImageFont.truetype("/System/Library/Fonts/STHeiti Medium.ttc", 48)
except:
font_title = ImageFont.load_default()
display_title = title[:10]
bbox = draw.textbbox((0, 0), text=display_title, font=font_title)
text_width = bbox[2] - bbox[0]
x = (width - text_width) // 2
y = height // 2 - 50
draw.text((x, y), text=display_title, fill='white', font=font_title)
img.save(output_path, 'JPEG', quality=95)
return output_path
def upload_cover(image_path, token):
"""上传封面图"""
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}
r = requests.post(url, files=files, timeout=30)
data = r.json()
if data.get("media_id"):
return data["media_id"]
else:
return None
def upload_article(token, article, thumb_media_id):
"""上传文章到草稿箱"""
draft = {
"articles": [{
"title": article["title"],
"author": article["author"],
"digest": article["digest"],
"content": article["content"],
"content_source_url": "",
"thumb_media_id": thumb_media_id,
"need_open_comment": 0,
"only_fans_can_comment": 0
}]
}
# 🔥 关键:显式指定 UTF-8 编码
url = f"https://api.weixin.qq.com/cgi-bin/draft/add?access_token={token}"
headers = {'Content-Type': 'application/json; charset=utf-8'}
json_data = json.dumps(draft, ensure_ascii=False).encode('utf-8')
r = requests.post(url, data=json_data, headers=headers, timeout=30)
data = r.json()
if data.get("media_id"):
return True, data["media_id"]
else:
return False, data
def main():
"""主流程"""
print("🚀 微信公众号双风格发布 Skill 启动...")
# 第一步:检查 IP 白名单
if not check_ip_whitelist():
print("\n❌ IP白名单未通过,请先添加IP后再运行!")
return
# 第二步:从 IMA 抓取素材
keyword = "手机配件 AI工具" # TODO: 让用户输入
materials = fetch_material_from_ima(keyword)
# 第三步:生成两种风格的文章
article1_content = generate_article_style1(keyword, materials)
article2_content = generate_article_style2(keyword, materials)
# 第四步:转换 HTML
article1_html = md_to_html_style1(article1_content)
article2_html = md_to_html_style2(article2_content)
# 第五步:生成封面图
cover1_path = "/tmp/cover_style1.jpg"
cover2_path = "/tmp/cover_style2.jpg"
create_cover("风格1标题", cover1_path)
create_cover("风格2标题", cover2_path)
# 第六步:上传封面图 + 上传文章(交叉发布)
# 风格1文章 → 公众号2
token2 = get_token(ACCOUNTS["公众号2"]["app_id"], ACCOUNTS["公众号2"]["app_secret"])
thumb_media_id1 = upload_cover(cover1_path, token2)
if thumb_media_id1:
article1 = {
"title": "风格1标题",
"author": "M",
"digest": "风格1摘要",
"content": article1_html
}
success, media_id = upload_article(token2, article1, thumb_media_id1)
if success:
print(f"✅ 风格1文章已上传到 {ACCOUNTS['公众号2']['name']}!")
# 风格2文章 → 公众号1
token1 = get_token(ACCOUNTS["公众号1"]["app_id"], ACCOUNTS["公众号1"]["app_secret"])
thumb_media_id2 = upload_cover(cover2_path, token1)
if thumb_media_id2:
article2 = {
"title": "风格2标题",
"author": "M",
"digest": "风格2摘要",
"content": article2_html
}
success, media_id = upload_article(token1, article2, thumb_media_id2)
if success:
print(f"✅ 风格2文章已上传到 {ACCOUNTS['公众号1']['name']}!")
print("\n🎊 全部完成!请到公众号后台查看草稿箱。")
if __name__ == "__main__":
main()
使用说明
安装
- 将本 Skill 放到
~/.agents/skills/wechat-dual-style-publisher/ - 确保已安装依赖:
requests,Pillow - 确保已安装 IMA skill(用于抓取素材)
调用方式
# 其他智能体调用本 Skill
from wechat_dual_style_publisher import main
# 运行主流程
main()
注意事项
- IP白名单必须先检查,否则无法上传
- 标题长度:≤8字节(2-3个中文字符),上传后手动改长标题
- 作者字段:只能用1-2个英文字符(如 "M"),不能用中文
- 编码问题:必须显式指定
charset=utf-8,否则中文乱码 - 素材来源:IMA 知识库,搜索「工作大脑」和「龙虾大脑」
常见问题
Q1:为什么风格1的文章上传到公众号2?
A:这是用户指定的交叉发布逻辑,目的是让两个公众号的内容风格差异化。
Q2:如何修改公众号凭证?
A:修改脚本开头的 ACCOUNTS 配置区。
Q3:如何添加更多写作风格?
A:在 generate_article_* 函数中添加新的风格生成逻辑。
Q4:IP白名单检查失败怎么办?
A:登录公众号后台,进入「设置与开发 → 基本配置 → IP白名单」,添加服务器IP 117.36.8.124。
版本历史
- v1.0 (2026-06-23): 初始版本,支持双风格交叉发布
- 风格1:原风格(接地气)
- 风格2:卡斯克风格
- 公众号1:西安广源光电
- 公众号2:西安手机配件报价
- 素材来源:IMA 知识库
作者:微信公众号文章写作智能体
创建时间:2026-06-23
最后更新:2026-06-23