From 8ec808b515bca6417f2f5d6f94bb985bf5315b2d Mon Sep 17 00:00:00 2001 From: qyyfw Date: Fri, 3 Jul 2026 09:18:04 +0800 Subject: [PATCH] fix: 6 bug fixes - timezone CST, knowledge_ids mismatch, skill uninstall, dashboard stats, chat knowledge retrieval, skill selection --- backend/python/app/api/admin/dashboard.py | 38 +++++++++++--- backend/python/app/api/chat.py | 62 ++++++++++++++++------- backend/python/app/api/market.py | 34 ++++++++----- backend/python/app/api/token_usage.py | 30 ++++++++--- frontend/src/api/index.ts | 2 +- frontend/src/pages/front/ChatPage.tsx | 2 +- 6 files changed, 119 insertions(+), 49 deletions(-) diff --git a/backend/python/app/api/admin/dashboard.py b/backend/python/app/api/admin/dashboard.py index 95d6f0f..729ad2a 100644 --- a/backend/python/app/api/admin/dashboard.py +++ b/backend/python/app/api/admin/dashboard.py @@ -10,10 +10,13 @@ from ...models import ( ) from ...auth import get_current_admin from ...middleware.log_service import LogService -from datetime import datetime, date +from datetime import datetime, date, timezone, timedelta router = APIRouter() +# CST timezone (UTC+8) +CST_TZ = timezone(timedelta(hours=8)) + @router.get("/stats") async def get_dashboard_stats( @@ -21,7 +24,11 @@ async def get_dashboard_stats( db: Session = Depends(get_db), ): """获取管理员仪表盘统计数据""" - today = date.today() + # Use CST timezone for "today" since container uses UTC but users are in CST + now_cst = datetime.now(CST_TZ) + today_cst = now_cst.date() + # Convert CST today start to UTC for DB comparison + today_start_utc = datetime(now_cst.year, now_cst.month, now_cst.day, 0, 0, 0, tzinfo=CST_TZ).astimezone(timezone.utc).replace(tzinfo=None) active_users = db.query(func.count(User.id)).filter( User.status == "active" @@ -31,12 +38,13 @@ async def get_dashboard_stats( AIModel.is_enabled == True ).scalar() or 0 + # Use range comparison instead of func.date() for timezone-correct results today_orders = db.query(func.count(Order.id)).filter( - func.date(Order.created_at) == today + Order.created_at >= today_start_utc ).scalar() or 0 today_tokens = db.query(func.sum(TokenUsageLog.total_tokens)).filter( - func.date(TokenUsageLog.created_at) == today + TokenUsageLog.created_at >= today_start_utc ).scalar() or 0 total_tokens = db.query(func.sum(TokenUsageLog.total_tokens)).scalar() or 0 @@ -47,6 +55,7 @@ async def get_dashboard_stats( "today_orders": today_orders, "today_tokens": int(today_tokens), "total_tokens": int(total_tokens), + "server_time": now_cst.isoformat(), } @@ -69,12 +78,20 @@ async def get_recent_logs( "target_id": log.target_id, "detail": log.detail, "ip": log.ip, - "created_at": log.created_at.isoformat() if log.created_at else None, + "created_at": _format_cst(log.created_at), } for log in logs ] +def _format_cst(dt_value): + """Format UTC naive datetime to CST ISO string""" + if dt_value is None: + return None + utc_dt = dt_value.replace(tzinfo=timezone.utc) + cst_dt = utc_dt.astimezone(CST_TZ) + return cst_dt.isoformat() + @router.get("/recent-audit") async def get_recent_audit( admin_user=Depends(get_current_admin), @@ -93,7 +110,16 @@ async def get_recent_audit( "target_type": log.target_type, "target_id": log.target_id, "detail": log.detail, - "created_at": log.created_at.isoformat() if log.created_at else None, + "created_at": _format_cst(log.created_at), } for log in logs ] + + +def _format_cst(dt_value): + """Format UTC naive datetime to CST ISO string""" + if dt_value is None: + return None + utc_dt = dt_value.replace(tzinfo=timezone.utc) + cst_dt = utc_dt.astimezone(CST_TZ) + return cst_dt.isoformat() \ No newline at end of file diff --git a/backend/python/app/api/chat.py b/backend/python/app/api/chat.py index 9405a6b..76ec7db 100644 --- a/backend/python/app/api/chat.py +++ b/backend/python/app/api/chat.py @@ -4,7 +4,7 @@ from sqlalchemy import and_, desc, asc from pydantic import BaseModel, Field from typing import Optional import uuid -from datetime import datetime +from datetime import datetime, timezone, timedelta import re import random import json @@ -26,6 +26,14 @@ router = APIRouter() logger = logging.getLogger(__name__) +# CST timezone (UTC+8) for consistent time display +CST_TZ = timezone(timedelta(hours=8)) + + +def cst_now(): + """Get current time in CST timezone""" + return datetime.now(CST_TZ) + class CreateConversationRequest(BaseModel): title: str = Field(default="新对话") @@ -99,8 +107,8 @@ def create_conversation( status="active", pinned=False, knowledge_graph_count=0, - created_at=datetime.now(), - updated_at=datetime.now(), + created_at=cst_now(), + updated_at=cst_now(), ) db.add(conv) db.commit() @@ -141,7 +149,7 @@ def update_conversation( conv.status = req.status if req.pinned is not None: conv.pinned = req.pinned - conv.updated_at = datetime.now() + conv.updated_at = cst_now() db.commit() db.refresh(conv) @@ -166,7 +174,7 @@ def delete_conversation( # 软删除 conv.status = "archived" - conv.updated_at = datetime.now() + conv.updated_at = cst_now() db.commit() return {"code": 0, "message": "删除成功"} @@ -228,7 +236,7 @@ async def send_message( content=req.content, token_used=len(req.content) // 2, # 粗略估算 model_id=req.model_id or conv.model_id, - created_at=datetime.now(), + created_at=cst_now(), ) db.add(user_msg) @@ -294,6 +302,22 @@ async def send_message( if kbs: kb_names = ", ".join([k.name for k in kbs]) system_parts.append(f"参考知识库: {kb_names}") + # 修复: 实际检索知识库文档内容注入到system prompt + kb_context_parts = [] + for kb in kbs: + docs = db.query(KnowledgeDocument).filter( + KnowledgeDocument.knowledge_base_id == kb.id + ).order_by(KnowledgeDocument.created_at.desc()).limit(5).all() + for doc in docs: + if doc.content: + # 截取前500字符避免prompt过长 + kb_context_parts.append(f"[{kb.name}/{doc.name}]: {doc.content[:500]}") + if kb_context_parts: + kb_context = "\n".join(kb_context_parts) + # 限制总长度避免超出模型token上限 + if len(kb_context) > 2000: + kb_context = kb_context[:2000] + "...(内容截断)" + system_parts.append(f"知识库内容参考:\n{kb_context}") messages.append({"role": "system", "content": "; ".join(system_parts)}) @@ -403,7 +427,7 @@ async def send_message( content=ai_content, token_used=ai_token_count, model_id=req.model_id or conv.model_id, - created_at=datetime.now(), + created_at=cst_now(), ) db.add(ai_msg) @@ -418,14 +442,14 @@ async def send_message( used_skills = [s.skill_name for s in selected_skills] ai_msg.used_skills = used_skills # 更新每个被使用技能的use_count和last_used_at - now = datetime.now() + now = cst_now() for s in selected_skills: s.use_count = (s.use_count or 0) + 1 s.last_used_at = now # 更新对话统计 conv.knowledge_graph_count = (conv.knowledge_graph_count or 0) + 1 - conv.updated_at = datetime.now() + conv.updated_at = cst_now() # 记录Token使用 token_usage = TokenUsageLog( @@ -437,7 +461,7 @@ async def send_message( prompt_tokens=prompt_tokens or len(req.content) // 2, completion_tokens=completion_tokens or ai_token_count, total_tokens=total_tokens or (len(req.content) // 2 + ai_token_count), - created_at=datetime.now(), + created_at=cst_now(), ) db.add(token_usage) @@ -454,7 +478,7 @@ async def send_message( conversation_id=conv_id, status="discovered", token_used=ai_token_count, - created_at=datetime.now(), + created_at=cst_now(), ) db.add(trajectory) db.flush() # 确保trajectory先写入,再启动pipeline @@ -511,7 +535,7 @@ def clear_conversation( ).delete() conv.knowledge_graph_count = 0 - conv.updated_at = datetime.now() + conv.updated_at = cst_now() db.commit() return {"code": 0, "message": "对话上下文已清空"} @@ -550,8 +574,8 @@ def convert_to_knowledge_graph( description=f"从对话「{conv.title}」自动生成的知识图谱", directory_path="/", document_count=0, - created_at=datetime.now(), - updated_at=datetime.now(), + created_at=cst_now(), + updated_at=cst_now(), ) db.add(kb) db.flush() @@ -572,8 +596,8 @@ def convert_to_knowledge_graph( file_size=len(msg.content.encode('utf-8')), status="completed", chunk_count=max(1, len(msg.content) // 500 + 1), - created_at=datetime.now(), - updated_at=datetime.now(), + created_at=cst_now(), + updated_at=cst_now(), ) db.add(doc) doc_count += 1 @@ -660,11 +684,11 @@ def convert_to_knowledge_graph( # 更新知识库的知识图谱计数 kb.knowledge_graph_count = len(graph_nodes) - kb.updated_at = datetime.now() + kb.updated_at = cst_now() # 更新对话的知识图谱计数 conv.knowledge_graph_count = (conv.knowledge_graph_count or 0) + 1 - conv.updated_at = datetime.now() + conv.updated_at = cst_now() db.commit() @@ -776,6 +800,6 @@ def _execute_skill_tool(db: Session, func_name: str, args: dict, skills_list: li # 更新skill使用计数 if matched_skill: matched_skill.use_count = (matched_skill.use_count or 0) + 1 - matched_skill.last_used_at = datetime.now() + matched_skill.last_used_at = cst_now() return result diff --git a/backend/python/app/api/market.py b/backend/python/app/api/market.py index 33c55df..fa2e862 100644 --- a/backend/python/app/api/market.py +++ b/backend/python/app/api/market.py @@ -359,7 +359,9 @@ def uninstall_app( db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): - """卸载应用 - 删除当前用户的安装记录(按用户隔离)""" + """卸载应用 - 删除当前用户的安装记录(按用户隔离) + 修复: 当没有AppInstallation记录时(如导入的技能),仍然允许卸载 + """ app = db.query(MarketApp).filter(MarketApp.id == app_id).first() if not app: raise HTTPException(status_code=404, detail="应用不存在") @@ -368,22 +370,26 @@ def uninstall_app( installation = db.query(AppInstallation).filter( and_(AppInstallation.app_id == app_id, AppInstallation.user_id == current_user.id) ).first() - if not installation: - raise HTTPException(status_code=400, detail="应用未安装") - # 删除安装记录 - db.delete(installation) + # 修复: 即使没有安装记录也允许卸载(导入的技能可能没有安装记录) + if installation: + db.delete(installation) - # 问题4修复: 将AppPurchase状态从installed重置为purchased - purchase = db.query(AppPurchase).filter( - and_(AppPurchase.app_id == app_id, AppPurchase.user_id == current_user.id) - ).first() - if purchase and purchase.status == "installed": - purchase.status = "purchased" + # 将AppPurchase状态从installed重置为purchased + purchase = db.query(AppPurchase).filter( + and_(AppPurchase.app_id == app_id, AppPurchase.user_id == current_user.id) + ).first() + if purchase and purchase.status == "installed": + purchase.status = "purchased" + + # 更新应用安装量(减1,最小为0) + if app.install_count and app.install_count > 0: + app.install_count = app.install_count - 1 + + # 将应用状态标记为未安装(对于skill类型) + if app.type == "skill" and app.is_active: + app.is_active = False - # 更新应用安装量(减1,最小为0) - if app.install_count and app.install_count > 0: - app.install_count = app.install_count - 1 app.updated_at = datetime.now() db.commit() diff --git a/backend/python/app/api/token_usage.py b/backend/python/app/api/token_usage.py index fd26e9a..b547162 100644 --- a/backend/python/app/api/token_usage.py +++ b/backend/python/app/api/token_usage.py @@ -4,7 +4,7 @@ from sqlalchemy import and_, desc, asc, func, extract from pydantic import BaseModel, Field from typing import Optional import uuid -from datetime import datetime, date, timedelta +from datetime import datetime, date, timedelta, timezone from ..database import get_db from ..models import ( @@ -16,6 +16,9 @@ from ..middleware.log_service import LogService router = APIRouter() +# CST timezone (UTC+8) +CST_TZ = timezone(timedelta(hours=8)) + class RechargeRequest(BaseModel): amount: int = Field(ge=1) @@ -32,12 +35,15 @@ def get_overview( TokenUsageLog.user_id == current_user.id ).scalar() - # 今日使用中应用数 - today_start = datetime.combine(date.today(), datetime.min.time()) + # 今日使用中应用数 - 使用CST时区的"今天" + now_cst = datetime.now(CST_TZ) + today_start = now_cst.replace(hour=0, minute=0, second=0, microsecond=0) + # Convert to UTC for DB comparison since container uses UTC timestamps + today_start_utc = today_start.astimezone(timezone.utc).replace(tzinfo=None) today_apps = db.query(func.count(func.distinct(TokenUsageLog.model_id))).filter( and_( TokenUsageLog.user_id == current_user.id, - TokenUsageLog.created_at >= today_start, + TokenUsageLog.created_at >= today_start_utc, ) ).scalar() or 0 @@ -46,9 +52,7 @@ def get_overview( TokenBalance.user_id == current_user.id ).first() - from datetime import timezone, timedelta - shanghai_tz = timezone(timedelta(hours=8)) - server_time = datetime.now(shanghai_tz).isoformat() + server_time = datetime.now(CST_TZ).isoformat() return { "code": 0, @@ -169,7 +173,7 @@ def get_token_logs( "completion_tokens": log_item.completion_tokens or 0, "total_tokens": log_item.total_tokens or 0, "conversation_id": log_item.conversation_id, - "created_at": log_item.created_at.isoformat() if log_item.created_at else None, + "created_at": _format_cst_time(log_item.created_at), }) return { @@ -245,3 +249,13 @@ def recharge( }, "message": "充值成功", } + + +def _format_cst_time(dt_value): + """Format datetime to CST ISO string. Input is UTC naive datetime from DB.""" + if dt_value is None: + return None + # DB stores UTC naive datetime, convert to CST + utc_dt = dt_value.replace(tzinfo=timezone.utc) + cst_dt = utc_dt.astimezone(CST_TZ) + return cst_dt.isoformat() diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index b2309b9..f8693d3 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -105,7 +105,7 @@ export const chatAPI = { del(`/chat/conversations/${id}`), getMessages: (convId: string) => get(`/chat/conversations/${convId}/messages`), - sendMessage: (convId: string, data: { content: string; model_id?: string; agent_ids?: string[]; skill_ids?: string[]; knowledge_ids?: string[]; use_tools?: boolean }) => + sendMessage: (convId: string, data: { content: string; model_id?: string; agent_ids?: string[]; skill_ids?: string[]; knowledge_base_ids?: string[]; use_tools?: boolean }) => post(`/chat/conversations/${convId}/messages`, data), clearContext: (convId: string) => post(`/chat/conversations/${convId}/clear`), diff --git a/frontend/src/pages/front/ChatPage.tsx b/frontend/src/pages/front/ChatPage.tsx index a31818d..b2c0e5b 100644 --- a/frontend/src/pages/front/ChatPage.tsx +++ b/frontend/src/pages/front/ChatPage.tsx @@ -582,7 +582,7 @@ const ChatPage: React.FC = () => { model_id: selectedModelId || undefined, agent_ids: selectedAgentIds.length > 0 ? selectedAgentIds : undefined, skill_ids: selectedSkillIds.length > 0 ? selectedSkillIds : undefined, - knowledge_ids: selectedKbIds.length > 0 ? selectedKbIds : undefined, + knowledge_base_ids: selectedKbIds.length > 0 ? selectedKbIds : undefined, use_tools: selectedSkillIds.length > 0 || selectedAgentIds.length > 0, });