Files
cursor-api/static/tokens.html
2025-03-05 04:21:37 +08:00

2471 lines
73 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<link rel="icon" type="image/x-icon" href="data:image/x-icon;,">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Token 信息管理</title>
<!-- 引入共享样式 -->
<link rel="stylesheet" href="/static/shared-styles.css">
<script src="/static/shared.js"></script>
<style>
/* 文件系统布局样式 */
.file-system {
background: var(--card-background);
border-radius: var(--border-radius);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-top: var(--spacing);
overflow: hidden;
height: calc(100vh - 250px);
display: flex;
flex-direction: column;
}
.toolbar {
display: flex;
align-items: center;
padding: 8px;
border-bottom: 1px solid var(--border-color);
background: var(--card-background);
}
.toolbar .search-box {
flex: 1;
margin-right: 8px;
position: relative;
}
.toolbar .search-box input {
width: 100%;
padding-left: 32px;
}
.toolbar .search-box::before {
content: "🔍";
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
color: var(--text-secondary);
}
.toolbar .view-toggles button {
height: 36px;
min-width: 36px;
}
.token-files {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
position: relative;
user-select: none;
padding: 0 12px 12px 12px;
}
.file-list {
width: 100%;
border-collapse: collapse;
margin-top: 0;
}
.file-list th {
position: sticky;
top: 0;
z-index: 10;
background: var(--card-background);
padding: 10px;
text-align: left;
font-weight: 500;
border-bottom: 1px solid var(--border-color);
color: var(--text-primary);
}
.file-list th:hover {
background: var(--disabled-bg);
}
.file-list td {
padding: 8px 10px;
border-bottom: 1px solid var(--border-color);
color: var(--text-primary);
}
.file-list tr {
cursor: pointer;
}
.file-list tr:hover {
background: var(--primary-color-alpha);
}
.file-list tr.selected {
background: var(--primary-color-alpha);
}
.file-list tr.focused {
background: var(--primary-color);
color: white;
}
.file-list tr.focused td {
color: white;
}
.file-icon {
display: inline-block;
width: 20px;
height: 20px;
margin-right: 8px;
vertical-align: middle;
}
.status-bar {
padding: 8px;
background: var(--card-background);
border-top: 1px solid var(--border-color);
color: var(--text-secondary);
font-size: 14px;
display: flex;
justify-content: space-between;
}
/* 右键菜单 */
.context-menu {
position: fixed;
background: var(--card-background);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
padding: 4px 0;
z-index: 1000;
min-width: 180px;
display: none;
}
.context-menu-item {
padding: 8px 16px;
cursor: pointer;
transition: all var(--transition-fast);
display: flex;
align-items: center;
position: relative;
}
.context-menu-item:hover {
background: var(--primary-color-alpha);
}
.context-menu-divider {
height: 1px;
background: var(--border-color);
margin: 4px 0;
}
.context-menu-shortcut {
margin-left: auto;
color: var(--text-secondary);
font-size: 12px;
}
/* 选择框 */
.selection-box {
position: absolute;
border: 1px dashed var(--primary-color);
background: rgba(33, 150, 243, 0.1);
z-index: 5;
pointer-events: none;
}
/* 详情面板 */
.details-panel {
position: fixed;
right: 0;
top: 0;
bottom: 0;
width: 500px;
background: var(--card-background);
border-left: 1px solid var(--border-color);
box-shadow: -2px 0 10px rgba(0, 0, 0, 0.1);
padding: 16px;
overflow-y: auto;
z-index: 100;
transform: translateX(100%);
transition: transform 0.3s ease;
}
.details-panel.open {
transform: translateX(0);
}
.details-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid var(--border-color);
}
.details-header h3 {
margin: 0;
}
.details-header button {
background: transparent;
border: none;
cursor: pointer;
font-size: 20px;
color: var(--text-secondary);
}
.details-content {
margin-bottom: 16px;
}
.details-property {
margin-bottom: 12px;
}
.details-property-name {
font-weight: 500;
color: var(--text-secondary);
margin-bottom: 4px;
}
.details-property-value {
color: var(--text-primary);
word-break: break-all;
}
.details-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.details-actions button {
flex: 1;
min-width: 120px;
}
/* 弹出对话框样式 */
.modal {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--card-background);
padding: 20px;
border-radius: var(--border-radius);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 1000;
width: 90%;
max-width: 500px;
color: var(--text-primary);
}
.modal-backdrop {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
}
.modal-header {
margin-bottom: 15px;
border-bottom: 1px solid var(--border-color);
padding-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
margin: 0;
color: var(--text-primary);
}
.modal-close {
background: transparent;
border: none;
color: var(--text-secondary);
font-size: 20px;
cursor: pointer;
}
.modal-body {
margin-bottom: 15px;
}
.modal-footer {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid var(--border-color);
text-align: right;
display: flex;
justify-content: flex-end;
gap: 10px;
}
/* 筛选面板 */
.filter-panel {
padding: 16px;
background: var(--card-background);
border-radius: var(--border-radius);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-top: var(--spacing);
}
.filter-row {
display: flex;
gap: 12px;
margin-bottom: 14px;
flex-wrap: wrap;
}
.filter-group {
flex: 1;
min-width: 220px;
position: relative;
}
.filter-group label {
display: block;
margin-bottom: 5px;
font-weight: 500;
color: var(--text-secondary);
}
.filter-group select {
width: 100%;
min-height: 38px;
}
.filter-input-group {
display: flex;
gap: 8px;
align-items: center;
}
.filter-input-group input {
flex: 1;
min-width: 0;
min-height: 38px;
}
.filter-input-group span {
color: var(--text-secondary);
font-size: 14px;
}
.filter-actions {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 16px;
align-items: flex-end;
}
.filter-actions-left {
flex: 1;
min-width: 220px;
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.filter-actions-right {
display: flex;
flex-wrap: wrap;
gap: 8px;
min-height: 40px;
}
/* 响应式调整 */
@media (max-width: 1200px) {
.filter-actions {
flex-direction: column;
align-items: stretch;
}
.filter-actions-left {
margin-bottom: 12px;
}
.filter-actions-right {
justify-content: flex-end;
}
}
@media (max-width: 768px) {
.filter-actions-right {
justify-content: flex-start;
}
.button-group button {
flex: 1;
min-width: 0;
}
#detailsPanel {
width: 100%;
}
}
/* 托盘消息 */
.toast-container {
position: fixed;
bottom: 20px;
right: 20px;
display: flex;
flex-direction: column;
gap: 10px;
z-index: 1000;
max-width: 350px;
max-height: 80vh;
overflow-y: hidden;
padding-top: 10px;
padding-bottom: 10px;
padding-right: 5px;
}
.toast {
background: var(--card-background);
color: var(--text-primary);
padding: 10px 16px;
border-radius: var(--border-radius);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
opacity: 0;
transform: translateY(20px);
transition: opacity 0.4s cubic-bezier(0.25, 0.8, 0.25, 1), transform 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
position: relative;
min-width: 200px;
margin-left: auto;
will-change: transform, opacity;
pointer-events: auto;
}
.toast.info {
border-left: 4px solid #2196F3;
}
.toast.error {
background: #f44336;
color: white;
}
.toast.success {
background: #4caf50;
color: white;
}
.toast.warning {
background: #ff9800;
color: white;
}
.toast.show {
opacity: 1;
transform: translateY(0);
}
/* 处理纵向滚动 */
@media (max-height: 600px) {
.file-system {
height: 400px;
}
}
.key-result {
background: var(--card-background);
padding: var(--spacing);
border-radius: var(--border-radius);
border: 1px solid var(--border-color);
margin-top: var(--spacing);
position: relative;
cursor: pointer;
transition: all var(--transition-fast);
}
.key-result:hover {
background: var(--primary-color-alpha);
border-color: var(--primary-color);
}
.key-result:active {
transform: translateY(1px);
}
.key-content {
overflow-x: auto;
white-space: nowrap;
scrollbar-width: thin;
-ms-overflow-style: none;
}
/* 批量操作区域 */
.batch-operations {
background: var(--card-background);
border-radius: var(--border-radius);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-top: var(--spacing);
padding: var(--spacing);
display: none;
}
.batch-operations .form-group {
margin-bottom: 12px;
}
.button-group {
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: flex-end;
align-items: flex-end;
margin: 0;
}
.button-group button {
height: 38px;
min-width: 100px;
white-space: nowrap;
}
.button-group button .context-menu-shortcut {
margin-left: 5px;
opacity: 0.7;
font-size: 12px;
}
/* 代理子菜单样式 */
.proxy-menu {
position: relative;
}
.proxy-submenu {
position: absolute;
left: 100%;
top: 0;
background: var(--card-background);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
padding: 4px 0;
min-width: 180px;
z-index: 1001;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s, visibility 0.2s;
}
.proxy-menu:hover>.proxy-submenu {
opacity: 1;
visibility: visible;
}
.context-menu-item.has-submenu::after {
content: "";
position: absolute;
right: 8px;
font-size: 18px;
}
.check-mark {
margin-left: auto;
visibility: hidden;
}
.check-mark::after {
content: "✓";
}
.context-menu-item.active .check-mark {
visibility: visible;
}
.context-menu-item.active span:first-child {
font-weight: bold;
}
.proxy-count {
color: var(--text-secondary);
font-size: 12px;
margin-left: 8px;
}
/* 向上展开的子菜单样式 */
.proxy-menu.open-upward .proxy-submenu {
top: auto;
bottom: 0;
}
/* 向左展开的子菜单样式 */
.proxy-menu.open-leftward .proxy-submenu {
right: 100%;
left: auto;
}
/* 代理名称可点击样式 */
.proxy-name {
cursor: pointer;
padding: 3px 8px;
border-radius: 4px;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
border: 1px solid transparent;
background-color: var(--disabled-bg);
font-size: 0.9em;
position: relative;
}
.proxy-name:hover {
background-color: var(--primary-color-alpha);
color: var(--primary-color);
border-color: var(--primary-color);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.proxy-name:active {
transform: translateY(0px);
box-shadow: none;
}
.proxy-name:hover::after {
opacity: 1;
}
</style>
</head>
<body>
<h1>Token 信息管理</h1>
<div class="container">
<div class="form-group">
<label>认证令牌:</label>
<input type="password" id="authToken" placeholder="输入 AUTH_TOKEN">
</div>
</div>
<!-- 筛选面板 -->
<div class="filter-panel">
<div class="filter-row">
<div class="filter-group">
<label>账户类型:</label>
<select id="membershipFilter">
<option value="all">全部类型</option>
<option value="free">免费版</option>
<option value="free_trial">试用版</option>
<option value="pro">专业版</option>
<option value="enterprise">企业版</option>
</select>
</div>
<div class="filter-group">
<label>用量状态:</label>
<div class="filter-input-group">
<input type="number" id="usageMin" placeholder="最小值">
<span></span>
<input type="number" id="usageMax" placeholder="最大值">
</div>
</div>
<div class="filter-group">
<label>试用剩余:</label>
<div class="filter-input-group">
<input type="number" id="trialMin" placeholder="最小天数">
<span></span>
<input type="number" id="trialMax" placeholder="最大天数">
</div>
</div>
</div>
<div class="filter-actions">
<div class="filter-actions-left">
<div class="filter-group">
<label>Profile状态:</label>
<select id="profileFilter">
<option value="all">全部状态</option>
<option value="has_profile">有Profile</option>
<option value="no_profile">无Profile</option>
</select>
</div>
<div class="filter-group">
<label>代理筛选:</label>
<select id="proxyFilter">
<option value="all">全部代理</option>
<option value="none">未指定代理</option>
<!-- 代理列表将在JavaScript中动态填充 -->
</select>
</div>
</div>
<div class="filter-actions-right">
<button id="refreshBtn" onclick="getTokenInfo()" class="primary">
<span>刷新列表</span>
<span class="context-menu-shortcut">F5</span>
</button>
<button id="addTokenBtn" onclick="addTokens()" class="secondary">添加Token</button>
<button id="exportBtn" onclick="exportTokens()" class="secondary">导出列表</button>
<button id="importBtn" onclick="importTokens()" class="secondary">导入列表</button>
</div>
</div>
</div>
<!-- 文件系统区域 -->
<div class="file-system">
<div class="toolbar">
<div class="search-box">
<input type="text" id="searchInput" placeholder="搜索Token或账户...">
</div>
</div>
<div class="token-files">
<table class="file-list">
<thead>
<tr>
<th>账户/Token</th>
<th style="width: 20%;">会员类型</th>
<th style="width: 20%;">用量</th>
<th style="width: 10%;">试用剩余</th>
<th style="width: 10%;">代理</th>
</tr>
</thead>
<tbody id="fileListBody">
<!-- 动态生成的文件列表 -->
</tbody>
</table>
<div id="emptyState" style="display: none; padding: 40px; text-align: center; color: var(--text-secondary);">
<div style="font-size: 48px; margin-bottom: 16px;">📁</div>
<h3>没有找到Token</h3>
<p>请添加Token或更改筛选条件</p>
<button onclick="addTokens()" class="primary">添加Token</button>
</div>
<!-- 选择框 -->
<div id="selectionBox" class="selection-box"></div>
</div>
<div class="status-bar">
<div id="selectionStatus">已选择: 0 个项目</div>
<div id="totalCount">共 0 个Token</div>
</div>
</div>
<!-- 右键菜单 -->
<div id="contextMenu" class="context-menu">
<div class="context-menu-item" onclick="viewDetails()">
<span>查看详情</span>
<span class="context-menu-shortcut">Enter</span>
</div>
<div class="context-menu-item" onclick="refreshSelectedProfiles()">
<span>刷新Profile</span>
<span class="context-menu-shortcut">F5</span>
</div>
<div class="context-menu-item" onclick="generateKey()">
<span>生成Key</span>
<span class="context-menu-shortcut">Ctrl+G</span>
</div>
<div class="context-menu-item" onclick="copyTokenToClipboard()">
<span>复制Token</span>
<span class="context-menu-shortcut">Ctrl+C</span>
</div>
<div class="context-menu-item proxy-menu">
<span>设置代理</span>
<div class="proxy-submenu" id="proxySubmenu">
<div class="context-menu-item" onclick="setProxy('')">
<span>未指定</span>
<span class="proxy-count">0</span>
<span class="check-mark"></span>
</div>
<div class="context-menu-divider"></div>
<!-- 代理列表将动态添加在这里 -->
</div>
</div>
<div class="context-menu-divider"></div>
<div class="context-menu-item" onclick="deleteSelectedTokens()">
<span>删除</span>
<span class="context-menu-shortcut">Delete</span>
</div>
</div>
<!-- 详情面板 -->
<div id="detailsPanel" class="details-panel">
<div class="details-header">
<h3 id="detailsTitle">Token详情</h3>
<button onclick="toggleDetailsPanel(false)" style="width: unset;">×</button>
</div>
<div id="detailsContent" class="details-content">
<!-- 详情内容将在这里动态生成 -->
</div>
<div class="details-actions">
<button onclick="refreshSelectedProfiles()" class="secondary">刷新Profile</button>
<button onclick="generateKey()" class="secondary">生成Key</button>
<button onclick="deleteSelectedTokens()" class="danger">删除</button>
</div>
</div>
<!-- 生成Key对话框 -->
<div class="modal-backdrop" id="keyModal-backdrop"></div>
<div class="modal" id="keyModal">
<div class="modal-header">
<h3>生成动态Key</h3>
<button class="modal-close" onclick="closeModal('keyModal')">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>Token:</label>
<div id="keyToken"
style="word-break: break-all; background: var(--disabled-bg); padding: 8px; border-radius: var(--border-radius); margin-bottom: 10px;">
</div>
</div>
<div class="form-group">
<label>图片处理能力:</label>
<select id="disableVision">
<option value="">跟随全局</option>
<option value="true">禁用</option>
<option value="false">启用</option>
</select>
</div>
<div class="form-group">
<label>慢速池:</label>
<select id="enableSlowPool">
<option value="">跟随全局</option>
<option value="true">启用</option>
<option value="false">禁用</option>
</select>
</div>
<div class="form-group">
<label>使用量检查模型规则:</label>
<select id="usageCheckType" onchange="toggleModelList()">
<option value="">跟随全局</option>
<option value="default">默认</option>
<option value="disabled">禁用</option>
<option value="all">所有</option>
<option value="custom">自定义</option>
</select>
<div id="modelListContainer" class="model-list"
style="display: none; max-height: 150px; overflow-y: auto; margin-top: 8px;">
<!-- 动态填充模型列表 -->
</div>
</div>
<div class="form-group">
<label>代理服务器:</label>
<select id="proxySelect">
<option value="">跟随Token设置</option>
<!-- 代理选项将在这里动态生成 -->
</select>
</div>
<div class="form-group">
<label>包含网络引用:</label>
<select id="includeWebReferences">
<option value="">跟随全局</option>
<option value="true">启用</option>
<option value="false">禁用</option>
</select>
</div>
<div class="key-result" id="keyResult" style="display: none;" onclick="copyGeneratedKey()">
<div class="key-content" id="keyContent"></div>
</div>
</div>
<div class="modal-footer">
<button onclick="closeModal('keyModal')" class="secondary">取消</button>
<button onclick="generateKeySubmit()" class="primary">生成</button>
</div>
</div>
<!-- 确认删除对话框 -->
<div class="modal-backdrop" id="confirmModal-backdrop"></div>
<div class="modal" id="confirmModal">
<div class="modal-header">
<h3>确认删除</h3>
<button class="modal-close" onclick="closeModal('confirmModal')">×</button>
</div>
<div class="modal-body">
<p id="confirmMessage">确定要删除选中的Token吗</p>
</div>
<div class="modal-footer">
<button onclick="closeModal('confirmModal')" class="secondary">取消</button>
<button onclick="confirmDeleteTokens()" class="danger">删除</button>
</div>
</div>
<!-- 批量添加对话框 -->
<div class="modal-backdrop" id="addTokensModal-backdrop"></div>
<div class="modal" id="addTokensModal">
<div class="modal-header">
<h3>批量添加Token</h3>
<button class="modal-close" onclick="closeModal('addTokensModal')">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>添加Token:</label>
<textarea id="addTokensInput" placeholder="每行输入一个token格式为token或token,checksum" rows="8"></textarea>
</div>
</div>
<div class="modal-footer">
<button onclick="closeModal('addTokensModal')" class="secondary">取消</button>
<button onclick="confirmAddTokens()" class="primary">添加</button>
</div>
</div>
<!-- 导入导出对话框 -->
<div class="modal-backdrop" id="importModal-backdrop"></div>
<div class="modal" id="importModal">
<div class="modal-header">
<h3>导入Token列表</h3>
<button class="modal-close" onclick="closeModal('importModal')">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>选择JSON文件:</label>
<input type="file" id="importFile" accept=".json">
</div>
</div>
<div class="modal-footer">
<button onclick="closeModal('importModal')" class="secondary">取消</button>
<button onclick="confirmImport()" class="primary">导入</button>
</div>
</div>
<!-- 代理选择对话框 -->
<div class="modal-backdrop" id="proxySelectModal-backdrop"></div>
<div class="modal" id="proxySelectModal">
<div class="modal-header">
<h3>选择代理</h3>
<button class="modal-close" onclick="closeModal('proxySelectModal')">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>账户/Token:</label>
<div id="currentTokenDisplay"
style="word-break: break-all; background: var(--disabled-bg); padding: 8px; border-radius: var(--border-radius); margin-bottom: 10px;">
</div>
</div>
<div class="form-group">
<label>代理列表:</label>
<select id="proxySelectDropdown" style="width: 100%">
<option value="">未指定</option>
<!-- 代理列表将动态添加 -->
</select>
</div>
</div>
<div class="modal-footer">
<button onclick="closeModal('proxySelectModal')" class="secondary">取消</button>
<button onclick="saveProxySelection()" class="primary">保存</button>
</div>
</div>
<!-- 全局通知区域 -->
<div id="toast-container" class="toast-container"></div>
<script>
// 全局变量
let allTokens = [];
let globalTags = [];
let selectedTag = 'all';
let currentToken = '';
let currentChecksum = '';
let tokenToDelete = null;
let tokenToEditTags = null;
let availableModels = [];
let selectedTokens = new Set(); // 存储选中的Token
let lastSelectedIndex = -1;
let isMouseDown = false;
let startPoint = { x: 0, y: 0 };
let selectionBox = document.getElementById('selectionBox');
let detailsPanelOpen = false;
let tokensToDelete = [];
// 代理相关全局变量
let proxyList = []; // 缓存代理列表
let currentProxy = ''; // 当前使用的通用代理
// 当前正在编辑代理的token
let currentEditingToken = '';
// 初始化TokenHandling
initializeTokenHandling('authToken');
// 检测是Mac还是其他系统Mac使用metaKey(Command键)其他系统使用ctrlKey
function isModifierKey(e) {
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
return isMac ? e.metaKey : e.ctrlKey;
}
// 初始化
document.addEventListener('DOMContentLoaded', async () => {
// 获取模型和Token信息
getModels();
// 获取代理列表
await getProxies();
getTokenInfo();
// 监听搜索输入
document.getElementById('searchInput').addEventListener('input', filterTokens);
// 监听筛选下拉框
document.getElementById('membershipFilter').addEventListener('change', filterTokens);
document.getElementById('usageMin').addEventListener('change', filterTokens);
document.getElementById('usageMax').addEventListener('change', filterTokens);
document.getElementById('trialMin').addEventListener('change', filterTokens);
document.getElementById('trialMax').addEventListener('change', filterTokens);
document.getElementById('profileFilter').addEventListener('change', filterTokens);
document.getElementById('proxyFilter').addEventListener('change', filterTokens); // 代理筛选监听
// 设置键盘快捷键
setupKeyboardShortcuts();
// 设置鼠标选择
setupMouseSelection();
// 设置文件系统的上下文菜单
setupContextMenu();
updateStatusBar();
// 快捷键支持
document.addEventListener('keydown', function (e) {
const modifierKey = isModifierKey(e);
if (modifierKey && e.key === 'Enter') {
e.preventDefault();
const activeElement = document.activeElement;
if (activeElement.id === 'tokenInput' || activeElement.id === 'batchTokenInput') {
// 根据当前焦点确定操作
const action = document.querySelector('.button-group button.active') ||
document.querySelector('.batch-actions button');
if (action) {
action.click();
}
}
} else if (modifierKey && e.key === 'a') {
if (document.activeElement.tagName !== 'INPUT' &&
document.activeElement.tagName !== 'TEXTAREA') {
e.preventDefault();
selectAllTokens();
}
}
});
});
// 设置键盘快捷键
function setupKeyboardShortcuts() {
document.addEventListener('keydown', function (e) {
const modifierKey = isModifierKey(e);
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
// 但允许Ctrl/Command+Enter执行提交操作
if (modifierKey && e.key === 'Enter' && e.target.id === 'tokenInput') {
addTokens();
}
return;
}
// 文件系统操作快捷键
switch (e.key) {
case 'a':
if (modifierKey) {
e.preventDefault();
selectAllTokens();
}
break;
case 'c':
if (modifierKey) {
e.preventDefault();
copyTokenToClipboard();
}
break;
case 'Delete':
e.preventDefault();
deleteSelectedTokens();
break;
case 'Enter':
e.preventDefault();
viewDetails();
break;
case 'F5':
e.preventDefault();
if (selectedTokens.size > 0) {
refreshSelectedProfiles();
} else {
getTokenInfo();
}
break;
case 'g':
if (modifierKey) {
e.preventDefault();
generateKey();
}
break;
case 'f':
if (modifierKey) {
e.preventDefault();
document.getElementById('searchInput').focus();
}
break;
case 'Escape':
closeAllModals();
if (detailsPanelOpen) {
toggleDetailsPanel(false);
}
break;
}
});
}
// 设置鼠标选择
function setupMouseSelection() {
const tokenFiles = document.querySelector('.token-files');
// 鼠标按下开始选择
tokenFiles.addEventListener('mousedown', function (e) {
// 仅在左键点击且不是点击在表格行上时启用框选
if (e.button !== 0 || e.target.closest('tr') || e.target.closest('button')) {
return;
}
isMouseDown = true;
const rect = tokenFiles.getBoundingClientRect();
startPoint = {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
selectionBox.style.left = startPoint.x + 'px';
selectionBox.style.top = startPoint.y + 'px';
selectionBox.style.width = '0px';
selectionBox.style.height = '0px';
selectionBox.style.display = 'block';
// 清除现有选择除非按住Ctrl键
if (!isModifierKey(e)) {
clearSelection();
}
e.preventDefault();
});
// 鼠标移动更新选择框
document.addEventListener('mousemove', function (e) {
if (!isMouseDown) return;
const rect = tokenFiles.getBoundingClientRect();
const currentPoint = {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
const width = Math.abs(currentPoint.x - startPoint.x);
const height = Math.abs(currentPoint.y - startPoint.y);
const left = Math.min(currentPoint.x, startPoint.x);
const top = Math.min(currentPoint.y, startPoint.y);
selectionBox.style.left = left + 'px';
selectionBox.style.top = top + 'px';
selectionBox.style.width = width + 'px';
selectionBox.style.height = height + 'px';
// 检测与行的相交并更新选择
const boxRect = selectionBox.getBoundingClientRect();
const rows = document.querySelectorAll('#fileListBody tr');
rows.forEach(row => {
const rowRect = row.getBoundingClientRect();
const token = row.dataset.token;
// 检查是否相交
if (
rowRect.top < boxRect.bottom &&
rowRect.bottom > boxRect.top &&
rowRect.left < boxRect.right &&
rowRect.right > boxRect.left
) {
selectToken(token, true);
} else if (!isModifierKey(e)) {
deselectToken(token);
}
});
updateSelectionStatus();
});
// 鼠标释放结束选择
document.addEventListener('mouseup', function () {
if (isMouseDown) {
isMouseDown = false;
selectionBox.style.display = 'none';
updateSelectionStatus();
}
});
// 防止拖动选择时选中文本
tokenFiles.addEventListener('selectstart', function (e) {
if (isMouseDown) {
e.preventDefault();
}
});
}
// 设置上下文菜单
function setupContextMenu() {
const tokenFiles = document.querySelector('.token-files');
const contextMenu = document.getElementById('contextMenu');
// 右键点击显示菜单
tokenFiles.addEventListener('contextmenu', function (e) {
const row = e.target.closest('tr');
if (!row) return;
e.preventDefault();
const token = row.dataset.token;
// 如果点击的行不在选中项中,则清除选择并选中该行
if (!selectedTokens.has(token)) {
clearSelection();
selectToken(token);
}
// 更新代理子菜单
updateProxySubmenu();
// 计算菜单位置
positionContextMenu(contextMenu, e.clientX, e.clientY);
});
// 点击其他地方关闭菜单
document.addEventListener('click', function (e) {
if (!contextMenu.contains(e.target)) {
contextMenu.style.display = 'none';
}
});
}
// 修改计算和设置菜单位置的函数
function positionContextMenu(menu, x, y) {
// 首先显示菜单但设为不可见,以便获取其尺寸
menu.style.display = 'block';
menu.style.visibility = 'hidden';
// 获取菜单尺寸和窗口尺寸
const menuRect = menu.getBoundingClientRect();
const menuWidth = menuRect.width;
const menuHeight = menuRect.height;
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
// 计算最佳位置
let posX = x;
let posY = y;
// 水平方向调整 - 如果菜单会超出右边界,则向左展开
if (x + menuWidth > windowWidth) {
posX = windowWidth - menuWidth - 5; // 5px的安全边距
}
// 垂直方向调整 - 如果菜单会超出底部边界,则向上展开
if (y + menuHeight > windowHeight) {
posY = windowHeight - menuHeight - 5; // 5px的安全边距
// 如果向上展开也不够空间,尽量显示在视口中央
if (posY < 0) {
posY = Math.max(5, (windowHeight - menuHeight) / 2);
}
}
// 应用计算后的位置
menu.style.left = `${posX}px`;
menu.style.top = `${posY}px`;
menu.style.visibility = 'visible';
// 调整子菜单方向
adjustSubmenuPosition(menu, windowWidth, windowHeight);
}
// 新增函数:调整子菜单位置
function adjustSubmenuPosition(menu, windowWidth, windowHeight) {
const proxyMenu = menu.querySelector('.proxy-menu');
if (!proxyMenu) return;
// 重置方向类
proxyMenu.classList.remove('open-upward', 'open-leftward');
// 获取菜单项位置
const menuItemRect = proxyMenu.getBoundingClientRect();
const submenu = proxyMenu.querySelector('.proxy-submenu');
// 先临时显示子菜单以获取其尺寸
const originalVisibility = submenu.style.visibility;
submenu.style.visibility = 'hidden';
submenu.style.opacity = '1';
const submenuRect = submenu.getBoundingClientRect();
const submenuHeight = submenuRect.height;
const submenuWidth = submenuRect.width;
// 还原子菜单状态
submenu.style.visibility = originalVisibility;
submenu.style.opacity = '';
// 检查右侧是否有足够空间
const rightSpace = windowWidth - menuItemRect.right;
if (rightSpace < submenuWidth) {
proxyMenu.classList.add('open-leftward');
}
// 检查底部是否有足够空间
const bottomSpace = windowHeight - menuItemRect.top;
if (bottomSpace < submenuHeight) {
// 如果底部空间不够,检查顶部是否有足够空间
const topSpace = menuItemRect.bottom;
if (topSpace >= submenuHeight || topSpace > bottomSpace) {
proxyMenu.classList.add('open-upward');
}
}
}
// 更新代理子菜单
function updateProxySubmenu() {
const submenu = document.getElementById('proxySubmenu');
if (!submenu) {
return;
}
// 获取选中token的代理信息
const proxyUsage = {};
const selectedTokenObjs = allTokens.filter(t => selectedTokens.has(t.token));
allTokens.forEach(token => {
const tags = token.tags || [];
const proxy = tags[1] || '';
proxyUsage[proxy] = (proxyUsage[proxy] || 0) + 1;
});
// 确保"未指定"选项始终存在
let html = `
<div class="context-menu-item ${selectedTokenObjs.length > 0 && proxyUsage[''] === selectedTokenObjs.length ? 'active' : ''}" onclick="setProxy('')">
<span>未指定</span>
<span class="proxy-count">${proxyUsage[''] || 0}</span>
<span class="check-mark"></span>
</div>
<div class="context-menu-divider"></div>
`;
// 添加所有可用代理
if (proxyList.length > 0) {
proxyList.forEach(proxy => {
const count = proxyUsage[proxy] || 0;
// 检查所有选中的token是否都使用了这个相同的代理
const isActive = selectedTokenObjs.length > 0 &&
selectedTokenObjs.every(token =>
(token.tags || [])[1] === proxy
);
html += `
<div class="context-menu-item ${isActive ? 'active' : ''}" onclick="setProxy('${proxy}')">
<span>${proxy}</span>
<span class="proxy-count">${count}</span>
<span class="check-mark"></span>
</div>
`;
});
} else {
html += `<div class="context-menu-item disabled">
<span>无可用代理</span>
</div>
`;
}
submenu.innerHTML = html;
// 修改代理菜单项添加has-submenu类
const proxyMenuEl = document.querySelector('.proxy-menu');
if (proxyMenuEl) {
proxyMenuEl.classList.add('has-submenu');
}
// 关闭右键菜单
const contextMenu = document.getElementById('contextMenu');
contextMenu.style.display = 'none';
}
// 获取Token信息
async function getTokenInfo() {
showToast('正在加载Token列表...', 'info');
// 保存当前筛选条件
const searchTerm = document.getElementById('searchInput').value;
const membershipFilter = document.getElementById('membershipFilter').value;
const usageMinValue = document.getElementById('usageMin').value;
const usageMaxValue = document.getElementById('usageMax').value;
const trialMinValue = document.getElementById('trialMin').value;
const trialMaxValue = document.getElementById('trialMax').value;
const profileFilter = document.getElementById('profileFilter').value;
const proxyFilter = document.getElementById('proxyFilter').value;
// 刷新代理列表
await getProxies();
const data = await makeAuthenticatedRequest('/tokens/get');
if (data && data.tokens) {
allTokens = data.tokens;
renderTokens(allTokens);
updateStatusBar();
updateProxyFilter(); // 更新代理筛选下拉框
// 恢复筛选条件
document.getElementById('searchInput').value = searchTerm;
document.getElementById('membershipFilter').value = membershipFilter;
document.getElementById('usageMin').value = usageMinValue;
document.getElementById('usageMax').value = usageMaxValue;
document.getElementById('trialMin').value = trialMinValue;
document.getElementById('trialMax').value = trialMaxValue;
document.getElementById('profileFilter').value = profileFilter;
document.getElementById('proxyFilter').value = proxyFilter;
// 应用筛选
filterTokens();
showToast('Token列表已更新', 'success');
} else {
showToast('获取Token列表失败', 'error');
}
}
// 更新代理筛选下拉框
function updateProxyFilter() {
const proxyFilterSelect = document.getElementById('proxyFilter');
// 保持第一个和第二个选项(全部代理和未指定代理)
while (proxyFilterSelect.options.length > 2) {
proxyFilterSelect.remove(2);
}
// 使用公共方法获取代理使用统计
const proxyUsage = getProxyUsageStats();
// 添加代理选项
proxyList.forEach(proxy => {
if (proxy) { // 确保代理名不为空
const option = document.createElement('option');
option.value = proxy;
option.textContent = `${proxy} (${proxyUsage[proxy] || 0})`;
proxyFilterSelect.appendChild(option);
}
});
}
// 设置代理
async function setProxy(proxyName) {
if (selectedTokens.size === 0) {
showToast('请先选择Token', 'info');
return;
}
const tokensToUpdate = [...selectedTokens];
// 第一项为空字符串,第二项为代理名称
const tags = proxyName ? ['', proxyName] : [];
showToast('正在更新代理设置...', 'info');
const data = await makeAuthenticatedRequest('/tokens/tags/update', {
method: 'POST',
body: JSON.stringify({
tokens: tokensToUpdate,
tags: tags
})
});
if (data && data.status === 'success') {
showToast('代理设置成功', 'success');
getTokenInfo(); // 刷新Token列表
} else {
showToast('代理设置失败', 'error');
}
}
// 更新生成Key对话框中的代理选择
function updateProxySelect() {
const proxySelect = document.getElementById('proxySelect');
if (!proxySelect || proxyList.length === 0) return;
// 清空现有选项(保留第一个"跟随Token设置"选项)
while (proxySelect.options.length > 1) {
proxySelect.remove(1);
}
// 添加代理选项
proxyList.forEach(proxy => {
const option = document.createElement('option');
option.value = proxy;
option.textContent = proxy;
proxySelect.appendChild(option);
});
}
// 渲染Token列表
function renderTokens(tokens) {
const fileListBody = document.getElementById('fileListBody');
const emptyState = document.getElementById('emptyState');
if (tokens.length === 0) {
fileListBody.innerHTML = '';
emptyState.style.display = 'block';
return;
}
emptyState.style.display = 'none';
fileListBody.innerHTML = tokens.map((token, index) => {
const profile = token.profile || {};
const user = profile.user || {};
const stripe = profile.stripe || {};
const usage = profile.usage || {};
const premium = usage.premium || {};
const email = user.email || '';
const displayName = email || token.token.substring(0, 15) + '...';
return `
<tr data-token="${token.token}" data-checksum="${token.checksum}" data-index="${index}" class="${selectedTokens.has(token.token) ? 'selected' : ''}">
<td>
<span class="file-icon">🔑</span>
${displayName}
</td>
<td>${formatMembershipType(stripe.membership_type)}</td>
<td>${premium.requests || 0}/${premium.max_requests || '∞'}</td>
<td>${stripe.days_remaining_on_trial > 0 ? `${stripe.days_remaining_on_trial}` : '-'}</td>
<td><span class="proxy-name" onclick="openProxySelector('${displayName}','${token.token}', event)">${token.tags ? token.tags[1] || '未指定' : '未指定'}</span></td>
</tr>
`;
}).join('');
// 添加点击事件处理
const rows = document.querySelectorAll('#fileListBody tr');
rows.forEach(row => {
row.addEventListener('click', function (e) {
// 如果点击的是代理名称,不处理行选择
if (e.target.classList.contains('proxy-name')) {
return;
}
const row = e.target.closest('tr');
if (!row) return;
const token = this.dataset.token;
const index = parseInt(this.dataset.index);
const modifierKey = isModifierKey(e);
if (e.shiftKey && lastSelectedIndex !== -1) {
// Shift+点击实现范围选择
const start = Math.min(lastSelectedIndex, index);
const end = Math.max(lastSelectedIndex, index);
clearSelection();
for (let i = start; i <= end; i++) {
const rowToken = rows[i].dataset.token;
selectToken(rowToken);
}
} else if (modifierKey) {
// Ctrl+点击实现多选
if (selectedTokens.has(token)) {
deselectToken(token);
} else {
selectToken(token, true);
lastSelectedIndex = index;
}
} else {
// 普通点击
clearSelection();
selectToken(token);
lastSelectedIndex = index;
}
updateSelectionStatus();
});
});
// 双击查看详情
fileListBody.addEventListener('dblclick', function (e) {
const row = e.target.closest('tr');
if (!row) return;
const token = row.dataset.token;
selectToken(token);
viewDetails();
});
updateSelectionStatus();
// 更新代理子菜单
updateProxySubmenu();
}
// 筛选Token
function filterTokens() {
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
const membershipFilter = document.getElementById('membershipFilter').value;
const usageMinValue = document.getElementById('usageMin').value;
const usageMaxValue = document.getElementById('usageMax').value;
const trialMinValue = document.getElementById('trialMin').value;
const trialMaxValue = document.getElementById('trialMax').value;
const profileFilter = document.getElementById('profileFilter').value;
const proxyFilter = document.getElementById('proxyFilter').value; // 新增代理筛选
const filteredTokens = allTokens.filter(token => {
const profile = token.profile || {};
const user = profile.user || {};
const stripe = profile.stripe || {};
const usage = profile.usage || {};
const premium = usage.premium || {};
// 搜索条件
const emailMatch = (user.email || '').toLowerCase().includes(searchTerm);
const tokenMatch = (token.token || '').toLowerCase().includes(searchTerm);
const searchMatch = searchTerm === '' || emailMatch || tokenMatch;
// 会员类型筛选
const membershipType = stripe.membership_type || '';
let membershipMatch = true;
if (membershipFilter !== 'all') {
membershipMatch = membershipType === membershipFilter;
}
// 使用量区间筛选
let usageMatch = true;
const currentUsage = premium.requests || 0;
if (usageMinValue !== '') {
usageMatch = usageMatch && currentUsage >= parseInt(usageMinValue);
}
if (usageMaxValue !== '') {
usageMatch = usageMatch && currentUsage <= parseInt(usageMaxValue);
}
// 试用期区间筛选
let trialMatch = true;
const daysRemaining = stripe.days_remaining_on_trial || 0;
if (trialMinValue !== '') {
trialMatch = trialMatch && daysRemaining >= parseInt(trialMinValue);
}
if (trialMaxValue !== '') {
trialMatch = trialMatch && daysRemaining <= parseInt(trialMaxValue);
}
// Profile筛选
let profileMatch = true;
if (profileFilter === 'has_profile') {
profileMatch = Object.keys(profile).length > 0;
} else if (profileFilter === 'no_profile') {
profileMatch = Object.keys(profile).length === 0;
}
// 代理筛选
let proxyMatch = true;
if (proxyFilter !== 'all') {
const tokenProxy = token.tags?.[1] || '';
if (proxyFilter === 'none') {
proxyMatch = tokenProxy === '';
} else {
proxyMatch = tokenProxy === proxyFilter;
}
}
return searchMatch && membershipMatch && usageMatch && trialMatch && profileMatch && proxyMatch;
});
renderTokens(filteredTokens);
// 在状态栏显示筛选结果
updateStatusBar(filteredTokens.length);
}
// 更新状态栏
function updateStatusBar(filteredCount) {
const total = allTokens.length;
const selected = selectedTokens.size;
const selectionStatus = document.getElementById('selectionStatus');
const totalCount = document.getElementById('totalCount');
if (selectionStatus) selectionStatus.textContent = `已选择: ${selected} 个项目`;
if (totalCount) {
// 使用!== undefined判断这样即使filteredCount为0也会正确显示
totalCount.textContent = `${filteredCount !== undefined ? filteredCount : total} 个Token`;
}
}
// 格式化会员类型
function formatMembershipType(type) {
if (!type) return '-';
const types = {
'free': '免费版',
'free_trial': '试用版',
'pro': '专业版',
'enterprise': '企业版'
};
return types[type] || type;
}
// 选择Token
function selectToken(token, append = false) {
if (!token) return;
if (!append) {
clearSelection();
}
selectedTokens.add(token);
const row = document.querySelector(`tr[data-token="${token}"]`);
if (row) {
row.classList.add('selected');
}
updateSelectionStatus();
}
// 取消选择Token
function deselectToken(token) {
if (!token) return;
selectedTokens.delete(token);
const row = document.querySelector(`tr[data-token="${token}"]`);
if (row) {
row.classList.remove('selected');
}
updateSelectionStatus();
}
// 清除所有选择
function clearSelection() {
selectedTokens.clear();
const rows = document.querySelectorAll('#fileListBody tr.selected');
rows.forEach(row => {
row.classList.remove('selected');
});
updateSelectionStatus();
}
// 选择所有Token
function selectAllTokens() {
clearSelection();
const rows = document.querySelectorAll('#fileListBody tr');
rows.forEach(row => {
const token = row.dataset.token;
selectedTokens.add(token);
row.classList.add('selected');
});
updateSelectionStatus();
}
// 更新选择状态
function updateSelectionStatus() {
updateStatusBar();
// 如果只选择了一个Token更新详情
if (selectedTokens.size === 1) {
updateDetailsPanel([...selectedTokens][0]);
}
}
// 查看Token详情
function viewDetails() {
if (selectedTokens.size === 0) return;
// 如果选择了多个Token只显示第一个
const token = [...selectedTokens][0];
updateDetailsPanel(token);
toggleDetailsPanel(true);
// 关闭右键菜单
document.getElementById('contextMenu').style.display = 'none';
}
// 更新详情面板
function updateDetailsPanel(token) {
const tokenObj = allTokens.find(t => t.token === token);
if (!tokenObj) return;
const profile = tokenObj.profile || {};
const user = profile.user || {};
const stripe = profile.stripe || {};
const usage = profile.usage || {};
const premium = usage.premium || {};
document.getElementById('detailsTitle').textContent = user.email || '未知账户';
const content = document.getElementById('detailsContent');
content.innerHTML = `
<div class="details-property">
<div class="details-property-name">Token</div>
<div class="details-property-value">${tokenObj.token}</div>
</div>
<div class="details-property">
<div class="details-property-name">Checksum</div>
<div class="details-property-value">${tokenObj.checksum || '-'}</div>
</div>
<div class="details-property">
<div class="details-property-name">会员类型</div>
<div class="details-property-value">${formatMembershipType(stripe.membership_type)}</div>
</div>
<div class="details-property">
<div class="details-property-name">Premium 用量</div>
<div class="details-property-value">${premium.requests || 0}/${premium.max_requests || '∞'}</div>
</div>
<div class="details-property">
<div class="details-property-name">试用剩余</div>
<div class="details-property-value">${stripe.days_remaining_on_trial > 0 ? `${stripe.days_remaining_on_trial}` : '-'}</div>
</div>
<div class="details-property">
<div class="details-property-name">用户 ID</div>
<div class="details-property-value">${user.id || '-'}</div>
</div>
<div class="details-property">
<div class="details-property-name">创建时间</div>
<div class="details-property-value">${formatDate(user.created_at) || '-'}</div>
</div>
<div class="details-property">
<div class="details-property-name">更新时间</div>
<div class="details-property-value">${formatDate(user.updated_at) || '-'}</div>
</div>
`;
}
// 切换详情面板显示
function toggleDetailsPanel(show) {
const panel = document.getElementById('detailsPanel');
if (show) {
panel.classList.add('open');
document.body.style.overflow = 'hidden'; // 禁用主体页面滚动
detailsPanelOpen = true;
} else {
panel.classList.remove('open');
document.body.style.overflow = ''; // 恢复主体页面滚动
detailsPanelOpen = false;
}
}
// 格式化日期
function formatDate(dateString) {
if (!dateString) return '-';
try {
const date = new Date(dateString);
return date.toLocaleString('zh-CN');
} catch (e) {
return dateString;
}
}
// 复制Token到剪贴板
// 添加兼容性检查和备用方法
function copyTokenToClipboard() {
if (selectedTokens.size === 0) {
showToast('请先选择Token', 'info');
return;
}
const tokensToCopy = [...selectedTokens].join('\n');
// 检查navigator.clipboard是否可用
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
navigator.clipboard.writeText(tokensToCopy)
.then(() => {
showToast(`已复制 ${selectedTokens.size} 个Token到剪贴板`, 'success');
})
.catch(err => {
// 如果剪贴板API失败使用备用方法
fallbackCopy(tokensToCopy);
});
} else {
// 浏览器不支持clipboard API使用备用方法
fallbackCopy(tokensToCopy);
}
// 关闭右键菜单
document.getElementById('contextMenu').style.display = 'none';
}
// 备用复制方法
function fallbackCopy(text) {
// 创建一个临时文本区域
const textArea = document.createElement('textarea');
textArea.value = text;
// 设置文本区域的样式,使其不可见
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
// 选择文本并复制
textArea.focus();
textArea.select();
let success = false;
try {
// 执行复制命令
success = document.execCommand('copy');
} catch (err) {
console.error('复制失败:', err);
}
// 移除临时元素
document.body.removeChild(textArea);
// 显示复制结果
if (success) {
showToast(`已复制 ${selectedTokens.size} 个Token到剪贴板`, 'success');
} else {
showToast('复制失败,请手动复制', 'error');
}
}
// 删除选中的Token
function deleteSelectedTokens(singleToken) {
// 如果提供了单个Token则只删除该Token
if (singleToken) {
tokensToDelete = [singleToken];
} else {
tokensToDelete = [...selectedTokens];
if (tokensToDelete.length === 0) {
showToast('请先选择要删除的Token', 'info');
return;
}
}
const count = tokensToDelete.length;
document.getElementById('confirmMessage').textContent =
count > 1 ? `确定要删除选中的 ${count} 个Token吗` : '确定要删除选中的Token吗';
showModal('confirmModal');
// 关闭右键菜单
document.getElementById('contextMenu').style.display = 'none';
}
// 确认删除Token
async function confirmDeleteTokens() {
if (tokensToDelete.length === 0) return;
closeModal('confirmModal');
showToast('正在删除Token...', 'info');
const data = await makeAuthenticatedRequest('/tokens/delete', {
body: JSON.stringify({
tokens: tokensToDelete,
expectation: 'failed_tokens'
})
});
if (data) {
const failedCount = data.failed_tokens?.length || 0;
const successCount = tokensToDelete.length - failedCount;
const message = `删除成功: ${successCount} 个Token${failedCount ? `,失败: ${failedCount}` : ''}`;
showToast(message, 'success');
clearSelection();
getTokenInfo();
} else {
showToast('删除失败', 'error');
}
tokensToDelete = [];
}
// 刷新选中Token的Profile
async function refreshSelectedProfiles() {
if (selectedTokens.size === 0) {
showToast('请先选择Token', 'info');
return;
}
const tokensToRefresh = [...selectedTokens];
showToast(`正在刷新 ${tokensToRefresh.length} 个Token的Profile...`, 'info');
// 关闭右键菜单
document.getElementById('contextMenu').style.display = 'none';
const data = await makeAuthenticatedRequest('/tokens/profile/update', {
body: JSON.stringify(tokensToRefresh)
});
if (data) {
showToast(`刷新完成: ${data.message}`, 'success');
getTokenInfo();
} else {
showToast('刷新失败', 'error');
}
}
// 批量添加Token
function addTokens() {
showModal('addTokensModal');
}
// 确认添加Token
async function confirmAddTokens() {
const tokensInput = document.getElementById('addTokensInput').value;
if (!tokensInput) {
showToast('请输入要添加的Token', 'warning');
return;
}
closeModal('addTokensModal');
showToast('正在添加Token...', 'info');
// 处理输入的tokens跳过空行和注释解析token和checksum
const tokenList = tokensInput.split('\n')
.map(line => line.trim())
.filter(line => line && !line.startsWith('#'))
.map(line => {
const parts = line.includes(',') ? line.split(',') : [line];
return {
token: parts[0].trim(),
checksum: parts[1]?.trim() || null
};
});
const data = await makeAuthenticatedRequest('/tokens/add', {
body: JSON.stringify({
tokens: tokenList,
tags: []
})
});
if (data) {
showToast(`添加成功: ${data.message}`, 'success');
document.getElementById('addTokensInput').value = '';
getTokenInfo();
} else {
showToast('添加失败', 'error');
}
}
// 导出Token列表
function exportTokens() {
const jsonData = JSON.stringify(allTokens, null, 2);
const blob = new Blob([jsonData], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'tokens_export.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showToast('Token列表已导出', 'success');
}
// 导入Token列表
function importTokens() {
showModal('importModal');
}
// 确认导入
async function confirmImport() {
const fileInput = document.getElementById('importFile');
if (!fileInput.files || fileInput.files.length === 0) {
showToast('请选择要导入的文件', 'warning');
return;
}
const file = fileInput.files[0];
const reader = new FileReader();
reader.onload = async function (e) {
try {
const tokensText = e.target.result;
closeModal('importModal');
showToast('正在导入Token...', 'info');
const data = await makeAuthenticatedRequest('/tokens/update', {
body: JSON.stringify({
tokens: tokensText
})
});
if (data) {
showToast('Token列表导入成功', 'success');
fileInput.value = '';
getTokenInfo();
}
} catch (error) {
showToast('导入失败: ' + error.message, 'error');
}
};
reader.readAsText(file);
}
// 生成Key
function generateKey(token, checksum) {
// 如果未提供token则使用选中的token
if (!token && selectedTokens.size === 0) {
showToast('请先选择Token', 'info');
return;
}
const tokenToUse = token || [...selectedTokens][0];
const checksumToUse = checksum || allTokens.find(t => t.token === tokenToUse)?.checksum || '';
document.getElementById('keyToken').textContent = tokenToUse;
// 重置表单
document.getElementById('disableVision').value = '';
document.getElementById('enableSlowPool').value = '';
document.getElementById('usageCheckType').value = '';
document.getElementById('includeWebReferences').value = '';
document.getElementById('keyContent').textContent = '';
document.getElementById('keyResult').style.display = 'none';
// 更新代理选择下拉框
updateProxySelect();
showModal('keyModal');
// 关闭右键菜单
document.getElementById('contextMenu').style.display = 'none';
}
// 生成Key提交
async function generateKeySubmit() {
const token = document.getElementById('keyToken').textContent;
const tokenObj = allTokens.find(t => t.token === token);
if (!token || !tokenObj) {
showToast('缺少Token', 'error');
return;
}
const disableVision = document.getElementById('disableVision').value;
const enableSlowPool = document.getElementById('enableSlowPool').value;
const usageCheckType = document.getElementById('usageCheckType').value;
const includeWebReferences = document.getElementById('includeWebReferences').value;
const proxyName = document.getElementById('proxySelect').value;
// 构建请求体
const requestBody = {
auth_token: tokenObj.checksum ? `${token},${tokenObj.checksum}` : token
};
// 添加可选参数
if (disableVision) {
requestBody.disable_vision = disableVision === 'true';
}
if (enableSlowPool) {
requestBody.enable_slow_pool = enableSlowPool === 'true';
}
if (includeWebReferences) {
requestBody.include_web_references = includeWebReferences === 'true';
}
// 如果选择了特定代理,则使用选择的代理
if (proxyName) {
requestBody.proxy_name = proxyName;
} else {
// 否则使用Token的tags中设置的代理
const tags = tokenObj.tags || [];
const tokenProxyName = tags[1] || '';
if (tokenProxyName) {
requestBody.proxy_name = tokenProxyName;
}
}
if (usageCheckType) {
requestBody.usage_check_models = {
type: usageCheckType
};
if (usageCheckType === 'custom') {
const selectedModels = Array.from(document.querySelectorAll('#modelListContainer input:checked'))
.map(input => input.value);
if (selectedModels.length > 0) {
requestBody.usage_check_models.model_ids = selectedModels.join(',');
}
}
}
try {
const response = await makeAuthenticatedRequest('/build-key', {
method: 'POST',
body: JSON.stringify(requestBody)
});
if (response && response.key) {
document.getElementById('keyContent').textContent = response.key;
document.getElementById('keyResult').style.display = 'block';
showToast('Key已生成点击复制', 'success');
} else {
showToast('生成Key失败: ' + (response.error || '未知错误'), 'error');
}
} catch (error) {
showToast('生成Key失败: ' + error.message, 'error');
}
}
// 复制生成的Key
function copyGeneratedKey() {
const key = document.getElementById('keyContent').textContent;
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
navigator.clipboard.writeText(key)
.then(() => {
showToast('Key已复制到剪贴板', 'success');
})
.catch(err => {
fallbackCopy(key);
});
} else {
fallbackCopy(key);
}
}
// 获取模型列表
async function getModels() {
const data = await (await fetch('/health')).json();
if (data && data.models) {
availableModels = data.models;
// 填充模型列表
renderModelList();
}
}
// 渲染模型列表
function renderModelList() {
const container = document.getElementById('modelListContainer');
container.innerHTML = availableModels.map(model => `
<div class="model-item">
<input type="checkbox" id="model_${model}" value="${model}">
<label for="model_${model}">${model}</label>
</div>
`).join('');
}
// 切换模型列表显示
function toggleModelList() {
const usageCheckType = document.getElementById('usageCheckType').value;
const container = document.getElementById('modelListContainer');
if (usageCheckType === 'custom') {
container.style.display = 'block';
} else {
container.style.display = 'none';
}
}
// 显示对话框
function showModal(modalId) {
const modal = document.getElementById(modalId);
const backdrop = document.getElementById(`${modalId}-backdrop`);
if (modal && backdrop) {
modal.style.display = 'block';
backdrop.style.display = 'block';
document.body.style.overflow = 'hidden'; // 禁用主体页面滚动
}
}
// 关闭对话框
function closeModal(modalId) {
const modal = document.getElementById(modalId);
const backdrop = document.getElementById(`${modalId}-backdrop`);
document.body.style.overflow = ''; // 恢复主体页面滚动
if (modal && backdrop) {
modal.style.display = 'none';
backdrop.style.display = 'none';
}
}
// 关闭所有对话框
function closeAllModals() {
const modals = document.querySelectorAll('.modal');
const backdrops = document.querySelectorAll('.modal-backdrop');
document.body.style.overflow = ''; // 恢复主体页面滚动
modals.forEach(modal => {
modal.style.display = 'none';
});
backdrops.forEach(backdrop => {
backdrop.style.display = 'none';
});
}
// 获取代理列表
async function getProxies() {
try {
const data = await makeAuthenticatedRequest('/proxies/get');
if (data && data.proxies) {
// 从代理对象中提取代理名称列表
proxyList = Object.keys(data.proxies.proxies || {});
currentProxy = data.general_proxy || '';
return true;
}
} catch (error) {
console.error('获取代理列表失败:', error);
}
return false;
}
// 显示通知消息
// 显示通知消息优化版本
function showToast(message, type = 'info', duration = 3000) {
// 获取或创建通知容器
let container = document.getElementById('toast-container');
if (!container) {
container = document.createElement('div');
container.id = 'toast-container';
container.className = 'toast-container';
document.body.appendChild(container);
}
// 创建新的通知元素
const toast = document.createElement('div');
toast.className = `toast ${type}`;
// 添加文本容器
toast.innerHTML = `<div class="toast-content">${message}</div>`;
// 添加到容器
container.appendChild(toast);
// 强制一次重排
void toast.offsetWidth;
// 显示通知使用requestAnimationFrame确保平滑过渡
requestAnimationFrame(() => {
toast.classList.add('show');
});
// 设置自动移除
setTimeout(() => {
toast.classList.remove('show');
// 通知消失动画完成后移除元素
setTimeout(() => {
if (container.contains(toast)) {
container.removeChild(toast);
}
// 如果没有更多通知,移除容器
if (container.children.length === 0) {
if (document.body.contains(container)) {
document.body.removeChild(container);
}
}
}, 400);
}, duration);
// 限制最大显示数量,如果超过则移除最早的
const maxToasts = 5; // 最大同时显示的通知数
const toasts = container.querySelectorAll('.toast');
if (toasts.length > maxToasts) {
const oldestToast = toasts[0]; // 最早的通知
oldestToast.classList.remove('show');
setTimeout(() => {
if (container.contains(oldestToast)) {
container.removeChild(oldestToast);
}
}, 400);
}
}
// 打开代理选择器
function openProxySelector(displayName, token, event) {
// 阻止事件冒泡,避免触发行选择
event.stopPropagation();
const tokenObj = allTokens.find(t => t.token === token);
if (!tokenObj) return;
currentEditingToken = token;
// 显示当前token信息
document.getElementById('currentTokenDisplay').textContent = displayName;
// 填充代理下拉框
const proxySelect = document.getElementById('proxySelectDropdown');
// 清空现有选项(保留第一个"未指定"选项)
while (proxySelect.options.length > 1) {
proxySelect.remove(1);
}
// 使用公共方法获取代理使用统计
const proxyUsage = getProxyUsageStats();
// 添加代理选项
proxyList.forEach(proxy => {
const option = document.createElement('option');
option.value = proxy;
option.textContent = `${proxy} (${proxyUsage[proxy] || 0})`;
proxySelect.appendChild(option);
});
// 设置当前选中的代理
const currentProxy = tokenObj.tags?.[1] || '';
proxySelect.value = currentProxy;
// 显示模态框
showModal('proxySelectModal');
}
// 保存代理选择
async function saveProxySelection() {
if (!currentEditingToken) return;
closeModal('proxySelectModal')
const selectedProxy = document.getElementById('proxySelectDropdown').value;
const tags = selectedProxy ? ['', selectedProxy] : [];
showToast('正在更新代理设置...', 'info');
const data = await makeAuthenticatedRequest('/tokens/tags/update', {
method: 'POST',
body: JSON.stringify({
tokens: [currentEditingToken],
tags: tags
})
});
if (data && data.status === 'success') {
showToast('代理设置成功', 'success');
closeModal('proxySelectModal');
getTokenInfo(); // 刷新Token列表
} else {
showToast('代理设置失败', 'error');
}
}
// 抽取统计代理使用数量的公共方法
function getProxyUsageStats() {
const proxyUsage = {};
allTokens.forEach(token => {
const proxy = token.tags?.[1] || '';
proxyUsage[proxy] = (proxyUsage[proxy] || 0) + 1;
});
return proxyUsage;
}
</script>
</body>
</html>