feat: 新增 wechat_oa/woa 微信办公号接口
This commit is contained in:
parent
3ebbdafbd7
commit
70aa57691d
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal 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
160
wechat_oa/woa.py
Normal 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'])
|
||||
Loading…
x
Reference in New Issue
Block a user