feat: 新增 wechat_oa/woa 微信办公号接口

This commit is contained in:
yumoqing 2026-11-03 21:43:44 +08:00
parent 3ebbdafbd7
commit 70aa57691d
2 changed files with 177 additions and 0 deletions

17
.gitignore vendored Normal file
View File

@ -0,0 +1,17 @@
# build/test artifacts
.DS_Store
__pycache__/
*.pyc
*.pyo
build/
*.egg-info/
*.dist-info/
*.whl
logs/
*.log
.idea/
~$*
models/mysql.ddl.sql
*.pid
init/
wechat_oa/private.py

160
wechat_oa/woa.py Normal file
View File

@ -0,0 +1,160 @@
import os
import time
import logging
from aiohttp import web
from wechatpy.crypto import WeChatCrypto
from wechatpy.exceptions import InvalidSignatureException
from wechatpy import parse_message
from wechatpy.replies import TextReply
from anthropic import AsyncAnthropic
# --- 配置区域 ---
CONFIG = {
"WOA_TOKEN": os.environ['WOA_TOKEN'], # 微信公众号后台设置的 Token
"WOA_APPID": os.environ['WOA_APPID'], # kaiyunayun 公众号 AppID
"WOA_AESKEY": os.environ['WOA_AESKEY'] # 如果开启加密模式则填写,否则留空
}
# 初始化日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# 初始化客户端
claude_client = AsyncAnthropic(api_key=CONFIG["ANTHROPIC_API_KEY"])
crypto = None
if CONFIG["WECHAT_ENCODING_AES_KEY"]:
crypto = WeChatCrypto(CONFIG["WECHAT_TOKEN"], CONFIG["WECHAT_ENCODING_AES_KEY"], CONFIG["WECHAT_APP_ID"])
# --- 内存数据库 (生产环境请替换为 Redis) ---
# 结构: { openid: [ {"role": "user", "content": "..."}, {"role": "assistant", "content": "..."} ] }
user_sessions = {}
def get_system_prompt():
return "你是一个运行在微信公众号里的智能助手,回答要简洁、友好,适合手机端阅读。"
async def get_claude_reply(openid, user_content):
"""调用 Claude API 获取回复"""
# 1. 初始化或获取用户会话历史
if openid not in user_sessions:
user_sessions[openid] = []
history = user_sessions[openid]
# 2. 添加当前用户消息
history.append({"role": "user", "content": user_content})
# 3. 限制历史记录长度 (防止 token 超限)
if len(history) > CONFIG["MAX_HISTORY"] * 2:
history = history[-CONFIG["MAX_HISTORY"] * 2:]
user_sessions[openid] = history
try:
# 4. 调用 Claude
response = await claude_client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
system=get_system_prompt(),
messages=history
)
ai_reply = response.content[0].text
# 5. 保存 AI 回复到历史
history.append({"role": "assistant", "content": ai_reply})
return ai_reply
except Exception as e:
logger.error(f"Claude API Error: {e}")
return "抱歉,大脑暂时短路了,请稍后再试。"
# --- HTTP 路由处理 ---
async def handle_wechat(request):
"""处理微信回调 (GET 验证 & POST 消息)"""
query = request.query
# 1. GET 请求:微信服务器验证
if request.method == "GET":
signature = query.get("signature", "")
timestamp = query.get("timestamp", "")
nonce = query.get("nonce", "")
echostr = query.get("echostr", "")
try:
# 如果开启了加密模式,使用 crypto.check_signature,否则手动验证
if crypto:
crypto.check_signature(signature, timestamp, nonce)
else:
# 手动验证逻辑 (简单版)
import hashlib
tmp_list = sorted([CONFIG["WECHAT_TOKEN"], timestamp, nonce])
tmp_str = "".join(tmp_list)
hash_str = hashlib.sha1(tmp_str.encode('utf-8')).hexdigest()
if hash_str != signature:
raise InvalidSignatureException()
return web.Response(text=echostr)
except InvalidSignatureException:
logger.warning("Invalid signature from WeChat")
return web.Response(text="Invalid signature", status=403)
# 2. POST 请求:接收用户消息
elif request.method == "POST":
body = await request.read()
# 如果开启加密,先解密
if crypto:
msg_signature = query.get("msg_signature", "")
timestamp = query.get("timestamp", "")
nonce = query.get("nonce", "")
try:
body = crypto.decrypt_message(body, msg_signature, timestamp, nonce)
except Exception as e:
logger.error(f"Decrypt error: {e}")
return web.Response(text="error")
# 解析消息
try:
msg = parse_message(body)
except Exception as e:
logger.error(f"Parse message error: {e}")
return web.Response(text="success") # 即使解析失败也返回 success 避免微信重试风暴
# 只处理文本消息
if msg.type == 'text':
openid = msg.source
user_content = msg.content
logger.info(f"User {openid}: {user_content}")
# 异步调用 Claude
reply_text = await get_claude_reply(openid, user_content)
# 构建回复
reply = TextReply(content=reply_text, message=msg)
response_xml = reply.render()
# 如果开启加密,需要加密返回
if crypto:
response_xml = crypto.encrypt_message(response_xml, nonce, timestamp)
return web.Response(text=response_xml, content_type='application/xml')
# 非文本消息或事件推送,直接返回 success 避免重试
return web.Response(text="success")
return web.Response(status=405)
# --- 启动服务 ---
async def init_app():
app = web.Application()
app.router.add_route('*', '/wechat', handle_wechat) # 路径需与公众号后台配置一致
return app
if __name__ == '__main__':
app = init_app()
logger.info(f"Starting WeChat Bot on port {CONFIG['PORT']}...")
web.run_app(app, host='0.0.0.0', port=CONFIG['PORT'])