diff --git a/.gitignore b/.gitignore index 29a2577..3ed8944 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ dist dist-ssr *.local *.tar +*.pid # Editor directories and files .vscode/* diff --git a/backend/.chat-glm.pid b/backend/.chat-glm.pid deleted file mode 100644 index 06a054d..0000000 --- a/backend/.chat-glm.pid +++ /dev/null @@ -1 +0,0 @@ -285640 diff --git a/backend/app/main.py b/backend/app/main.py index 9c0d786..c10ed51 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -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() diff --git a/backend/app/routes/glm_routes.py b/backend/app/routes/glm_routes.py index 819255d..c78d9db 100644 --- a/backend/app/routes/glm_routes.py +++ b/backend/app/routes/glm_routes.py @@ -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: - 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)}") + 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_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)) diff --git a/backend/app/routes/message_routes.py b/backend/app/routes/message_routes.py index a476dbc..e45564b 100644 --- a/backend/app/routes/message_routes.py +++ b/backend/app/routes/message_routes.py @@ -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("消息服务已关闭") diff --git a/backend/app/services/glm_service.py b/backend/app/services/glm_service.py index 9a31390..5b22adc 100644 --- a/backend/app/services/glm_service.py +++ b/backend/app/services/glm_service.py @@ -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,33 +43,153 @@ 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: + 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() + 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}") + logger.info(f"重新连接后加载激活角色: {active_role.name}") return active_role # 2. 尝试获取默认角色 @@ -64,11 +197,11 @@ class GLMService: if default_role: default_role.is_active = True await default_role.save() - logger.info(f"加载默认角色并激活: {default_role.name}") + logger.info(f"重新连接后加载默认角色并激活: {default_role.name}") return default_role # 3. 创建默认角色 - logger.info("创建默认角色配置") + 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服务已关闭") diff --git a/backend/app/services/message_service.py b/backend/app/services/message_service.py index 1d2923e..cf8749a 100644 --- a/backend/app/services/message_service.py +++ b/backend/app/services/message_service.py @@ -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, }, diff --git a/backend/data.json b/backend/data.json index b71e22c..9461441 100644 --- a/backend/data.json +++ b/backend/data.json @@ -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 } } \ No newline at end of file diff --git a/backend/pyproject.toml b/backend/pyproject.toml index cd4c2c7..724b341 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -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", ] diff --git a/backend/serv.sh b/backend/serv.sh index dcbfa0a..9c04c1a 100755 --- a/backend/serv.sh +++ b/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 - log_warning "日志文件不存在: $LOG_FILE" + # 尝试查找最新的日志文件 + 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 "日志文件不存在: $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 " stop 停止 $APP_NAME 服务器" - echo " restart 重启 $APP_NAME 服务器" - echo " status 查看 $APP_NAME 服务器状态" - echo " logs 查看 $APP_NAME 服务器日志" - echo " help 显示此帮助信息" + echo " start [--reload] 启动 $APP_NAME 服务器(默认禁用热重载)" + echo " stop 停止 $APP_NAME 服务器" + echo " restart [--reload] 重启 $APP_NAME 服务器" + echo " status 查看 $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 diff --git a/backend/uv.lock b/backend/uv.lock index 7c43726..bbc09cc 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -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" }, ] diff --git a/backstage/.env.sample b/backstage/.env.sample new file mode 100644 index 0000000..66ad3a8 --- /dev/null +++ b/backstage/.env.sample @@ -0,0 +1,2 @@ +# API 基础地址 +VITE_API_BASE_URL=http://mp.yizhezhe.com/api \ No newline at end of file diff --git a/backstage/src/App.jsx b/backstage/src/App.jsx index bf7bd53..3a63d11 100644 --- a/backstage/src/App.jsx +++ b/backstage/src/App.jsx @@ -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() { } /> } /> } /> + } /> diff --git a/backstage/src/components/layout/Layout.jsx b/backstage/src/components/layout/Layout.jsx index 43a9694..b4b1c98 100644 --- a/backstage/src/components/layout/Layout.jsx +++ b/backstage/src/components/layout/Layout.jsx @@ -44,19 +44,6 @@ const Layout = () => { ))} -
-
-
-
- AD -
-
-
-

管理员

-

admin@example.com

-
-
-
{/* 主内容区 */} diff --git a/backstage/src/components/ui/DocumentationViewer.jsx b/backstage/src/components/ui/DocumentationViewer.jsx deleted file mode 100644 index f856e45..0000000 --- a/backstage/src/components/ui/DocumentationViewer.jsx +++ /dev/null @@ -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 工具 - - // 模拟延迟 diff --git a/backstage/src/pages/Dashboard.jsx b/backstage/src/pages/Dashboard.jsx index 4062a4b..2ebdb9e 100644 --- a/backstage/src/pages/Dashboard.jsx +++ b/backstage/src/pages/Dashboard.jsx @@ -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 = () => {

快速操作

- - - - + diff --git a/backstage/src/pages/LoadArchive.jsx b/backstage/src/pages/LoadArchive.jsx new file mode 100644 index 0000000..f13676a --- /dev/null +++ b/backstage/src/pages/LoadArchive.jsx @@ -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 ( +
+ {/* 页面标题和操作 */} +
+
+

加载会话存档

+

从企业微信加载会话存档数据到数据库

+
+
+ + + +
+
+ + {/* 整体状态 */} +
+
+
+
+

加载状态

+

+ {progress.status === 'idle' && '等待开始加载...'} + {progress.status === 'loading' && '正在加载会话存档...'} + {progress.status === 'success' && '加载完成!'} + {progress.status === 'error' && '加载失败'} +

+
+
+
开始时间
+
{formatTime(progress.startTime)}
+
+
+
结束时间
+
{formatTime(progress.endTime)}
+
+
+
总耗时
+
{formatDuration(progress.totalDuration)}
+
+
+ + {progress.error && ( +
+ 错误: {progress.error} +
+ )} +
+
+ + {/* 步骤流程 */} +
+
+

加载步骤流程

+
+ {progress.steps.map((step, index) => ( +
+
+
+
+ {index + 1} +
+
+

{step.name}

+
+ + {getStatusIcon(step.status)} {step.status === 'pending' ? '等待中' : + step.status === 'running' ? '进行中' : + step.status === 'success' ? '成功' : '失败'} + + {step.startTime && ( + + 开始: {formatTime(step.startTime)} + + )} + {step.endTime && ( + + 结束: {formatTime(step.endTime)} + + )} + {step.duration && ( + + 耗时: {formatDuration(step.duration)} + + )} +
+
+
+
+ + {step.details && ( +
+
+
{step.details}
+
+
+ )} +
+ ))} +
+
+
+ + {/* 结果详情 */} + {progress.result && ( +
+
+

加载结果详情

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
指标数值说明
拉取消息总数{progress.result.total_fetched || 0}从企业微信拉取的消息总数
加密消息数量{progress.result.encrypted_count || 0}需要解密的加密消息数量
成功解密{progress.result.decrypted_success || 0}成功解密的加密消息数量
解密失败{progress.result.decrypted_failed || 0}解密失败的消息数量
成功存储{progress.result.saved_count || 0}成功保存到数据库的消息数量
跳过重复{progress.result.duplicates_skipped || 0}数据库中已存在,跳过存储的消息数量
存储失败{progress.result.failed_to_save || 0}存储到数据库失败的消息数量
最大seq值{progress.result.max_seq || 0}本次拉取的最大seq值,用于下次拉取
建议下次seq{progress.result.next_seq || (progress.result.max_seq || 0) + 1}下次拉取建议使用的seq值
+
+
+
+ )} +
+ ) +} + +export default LoadArchive diff --git a/backstage/src/pages/Messages.jsx b/backstage/src/pages/Messages.jsx index 5edf07b..869d7d6 100644 --- a/backstage/src/pages/Messages.jsx +++ b/backstage/src/pages/Messages.jsx @@ -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 + } + } + + // 处理消息内容 + 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 // 保留原始数据供详细查看使用 + } } - const handleSearch = (e) => { + // 加载消息列表 - 使用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 ( +
+
消息内容:
+
+ {rawData.text?.content || selectedMessage.content} +
+
+ ) + case 'image': + return ( +
+
图片消息:
+
+ {rawData.image?.md5sum ? ( +
+
图片MD5: {rawData.image.md5sum}
+
图片大小: {rawData.image.filesize || '未知'} bytes
+
+ (图片文件需要从企业微信服务器下载) +
+
+ ) : ( +
无图片详细信息
+ )} +
+
+ ) + case 'file': + return ( +
+
文件消息:
+
+ {rawData.file ? ( +
+
文件名: {rawData.file.filename || '未知'}
+
文件大小: {rawData.file.filesize || '未知'} bytes
+
文件扩展名: {rawData.file.fileext || '未知'}
+
+ (文件需要从企业微信服务器下载) +
+
+ ) : ( +
无文件详细信息
+ )} +
+
+ ) + case 'voice': + return ( +
+
语音消息:
+
+ {rawData.voice ? ( +
+
语音时长: {rawData.voice.play_length || '未知'} 秒
+
文件大小: {rawData.voice.filesize || '未知'} bytes
+
+ (语音文件需要从企业微信服务器下载) +
+
+ ) : ( +
无语音详细信息
+ )} +
+
+ ) + default: + return ( +
+
消息内容:
+
+ {selectedMessage.content} +
+
+ ) + } } return ( -
+
+ {}} // 只读,通过状态控制 + /> +
{/* 页面标题和操作 */}
@@ -51,14 +325,14 @@ const Messages = () => { )} - + */}
@@ -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)} />
- handleFilterChange('msgtype', e.target.value)} + > - handleFilterChange('roomType', e.target.value)} + > - +
@@ -153,14 +456,13 @@ const Messages = () => { {message.time}
- -
@@ -172,90 +474,164 @@ const Messages = () => { {/* 分页 */}
- 显示 1-5 条,共 1234 条消息 -
-
- - - - - - + {messages.length > 0 ? ( + `显示 ${(pagination.page - 1) * pagination.pageSize + 1}-${Math.min(pagination.page * pagination.pageSize, pagination.total)} 条,共 ${pagination.total} 条消息` + ) : ( + '暂无消息数据' + )}
+ {pagination.totalPages > 1 && ( +
+ + {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 ( + + ) + })} + +
+ )}
- {/* 统计信息 */} -
-
-
-

今日消息统计

-
-
- 文本消息 - 45 条 +
+ + {/* 抽屉侧边栏 */} +
+ +
+ {selectedMessage && ( +
+
+

消息详情

+
-
- 图片消息 - 12 条 -
-
- 文件消息 - 8 条 -
-
- 语音消息 - 3 条 -
-
-
-
- -
-
-

加载状态

-
-
-
- 上次加载时间 - 2025-12-12 10:30 + + {/* 消息基本信息 */} +
+
+
发送者:
+
+
+
+ {selectedMessage.from.charAt(0)} +
+
+
+
{selectedMessage.from}
+
+ ID: {selectedMessage.rawData.from_user || '未知'} +
+
+
- -
-
-
- 加载成功率 - 98.5% + +
+
+
消息类型:
+ + {selectedMessage.type === 'text' ? '文本' : selectedMessage.type === 'image' ? '图片' : selectedMessage.type === 'file' ? '文件' : '语音'} + +
+
+
聊天类型:
+ + {selectedMessage.room} + +
+
+ +
+
发送时间:
+
{selectedMessage.time}
+
+ 原始时间戳: {selectedMessage.rawData.msgtime || '未知'} +
+
+ + {selectedMessage.rawData.tolist && selectedMessage.rawData.tolist.length > 0 && ( +
+
接收者:
+
+ {selectedMessage.rawData.tolist.map((to, index) => ( +
+ {to} +
+ ))} +
+
+ )} + + {/* 消息内容 */} + {renderMessageContent()} + + {/* 原始数据 */} +
+
+ +
+ 查看原始数据 +
+
+
+                        {JSON.stringify(selectedMessage.rawData, null, 2)}
+                      
+
+
+
+ + {/* 操作按钮 */} +
+ +
-
-
-
- -
-
-

快速操作

-
- - - - -
-
+ )}
diff --git a/backstage/src/services/api.js b/backstage/src/services/api.js index 897dab1..57c7c67 100644 --- a/backstage/src/services/api.js +++ b/backstage/src/services/api.js @@ -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 diff --git a/frontend/src/components/WorkflowProgress.jsx b/frontend/src/components/WorkflowProgress.jsx index 2b9c33d..07829dd 100644 --- a/frontend/src/components/WorkflowProgress.jsx +++ b/frontend/src/components/WorkflowProgress.jsx @@ -43,8 +43,8 @@ const WorkflowProgress = ({ {stepId === 'context' && stepInfo.data.scene && (
场景: {stepInfo.data.scene}
)} - {stepId === 'customer' && stepInfo.data.externalUserId && ( -
客户ID: {stepInfo.data.externalUserId}
+ {stepId === 'customer' && stepInfo.data.userId && ( +
{stepInfo.data.userId}
)} {stepId === 'tags' && stepInfo.data.tags && (
标签数: {stepInfo.data.tags.length}
diff --git a/frontend/src/hooks/useWeCom.js b/frontend/src/hooks/useWeCom.js index 9c887c6..add50b5 100644 --- a/frontend/src/hooks/useWeCom.js +++ b/frontend/src/hooks/useWeCom.js @@ -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); diff --git a/frontend/src/services/wecomService.js b/frontend/src/services/wecomService.js index 65ff027..5265ab9 100644 --- a/frontend/src/services/wecomService.js +++ b/frontend/src/services/wecomService.js @@ -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 = {