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
+119 -49

No files matched your search

+32 -6
View File
@@ -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()
+43 -19
View File
@@ -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
+20 -14
View File
@@ -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()
+22 -8
View File
@@ -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()
+1 -1
View File
@@ -105,7 +105,7 @@ export const chatAPI = {
del<any>(`/chat/conversations/${id}`),
getMessages: (convId: string) =>
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),
clearContext: (convId: string) =>
post<any>(`/chat/conversations/${convId}/clear`),
+1 -1
View File
@@ -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,
});