更新后台管理消息列表;

This commit is contained in:
karlcarlo
2025-12-15 17:44:35 +08:00
parent 41d2d893d3
commit a7b3454f61
22 changed files with 1620 additions and 356 deletions

1
.gitignore vendored
View File

@@ -26,6 +26,7 @@ dist
dist-ssr dist-ssr
*.local *.local
*.tar *.tar
*.pid
# Editor directories and files # Editor directories and files
.vscode/* .vscode/*

View File

@@ -1 +0,0 @@
285640

View File

@@ -1,3 +1,6 @@
import os
import logging
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from pymongo import AsyncMongoClient from pymongo import AsyncMongoClient
@@ -6,10 +9,16 @@ from app.models import models
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
import uvicorn
import os
from app.routes import auth_routes, wework_routes, glm_routes, message_routes 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 @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
@@ -24,27 +33,8 @@ async def lifespan(app: FastAPI):
) # 批量注册模型 ) # 批量注册模型
print(f"🗄️ 数据库连接成功: {database_name}") 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 # 应用在此处进入正常运行状态,接收请求 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() await client.close()
print(f"数据库连接关闭") print(f"数据库连接关闭")
@@ -85,50 +75,4 @@ async def root():
"service": "企业微信认证与会话存档服务", "service": "企业微信认证与会话存档服务",
"version": "1.0.0", "version": "1.0.0",
"description": "获取和管理企业微信access_token,会话存档功能,以及DeepSeek AI聊天服务", "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()

View File

@@ -1,6 +1,6 @@
from fastapi import APIRouter, HTTPException, Depends from fastapi import APIRouter, HTTPException, Depends
from pydantic import BaseModel from pydantic import BaseModel
from typing import List, Optional from typing import List, Optional, Dict, Any
import httpx import httpx
import os import os
import logging import logging
@@ -23,6 +23,10 @@ class Message(BaseModel):
class ChatRequest(BaseModel): class ChatRequest(BaseModel):
message: str message: str
conversation_id: Optional[str] = None 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): class ChatResponse(BaseModel):
@@ -194,44 +198,233 @@ async def update_role_config(
@router.post("/chat", response_model=ChatResponse) @router.post("/chat", response_model=ChatResponse)
async def chat(request: ChatRequest, service: GLMService = Depends(get_glm_service)): 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: if request.conversation_id and request.conversation_id in conversations:
conversation_history = conversations[request.conversation_id] 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: else:
conversation_history = [] conversation_history = []
request.conversation_id = f"conv_{len(conversations) + 1}" request.conversation_id = f"conv_{len(conversations) + 1}"
conversations[request.conversation_id] = conversation_history conversations[request.conversation_id] = conversation_history
logger.info(f"新建对话: {request.conversation_id}")
# 获取当前角色配置 # 步骤3: 构建系统提示词
try: 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() role = await service.get_current_role()
system_prompt = build_system_prompt(role.to_role_config_dict()) role_config = role.to_role_config_dict()
except Exception as e: logger.info(
logger.error(f"获取角色配置失败: {e}") f"获取角色配置: name='{role.name}', "
raise HTTPException(status_code=500, detail=f"获取角色配置失败: {str(e)}") 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}] 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": msg["role"], "content": msg["content"]})
# 添加当前用户消息 # 添加当前用户消息
messages.append({"role": "user", "content": request.message}) messages.append({"role": "user", "content": request.message})
conversation_history.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: try:
logger.info(
f"调用DeepSeek API, conversation_id={request.conversation_id}, 总消息数: {len(messages)}"
)
response_text = await call_deepseek_api(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}) conversation_history.append({"role": "assistant", "content": response_text})
# 步骤7: 返回响应
logger.info(
f"返回聊天响应: conversation_id={request.conversation_id}, 响应长度: {len(response_text)} 字符"
)
return ChatResponse( return ChatResponse(
response=response_text, conversation_id=request.conversation_id response=response_text, conversation_id=request.conversation_id
) )
except Exception as e: 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)) raise HTTPException(status_code=500, detail=str(e))

View File

@@ -135,6 +135,40 @@ async def get_message_stats(service: MessageService = Depends(get_message_servic
raise HTTPException(status_code=500, detail=f"获取消息统计失败: {str(e)}") 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}") @router.get("/{msgid}")
async def get_message_by_id( async def get_message_by_id(
msgid: str, service: MessageService = Depends(get_message_service) msgid: str, service: MessageService = Depends(get_message_service)
@@ -150,13 +184,3 @@ async def get_message_by_id(
except Exception as e: except Exception as e:
logger.error(f"查询消息详情失败 (msgid: {msgid}): {e}") logger.error(f"查询消息详情失败 (msgid: {msgid}): {e}")
raise HTTPException(status_code=500, detail=f"查询消息详情失败: {str(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

@@ -3,8 +3,13 @@ GLM聊天服务,包含角色配置管理
""" """
import logging import logging
import asyncio
import os
from typing import Optional from typing import Optional
from pymongo import AsyncMongoClient
from beanie import init_beanie
from app.models.glm_role import GLMRole from app.models.glm_role import GLMRole
from app.models import models
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -14,11 +19,19 @@ class GLMService:
def __init__(self): def __init__(self):
self._current_role: Optional[GLMRole] = None 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: async def get_current_role(self) -> GLMRole:
"""获取当前激活的角色配置""" """获取当前激活的角色配置"""
if self._current_role is None: 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 return self._current_role
async def update_role(self, role_data: dict) -> GLMRole: async def update_role(self, role_data: dict) -> GLMRole:
@@ -30,29 +43,111 @@ class GLMService:
Returns: Returns:
更新后的GLMRole实例 更新后的GLMRole实例
""" """
logger.info(f"开始更新角色配置,更新字段: {list(role_data.keys())}")
role = await self.get_current_role() 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"] 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"] 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"] 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.restrictions = role_data["restrictions"]
role.update_timestamp() 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 self._current_role = role
logger.info(f"角色配置已更新: {role.name}") logger.info(f"角色配置更新完成: name='{role.name}'")
return role 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: async def _load_or_create_role(self) -> GLMRole:
"""加载或创建默认角色""" """加载或创建默认角色"""
try:
# 确保数据库连接正常
await self._ensure_db_connection()
# 1. 尝试获取激活的角色 # 1. 尝试获取激活的角色
active_role = await GLMRole.find_one(GLMRole.is_active == True) active_role = await GLMRole.find_one(GLMRole.is_active == True)
if active_role: if active_role:
@@ -70,6 +165,44 @@ class GLMService:
# 3. 创建默认角色 # 3. 创建默认角色
logger.info("创建默认角色配置") logger.info("创建默认角色配置")
return await self._create_default_role() 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: async def _create_default_role(self) -> GLMRole:
"""创建默认角色配置""" """创建默认角色配置"""
@@ -89,10 +222,40 @@ class GLMService:
logger.info(f"默认角色创建成功: {role.name}") logger.info(f"默认角色创建成功: {role.name}")
return role 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): async def ensure_default_role(self):
"""确保存在默认角色配置(应用启动时调用)""" """确保存在默认角色配置(应用启动时调用)"""
try: 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}") logger.info(f"默认角色已确保存在: {role.name}")
return role return role
except Exception as e: except Exception as e:
@@ -102,6 +265,13 @@ class GLMService:
async def close(self): async def close(self):
"""关闭服务,清理资源""" """关闭服务,清理资源"""
self._current_role = None 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服务已关闭") logger.info("GLM服务已关闭")

View File

@@ -1,18 +1,18 @@
#!/usr/bin/env python3
""" """
消息服务 消息服务
提供加载会话存档、解密、保存到数据库的整合功能 提供加载会话存档、解密、保存到数据库的整合功能
""" """
import asyncio
import json import json
import os import os
import sys
import logging import logging
import base64 import base64
from pathlib import Path from pathlib import Path
from datetime import datetime from datetime import datetime
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
import sys
# 添加scripts目录到路径 # 添加scripts目录到路径
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "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.primitives.asymmetric import padding
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
from app.models.message import Message
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -36,10 +39,8 @@ class MessageService:
corpid: 企业ID,如果为None则从环境变量CORPID读取 corpid: 企业ID,如果为None则从环境变量CORPID读取
secret: 聊天内容存档的Secret,如果为None则从环境变量WEWORK_SECRET读取 secret: 聊天内容存档的Secret,如果为None则从环境变量WEWORK_SECRET读取
""" """
self.corpid = corpid or os.getenv("CORPID", "wwdf65802ca25ec195") self.corpid = corpid or os.getenv("CORPID", "")
self.secret = secret or os.getenv( self.secret = secret or os.getenv("WEWORK_SECRET", "")
"WEWORK_SECRET", "-Ta6WMWxBhfGolWnnlO15nQckj3DRKAowUOdX2fwvzE"
)
# SDK实例(延迟初始化) # SDK实例(延迟初始化)
self._sdk: Optional[WeWorkFinanceSDK] = None self._sdk: Optional[WeWorkFinanceSDK] = None
@@ -266,25 +267,6 @@ class MessageService:
logger.info(f"开始将 {len(messages)} 条消息存储到数据库...") logger.info(f"开始将 {len(messages)} 条消息存储到数据库...")
try: 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 batch_size = 100
for i in range(0, len(messages), batch_size): for i in range(0, len(messages), batch_size):
@@ -352,9 +334,6 @@ class MessageService:
logger.error(f"批量插入失败: {e}") logger.error(f"批量插入失败: {e}")
stats["failed"] += len(new_messages) stats["failed"] += len(new_messages)
# 关闭数据库连接
await client.close()
logger.info(f"数据库存储完成:") logger.info(f"数据库存储完成:")
logger.info(f" 总计消息: {stats['total_messages']}") logger.info(f" 总计消息: {stats['total_messages']}")
logger.info(f" 成功存储: {stats['saved']}") logger.info(f" 成功存储: {stats['saved']}")
@@ -392,23 +371,6 @@ class MessageService:
分页查询结果 分页查询结果
""" """
try: 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 = {} query = {}
if msgtype: if msgtype:
@@ -432,8 +394,12 @@ class MessageService:
# 获取总记录数 # 获取总记录数
total_count = await Message.find(query).count() total_count = await Message.find(query).count()
# 关闭数据库连接 # 记录成功日志
await client.close() logger.info(
f"成功查询消息列表: 查询条件={query}, "
f"页码={page}, 每页数量={page_size}, "
f"总记录数={total_count}, 返回记录数={len(messages)}"
)
# 准备响应数据 # 准备响应数据
return { return {
@@ -519,6 +485,7 @@ class MessageService:
"decrypted_failed": 0, "decrypted_failed": 0,
"saved_count": 0, "saved_count": 0,
"duplicates_skipped": 0, "duplicates_skipped": 0,
"seq": seq,
"max_seq": seq, "max_seq": seq,
"next_seq": seq, "next_seq": seq,
}, },
@@ -552,6 +519,7 @@ class MessageService:
"saved_count": db_stats["saved"], "saved_count": db_stats["saved"],
"duplicates_skipped": db_stats["skipped_duplicates"], "duplicates_skipped": db_stats["skipped_duplicates"],
"failed_to_save": db_stats["failed"], "failed_to_save": db_stats["failed"],
"seq": seq,
"max_seq": max_seq, "max_seq": max_seq,
"next_seq": max_seq + 1 if total_fetched > 0 else seq, "next_seq": max_seq + 1 if total_fetched > 0 else seq,
}, },

View File

@@ -1,12 +1,12 @@
{ {
"access_token": "cB51r2D8h73vFEY2WOUOpT7oKOAsTrYzh5kb_xcXTbLsw69U-Wxy9KnM_ealJEef_XdrPyqXcc82IYiJFIFVnwEXy65wF3xQmtm3JxvGq293eSDnNvuxWWRFjoQSf7ani47d_GMAELYoaLcM4S7kFPJxoq7kZz0g2dizOQ0vKiApNgQs3B_nxqm8cAuCgZuSw0JJDPAYZyci4lnRCVe0ww", "access_token": "Lh3zzZFb7iaVkv4zt9VNoZntkS6irpWEpuOctdZcq9g2FWMo5XtNaX7TuFfqoNZePQYce2qRYYMGJxKBxr0hBSRHrLMwDFOiq6_BGiyNFWCs5xvZL1OHVch-FQg2haMjjiFrOPVpB1CGXEoVu5Rm-QFnEZuQrkH292MkF0V9_v-_gJ2EdRgcQ94QTeMnaYnM1bFo3fupAt58MnJ8eoPl8A",
"fetch_time": 1765476407.16114, "fetch_time": 1765768551.7105587,
"expires_in": 7200, "expires_in": 7200,
"expiry_time": 1765482407.16114, "expiry_time": 1765774551.7105587,
"ticket_data": { "ticket_data": {
"ticket": "fkOdR8Y0baUUdG33FhsIfg==", "ticket": "uR5/VwmCPuQPkoAuQbOZcQ==",
"fetch_time": 1765476407.4518044, "fetch_time": 1765768552.0206096,
"expires_in": 7200, "expires_in": 7200,
"expiry_time": 1765483607.4518046 "expiry_time": 1765775752.0206099
} }
} }

View File

@@ -12,6 +12,7 @@ dependencies = [
"httpx>=0.28.1", "httpx>=0.28.1",
"pydantic>=2.12.4", "pydantic>=2.12.4",
"pydantic-settings>=2.12.0", "pydantic-settings>=2.12.0",
"python-dotenv>=1.2.1",
"python-multipart>=0.0.20", "python-multipart>=0.0.20",
"uvicorn>=0.38.0", "uvicorn>=0.38.0",
] ]

View File

@@ -61,7 +61,10 @@ init_config() {
# 日志文件路径 # 日志文件路径
LOG_DIR="${APP_DIR}/logs" 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_NAME"
log_info "应用模块: $APP_MODULE" log_info "应用模块: $APP_MODULE"
@@ -70,6 +73,7 @@ init_config() {
log_info "服务器端口: $APP_PORT" log_info "服务器端口: $APP_PORT"
log_info "工作进程数: $APP_WORKERS" log_info "工作进程数: $APP_WORKERS"
log_info "日志级别: $APP_LOG_LEVEL" log_info "日志级别: $APP_LOG_LEVEL"
log_info "日志文件: $LOG_FILE"
} }
# 检查是否在正确的目录 # 检查是否在正确的目录
@@ -113,6 +117,16 @@ setup_logs() {
# 启动 Unicorn 服务器 # 启动 Unicorn 服务器
start_server() { 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..." log_info "正在启动 $APP_NAME..."
# 检查是否已经在运行 # 检查是否已经在运行
@@ -127,15 +141,25 @@ start_server() {
fi fi
fi fi
# 启动服务器 # 构建启动命令
nohup uv run uvicorn \ local uvicorn_cmd="uv run uvicorn \
"$APP_MODULE" \ $APP_MODULE \
--host "$APP_HOST" \ --host $APP_HOST \
--port "$APP_PORT" \ --port $APP_PORT \
--workers "$APP_WORKERS" \ --workers $APP_WORKERS \
--log-level "$APP_LOG_LEVEL" \ --log-level $APP_LOG_LEVEL \
--access-log \ --access-log"
--reload > "$LOG_FILE" 2>&1 &
# 根据参数添加 --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=$! local server_pid=$!
echo $server_pid > "$PID_FILE" echo $server_pid > "$PID_FILE"
@@ -146,8 +170,9 @@ start_server() {
log_success "$APP_NAME 已启动 (PID: $server_pid)" log_success "$APP_NAME 已启动 (PID: $server_pid)"
log_info "日志文件: $LOG_FILE" log_info "日志文件: $LOG_FILE"
log_info "访问地址: http://$APP_HOST:$APP_PORT" log_info "访问地址: http://$APP_HOST:$APP_PORT"
log_info "热重载: $([ "$enable_reload" = true ] && echo "已启用" || echo "已禁用")"
else else
log_error "$APP_NAME 启动失败" log_error "$APP_NAME 启动失败,请检查日志文件: $LOG_FILE"
rm -f "$PID_FILE" rm -f "$PID_FILE"
exit 1 exit 1
fi fi
@@ -192,7 +217,8 @@ restart_server() {
log_info "正在重启 $APP_NAME..." log_info "正在重启 $APP_NAME..."
stop_server stop_server
sleep 2 sleep 2
start_server # 传递所有参数给 start_server(包括可能的 --reload)
start_server "$@"
} }
# 查看服务器状态 # 查看服务器状态
@@ -216,35 +242,79 @@ status_server() {
# 查看服务器日志 # 查看服务器日志
tail_logs() { tail_logs() {
if [ -f "$LOG_FILE" ]; then local target_log_file="$LOG_FILE"
log_info "显示日志文件最后 50 行: $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 "==========================================" 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 "==========================================" echo "=========================================="
else else
log_warning "日志文件不存在: $LOG_FILE" log_warning "日志文件不存在: $target_log_file"
log_info "可用的日志文件:"
ls -la "${LOG_DIR}/"*.log 2>/dev/null || echo "无日志文件"
fi
fi fi
} }
# 显示帮助信息 # 显示帮助信息
show_help() { show_help() {
echo "用法: $0 {start|stop|restart|status|logs|help}" echo "用法: $0 {start|stop|restart|status|logs|help} [--reload] [日期]"
echo "" echo ""
echo "服务管理命令:" echo "服务管理命令:"
echo " start 启动 $APP_NAME 服务器" echo " start [--reload] 启动 $APP_NAME 服务器(默认禁用热重载)"
echo " stop 停止 $APP_NAME 服务器" echo " stop 停止 $APP_NAME 服务器"
echo " restart 重启 $APP_NAME 服务器" echo " restart [--reload] 重启 $APP_NAME 服务器"
echo " status 查看 $APP_NAME 服务器状态" echo " status 查看 $APP_NAME 服务器状态"
echo " logs 查看 $APP_NAME 服务器日志" echo " logs [YYYY-MM-DD] 查看 $APP_NAME 服务器日志(可指定日期)"
echo " help 显示此帮助信息" echo " help 显示此帮助信息"
echo "" echo ""
echo "可选参数:"
echo " --reload 启用热重载模式(开发环境使用)"
echo " 日期参数 格式: YYYY-MM-DD,用于查看特定日期日志"
echo ""
echo "环境变量配置:" echo "环境变量配置:"
echo " 所有配置都从 .env 文件读取,包括:" echo " 所有配置都从 .env 文件读取,包括:"
echo " APP_NAME, APP_MODULE, APP_PORT, APP_WORKERS, APP_LOG_LEVEL 等" echo " APP_NAME, APP_MODULE, APP_PORT, APP_WORKERS, APP_LOG_LEVEL 等"
echo "" echo ""
echo "日志功能:"
echo " • 日志按日期分割: ${APP_NAME}-YYYY-MM-DD.log"
echo " • 相同日期日志自动追加"
echo " • 支持查看特定日期日志"
echo ""
echo "示例:" echo "示例:"
echo " $0 start # 启动服务器" echo " $0 start # 启动服务器(生产环境推荐)"
echo " $0 status # 查看状态" 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 case "$1" in
start) start)
start_server # 传递剩余参数给 start_server
shift
start_server "$@"
;; ;;
stop) stop)
stop_server stop_server
;; ;;
restart) restart)
restart_server # 重启时也支持 --reload 参数
shift
restart_server "$@"
;; ;;
status) status)
status_server status_server
;; ;;
logs) logs)
tail_logs # 传递可能的日期参数给 tail_logs
shift
tail_logs "$@"
;; ;;
help|--help|-h) help|--help|-h)
show_help show_help

2
backend/uv.lock generated
View File

@@ -58,6 +58,7 @@ dependencies = [
{ name = "httpx" }, { name = "httpx" },
{ name = "pydantic" }, { name = "pydantic" },
{ name = "pydantic-settings" }, { name = "pydantic-settings" },
{ name = "python-dotenv" },
{ name = "python-multipart" }, { name = "python-multipart" },
{ name = "uvicorn" }, { name = "uvicorn" },
] ]
@@ -71,6 +72,7 @@ requires-dist = [
{ name = "httpx", specifier = ">=0.28.1" }, { name = "httpx", specifier = ">=0.28.1" },
{ name = "pydantic", specifier = ">=2.12.4" }, { name = "pydantic", specifier = ">=2.12.4" },
{ name = "pydantic-settings", specifier = ">=2.12.0" }, { name = "pydantic-settings", specifier = ">=2.12.0" },
{ name = "python-dotenv", specifier = ">=1.2.1" },
{ name = "python-multipart", specifier = ">=0.0.20" }, { name = "python-multipart", specifier = ">=0.0.20" },
{ name = "uvicorn", specifier = ">=0.38.0" }, { name = "uvicorn", specifier = ">=0.38.0" },
] ]

2
backstage/.env.sample Normal file
View File

@@ -0,0 +1,2 @@
# API 基础地址
VITE_API_BASE_URL=http://mp.yizhezhe.com/api

View File

@@ -4,6 +4,7 @@ import Dashboard from './pages/Dashboard'
import Messages from './pages/Messages' import Messages from './pages/Messages'
import Settings from './pages/Settings' import Settings from './pages/Settings'
import Login from './pages/Login' import Login from './pages/Login'
import LoadArchive from './pages/LoadArchive'
function App() { function App() {
// 简单的认证检查(实际项目中应该使用更完善的认证方案) // 简单的认证检查(实际项目中应该使用更完善的认证方案)
@@ -17,6 +18,7 @@ function App() {
<Route index element={<Dashboard />} /> <Route index element={<Dashboard />} />
<Route path="messages" element={<Messages />} /> <Route path="messages" element={<Messages />} />
<Route path="settings" element={<Settings />} /> <Route path="settings" element={<Settings />} />
<Route path="load-archive" element={<LoadArchive />} />
</Route> </Route>
</Routes> </Routes>
</Router> </Router>

View File

@@ -44,19 +44,6 @@ const Layout = () => {
))} ))}
</nav> </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> </div>
{/* 主内容区 */} {/* 主内容区 */}

View File

@@ -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 工具
// 模拟延迟

View File

@@ -1,4 +1,7 @@
import { useNavigate } from 'react-router-dom'
const Dashboard = () => { const Dashboard = () => {
const navigate = useNavigate()
const stats = [ const stats = [
{ title: '总消息数', value: '1,234', change: '+12%', icon: '💬', color: 'bg-primary' }, { title: '总消息数', value: '1,234', change: '+12%', icon: '💬', color: 'bg-primary' },
{ title: '今日新增', value: '56', change: '+5%', icon: '📈', color: 'bg-success' }, { title: '今日新增', value: '56', change: '+5%', icon: '📈', color: 'bg-success' },
@@ -75,19 +78,31 @@ const Dashboard = () => {
<div className="card-body"> <div className="card-body">
<h2 className="card-title">快速操作</h2> <h2 className="card-title">快速操作</h2>
<div className="grid grid-cols-2 gap-4"> <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> <span className="mr-2">📥</span>
加载存档 加载存档
</button> </button>
<button className="btn btn-outline"> <button
<span className="mr-2">📊</span> className="btn btn-outline"
查看统计 onClick={() => navigate('/messages')}
</button> >
<button className="btn btn-outline">
<span className="mr-2">🔍</span> <span className="mr-2">🔍</span>
搜索消息 搜索消息
</button> </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> <span className="mr-2">⚙️</span>
系统设置 系统设置
</button> </button>

View 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

View File

@@ -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 Messages = () => {
const navigate = useNavigate()
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [messages, setMessages] = useState([ const [messages, setMessages] = useState([])
{ id: 1, from: '张三', content: '你好,我想了解一下产品详情', time: '2025-12-12 10:30', type: 'text', room: '单聊' }, const [pagination, setPagination] = useState({
{ id: 2, from: '李四', content: '这是会议纪要文件', time: '2025-12-12 09:15', type: 'file', room: '群聊-项目组' }, page: 1,
{ id: 3, from: '王五', content: '产品图片已发送', time: '2025-12-11 16:45', type: 'image', room: '单聊' }, pageSize: 20,
{ id: 4, from: '赵六', content: '关于下周会议安排', time: '2025-12-11 14:20', type: 'text', room: '群聊-管理组' }, total: 0,
{ id: 5, from: '钱七', content: '语音消息', time: '2025-12-10 11:10', type: 'voice', room: '单聊' }, 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) const [selectedMessage, setSelectedMessage] = useState(null)
// 模拟 API 调用 const [isDrawerOpen, setIsDrawerOpen] = useState(false)
setTimeout(() => {
setLoading(false) // 数据格式转换函数
alert('会话存档加载成功!') const formatMessage = (backendMsg) => {
}, 1500) // 处理发送者显示名称
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() e.preventDefault()
// 搜索逻辑 console.log('执行搜索操作')
alert('搜索功能开发中...') 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 ( 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 className="flex flex-col justify-between gap-4 sm:flex-row sm:items-center">
<div> <div>
@@ -51,14 +325,14 @@ const Messages = () => {
</> </>
)} )}
</button> </button>
<button className="btn btn-outline"> {/* <button className="btn btn-outline">
<span className="mr-2">📊</span> <span className="mr-2">📊</span>
查看统计 查看统计
</button> </button>
<button className="btn btn-outline"> <button className="btn btn-outline">
<span className="mr-2">📤</span> <span className="mr-2">📤</span>
导出数据 导出数据
</button> </button> */}
</div> </div>
</div> </div>
@@ -71,24 +345,53 @@ const Messages = () => {
type="text" type="text"
placeholder="搜索消息内容、发送者..." placeholder="搜索消息内容、发送者..."
className="input input-bordered w-full" className="input input-bordered w-full"
value={filters.search}
onChange={(e) => handleFilterChange('search', e.target.value)}
/> />
</div> </div>
<div className="flex gap-2"> <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="">所有消息类型</option>
<option value="text">文本消息</option> <option value="text">文本消息</option>
<option value="image">图片消息</option> <option value="image">图片消息</option>
<option value="file">文件消息</option> <option value="file">文件消息</option>
<option value="voice">语音消息</option> <option value="voice">语音消息</option>
</select> </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="">所有聊天类型</option>
<option value="single">单聊</option> <option value="single">单聊</option>
<option value="group">群聊</option> <option value="group">群聊</option>
</select> </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> <span className="mr-2">🔍</span>
搜索 搜索
</>
)}
</button>
<button
type="button"
className="btn btn-outline"
onClick={() => {
setFilters({ msgtype: '', roomType: '', search: '' })
loadMessages(1)
}}
>
重置
</button> </button>
</div> </div>
</form> </form>
@@ -153,14 +456,13 @@ const Messages = () => {
<td>{message.time}</td> <td>{message.time}</td>
<td> <td>
<div className="flex gap-2"> <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> <span className="mr-1">👁️</span>
查看 查看
</button> </button>
<button className="btn btn-ghost btn-xs">
<span className="mr-1">📋</span>
复制
</button>
</div> </div>
</td> </td>
</tr> </tr>
@@ -172,91 +474,165 @@ const Messages = () => {
{/* 分页 */} {/* 分页 */}
<div className="flex items-center justify-between pt-4"> <div className="flex items-center justify-between pt-4">
<div className="text-sm text-base-content/70"> <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> </div>
{pagination.totalPages > 1 && (
<div className="join"> <div className="join">
<button className="join-item btn btn-sm">«</button> <button
<button className="join-item btn btn-sm btn-active">1</button> className="join-item btn btn-sm"
<button className="join-item btn btn-sm">2</button> onClick={() => handlePageChange(pagination.page - 1)}
<button className="join-item btn btn-sm">3</button> disabled={pagination.page <= 1}
<button className="join-item btn btn-sm">4</button> >
<button className="join-item btn btn-sm">»</button> «
</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> </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>
<div className="card bg-base-100 shadow"> {/* 抽屉侧边栏 */}
<div className="card-body"> <div className="drawer-side">
<h3 className="card-title">加载状态</h3> <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 className="space-y-4">
<div> <div>
<div className="flex justify-between mb-1"> <div className="font-semibold mb-1">发送者:</div>
<span>上次加载时间</span> <div className="flex items-center gap-3">
<span className="font-bold">2025-12-12 10:30</span> <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> </div>
<progress className="progress progress-primary w-full" value="100" max="100"></progress>
</div> </div>
<div> <div>
<div className="flex justify-between mb-1"> <div className="font-bold">{selectedMessage.from}</div>
<span>加载成功率</span> <div className="text-sm text-base-content/70">
<span className="font-bold">98.5%</span> ID: {selectedMessage.rawData.from_user || '未知'}
</div>
<progress className="progress progress-success w-full" value="98.5" max="100"></progress>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div className="card bg-base-100 shadow"> <div className="grid grid-cols-2 gap-4">
<div className="card-body"> <div>
<h3 className="card-title">快速操作</h3> <div className="font-semibold mb-1">消息类型:</div>
<div className="space-y-2"> <span className={`badge ${selectedMessage.type === 'text' ? 'badge-info' : selectedMessage.type === 'image' ? 'badge-success' : selectedMessage.type === 'file' ? 'badge-warning' : 'badge-error'}`}>
<button className="btn btn-outline btn-block justify-start"> {selectedMessage.type === 'text' ? '文本' : selectedMessage.type === 'image' ? '图片' : selectedMessage.type === 'file' ? '文件' : '语音'}
<span className="mr-2">🔄</span> </span>
刷新消息列表 </div>
</button> <div>
<button className="btn btn-outline btn-block justify-start"> <div className="font-semibold mb-1">聊天类型:</div>
<span className="mr-2">🧹</span> <span className={`badge ${selectedMessage.room.includes('群聊') ? 'badge-primary' : 'badge-secondary'}`}>
清理旧数据 {selectedMessage.room}
</button> </span>
<button className="btn btn-outline btn-block justify-start"> </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> <span className="mr-2">📋</span>
批量操作 复制内容
</button> </button>
<button className="btn btn-outline btn-block justify-start"> <button className="btn btn-outline flex-1">
<span className="mr-2">⚙️</span> <span className="mr-2">📤</span>
消息设置 导出
</button> </button>
</div> </div>
</div> </div>
</div> </div>
)}
</div>
</div> </div>
</div> </div>
) )

View File

@@ -1,7 +1,7 @@
import axios from 'axios' import axios from 'axios'
// API 基础配置 // 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 实例 // 创建 axios 实例
const apiClient = axios.create({ const apiClient = axios.create({
@@ -88,6 +88,10 @@ export const messageApi = {
// 获取单个消息详情 // 获取单个消息详情
getMessageById: (msgid) => getMessageById: (msgid) =>
apiClient.get(`/messages/${msgid}`), apiClient.get(`/messages/${msgid}`),
// 获取配置信息
getConfig: () =>
apiClient.get('/messages/config'),
} }
// 认证相关 API // 认证相关 API

View File

@@ -43,8 +43,8 @@ const WorkflowProgress = ({
{stepId === 'context' && stepInfo.data.scene && ( {stepId === 'context' && stepInfo.data.scene && (
<div>场景: {stepInfo.data.scene}</div> <div>场景: {stepInfo.data.scene}</div>
)} )}
{stepId === 'customer' && stepInfo.data.externalUserId && ( {stepId === 'customer' && stepInfo.data.userId && (
<div>客户ID: {stepInfo.data.externalUserId}</div> <div>{stepInfo.data.userId}</div>
)} )}
{stepId === 'tags' && stepInfo.data.tags && ( {stepId === 'tags' && stepInfo.data.tags && (
<div>标签数: {stepInfo.data.tags.length}</div> <div>标签数: {stepInfo.data.tags.length}</div>

View File

@@ -248,16 +248,47 @@ export const useWeCom = () => {
ww.getCurExternalContact({ ww.getCurExternalContact({
success: (res) => { success: (res) => {
clearTimeout(timeoutId); clearTimeout(timeoutId);
console.log('获取当前客户ID成功:', res); console.log('获取当前客户ID成功,原始数据:', res);
// 检查返回的数据结构 // 详细检查返回的数据结构
if (res && res.externalUserId) { console.log('返回数据键名:', Object.keys(res || {}));
resolve(res); console.log('返回数据类型:', typeof res);
} else {
console.warn('获取当前客户ID返回的数据格式异常:', res); // 确保始终返回包含externalUserId字段的对象
// 即使数据格式异常,也返回数据,让上层处理 let result = res || {};
resolve(res || { externalUserId: 'unknown', data: 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) => { fail: (err) => {
clearTimeout(timeoutId); clearTimeout(timeoutId);

View File

@@ -436,11 +436,15 @@ export const workflowService = {
} }
console.log('当前页面支持获取客户ID,entry:', contextInfo.entry); console.log('当前页面支持获取客户ID,entry:', contextInfo.entry);
// alert(`当前页面支持获取客户ID,entry: ${contextInfo.entry}`);
// 在支持获取客户ID的页面,尝试获取真实客户ID // 在支持获取客户ID的页面,尝试获取真实客户ID
customerResult = await weComHook.getCurExternalContact() customerResult = await weComHook.getCurExternalContact()
.then(customerInfo => { .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 { return {
...customerInfo, ...customerInfo,
context: contextInfo, context: contextInfo,
@@ -500,6 +504,7 @@ export const workflowService = {
// 5. 获取会话历史 // 5. 获取会话历史
if (onProgress) onProgress('history', 'loading'); if (onProgress) onProgress('history', 'loading');
let historyResult; let historyResult;
// alert(`5. 获取会话历史开始 userId: ${customerResult.externalUserId}`);
if (!customerResult.externalUserId || customerResult.mock) { if (!customerResult.externalUserId || customerResult.mock) {
console.warn('使用模拟历史数据'); console.warn('使用模拟历史数据');
historyResult = { historyResult = {