添加用户消息记录接口;

This commit is contained in:
karlcarlo
2025-12-11 03:53:47 +08:00
parent 6029da058a
commit 420fed4bcf
49 changed files with 7574 additions and 426 deletions

View File

@@ -0,0 +1,114 @@
from fastapi import APIRouter, Depends, Request
from typing import Dict
from app.deps.auth_deps import get_valid_token, get_token_information, get_valid_ticket, get_ticket_information
from app.services.auth_service import generate_signature
router = APIRouter(prefix="/auth", tags=["认证"])
@router.get("/token")
async def get_token(
token: str = Depends(get_valid_token)
) -> Dict:
"""
获取access_token
"""
return {
"success": True,
"access_token": token,
"message": "获取token成功"
}
@router.get("/token/info")
async def get_token_info(
token_info: Dict = Depends(get_token_information)
) -> Dict:
"""
获取token的详细信息
包括获取时间、过期时间、剩余时间等
"""
return {
"success": True,
"data": token_info,
"message": "获取token信息成功"
}
@router.get("/ticket")
async def get_ticket(
ticket: str = Depends(get_valid_ticket)
) -> Dict:
"""
获取jsapi_ticket
"""
return {
"success": True,
"ticket": ticket,
"message": "获取ticket成功"
}
@router.get("/ticket/info")
async def get_ticket_info(
ticket_info: Dict = Depends(get_ticket_information)
) -> Dict:
"""
获取ticket的详细信息
包括获取时间、过期时间、剩余时间等
"""
return {
"success": True,
"data": ticket_info,
"message": "获取ticket信息成功"
}
@router.post("/signature")
async def get_signature(request: Request) -> Dict:
"""
生成应用签名
请求参数:
{
"url": "当前页面完整URL",
"timestamp": 时间戳(可选,秒),
"nonceStr": "随机字符串(可选)"
}
"""
try:
# 解析请求体
body = await request.json()
url = body.get("url")
if not url:
return {
"success": False,
"message": "缺少必要参数:url"
}
timestamp = body.get("timestamp")
nonce_str = body.get("nonceStr")
# 生成签名
signature_data = generate_signature(
url=url,
timestamp=timestamp,
nonce_str=nonce_str
)
return {
"success": True,
"data": {
"signature": signature_data["signature"],
"timestamp": signature_data["timestamp"],
"nonceStr": signature_data["nonceStr"]
},
"message": "签名生成成功"
}
except Exception as e:
return {
"success": False,
"message": f"生成签名失败: {str(e)}"
}

View File

@@ -0,0 +1,206 @@
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from typing import List, Optional
import httpx
import os
# 创建API路由,使用/glm前缀
router = APIRouter(prefix="/glm", tags=["GLM聊天"])
# 数据模型
class Message(BaseModel):
role: str
content: str
class ChatRequest(BaseModel):
message: str
conversation_id: Optional[str] = None
class ChatResponse(BaseModel):
response: str
conversation_id: str
class RoleConfig(BaseModel):
name: str
description: str
skills: List[str]
restrictions: List[str]
# 默认角色配置
DEFAULT_ROLE_CONFIG = RoleConfig(
name="兜兜",
description="你是兜兜,1989年出生,有一个5岁的女儿(2018年出生)。你是一位对用户负责、选品严格的宝妈客服。当用户给你发来疑问时,你总能第一时间回复,并以亲切温暖的风格与用户交流。",
skills=[
"亲切温暖的沟通",
"轻松友好的语气",
"简洁明了的描述",
"亲昵且口语化的用词",
"乱码话术重复问询",
],
restrictions=[
"回复必须基于提供的知识库中的内容",
"每条回复应保持在80字以内,使用口语化语言",
"回复中可以适当加入表情符号,但不要过度使用",
"只讨论与宝妈相关的主题,如育儿、选品、团购等",
"始终以用户的需求和满意度为优先",
],
)
# 全局变量存储角色配置和对话历史
current_role_config = DEFAULT_ROLE_CONFIG
conversations = {}
def build_system_prompt(role_config: RoleConfig) -> str:
"""构建系统提示词"""
prompt = f"""# 角色
{role_config.description}
## 技能
### 技能 1: 亲切温暖的沟通
- **风格**:你的风格非常亲切,像是一个知心朋友在和大家聊天,给人一种温暖的感觉。你经常使用"宝"、"美妈"等亲昵的称呼,拉近了与用户的距离。
- **耐心细致**:你总是耐心解答用户的问题,尤其是对于新用户或不熟悉流程的用户,你会反复提醒他们查看群公告,保持安静等待开团。
- **负责靠谱**:你给人一种非常靠谱的感觉,尤其是在售后问题上,你会主动承担责任,承诺包处理满意,给用户很强的信任感。
### 技能 2: 轻松友好的语气
- **轻松友好**:你的语气非常轻松,像是在和朋友聊天,不会给人压力。比如"不着急哦"、"爱你!"等表达方式,让人感到轻松愉快。
- **安抚性**:当用户有疑问或遇到问题时,你的语气总是充满安抚性,比如"美妈不用担心"、"兜兜来处理哈",给人一种安心感。
- **鼓励性**:你经常用"不着急"、"等开团的时候会有统一说明"等话语,鼓励用户耐心等待,保持积极的心态。
### 技能 3: 简洁明了的描述
- **简洁明了**:你的描述非常简洁,直接回答用户的问题,不会绕弯子。比如"贝亲是咱们深度合作品牌,经常上团的",直接给出了明确的答案。
- **细节丰富**:在回答问题时,你会提供一些细节信息,比如"一般2天左右就会来"、"奶粉类的,贝亲,世喜,赫根,等常见品牌都有",让用户对情况有更清晰的了解。
- **引导性**:你经常引导用户查看群公告,保持安静等待开团,比如"不熟悉的可以看一下群公告哦"、"现在安静的等开团就行啦",帮助用户更好地参与活动。
### 技能 4: 亲昵且口语化的用词
- **亲昵称呼**:你经常使用"宝"、"美妈"、"大家"等亲昵的称呼,拉近与用户的距离,给人一种亲切感。
- **口语化**:你的用词非常口语化,像是日常聊天,比如"不着急哦"、"爱你!"、"兜兜来处理哈",给人一种轻松自然的感觉,称呼自己为"兜兜"或者"我"。
- **积极正面**:你的用词总是积极正面的,比如"不用担心"、"包处理满意"、"很快"、"常见品牌都有",给用户传递了积极的信息。
### 技能 5: 乱码话术重复问询
- **确认问题**:用户说的看不懂的情况来进行再次确认,比如 "45615465" "分隔符规划规范的" "8分隔符公司的"等跟常规问题没关联的话术来重复确认,例如:"宝看不懂你说的是什么啊,能在具体点嘛"
## 限制
- 回复必须基于提供的知识库中的内容。
- 每条回复应保持在80字以内,使用口语化语言。
- 回复中可以适当加入表情符号,但不要过度使用。
- 只讨论与宝妈相关的主题,如育儿、选品、团购等。
- 始终以用户的需求和满意度为优先。
- 在回答问题时,确保信息准确无误,避免误导用户。
- 保持沟通的亲切性和温暖感,但不要过度使用亲昵称呼,以免显得过于随意。
请按照以上角色设定和限制来回答用户的问题。"""
return prompt
def get_deepseek_api_key() -> str:
"""获取DeepSeek API密钥"""
api_key = os.getenv("DEEPSEEK_API_KEY", "your-deepseek-api-key")
return api_key
def is_api_key_configured() -> bool:
"""检查API密钥是否已配置"""
api_key = get_deepseek_api_key()
return api_key != "your-deepseek-api-key"
async def call_deepseek_api(messages: List[dict]) -> str:
"""调用DeepSeek API"""
# 直接使用环境变量获取API密钥
api_key = get_deepseek_api_key()
if not is_api_key_configured():
# 模拟响应用于演示
return "宝,这个问题兜兜需要查一下资料哦,稍等哈~ 😊"
url = "https://api.deepseek.com/v1/chat/completions"
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"}
data = {
"model": "deepseek-chat",
"messages": messages,
"stream": False,
"max_tokens": 500,
}
try:
async with httpx.AsyncClient() as client:
response = await client.post(url, headers=headers, json=data, timeout=30.0)
response.raise_for_status()
result = response.json()
return result["choices"][0]["message"]["content"]
except Exception as e:
raise HTTPException(status_code=500, detail=f"API调用失败: {str(e)}")
@router.get("/")
async def root():
return {"message": "DeepSeek AI Chat Assistant API"}
@router.get("/role")
async def get_role_config():
"""获取当前角色配置"""
return current_role_config
@router.put("/role")
async def update_role_config(role_config: RoleConfig):
"""更新角色配置"""
global current_role_config
current_role_config = role_config
return {"message": "角色配置已更新", "role_config": current_role_config}
@router.post("/chat", response_model=ChatResponse)
async def chat(request: ChatRequest):
"""处理聊天请求"""
# 创建或获取对话历史
if request.conversation_id and request.conversation_id in conversations:
conversation_history = conversations[request.conversation_id]
else:
conversation_history = []
request.conversation_id = f"conv_{len(conversations) + 1}"
conversations[request.conversation_id] = conversation_history
# 构建消息列表
messages = [{"role": "system", "content": build_system_prompt(current_role_config)}]
# 添加历史消息
for msg in conversation_history[-10:]: # 保留最近10条历史
messages.append({"role": msg["role"], "content": msg["content"]})
# 添加当前用户消息
messages.append({"role": "user", "content": request.message})
conversation_history.append({"role": "user", "content": request.message})
# 调用DeepSeek API
try:
response_text = await call_deepseek_api(messages)
# 添加助手回复到历史
conversation_history.append({"role": "assistant", "content": response_text})
return ChatResponse(
response=response_text, conversation_id=request.conversation_id
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/conversations/{conversation_id}")
async def get_conversation(conversation_id: str):
"""获取特定对话的历史"""
if conversation_id not in conversations:
raise HTTPException(status_code=404, detail="对话不存在")
return {
"conversation_id": conversation_id,
"messages": conversations[conversation_id],
}

View File

@@ -0,0 +1,162 @@
#!/usr/bin/env python3
"""
消息API路由
提供加载会话存档、查询消息等功能的API接口
"""
from typing import Optional
from fastapi import APIRouter, HTTPException, Query, Depends
import logging
from app.services.message_service import get_message_service, MessageService
logger = logging.getLogger(__name__)
router = APIRouter(
prefix="/messages",
tags=["messages"],
responses={404: {"description": "Not found"}},
)
@router.post("/load")
async def load_messages(
seq: int = Query(0, description="起始seq值,首次使用0"),
limit: int = Query(100, description="拉取数量限制", ge=1, le=1000),
service: MessageService = Depends(get_message_service),
):
"""
加载会话存档到数据库
完整流程:拉取 → 解密 → 保存
- 从企业微信拉取会话存档
- 自动解密所有加密消息
- 自动保存到数据库(去重处理)
参数说明:
- seq: 起始序列号,首次拉取使用0,后续使用上次返回的max_seq
- limit: 一次拉取的消息数量,最大1000
返回示例:
```json
{
"success": true,
"message": "消息加载完成",
"data": {
"total_fetched": 150,
"encrypted_count": 120,
"decrypted_success": 115,
"decrypted_failed": 5,
"saved_count": 145,
"duplicates_skipped": 5,
"failed_to_save": 0,
"max_seq": 12345,
"next_seq": 12346
},
"execution_time": "0:00:02.345678",
"timestamp": "2025-12-10T16:44:22.123456"
}
```
"""
try:
logger.info(f"API调用: 加载消息, seq={seq}, limit={limit}")
result = await service.load_messages(seq=seq, limit=limit)
if not result.get("success", False):
raise HTTPException(
status_code=500, detail=result.get("message", "加载消息失败")
)
return result
except Exception as e:
logger.error(f"加载消息API调用失败: {e}")
raise HTTPException(status_code=500, detail=f"加载消息失败: {str(e)}")
@router.get("/")
async def get_messages(
page: int = Query(1, description="页码", ge=1),
page_size: int = Query(20, description="每页数量", ge=1, le=100),
msgtype: Optional[str] = Query(None, description="消息类型过滤"),
roomid: Optional[str] = Query(None, description="群聊ID过滤"),
from_user: Optional[str] = Query(None, description="发送者过滤"),
service: MessageService = Depends(get_message_service),
):
"""
查询消息列表
支持分页和过滤条件查询
"""
try:
logger.info(
f"API调用: 查询消息列表, page={page}, page_size={page_size}, "
f"msgtype={msgtype}, roomid={roomid}, from_user={from_user}"
)
result = await service.get_messages(
page=page,
page_size=page_size,
msgtype=msgtype,
roomid=roomid,
from_user=from_user,
)
if not result.get("success", False):
raise HTTPException(
status_code=500, detail=result.get("message", "查询消息列表失败")
)
return result
except Exception as e:
logger.error(f"查询消息列表失败: {e}")
raise HTTPException(status_code=500, detail=f"查询消息列表失败: {str(e)}")
@router.get("/stats")
async def get_message_stats(service: MessageService = Depends(get_message_service)):
"""
获取消息统计信息
返回数据库中消息的统计信息
"""
try:
# 这里可以添加统计逻辑
# 暂时返回空统计,实际实现需要连接数据库统计
return {
"success": True,
"data": {"total_messages": 0, "message_types": {}, "date_distribution": {}},
}
except Exception as e:
logger.error(f"获取消息统计失败: {e}")
raise HTTPException(status_code=500, detail=f"获取消息统计失败: {str(e)}")
@router.get("/{msgid}")
async def get_message_by_id(
msgid: str, service: MessageService = Depends(get_message_service)
):
"""
根据消息ID获取单个消息详情
"""
try:
# 这里可以添加根据msgid查询消息的逻辑
# 暂时返回空,实际实现需要连接数据库查询
return {"success": True, "data": None, "message": "消息查询功能待实现"}
except Exception as e:
logger.error(f"查询消息详情失败 (msgid: {msgid}): {e}")
raise HTTPException(status_code=500, detail=f"查询消息详情失败: {str(e)}")
# 应用关闭时的清理
@router.on_event("shutdown")
async def shutdown_event():
"""应用关闭时清理资源"""
from app.services.message_service import close_message_service
close_message_service()
logger.info("消息服务已关闭")

View File

@@ -0,0 +1,208 @@
#!/usr/bin/env python3
"""
企业微信会话存档API路由
"""
from typing import Optional
from fastapi import APIRouter, HTTPException, Query, Depends, BackgroundTasks
from fastapi.responses import FileResponse, JSONResponse
import logging
from app.services.wework_service import get_wework_service, WeworkService
logger = logging.getLogger(__name__)
router = APIRouter(
prefix="/wework",
tags=["wework"],
responses={404: {"description": "Not found"}},
)
def get_service():
"""依赖注入:获取企业微信服务实例"""
return get_wework_service()
@router.get("/health")
async def health_check(service: WeworkService = Depends(get_service)):
"""健康检查端点"""
return {
"status": "healthy",
"service": "wework",
"corpid": service.corpid,
"media_dir": str(service.media_dir)
}
@router.get("/chat")
async def get_chat(
seq: int = Query(0, description="从指定的seq开始拉取消息,首次使用请使用0"),
limit: int = Query(100, description="一次拉取的消息条数,最大值1000条", ge=1, le=1000),
proxy: Optional[str] = Query(None, description="代理链接,如:socks5://10.0.0.1:8081 或 http://10.0.0.1:8081"),
passwd: Optional[str] = Query(None, description="代理账号密码,如:user_name:passwd_123"),
timeout: int = Query(5, description="超时时间,单位秒", ge=1, le=60),
service: WeworkService = Depends(get_service)
):
"""
拉取会话存档
从企业微信拉取聊天记录,支持代理和分页
"""
try:
result = service.get_chat_data(
seq=seq,
limit=limit,
proxy=proxy or "",
passwd=passwd or "",
timeout=timeout
)
return result
except Exception as e:
logger.error(f"拉取会话存档失败: {e}")
raise HTTPException(status_code=500, detail=f"拉取会话存档失败: {str(e)}")
@router.post("/media")
async def download_media(
fileid: str = Query(..., description="媒体文件id,从解密后的会话存档中得到"),
filename: Optional[str] = Query(None, description="保存的文件名,如果为空则自动生成"),
proxy: Optional[str] = Query(None, description="代理链接"),
passwd: Optional[str] = Query(None, description="代理账号密码"),
timeout: int = Query(5, description="超时时间,单位秒", ge=1, le=60),
background_tasks: BackgroundTasks = None,
service: WeworkService = Depends(get_service)
):
"""
拉取媒体文件
下载企业微信中的媒体文件(图片、语音、视频等),支持大文件分片下载
"""
try:
result = service.get_media_data(
sdk_fileid=fileid,
filename=filename,
proxy=proxy or "",
passwd=passwd or "",
timeout=timeout
)
if result.get("success"):
# 可以添加后台任务,比如发送下载完成通知等
if background_tasks:
background_tasks.add_task(
logger.info,
f"媒体文件下载完成: {result['filename']}, 大小: {result['filesize']} 字节"
)
return result
else:
raise HTTPException(status_code=500, detail=result.get("error", "下载失败"))
except Exception as e:
logger.error(f"拉取媒体文件失败: {e}")
raise HTTPException(status_code=500, detail=f"拉取媒体文件失败: {str(e)}")
@router.get("/media/files")
async def list_media_files(
service: WeworkService = Depends(get_service)
):
"""
获取已下载的媒体文件列表
返回所有已下载的媒体文件信息
"""
try:
files = service.get_media_files()
return {
"count": len(files),
"files": files
}
except Exception as e:
logger.error(f"获取媒体文件列表失败: {e}")
raise HTTPException(status_code=500, detail=f"获取媒体文件列表失败: {str(e)}")
@router.get("/media/download/{filename}")
async def download_file(
filename: str,
service: WeworkService = Depends(get_service)
):
"""
下载媒体文件
通过文件名下载已保存的媒体文件
"""
try:
file_path = service.media_dir / filename
if not file_path.exists():
raise HTTPException(status_code=404, detail="文件不存在")
return FileResponse(
path=file_path,
filename=filename,
media_type='application/octet-stream'
)
except HTTPException:
raise
except Exception as e:
logger.error(f"下载文件失败: {e}")
raise HTTPException(status_code=500, detail=f"下载文件失败: {str(e)}")
@router.delete("/media/cleanup")
async def cleanup_media_files(
days: int = Query(7, description="保留天数,早于此天数的文件将被删除", ge=1, le=365),
service: WeworkService = Depends(get_service)
):
"""
清理旧的媒体文件
删除指定天数之前的媒体文件
"""
try:
deleted_count = service.cleanup_old_files(days)
return {
"message": f"成功删除 {deleted_count} 个旧文件",
"deleted_count": deleted_count,
"days": days
}
except Exception as e:
logger.error(f"清理媒体文件失败: {e}")
raise HTTPException(status_code=500, detail=f"清理媒体文件失败: {str(e)}")
@router.post("/decrypt")
async def decrypt_message(
encrypt_key: str = Query(..., description="解密密钥,使用RSA私钥解密encrypt_random_key后得到"),
encrypt_msg: str = Query(..., description="加密的消息内容"),
service: WeworkService = Depends(get_service)
):
"""
解密会话内容
使用企业自持的RSA私钥解密后的encrypt_key来解密encrypt_chat_msg
"""
try:
result = service.decrypt_data(encrypt_key, encrypt_msg)
return result
except Exception as e:
logger.error(f"解密消息失败: {e}")
raise HTTPException(status_code=500, detail=f"解密消息失败: {str(e)}")
@router.get("/config")
async def get_config(
service: WeworkService = Depends(get_service)
):
"""
获取当前配置
返回企业微信服务的配置信息
"""
return {
"corpid": service.corpid,
"corpid_masked": service.corpid[:8] + "..." + service.corpid[-4:] if len(service.corpid) > 12 else service.corpid,
"media_dir": str(service.media_dir),
"media_dir_exists": service.media_dir.exists()
}
# 应用关闭时的清理
@router.on_event("shutdown")
async def shutdown_event():
"""应用关闭时清理资源"""
from app.services.wework_service import close_wework_service
close_wework_service()
logger.info("企业微信服务已关闭")