Files
cursor-api/static/tokens.html
2025-07-27 09:04:19 +08:00

3743 lines
111 KiB
HTML
Raw Permalink 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-CN">
<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);
height: 80vh;
overflow-y: auto;
}
.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;
}
/* 时区名称和代理名称共享样式 */
.timezone-name,
.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;
}
.timezone-name:hover,
.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);
}
.timezone-name:active,
.proxy-name:active {
transform: translateY(0px);
box-shadow: none;
}
.timezone-name:hover::after,
.proxy-name:hover::after {
opacity: 1;
}
/* 状态指示标样式 */
.status-indicator {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 5px;
}
.status-enabled {
background-color: #4caf50;
/* 绿色 */
}
.status-disabled {
background-color: #f44336;
/* 红色 */
}
.status-other {
background-color: #ffc107;
/* 黄色 */
}
/* 状态子菜单样式 */
.status-menu {
position: relative;
}
.status-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: 150px;
z-index: 1001;
opacity: 0;
visibility: hidden;
transition:
opacity 0.2s,
visibility 0.2s;
}
.status-menu:hover .status-submenu {
opacity: 1;
visibility: visible;
}
/* 添加向上和向左打开的样式 */
.status-menu.open-upward .status-submenu {
top: auto;
bottom: 0;
}
.status-menu.open-leftward .status-submenu {
left: auto;
right: 100%;
}
/* 添加状态计数样式 */
.status-count {
color: var(--text-secondary);
font-size: 12px;
margin-left: 8px;
}
/* 复制成功时的视觉反馈 */
.copied {
position: relative;
animation: copySuccess 0.3s ease-in-out;
}
@keyframes copySuccess {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
background-color: var(--primary-color-alpha);
}
100% {
transform: scale(1);
}
}
/* 内联编辑输入框样式 */
.inline-edit {
background: transparent;
border: 1px solid var(--primary-color);
border-radius: 3px;
padding: 2px 6px;
font-size: inherit;
font-family: inherit;
color: inherit;
width: 100%;
min-width: 150px;
outline: none;
}
.inline-edit:focus {
background: var(--card-background);
box-shadow: 0 0 0 2px var(--primary-color-alpha);
}
</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="pro_plus">专业增强版</option>
<option value="ultra">旗舰版</option>
<option value="enterprise">企业版</option>
</select>
</div>
<div class="filter-group">
<label>时区筛选:</label>
<select id="timezoneFilter">
<option value="all">全部时区</option>
<option value="none">未指定时区</option>
<!-- 时区列表将在JavaScript中动态填充 -->
</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>令牌</th>
<th style="width: 15%">会员类型</th>
<th style="width: 15%">用量</th>
<th style="width: 10%">试用剩余</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 status-menu">
<span>切换状态</span>
<div class="status-submenu" id="statusSubmenu">
<div class="context-menu-item" onclick="setTokenStatus('enabled')">
<span>启用</span>
<span class="status-count">0</span>
<span class="check-mark"></span>
</div>
<div class="context-menu-item" onclick="setTokenStatus('disabled')">
<span>禁用</span>
<span class="status-count">0</span>
<span class="check-mark"></span>
</div>
</div>
</div>
<div class="context-menu-item" onclick="viewDetails()">
<span>查看详情</span>
<span class="context-menu-shortcut">Enter</span>
</div>
<div class="context-menu-item" onclick="renameToken()">
<span>重命名</span>
<span class="context-menu-shortcut">F2</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="upgradeSelectedTokens()">
<span>刷新Token</span>
<span class="context-menu-shortcut">Ctrl+U</span>
</div>
<div class="context-menu-item" onclick="refreshSelectedConfigVersions()">
<span>刷新Config Version</span>
<span class="context-menu-shortcut">F6</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" onclick="openTimezoneSelector()">
<span>设置时区</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="refreshSelectedConfigVersions()" class="secondary">
刷新Config Version
</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 class="form-group">
<label>Token状态:</label>
<select id="addTokensStatus">
<option value="enabled">启用</option>
<option value="disabled">禁用</option>
</select>
</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>令牌:</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>
<!-- 时区选择对话框 -->
<div class="modal-backdrop" id="timezoneModal-backdrop"></div>
<div class="modal" id="timezoneModal">
<div class="modal-header">
<h3>设置时区</h3>
<button class="modal-close" onclick="closeModal('timezoneModal')">
×
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>令牌:</label>
<div
id="timezoneTokenDisplay"
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>
<input
type="text"
id="timezoneSearchInput"
placeholder="输入关键词搜索时区..."
style="width: 100%"
/>
</div>
<div class="form-group">
<label>时区列表:</label>
<div
id="timezoneListContainer"
style="
max-height: 300px;
overflow-y: auto;
margin-top: 8px;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
"
>
<!-- 时区列表将在这里动态生成 -->
</div>
</div>
</div>
<div class="modal-footer">
<button onclick="closeModal('timezoneModal')" class="secondary">
取消
</button>
<button onclick="saveTimezoneSelection()" class="primary">保存</button>
</div>
</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);
document
.getElementById("timezoneFilter")
.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 && !detailsPanelOpen) {
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 "F6":
e.preventDefault();
if (selectedTokens.size > 0) {
refreshSelectedConfigVersions();
}
break;
case "u":
if (modifierKey) {
e.preventDefault();
upgradeSelectedTokens();
}
break;
case "g":
if (modifierKey) {
e.preventDefault();
generateKey();
}
break;
case "f":
if (modifierKey) {
e.preventDefault();
document.getElementById("searchInput").focus();
}
break;
case "F2":
e.preventDefault();
if (selectedTokens.size === 1) {
startRenaming([...selectedTokens][0]);
} else if (selectedTokens.size > 1) {
showToast("一次只能重命名一个Token", "info");
} else {
showToast("请先选择要重命名的Token", "info");
}
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);
updateSelectionStatus();
}
// 设置当前处理的token
currentToken = token;
// 更新代理子菜单
updateProxySubmenu();
// 更新状态子菜单
updateStatusSubmenu();
// 更新多选时的菜单项文本
if (selectedTokens.size > 1) {
document.querySelector(
'.context-menu-item[onclick="viewDetails()"] span:first-child',
).textContent = "查看详情(单个)";
document.querySelector(
'.context-menu-item[onclick="refreshSelectedProfiles()"] span:first-child',
).textContent = `刷新Profile(已选${selectedTokens.size}个)`;
document.querySelector(
'.context-menu-item[onclick="refreshSelectedConfigVersions()"] span:first-child',
).textContent = `刷新Config Version(已选${selectedTokens.size}个)`;
document.querySelector(
'.context-menu-item[onclick="upgradeSelectedTokens()"] span:first-child',
).textContent = `刷新Token(已选${selectedTokens.size}个)`;
document.querySelector(
'.context-menu-item[onclick="openTimezoneSelector()"] span:first-child',
).textContent = `设置时区(已选${selectedTokens.size}个)`;
document.querySelector(
".context-menu-item.proxy-menu span:first-child",
).textContent = `设置代理(已选${selectedTokens.size}个)`;
document.querySelector(
'.context-menu-item[onclick="deleteSelectedTokens()"] span:first-child',
).textContent = `删除(已选${selectedTokens.size}个)`;
} else {
document.querySelector(
'.context-menu-item[onclick="viewDetails()"] span:first-child',
).textContent = "查看详情";
document.querySelector(
'.context-menu-item[onclick="refreshSelectedProfiles()"] span:first-child',
).textContent = "刷新Profile";
document.querySelector(
'.context-menu-item[onclick="refreshSelectedConfigVersions()"] span:first-child',
).textContent = "刷新Config Version";
document.querySelector(
'.context-menu-item[onclick="upgradeSelectedTokens()"] span:first-child',
).textContent = "刷新Token";
document.querySelector(
'.context-menu-item[onclick="openTimezoneSelector()"] span:first-child',
).textContent = "设置时区";
document.querySelector(
".context-menu-item.proxy-menu span:first-child",
).textContent = "设置代理";
document.querySelector(
'.context-menu-item[onclick="deleteSelectedTokens()"] span:first-child',
).textContent = "删除";
}
// 计算菜单位置
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) >> 1);
}
}
// 应用计算后的位置
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对象 - 格式: [index, alias, tokenData]
const selectedTokenArrs = allTokens.filter((tokenArr) => {
const [_, __, tokenData] = tokenArr;
return selectedTokens.has(tokenData.bundle.primary_token);
});
const selectionSize = selectedTokenArrs.length;
// 计算选中项中的代理使用情况
const selectedProxyUsage = { "": 0 }; // 初始化 "未指定" 计数
proxyList.forEach((proxy) => {
selectedProxyUsage[proxy] = 0;
}); // 初始化已知代理计数
selectedTokenArrs.forEach((tokenArr) => {
const [_, __, tokenData] = tokenArr;
const proxy = tokenData.bundle.proxy || ""; // 获取选中 token 的代理
// 确保计数对象里有这个key
if (!(proxy in selectedProxyUsage)) {
selectedProxyUsage[proxy] = 0;
}
selectedProxyUsage[proxy]++; // 增加对应代理的计数
});
// 构建 "未指定" 选项
const isUnspecifiedActive =
selectionSize > 0 && selectedProxyUsage[""] === selectionSize;
let html = `
<div class="context-menu-item ${isUnspecifiedActive ? "active" : ""}" onclick="setProxy('')">
<span>未指定</span>
<span class="proxy-count">${selectedProxyUsage[""]}</span>
<span class="check-mark"></span>
</div>
<div class="context-menu-divider"></div>
`;
// 添加所有可用代理选项
if (proxyList.length > 0) {
proxyList.forEach((proxy) => {
if (!proxy) return; // 跳过空代理名
const count = selectedProxyUsage[proxy] || 0; // 获取选中项中使用这个代理的数量
// 检查是否所有选中的 Token 都使用了此代理
const isActive =
selectionSize > 0 && count === selectionSize && 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");
}
}
// 获取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;
const timezoneFilter = document.getElementById("timezoneFilter").value; // 保存时区筛选条件
// 刷新代理列表
await getProxies();
const data = await makeAuthenticatedRequest("/tokens/get");
if (data && data.tokens) {
// 直接使用新的数据格式
allTokens = data.tokens;
renderTokens(allTokens);
updateStatusBar();
updateProxyFilter(); // 更新代理筛选下拉框
updateTimezoneFilter(); // 更新时区筛选下拉框
updateStatusSubmenu(); // 更新状态子菜单
// 恢复筛选条件
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;
document.getElementById("timezoneFilter").value = timezoneFilter; // 恢复时区筛选条件
// 应用筛选
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;
}
// 获取选中token的别名 - 格式: [index, alias, tokenData]
const aliasesToUpdate = [...selectedTokens].map((token) => {
const tokenArr = allTokens.find((arr) => {
const [_, __, tokenData] = arr;
return tokenData.bundle.primary_token === token;
});
return tokenArr ? tokenArr[1] : token; // tokenArr[1] 是 alias
});
showToast("正在更新代理设置...", "info");
try {
const data = await makeAuthenticatedRequest("/tokens/proxy/set", {
body: JSON.stringify({
aliases: aliasesToUpdate,
proxy: proxyName || null, // 空字符串发送null以清除代理
}),
});
if (data && data.status === "success") {
showToast(data.message || "代理设置成功", "success");
getTokenInfo(); // 刷新Token列表
} else {
showToast("代理设置失败", "error");
}
} catch (error) {
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((tokenArr, idx) => {
// 格式: [index, alias, tokenData]
const [index, alias, tokenData] = tokenArr;
const bundle = tokenData.bundle || {};
const user = bundle.user || {};
const stripe = tokenData.stripe || {};
const usage = tokenData.usage || {};
const premium = usage.premium || {};
const token = bundle.primary_token;
const checksum = bundle.checksum
? `${bundle.checksum.first}${bundle.checksum.second}`
: "";
const status = tokenData.status;
const timezone = bundle.timezone || "";
const proxy = bundle.proxy || "";
const displayName =
alias || user.email || token.substring(0, 15) + "...";
return `
<tr data-token="${token}" data-alias="${alias}" data-checksum="${checksum}" data-index="${idx}" class="${selectedTokens.has(token) ? "selected" : ""}">
<td>
<span class="status-indicator ${status === "disabled" ? "status-disabled" : status === undefined ? "status-enabled" : "status-" + status}"></span>
<span class="file-icon">🔑</span>
${displayName}
</td>
<td>${formatMembershipType(stripe.membership_type)}</td>
<td>${premium.num_requests || 0}/${premium.max_requests || "∞"}</td>
<td>${stripe.days_remaining_on_trial > 0 ? `${stripe.days_remaining_on_trial}` : "-"}</td>
<td><span class="timezone-name" onclick="openTimezoneSelectorForToken('${token}', event)">${timezone || "未指定"}</span></td>
<td><span class="proxy-name" onclick="openProxySelector('${displayName}','${token}', event)">${proxy || "未指定"}</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") ||
e.target.classList.contains("timezone-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 openTimezoneSelectorForToken(token, event) {
// 阻止事件冒泡,避免触发行选择
event.stopPropagation();
// 选中该Token
clearSelection();
selectToken(token);
// 打开时区选择器
openTimezoneSelector();
}
// 筛选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 timezoneFilter = document.getElementById("timezoneFilter").value; // 新增时区筛选
const filteredTokens = allTokens.filter((tokenArr) => {
// 格式: [index, alias, tokenData]
const [index, alias, tokenData] = tokenArr;
const bundle = tokenData.bundle || {};
const user = bundle.user || {};
const stripe = tokenData.stripe || {};
const usage = tokenData.usage || {};
const premium = usage.premium || {};
const token = bundle.primary_token;
const timezone = bundle.timezone || "";
const proxy = bundle.proxy || "";
// 搜索条件
const emailMatch = (user.email || "")
.toLowerCase()
.includes(searchTerm);
const tokenMatch = (token || "").toLowerCase().includes(searchTerm);
const aliasMatch = (alias || "").toLowerCase().includes(searchTerm);
const searchMatch =
searchTerm === "" || emailMatch || tokenMatch || aliasMatch;
// 会员类型筛选
const membershipType = stripe.membership_type || "";
let membershipMatch = true;
if (membershipFilter !== "all") {
membershipMatch = membershipType === membershipFilter;
}
// 使用量区间筛选
let usageMatch = true;
const currentUsage = premium.num_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 = user && Object.keys(user).length > 0;
} else if (profileFilter === "no_profile") {
profileMatch = !user || Object.keys(user).length === 0;
}
// 代理筛选
let proxyMatch = true;
if (proxyFilter !== "all") {
if (proxyFilter === "none") {
proxyMatch = proxy === "";
} else {
proxyMatch = proxy === proxyFilter;
}
}
// 时区筛选
let timezoneMatch = true;
if (timezoneFilter !== "all") {
if (timezoneFilter === "none") {
timezoneMatch = timezone === "";
} else {
timezoneMatch = timezone === timezoneFilter;
}
}
return (
searchMatch &&
membershipMatch &&
usageMatch &&
trialMatch &&
profileMatch &&
proxyMatch &&
timezoneMatch
);
});
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: "专业版",
pro_plus: "专业增强版",
ultra: "旗舰版",
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) {
// 查找token对象 - 格式: [index, alias, tokenData]
const tokenArr = allTokens.find((arr) => {
const [_, __, tokenData] = arr;
return tokenData.bundle.primary_token === token;
});
if (!tokenArr) return;
const [index, alias, tokenData] = tokenArr;
const bundle = tokenData.bundle || {};
const user = bundle.user || {};
const stripe = tokenData.stripe || {};
const usage = tokenData.usage || {};
const premium = usage.premium || {};
const timezone = bundle.timezone || "-";
const proxy = bundle.proxy || "-";
const checksum = bundle.checksum
? `${bundle.checksum.first}${bundle.checksum.second}`
: "-";
document.getElementById("detailsTitle").textContent =
user.email || "未知账户";
const content = document.getElementById("detailsContent");
content.innerHTML = `
<div class="details-property">
<div class="details-property-name">别名</div>
<div class="details-property-value">${alias}</div>
</div>
<div class="details-property">
<div class="details-property-name">令牌</div>
<div class="details-property-value">${bundle.primary_token}</div>
</div>
<div class="details-property">
<div class="details-property-name">校验和</div>
<div class="details-property-value">${checksum}</div>
</div>
<div class="details-property">
<div class="details-property-name">Config Version</div>
<div class="details-property-value">${bundle.config_version || "-"}</div>
</div>
<div class="details-property">
<div class="details-property-name">Session ID</div>
<div class="details-property-value">${bundle.session_id || "-"}</div>
</div>
<div class="details-property">
<div class="details-property-name">时区</div>
<div class="details-property-value">${timezone}</div>
</div>
<div class="details-property">
<div class="details-property-name">代理</div>
<div class="details-property-value">${proxy}</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.num_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复制到剪贴板
* 格式:有别名时为 "{alias}::{token},{checksum}",无别名时为 "{token},{checksum}"
*/
async function copyTokenToClipboard() {
if (selectedTokens.size === 0) {
showToast("请先选择Token", "info");
return;
}
// 获取选中的token对象 - 格式: [index, alias, tokenData]
const selectedTokenArrs = allTokens.filter((tokenArr) => {
const [_, __, tokenData] = tokenArr;
return selectedTokens.has(tokenData.bundle.primary_token);
});
const tokensToCopy = selectedTokenArrs
.map(formatTokenForCopy)
.join("\n");
const success = await copyToClipboard(tokensToCopy, {
showMessage: false,
onSuccess: () =>
showToast(
`已复制 ${selectedTokens.size} 个Token到剪贴板`,
"success",
),
onError: () => showToast("复制失败,请手动复制", "error"),
});
// 关闭右键菜单
document.getElementById("contextMenu").style.display = "none";
}
/**
* 格式化单个Token对象为复制格式
* @param {Array} tokenArr - Token数组 [index, alias, tokenData]
* @returns {string} 格式化后的字符串
*/
function formatTokenForCopy(tokenArr) {
const [index, alias, tokenData] = tokenArr;
const token = tokenData.bundle.primary_token;
const checksum = tokenData.bundle.checksum
? `${tokenData.bundle.checksum.first}${tokenData.bundle.checksum.second}`
: "";
const checksumPart = checksum ? `,${checksum}` : "";
return alias
? `${alias}::${token}${checksumPart}`
: `${token}${checksumPart}`;
} // 删除选中的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");
// 将token转换为alias - 格式: [index, alias, tokenData]
const aliasesToDelete = tokensToDelete.map((token) => {
const tokenArr = allTokens.find((arr) => {
const [_, __, tokenData] = arr;
return tokenData.bundle.primary_token === token;
});
return tokenArr ? tokenArr[1] : token; // tokenArr[1] 是 alias
});
const data = await makeAuthenticatedRequest("/tokens/del", {
body: JSON.stringify({
aliases: aliasesToDelete,
include_failed_tokens: true,
}),
});
if (data) {
const failedCount = data.failed_tokens?.length || 0;
const successCount = aliasesToDelete.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;
}
// 将token转换为alias - 格式: [index, alias, tokenData]
const aliasesToRefresh = [...selectedTokens].map((token) => {
const tokenArr = allTokens.find((arr) => {
const [_, __, tokenData] = arr;
return tokenData.bundle.primary_token === token;
});
return tokenArr ? tokenArr[1] : token; // tokenArr[1] 是 alias
});
showToast(
`正在刷新 ${aliasesToRefresh.length} 个Token的Profile...`,
"info",
);
// 关闭右键菜单
document.getElementById("contextMenu").style.display = "none";
const data = await makeAuthenticatedRequest("/tokens/profile/update", {
body: JSON.stringify(aliasesToRefresh),
});
if (data) {
showToast(`刷新完成: ${data.message}`, "success");
getTokenInfo();
} else {
showToast("刷新失败", "error");
}
}
// 刷新选中Token的配置版本
async function refreshSelectedConfigVersions() {
if (selectedTokens.size === 0) {
showToast("请先选择Token", "info");
return;
}
// 将token转换为alias - 格式: [index, alias, tokenData]
const aliasesToRefresh = [...selectedTokens].map((token) => {
const tokenArr = allTokens.find((arr) => {
const [_, __, tokenData] = arr;
return tokenData.bundle.primary_token === token;
});
return tokenArr ? tokenArr[1] : token; // tokenArr[1] 是 alias
});
showToast(
`正在刷新 ${aliasesToRefresh.length} 个Token的Config Version...`,
"info",
);
// 关闭右键菜单
document.getElementById("contextMenu").style.display = "none";
const data = await makeAuthenticatedRequest(
"/tokens/config-version/update",
{
body: JSON.stringify(aliasesToRefresh),
},
);
if (data) {
showToast(`刷新完成: ${data.message}`, "success");
getTokenInfo();
} else {
showToast("刷新失败", "error");
}
}
// 批量添加Token
function addTokens() {
showModal("addTokensModal");
}
/**
* 添加Token确认处理
* - 验证输入
* - 解析Token列表
* - 提交请求
* - 更新UI状态
*/
async function confirmAddTokens() {
const tokensInput = document.getElementById("addTokensInput").value;
const status = document.getElementById("addTokensStatus").value;
if (!tokensInput) {
showToast("请输入要添加的Token", "warning");
return;
}
closeModal("addTokensModal");
showToast("正在添加Token...", "info");
const tokenList = tokensInput
.split("\n")
.map((line) => line.trim())
.filter((line) => line && !line.startsWith("#"))
.map((line) => ({
...parseTokenLine(line),
status: status,
}));
const data = await makeAuthenticatedRequest("/tokens/add", {
body: JSON.stringify({
tokens: tokenList,
tags: {},
status: status,
}),
});
if (data) {
showToast(`添加成功: ${data.message}`, "success");
document.getElementById("addTokensInput").value = "";
getTokenInfo();
} else {
showToast("添加失败", "error");
}
}
/**
* 解析Token行
* 支持格式:
* - alias::token,checksum
* - alias%3A%3Atoken,checksum
* - token,checksum
* - alias::token
* - alias%3A%3Atoken
* - token
* @param {string} line 非空输入行
* @returns {{alias: string|null, token: string, checksum: string|null}}
*/
function parseTokenLine(line) {
const [main, checksum] = line.split(",").map((s) => s?.trim() || null);
const separator = "::";
const encodedSeparator = "%3A%3A";
const sepIndex = main.includes(separator)
? main.indexOf(separator)
: main.indexOf(encodedSeparator);
const sepLength = main.includes(separator) ? 2 : 6;
const hasAlias = sepIndex !== -1;
const alias = hasAlias
? main.substring(0, sepIndex).trim() || null
: null;
const token = hasAlias
? main.substring(sepIndex + sepLength).trim()
: main;
return { alias, token, checksum };
}
// 导出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 tokensJson = JSON.parse(e.target.result);
if (!Array.isArray(tokensJson)) {
throw new Error("导入的数据格式不正确,应为数组");
}
// 处理数据格式转换:从 [number, string, tokenData] 转换为 [string, tokenData]
// 删除数组的第一项 number保留 alias 和 tokenData
const processedTokens = tokensJson.map((tokenArr) => {
if (Array.isArray(tokenArr) && tokenArr.length >= 3) {
// 删除第一项 number保留 alias 和 tokenData
return [tokenArr[1], tokenArr[2]];
} else if (Array.isArray(tokenArr) && tokenArr.length === 2) {
// 如果已经是正确格式,直接返回
return tokenArr;
} else {
throw new Error(`无效的Token数据格式期望数组长度为2或3实际长度为${Array.isArray(tokenArr) ? tokenArr.length : '非数组'}`);
}
});
closeModal("importModal");
showToast("正在导入Token...", "info");
const data = await makeAuthenticatedRequest("/tokens/set", {
body: JSON.stringify(processedTokens),
});
if (data) {
showToast("Token列表导入成功", "success");
fileInput.value = "";
getTokenInfo();
} else {
showToast("导入失败,服务器未返回有效响应", "error");
}
} 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]; // 查找token的checksum - 格式: [index, alias, tokenData]
let checksumToUse = checksum;
if (!checksumToUse) {
const tokenArr = allTokens.find((arr) => {
const [_, __, tokenData] = arr;
return tokenData.bundle.primary_token === tokenToUse;
});
if (tokenArr) {
const [_, __, tokenData] = tokenArr;
const bundle = tokenData.bundle;
checksumToUse = bundle.checksum
? `${bundle.checksum.first}${bundle.checksum.second}`
: "";
}
}
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; // 查找token对象 - 格式: [index, alias, tokenData]
const tokenArr = allTokens.find((arr) => {
const [_, __, tokenData] = arr;
return tokenData.bundle.primary_token === token;
});
if (!token || !tokenArr) {
showToast("缺少Token", "error");
return;
}
const [_, __, tokenData] = tokenArr;
const bundle = tokenData.bundle;
// 验证必需字段
if (
!bundle.checksum ||
!bundle.checksum.first ||
!bundle.checksum.second
) {
showToast("Token缺少校验和信息无法生成Key", "error");
return;
}
if (!bundle.client_key) {
showToast("Token缺少客户端密钥无法生成Key", "error");
return;
}
if (!bundle.config_version) {
showToast("Token缺少配置版本无法生成Key", "error");
return;
}
if (!bundle.session_id) {
showToast("Token缺少会话ID无法生成Key", "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 = {
token: bundle.primary_token,
checksum: {
first: bundle.checksum.first,
second: bundle.checksum.second,
},
client_key: bundle.client_key,
config_version: bundle.config_version,
session_id: bundle.session_id,
};
// 添加可选参数
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的bundle中设置的代理
const tokenProxyName = bundle.proxy || "";
if (tokenProxyName) {
requestBody.proxy_name = tokenProxyName;
}
}
// 时区设置
if (bundle.timezone) {
requestBody.timezone = bundle.timezone;
}
// GCPP Host设置
if (bundle.gcpp_host) {
requestBody.gcpp_host = bundle.gcpp_host;
}
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", {
body: JSON.stringify(requestBody),
});
if (response && response.keys && response.keys.length > 0) {
document.getElementById("keyContent").textContent =
response.keys[0];
document.getElementById("keyResult").style.display = "block";
showToast("Key已生成点击复制", "success");
} else {
showToast(
"生成Key失败: " + (response.error || "未知错误"),
"error",
);
}
} catch (error) {
showToast("生成Key失败: " + error.message, "error");
}
}
// 复制生成的Key
async function copyGeneratedKey() {
const key = document.getElementById("keyContent").textContent;
await copyToClipboard(key, {
showMessage: false,
successMessage: "Key已复制到剪贴板",
errorMessage: "Key复制失败请手动复制",
onSuccess: () => showToast("Key已复制到剪贴板", "success"),
onError: () => showToast("Key复制失败请手动复制", "error"),
sourceElement: document.getElementById("keyResult"),
});
}
// 获取模型列表
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) {
// 从代理对象中提取代理名称列表
proxyList = Object.keys(data.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();
// 查找token对象 - 格式: [index, alias, tokenData]
const tokenArr = allTokens.find((arr) => {
const [_, __, tokenData] = arr;
return tokenData.bundle.primary_token === token;
});
if (!tokenArr) return;
const [_, __, tokenData] = tokenArr;
const bundle = tokenData.bundle;
currentEditingToken = token;
// 显示当前token信息
document.getElementById("currentTokenDisplay").textContent =
displayName;
// 填充代理下拉框
const proxySelect = document.getElementById("proxySelectDropdown");
// 清空现有选项(保留第一个"未指定"选项)
while (proxySelect.options.length > 1) {
proxySelect.remove(1);
}
// 使用公共方法获取代理使用统计
const proxyUsage = getProxyUsageStats();
// 获取当前选中的代理
const currentProxy = bundle.proxy || "";
// 添加代理选项
proxyList.forEach((proxy) => {
const option = document.createElement("option");
option.value = proxy;
// 如果是当前token使用的代理加粗显示
if (proxy === currentProxy) {
option.innerHTML = `<strong>${proxy}</strong> (${proxyUsage[proxy] || 0})`;
} else {
option.textContent = `${proxy} (${proxyUsage[proxy] || 0})`;
}
proxySelect.appendChild(option);
});
// 设置当前选中的代理
proxySelect.value = currentProxy;
// 显示模态框
showModal("proxySelectModal");
}
// 保存代理选择
async function saveProxySelection() {
if (!currentEditingToken) return;
closeModal("proxySelectModal");
const selectedProxy = document.getElementById(
"proxySelectDropdown",
).value;
// 获取token的别名 - 格式: [index, alias, tokenData]
const tokenArr = allTokens.find((arr) => {
const [_, __, tokenData] = arr;
return tokenData.bundle.primary_token === currentEditingToken;
});
const alias = tokenArr ? tokenArr[1] : currentEditingToken; // tokenArr[1] 是 alias
showToast("正在更新代理设置...", "info");
try {
const data = await makeAuthenticatedRequest("/tokens/proxy/set", {
body: JSON.stringify({
aliases: [alias],
proxy: selectedProxy || null, // 空字符串发送null以清除代理
}),
});
if (data && data.status === "success") {
showToast(data.message || "代理设置成功", "success");
getTokenInfo(); // 刷新Token列表
} else {
showToast("代理设置失败", "error");
}
} catch (error) {
showToast("代理设置失败", "error");
}
}
// 抽取统计代理使用数量的公共方法
function getProxyUsageStats() {
const proxyUsage = {};
allTokens.forEach((tokenArr) => {
const [_, __, tokenData] = tokenArr;
const proxy = tokenData.bundle.proxy || "";
proxyUsage[proxy] = (proxyUsage[proxy] || 0) + 1;
});
return proxyUsage;
}
// 修正时区选择器函数
// 打开时区选择器
function openTimezoneSelector() {
if (selectedTokens.size === 0) {
showToast("请先选择Token", "info");
return;
}
// 设置当前正在编辑的token
currentEditingToken = [...selectedTokens][0]; // 查找token对象 - 格式: [index, alias, tokenData]
const tokenArr = allTokens.find((arr) => {
const [_, __, tokenData] = arr;
return tokenData.bundle.primary_token === currentEditingToken;
});
if (!tokenArr) return;
const [_, alias, tokenData] = tokenArr;
const bundle = tokenData.bundle || {};
const user = bundle.user || {};
// 使用邮箱或者token的简短显示
const email = user.email || "";
const displayName =
email || currentEditingToken.substring(0, 15) + "...";
// 显示当前选中的Token数量和账户信息
let displayText = "";
if (selectedTokens.size === 1) {
displayText = displayName;
} else {
displayText = `已选择 ${selectedTokens.size} 个令牌`;
}
document.getElementById("timezoneTokenDisplay").textContent =
displayText;
// 初始化时区列表
initializeTimezoneList();
// 设置当前选中的时区
const currentTimezone = bundle.timezone || "";
highlightSelectedTimezone(currentTimezone);
// 显示模态框
showModal("timezoneModal");
// 设置搜索框事件
document
.getElementById("timezoneSearchInput")
.addEventListener("input", searchTimezones);
document.getElementById("timezoneSearchInput").value = "";
// 关闭右键菜单
document.getElementById("contextMenu").style.display = "none";
}
// 全局时区列表数组
let timezoneList = [];
// 初始化时区列表
function initializeTimezoneList() {
try {
// 尝试使用Intl.supportedValuesOf获取时区列表现代浏览器支持
if (
typeof Intl !== "undefined" &&
typeof Intl.supportedValuesOf === "function"
) {
timezoneList = Intl.supportedValuesOf("timeZone");
} else {
// 回退到预定义的常用时区列表
timezoneList = [
"Africa/Abidjan",
"Africa/Accra",
"Africa/Addis_Ababa",
"Africa/Algiers",
"Africa/Cairo",
"Africa/Casablanca",
"Africa/Johannesburg",
"Africa/Lagos",
"Africa/Nairobi",
"America/Anchorage",
"America/Argentina/Buenos_Aires",
"America/Bogota",
"America/Chicago",
"America/Denver",
"America/Los_Angeles",
"America/Mexico_City",
"America/New_York",
"America/Phoenix",
"America/Sao_Paulo",
"America/Toronto",
"Asia/Bangkok",
"Asia/Dubai",
"Asia/Hong_Kong",
"Asia/Jakarta",
"Asia/Karachi",
"Asia/Kolkata",
"Asia/Manila",
"Asia/Seoul",
"Asia/Shanghai",
"Asia/Singapore",
"Asia/Taipei",
"Asia/Tehran",
"Asia/Tokyo",
"Australia/Melbourne",
"Australia/Perth",
"Australia/Sydney",
"Europe/Amsterdam",
"Europe/Athens",
"Europe/Berlin",
"Europe/Brussels",
"Europe/Istanbul",
"Europe/London",
"Europe/Madrid",
"Europe/Moscow",
"Europe/Paris",
"Europe/Rome",
"Europe/Stockholm",
"Pacific/Auckland",
"Pacific/Honolulu",
"UTC",
];
}
renderTimezoneList(timezoneList);
} catch (error) {
console.error("获取时区列表失败:", error);
showToast("获取时区列表失败", "error");
}
}
// 渲染时区列表
function renderTimezoneList(timezones) {
const container = document.getElementById("timezoneListContainer");
// 添加一个"未指定"选项
let html = `
<div class="timezone-item" data-timezone="" onclick="selectTimezone('')">
<span>未指定</span>
</div>
`;
// 添加其他时区选项
timezones.forEach((timezone) => {
html += `
<div class="timezone-item" data-timezone="${timezone}" onclick="selectTimezone('${timezone}')">
<span>${timezone}</span>
</div>
`;
});
container.innerHTML = html;
// 添加样式
const style = document.createElement("style");
style.textContent = `
.timezone-item {
padding: 8px 16px;
cursor: pointer;
transition: all var(--transition-fast);
border-bottom: 1px solid var(--border-color);
}
.timezone-item:last-child {
border-bottom: none;
}
.timezone-item:hover {
background: var(--primary-color-alpha);
}
.timezone-item.selected {
background: var(--primary-color);
color: white;
}
`;
document.head.appendChild(style);
}
// 搜索时区
function searchTimezones() {
const searchTerm = document
.getElementById("timezoneSearchInput")
.value.toLowerCase();
// 如果搜索词为空,显示所有时区
if (!searchTerm) {
renderTimezoneList(timezoneList);
return;
}
// 过滤匹配的时区
const filteredTimezones = timezoneList.filter((timezone) =>
timezone.toLowerCase().includes(searchTerm),
);
renderTimezoneList(filteredTimezones);
}
// 选择时区
let selectedTimezone = "";
function selectTimezone(timezone) {
selectedTimezone = timezone;
highlightSelectedTimezone(timezone);
}
// 高亮显示选中的时区
function highlightSelectedTimezone(timezone) {
// 移除所有选中状态
const items = document.querySelectorAll(".timezone-item");
items.forEach((item) => item.classList.remove("selected"));
// 添加选中状态
const selectedItem = document.querySelector(
`.timezone-item[data-timezone="${timezone}"]`,
);
if (selectedItem) {
selectedItem.classList.add("selected");
}
}
// 保存时区选择
async function saveTimezoneSelection() {
if (selectedTokens.size === 0) return;
closeModal("timezoneModal");
// 获取选中token的别名 - 格式: [index, alias, tokenData]
const aliasesToUpdate = [...selectedTokens].map((token) => {
const tokenArr = allTokens.find((arr) => {
const [_, __, tokenData] = arr;
return tokenData.bundle.primary_token === token;
});
return tokenArr ? tokenArr[1] : token; // tokenArr[1] 是 alias
});
showToast(
`正在更新${aliasesToUpdate.length}个Token的时区设置...`,
"info",
);
try {
const data = await makeAuthenticatedRequest("/tokens/timezone/set", {
body: JSON.stringify({
aliases: aliasesToUpdate,
timezone: selectedTimezone || null, // 空字符串发送null以清除时区
}),
});
if (data && data.status === "success") {
showToast(data.message || "时区设置成功", "success");
getTokenInfo(); // 刷新Token列表
} else {
showToast("时区设置失败", "error");
}
} catch (error) {
showToast("时区设置失败", "error");
console.error("设置时区出错:", error);
}
}
// 添加更新时区筛选下拉框功能
function updateTimezoneFilter() {
const timezoneFilterSelect = document.getElementById("timezoneFilter");
// 保持第一个和第二个选项(全部时区和未指定时区)
while (timezoneFilterSelect.options.length > 2) {
timezoneFilterSelect.remove(2);
}
// 获取所有使用的时区并统计数量
const timezoneUsage = {};
allTokens.forEach((tokenArr) => {
const [_, __, tokenData] = tokenArr;
const timezone = tokenData.bundle.timezone || "";
if (timezone) {
// 只统计非空时区
timezoneUsage[timezone] = (timezoneUsage[timezone] || 0) + 1;
}
});
// 按使用量排序时区
const sortedTimezones = Object.keys(timezoneUsage).sort(
(a, b) => timezoneUsage[b] - timezoneUsage[a],
);
// 添加时区选项
sortedTimezones.forEach((timezone) => {
const option = document.createElement("option");
option.value = timezone;
option.textContent = `${timezone} (${timezoneUsage[timezone]})`;
timezoneFilterSelect.appendChild(option);
});
}
// 设置Token状态
async function setTokenStatus(status) {
if (selectedTokens.size === 0) {
showToast("请先选择Token", "info");
return;
}
// 将token转换为alias - 格式: [index, alias, tokenData]
const aliasesToUpdate = [...selectedTokens].map((token) => {
const tokenArr = allTokens.find((arr) => {
const [_, __, tokenData] = arr;
return tokenData.bundle.primary_token === token;
});
return tokenArr ? tokenArr[1] : token; // tokenArr[1] 是 alias
});
showToast("正在更新Token状态...", "info");
try {
const result = await makeAuthenticatedRequest("/tokens/status/set", {
body: JSON.stringify({
aliases: aliasesToUpdate,
status: status,
}),
});
if (result && result.status === "success") {
showToast(result.message || "状态更新成功", "success");
getTokenInfo(); // 刷新Token列表
} else {
// 显示详细的错误信息
const errorMessage =
result?.error || result?.message || "状态更新失败";
showToast(errorMessage, "error");
}
} catch (error) {
// 显示详细的错误信息
const errorMessage =
error.response?.data?.message ||
error.response?.data?.error ||
error.message;
showToast(`状态更新失败: ${errorMessage}`, "error");
}
// 关闭上下文菜单
document.getElementById("contextMenu").style.display = "none";
// 更新状态子菜单
updateStatusSubmenu();
}
// 更新状态子菜单
function updateStatusSubmenu() {
const submenu = document.getElementById("statusSubmenu");
if (!submenu) return;
// 获取选中的token对象 - 格式: [index, alias, tokenData]
const selectedTokenArrs = allTokens.filter((tokenArr) => {
const [_, __, tokenData] = tokenArr;
return selectedTokens.has(tokenData.bundle.primary_token);
});
const selectionSize = selectedTokenArrs.length;
// 计算选中项的状态统计
const selectedStatusStats = {
enabled: 0,
disabled: 0,
};
selectedTokenArrs.forEach((tokenArr) => {
const [_, __, tokenData] = tokenArr;
// 将 undefined 或 'enabled' 状态视为 'enabled'
const status =
tokenData.status === "disabled" ? "disabled" : "enabled";
selectedStatusStats[status]++; // 增加对应状态的计数
});
// 更新 "启用" 选项
const enabledItem = submenu.querySelector(
'.context-menu-item[onclick="setTokenStatus(\\"enabled\\")"]',
);
if (enabledItem) {
const enabledCountSpan = enabledItem.querySelector(".status-count");
if (enabledCountSpan) {
// 显示选中项中启用的数量
enabledCountSpan.textContent = selectedStatusStats["enabled"];
}
// 检查是否所有选中的 Token 都处于启用状态
const isEnabledActive =
selectionSize > 0 &&
selectedStatusStats["enabled"] === selectionSize;
enabledItem.classList.toggle("active", isEnabledActive); // 设置选中标记
}
// 更新 "禁用" 选项
const disabledItem = submenu.querySelector(
'.context-menu-item[onclick="setTokenStatus(\\"disabled\\")"]',
);
if (disabledItem) {
const disabledCountSpan = disabledItem.querySelector(".status-count");
if (disabledCountSpan) {
// 显示选中项中禁用的数量
disabledCountSpan.textContent = selectedStatusStats["disabled"];
}
// 检查是否所有选中的 Token 都处于禁用状态
const isDisabledActive =
selectionSize > 0 &&
selectedStatusStats["disabled"] === selectionSize;
disabledItem.classList.toggle("active", isDisabledActive); // 设置选中标记
}
// 添加 has-submenu 类以便显示箭头
const statusMenuEl = document.querySelector(".status-menu");
if (statusMenuEl) {
statusMenuEl.classList.add("has-submenu");
}
}
// 重命名Token
function renameToken() {
if (selectedTokens.size === 0) {
showToast("请先选择Token", "info");
return;
}
if (selectedTokens.size > 1) {
showToast("一次只能重命名一个Token", "info");
return;
}
const token = [...selectedTokens][0];
startRenaming(token);
// 关闭右键菜单
document.getElementById("contextMenu").style.display = "none";
}
// 开始重命名
function startRenaming(token) {
const row = document.querySelector(`tr[data-token="${token}"]`);
if (!row) return;
const tokenCell = row.querySelector("td:first-child");
if (!tokenCell) return;
// 获取当前别名 - 格式: [index, alias, tokenData]
const tokenArr = allTokens.find((arr) => {
const [_, __, tokenData] = arr;
return tokenData.bundle.primary_token === token;
});
if (!tokenArr) return;
const [index, currentAlias, tokenData] = tokenArr;
const bundle = tokenData.bundle || {};
const user = bundle.user || {};
const displayName =
currentAlias || user.email || token.substring(0, 15) + "...";
// 保存原始内容
const originalContent = tokenCell.innerHTML;
// 创建输入框
const input = document.createElement("input");
input.type = "text";
input.className = "inline-edit";
input.value = currentAlias || "";
input.placeholder = user.email || "输入别名";
// 清空单元格并添加输入框
tokenCell.innerHTML = "";
tokenCell.appendChild(input);
// 聚焦并选中文本
input.focus();
input.select();
// 保存编辑
const saveEdit = async () => {
const newAlias = input.value.trim();
// 恢复原始内容
tokenCell.innerHTML = originalContent;
// 如果别名没有变化,直接返回
if (newAlias === currentAlias) {
return;
}
// 调用API更新别名
try {
showToast("正在更新别名...", "info");
const requestBody = {};
requestBody[currentAlias || token] = newAlias;
const data = await makeAuthenticatedRequest("/tokens/alias/set", {
body: JSON.stringify(requestBody),
});
if (data && data.status === "success") {
showToast("别名更新成功", "success");
getTokenInfo(); // 刷新Token列表
} else {
showToast("别名更新失败", "error");
}
} catch (error) {
showToast("别名更新失败: " + error.message, "error");
}
};
// 取消编辑
const cancelEdit = () => {
tokenCell.innerHTML = originalContent;
};
// 监听键盘事件
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
saveEdit();
} else if (e.key === "Escape") {
e.preventDefault();
cancelEdit();
}
});
// 失去焦点时保存
input.addEventListener("blur", saveEdit);
}
// 刷新选中的Token
async function upgradeSelectedTokens() {
if (selectedTokens.size === 0) {
showToast("请先选择要刷新的Token", "info");
return;
}
// 将token转换为alias - 格式: [index, alias, tokenData]
const aliasesToUpgrade = [...selectedTokens].map((token) => {
const tokenArr = allTokens.find((arr) => {
const [_, __, tokenData] = arr;
return tokenData.bundle.primary_token === token;
});
return tokenArr ? tokenArr[1] : token; // tokenArr[1] 是 alias
});
showToast(`正在刷新 ${aliasesToUpgrade.length} 个Token...`, "info");
// 关闭右键菜单
document.getElementById("contextMenu").style.display = "none";
try {
const data = await makeAuthenticatedRequest("/tokens/refresh", {
body: JSON.stringify(aliasesToUpgrade),
});
if (data && data.status === "success") {
showToast(data.message || "刷新成功", "success");
getTokenInfo();
} else {
showToast(
"刷新失败: " + (data.message || data.error || "未知错误"),
"error",
);
}
} catch (error) {
const errorMessage =
error.response?.data?.message ||
error.response?.data?.error ||
error.message;
showToast(`刷新失败: ${errorMessage}`, "error");
}
}
</script>
</body>
</html>