# 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 = ( "" f"" f"" f"{int(time.time())}" "" f"" "" ) return reply_xml