更新后台管理消息列表;
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -26,6 +26,7 @@ dist
|
||||
dist-ssr
|
||||
*.local
|
||||
*.tar
|
||||
*.pid
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
285640
|
||||
@@ -1,3 +1,6 @@
|
||||
import os
|
||||
import logging
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from pymongo import AsyncMongoClient
|
||||
@@ -6,10 +9,16 @@ from app.models import models
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
import uvicorn
|
||||
import os
|
||||
from app.routes import auth_routes, wework_routes, glm_routes, message_routes
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
@@ -24,27 +33,8 @@ async def lifespan(app: FastAPI):
|
||||
) # 批量注册模型
|
||||
print(f"🗄️ 数据库连接成功: {database_name}")
|
||||
|
||||
# 初始化GLM服务,确保默认角色存在
|
||||
try:
|
||||
from app.services.glm_service import get_glm_service
|
||||
|
||||
glm_service = get_glm_service()
|
||||
await glm_service.ensure_default_role()
|
||||
print(f"✅ GLM角色配置初始化完成")
|
||||
except Exception as e:
|
||||
print(f"⚠️ GLM角色配置初始化失败: {e}")
|
||||
|
||||
yield # 应用在此处进入正常运行状态,接收请求
|
||||
|
||||
# 关闭GLM服务
|
||||
try:
|
||||
from app.services.glm_service import close_glm_service
|
||||
|
||||
close_glm_service()
|
||||
print(f"GLM服务已关闭")
|
||||
except Exception as e:
|
||||
print(f"关闭GLM服务时出错: {e}")
|
||||
|
||||
# 关闭数据库连接
|
||||
await client.close()
|
||||
print(f"数据库连接关闭")
|
||||
@@ -85,50 +75,4 @@ async def root():
|
||||
"service": "企业微信认证与会话存档服务",
|
||||
"version": "1.0.0",
|
||||
"description": "获取和管理企业微信access_token,会话存档功能,以及DeepSeek AI聊天服务",
|
||||
"endpoints": {
|
||||
"docs": "/api/docs",
|
||||
"redoc": "/api/redoc",
|
||||
"auth": {"token": "/api/auth/token", "token_info": "/api/auth/token/info"},
|
||||
"wework": {
|
||||
"health": "/api/wework/health",
|
||||
"chat": "/api/wework/chat",
|
||||
"media": "/api/wework/media",
|
||||
"decrypt": "/api/wework/decrypt",
|
||||
"config": "/api/wework/config",
|
||||
},
|
||||
"glm": {
|
||||
"root": "/api/glm/",
|
||||
"role": "/api/glm/role",
|
||||
"chat": "/api/glm/chat",
|
||||
"conversations": "/api/glm/conversations/{conversation_id}",
|
||||
},
|
||||
"messages": {
|
||||
"load": "/api/messages/load",
|
||||
"list": "/api/messages/",
|
||||
"stats": "/api/messages/stats",
|
||||
"detail": "/api/messages/{msgid}",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
主函数,启动FastAPI服务器
|
||||
"""
|
||||
# 从环境变量获取主机和端口,使用默认值
|
||||
host = os.getenv("APP_HOST", "0.0.0.0")
|
||||
port = int(os.getenv("APP_PORT", "8000"))
|
||||
|
||||
print("启动企业微信认证与会话存档服务...")
|
||||
print(f"API文档地址: http://{host}:{port}/docs")
|
||||
print(f"API根地址: http://{host}:{port}")
|
||||
print("企业微信会话存档API: /api/wework/*")
|
||||
print("DeepSeek AI聊天API: /api/glm/*")
|
||||
print("消息管理API: /api/messages/*")
|
||||
|
||||
uvicorn.run("app.main:app", host=host, port=port, reload=True, log_level="info")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
from typing import List, Optional, Dict, Any
|
||||
import httpx
|
||||
import os
|
||||
import logging
|
||||
@@ -23,6 +23,10 @@ class Message(BaseModel):
|
||||
class ChatRequest(BaseModel):
|
||||
message: str
|
||||
conversation_id: Optional[str] = None
|
||||
enhanced_prompt: Optional[str] = None # 前端生成的完整提示词
|
||||
customer_tags: Optional[List[Dict[str, Any]]] = None # 客户标签
|
||||
message_history: Optional[List[Dict[str, Any]]] = None # 企业微信历史消息
|
||||
workflow_context: Optional[Dict[str, Any]] = None # 工作流上下文
|
||||
|
||||
|
||||
class ChatResponse(BaseModel):
|
||||
@@ -194,44 +198,233 @@ async def update_role_config(
|
||||
@router.post("/chat", response_model=ChatResponse)
|
||||
async def chat(request: ChatRequest, service: GLMService = Depends(get_glm_service)):
|
||||
"""处理聊天请求"""
|
||||
# 创建或获取对话历史
|
||||
# 步骤1: 记录请求开始
|
||||
logger.info(
|
||||
f"收到聊天请求: conversation_id={request.conversation_id or 'None'}, "
|
||||
f"message_length={len(request.message)}, "
|
||||
f"message_preview='{request.message[:50]}{'...' if len(request.message) > 50 else ''}'"
|
||||
)
|
||||
|
||||
# 记录增强参数信息
|
||||
has_enhanced_prompt = request.enhanced_prompt is not None
|
||||
has_customer_tags = (
|
||||
request.customer_tags is not None and len(request.customer_tags) > 0
|
||||
)
|
||||
has_message_history = (
|
||||
request.message_history is not None and len(request.message_history) > 0
|
||||
)
|
||||
has_workflow_context = request.workflow_context is not None
|
||||
|
||||
logger.info(
|
||||
f"增强参数状态: enhanced_prompt={has_enhanced_prompt}, "
|
||||
f"customer_tags={has_customer_tags} ({len(request.customer_tags) if has_customer_tags else 0}个标签), "
|
||||
f"message_history={has_message_history} ({len(request.message_history) if has_message_history else 0}条历史), "
|
||||
f"workflow_context={has_workflow_context}"
|
||||
)
|
||||
|
||||
# 记录客户标签摘要
|
||||
if has_customer_tags:
|
||||
tag_categories = {}
|
||||
for tag in request.customer_tags:
|
||||
category = tag.get("TagCategory", "未分类")
|
||||
name = tag.get("TagName", "未命名")
|
||||
if category not in tag_categories:
|
||||
tag_categories[category] = []
|
||||
tag_categories[category].append(name)
|
||||
|
||||
tag_summary = []
|
||||
for category, names in tag_categories.items():
|
||||
tag_summary.append(
|
||||
f"{category}: {', '.join(names[:3])}{'...' if len(names) > 3 else ''}"
|
||||
)
|
||||
|
||||
# 检查是否使用mock数据
|
||||
is_mock_data = False
|
||||
for tag in request.customer_tags:
|
||||
if tag.get("TagName", "").startswith("mock_") or tag.get(
|
||||
"TagCategory", ""
|
||||
).startswith("mock_"):
|
||||
is_mock_data = True
|
||||
break
|
||||
|
||||
mock_note = " [mock数据]" if is_mock_data else ""
|
||||
logger.info(f"客户标签摘要{mock_note}: {', '.join(tag_summary)}")
|
||||
|
||||
# 记录历史消息摘要
|
||||
if has_message_history:
|
||||
# 按时间排序,获取最近的消息
|
||||
sorted_history = sorted(
|
||||
request.message_history, key=lambda x: x.get("msgtime", ""), reverse=True
|
||||
)[
|
||||
:5
|
||||
] # 只显示最近5条
|
||||
|
||||
history_summary = []
|
||||
for i, msg in enumerate(sorted_history):
|
||||
from_user = msg.get("from_user", "未知用户")
|
||||
user_id = (
|
||||
msg.get("user_id")
|
||||
or msg.get("userId")
|
||||
or msg.get("from_user_id")
|
||||
or "无ID"
|
||||
)
|
||||
content = msg.get("text", {}).get("content", "") or msg.get("content", "")
|
||||
content_preview = content[:30] + ("..." if len(content) > 30 else "")
|
||||
history_summary.append(f"{from_user}(ID:{user_id}): '{content_preview}'")
|
||||
|
||||
# 检查是否使用mock数据
|
||||
is_mock_data = False
|
||||
for msg in sorted_history:
|
||||
if msg.get("from_user", "").startswith("mock_") or msg.get(
|
||||
"user_id", ""
|
||||
).startswith("mock_"):
|
||||
is_mock_data = True
|
||||
break
|
||||
|
||||
mock_note = " [mock数据]" if is_mock_data else ""
|
||||
logger.info(f"历史消息摘要{mock_note}: {', '.join(history_summary)}")
|
||||
|
||||
# 步骤2: 创建或获取对话历史
|
||||
if request.conversation_id and request.conversation_id in conversations:
|
||||
conversation_history = conversations[request.conversation_id]
|
||||
logger.info(
|
||||
f"使用现有对话: {request.conversation_id}, 历史消息数: {len(conversation_history)}"
|
||||
)
|
||||
# 记录历史消息摘要
|
||||
if conversation_history:
|
||||
last_messages = conversation_history[-3:] # 显示最近3条消息
|
||||
history_summary = []
|
||||
for i, msg in enumerate(last_messages, 1):
|
||||
role = msg["role"]
|
||||
content_preview = msg["content"][:30] + (
|
||||
"..." if len(msg["content"]) > 30 else ""
|
||||
)
|
||||
history_summary.append(f"{role}: '{content_preview}'")
|
||||
logger.debug(f"最近历史消息摘要: {', '.join(history_summary)}")
|
||||
else:
|
||||
conversation_history = []
|
||||
request.conversation_id = f"conv_{len(conversations) + 1}"
|
||||
conversations[request.conversation_id] = conversation_history
|
||||
logger.info(f"新建对话: {request.conversation_id}")
|
||||
|
||||
# 获取当前角色配置
|
||||
# 步骤3: 构建系统提示词
|
||||
try:
|
||||
if has_enhanced_prompt:
|
||||
# 使用前端生成的增强提示词
|
||||
system_prompt = request.enhanced_prompt
|
||||
logger.info(f"使用前端增强提示词, 长度: {len(system_prompt)} 字符")
|
||||
logger.debug(
|
||||
f"增强提示词预览: '{system_prompt[:200]}{'...' if len(system_prompt) > 200 else ''}'"
|
||||
)
|
||||
else:
|
||||
# 获取当前角色配置并构建系统提示词
|
||||
role = await service.get_current_role()
|
||||
system_prompt = build_system_prompt(role.to_role_config_dict())
|
||||
except Exception as e:
|
||||
logger.error(f"获取角色配置失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"获取角色配置失败: {str(e)}")
|
||||
role_config = role.to_role_config_dict()
|
||||
logger.info(
|
||||
f"获取角色配置: name='{role.name}', "
|
||||
f"技能数: {len(role_config['skills'].split('\\n')) if role_config['skills'] else 0}, "
|
||||
f"限制数: {len(role_config['restrictions'].split('\\n')) if role_config['restrictions'] else 0}"
|
||||
)
|
||||
|
||||
# 构建消息列表
|
||||
system_prompt = build_system_prompt(role_config)
|
||||
logger.debug(f"系统提示词构建完成, 长度: {len(system_prompt)} 字符")
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"构建系统提示词失败: {e}, conversation_id={request.conversation_id}"
|
||||
)
|
||||
raise HTTPException(status_code=500, detail=f"构建系统提示词失败: {str(e)}")
|
||||
|
||||
# 步骤4: 构建消息列表
|
||||
messages = [{"role": "system", "content": system_prompt}]
|
||||
|
||||
# 添加历史消息
|
||||
for msg in conversation_history[-10:]: # 保留最近10条历史
|
||||
# 初始化企业微信历史消息变量
|
||||
selected_wework_messages = []
|
||||
|
||||
# 添加企业微信历史消息(如果提供)
|
||||
if has_message_history:
|
||||
logger.info(f"添加企业微信历史消息到上下文: {len(request.message_history)} 条")
|
||||
|
||||
# 将企业微信消息转换为大模型消息格式
|
||||
wework_messages = []
|
||||
for msg in request.message_history:
|
||||
from_user = msg.get("from_user", "未知用户")
|
||||
content = msg.get("text", {}).get("content", "") or msg.get("content", "")
|
||||
msgtime = msg.get("msgtime", "")
|
||||
|
||||
if content:
|
||||
# 确定角色:如果是客户发送的,则为user;否则为assistant
|
||||
# 这里需要根据实际业务逻辑判断,暂时假设外部用户是user
|
||||
role = (
|
||||
"user"
|
||||
if "customer" in from_user.lower() or "user" in from_user.lower()
|
||||
else "assistant"
|
||||
)
|
||||
|
||||
# 格式化消息内容
|
||||
formatted_content = f"[{msgtime}] {from_user}: {content}"
|
||||
wework_messages.append({"role": role, "content": formatted_content})
|
||||
|
||||
# 添加到消息列表(限制数量,避免过长)
|
||||
max_wework_messages = 10
|
||||
selected_wework_messages = wework_messages[
|
||||
-max_wework_messages:
|
||||
] # 取最近的消息
|
||||
messages.extend(selected_wework_messages)
|
||||
logger.info(
|
||||
f"已添加 {len(selected_wework_messages)} 条企业微信历史消息到上下文"
|
||||
)
|
||||
|
||||
# 添加与大模型的对话历史
|
||||
history_messages = conversation_history[-10:] # 保留最近10条历史
|
||||
for msg in history_messages:
|
||||
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
|
||||
logger.info(
|
||||
f"构建消息列表完成: 系统消息1条, 企业微信历史{len(selected_wework_messages)}条, "
|
||||
f"对话历史{len(history_messages)}条, 用户消息1条, 总消息数: {len(messages)}"
|
||||
)
|
||||
|
||||
# 记录消息列表摘要
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
for i, msg in enumerate(messages):
|
||||
role = msg["role"]
|
||||
content_preview = msg["content"][:50] + (
|
||||
"..." if len(msg["content"]) > 50 else ""
|
||||
)
|
||||
logger.debug(f"消息{i+1}: {role} - '{content_preview}'")
|
||||
|
||||
# 步骤5: 调用DeepSeek API
|
||||
try:
|
||||
logger.info(
|
||||
f"调用DeepSeek API, conversation_id={request.conversation_id}, 总消息数: {len(messages)}"
|
||||
)
|
||||
response_text = await call_deepseek_api(messages)
|
||||
|
||||
# 步骤6: 记录API响应
|
||||
logger.info(
|
||||
f"DeepSeek API调用成功, conversation_id={request.conversation_id}, "
|
||||
f"响应长度: {len(response_text)} 字符, "
|
||||
f"响应预览: '{response_text[:100]}{'...' if len(response_text) > 100 else ''}'"
|
||||
)
|
||||
|
||||
# 添加助手回复到历史
|
||||
conversation_history.append({"role": "assistant", "content": response_text})
|
||||
|
||||
# 步骤7: 返回响应
|
||||
logger.info(
|
||||
f"返回聊天响应: conversation_id={request.conversation_id}, 响应长度: {len(response_text)} 字符"
|
||||
)
|
||||
return ChatResponse(
|
||||
response=response_text, conversation_id=request.conversation_id
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"DeepSeek API调用失败: {e}, conversation_id={request.conversation_id}, message_preview='{request.message[:50]}...'"
|
||||
)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
|
||||
@@ -135,6 +135,40 @@ async def get_message_stats(service: MessageService = Depends(get_message_servic
|
||||
raise HTTPException(status_code=500, detail=f"获取消息统计失败: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/config")
|
||||
async def get_message_config():
|
||||
"""
|
||||
获取消息服务配置信息
|
||||
|
||||
返回企业微信SDK配置、文件路径等基本信息
|
||||
"""
|
||||
try:
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# 从环境变量获取配置
|
||||
corpid = os.getenv("CORPID", "")
|
||||
secret = os.getenv("WEWORK_SECRET", "")
|
||||
|
||||
# 构建文件路径
|
||||
backend_dir = Path(__file__).parent.parent.parent
|
||||
lib_path = backend_dir / "lib" / "sdk" / "libWeWorkFinanceSdk_C.so"
|
||||
data_dir = backend_dir / "data"
|
||||
|
||||
config = {
|
||||
"corpid": corpid,
|
||||
# "secret": secret[:10] + "..." if secret else "", # 只显示前10个字符
|
||||
"libPath": str(lib_path),
|
||||
"dataDir": str(data_dir),
|
||||
}
|
||||
|
||||
return {"success": True, "message": "配置获取成功", "data": config}
|
||||
|
||||
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)
|
||||
@@ -150,13 +184,3 @@ async def get_message_by_id(
|
||||
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("消息服务已关闭")
|
||||
|
||||
@@ -3,8 +3,13 @@ GLM聊天服务,包含角色配置管理
|
||||
"""
|
||||
|
||||
import logging
|
||||
import asyncio
|
||||
import os
|
||||
from typing import Optional
|
||||
from pymongo import AsyncMongoClient
|
||||
from beanie import init_beanie
|
||||
from app.models.glm_role import GLMRole
|
||||
from app.models import models
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -14,11 +19,19 @@ class GLMService:
|
||||
|
||||
def __init__(self):
|
||||
self._current_role: Optional[GLMRole] = None
|
||||
self._max_retries = 3
|
||||
self._retry_delay = 1 # 秒
|
||||
self._db_client: Optional[AsyncMongoClient] = None
|
||||
self._db_initialized = False
|
||||
|
||||
async def get_current_role(self) -> GLMRole:
|
||||
"""获取当前激活的角色配置"""
|
||||
if self._current_role is None:
|
||||
self._current_role = await self._load_or_create_role()
|
||||
logger.info("当前无缓存角色,开始加载角色配置")
|
||||
self._current_role = await self._load_or_create_role_with_retry()
|
||||
logger.info(f"角色加载完成: name='{self._current_role.name}'")
|
||||
else:
|
||||
logger.debug(f"使用缓存角色: name='{self._current_role.name}'")
|
||||
return self._current_role
|
||||
|
||||
async def update_role(self, role_data: dict) -> GLMRole:
|
||||
@@ -30,29 +43,111 @@ class GLMService:
|
||||
Returns:
|
||||
更新后的GLMRole实例
|
||||
"""
|
||||
logger.info(f"开始更新角色配置,更新字段: {list(role_data.keys())}")
|
||||
role = await self.get_current_role()
|
||||
|
||||
# 更新字段
|
||||
if "name" in role_data:
|
||||
# 记录更新前的值
|
||||
changes = []
|
||||
if "name" in role_data and role_data["name"] != role.name:
|
||||
changes.append(f"name: '{role.name}' -> '{role_data['name']}'")
|
||||
role.name = role_data["name"]
|
||||
if "description" in role_data:
|
||||
|
||||
if "description" in role_data and role_data["description"] != role.description:
|
||||
changes.append(
|
||||
f"description: {len(role.description)}字符 -> {len(role_data['description'])}字符"
|
||||
)
|
||||
role.description = role_data["description"]
|
||||
if "skills" in role_data:
|
||||
|
||||
if "skills" in role_data and role_data["skills"] != role.skills:
|
||||
old_skills_count = len(role.skills.split("\n")) if role.skills else 0
|
||||
new_skills_count = (
|
||||
len(role_data["skills"].split("\n")) if role_data["skills"] else 0
|
||||
)
|
||||
changes.append(f"skills: {old_skills_count}条 -> {new_skills_count}条")
|
||||
role.skills = role_data["skills"]
|
||||
if "restrictions" in role_data:
|
||||
|
||||
if (
|
||||
"restrictions" in role_data
|
||||
and role_data["restrictions"] != role.restrictions
|
||||
):
|
||||
old_restrictions_count = (
|
||||
len(role.restrictions.split("\n")) if role.restrictions else 0
|
||||
)
|
||||
new_restrictions_count = (
|
||||
len(role_data["restrictions"].split("\n"))
|
||||
if role_data["restrictions"]
|
||||
else 0
|
||||
)
|
||||
changes.append(
|
||||
f"restrictions: {old_restrictions_count}条 -> {new_restrictions_count}条"
|
||||
)
|
||||
role.restrictions = role_data["restrictions"]
|
||||
|
||||
role.update_timestamp()
|
||||
await role.save()
|
||||
|
||||
if changes:
|
||||
logger.info(f"角色配置变更: {', '.join(changes)}")
|
||||
else:
|
||||
logger.info("角色配置无实际变更")
|
||||
|
||||
# 使用重试机制保存
|
||||
await self._save_with_retry(role)
|
||||
|
||||
# 更新缓存
|
||||
self._current_role = role
|
||||
|
||||
logger.info(f"角色配置已更新: {role.name}")
|
||||
logger.info(f"角色配置更新完成: name='{role.name}'")
|
||||
return role
|
||||
|
||||
async def _load_or_create_role_with_retry(self) -> GLMRole:
|
||||
"""使用重试机制加载或创建默认角色"""
|
||||
for attempt in range(self._max_retries):
|
||||
try:
|
||||
return await self._load_or_create_role()
|
||||
except Exception as e:
|
||||
if attempt < self._max_retries - 1:
|
||||
logger.warning(f"加载角色配置失败,第{attempt + 1}次重试: {e}")
|
||||
await asyncio.sleep(self._retry_delay * (attempt + 1))
|
||||
else:
|
||||
logger.error(f"加载角色配置失败,已达到最大重试次数: {e}")
|
||||
raise
|
||||
|
||||
async def _ensure_db_connection(self) -> None:
|
||||
"""确保数据库连接正常,如果连接已关闭则重新连接"""
|
||||
if not self._db_initialized:
|
||||
await self._init_db_connection()
|
||||
|
||||
async def _init_db_connection(self) -> None:
|
||||
"""初始化数据库连接"""
|
||||
try:
|
||||
# 从环境变量获取数据库配置
|
||||
mongodb_url = os.getenv("MONGODB_URL", "mongodb://localhost:27017")
|
||||
database_name = os.getenv("DATABASE_NAME", "douba_chat_dev")
|
||||
|
||||
# 创建新的数据库连接
|
||||
if self._db_client:
|
||||
try:
|
||||
await self._db_client.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
self._db_client = AsyncMongoClient(mongodb_url)
|
||||
await init_beanie(
|
||||
database=self._db_client[database_name], document_models=models
|
||||
)
|
||||
self._db_initialized = True
|
||||
logger.info(f"数据库连接初始化成功: {database_name}")
|
||||
except Exception as e:
|
||||
logger.error(f"数据库连接初始化失败: {e}")
|
||||
self._db_initialized = False
|
||||
raise
|
||||
|
||||
async def _load_or_create_role(self) -> GLMRole:
|
||||
"""加载或创建默认角色"""
|
||||
try:
|
||||
# 确保数据库连接正常
|
||||
await self._ensure_db_connection()
|
||||
|
||||
# 1. 尝试获取激活的角色
|
||||
active_role = await GLMRole.find_one(GLMRole.is_active == True)
|
||||
if active_role:
|
||||
@@ -70,6 +165,44 @@ class GLMService:
|
||||
# 3. 创建默认角色
|
||||
logger.info("创建默认角色配置")
|
||||
return await self._create_default_role()
|
||||
except Exception as e:
|
||||
logger.error(f"加载或创建角色失败: {e}")
|
||||
# 检查是否是数据库连接错误
|
||||
if (
|
||||
"Cannot use AsyncMongoClient after close" in str(e)
|
||||
or "not connected" in str(e).lower()
|
||||
):
|
||||
logger.error("数据库连接已关闭,尝试重新连接...")
|
||||
self._db_initialized = False
|
||||
# 尝试重新连接
|
||||
try:
|
||||
await self._init_db_connection()
|
||||
logger.info("数据库重新连接成功,重试操作...")
|
||||
# 重试操作
|
||||
return await self._retry_after_reconnect()
|
||||
except Exception as reconnect_error:
|
||||
logger.error(f"数据库重新连接失败: {reconnect_error}")
|
||||
raise
|
||||
|
||||
async def _retry_after_reconnect(self) -> GLMRole:
|
||||
"""重新连接后重试操作"""
|
||||
# 1. 尝试获取激活的角色
|
||||
active_role = await GLMRole.find_one(GLMRole.is_active == True)
|
||||
if active_role:
|
||||
logger.info(f"重新连接后加载激活角色: {active_role.name}")
|
||||
return active_role
|
||||
|
||||
# 2. 尝试获取默认角色
|
||||
default_role = await GLMRole.find_one(GLMRole.is_default == True)
|
||||
if default_role:
|
||||
default_role.is_active = True
|
||||
await default_role.save()
|
||||
logger.info(f"重新连接后加载默认角色并激活: {default_role.name}")
|
||||
return default_role
|
||||
|
||||
# 3. 创建默认角色
|
||||
logger.info("重新连接后创建默认角色配置")
|
||||
return await self._create_default_role()
|
||||
|
||||
async def _create_default_role(self) -> GLMRole:
|
||||
"""创建默认角色配置"""
|
||||
@@ -89,10 +222,40 @@ class GLMService:
|
||||
logger.info(f"默认角色创建成功: {role.name}")
|
||||
return role
|
||||
|
||||
async def _save_with_retry(self, role: GLMRole) -> None:
|
||||
"""使用重试机制保存角色"""
|
||||
for attempt in range(self._max_retries):
|
||||
try:
|
||||
# 确保数据库连接正常
|
||||
await self._ensure_db_connection()
|
||||
await role.save()
|
||||
return
|
||||
except Exception as e:
|
||||
if attempt < self._max_retries - 1:
|
||||
logger.warning(f"保存角色配置失败,第{attempt + 1}次重试: {e}")
|
||||
# 检查是否是数据库连接错误
|
||||
if (
|
||||
"Cannot use AsyncMongoClient after close" in str(e)
|
||||
or "not connected" in str(e).lower()
|
||||
):
|
||||
logger.warning("检测到数据库连接问题,尝试重新连接...")
|
||||
self._db_initialized = False
|
||||
try:
|
||||
await self._init_db_connection()
|
||||
logger.info("数据库重新连接成功,继续重试...")
|
||||
except Exception as reconnect_error:
|
||||
logger.error(f"数据库重新连接失败: {reconnect_error}")
|
||||
await asyncio.sleep(self._retry_delay * (attempt + 1))
|
||||
else:
|
||||
logger.error(f"保存角色配置失败,已达到最大重试次数: {e}")
|
||||
raise
|
||||
|
||||
async def ensure_default_role(self):
|
||||
"""确保存在默认角色配置(应用启动时调用)"""
|
||||
try:
|
||||
role = await self._load_or_create_role()
|
||||
# 确保数据库连接正常
|
||||
await self._ensure_db_connection()
|
||||
role = await self._load_or_create_role_with_retry()
|
||||
logger.info(f"默认角色已确保存在: {role.name}")
|
||||
return role
|
||||
except Exception as e:
|
||||
@@ -102,6 +265,13 @@ class GLMService:
|
||||
async def close(self):
|
||||
"""关闭服务,清理资源"""
|
||||
self._current_role = None
|
||||
if self._db_client:
|
||||
try:
|
||||
await self._db_client.close()
|
||||
logger.info("数据库连接已关闭")
|
||||
except Exception as e:
|
||||
logger.error(f"关闭数据库连接时出错: {e}")
|
||||
self._db_initialized = False
|
||||
logger.info("GLM服务已关闭")
|
||||
|
||||
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
消息服务
|
||||
提供加载会话存档、解密、保存到数据库的整合功能
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
import base64
|
||||
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Any, Optional
|
||||
import sys
|
||||
|
||||
|
||||
# 添加scripts目录到路径
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "scripts"))
|
||||
@@ -22,6 +22,9 @@ from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
|
||||
|
||||
from app.models.message import Message
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -36,10 +39,8 @@ class MessageService:
|
||||
corpid: 企业ID,如果为None则从环境变量CORPID读取
|
||||
secret: 聊天内容存档的Secret,如果为None则从环境变量WEWORK_SECRET读取
|
||||
"""
|
||||
self.corpid = corpid or os.getenv("CORPID", "wwdf65802ca25ec195")
|
||||
self.secret = secret or os.getenv(
|
||||
"WEWORK_SECRET", "-Ta6WMWxBhfGolWnnlO15nQckj3DRKAowUOdX2fwvzE"
|
||||
)
|
||||
self.corpid = corpid or os.getenv("CORPID", "")
|
||||
self.secret = secret or os.getenv("WEWORK_SECRET", "")
|
||||
|
||||
# SDK实例(延迟初始化)
|
||||
self._sdk: Optional[WeWorkFinanceSDK] = None
|
||||
@@ -266,25 +267,6 @@ class MessageService:
|
||||
logger.info(f"开始将 {len(messages)} 条消息存储到数据库...")
|
||||
|
||||
try:
|
||||
# 导入数据库相关模块
|
||||
from pymongo import AsyncMongoClient
|
||||
from beanie import init_beanie
|
||||
from app.models import models
|
||||
from app.models.message import Message
|
||||
|
||||
# 从环境变量获取数据库配置
|
||||
mongodb_url = os.getenv(
|
||||
"MONGODB_URL",
|
||||
"mongodb://dblink:mgdb4link@localhost:27017/?authSource=admin",
|
||||
)
|
||||
database_name = os.getenv("DATABASE_NAME", "douba_chat_dev")
|
||||
|
||||
# 初始化数据库连接
|
||||
client = AsyncMongoClient(mongodb_url)
|
||||
await init_beanie(database=client[database_name], document_models=models)
|
||||
|
||||
logger.info(f"数据库连接成功: {database_name}")
|
||||
|
||||
# 批量处理消息
|
||||
batch_size = 100
|
||||
for i in range(0, len(messages), batch_size):
|
||||
@@ -352,9 +334,6 @@ class MessageService:
|
||||
logger.error(f"批量插入失败: {e}")
|
||||
stats["failed"] += len(new_messages)
|
||||
|
||||
# 关闭数据库连接
|
||||
await client.close()
|
||||
|
||||
logger.info(f"数据库存储完成:")
|
||||
logger.info(f" 总计消息: {stats['total_messages']}")
|
||||
logger.info(f" 成功存储: {stats['saved']}")
|
||||
@@ -392,23 +371,6 @@ class MessageService:
|
||||
分页查询结果
|
||||
"""
|
||||
try:
|
||||
# 导入数据库相关模块
|
||||
from pymongo import AsyncMongoClient
|
||||
from beanie import init_beanie
|
||||
from app.models import models
|
||||
from app.models.message import Message
|
||||
|
||||
# 从环境变量获取数据库配置
|
||||
mongodb_url = os.getenv(
|
||||
"MONGODB_URL",
|
||||
"mongodb://dblink:mgdb4link@localhost:27017/?authSource=admin",
|
||||
)
|
||||
database_name = os.getenv("DATABASE_NAME", "douba_chat_dev")
|
||||
|
||||
# 初始化数据库连接
|
||||
client = AsyncMongoClient(mongodb_url)
|
||||
await init_beanie(database=client[database_name], document_models=models)
|
||||
|
||||
# 构建查询条件
|
||||
query = {}
|
||||
if msgtype:
|
||||
@@ -432,8 +394,12 @@ class MessageService:
|
||||
# 获取总记录数
|
||||
total_count = await Message.find(query).count()
|
||||
|
||||
# 关闭数据库连接
|
||||
await client.close()
|
||||
# 记录成功日志
|
||||
logger.info(
|
||||
f"成功查询消息列表: 查询条件={query}, "
|
||||
f"页码={page}, 每页数量={page_size}, "
|
||||
f"总记录数={total_count}, 返回记录数={len(messages)}"
|
||||
)
|
||||
|
||||
# 准备响应数据
|
||||
return {
|
||||
@@ -519,6 +485,7 @@ class MessageService:
|
||||
"decrypted_failed": 0,
|
||||
"saved_count": 0,
|
||||
"duplicates_skipped": 0,
|
||||
"seq": seq,
|
||||
"max_seq": seq,
|
||||
"next_seq": seq,
|
||||
},
|
||||
@@ -552,6 +519,7 @@ class MessageService:
|
||||
"saved_count": db_stats["saved"],
|
||||
"duplicates_skipped": db_stats["skipped_duplicates"],
|
||||
"failed_to_save": db_stats["failed"],
|
||||
"seq": seq,
|
||||
"max_seq": max_seq,
|
||||
"next_seq": max_seq + 1 if total_fetched > 0 else seq,
|
||||
},
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"access_token": "cB51r2D8h73vFEY2WOUOpT7oKOAsTrYzh5kb_xcXTbLsw69U-Wxy9KnM_ealJEef_XdrPyqXcc82IYiJFIFVnwEXy65wF3xQmtm3JxvGq293eSDnNvuxWWRFjoQSf7ani47d_GMAELYoaLcM4S7kFPJxoq7kZz0g2dizOQ0vKiApNgQs3B_nxqm8cAuCgZuSw0JJDPAYZyci4lnRCVe0ww",
|
||||
"fetch_time": 1765476407.16114,
|
||||
"access_token": "Lh3zzZFb7iaVkv4zt9VNoZntkS6irpWEpuOctdZcq9g2FWMo5XtNaX7TuFfqoNZePQYce2qRYYMGJxKBxr0hBSRHrLMwDFOiq6_BGiyNFWCs5xvZL1OHVch-FQg2haMjjiFrOPVpB1CGXEoVu5Rm-QFnEZuQrkH292MkF0V9_v-_gJ2EdRgcQ94QTeMnaYnM1bFo3fupAt58MnJ8eoPl8A",
|
||||
"fetch_time": 1765768551.7105587,
|
||||
"expires_in": 7200,
|
||||
"expiry_time": 1765482407.16114,
|
||||
"expiry_time": 1765774551.7105587,
|
||||
"ticket_data": {
|
||||
"ticket": "fkOdR8Y0baUUdG33FhsIfg==",
|
||||
"fetch_time": 1765476407.4518044,
|
||||
"ticket": "uR5/VwmCPuQPkoAuQbOZcQ==",
|
||||
"fetch_time": 1765768552.0206096,
|
||||
"expires_in": 7200,
|
||||
"expiry_time": 1765483607.4518046
|
||||
"expiry_time": 1765775752.0206099
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ dependencies = [
|
||||
"httpx>=0.28.1",
|
||||
"pydantic>=2.12.4",
|
||||
"pydantic-settings>=2.12.0",
|
||||
"python-dotenv>=1.2.1",
|
||||
"python-multipart>=0.0.20",
|
||||
"uvicorn>=0.38.0",
|
||||
]
|
||||
|
||||
126
backend/serv.sh
126
backend/serv.sh
@@ -61,7 +61,10 @@ init_config() {
|
||||
|
||||
# 日志文件路径
|
||||
LOG_DIR="${APP_DIR}/logs"
|
||||
LOG_FILE="${LOG_DIR}/${APP_NAME}.log"
|
||||
|
||||
# 获取当前日期用于日志文件分割
|
||||
CURRENT_DATE=$(date +%Y-%m-%d)
|
||||
LOG_FILE="${LOG_DIR}/${APP_NAME}-${CURRENT_DATE}.log"
|
||||
|
||||
log_info "应用名称: $APP_NAME"
|
||||
log_info "应用模块: $APP_MODULE"
|
||||
@@ -70,6 +73,7 @@ init_config() {
|
||||
log_info "服务器端口: $APP_PORT"
|
||||
log_info "工作进程数: $APP_WORKERS"
|
||||
log_info "日志级别: $APP_LOG_LEVEL"
|
||||
log_info "日志文件: $LOG_FILE"
|
||||
}
|
||||
|
||||
# 检查是否在正确的目录
|
||||
@@ -113,6 +117,16 @@ setup_logs() {
|
||||
|
||||
# 启动 Unicorn 服务器
|
||||
start_server() {
|
||||
local enable_reload=false
|
||||
|
||||
# 检查是否传递了 --reload 参数
|
||||
for arg in "$@"; do
|
||||
if [ "$arg" = "--reload" ]; then
|
||||
enable_reload=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
log_info "正在启动 $APP_NAME..."
|
||||
|
||||
# 检查是否已经在运行
|
||||
@@ -127,15 +141,25 @@ start_server() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# 启动服务器
|
||||
nohup uv run uvicorn \
|
||||
"$APP_MODULE" \
|
||||
--host "$APP_HOST" \
|
||||
--port "$APP_PORT" \
|
||||
--workers "$APP_WORKERS" \
|
||||
--log-level "$APP_LOG_LEVEL" \
|
||||
--access-log \
|
||||
--reload > "$LOG_FILE" 2>&1 &
|
||||
# 构建启动命令
|
||||
local uvicorn_cmd="uv run uvicorn \
|
||||
$APP_MODULE \
|
||||
--host $APP_HOST \
|
||||
--port $APP_PORT \
|
||||
--workers $APP_WORKERS \
|
||||
--log-level $APP_LOG_LEVEL \
|
||||
--access-log"
|
||||
|
||||
# 根据参数添加 --reload
|
||||
if [ "$enable_reload" = true ]; then
|
||||
uvicorn_cmd="$uvicorn_cmd --reload"
|
||||
log_info "启用热重载模式"
|
||||
else
|
||||
log_info "禁用热重载模式(生产环境推荐)"
|
||||
fi
|
||||
|
||||
# 启动服务器(使用 >> 追加日志,相同日期日志不会丢失)
|
||||
nohup $uvicorn_cmd >> "$LOG_FILE" 2>&1 &
|
||||
|
||||
local server_pid=$!
|
||||
echo $server_pid > "$PID_FILE"
|
||||
@@ -146,8 +170,9 @@ start_server() {
|
||||
log_success "$APP_NAME 已启动 (PID: $server_pid)"
|
||||
log_info "日志文件: $LOG_FILE"
|
||||
log_info "访问地址: http://$APP_HOST:$APP_PORT"
|
||||
log_info "热重载: $([ "$enable_reload" = true ] && echo "已启用" || echo "已禁用")"
|
||||
else
|
||||
log_error "$APP_NAME 启动失败"
|
||||
log_error "$APP_NAME 启动失败,请检查日志文件: $LOG_FILE"
|
||||
rm -f "$PID_FILE"
|
||||
exit 1
|
||||
fi
|
||||
@@ -192,7 +217,8 @@ restart_server() {
|
||||
log_info "正在重启 $APP_NAME..."
|
||||
stop_server
|
||||
sleep 2
|
||||
start_server
|
||||
# 传递所有参数给 start_server(包括可能的 --reload)
|
||||
start_server "$@"
|
||||
}
|
||||
|
||||
# 查看服务器状态
|
||||
@@ -216,35 +242,79 @@ status_server() {
|
||||
|
||||
# 查看服务器日志
|
||||
tail_logs() {
|
||||
if [ -f "$LOG_FILE" ]; then
|
||||
log_info "显示日志文件最后 50 行: $LOG_FILE"
|
||||
local target_log_file="$LOG_FILE"
|
||||
|
||||
# 检查是否有日期参数
|
||||
if [ $# -gt 0 ]; then
|
||||
local date_param="$1"
|
||||
# 验证日期格式是否为 YYYY-MM-DD
|
||||
if [[ "$date_param" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
|
||||
target_log_file="${LOG_DIR}/${APP_NAME}-${date_param}.log"
|
||||
log_info "查看指定日期日志: $date_param"
|
||||
else
|
||||
log_warning "日期参数格式错误,使用默认日期日志"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f "$target_log_file" ]; then
|
||||
log_info "显示日志文件最后 50 行: $target_log_file"
|
||||
echo "=========================================="
|
||||
tail -50 "$LOG_FILE"
|
||||
tail -50 "$target_log_file"
|
||||
echo "=========================================="
|
||||
|
||||
# 显示文件基本信息
|
||||
local file_size=$(du -h "$target_log_file" | cut -f1)
|
||||
local line_count=$(wc -l < "$target_log_file")
|
||||
log_info "文件大小: $file_size, 行数: $line_count"
|
||||
else
|
||||
# 尝试查找最新的日志文件
|
||||
local latest_log=$(ls -t "${LOG_DIR}/${APP_NAME}-"*.log 2>/dev/null | head -1)
|
||||
if [ -f "$latest_log" ]; then
|
||||
log_warning "指定日志文件不存在,显示最新日志文件: $latest_log"
|
||||
log_info "显示日志文件最后 50 行: $latest_log"
|
||||
echo "=========================================="
|
||||
tail -50 "$latest_log"
|
||||
echo "=========================================="
|
||||
else
|
||||
log_warning "日志文件不存在: $LOG_FILE"
|
||||
log_warning "日志文件不存在: $target_log_file"
|
||||
log_info "可用的日志文件:"
|
||||
ls -la "${LOG_DIR}/"*.log 2>/dev/null || echo "无日志文件"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# 显示帮助信息
|
||||
show_help() {
|
||||
echo "用法: $0 {start|stop|restart|status|logs|help}"
|
||||
echo "用法: $0 {start|stop|restart|status|logs|help} [--reload] [日期]"
|
||||
echo ""
|
||||
echo "服务管理命令:"
|
||||
echo " start 启动 $APP_NAME 服务器"
|
||||
echo " start [--reload] 启动 $APP_NAME 服务器(默认禁用热重载)"
|
||||
echo " stop 停止 $APP_NAME 服务器"
|
||||
echo " restart 重启 $APP_NAME 服务器"
|
||||
echo " restart [--reload] 重启 $APP_NAME 服务器"
|
||||
echo " status 查看 $APP_NAME 服务器状态"
|
||||
echo " logs 查看 $APP_NAME 服务器日志"
|
||||
echo " logs [YYYY-MM-DD] 查看 $APP_NAME 服务器日志(可指定日期)"
|
||||
echo " help 显示此帮助信息"
|
||||
echo ""
|
||||
echo "可选参数:"
|
||||
echo " --reload 启用热重载模式(开发环境使用)"
|
||||
echo " 日期参数 格式: YYYY-MM-DD,用于查看特定日期日志"
|
||||
echo ""
|
||||
echo "环境变量配置:"
|
||||
echo " 所有配置都从 .env 文件读取,包括:"
|
||||
echo " APP_NAME, APP_MODULE, APP_PORT, APP_WORKERS, APP_LOG_LEVEL 等"
|
||||
echo ""
|
||||
echo "日志功能:"
|
||||
echo " • 日志按日期分割: ${APP_NAME}-YYYY-MM-DD.log"
|
||||
echo " • 相同日期日志自动追加"
|
||||
echo " • 支持查看特定日期日志"
|
||||
echo ""
|
||||
echo "示例:"
|
||||
echo " $0 start # 启动服务器"
|
||||
echo " $0 status # 查看状态"
|
||||
echo " $0 start # 启动服务器(生产环境推荐)"
|
||||
echo " $0 start --reload # 启动服务器并启用热重载(开发环境)"
|
||||
echo " $0 restart --reload # 重启服务器并启用热重载"
|
||||
echo " $0 status # 查看服务器状态"
|
||||
echo " $0 logs # 查看今天日志"
|
||||
echo " $0 logs 2025-12-14 # 查看指定日期日志"
|
||||
}
|
||||
|
||||
# 主函数
|
||||
@@ -264,19 +334,25 @@ main() {
|
||||
# 处理命令行参数
|
||||
case "$1" in
|
||||
start)
|
||||
start_server
|
||||
# 传递剩余参数给 start_server
|
||||
shift
|
||||
start_server "$@"
|
||||
;;
|
||||
stop)
|
||||
stop_server
|
||||
;;
|
||||
restart)
|
||||
restart_server
|
||||
# 重启时也支持 --reload 参数
|
||||
shift
|
||||
restart_server "$@"
|
||||
;;
|
||||
status)
|
||||
status_server
|
||||
;;
|
||||
logs)
|
||||
tail_logs
|
||||
# 传递可能的日期参数给 tail_logs
|
||||
shift
|
||||
tail_logs "$@"
|
||||
;;
|
||||
help|--help|-h)
|
||||
show_help
|
||||
|
||||
2
backend/uv.lock
generated
2
backend/uv.lock
generated
@@ -58,6 +58,7 @@ dependencies = [
|
||||
{ name = "httpx" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "python-multipart" },
|
||||
{ name = "uvicorn" },
|
||||
]
|
||||
@@ -71,6 +72,7 @@ requires-dist = [
|
||||
{ name = "httpx", specifier = ">=0.28.1" },
|
||||
{ name = "pydantic", specifier = ">=2.12.4" },
|
||||
{ name = "pydantic-settings", specifier = ">=2.12.0" },
|
||||
{ name = "python-dotenv", specifier = ">=1.2.1" },
|
||||
{ name = "python-multipart", specifier = ">=0.0.20" },
|
||||
{ name = "uvicorn", specifier = ">=0.38.0" },
|
||||
]
|
||||
|
||||
2
backstage/.env.sample
Normal file
2
backstage/.env.sample
Normal file
@@ -0,0 +1,2 @@
|
||||
# API 基础地址
|
||||
VITE_API_BASE_URL=http://mp.yizhezhe.com/api
|
||||
@@ -4,6 +4,7 @@ import Dashboard from './pages/Dashboard'
|
||||
import Messages from './pages/Messages'
|
||||
import Settings from './pages/Settings'
|
||||
import Login from './pages/Login'
|
||||
import LoadArchive from './pages/LoadArchive'
|
||||
|
||||
function App() {
|
||||
// 简单的认证检查(实际项目中应该使用更完善的认证方案)
|
||||
@@ -17,6 +18,7 @@ function App() {
|
||||
<Route index element={<Dashboard />} />
|
||||
<Route path="messages" element={<Messages />} />
|
||||
<Route path="settings" element={<Settings />} />
|
||||
<Route path="load-archive" element={<LoadArchive />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</Router>
|
||||
|
||||
@@ -44,19 +44,6 @@ const Layout = () => {
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="border-t border-base-300 p-4">
|
||||
<div className="flex items-center">
|
||||
<div className="avatar placeholder">
|
||||
<div className="bg-neutral text-neutral-content w-8 rounded-full">
|
||||
<span className="text-xs">AD</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="text-sm font-medium">管理员</p>
|
||||
<p className="text-xs text-base-content/70">admin@example.com</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 主内容区 */}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
const DocumentationViewer = () => {
|
||||
const [library, setLibrary] = useState('react')
|
||||
const [mode, setMode] = useState('code')
|
||||
const [topic, setTopic] = useState('')
|
||||
const [page, setPage] = useState(1)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [docs, setDocs] = useState(null)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const libraries = [
|
||||
{ id: 'react', name: 'React', description: '用于构建用户界面的 JavaScript 库' },
|
||||
{ id: 'tailwindcss', name: 'Tailwind CSS', description: '实用优先的 CSS 框架' },
|
||||
{ id: 'daisyui', name: 'DaisyUI', description: 'Tailwind CSS 组件库' },
|
||||
{ id: 'vite', name: 'Vite', description: '下一代前端工具' },
|
||||
{ id: 'axios', name: 'Axios', description: '基于 Promise 的 HTTP 客户端' },
|
||||
]
|
||||
|
||||
const modes = [
|
||||
{ id: 'code', name: '代码文档', description: 'API 参考和代码示例' },
|
||||
{ id: 'info', name: '概念指南', description: '概念性指南和架构信息' },
|
||||
]
|
||||
|
||||
const handleFetchDocs = async () => {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
setDocs(null)
|
||||
|
||||
try {
|
||||
// 这里应该调用 Context7 MCP 工具
|
||||
// 由于我们无法直接在前端调用 MCP 工具,这里模拟 API 调用
|
||||
// 实际项目中应该通过后端代理调用 MCP 工具
|
||||
|
||||
// 模拟延迟
|
||||
@@ -1,4 +1,7 @@
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
const Dashboard = () => {
|
||||
const navigate = useNavigate()
|
||||
const stats = [
|
||||
{ title: '总消息数', value: '1,234', change: '+12%', icon: '💬', color: 'bg-primary' },
|
||||
{ title: '今日新增', value: '56', change: '+5%', icon: '📈', color: 'bg-success' },
|
||||
@@ -75,19 +78,31 @@ const Dashboard = () => {
|
||||
<div className="card-body">
|
||||
<h2 className="card-title">快速操作</h2>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<button className="btn btn-primary">
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => navigate('/load-archive')}
|
||||
>
|
||||
<span className="mr-2">📥</span>
|
||||
加载存档
|
||||
</button>
|
||||
<button className="btn btn-outline">
|
||||
<span className="mr-2">📊</span>
|
||||
查看统计
|
||||
</button>
|
||||
<button className="btn btn-outline">
|
||||
<button
|
||||
className="btn btn-outline"
|
||||
onClick={() => navigate('/messages')}
|
||||
>
|
||||
<span className="mr-2">🔍</span>
|
||||
搜索消息
|
||||
</button>
|
||||
<button className="btn btn-outline">
|
||||
<button
|
||||
className="btn btn-outline"
|
||||
onClick={() => navigate('/messages')}
|
||||
>
|
||||
<span className="mr-2">📋</span>
|
||||
消息列表
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline"
|
||||
onClick={() => navigate('/settings')}
|
||||
>
|
||||
<span className="mr-2">⚙️</span>
|
||||
系统设置
|
||||
</button>
|
||||
|
||||
499
backstage/src/pages/LoadArchive.jsx
Normal file
499
backstage/src/pages/LoadArchive.jsx
Normal file
@@ -0,0 +1,499 @@
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { messageApi } from '../services/api'
|
||||
|
||||
const LoadArchive = () => {
|
||||
const navigate = useNavigate()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [progress, setProgress] = useState({
|
||||
status: 'idle', // idle, loading, success, error
|
||||
currentStep: 0,
|
||||
steps: [
|
||||
{
|
||||
id: 'config',
|
||||
name: '初始化配置',
|
||||
status: 'pending', // pending, running, success, error
|
||||
details: null,
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
duration: null
|
||||
},
|
||||
{
|
||||
id: 'fetch_data',
|
||||
name: '拉取会话数据',
|
||||
status: 'pending',
|
||||
details: null,
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
duration: null
|
||||
},
|
||||
{
|
||||
id: 'save_db',
|
||||
name: '保存到数据库',
|
||||
status: 'pending',
|
||||
details: null,
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
duration: null
|
||||
},
|
||||
{
|
||||
id: 'complete',
|
||||
name: '完成',
|
||||
status: 'pending',
|
||||
details: null,
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
duration: null
|
||||
}
|
||||
],
|
||||
config: {
|
||||
corpid: '',
|
||||
secret: '',
|
||||
libPath: '',
|
||||
rsaKeyPath: '',
|
||||
dataDir: ''
|
||||
},
|
||||
result: null,
|
||||
error: null,
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
totalDuration: null
|
||||
})
|
||||
|
||||
// 更新步骤状态
|
||||
const updateStep = (stepId, updates) => {
|
||||
setProgress(prev => ({
|
||||
...prev,
|
||||
steps: prev.steps.map(step =>
|
||||
step.id === stepId ? { ...step, ...updates } : step
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
// 开始加载会话存档
|
||||
const startLoadArchive = async () => {
|
||||
const startTime = new Date()
|
||||
setLoading(true)
|
||||
setProgress(prev => ({
|
||||
...prev,
|
||||
status: 'loading',
|
||||
startTime,
|
||||
error: null,
|
||||
result: null
|
||||
}))
|
||||
|
||||
try {
|
||||
// 步骤1: 初始化配置
|
||||
updateStep('config', {
|
||||
status: 'running',
|
||||
startTime: new Date(),
|
||||
details: '正在读取配置信息...'
|
||||
})
|
||||
|
||||
// 获取配置信息
|
||||
const configResponse = await messageApi.getConfig()
|
||||
if (!configResponse.success) {
|
||||
throw new Error(configResponse.message || '获取配置信息失败')
|
||||
}
|
||||
|
||||
const config = configResponse.data
|
||||
|
||||
setProgress(prev => ({
|
||||
...prev,
|
||||
config
|
||||
}))
|
||||
|
||||
updateStep('config', {
|
||||
status: 'success',
|
||||
endTime: new Date(),
|
||||
duration: 500,
|
||||
details: `配置加载完成\n企业ID: ${config.corpid}\n动态库路径: ${config.libPath}\n数据目录: ${config.dataDir}`
|
||||
})
|
||||
|
||||
// 步骤3: 拉取会话数据
|
||||
updateStep('fetch_data', {
|
||||
status: 'running',
|
||||
startTime: new Date(),
|
||||
details: '正在从企业微信拉取会话数据...'
|
||||
})
|
||||
|
||||
// 调用加载API
|
||||
const loadResponse = await messageApi.loadMessages(0, 1000)
|
||||
|
||||
if (!loadResponse.success) {
|
||||
throw new Error(loadResponse.message || '加载会话数据失败')
|
||||
}
|
||||
|
||||
const result = loadResponse.data
|
||||
|
||||
updateStep('fetch_data', {
|
||||
status: 'success',
|
||||
endTime: new Date(),
|
||||
duration: 2000, // 模拟耗时
|
||||
details: `✓ 数据拉取成功\n共获取 ${result.total_fetched || 0} 条聊天记录`
|
||||
})
|
||||
|
||||
// 步骤5: 保存到数据库
|
||||
updateStep('save_db', {
|
||||
status: 'running',
|
||||
startTime: new Date(),
|
||||
details: '开始存储消息到数据库...'
|
||||
})
|
||||
|
||||
// 模拟数据库保存过程
|
||||
await new Promise(resolve => setTimeout(resolve, 1200))
|
||||
|
||||
updateStep('save_db', {
|
||||
status: 'success',
|
||||
endTime: new Date(),
|
||||
duration: 1200,
|
||||
details: `数据库存储完成:\n总计消息: ${result.total_fetched || 0}\n成功存储: ${result.saved_count || 0}\n跳过重复: ${result.duplicates_skipped || 0}\n存储失败: ${result.failed_to_save || 0}`
|
||||
})
|
||||
|
||||
// 步骤6: 完成
|
||||
updateStep('complete', {
|
||||
status: 'running',
|
||||
startTime: new Date(),
|
||||
details: '正在完成加载流程...'
|
||||
})
|
||||
|
||||
const endTime = new Date()
|
||||
const totalDuration = endTime - startTime
|
||||
|
||||
updateStep('complete', {
|
||||
status: 'success',
|
||||
endTime,
|
||||
duration: 500,
|
||||
details: `测试完成\n总耗时: ${(totalDuration / 1000).toFixed(2)}秒`
|
||||
})
|
||||
|
||||
setProgress(prev => ({
|
||||
...prev,
|
||||
status: 'success',
|
||||
endTime,
|
||||
totalDuration,
|
||||
result
|
||||
}))
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载会话存档失败:', error)
|
||||
|
||||
const currentStep = progress.steps.find(step => step.status === 'running')
|
||||
if (currentStep) {
|
||||
updateStep(currentStep.id, {
|
||||
status: 'error',
|
||||
endTime: new Date(),
|
||||
details: `错误: ${error.message}`
|
||||
})
|
||||
}
|
||||
|
||||
const endTime = new Date()
|
||||
const totalDuration = endTime - startTime
|
||||
|
||||
setProgress(prev => ({
|
||||
...prev,
|
||||
status: 'error',
|
||||
endTime,
|
||||
totalDuration,
|
||||
error: error.message
|
||||
}))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 重置状态
|
||||
const resetProgress = () => {
|
||||
setProgress({
|
||||
status: 'idle',
|
||||
currentStep: 0,
|
||||
steps: [
|
||||
{
|
||||
id: 'config',
|
||||
name: '初始化配置',
|
||||
status: 'pending',
|
||||
details: null,
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
duration: null
|
||||
},
|
||||
{
|
||||
id: 'fetch_data',
|
||||
name: '拉取会话数据',
|
||||
status: 'pending',
|
||||
details: null,
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
duration: null
|
||||
},
|
||||
{
|
||||
id: 'save_db',
|
||||
name: '保存到数据库',
|
||||
status: 'pending',
|
||||
details: null,
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
duration: null
|
||||
},
|
||||
{
|
||||
id: 'complete',
|
||||
name: '完成',
|
||||
status: 'pending',
|
||||
details: null,
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
duration: null
|
||||
}
|
||||
],
|
||||
config: {
|
||||
corpid: '',
|
||||
secret: '',
|
||||
libPath: '',
|
||||
rsaKeyPath: '',
|
||||
dataDir: ''
|
||||
},
|
||||
result: null,
|
||||
error: null,
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
totalDuration: null
|
||||
})
|
||||
}
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status) => {
|
||||
switch (status) {
|
||||
case 'pending': return 'bg-gray-100 text-gray-800'
|
||||
case 'running': return 'bg-blue-100 text-blue-800'
|
||||
case 'success': return 'bg-green-100 text-green-800'
|
||||
case 'error': return 'bg-red-100 text-red-800'
|
||||
default: return 'bg-gray-100 text-gray-800'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态图标
|
||||
const getStatusIcon = (status) => {
|
||||
switch (status) {
|
||||
case 'pending': return '⏳'
|
||||
case 'running': return '🔄'
|
||||
case 'success': return '✅'
|
||||
case 'error': return '❌'
|
||||
default: return '⏳'
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time) => {
|
||||
if (!time) return '--:--:--'
|
||||
return new Date(time).toLocaleTimeString('zh-CN')
|
||||
}
|
||||
|
||||
// 格式化时长
|
||||
const formatDuration = (duration) => {
|
||||
if (!duration) return '--'
|
||||
return `${(duration / 1000).toFixed(2)}秒`
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-6">
|
||||
{/* 页面标题和操作 */}
|
||||
<div className="flex flex-col justify-between gap-4 sm:flex-row sm:items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">加载会话存档</h1>
|
||||
<p className="text-base-content/70 mt-2">从企业微信加载会话存档数据到数据库</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
onClick={startLoadArchive}
|
||||
disabled={loading || progress.status === 'loading'}
|
||||
className="btn btn-primary"
|
||||
>
|
||||
{loading || progress.status === 'loading' ? (
|
||||
<>
|
||||
<span className="loading loading-spinner loading-sm"></span>
|
||||
加载中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="mr-2">🚀</span>
|
||||
开始加载
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={resetProgress}
|
||||
disabled={loading || progress.status === 'loading'}
|
||||
className="btn btn-outline"
|
||||
>
|
||||
<span className="mr-2">🔄</span>
|
||||
重置
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigate('/messages')}
|
||||
className="btn btn-outline"
|
||||
>
|
||||
<span className="mr-2">←</span>
|
||||
返回消息管理
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 整体状态 */}
|
||||
<div className="card bg-base-100 shadow">
|
||||
<div className="card-body">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="card-title">加载状态</h3>
|
||||
<p className="text-sm text-base-content/70">
|
||||
{progress.status === 'idle' && '等待开始加载...'}
|
||||
{progress.status === 'loading' && '正在加载会话存档...'}
|
||||
{progress.status === 'success' && '加载完成!'}
|
||||
{progress.status === 'error' && '加载失败'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-sm text-base-content/70">开始时间</div>
|
||||
<div className="font-bold">{formatTime(progress.startTime)}</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-sm text-base-content/70">结束时间</div>
|
||||
<div className="font-bold">{formatTime(progress.endTime)}</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-sm text-base-content/70">总耗时</div>
|
||||
<div className="font-bold">{formatDuration(progress.totalDuration)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{progress.error && (
|
||||
<div className="alert alert-error mt-4">
|
||||
<span>错误: {progress.error}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 步骤流程 */}
|
||||
<div className="card bg-base-100 shadow">
|
||||
<div className="card-body">
|
||||
<h3 className="card-title mb-4">加载步骤流程</h3>
|
||||
<div className="space-y-4">
|
||||
{progress.steps.map((step, index) => (
|
||||
<div key={step.id} className="border border-gray-200 rounded-lg p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${getStatusColor(step.status)}`}>
|
||||
<span className="font-bold">{index + 1}</span>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-bold text-lg">{step.name}</h4>
|
||||
<div className="flex items-center space-x-2 mt-1">
|
||||
<span className={`text-xs px-2 py-1 rounded-full ${getStatusColor(step.status)}`}>
|
||||
{getStatusIcon(step.status)} {step.status === 'pending' ? '等待中' :
|
||||
step.status === 'running' ? '进行中' :
|
||||
step.status === 'success' ? '成功' : '失败'}
|
||||
</span>
|
||||
{step.startTime && (
|
||||
<span className="text-xs text-gray-500">
|
||||
开始: {formatTime(step.startTime)}
|
||||
</span>
|
||||
)}
|
||||
{step.endTime && (
|
||||
<span className="text-xs text-gray-500">
|
||||
结束: {formatTime(step.endTime)}
|
||||
</span>
|
||||
)}
|
||||
{step.duration && (
|
||||
<span className="text-xs text-gray-500">
|
||||
耗时: {formatDuration(step.duration)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{step.details && (
|
||||
<div className="mt-3 ml-11">
|
||||
<div className="bg-gray-50 rounded p-3">
|
||||
<pre className="text-sm whitespace-pre-wrap font-mono">{step.details}</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 结果详情 */}
|
||||
{progress.result && (
|
||||
<div className="card bg-base-100 shadow">
|
||||
<div className="card-body">
|
||||
<h3 className="card-title mb-4">加载结果详情</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="table table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>指标</th>
|
||||
<th>数值</th>
|
||||
<th>说明</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>拉取消息总数</td>
|
||||
<td className="font-bold">{progress.result.total_fetched || 0}</td>
|
||||
<td>从企业微信拉取的消息总数</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>加密消息数量</td>
|
||||
<td className="font-bold">{progress.result.encrypted_count || 0}</td>
|
||||
<td>需要解密的加密消息数量</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>成功解密</td>
|
||||
<td className="font-bold text-green-600">{progress.result.decrypted_success || 0}</td>
|
||||
<td>成功解密的加密消息数量</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>解密失败</td>
|
||||
<td className="font-bold text-red-600">{progress.result.decrypted_failed || 0}</td>
|
||||
<td>解密失败的消息数量</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>成功存储</td>
|
||||
<td className="font-bold text-green-600">{progress.result.saved_count || 0}</td>
|
||||
<td>成功保存到数据库的消息数量</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>跳过重复</td>
|
||||
<td className="font-bold text-yellow-600">{progress.result.duplicates_skipped || 0}</td>
|
||||
<td>数据库中已存在,跳过存储的消息数量</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>存储失败</td>
|
||||
<td className="font-bold text-red-600">{progress.result.failed_to_save || 0}</td>
|
||||
<td>存储到数据库失败的消息数量</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>最大seq值</td>
|
||||
<td className="font-bold">{progress.result.max_seq || 0}</td>
|
||||
<td>本次拉取的最大seq值,用于下次拉取</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>建议下次seq</td>
|
||||
<td className="font-bold">{progress.result.next_seq || (progress.result.max_seq || 0) + 1}</td>
|
||||
<td>下次拉取建议使用的seq值</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoadArchive
|
||||
@@ -1,32 +1,306 @@
|
||||
import { useState } from 'react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { messageApi } from '../services/api'
|
||||
|
||||
const Messages = () => {
|
||||
const navigate = useNavigate()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [messages, setMessages] = useState([
|
||||
{ id: 1, from: '张三', content: '你好,我想了解一下产品详情', time: '2025-12-12 10:30', type: 'text', room: '单聊' },
|
||||
{ id: 2, from: '李四', content: '这是会议纪要文件', time: '2025-12-12 09:15', type: 'file', room: '群聊-项目组' },
|
||||
{ id: 3, from: '王五', content: '产品图片已发送', time: '2025-12-11 16:45', type: 'image', room: '单聊' },
|
||||
{ id: 4, from: '赵六', content: '关于下周会议安排', time: '2025-12-11 14:20', type: 'text', room: '群聊-管理组' },
|
||||
{ id: 5, from: '钱七', content: '语音消息', time: '2025-12-10 11:10', type: 'voice', room: '单聊' },
|
||||
])
|
||||
const [messages, setMessages] = useState([])
|
||||
const [pagination, setPagination] = useState({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
totalPages: 0
|
||||
})
|
||||
const [filters, setFilters] = useState({
|
||||
msgtype: '',
|
||||
roomType: '',
|
||||
search: ''
|
||||
})
|
||||
const [stats, setStats] = useState({
|
||||
today: { text: 0, image: 0, file: 0, voice: 0 },
|
||||
loadStatus: { lastLoadTime: '', successRate: 0 }
|
||||
})
|
||||
|
||||
const handleLoadArchive = async () => {
|
||||
setLoading(true)
|
||||
// 模拟 API 调用
|
||||
setTimeout(() => {
|
||||
setLoading(false)
|
||||
alert('会话存档加载成功!')
|
||||
}, 1500)
|
||||
// 抽屉状态
|
||||
const [selectedMessage, setSelectedMessage] = useState(null)
|
||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
|
||||
|
||||
// 数据格式转换函数
|
||||
const formatMessage = (backendMsg) => {
|
||||
// 处理发送者显示名称
|
||||
let fromDisplay = '未知用户'
|
||||
if (backendMsg.from_user) {
|
||||
// 如果是企业微信用户ID,可以尝试映射或截断显示
|
||||
if (backendMsg.from_user.startsWith('wmx')) {
|
||||
fromDisplay = `用户${backendMsg.from_user.slice(-6)}`
|
||||
} else {
|
||||
fromDisplay = backendMsg.from_user
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = (e) => {
|
||||
// 处理消息内容
|
||||
let content = '无内容'
|
||||
if (backendMsg.text && backendMsg.text.content) {
|
||||
content = backendMsg.text.content
|
||||
} else if (backendMsg.msgtype === 'image') {
|
||||
content = '[图片消息]'
|
||||
} else if (backendMsg.msgtype === 'file') {
|
||||
content = '[文件消息]'
|
||||
} else if (backendMsg.msgtype === 'voice') {
|
||||
content = '[语音消息]'
|
||||
}
|
||||
|
||||
// 处理时间显示
|
||||
let timeDisplay = '未知时间'
|
||||
if (backendMsg.msgtime) {
|
||||
try {
|
||||
const date = new Date(backendMsg.msgtime)
|
||||
timeDisplay = date.toLocaleString('zh-CN')
|
||||
} catch (e) {
|
||||
timeDisplay = backendMsg.msgtime
|
||||
}
|
||||
}
|
||||
|
||||
// 处理聊天类型
|
||||
let roomDisplay = '单聊'
|
||||
if (backendMsg.roomid) {
|
||||
roomDisplay = `群聊-${backendMsg.roomid.slice(-8)}`
|
||||
}
|
||||
|
||||
return {
|
||||
id: backendMsg.msgid || backendMsg._id,
|
||||
from: fromDisplay,
|
||||
content: content,
|
||||
time: timeDisplay,
|
||||
type: backendMsg.msgtype || 'text',
|
||||
room: roomDisplay,
|
||||
rawData: backendMsg // 保留原始数据供详细查看使用
|
||||
}
|
||||
}
|
||||
|
||||
// 加载消息列表 - 使用React 19的use hook
|
||||
const loadMessages = async (page = 1, filterParams = {}) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const currentFilters = { ...filters, ...filterParams }
|
||||
const params = {
|
||||
page,
|
||||
page_size: pagination.pageSize
|
||||
}
|
||||
|
||||
// 如果有消息类型过滤
|
||||
if (currentFilters.msgtype) {
|
||||
params.msgtype = currentFilters.msgtype
|
||||
}
|
||||
|
||||
// 如果有群聊过滤
|
||||
if (currentFilters.roomType === 'group') {
|
||||
params.roomid = { $exists: true, $ne: null }
|
||||
} else if (currentFilters.roomType === 'single') {
|
||||
params.roomid = null
|
||||
}
|
||||
|
||||
// 如果有搜索关键词
|
||||
if (currentFilters.search) {
|
||||
params.search = currentFilters.search
|
||||
}
|
||||
|
||||
const response = await messageApi.getMessages(params)
|
||||
if (response.success) {
|
||||
const formattedMessages = response.data.messages.map(formatMessage)
|
||||
setMessages(formattedMessages)
|
||||
setPagination({
|
||||
page: response.data.page,
|
||||
pageSize: response.data.page_size,
|
||||
total: response.data.total,
|
||||
totalPages: response.data.total_pages
|
||||
})
|
||||
|
||||
// 更新统计信息
|
||||
updateStats(formattedMessages)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载消息失败:', error)
|
||||
alert('加载消息失败,请检查网络连接或后端服务')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新统计信息
|
||||
const updateStats = (messages) => {
|
||||
// 今日消息统计(简化版,实际应该从后端获取)
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
const todayMessages = messages.filter(msg =>
|
||||
msg.time.includes(today.replace(/-/g, '/'))
|
||||
)
|
||||
|
||||
const todayStats = {
|
||||
text: todayMessages.filter(m => m.type === 'text').length,
|
||||
image: todayMessages.filter(m => m.type === 'image').length,
|
||||
file: todayMessages.filter(m => m.type === 'file').length,
|
||||
voice: todayMessages.filter(m => m.type === 'voice').length
|
||||
}
|
||||
|
||||
setStats(prev => ({
|
||||
...prev,
|
||||
today: todayStats,
|
||||
loadStatus: {
|
||||
lastLoadTime: new Date().toLocaleString('zh-CN'),
|
||||
successRate: messages.length > 0 ? 95 : 0
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// 获取统计信息用于显示(如果需要)
|
||||
const getStatsDisplay = () => {
|
||||
return {
|
||||
today: stats.today,
|
||||
loadStatus: stats.loadStatus
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到加载会话存档页面
|
||||
const handleLoadArchive = () => {
|
||||
navigate('/load-archive')
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = async (e) => {
|
||||
e.preventDefault()
|
||||
// 搜索逻辑
|
||||
alert('搜索功能开发中...')
|
||||
console.log('执行搜索操作')
|
||||
await loadMessages(1)
|
||||
}
|
||||
|
||||
// 过滤条件变化处理
|
||||
const handleFilterChange = (key, value) => {
|
||||
setFilters(prev => ({ ...prev, [key]: value }))
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const handlePageChange = (page) => {
|
||||
if (page >= 1 && page <= pagination.totalPages) {
|
||||
console.log(`切换到第 ${page} 页`)
|
||||
loadMessages(page)
|
||||
}
|
||||
}
|
||||
|
||||
// 查看消息详情
|
||||
const handleViewMessage = (message) => {
|
||||
setSelectedMessage(message)
|
||||
setIsDrawerOpen(true)
|
||||
}
|
||||
|
||||
// 关闭抽屉
|
||||
const closeDrawer = () => {
|
||||
setIsDrawerOpen(false)
|
||||
setSelectedMessage(null)
|
||||
}
|
||||
|
||||
// 组件加载时初始化数据
|
||||
useEffect(() => {
|
||||
console.log(`Messages组件加载,初始化数据`)
|
||||
loadMessages(1)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
// 渲染消息内容
|
||||
const renderMessageContent = () => {
|
||||
if (!selectedMessage) return null
|
||||
|
||||
const { rawData } = selectedMessage
|
||||
|
||||
switch (selectedMessage.type) {
|
||||
case 'text':
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<div className="font-semibold mb-2">消息内容:</div>
|
||||
<div className="bg-base-200 rounded-lg p-4 whitespace-pre-wrap break-words">
|
||||
{rawData.text?.content || selectedMessage.content}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
case 'image':
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<div className="font-semibold mb-2">图片消息:</div>
|
||||
<div className="bg-base-200 rounded-lg p-4">
|
||||
{rawData.image?.md5sum ? (
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm">图片MD5: {rawData.image.md5sum}</div>
|
||||
<div className="text-sm">图片大小: {rawData.image.filesize || '未知'} bytes</div>
|
||||
<div className="text-sm text-base-content/70">
|
||||
(图片文件需要从企业微信服务器下载)
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-base-content/70">无图片详细信息</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
case 'file':
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<div className="font-semibold mb-2">文件消息:</div>
|
||||
<div className="bg-base-200 rounded-lg p-4">
|
||||
{rawData.file ? (
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm">文件名: {rawData.file.filename || '未知'}</div>
|
||||
<div className="text-sm">文件大小: {rawData.file.filesize || '未知'} bytes</div>
|
||||
<div className="text-sm">文件扩展名: {rawData.file.fileext || '未知'}</div>
|
||||
<div className="text-sm text-base-content/70">
|
||||
(文件需要从企业微信服务器下载)
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-base-content/70">无文件详细信息</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
case 'voice':
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<div className="font-semibold mb-2">语音消息:</div>
|
||||
<div className="bg-base-200 rounded-lg p-4">
|
||||
{rawData.voice ? (
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm">语音时长: {rawData.voice.play_length || '未知'} 秒</div>
|
||||
<div className="text-sm">文件大小: {rawData.voice.filesize || '未知'} bytes</div>
|
||||
<div className="text-sm text-base-content/70">
|
||||
(语音文件需要从企业微信服务器下载)
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-base-content/70">无语音详细信息</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<div className="font-semibold mb-2">消息内容:</div>
|
||||
<div className="bg-base-200 rounded-lg p-4">
|
||||
{selectedMessage.content}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="drawer drawer-end">
|
||||
<input
|
||||
id="message-drawer"
|
||||
type="checkbox"
|
||||
className="drawer-toggle"
|
||||
checked={isDrawerOpen}
|
||||
onChange={() => {}} // 只读,通过状态控制
|
||||
/>
|
||||
<div className="drawer-content space-y-6">
|
||||
{/* 页面标题和操作 */}
|
||||
<div className="flex flex-col justify-between gap-4 sm:flex-row sm:items-center">
|
||||
<div>
|
||||
@@ -51,14 +325,14 @@ const Messages = () => {
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<button className="btn btn-outline">
|
||||
{/* <button className="btn btn-outline">
|
||||
<span className="mr-2">📊</span>
|
||||
查看统计
|
||||
</button>
|
||||
<button className="btn btn-outline">
|
||||
<span className="mr-2">📤</span>
|
||||
导出数据
|
||||
</button>
|
||||
</button> */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -71,24 +345,53 @@ const Messages = () => {
|
||||
type="text"
|
||||
placeholder="搜索消息内容、发送者..."
|
||||
className="input input-bordered w-full"
|
||||
value={filters.search}
|
||||
onChange={(e) => handleFilterChange('search', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<select className="select select-bordered">
|
||||
<select
|
||||
className="select select-bordered"
|
||||
value={filters.msgtype}
|
||||
onChange={(e) => handleFilterChange('msgtype', e.target.value)}
|
||||
>
|
||||
<option value="">所有消息类型</option>
|
||||
<option value="text">文本消息</option>
|
||||
<option value="image">图片消息</option>
|
||||
<option value="file">文件消息</option>
|
||||
<option value="voice">语音消息</option>
|
||||
</select>
|
||||
<select className="select select-bordered">
|
||||
<select
|
||||
className="select select-bordered"
|
||||
value={filters.roomType}
|
||||
onChange={(e) => handleFilterChange('roomType', e.target.value)}
|
||||
>
|
||||
<option value="">所有聊天类型</option>
|
||||
<option value="single">单聊</option>
|
||||
<option value="group">群聊</option>
|
||||
</select>
|
||||
<button type="submit" className="btn btn-primary">
|
||||
<button type="submit" className="btn btn-primary" disabled={loading}>
|
||||
{loading ? (
|
||||
<>
|
||||
<span className="loading loading-spinner loading-sm mr-2"></span>
|
||||
搜索中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="mr-2">🔍</span>
|
||||
搜索
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline"
|
||||
onClick={() => {
|
||||
setFilters({ msgtype: '', roomType: '', search: '' })
|
||||
loadMessages(1)
|
||||
}}
|
||||
>
|
||||
重置
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -153,14 +456,13 @@ const Messages = () => {
|
||||
<td>{message.time}</td>
|
||||
<td>
|
||||
<div className="flex gap-2">
|
||||
<button className="btn btn-ghost btn-xs">
|
||||
<button
|
||||
className="btn btn-ghost btn-xs"
|
||||
onClick={() => handleViewMessage(message)}
|
||||
>
|
||||
<span className="mr-1">👁️</span>
|
||||
查看
|
||||
</button>
|
||||
<button className="btn btn-ghost btn-xs">
|
||||
<span className="mr-1">📋</span>
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -172,91 +474,165 @@ const Messages = () => {
|
||||
{/* 分页 */}
|
||||
<div className="flex items-center justify-between pt-4">
|
||||
<div className="text-sm text-base-content/70">
|
||||
显示 1-5 条,共 1234 条消息
|
||||
{messages.length > 0 ? (
|
||||
`显示 ${(pagination.page - 1) * pagination.pageSize + 1}-${Math.min(pagination.page * pagination.pageSize, pagination.total)} 条,共 ${pagination.total} 条消息`
|
||||
) : (
|
||||
'暂无消息数据'
|
||||
)}
|
||||
</div>
|
||||
{pagination.totalPages > 1 && (
|
||||
<div className="join">
|
||||
<button className="join-item btn btn-sm">«</button>
|
||||
<button className="join-item btn btn-sm btn-active">1</button>
|
||||
<button className="join-item btn btn-sm">2</button>
|
||||
<button className="join-item btn btn-sm">3</button>
|
||||
<button className="join-item btn btn-sm">4</button>
|
||||
<button className="join-item btn btn-sm">»</button>
|
||||
<button
|
||||
className="join-item btn btn-sm"
|
||||
onClick={() => handlePageChange(pagination.page - 1)}
|
||||
disabled={pagination.page <= 1}
|
||||
>
|
||||
«
|
||||
</button>
|
||||
{Array.from({ length: Math.min(5, pagination.totalPages) }, (_, i) => {
|
||||
let pageNum = i + 1
|
||||
if (pagination.totalPages > 5) {
|
||||
if (pagination.page <= 3) {
|
||||
pageNum = i + 1
|
||||
} else if (pagination.page >= pagination.totalPages - 2) {
|
||||
pageNum = pagination.totalPages - 4 + i
|
||||
} else {
|
||||
pageNum = pagination.page - 2 + i
|
||||
}
|
||||
}
|
||||
return (
|
||||
<button
|
||||
key={pageNum}
|
||||
className={`join-item btn btn-sm ${pagination.page === pageNum ? 'btn-active' : ''}`}
|
||||
onClick={() => handlePageChange(pageNum)}
|
||||
>
|
||||
{pageNum}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
<button
|
||||
className="join-item btn btn-sm"
|
||||
onClick={() => handlePageChange(pagination.page + 1)}
|
||||
disabled={pagination.page >= pagination.totalPages}
|
||||
>
|
||||
»
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计信息 */}
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-3">
|
||||
<div className="card bg-base-100 shadow">
|
||||
<div className="card-body">
|
||||
<h3 className="card-title">今日消息统计</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span>文本消息</span>
|
||||
<span className="font-bold">45 条</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>图片消息</span>
|
||||
<span className="font-bold">12 条</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>文件消息</span>
|
||||
<span className="font-bold">8 条</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>语音消息</span>
|
||||
<span className="font-bold">3 条</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card bg-base-100 shadow">
|
||||
<div className="card-body">
|
||||
<h3 className="card-title">加载状态</h3>
|
||||
{/* 抽屉侧边栏 */}
|
||||
<div className="drawer-side">
|
||||
<label
|
||||
htmlFor="message-drawer"
|
||||
className="drawer-overlay"
|
||||
onClick={closeDrawer}
|
||||
></label>
|
||||
<div className="bg-base-100 w-full max-w-md h-full overflow-y-auto">
|
||||
{selectedMessage && (
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-2xl font-bold">消息详情</h2>
|
||||
<button
|
||||
onClick={closeDrawer}
|
||||
className="btn btn-ghost btn-sm"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 消息基本信息 */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="flex justify-between mb-1">
|
||||
<span>上次加载时间</span>
|
||||
<span className="font-bold">2025-12-12 10:30</span>
|
||||
<div className="font-semibold mb-1">发送者:</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="avatar placeholder">
|
||||
<div className="bg-neutral text-neutral-content w-10 rounded-full">
|
||||
<span className="text-sm">{selectedMessage.from.charAt(0)}</span>
|
||||
</div>
|
||||
<progress className="progress progress-primary w-full" value="100" max="100"></progress>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between mb-1">
|
||||
<span>加载成功率</span>
|
||||
<span className="font-bold">98.5%</span>
|
||||
</div>
|
||||
<progress className="progress progress-success w-full" value="98.5" max="100"></progress>
|
||||
<div className="font-bold">{selectedMessage.from}</div>
|
||||
<div className="text-sm text-base-content/70">
|
||||
ID: {selectedMessage.rawData.from_user || '未知'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card bg-base-100 shadow">
|
||||
<div className="card-body">
|
||||
<h3 className="card-title">快速操作</h3>
|
||||
<div className="space-y-2">
|
||||
<button className="btn btn-outline btn-block justify-start">
|
||||
<span className="mr-2">🔄</span>
|
||||
刷新消息列表
|
||||
</button>
|
||||
<button className="btn btn-outline btn-block justify-start">
|
||||
<span className="mr-2">🧹</span>
|
||||
清理旧数据
|
||||
</button>
|
||||
<button className="btn btn-outline btn-block justify-start">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div className="font-semibold mb-1">消息类型:</div>
|
||||
<span className={`badge ${selectedMessage.type === 'text' ? 'badge-info' : selectedMessage.type === 'image' ? 'badge-success' : selectedMessage.type === 'file' ? 'badge-warning' : 'badge-error'}`}>
|
||||
{selectedMessage.type === 'text' ? '文本' : selectedMessage.type === 'image' ? '图片' : selectedMessage.type === 'file' ? '文件' : '语音'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-semibold mb-1">聊天类型:</div>
|
||||
<span className={`badge ${selectedMessage.room.includes('群聊') ? 'badge-primary' : 'badge-secondary'}`}>
|
||||
{selectedMessage.room}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="font-semibold mb-1">发送时间:</div>
|
||||
<div className="text-lg">{selectedMessage.time}</div>
|
||||
<div className="text-sm text-base-content/70">
|
||||
原始时间戳: {selectedMessage.rawData.msgtime || '未知'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedMessage.rawData.tolist && selectedMessage.rawData.tolist.length > 0 && (
|
||||
<div>
|
||||
<div className="font-semibold mb-1">接收者:</div>
|
||||
<div className="bg-base-200 rounded-lg p-3">
|
||||
{selectedMessage.rawData.tolist.map((to, index) => (
|
||||
<div key={index} className="text-sm">
|
||||
{to}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 消息内容 */}
|
||||
{renderMessageContent()}
|
||||
|
||||
{/* 原始数据 */}
|
||||
<div className="mt-6">
|
||||
<div className="collapse collapse-arrow bg-base-200">
|
||||
<input type="checkbox" />
|
||||
<div className="collapse-title font-semibold">
|
||||
查看原始数据
|
||||
</div>
|
||||
<div className="collapse-content">
|
||||
<pre className="text-xs bg-base-300 p-3 rounded-lg overflow-x-auto">
|
||||
{JSON.stringify(selectedMessage.rawData, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex gap-2 mt-6 pt-4 border-t border-base-300">
|
||||
<button className="btn btn-primary flex-1">
|
||||
<span className="mr-2">📋</span>
|
||||
批量操作
|
||||
复制内容
|
||||
</button>
|
||||
<button className="btn btn-outline btn-block justify-start">
|
||||
<span className="mr-2">⚙️</span>
|
||||
消息设置
|
||||
<button className="btn btn-outline flex-1">
|
||||
<span className="mr-2">📤</span>
|
||||
导出
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import axios from 'axios'
|
||||
|
||||
// API 基础配置
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000/api'
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api'
|
||||
|
||||
// 创建 axios 实例
|
||||
const apiClient = axios.create({
|
||||
@@ -88,6 +88,10 @@ export const messageApi = {
|
||||
// 获取单个消息详情
|
||||
getMessageById: (msgid) =>
|
||||
apiClient.get(`/messages/${msgid}`),
|
||||
|
||||
// 获取配置信息
|
||||
getConfig: () =>
|
||||
apiClient.get('/messages/config'),
|
||||
}
|
||||
|
||||
// 认证相关 API
|
||||
|
||||
@@ -43,8 +43,8 @@ const WorkflowProgress = ({
|
||||
{stepId === 'context' && stepInfo.data.scene && (
|
||||
<div>场景: {stepInfo.data.scene}</div>
|
||||
)}
|
||||
{stepId === 'customer' && stepInfo.data.externalUserId && (
|
||||
<div>客户ID: {stepInfo.data.externalUserId}</div>
|
||||
{stepId === 'customer' && stepInfo.data.userId && (
|
||||
<div>{stepInfo.data.userId}</div>
|
||||
)}
|
||||
{stepId === 'tags' && stepInfo.data.tags && (
|
||||
<div>标签数: {stepInfo.data.tags.length}</div>
|
||||
|
||||
@@ -248,16 +248,47 @@ export const useWeCom = () => {
|
||||
ww.getCurExternalContact({
|
||||
success: (res) => {
|
||||
clearTimeout(timeoutId);
|
||||
console.log('获取当前客户ID成功:', res);
|
||||
console.log('获取当前客户ID成功,原始数据:', res);
|
||||
|
||||
// 检查返回的数据结构
|
||||
if (res && res.externalUserId) {
|
||||
resolve(res);
|
||||
} else {
|
||||
console.warn('获取当前客户ID返回的数据格式异常:', res);
|
||||
// 即使数据格式异常,也返回数据,让上层处理
|
||||
resolve(res || { externalUserId: 'unknown', data: res });
|
||||
// 详细检查返回的数据结构
|
||||
console.log('返回数据键名:', Object.keys(res || {}));
|
||||
console.log('返回数据类型:', typeof res);
|
||||
|
||||
// 确保始终返回包含externalUserId字段的对象
|
||||
let result = res || {};
|
||||
if (!result.externalUserId) {
|
||||
console.warn('企业微信API返回的数据没有externalUserId字段,数据:', result);
|
||||
// 尝试从其他可能的字段获取客户ID
|
||||
const possibleUserIdFields = ['userId', 'userid', 'external_userid', 'externalUserid', 'user_id', 'external_user_id'];
|
||||
for (const field of possibleUserIdFields) {
|
||||
if (result[field]) {
|
||||
console.log(`从${field}字段找到客户ID:`, result[field]);
|
||||
result.externalUserId = result[field];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果还是没有找到,检查是否有嵌套结构
|
||||
if (!result.externalUserId && result.data) {
|
||||
console.log('检查嵌套的data字段:', result.data);
|
||||
for (const field of possibleUserIdFields) {
|
||||
if (result.data[field]) {
|
||||
console.log(`从data.${field}字段找到客户ID:`, result.data[field]);
|
||||
result.externalUserId = result.data[field];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果还是没有找到,设置为'unknown'
|
||||
if (!result.externalUserId) {
|
||||
result.externalUserId = 'unknown';
|
||||
console.warn('无法从返回数据中找到客户ID,设置为unknown');
|
||||
}
|
||||
}
|
||||
|
||||
console.log('处理后的客户ID数据:', result);
|
||||
resolve(result);
|
||||
},
|
||||
fail: (err) => {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
@@ -436,11 +436,15 @@ export const workflowService = {
|
||||
}
|
||||
|
||||
console.log('当前页面支持获取客户ID,entry:', contextInfo.entry);
|
||||
// alert(`当前页面支持获取客户ID,entry: ${contextInfo.entry}`);
|
||||
|
||||
// 在支持获取客户ID的页面,尝试获取真实客户ID
|
||||
customerResult = await weComHook.getCurExternalContact()
|
||||
.then(customerInfo => {
|
||||
console.log('成功获取到客户ID:', customerInfo);
|
||||
console.log('成功获取到客户ID,完整数据:', customerInfo);
|
||||
console.log('客户ID字段值:', customerInfo.externalUserId);
|
||||
console.log('客户数据所有键名:', Object.keys(customerInfo || {}));
|
||||
// alert(`成功获取到客户ID: ${customerInfo.externalUserId}\n完整数据键名: ${Object.keys(customerInfo || {}).join(', ')}`);
|
||||
return {
|
||||
...customerInfo,
|
||||
context: contextInfo,
|
||||
@@ -500,6 +504,7 @@ export const workflowService = {
|
||||
// 5. 获取会话历史
|
||||
if (onProgress) onProgress('history', 'loading');
|
||||
let historyResult;
|
||||
// alert(`5. 获取会话历史开始 userId: ${customerResult.externalUserId}`);
|
||||
if (!customerResult.externalUserId || customerResult.mock) {
|
||||
console.warn('使用模拟历史数据');
|
||||
historyResult = {
|
||||
|
||||
Reference in New Issue
Block a user