Files
deploy 148e4a21c2 fix: 全面修复6项问题——Auto模型路由/媒体提示词优化/微信Bot/Token驾驶舱/技能市场清空
1. resolve_auto_model: image/video/audio无显式默认时自动发现启用模型(原返回None→文本模型回退→AI说无法生成)
2. 新增_optimize_media_prompt: 调用文本LLM精炼用户原始输入为AGNES高质量提示词(去除对话噪音+英文优化)
3. detect_media_intent: 扩展关键词覆盖多轮对话模式(做图片/生成个图/画一下/制作视频等)
4. chat.py+im_llm.py: 所有httpx调用补trust_env=False+_proxy_for_url, 规避容器坏代理
5. token_usage.py: 模型名回退链(model表→日志自带的model_name→auto标签), 不再显示unknown
6. skills_admin.py: 新增POST /batch-delete批量删除API(ids空则删全部)
7. 技能市场数据已清空, 用户可重新导入技能
2026-07-15 17:05:00 +08:00

280 lines
8.5 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
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 ..database import get_db
from ..models import (
User, TokenUsageLog, TokenBalance, AIModel, SmsConfig,
gen_uuid
)
from ..auth import get_current_user
from ..middleware.log_service import LogService
from ..sms_client import send_sms_text
router = APIRouter()
class RechargeRequest(BaseModel):
amount: int = Field(ge=1)
channel: str = Field(default="simulation")
@router.get("/overview")
def get_overview(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
# 总消耗
total_used = db.query(func.coalesce(func.sum(TokenUsageLog.total_tokens), 0)).filter(
TokenUsageLog.user_id == current_user.id
).scalar()
# 今日使用中应用数
today_start = datetime.combine(date.today(), datetime.min.time())
today_apps = db.query(func.count(func.distinct(TokenUsageLog.model_id))).filter(
and_(
TokenUsageLog.user_id == current_user.id,
TokenUsageLog.created_at >= today_start,
)
).scalar() or 0
# 余额
balance = db.query(TokenBalance).filter(
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()
return {
"code": 0,
"data": {
"total_used": total_used or 0,
"balance": balance.balance if balance else 0,
"today_apps": today_apps,
"server_time": server_time,
},
"message": "ok",
}
@router.get("/rankings")
def get_rankings(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
# Token消耗最多的模型
model_rankings = (
db.query(
TokenUsageLog.model_id,
func.sum(TokenUsageLog.total_tokens).label("total"),
func.count(TokenUsageLog.id).label("count"),
)
.filter(TokenUsageLog.user_id == current_user.id)
.group_by(TokenUsageLog.model_id)
.order_by(desc("total"))
.all()
)
models_list = []
for r in model_rankings:
model = db.query(AIModel).filter(AIModel.id == r.model_id).first()
if model:
model_name = model.name
else:
# 模型已被删除,从最近一条日志中取 model_name 作为回退
recent = db.query(TokenUsageLog.model_name).filter(
TokenUsageLog.model_id == r.model_id,
TokenUsageLog.model_name.isnot(None),
).first()
model_name = (recent.model_name if recent else None) or (
"Auto 智能匹配" if r.model_id == "auto" else "未知模型"
)
models_list.append({
"model_id": r.model_id,
"model_name": model_name,
"total_tokens": int(r.total),
"usage_count": int(r.count),
})
# TOP3应用
app_rankings = (
db.query(
TokenUsageLog.conversation_id,
func.sum(TokenUsageLog.total_tokens).label("total"),
)
.filter(TokenUsageLog.user_id == current_user.id)
.group_by(TokenUsageLog.conversation_id)
.order_by(desc("total"))
.limit(3)
.all()
)
apps_list = []
for r in app_rankings:
apps_list.append({
"conversation_id": r.conversation_id,
"total_tokens": int(r.total),
})
return {
"code": 0,
"data": {
"model_rankings": models_list,
"top_apps": apps_list,
},
"message": "ok",
}
@router.get("/logs")
def get_token_logs(
start_date: Optional[str] = Query(None, description="开始日期 YYYY-MM-DD"),
end_date: Optional[str] = Query(None, description="结束日期 YYYY-MM-DD"),
model_id: Optional[str] = Query(None),
app_type: Optional[str] = Query(None),
page: int = Query(default=1, ge=1),
page_size: int = Query(default=20, ge=1, le=100),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
query = db.query(TokenUsageLog).filter(TokenUsageLog.user_id == current_user.id)
if start_date:
try:
start_dt = datetime.strptime(start_date, "%Y-%m-%d")
query = query.filter(TokenUsageLog.created_at >= start_dt)
except ValueError:
raise HTTPException(status_code=400, detail="开始日期格式错误,请使用YYYY-MM-DD")
if end_date:
try:
end_dt = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1)
query = query.filter(TokenUsageLog.created_at < end_dt)
except ValueError:
raise HTTPException(status_code=400, detail="结束日期格式错误,请使用YYYY-MM-DD")
if model_id:
query = query.filter(TokenUsageLog.model_id == model_id)
total = query.count()
logs = (
query.order_by(desc(TokenUsageLog.created_at))
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
result = []
for log_item in logs:
model = db.query(AIModel).filter(AIModel.id == log_item.model_id).first()
if model:
model_name = model.name
elif log_item.model_name:
model_name = log_item.model_name # 日志自带的名称
elif log_item.model_id == "auto":
model_name = "🤖 Auto 智能匹配"
else:
model_name = "未知模型 (ID: {})".format(log_item.model_id[:8] if log_item.model_id else "?")
result.append({
"id": log_item.id,
"model_id": log_item.model_id,
"model_name": model_name,
"prompt_tokens": log_item.prompt_tokens or 0,
"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,
})
return {
"code": 0,
"data": {
"total": total,
"page": page,
"page_size": page_size,
"items": result,
},
"message": "ok",
}
@router.get("/balance")
def get_balance(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
balance = db.query(TokenBalance).filter(
TokenBalance.user_id == current_user.id
).first()
return {
"code": 0,
"data": {
"balance": balance.balance if balance else 0,
"updated_at": str(balance.updated_at) if balance and balance.updated_at else None,
},
"message": "ok",
}
@router.post("/recharge")
def recharge(
req: RechargeRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
# 获取或创建余额记录
balance = db.query(TokenBalance).filter(
TokenBalance.user_id == current_user.id
).first()
if not balance:
balance = TokenBalance(
id=gen_uuid(),
user_id=current_user.id,
balance=0,
updated_at=datetime.now(),
)
db.add(balance)
db.flush()
old_balance = balance.balance or 0
balance.balance = old_balance + req.amount
balance.updated_at = datetime.now()
# 记录操作日志
LogService.log_operation(
user_id=current_user.id, username=current_user.username, action="recharge",
target="token_balance", target_id=balance.id,
detail=f"充值 {req.amount} tokens,余额从 {old_balance} 变为 {balance.balance}",
)
db.commit()
# 充值成功后发送短信通知(若已配置短信且用户绑定手机号)
try:
sms_cfg = db.query(SmsConfig).first()
if sms_cfg and sms_cfg.provider and current_user.phone:
send_sms_text(
sms_cfg, current_user.phone,
f"您已成功充值 {req.amount} tokens,当前余额 {balance.balance} tokens。",
)
except Exception as sms_err:
import logging
logging.getLogger("aiflow").warning(f"充值短信通知失败(不影响充值结果): {sms_err}")
return {
"code": 0,
"data": {
"balance": balance.balance,
"recharged": req.amount,
},
"message": "充值成功",
}