import json
import logging
import asyncio
from typing import Optional, Dict, Any
from fastapi import FastAPI, Request, Response, HTTPException
from pydantic import BaseModel, Field
import httpx
from pydantic_settings import BaseSettings
from openai import OpenAI
import re
import xml.etree.ElementTree as ET
# ==================== 配置类 ====================
class Settings(BaseSettings):
version: str = "1.0"
app_name: str = "拖车调度客服系统"
host: str = "0.0.0.0"
port: int = 8081
wechat_appid: str = "wx_V4GKlfb0V4NLpW5HHKR5U"
wechat_device_id: str = "wx_wR_U4zPj2M_OTS3BCyoE4"
target_group: str = "52692331298@chatroom"
vllm_api_url: str = "http://localhost:8000/v1"
vllm_model: str = "Qwen"
ai_timeout: int = 30
forward_api_url: str = "http://api.geweapi.com/gewe/v2/api/message/postText"
forward_api_token: str = "019abe72-4122-48e3-9a7c-ba47a560da5d"
forward_timeout: int = 10
class Config:
env_file = ".env"
settings = Settings()
# ==================== 数据模型 ====================
class WeChatMessage(BaseModel):
TypeName: str
Appid: str
Wxid: str
Data: Dict[str, Any]
class ApifoxModel(BaseModel):
token: str = Field(..., alias="X-GEWE-TOKEN", serialization_alias="X-GEWE-TOKEN")
app_id: str = Field(..., alias="appId")
ats: Optional[str] = Field(None)
content: str = Field(...)
to_wxid: str = Field(..., alias="toWxid")
class Config:
populate_by_name = True
json_encoders = {str: lambda v: v.replace("\u2005", " ") if v else v}
# ==================== FastAPI应用 ====================
app = FastAPI(
title=settings.app_name,
description="智能拖车调度服务接口",
version=settings.version
)
# ==================== 日志配置 ====================
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[logging.StreamHandler(), logging.FileHandler('wechat_service.log')]
)
logger = logging.getLogger("wechat.service")
# ==================== 服务类 ====================
class WeChatService:
@staticmethod
def parse_message(raw_data: bytes) -> WeChatMessage:
try:
data = json.loads(raw_data.decode('utf-8'))
return WeChatMessage(**data)
except Exception as e:
logger.error(f"消息解析失败: {str(e)}")
raise HTTPException(400, detail="Invalid message format")
@staticmethod
def extract_content(msg: WeChatMessage) -> Dict[str, Any]:
content = msg.Data["Content"]["string"]
speaker_id, _, actual_content = content.partition(":")
# 新增:解析引用消息
quoted_msg = None
if msg.Data.get("MsgType") == 49: # 引用消息类型
quoted_msg = WeChatService.parse_quoted_message(msg.Data)
return {
"is_group": "@chatroom" in msg.Data["FromUserName"]["string"],
"group_id": msg.Data["FromUserName"]["string"].strip(),
"speaker_id": speaker_id.strip(),
"speaker_nickname": msg.Data.get("PushContent", "").split(":")[0].strip(),
"content": actual_content.strip(),
"msg_type": msg.Data["MsgType"],
"msg_id": msg.Data.get("MsgId", ""),
"original_msg": msg.Data,
"quoted_msg": quoted_msg # 新增:引用消息内容
}
@staticmethod
def parse_quoted_message(msg_data: Dict) -> Optional[Dict]:
"""解析引用消息内容"""
try:
content = msg_data.get("Content", {}).get("string", "")
xml_match = re.search(r'<msg>.*?</msg>', content, re.DOTALL)
if not xml_match:
return None
xml_content = xml_match.group(0)
root = ET.fromstring(xml_content)
refer_msg = root.find('.//refermsg')
if refer_msg is None:
return None
return {
"msg_id": refer_msg.find('svrid').text if refer_msg.find('svrid') is not None else "",
"sender": refer_msg.find('fromusr').text if refer_msg.find('fromusr') is not None else "",
"content": refer_msg.find('content').text if refer_msg.find('content') is not None else "",
"display_name": refer_msg.find('displayname').text if refer_msg.find('displayname') is not None else ""
}
except Exception as e:
logger.error(f"解析引用消息失败: {str(e)}")
return None
@staticmethod
def parse_location_message(content: str) -> Optional[str]:
try:
xml_match = re.search(r'<msg>.*?</msg>', content, re.DOTALL)
if not xml_match:
return None
xml_content = xml_match.group(0)
root = ET.fromstring(xml_content)
location = root.find('location')
if location is None:
return None
label = location.get('label', '未知位置')
x = location.get('x', '0.0')
y = location.get('y', '0.0')
return f"位置:{label},经纬度:{x},{y}"
except Exception as e:
logger.error(f"解析位置消息失败: {str(e)}")
return None
class AIService:
@staticmethod
async def generate_reply(prompt: str, quoted_content: Optional[str] = None) -> str:
try:
# 如果有引用消息,将其加入prompt
if quoted_content:
prompt = f"""你正在回复一条引用消息,请根据上下文进行回复。
被引用的原消息内容: "{quoted_content}"
当前需要回复的内容: "{prompt}"
请直接回复用户的问题,保持专业和礼貌:"""
client = OpenAI(base_url=settings.vllm_api_url, api_key="EMPTY")
response = client.completions.create(
model=settings.vllm_model,
prompt=prompt,
max_tokens=100, # 增加token限制以便更好处理引用消息
temperature=0.7,
top_p=0.9,
stop=["\n\n", "。", "!", "?"],
)
return response.choices[0].text.strip()
except Exception as e:
logger.error(f"AI生成失败: {str(e)}")
return "收到您的请求,客服将尽快处理"
class MessageSender:
@staticmethod
async def send(content: str, to_wxid: str, at_wxid: Optional[str] = None,
original_msg: Optional[Dict] = None, quoted_msg: Optional[Dict] = None) -> bool:
try:
clean_content = content.split('\n')[0].strip()
ats = at_wxid if at_wxid else None
# 优先使用传入的引用消息,如果没有则使用original_msg中的引用
quoted_msg = quoted_msg or (original_msg.get("quoted_msg") if original_msg else None)
if quoted_msg:
# 构建引用回复消息
xml_template = """<?xml version='1.0'?>
<msg>
<appmsg appid="" sdkver="0">
<title>{title}</title>
<des></des>
<action>view</action>
<type>57</type>
<showtype>0</showtype>
<content></content>
<url></url>
<lowurl></lowurl>
<appattach>
<totallen>0</totallen>
<attachid></attachid>
<fileext></fileext>
</appattach>
<extinfo></extinfo>
<sourceusername></sourceusername>
<sourcedisplayname></sourcedisplayname>
<commenturl></commenturl>
<thumburl></thumburl>
<mediatagname></mediatagname>
<messageaction><![CDATA[]]></messageaction>
<messagemedia></messagemedia>
<messageattr><![CDATA[]]></messageattr>
<refermsg>
<type>1</type>
<svrid>{msg_id}</svrid>
<fromusr>{from_user}</fromusr>
<chatusr>{chat_user}</chatusr>
<displayname>{display_name}</displayname>
<content>{quoted_content}</content>
<msgsource><![CDATA[]]></msgsource>
</refermsg>
<appname>{app_name}</appname>
</appmsg>
<fromusername>{bot_wxid}</fromusername>
<scene>0</scene>
<appinfo>
<version>1</version>
<appname></appname>
</appinfo>
<commenturl></commenturl>
</msg>"""
xml_content = xml_template.format(
title=clean_content[:60],
msg_id=quoted_msg.get("msg_id", ""),
from_user=quoted_msg.get("sender", ""),
chat_user=to_wxid,
quoted_content=quoted_msg.get("content", ""),
display_name=quoted_msg.get("display_name", "未知用户"),
app_name=settings.app_name,
bot_wxid=settings.wechat_device_id
)
forward_data = {
"appId": settings.wechat_appid,
"toWxid": to_wxid,
"xml": xml_content
}
async with httpx.AsyncClient(timeout=settings.forward_timeout) as client:
response = await client.post(
"http://api.geweapi.com/gewe/v2/api/message/forwardUrl",
json=forward_data,
headers={
"X-GEWE-TOKEN": settings.forward_api_token,
"Content-Type": "application/json"
}
)
return response.json().get("ret", -1) == 0
elif original_msg:
# 普通引用回复
msg_id = original_msg.get("MsgId", "")
from_user = original_msg.get("FromUserName", {}).get("string", "")
original_content = original_msg.get("Content", {}).get("string", "")
if ':' in original_content:
quoted_sender, quoted_content = original_content.split(':', 1)
quoted_content = quoted_content.strip()
else:
quoted_sender = from_user
quoted_content = original_content
xml_template = """<?xml version='1.0'?>
<msg>
<appmsg appid="" sdkver="0">
<title>{title}</title>
<des></des>
<action>view</action>
<type>57</type>
<showtype>0</showtype>
<content></content>
<url></url>
<lowurl></lowurl>
<appattach>
<totallen>0</totallen>
<attachid></attachid>
<fileext></fileext>
</appattach>
<extinfo></extinfo>
<sourceusername></sourceusername>
<sourcedisplayname></sourcedisplayname>
<commenturl></commenturl>
<thumburl></thumburl>
<mediatagname></mediatagname>
<messageaction><![CDATA[]]></messageaction>
<messagemedia></messagemedia>
<messageattr><![CDATA[]]></messageattr>
<refermsg>
<type>1</type>
<svrid>{msg_id}</svrid>
<fromusr>{from_user}</fromusr>
<chatusr>{chat_user}</chatusr>
<displayname>{display_name}</displayname>
<content>{quoted_content}</content>
<msgsource><![CDATA[]]></msgsource>
</refermsg>
<appname>{app_name}</appname>
</appmsg>
<fromusername>{bot_wxid}</fromusername>
<scene>0</scene>
<appinfo>
<version>1</version>
<appname></appname>
</appinfo>
<commenturl></commenturl>
</msg>"""
xml_content = xml_template.format(
title=clean_content[:60],
msg_id=msg_id,
from_user=from_user,
chat_user=to_wxid,
quoted_content=quoted_content,
display_name=original_msg.get("PushContent", "").split(":")[0].strip(),
app_name=settings.app_name,
bot_wxid=settings.wechat_device_id
)
forward_data = {
"appId": settings.wechat_appid,
"toWxid": to_wxid,
"xml": xml_content
}
async with httpx.AsyncClient(timeout=settings.forward_timeout) as client:
response = await client.post(
"http://api.geweapi.com/gewe/v2/api/message/forwardUrl",
json=forward_data,
headers={
"X-GEWE-TOKEN": settings.forward_api_token,
"Content-Type": "application/json"
}
)
return response.json().get("ret", -1) == 0
else:
# 普通消息发送
message = ApifoxModel(
token=settings.forward_api_token,
app_id=settings.wechat_appid,
content=clean_content,
to_wxid=to_wxid,
ats=ats
)
async with httpx.AsyncClient(timeout=settings.forward_timeout) as client:
response = await client.post(
settings.forward_api_url,
json=message.model_dump(by_alias=True, exclude_none=True),
headers={
"X-GEWE-TOKEN": settings.forward_api_token,
"Content-Type": "application/json"
}
)
return response.json().get("ret", -1) == 0
except Exception as e:
logger.error(f"发送失败: {str(e)}")
return False
# ==================== 路由处理 ====================
@app.post("/process")
async def process_message(request: Request):
try:
raw_data = await request.body()
logger.info(f"接收到的原始字节: {raw_data}")
wechat_msg = WeChatService.parse_message(raw_data)
msg_info = WeChatService.extract_content(wechat_msg)
logger.info(f"接收到的消息内容: {msg_info}")
# 支持处理文本消息(1)、位置消息(48)和引用消息(49)
if not (msg_info["is_group"] and
msg_info["group_id"] == settings.target_group and
msg_info["msg_type"] in [1, 48, 49]):
return Response(content="success")
# 处理位置消息
if msg_info["msg_type"] == 48:
location_label = WeChatService.parse_location_message(msg_info["content"])
if location_label:
query_content = f"位置:{location_label}"
logger.info(f"解析到的位置信息: {query_content}")
else:
return Response(content="success")
else:
query_content = msg_info["content"].strip()
logger.info(f"提取到的文本内容: {query_content}")
# 生成AI回复,如果有引用消息则带上引用内容
quoted_content = msg_info.get("quoted_msg", {}).get("content") if msg_info.get("quoted_msg") else None
if query_content == "在吗":
reply = "您好,请问有什么可以帮您?"
elif "拖车" in query_content:
reply = "好的,请您发一下电话和定位,我马上为您安排服务。"
else:
prompt = f"""你是一个专业的拖车调度客服AI助手,请按照以下原则与客户互动:
1. 当收到不明确信息时,礼貌询问具体要求
2. 对于非工作相关问题,礼貌回复需要查询后答复
3. 当用户明确需要服务时,立即询问定位和电话信息
4. 收到部分信息时,询问缺少的信息
5. 收到完整信息后,确认正在创建订单
当前问题:"{query_content}"。请直接回答用户问题。"""
reply = await AIService.generate_reply(prompt, quoted_content)
logger.info(f"生成的回复内容: {reply}")
await MessageSender.send(
content=reply,
to_wxid=msg_info["group_id"],
at_wxid=msg_info["speaker_id"],
original_msg=msg_info["original_msg"],
quoted_msg=msg_info.get("quoted_msg")
)
return Response(content="success")
except Exception as e:
logger.error(f"处理异常: {str(e)}")
return Response(content="success", status_code=500)
# ==================== 启动服务 ====================
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host=settings.host, port=settings.port) 引用接收到的信息内容显示不存在 引用引用中的消息内容就显示存在 该如何解决给出完整代码