Files
yidaima_tools/wechat_publisher.py
王鹏 a2f5875d1b init
2026-04-09 14:55:54 +08:00

271 lines
7.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
import json
import time
from pathlib import Path
from typing import Optional
import requests
class WeChatPublisher:
"""微信公众号草稿箱发布器"""
def __init__(self, appid: str, appsecret: str):
self.appid = appid
self.appsecret = appsecret
self.access_token: Optional[str] = None
self.token_expires_at: float = 0
def _get_access_token(self) -> str:
"""获取或刷新 access_token"""
if self.access_token and time.time() < self.token_expires_at - 300:
return self.access_token
url = "https://api.weixin.qq.com/cgi-bin/token"
params = {
"grant_type": "client_credential",
"appid": self.appid,
"secret": self.appsecret
}
response = requests.get(url, params=params, timeout=30)
data = response.json()
if "access_token" not in data:
errcode = data.get("errcode")
errmsg = data.get("errmsg", "")
if errcode == 40164 or "not in whitelist" in errmsg.lower() or "ip" in errmsg.lower():
import re
ip_match = re.search(r'(\d+\.\d+\.\d+\.\d+)', errmsg)
current_ip = ip_match.group(1) if ip_match else "当前IP"
raise RuntimeError(
f"获取 access_token 失败: IP 不在白名单中\n"
f"当前 IP: {current_ip}\n"
f"解决方法: 登录微信公众平台 → 开发 → 基本配置 → IP白名单 → 添加以上IP地址"
)
elif errcode == 40013:
raise RuntimeError(f"获取 access_token 失败: AppID 无效")
elif errcode == 40125:
raise RuntimeError(f"获取 access_token 失败: AppSecret 无效")
else:
raise RuntimeError(f"获取 access_token 失败: {data}")
self.access_token = data["access_token"]
self.token_expires_at = time.time() + data.get("expires_in", 7200)
return self.access_token
def upload_thumb(self, image_path: str) -> str:
"""
上传封面图片获取 thumb_media_id
Args:
image_path: 图片文件路径
Returns:
thumb_media_id
"""
token = self._get_access_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}
response = requests.post(url, files=files, timeout=30)
result = response.json()
if result.get("errcode"):
raise RuntimeError(f"上传封面图片失败: {result}")
media_id = result.get("media_id")
if not media_id:
raise RuntimeError(f"上传封面图片失败,未返回 media_id: {result}")
return media_id
def upload_thumb_from_url(self, image_url: str) -> str:
"""
从URL下载图片并上传获取 thumb_media_id
Args:
image_url: 图片URL
Returns:
thumb_media_id
"""
import tempfile
# 下载图片
response = requests.get(image_url, timeout=30)
response.raise_for_status()
# 保存到临时文件
suffix = Path(image_url).suffix or '.jpg'
with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as f:
f.write(response.content)
temp_path = f.name
try:
return self.upload_thumb(temp_path)
finally:
# 清理临时文件
Path(temp_path).unlink(missing_ok=True)
def add_draft(
self,
title: str,
content: str,
thumb_media_id: str,
author: str = "",
digest: str = "",
content_source_url: str = ""
) -> dict:
"""
添加草稿到公众号草稿箱
Args:
title: 文章标题
content: 文章内容HTML格式
thumb_media_id: 封面图片的 media_id必需
author: 作者
digest: 摘要
content_source_url: 阅读原文链接
Returns:
API 返回的原始数据
"""
token = self._get_access_token()
url = f"https://api.weixin.qq.com/cgi-bin/draft/add?access_token={token}"
# 构建图文消息
article = {
"title": title,
"content": content,
"thumb_media_id": thumb_media_id,
"show_cover_pic": 1,
"need_open_comment": 0,
"only_fans_can_comment": 0
}
if author:
article["author"] = author
if digest:
article["digest"] = digest
if content_source_url:
article["content_source_url"] = content_source_url
payload = {"articles": [article]}
# 调试信息
payload_str = json.dumps(payload, ensure_ascii=False)
print(f"[WeChat] 发送内容长度: {len(payload_str)} 字符")
response = requests.post(
url,
data=payload_str.encode('utf-8'),
headers={"Content-Type": "application/json; charset=utf-8"},
timeout=30
)
result = response.json()
if result.get("errcode", 0) != 0:
raise RuntimeError(f"添加草稿失败: {result}")
return result
def publish_to_wechat(
title: str,
content: str,
appid: str,
appsecret: str,
thumb_image_path: Optional[str] = None,
thumb_image_url: Optional[str] = None
) -> bool:
"""
便捷函数:发布文章到微信公众号草稿箱
Args:
title: 文章标题
content: 文章内容HTML格式
appid: 微信公众号 AppID
appsecret: 微信公众号 AppSecret
thumb_image_path: 封面图片本地路径(可选,与 thumb_image_url 二选一)
thumb_image_url: 封面图片URL可选与 thumb_image_path 二选一)
Returns:
是否成功
"""
try:
publisher = WeChatPublisher(appid, appsecret)
# 获取封面图片的 media_id
if thumb_image_path:
print(f"[WeChat] 正在上传封面图片: {thumb_image_path}")
thumb_media_id = publisher.upload_thumb(thumb_image_path)
elif thumb_image_url:
print(f"[WeChat] 正在下载并上传封面图片: {thumb_image_url}")
thumb_media_id = publisher.upload_thumb_from_url(thumb_image_url)
else:
raise RuntimeError("必须提供封面图片路径或URL")
print(f"[WeChat] 封面图片上传成功media_id: {thumb_media_id}")
# 添加草稿
result = publisher.add_draft(
title=title,
content=content,
thumb_media_id=thumb_media_id
)
print(f"[WeChat] 草稿添加成功media_id: {result.get('media_id')}")
return True
except Exception as e:
print(f"[WeChat] 发布失败: {e}")
return False
def main():
"""测试发布功能"""
from config_loader import get_config
config = get_config()
appid = config.wechat_appid
appsecret = config.wechat_appsecret
if not appid or not appsecret:
print("[ERROR] 未配置微信公众号,请编辑 config.yaml 文件:")
print(" wechat:")
print(" appid: \"your_appid\"")
print(" appsecret: \"your_appsecret\"")
return
# 测试内容
title = "测试文章"
content = "<p>这是一篇测试文章</p>"
# 使用默认封面图片
default_thumb_path = r"c:\Users\南音\Desktop\yidaima\assets\img\bg.jpg"
success = publish_to_wechat(
title=title,
content=content,
appid=appid,
appsecret=appsecret,
thumb_image_path=default_thumb_path
)
if success:
print("[INFO] 发布成功!")
else:
print("[ERROR] 发布失败!")
if __name__ == "__main__":
main()