diff --git a/api.py b/api.py index 1281cc3..95fb433 100644 --- a/api.py +++ b/api.py @@ -1,9 +1,9 @@ -from fastapi import FastAPI, HTTPException, status, UploadFile +from fastapi import FastAPI, HTTPException, status, UploadFile, Request from pydantic import BaseModel -from typing import Optional +from typing import Optional, List from sqlalchemy import select, func, delete, desc from pathlib import Path -from database import get_session, AccountModel, init_db +from database import get_session, AccountModel, AccountUsageRecordModel, init_db from fastapi.middleware.cors import CORSMiddleware from datetime import datetime import uvicorn @@ -1103,7 +1103,7 @@ async def import_accounts(file: UploadFile): # 添加"使用Token"功能 @app.post("/account/use-token/{id}", tags=["Accounts"]) -async def use_account_token(id: int): +async def use_account_token(id: int, request: Request): """使用指定账号的Token更新Cursor认证""" try: async with get_session() as session: @@ -1130,6 +1130,25 @@ async def use_account_token(id: int): resetter = CursorShadowPatcher() patch_success = resetter.reset_machine_ids() + + # 记录使用记录 + if success: + # 获取请求客户端IP + client_ip = request.client.host + # 获取用户代理 + user_agent = request.headers.get("User-Agent", "") + + # 创建使用记录 + usage_record = AccountUsageRecordModel( + id=int(time.time() * 1000), # 使用毫秒级时间戳作为ID + account_id=id, + email=account.email, + ip=client_ip, + user_agent=user_agent, + created_at=datetime.now().isoformat() + ) + session.add(usage_record) + await session.commit() if success and patch_success: return { @@ -1149,6 +1168,52 @@ async def use_account_token(id: int): error(traceback.format_exc()) return {"success": False, "message": f"使用Token失败: {str(e)}"} +# 添加获取账号使用记录接口 +@app.get("/account/{id}/usage-records", tags=["Accounts"]) +async def get_account_usage_records(id: int): + """获取指定账号的使用记录""" + try: + async with get_session() as session: + # 通过ID查询账号 + result = await session.execute( + select(AccountModel).where(AccountModel.id == id) + ) + account = result.scalar_one_or_none() + + if not account: + return {"success": False, "message": f"ID为 {id} 的账号不存在"} + + # 查询使用记录 + result = await session.execute( + select(AccountUsageRecordModel) + .where(AccountUsageRecordModel.account_id == id) + .order_by(desc(AccountUsageRecordModel.created_at)) + ) + + records = result.scalars().all() + + # 转换记录为字典列表 + records_list = [] + for record in records: + records_list.append({ + "id": record.id, + "account_id": record.account_id, + "email": record.email, + "ip": record.ip, + "user_agent": record.user_agent, + "created_at": record.created_at + }) + + return { + "success": True, + "records": records_list + } + + except Exception as e: + error(f"获取账号使用记录失败: {str(e)}") + error(traceback.format_exc()) + return {"success": False, "message": f"获取账号使用记录失败: {str(e)}"} + # 添加"重置设备id"功能 @app.get("/reset-machine", tags=["System"]) async def reset_machine(): diff --git a/database.py b/database.py index a0603f8..8a441e6 100644 --- a/database.py +++ b/database.py @@ -1,6 +1,6 @@ from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker from sqlalchemy.orm import DeclarativeBase -from sqlalchemy import Column, String, Text, text, BigInteger +from sqlalchemy import Column, String, Text, text, BigInteger, ForeignKey from contextlib import asynccontextmanager from logger import info, error from config import DATABASE_URL @@ -24,6 +24,17 @@ class AccountModel(Base): id = Column(BigInteger, nullable=False, index=True) # 添加毫秒时间戳列并创建索引 +# 账号使用记录模型 +class AccountUsageRecordModel(Base): + __tablename__ = "account_usage_records" + id = Column(BigInteger, primary_key=True, autoincrement=True) + account_id = Column(BigInteger, nullable=False, index=True) # 账号ID + email = Column(String, nullable=False, index=True) # 账号邮箱 + ip = Column(String, nullable=True) # 使用者IP + user_agent = Column(Text, nullable=True) # 使用者UA + created_at = Column(Text, nullable=False) # 创建时间 + + def create_engine(): """创建数据库引擎""" # 直接使用配置文件中的数据库URL diff --git a/index.html b/index.html index b0def13..8d37bd9 100644 --- a/index.html +++ b/index.html @@ -589,6 +589,44 @@ + + +
diff --git a/static/js/app.js b/static/js/app.js index 2178dc9..8b196b3 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -397,15 +397,17 @@ function updateAccountsTable(accounts) { - ${renderTokenColumn(account.token, account.id)} + ${renderTokenColumn(account.token, account.id, account.email)} ${renderUsageProgress(account.usage_limit)} ${account.created_at || '未知'} - +
+ +
@@ -493,9 +495,11 @@ function renderAccountsTable() { ${account.created_at || '未知'} - +
+ +
@@ -960,6 +964,13 @@ function bindTableEvents() { const email = $(this).data('email'); getAccountUsage(email); }); + + // 查看使用记录按钮 + $('.view-records-btn').off('click').on('click', function() { + const email = $(this).data('email'); + const id = $(this).data('id'); + getAccountUsageRecords(email, id); + }); // 删除按钮 $('.delete-account-btn').off('click').on('click', function() { @@ -1137,12 +1148,15 @@ function renderUsageProgress(usageLimit) { } // 修改Token列的渲染方式 -function renderTokenColumn(token, accountId) { +function renderTokenColumn(token, accountId, email) { return ` + `; } @@ -1690,4 +1704,58 @@ function importAccounts(file) { showAlert('danger', '导入账号失败: ' + (xhr.responseJSON?.detail || xhr.statusText)); } }); +} + +// 获取账号使用记录 +function getAccountUsageRecords(email, id) { + showLoading(); + + // 设置模态框中的账号邮箱 + $('#recordEmail').text(email); + + // 清空记录列表 + $('#usageRecordBody').empty(); + + fetch(`/account/${id}/usage-records`) + .then(response => response.json()) + .then(data => { + hideLoading(); + + if (data.success) { + const records = data.records; + + if (records && records.length > 0) { + // 隐藏无记录提示 + $('#no-records').hide(); + + // 显示记录 + records.forEach(record => { + const row = ` + + ${formatDateTime(record.created_at)} + ${record.ip || '-'} + + ${record.user_agent || '-'} + + + `; + $('#usageRecordBody').append(row); + }); + } else { + // 显示无记录提示 + $('#usageRecordBody').empty(); + $('#no-records').show(); + } + + // 显示模态框 + new bootstrap.Modal(document.getElementById('usageRecordModal')).show(); + } else { + showAlert(`获取使用记录失败: ${data.message || '未知错误'}`, 'danger'); + } + }) + .catch(error => { + console.error('获取使用记录时发生错误:', error); + hideLoading(); + showAlert('获取使用记录失败,请稍后重试', 'danger'); + }); } \ No newline at end of file