fix: 6 bug fixes - timezone CST, knowledge_ids mismatch, skill uninstall, dashboard stats, chat knowledge retrieval, skill selection

This commit is contained in:
qyyfw committed 2026-07-03 09:18:04 +08:00
1 parent f7ab61bd9d
commit 8ec808b515
6 files changed
+110 -40

No files matched your search

+32 -6
View File
@@ -10,10 +10,13 @@ from ...models import (
) )
from ...auth import get_current_admin from ...auth import get_current_admin
from ...middleware.log_service import LogService from ...middleware.log_service import LogService
from datetime import datetime, date from datetime import datetime, date, timezone, timedelta
router = APIRouter() router = APIRouter()
# CST timezone (UTC+8)
CST_TZ = timezone(timedelta(hours=8))
@router.get("/stats") @router.get("/stats")
async def get_dashboard_stats( async def get_dashboard_stats(
@@ -21,7 +24,11 @@ async def get_dashboard_stats(
db: Session = Depends(get_db), 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( active_users = db.query(func.count(User.id)).filter(
User.status == "active" User.status == "active"
@@ -31,12 +38,13 @@ async def get_dashboard_stats(
AIModel.is_enabled == True AIModel.is_enabled == True
).scalar() or 0 ).scalar() or 0
# Use range comparison instead of func.date() for timezone-correct results
today_orders = db.query(func.count(Order.id)).filter( today_orders = db.query(func.count(Order.id)).filter(
func.date(Order.created_at) == today Order.created_at >= today_start_utc
).scalar() or 0 ).scalar() or 0
today_tokens = db.query(func.sum(TokenUsageLog.total_tokens)).filter( 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 ).scalar() or 0
total_tokens = db.query(func.sum(TokenUsageLog.total_tokens)).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_orders": today_orders,
"today_tokens": int(today_tokens), "today_tokens": int(today_tokens),
"total_tokens": int(total_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, "target_id": log.target_id,
"detail": log.detail, "detail": log.detail,
"ip": log.ip, "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 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") @router.get("/recent-audit")
async def get_recent_audit( async def get_recent_audit(
admin_user=Depends(get_current_admin), admin_user=Depends(get_current_admin),
@@ -93,7 +110,16 @@ async def get_recent_audit(
"target_type": log.target_type, "target_type": log.target_type,
"target_id": log.target_id, "target_id": log.target_id,
"detail": log.detail, "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 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()
+43 -19
View File
@@ -4,7 +4,7 @@ from sqlalchemy import and_, desc, asc
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from typing import Optional from typing import Optional
import uuid import uuid
from datetime import datetime from datetime import datetime, timezone, timedelta
import re import re
import random import random
import json import json
@@ -26,6 +26,14 @@ router = APIRouter()
logger = logging.getLogger(__name__) 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): class CreateConversationRequest(BaseModel):
title: str = Field(default="新对话") title: str = Field(default="新对话")
@@ -99,8 +107,8 @@ def create_conversation(
status="active", status="active",
pinned=False, pinned=False,
knowledge_graph_count=0, knowledge_graph_count=0,
created_at=datetime.now(), created_at=cst_now(),
updated_at=datetime.now(), updated_at=cst_now(),
) )
db.add(conv) db.add(conv)
db.commit() db.commit()
@@ -141,7 +149,7 @@ def update_conversation(
conv.status = req.status conv.status = req.status
if req.pinned is not None: if req.pinned is not None:
conv.pinned = req.pinned conv.pinned = req.pinned
conv.updated_at = datetime.now() conv.updated_at = cst_now()
db.commit() db.commit()
db.refresh(conv) db.refresh(conv)
@@ -166,7 +174,7 @@ def delete_conversation(
# 软删除 # 软删除
conv.status = "archived" conv.status = "archived"
conv.updated_at = datetime.now() conv.updated_at = cst_now()
db.commit() db.commit()
return {"code": 0, "message": "删除成功"} return {"code": 0, "message": "删除成功"}
@@ -228,7 +236,7 @@ async def send_message(
content=req.content, content=req.content,
token_used=len(req.content) // 2, # 粗略估算 token_used=len(req.content) // 2, # 粗略估算
model_id=req.model_id or conv.model_id, model_id=req.model_id or conv.model_id,
created_at=datetime.now(), created_at=cst_now(),
) )
db.add(user_msg) db.add(user_msg)
@@ -294,6 +302,22 @@ async def send_message(
if kbs: if kbs:
kb_names = ", ".join([k.name for k in kbs]) kb_names = ", ".join([k.name for k in kbs])
system_parts.append(f"参考知识库: {kb_names}") 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)}) messages.append({"role": "system", "content": "; ".join(system_parts)})
@@ -403,7 +427,7 @@ async def send_message(
content=ai_content, content=ai_content,
token_used=ai_token_count, token_used=ai_token_count,
model_id=req.model_id or conv.model_id, model_id=req.model_id or conv.model_id,
created_at=datetime.now(), created_at=cst_now(),
) )
db.add(ai_msg) db.add(ai_msg)
@@ -418,14 +442,14 @@ async def send_message(
used_skills = [s.skill_name for s in selected_skills] used_skills = [s.skill_name for s in selected_skills]
ai_msg.used_skills = used_skills ai_msg.used_skills = used_skills
# 更新每个被使用技能的use_count和last_used_at # 更新每个被使用技能的use_count和last_used_at
now = datetime.now() now = cst_now()
for s in selected_skills: for s in selected_skills:
s.use_count = (s.use_count or 0) + 1 s.use_count = (s.use_count or 0) + 1
s.last_used_at = now s.last_used_at = now
# 更新对话统计 # 更新对话统计
conv.knowledge_graph_count = (conv.knowledge_graph_count or 0) + 1 conv.knowledge_graph_count = (conv.knowledge_graph_count or 0) + 1
conv.updated_at = datetime.now() conv.updated_at = cst_now()
# 记录Token使用 # 记录Token使用
token_usage = TokenUsageLog( token_usage = TokenUsageLog(
@@ -437,7 +461,7 @@ async def send_message(
prompt_tokens=prompt_tokens or len(req.content) // 2, prompt_tokens=prompt_tokens or len(req.content) // 2,
completion_tokens=completion_tokens or ai_token_count, completion_tokens=completion_tokens or ai_token_count,
total_tokens=total_tokens or (len(req.content) // 2 + 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) db.add(token_usage)
@@ -454,7 +478,7 @@ async def send_message(
conversation_id=conv_id, conversation_id=conv_id,
status="discovered", status="discovered",
token_used=ai_token_count, token_used=ai_token_count,
created_at=datetime.now(), created_at=cst_now(),
) )
db.add(trajectory) db.add(trajectory)
db.flush() # 确保trajectory先写入,再启动pipeline db.flush() # 确保trajectory先写入,再启动pipeline
@@ -511,7 +535,7 @@ def clear_conversation(
).delete() ).delete()
conv.knowledge_graph_count = 0 conv.knowledge_graph_count = 0
conv.updated_at = datetime.now() conv.updated_at = cst_now()
db.commit() db.commit()
return {"code": 0, "message": "对话上下文已清空"} return {"code": 0, "message": "对话上下文已清空"}
@@ -550,8 +574,8 @@ def convert_to_knowledge_graph(
description=f"从对话「{conv.title}」自动生成的知识图谱", description=f"从对话「{conv.title}」自动生成的知识图谱",
directory_path="/", directory_path="/",
document_count=0, document_count=0,
created_at=datetime.now(), created_at=cst_now(),
updated_at=datetime.now(), updated_at=cst_now(),
) )
db.add(kb) db.add(kb)
db.flush() db.flush()
@@ -572,8 +596,8 @@ def convert_to_knowledge_graph(
file_size=len(msg.content.encode('utf-8')), file_size=len(msg.content.encode('utf-8')),
status="completed", status="completed",
chunk_count=max(1, len(msg.content) // 500 + 1), chunk_count=max(1, len(msg.content) // 500 + 1),
created_at=datetime.now(), created_at=cst_now(),
updated_at=datetime.now(), updated_at=cst_now(),
) )
db.add(doc) db.add(doc)
doc_count += 1 doc_count += 1
@@ -660,11 +684,11 @@ def convert_to_knowledge_graph(
# 更新知识库的知识图谱计数 # 更新知识库的知识图谱计数
kb.knowledge_graph_count = len(graph_nodes) 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.knowledge_graph_count = (conv.knowledge_graph_count or 0) + 1
conv.updated_at = datetime.now() conv.updated_at = cst_now()
db.commit() db.commit()
@@ -776,6 +800,6 @@ def _execute_skill_tool(db: Session, func_name: str, args: dict, skills_list: li
# 更新skill使用计数 # 更新skill使用计数
if matched_skill: if matched_skill:
matched_skill.use_count = (matched_skill.use_count or 0) + 1 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 return result
+11 -5
View File
@@ -359,7 +359,9 @@ def uninstall_app(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
"""卸载应用 - 删除当前用户的安装记录(按用户隔离)""" """卸载应用 - 删除当前用户的安装记录(按用户隔离)
修复: 当没有AppInstallation记录时(如导入的技能),仍然允许卸载
"""
app = db.query(MarketApp).filter(MarketApp.id == app_id).first() app = db.query(MarketApp).filter(MarketApp.id == app_id).first()
if not app: if not app:
raise HTTPException(status_code=404, detail="应用不存在") raise HTTPException(status_code=404, detail="应用不存在")
@@ -368,13 +370,12 @@ def uninstall_app(
installation = db.query(AppInstallation).filter( installation = db.query(AppInstallation).filter(
and_(AppInstallation.app_id == app_id, AppInstallation.user_id == current_user.id) and_(AppInstallation.app_id == app_id, AppInstallation.user_id == current_user.id)
).first() ).first()
if not installation:
raise HTTPException(status_code=400, detail="应用未安装")
# 删除安装记录 # 修复: 即使没有安装记录也允许卸载(导入的技能可能没有安装记录)
if installation:
db.delete(installation) db.delete(installation)
# 问题4修复: 将AppPurchase状态从installed重置为purchased # 将AppPurchase状态从installed重置为purchased
purchase = db.query(AppPurchase).filter( purchase = db.query(AppPurchase).filter(
and_(AppPurchase.app_id == app_id, AppPurchase.user_id == current_user.id) and_(AppPurchase.app_id == app_id, AppPurchase.user_id == current_user.id)
).first() ).first()
@@ -384,6 +385,11 @@ def uninstall_app(
# 更新应用安装量(减1,最小为0) # 更新应用安装量(减1,最小为0)
if app.install_count and app.install_count > 0: if app.install_count and app.install_count > 0:
app.install_count = app.install_count - 1 app.install_count = app.install_count - 1
# 将应用状态标记为未安装(对于skill类型)
if app.type == "skill" and app.is_active:
app.is_active = False
app.updated_at = datetime.now() app.updated_at = datetime.now()
db.commit() db.commit()
+22 -8
View File
@@ -4,7 +4,7 @@ from sqlalchemy import and_, desc, asc, func, extract
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from typing import Optional from typing import Optional
import uuid import uuid
from datetime import datetime, date, timedelta from datetime import datetime, date, timedelta, timezone
from ..database import get_db from ..database import get_db
from ..models import ( from ..models import (
@@ -16,6 +16,9 @@ from ..middleware.log_service import LogService
router = APIRouter() router = APIRouter()
# CST timezone (UTC+8)
CST_TZ = timezone(timedelta(hours=8))
class RechargeRequest(BaseModel): class RechargeRequest(BaseModel):
amount: int = Field(ge=1) amount: int = Field(ge=1)
@@ -32,12 +35,15 @@ def get_overview(
TokenUsageLog.user_id == current_user.id TokenUsageLog.user_id == current_user.id
).scalar() ).scalar()
# 今日使用中应用数 # 今日使用中应用数 - 使用CST时区的"今天"
today_start = datetime.combine(date.today(), datetime.min.time()) 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( today_apps = db.query(func.count(func.distinct(TokenUsageLog.model_id))).filter(
and_( and_(
TokenUsageLog.user_id == current_user.id, TokenUsageLog.user_id == current_user.id,
TokenUsageLog.created_at >= today_start, TokenUsageLog.created_at >= today_start_utc,
) )
).scalar() or 0 ).scalar() or 0
@@ -46,9 +52,7 @@ def get_overview(
TokenBalance.user_id == current_user.id TokenBalance.user_id == current_user.id
).first() ).first()
from datetime import timezone, timedelta server_time = datetime.now(CST_TZ).isoformat()
shanghai_tz = timezone(timedelta(hours=8))
server_time = datetime.now(shanghai_tz).isoformat()
return { return {
"code": 0, "code": 0,
@@ -169,7 +173,7 @@ def get_token_logs(
"completion_tokens": log_item.completion_tokens or 0, "completion_tokens": log_item.completion_tokens or 0,
"total_tokens": log_item.total_tokens or 0, "total_tokens": log_item.total_tokens or 0,
"conversation_id": log_item.conversation_id, "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 { return {
@@ -245,3 +249,13 @@ def recharge(
}, },
"message": "充值成功", "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()
+1 -1
View File
@@ -105,7 +105,7 @@ export const chatAPI = {
del<any>(`/chat/conversations/${id}`), del<any>(`/chat/conversations/${id}`),
getMessages: (convId: string) => getMessages: (convId: string) =>
get<any[]>(`/chat/conversations/${convId}/messages`), get<any[]>(`/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<any>(`/chat/conversations/${convId}/messages`, data), post<any>(`/chat/conversations/${convId}/messages`, data),
clearContext: (convId: string) => clearContext: (convId: string) =>
post<any>(`/chat/conversations/${convId}/clear`), post<any>(`/chat/conversations/${convId}/clear`),
+1 -1
View File
@@ -582,7 +582,7 @@ const ChatPage: React.FC = () => {
model_id: selectedModelId || undefined, model_id: selectedModelId || undefined,
agent_ids: selectedAgentIds.length > 0 ? selectedAgentIds : undefined, agent_ids: selectedAgentIds.length > 0 ? selectedAgentIds : undefined,
skill_ids: selectedSkillIds.length > 0 ? selectedSkillIds : 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, use_tools: selectedSkillIds.length > 0 || selectedAgentIds.length > 0,
}); });