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() {
管理员
-admin@example.com
-