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. 技能市场数据已清空, 用户可重新导入技能
280 lines
8.5 KiB
Python
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": "充值成功",
|
|
}
|