""" WeChat Work (企业微信) Integration Service 功能: 1. 接收企业微信群聊消息事件 2. AI 判断消息是否为有价值的信号 3. 自动创建信号并关联到项目 4. 推送通知给相关角色 """ import asyncio import json import hashlib import hmac import base64 import struct from datetime import datetime, timezone from typing import Any, Dict, List, Optional import httpx from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.future import select from sqlalchemy import text from ...models import ( Signal, Project, Integration ) from ..signal_capture_service import ProactiveSignalCaptureService class WeChatWorkIntegrationService: """企业微信集成服务""" def __init__(self, db: AsyncSession): self.db = db # 从数据库获取企业微信配置 self.corp_id = None self.agent_id = None self.app_secret = None self.token = None self.encoding_aes_key = None self.tenant_id = 1 async def load_config(self, tenant_id: int = 1): """从数据库加载企业微信配置""" self.tenant_id = tenant_id result = await self.db.execute( select(Integration).where( Integration.tenant_id == tenant_id, Integration.type == 'wecom', Integration.is_active.is_(True), ) ) integration = result.scalar_one_or_none() if integration: if isinstance(integration.config, str): config = json.loads(integration.config) if integration.config else {} else: config = integration.config or {} self.corp_id = config.get('corp_id') self.agent_id = config.get('agent_id') self.app_secret = config.get('app_secret') self.token = config.get('token') self.encoding_aes_key = config.get('encoding_aes_key') else: self.corp_id = None self.agent_id = None self.app_secret = None self.token = None self.encoding_aes_key = None def is_configured(self) -> bool: """检查是否已配置""" return all([ self.corp_id, self.agent_id, self.app_secret, self.token, self.encoding_aes_key ]) async def save_config( self, corp_id: str, agent_id: str, app_secret: str, token: str, encoding_aes_key: str, tenant_id: int = 1 ): """保存企业微信配置到数据库""" config = { 'corp_id': corp_id, 'agent_id': agent_id, 'app_secret': app_secret, 'token': token, 'encoding_aes_key': encoding_aes_key } # 查找或创建集成记录 result = await self.db.execute( select(Integration).where( Integration.tenant_id == tenant_id, Integration.type == 'wecom' ) ) integration = result.scalar_one_or_none() if integration: integration.config = config integration.is_active = True else: integration = Integration( type='wecom', config=config, is_active=True, tenant_id=tenant_id ) self.db.add(integration) await self.db.commit() # 更新内存配置 self.corp_id = corp_id self.agent_id = agent_id self.app_secret = app_secret self.token = token self.encoding_aes_key = encoding_aes_key self.tenant_id = tenant_id def verify_url(self, msg_signature: str, timestamp: str, nonce: str, echostr: str) -> str: """ 验证企业微信事件订阅 URL 企业微信会发送一个 GET 请求来验证 URL 有效性 """ if not self.is_configured(): raise ValueError("企业微信未配置") # 计算签名 sign_str = f"{self.token}{timestamp}{nonce}" signature = hashlib.sha256(sign_str.encode()).hexdigest() if signature != msg_signature: raise ValueError("签名验证失败") # 解密 echostr decrypted = self._decrypt(echostr) return decrypted def verify_callback(self, msg_signature: str, timestamp: str, nonce: str, data: str) -> bool: """ 验证企业微信回调签名 """ if not self.token: return False # 计算签名 sign_str = f"{self.token}{timestamp}{nonce}{data}" computed_signature = hashlib.sha256(sign_str.encode()).hexdigest() return computed_signature == msg_signature async def handle_message_event( self, event_data: Dict[str, Any] ) -> Dict[str, Any]: """ 处理企业微信消息事件 支持的事件类型: - add_half_user: 添加企业成员 - update_user: 更新成员信息 - send_msg_success: 消息发送成功 """ try: event_type = event_data.get('Event', '') # 企业微信群聊消息通过其他方式处理,这里主要是事件处理 return { 'code': 0, 'msg': f'Event {event_type} received' } except Exception as e: print(f"[WeCom] Handle event error: {e}") return { 'code': 1, 'msg': str(e) } def _decrypt(self, encoded_text: str) -> str: """ 解密企业微信加密消息 AES 加密的消息格式: base64编码的16字节随机字符串 + 4字节消息长度 + 消息内容 """ try: # Base64 解码 cipher_text = base64.b64decode(encoded_text) # AES 密钥处理 aes_key = base64.b64decode(self.encoding_aes_key + "=") # 解密 from Crypto.Cipher import AES cipher = AES.new(aes_key, AES.MODE_CBC, aes_key[:16]) decrypted = cipher.decrypt(cipher_text) # 去除补位字符 pad = decrypted[-1] if isinstance(pad, str): pad = ord(pad) decrypted = decrypted[:-pad] # 提取消息内容(前16字节是随机字符串,接着4字节是消息长度) content = decrypted[16:] msg_len = struct.unpack(">I", content[:4])[0] message = content[4:4 + msg_len].decode('utf-8') return message except Exception as e: print(f"[WeCom] Decrypt error: {e}") raise ValueError("消息解密失败") async def send_message_to_group( self, chat_id: str, message: str ) -> Dict[str, Any]: """ 发送消息到企业微信群聊 用于推送通知、简报等 """ if not self.is_configured(): raise ValueError("企业微信未配置") # 获取 access_token access_token = await self._get_access_token() # 发送消息 API url = "https://qyapi.weixin.qq.com/cgi-bin/message/send" params = {"access_token": access_token} message_data = { "touser": "", "msgtype": "text", "agentid": int(self.agent_id), "text": { "content": message }, "safe": 0 } # 如果是群聊,使用 chatid if chat_id: message_data["chatid"] = chat_id message_data["msgtype"] = "text" async with httpx.AsyncClient(timeout=20.0, trust_env=False) as client: resp = await client.post(url, params=params, json=message_data) resp.raise_for_status() return resp.json() async def _get_access_token(self) -> str: """获取企业微信 access_token""" if not self.corp_id or not self.app_secret: raise ValueError("企业微信未配置") url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken" params = { "corpid": self.corp_id, "corpsecret": self.app_secret } async with httpx.AsyncClient(timeout=20.0, trust_env=False) as client: resp = await client.get(url, params=params) resp.raise_for_status() result = resp.json() if result.get('errcode') == 0: return result.get('access_token') raise Exception(f"获取 token 失败: {result}") # 全局服务实例 def get_wecom_service(db: AsyncSession) -> WeChatWorkIntegrationService: """获取企业微信服务实例""" return WeChatWorkIntegrationService(db)