fix: 6 bug fixes - timezone CST, knowledge_ids mismatch, skill uninstall, dashboard stats, chat knowledge retrieval, skill selection
This commit is contained in:
1 parent
f7ab61bd9d
commit
8ec808b515
6 files changed
+110
-40
No files matched your search
@@ -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()
|
||||
@@ -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
|
||||
@@ -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,13 +370,12 @@ 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="应用未安装")
|
||||
|
||||
# 删除安装记录
|
||||
# 修复: 即使没有安装记录也允许卸载(导入的技能可能没有安装记录)
|
||||
if installation:
|
||||
db.delete(installation)
|
||||
|
||||
# 问题4修复: 将AppPurchase状态从installed重置为purchased
|
||||
# 将AppPurchase状态从installed重置为purchased
|
||||
purchase = db.query(AppPurchase).filter(
|
||||
and_(AppPurchase.app_id == app_id, AppPurchase.user_id == current_user.id)
|
||||
).first()
|
||||
@@ -384,6 +385,11 @@ def uninstall_app(
|
||||
# 更新应用安装量(减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
|
||||
|
||||
app.updated_at = datetime.now()
|
||||
|
||||
db.commit()
|
||||
|
||||
@@ -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()
|
||||
@@ -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`),
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user