pipeline-sdlc/wwwroot/api/wechat_callback.dspy

153 lines
5.9 KiB
Plaintext
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.

# wechat_callback.dspy - 微信回调(验签 + 验证码绑定 + 消息接收 + openid映射 + gateway路由 + 被动回复)
# GET: 服务器配置验证(验签返回 echostr
# POST: 接收消息 → 绑定验证码 或 openid映射 user_id → gateway.run_message → 回复 XML
import json
import hashlib
import re
import time
import xml.etree.ElementTree as ET
signature = (params_kw or {}).get('signature', '')
timestamp = (params_kw or {}).get('timestamp', '')
nonce = (params_kw or {}).get('nonce', '')
echostr = (params_kw or {}).get('echostr', '')
dbname = get_module_dbname('pipeline-sdlc')
SYS_ORG_ID = '0' # 系统级公众号配置
async def _get_channel_config(sor):
"""取系统级公众号配置org_id='0' 单条)。"""
recs = await sor.sqlExe(
"SELECT appid, token, encoding_aes_key FROM wechat_channel_config "
"WHERE org_id=${o}$ AND enabled='1' LIMIT 1", {"o": SYS_ORG_ID})
if recs:
r = recs[0]
return {
'appid': getattr(r, 'appid', ''),
'token': getattr(r, 'token', ''),
'encoding_aes_key': getattr(r, 'encoding_aes_key', ''),
}
return None
async def _resolve_user(sor, openid):
"""openid → user_id未绑定返回空。"""
recs = await sor.sqlExe(
"SELECT user_id FROM wechat_user_binding WHERE openid=${o}$", {"o": openid})
return getattr(recs[0], 'user_id', '') if recs else ''
# ── GET 验签 ──
if echostr:
async with DBPools().sqlorContext(dbname) as sor:
cfg = await _get_channel_config(sor)
if not cfg:
return "未配置微信通道"
tmp = sorted([cfg['token'], timestamp, nonce])
sha1 = hashlib.sha1(''.join(tmp).encode('utf-8')).hexdigest()
if sha1 == signature:
return echostr
return "验签失败"
# ── POST 消息处理 ──
# 微信消息 XML 在 request body。Sage DSPY 环境:优先取 params_kw 里的 xml否则取 request body。
xml_body = (params_kw or {}).get('xml', '')
if not xml_body:
try:
xml_body = await request.text() if hasattr(request, 'text') else ''
except Exception:
xml_body = ''
if not xml_body:
try:
xml_body = (await request.read()).decode('utf-8') if hasattr(request, 'read') else ''
except Exception:
xml_body = ''
if not xml_body:
return ""
try:
root = ET.fromstring(xml_body)
except Exception:
return ""
def _x(node, name):
e = node.find(name)
return e.text if e is not None else ''
to_user = _x(root, 'ToUserName') # 公众号原始ID
from_user = _x(root, 'FromUserName') # openid
msg_type = _x(root, 'MsgType')
content = _x(root, 'Content').strip()
if msg_type != 'text' or not from_user:
reply_text = "暂只支持文本消息"
else:
async with DBPools().sqlorContext(dbname) as sor:
# ── 验证码绑定:用户发 "绑定 123456" 或 "bd 123456" ──
bind_match = re.match(r'^(?:绑定|bd)\s*(\d{6})\s*$', content, re.IGNORECASE)
if bind_match:
code = bind_match.group(1)
recs = await sor.sqlExe(
"SELECT user_id, org_id FROM wechat_bind_code "
"WHERE code=${c}$ AND expires_at > NOW()", {"c": code})
if not recs:
reply_text = "验证码无效或已过期,请在系统中重新生成"
else:
b = recs[0]
bind_uid = getattr(b, 'user_id', '')
bind_org = getattr(b, 'org_id', '')
# openid 是否已被其他账号绑定
ex = await sor.sqlExe(
"SELECT user_id FROM wechat_user_binding WHERE openid=${o}$", {"o": from_user})
if ex and getattr(ex[0], 'user_id', '') != bind_uid:
reply_text = "该微信已绑定其他账号"
else:
await sor.sqlExe(
"DELETE FROM wechat_user_binding WHERE user_id=${u}$", {"u": bind_uid})
await sor.C('wechat_user_binding', {
'id': getID(), 'openid': from_user, 'user_id': bind_uid,
'org_id': bind_org,
})
await sor.sqlExe(
"DELETE FROM wechat_bind_code WHERE code=${c}$", {"c": code})
reply_text = "✅ 绑定成功!现在可以直接发消息给我了"
else:
# ── 正常消息openid → user_id → gateway ──
user_id = await _resolve_user(sor, from_user)
if not user_id:
reply_text = "您还未绑定账号。请先在系统中「微信通道 → 绑定微信」生成验证码,然后回复「绑定 验证码」"
else:
from pipeline_service.gateway import get_gateway
gw = get_gateway()
parts = []
try:
async for chunk in gw.run_message("wechat", user_id, content):
d = json.loads(chunk)
t = d.get('type', '')
if t == 'reply':
parts.append(d.get('message', ''))
elif t == 'ask_user':
parts.append("❓ " + d.get('message', ''))
elif t == 'error':
parts.append("⚠️ " + d.get('message', ''))
# tool_call/tool_result 不推送给微信(避免刷屏)
except Exception as e:
parts.append(f"ERROR: {str(e)[:200]}")
reply_text = "\n".join(parts) if parts else "(无回复)"
# ── 被动回复 XML ──
reply_xml = (
"<xml>"
f"<ToUserName><![CDATA[{from_user}]]></ToUserName>"
f"<FromUserName><![CDATA[{to_user}]]></FromUserName>"
f"<CreateTime>{int(time.time())}</CreateTime>"
"<MsgType><![CDATA[text]]></MsgType>"
f"<Content><![CDATA[{reply_text}]]></Content>"
"</xml>"
)
return reply_xml