修复了一些问题

This commit is contained in:
wisdgod
2025-01-23 12:34:56 +08:00
parent 3e304f53d4
commit 36b42e27de
73 changed files with 4918 additions and 2350 deletions

View File

@@ -140,7 +140,7 @@
<div id="usageProgressContainer" class="progress-container"></div>
</div>
<div id="message" class="message"></div>
<div id="message"></div>
<footer class="footer">
<div id="version"></div>
@@ -198,11 +198,16 @@
// 获取模型列表
async function getModels() {
const modelList = document.getElementById('modelList');
const suffix = document.getElementById('customSuffix').checked ?
document.getElementById('suffixInput').value : '';
try {
const modelList = document.getElementById('modelList');
const suffix = document.getElementById('customSuffix').checked ?
document.getElementById('suffixInput').value : '';
modelList.value = globalModels.map(model => model + suffix).join(',');
modelList.value = globalModels.map(model => model + suffix).join(',');
showGlobalMessage('模型列表已更新');
} catch (error) {
showGlobalMessage('获取模型列表失败', true);
}
}
// 复制模型列表
@@ -234,7 +239,7 @@
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
throw new Error(`请求失败 (${response.status})`);
}
return await response.json();
@@ -261,9 +266,13 @@
calibrationCache.set(token, {
user_id: result.user_id,
create_at: result.create_at,
checksum_time: calibResult.checksum_time
checksum_time: result.checksum_time
});
updateUsageDisplay(null, calibrationCache.get(token));
// 显示基本校准信息
const container = document.getElementById('userInfoContainer');
container.style.display = 'block';
updateUsageDisplay({ user: null }, calibrationCache.get(token));
}
}
}
@@ -275,15 +284,13 @@
showGlobalMessage('请输入 Token', true);
return;
}
// 如果没有校准缓存,先进行校准
if (!calibrationCache.has(token)) {
const calibResult = await makeTokenRequest('/basic-calibration', token);
if (calibResult && calibResult.status !== 'error') {
calibrationCache.set(token, {
user_id: calibResult.user_id,
create_at: calibResult.create_at,
checksum_time: calibResult.checksum_time
});
showGlobalMessage('正在进行 Token 校准...');
await calibrateToken();
if (!calibrationCache.has(token)) {
return; // 如果校准失败,直接返回
}
}
@@ -292,6 +299,7 @@
const container = document.getElementById('userInfoContainer');
container.style.display = 'block';
updateUsageDisplay(result, calibrationCache.get(token));
showGlobalMessage('用户信息获取成功');
}
}
@@ -350,11 +358,16 @@
});
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', () => {
startStatusCheck();
document.addEventListener('DOMContentLoaded', async () => {
try {
await startStatusCheck();
showGlobalMessage('系统初始化完成');
// 监听后缀输入变化
document.getElementById('suffixInput').addEventListener('input', getModels);
// 监听后缀输入变化
document.getElementById('suffixInput').addEventListener('input', getModels);
} catch (error) {
showGlobalMessage('系统初始化失败', true);
}
});
</script>
</body>

253
static/build_key.html Normal file
View File

@@ -0,0 +1,253 @@
<!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>Key 构建</title>
<!-- 引入共享样式 -->
<link rel="stylesheet" href="/static/shared-styles.css">
<script src="/static/shared.js"></script>
<style>
.key-result {
word-break: break-all;
background: var(--card-background);
padding: var(--spacing);
border-radius: var(--border-radius);
border: 1px solid var(--border-color);
margin-top: var(--spacing);
position: relative;
}
.copy-button {
position: absolute;
right: 10px;
top: 10px;
padding: 4px 8px;
font-size: 14px;
background: var(--primary-color-alpha);
color: var(--primary-color);
border: 1px solid var(--primary-color);
border-radius: 4px;
cursor: pointer;
transition: all var(--transition-fast);
}
.copy-button:hover {
background: var(--primary-color);
color: white;
}
.model-list {
max-height: 150px;
overflow-y: auto;
padding: 8px;
border: 1px solid var(--border-color);
border-radius: 4px;
margin-top: 8px;
background: var(--card-background);
}
.model-item {
display: flex;
align-items: center;
margin-bottom: 4px;
}
.model-item input[type="checkbox"] {
margin-right: 8px;
}
</style>
</head>
<body>
<h1>Key 构建</h1>
<div class="container">
<div class="form-group">
<label>服务认证令牌:</label>
<input type="password" id="authToken" placeholder="输入服务认证令牌">
</div>
<div class="form-group">
<label>数据认证令牌:</label>
<input type="password" id="dataToken" placeholder="输入数据认证令牌">
</div>
<div class="form-group">
<label>流第一个块检查:</label>
<select id="enableStreamCheck">
<option value="">跟随全局</option>
<option value="true">启用</option>
<option value="false">禁用</option>
</select>
</div>
<div class="form-group">
<label>包含停止流:</label>
<select id="includeStopStream">
<option value="">跟随全局</option>
<option value="true">启用</option>
<option value="false">禁用</option>
</select>
</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;">
<!-- 模型列表将通过 JavaScript 动态填充 -->
</div>
</div>
<div class="button-group">
<button onclick="buildKey()">构建 Key</button>
<button onclick="clearForm()" class="secondary">清空表单</button>
</div>
</div>
<div id="keyResult" class="key-result" style="display: none;">
<button class="copy-button" onclick="copyKey()">复制</button>
<div id="keyContent"></div>
</div>
<div id="message"></div>
<script>
let availableModels = [];
async function getModels() {
try {
const response = await fetch('/v1/models');
const data = await response.json();
availableModels = data.data.map(model => model.id);
updateModelList();
} catch (error) {
showGlobalMessage('获取模型列表失败', true);
}
}
function updateModelList() {
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 type = document.getElementById('usageCheckType').value;
const container = document.getElementById('modelListContainer');
container.style.display = type === 'custom' ? 'block' : 'none';
}
async function buildKey() {
const authToken = document.getElementById('authToken').value;
const dataToken = document.getElementById('dataToken').value;
if (!authToken) {
showGlobalMessage('请输入服务认证令牌', true);
return;
}
if (!dataToken) {
showGlobalMessage('请输入数据认证令牌', true);
return;
}
const type = document.getElementById('usageCheckType').value;
let modelIds = '';
if (type === 'custom') {
modelIds = Array.from(document.querySelectorAll('#modelListContainer input:checked'))
.map(input => input.value)
.join(',');
}
const data = {
auth_token: dataToken,
enable_stream_check: parseBooleanFromString(document.getElementById('enableStreamCheck').value, undefined),
include_stop_stream: parseBooleanFromString(document.getElementById('includeStopStream').value, undefined),
disable_vision: parseBooleanFromString(document.getElementById('disableVision').value, undefined),
enable_slow_pool: parseBooleanFromString(document.getElementById('enableSlowPool').value, undefined),
usage_check_models: type ? {
type: type,
model_ids: type === 'custom' ? modelIds : undefined
} : undefined
};
try {
const response = await makeAuthenticatedRequest('/build-key', {
method: 'POST',
body: JSON.stringify(data)
});
if (response) {
const keyResult = document.getElementById('keyResult');
const keyContent = document.getElementById('keyContent');
keyContent.textContent = response.key || response.error;
keyResult.style.display = 'block';
showGlobalMessage(response.key ? 'Key 构建成功' : '构建失败: ' + response.error, !response.key);
}
} catch (error) {
showGlobalMessage('请求失败: ' + error.message, true);
}
}
function copyKey() {
const keyContent = document.getElementById('keyContent').textContent;
navigator.clipboard.writeText(keyContent).then(() => {
showGlobalMessage('Key 已复制到剪贴板');
}).catch(() => {
showGlobalMessage('复制失败', true);
});
}
function clearForm() {
document.getElementById('authToken').value = '';
document.getElementById('dataToken').value = '';
document.getElementById('enableStreamCheck').value = '';
document.getElementById('includeStopStream').value = '';
document.getElementById('disableVision').value = '';
document.getElementById('enableSlowPool').value = '';
document.getElementById('usageCheckType').value = 'default';
document.getElementById('modelListContainer').style.display = 'none';
document.getElementById('keyResult').style.display = 'none';
showGlobalMessage('表单已清空');
}
// 初始化
document.addEventListener('DOMContentLoaded', () => {
getModels();
const authToken = getAuthToken();
if (authToken) {
document.getElementById('authToken').value = authToken;
fetchLogs();
}
});
initializeTokenHandling('authToken');
</script>
</body>
</html>

View File

@@ -21,12 +21,13 @@
<option value="/">根路径 (/)</option>
<option value="/logs">日志页面 (/logs)</option>
<option value="/config">配置页面 (/config)</option>
<option value="/tokeninfo">Token 信息页面 (/tokeninfo)</option>
<option value="/tokens">Token 管理页面 (/tokens)</option>
<option value="/static/shared-styles.css">共享样式 (/static/shared-styles.css)</option>
<option value="/static/shared.js">共享脚本 (/static/shared.js)</option>
<option value="/about">关于页面 (/about)</option>
<option value="/readme">ReadMe文档 (/readme)</option>
<option value="/api">api调用 (/api)</option>
<option value="/build-key">构建动态 Key (/build-key)</option>
</select>
</div>
@@ -92,14 +93,40 @@
<div class="form-group">
<label>使用量检查模型规则:</label>
<select id="check_usage_models_type">
<select id="usage_check_models_type">
<option value="">保持不变</option>
<option value="none">禁用</option>
<option value="default">默认</option>
<option value="all">所有</option>
<option value="list">自定义列表</option>
</select>
<input type="text" id="check_usage_models_list" placeholder="模型列表,以逗号分隔" style="display: none;">
<input type="text" id="usage_check_models_list" placeholder="模型列表,以逗号分隔" style="display: none;">
</div>
<div class="form-group">
<label>是否允许动态配置Key:</label>
<select id="enable_dynamic_key">
<option value="">保持不变</option>
<option value="true">启用</option>
<option value="false">禁用</option>
</select>
</div>
<div class="form-group">
<label>代理设置:</label>
<select id="proxies_type" onchange="handleProxiesTypeChange()">
<option value="">保持不变</option>
<option value="no">不使用代理</option>
<option value="system">使用系统代理</option>
<option value="list">自定义代理列表</option>
</select>
<input type="text" id="proxies_list" placeholder="代理地址列表,以逗号分隔 (例如: http://127.0.0.1:7890)"
style="display: none;">
</div>
<div class="form-group">
<label>共享令牌(空表示禁用):</label>
<input type="text" id="shareToken">
</div>
<div class="form-group">
@@ -114,127 +141,175 @@
</div>
</div>
<div id="result" class="message"></div>
<div id="message"></div>
<script>
async function fetchConfig() {
const path = document.getElementById('path').value;
const data = await makeAuthenticatedRequest('/config', {
body: JSON.stringify({ action: 'get', path })
});
try {
const path = document.getElementById('path').value;
const data = await makeAuthenticatedRequest('/config', {
body: JSON.stringify({ action: 'get', path })
});
if (data) {
let content = '';
if (data) {
let content = '';
// 获取当前路径的页面内容
const pageContent = data.data.page_content;
// 获取当前路径的页面内容
const pageContent = data.data.page_content;
// 如果是 default 类型,需要从路径获取内容
if (pageContent?.type === 'default') {
// 直接从路径获取内容
const response = await fetch(path);
content = await response.text();
} else if (pageContent?.type === 'text' || pageContent?.type === 'html') {
content = pageContent.content;
// 如果是 default 类型,需要从路径获取内容
if (pageContent?.type === 'default') {
// 直接从路径获取内容
const response = await fetch(path);
content = await response.text();
} else if (pageContent?.type === 'text' || pageContent?.type === 'html') {
content = pageContent.content;
}
// 更新表单
document.getElementById('content').value = content || '';
document.getElementById('content_type').value = pageContent?.type || 'default';
let visionValue = data.data.vision_ability || '';
// 标准化 vision_ability 的值
switch (visionValue) {
case 'none':
visionValue = 'disabled';
break;
case 'base64':
visionValue = 'base64-only';
break;
case 'all':
visionValue = 'base64-http';
break;
}
document.getElementById('enable_stream_check').value =
parseStringFromBoolean(data.data.enable_stream_check, '');
document.getElementById('include_stop_stream').value =
parseStringFromBoolean(data.data.include_stop_stream, '');
document.getElementById('vision_ability').value = visionValue;
document.getElementById('enable_slow_pool').value =
parseStringFromBoolean(data.data.enable_slow_pool, '');
document.getElementById('enable_all_claude').value =
parseStringFromBoolean(data.data.enable_all_claude, '');
document.getElementById('usage_check_models_type').value = data.data.usage_check_models?.type || '';
document.getElementById('usage_check_models_list').value = data.data.usage_check_models?.type === 'list' ? data.data.usage_check_models?.content || '' : document.getElementById('usage_check_models_list').value;
document.getElementById('enable_dynamic_key').value =
parseStringFromBoolean(data.data.enable_dynamic_key, '');
// 处理代理设置
const proxies = data.data.proxies || '';
let proxiesType = '';
let proxiesList = '';
if (proxies === '') {
proxiesType = 'no';
} else if (proxies === 'system') {
proxiesType = 'system';
} else {
proxiesType = 'list';
proxiesList = proxies;
}
document.getElementById('proxies_type').value = proxiesType;
document.getElementById('proxies_list').value = proxiesList;
handleProxiesTypeChange();
document.getElementById('shareToken').value = data.data.share_token || '';
// 添加获取配置成功提示
showGlobalMessage(`成功获取 ${path} 的配置`, false);
}
// 更新表单
document.getElementById('content').value = content || '';
document.getElementById('content_type').value = pageContent?.type || 'default';
let visionValue = data.data.vision_ability || '';
// 标准化 vision_ability 的值
switch (visionValue) {
case 'none':
visionValue = 'disabled';
break;
case 'base64':
visionValue = 'base64-only';
break;
case 'all':
visionValue = 'base64-http';
break;
}
document.getElementById('enable_stream_check').value =
parseStringFromBoolean(data.data.enable_stream_check, '');
document.getElementById('include_stop_stream').value =
parseStringFromBoolean(data.data.include_stop_stream, '');
document.getElementById('vision_ability').value = visionValue;
document.getElementById('enable_slow_pool').value =
parseStringFromBoolean(data.data.enable_slow_pool, '');
document.getElementById('enable_all_claude').value =
parseStringFromBoolean(data.data.enable_all_claude, '');
document.getElementById('check_usage_models_type').value = data.data.check_usage_models?.type || '';
document.getElementById('check_usage_models_list').value = data.data.check_usage_models?.type === 'list' ? data.data.check_usage_models?.content || '' : document.getElementById('check_usage_models_list').value;
} catch (error) {
showGlobalMessage(error.message || '获取配置失败', true);
}
}
async function updateConfig(action) {
if (action === 'get') {
await fetchConfig();
return;
}
const contentType = document.getElementById('content_type').value;
const content = document.getElementById('content').value;
// 根据内容类型构造 content 对象
let contentObj = { type: 'default' };
if (action === 'update' && contentType !== 'default') {
contentObj = {
type: contentType,
content: content
};
}
const data = {
action,
path: document.getElementById('path').value,
...(contentObj && { content: contentObj }),
...(document.getElementById('enable_stream_check').value && {
enable_stream_check: parseBooleanFromString(document.getElementById('enable_stream_check').value)
}),
...(document.getElementById('include_stop_stream').value && {
include_stop_stream: parseBooleanFromString(document.getElementById('include_stop_stream').value)
}),
...(document.getElementById('vision_ability').value && {
vision_ability: document.getElementById('vision_ability').value
}),
...(document.getElementById('enable_slow_pool').value && {
enable_slow_pool: parseBooleanFromString(document.getElementById('enable_slow_pool').value)
}),
...(document.getElementById('enable_all_claude').value && {
enable_all_claude: parseBooleanFromString(document.getElementById('enable_all_claude').value)
}),
...(document.getElementById('check_usage_models_type').value && {
check_usage_models: {
type: document.getElementById('check_usage_models_type').value,
...(document.getElementById('check_usage_models_type').value === 'list' && {
content: document.getElementById('check_usage_models_list').value
})
}
})
};
const result = await makeAuthenticatedRequest('/config', {
body: JSON.stringify(data)
});
if (result) {
showMessage('result', result.message, false);
if (action === 'update' || action === 'reset') {
try {
if (action === 'get') {
await fetchConfig();
return;
}
const contentType = document.getElementById('content_type').value;
const content = document.getElementById('content').value;
// 根据内容类型构造 content 对象
let contentObj = { type: 'default' };
if (action === 'update' && contentType !== 'default') {
contentObj = {
type: contentType,
content: content
};
}
const shareToken = document.getElementById('shareToken').value.trim();
const data = {
action,
path: document.getElementById('path').value,
...(contentObj && { content: contentObj }),
...(document.getElementById('enable_stream_check').value && {
enable_stream_check: parseBooleanFromString(document.getElementById('enable_stream_check').value)
}),
...(document.getElementById('include_stop_stream').value && {
include_stop_stream: parseBooleanFromString(document.getElementById('include_stop_stream').value)
}),
...(document.getElementById('vision_ability').value && {
vision_ability: document.getElementById('vision_ability').value
}),
...(document.getElementById('enable_slow_pool').value && {
enable_slow_pool: parseBooleanFromString(document.getElementById('enable_slow_pool').value)
}),
...(document.getElementById('enable_all_claude').value && {
enable_all_claude: parseBooleanFromString(document.getElementById('enable_all_claude').value)
}),
...(document.getElementById('usage_check_models_type').value && {
usage_check_models: {
type: document.getElementById('usage_check_models_type').value,
...(document.getElementById('usage_check_models_type').value === 'list' && {
content: document.getElementById('usage_check_models_list').value
})
}
}),
...(document.getElementById('enable_dynamic_key').value && {
enable_dynamic_key: parseBooleanFromString(document.getElementById('enable_dynamic_key').value)
}),
...(document.getElementById('proxies_type').value && {
proxies: (() => {
const type = document.getElementById('proxies_type').value;
switch (type) {
case 'no':
return '';
case 'system':
return 'system';
case 'list':
return document.getElementById('proxies_list').value;
default:
return undefined;
}
})()
}),
...(shareToken && {
share_token: shareToken
}),
};
const result = await makeAuthenticatedRequest('/config', {
body: JSON.stringify(data)
});
if (result) {
showGlobalMessage(result.message, false);
if (action === 'update' || action === 'reset') {
await fetchConfig();
}
}
} catch (error) {
showGlobalMessage(error.message || '操作失败', true);
}
}
function showSuccess(message) {
showMessage('result', message, false);
}
function showError(message) {
showMessage('result', message, true);
}
// 添加按钮事件监听
document.getElementById('path').addEventListener('change', fetchConfig);
@@ -248,10 +323,27 @@
initializeTokenHandling('authToken');
// 添加使用量检查模型类型变更处理
document.getElementById('check_usage_models_type').addEventListener('change', function() {
const input = document.getElementById('check_usage_models_list');
document.getElementById('usage_check_models_type').addEventListener('change', function () {
const input = document.getElementById('usage_check_models_list');
input.style.display = this.value === 'list' ? 'inline-block' : 'none';
});
// 添加代理类型变更处理函数
function handleProxiesTypeChange() {
const type = document.getElementById('proxies_type').value;
const list = document.getElementById('proxies_list');
list.style.display = type === 'list' ? 'inline-block' : 'none';
}
// 页面加载完成后自动获取配置
document.addEventListener('DOMContentLoaded', async () => {
try {
await fetchConfig();
showGlobalMessage('页面加载完成', false);
} catch (error) {
showGlobalMessage('初始化配置加载失败', true);
}
});
</script>
</body>

View File

@@ -354,6 +354,28 @@
#logsTable tr:hover td {
background-color: var(--hover-color, rgba(0, 0, 0, 0.02));
}
.modal-actions {
display: flex;
align-items: center;
gap: 12px;
}
.danger-button {
padding: 6px 12px;
font-size: 14px;
border-radius: var(--border-radius);
background: var(--error-color-alpha);
color: var(--error-color);
border: 1px solid var(--error-color);
cursor: pointer;
transition: all var(--transition-fast);
}
.danger-button:hover {
background: var(--error-color);
color: white;
}
</style>
</head>
@@ -423,7 +445,10 @@
<div class="modal-content">
<div class="modal-header">
<h3>Token 详细信息</h3>
<span class="close">&times;</span>
<div class="modal-actions">
<button class="danger-button" id="deleteTokenBtn">删除此Token</button>
<span class="close">&times;</span>
</div>
</div>
<table class="message-table">
<tr>
@@ -543,6 +568,42 @@
function showTokenModal(tokenInfo) {
const modal = document.getElementById('tokenModal');
const deleteBtn = document.getElementById('deleteTokenBtn');
// 存储当前token用于删除操作
const currentToken = tokenInfo.token;
// 更新删除按钮点击事件
deleteBtn.onclick = async () => {
if (!currentToken) {
showGlobalMessage('无效的Token', true);
return;
}
if (!confirm('确定要删除此Token吗此操作不可撤销。')) {
return;
}
const data = await makeAuthenticatedRequest('/tokens/delete', {
method: 'POST',
body: JSON.stringify({
tokens: [currentToken],
expectation: 'failed_tokens'
})
});
if (data) {
modal.style.display = 'none';
let message = 'Token删除成功';
if (data.failed_tokens?.length) {
message = 'Token删除失败未找到该Token';
}
showGlobalMessage(message);
// 刷新日志列表
fetchLogs();
}
};
document.getElementById('modalToken').textContent = tokenInfo.token || '-';
document.getElementById('modalChecksum').textContent = tokenInfo.checksum || '-';
@@ -634,7 +695,7 @@
const tbody = document.getElementById('logsBody');
updateStats(data);
tbody.innerHTML = data.logs.map(log => `<tr><td>${log.id}</td><td>${new Date(log.timestamp).toLocaleString()}</td><td>${log.model}</td><td><div class="token-info-tooltip"><button class="info-button" onclick='showTokenModal(${JSON.stringify(log.token_info)})'>查看详情<div class="tooltip-content">${formatSimpleTokenInfo(log.token_info)}</div></button></div></td><td>${log.prompt ?`<div class="token-info-tooltip prompt-preview"><button class="info-button" onclick="showPromptModal(decodeURIComponent('${encodeURIComponent(log.prompt).replace(/'/g, "\\'")}'))">查看对话<div class="tooltip-content">${formatPromptPreview(log.prompt)}</div></button></div>` :'-'}</td><td>${formatTiming(log.timing.total, log.timing.first)}</td><td>${log.stream ? '是' : '否'}</td><td>${log.status}</td><td>${log.error || '-'}</td></tr>`).join('');
tbody.innerHTML = data.logs.map(log => `<tr><td>${log.id}</td><td>${new Date(log.timestamp).toLocaleString()}</td><td>${log.model}</td><td><div class="token-info-tooltip"><button class="info-button" onclick='showTokenModal(${JSON.stringify(log.token_info)})'>查看详情<div class="tooltip-content">${formatSimpleTokenInfo(log.token_info)}</div></button></div></td><td>${log.prompt ? `<div class="token-info-tooltip prompt-preview"><button class="info-button" onclick="showPromptModal(decodeURIComponent('${encodeURIComponent(log.prompt).replace(/'/g, "\\'")}'))">查看对话<div class="tooltip-content">${formatPromptPreview(log.prompt)}</div></button></div>` : '-'}</td><td>${formatTiming(log.timing.total, log.timing.first)}</td><td>${log.stream ? '是' : '否'}</td><td>${log.status}</td><td>${log.error || '-'}</td></tr>`).join('');
}
function formatTiming(total, first) {

View File

@@ -1,651 +0,0 @@
<h1>cursor-api</h1>
<h2>说明</h2>
<ul>
<li>当前版本已稳定,若发现响应出现缺字漏字,与本程序无关。</li>
<li>若发现首字慢,与本程序无关。</li>
<li>若发现响应出现乱码,也与本程序无关。</li>
<li>属于官方的问题,请不要像作者反馈。</li>
<li>本程序拥有堪比客户端原本的速度,甚至可能更快。</li>
<li>本程序的性能是非常厉害的。</li>
<li>根据本项目开源协议Fork的项目不能以作者的名义进行任何形式的宣传、推广或声明。</li>
</ul>
<h2>获取key</h2>
<ol>
<li>访问 <a href="https://www.cursor.com">www.cursor.com</a> 并完成注册登录</li>
<li>在浏览器中打开开发者工具F12</li>
<li>在 Application-Cookies 中查找名为 <code>WorkosCursorSessionToken</code> 的条目,并复制其第三个字段。请注意,%3A%3A 是 :: 的 URL 编码形式cookie
的值使用冒号 (:) 进行分隔。</li>
</ol>
<h2>配置说明</h2>
<h3>环境变量</h3>
<ul>
<li><code>PORT</code>: 服务器端口号默认3000</li>
<li><code>AUTH_TOKEN</code>: 认证令牌必须用于API认证</li>
<li><code>ROUTE_PREFIX</code>: 路由前缀(可选)</li>
<li><code>TOKEN_FILE</code>: token文件路径默认.token</li>
<li><code>TOKEN_LIST_FILE</code>: token列表文件路径默认.token-list</li>
</ul>
<p>更多请查看 <code>/env-example</code></p>
<h3>Token文件格式</h3>
<ol>
<li>
<p><code>.token</code> 文件每行一个token支持以下格式</p>
<pre><code># 这是注释
token1
# alias与标签的作用差不多
alias::token2
</code></pre>
<p>alias 可以是任意值,用于区分不同的 token更方便管理WorkosCursorSessionToken 是相同格式<br>
该文件将自动向.token-list文件中追加token同时自动生成checksum</p>
</li>
<li>
<p><code>.token-list</code> 文件每行为token和checksum的对应关系</p>
<pre><code># 这里的#表示这行在下次读取要删除
token1,checksum1
# alias被舍弃会自动删除最后一个:或%3A的后一位前的所有内容
token2,checksum2
</code></pre>
<p>该文件可以被自动管理,但用户仅可在确认自己拥有修改能力时修改,一般仅有以下情况需要手动修改:</p>
<ul>
<li>需要删除某个 token</li>
<li>需要使用已有 checksum 来对应某一个 token</li>
</ul>
</li>
</ol>
<h3>模型列表</h3>
<p>写死了,后续也不会会支持自定义模型列表</p>
<pre><code>claude-3.5-sonnet
gpt-4
gpt-4o
claude-3-opus
cursor-fast
cursor-small
gpt-3.5-turbo
gpt-4-turbo-2024-04-09
gpt-4o-128k
gemini-1.5-flash-500k
claude-3-haiku-200k
claude-3-5-sonnet-200k
claude-3-5-sonnet-20241022
gpt-4o-mini
o1-mini
o1-preview
o1
claude-3.5-haiku
gemini-exp-1206
gemini-2.0-flash-thinking-exp
gemini-2.0-flash-exp
</code></pre>
<h1>接口说明</h1>
<h2>基础对话</h2>
<ul>
<li>接口地址: <code>/v1/chat/completions</code></li>
<li>请求方法: POST</li>
<li>认证方式: Bearer Token
<ol>
<li>使用环境变量 <code>AUTH_TOKEN</code> 进行认证</li>
<li>使用 <code>.token</code> 文件中的令牌列表进行轮询认证</li>
<li>在v0.1.3-rc.3支持直接使用 token,checksum 进行认证,但未提供配置关闭</li>
</ol>
</li>
</ul>
<h3>请求格式</h3>
<pre><code class="language-json">{
"model": "string",
"messages": [
{
"role": "system" | "user" | "assistant", // 也可以是 "developer" | "human" | "ai"
"content": "string" | [
{
"type": "text" | "image_url",
"text": "string",
"image_url": {
"url": "string"
}
}
]
}
],
"stream": boolean
}
</code></pre>
<h3>响应格式</h3>
<p>如果 <code>stream</code><code>false</code>:</p>
<pre><code class="language-json">{
"id": "string",
"object": "chat.completion",
"created": number,
"model": "string",
"choices": [
{
"index": number,
"message": {
"role": "assistant",
"content": "string"
},
"finish_reason": "stop" | "length"
}
],
"usage": {
"prompt_tokens": 0,
"completion_tokens": 0,
"total_tokens": 0
}
}
</code></pre>
<p>不进行 tokens 计算主要是担心性能问题。</p>
<p>如果 <code>stream</code><code>true</code>:</p>
<pre><code>data: {"id":"string","object":"chat.completion.chunk","created":number,"model":"string","choices":[{"index":number,"delta":{"role":"assistant","content":"string"},"finish_reason":null}]}
data: {"id":"string","object":"chat.completion.chunk","created":number,"model":"string","choices":[{"index":number,"delta":{"content":"string"},"finish_reason":null}]}
data: {"id":"string","object":"chat.completion.chunk","created":number,"model":"string","choices":[{"index":number,"delta":{},"finish_reason":"stop"}]}
data: [DONE]
</code></pre>
<h2>Token管理接口</h2>
<h3>简易Token信息管理页面</h3>
<ul>
<li>接口地址: <code>/tokeninfo</code></li>
<li>请求方法: GET</li>
<li>响应格式: HTML页面</li>
<li>功能: 获取 .token 和 .token-list 文件内容,并允许用户方便地使用 API 修改文件内容</li>
</ul>
<h3>更新Token信息 (GET)</h3>
<ul>
<li>接口地址: <code>/update-tokeninfo</code></li>
<li>请求方法: GET</li>
<li>认证方式: 不需要</li>
<li>功能: 重新加载tokens并更新应用状态</li>
<li>响应格式:</li>
</ul>
<pre><code class="language-json">{
"status": "success",
"message": "Token list has been reloaded"
}
</code></pre>
<h3>更新Token信息 (POST)</h3>
<ul>
<li>接口地址: <code>/update-tokeninfo</code></li>
<li>请求方法: POST</li>
<li>认证方式: Bearer Token</li>
<li>请求格式:</li>
</ul>
<pre><code class="language-json">{
"tokens": "string",
"token_list": "string"
}
</code></pre>
<ul>
<li>响应格式:</li>
</ul>
<pre><code class="language-json">{
"status": "success",
"token_file": "string",
"token_list_file": "string",
"tokens_count": number,
"message": "Token files have been updated and reloaded"
}
</code></pre>
<h3>获取Token信息</h3>
<ul>
<li>接口地址: <code>/get-tokeninfo</code></li>
<li>请求方法: POST</li>
<li>认证方式: Bearer Token</li>
<li>响应格式:</li>
</ul>
<pre><code class="language-json">{
"status": "success",
"token_file": "string",
"token_list_file": "string",
"tokens": "string",
"tokens_count": number,
"token_list": "string"
}
</code></pre>
<h2>配置管理接口</h2>
<h3>配置页面</h3>
<ul>
<li>接口地址: <code>/config</code></li>
<li>请求方法: GET</li>
<li>响应格式: HTML页面</li>
<li>功能: 提供配置管理界面,可以修改页面内容和系统配置</li>
</ul>
<h3>更新配置</h3>
<ul>
<li>接口地址: <code>/config</code></li>
<li>请求方法: POST</li>
<li>认证方式: Bearer Token</li>
<li>请求格式:</li>
</ul>
<pre><code class="language-json">{
"action": "get" | "update" | "reset",
"path": "string",
"content": {
"type": "default" | "text" | "html",
"content": "string"
},
"enable_stream_check": boolean,
"include_stop_stream": boolean,
"vision_ability": "none" | "base64" | "all", // "disabled" | "base64-only" | "base64-http"
"enable_slow_pool": boolean,
"enable_all_claude": boolean,
"check_usage_models": {
"type": "none" | "default" | "all" | "list",
"content": "string"
}
}
</code></pre>
<ul>
<li>响应格式:</li>
</ul>
<pre><code class="language-json">{
"status": "success",
"message": "string",
"data": {
"page_content": {
"type": "default" | "text" | "html", // 对于js和css后两者是一样的
"content": "string"
},
"enable_stream_check": boolean,
"include_stop_stream": boolean,
"vision_ability": "none" | "base64" | "all",
"enable_slow_pool": boolean,
"enable_all_claude": boolean,
"check_usage_models": {
"type": "none" | "default" | "all" | "list",
"content": "string"
}
}
}
</code></pre>
<p>注意:<code>check_usage_models</code> 字段的默认值为:</p>
<pre><code class="language-json">{
"type": "default",
"content": "claude-3-5-sonnet-20241022,claude-3.5-sonnet,gemini-exp-1206,gpt-4,gpt-4-turbo-2024-04-09,gpt-4o,claude-3.5-haiku,gpt-4o-128k,gemini-1.5-flash-500k,claude-3-haiku-200k,claude-3-5-sonnet-200k"
}</code></pre>
<p>这些模型将默认进行使用量检查。您可以通过配置接口修改此设置。</p>
<p>路径修改注意:选择类型再修改文本,否则选择默认时内容的修改无效,在更新配置后自动被覆盖导致内容丢失,自行改进。</p>
<h2>静态资源接口</h2>
<h3>获取共享样式</h3>
<ul>
<li>接口地址: <code>/static/shared-styles.css</code></li>
<li>请求方法: GET</li>
<li>响应格式: CSS文件</li>
<li>功能: 获取共享样式表</li>
</ul>
<h3>获取共享脚本</h3>
<ul>
<li>接口地址: <code>/static/shared.js</code></li>
<li>请求方法: GET</li>
<li>响应格式: JavaScript文件</li>
<li>功能: 获取共享JavaScript代码</li>
</ul>
<h3>环境变量示例</h3>
<ul>
<li>接口地址: <code>/env-example</code></li>
<li>请求方法: GET</li>
<li>响应格式: 文本文件</li>
<li>功能: 获取环境变量配置示例</li>
</ul>
<h2>其他接口</h2>
<h3>获取模型列表</h3>
<ul>
<li>接口地址: <code>/v1/models</code></li>
<li>请求方法: GET</li>
<li>响应格式:</li>
</ul>
<pre><code class="language-json">{
"object": "list",
"data": [
{
"id": "string",
"object": "model",
"created": number,
"owned_by": "string"
}
]
}
</code></pre>
<h3>获取一个随机hash</h3>
<ul>
<li>接口地址: <code>/get-hash</code></li>
<li>请求方法: GET</li>
<li>响应格式:</li>
</ul>
<pre><code class="language-plaintext">string</code></pre>
<h3>获取或修复checksum</h3>
<ul>
<li>接口地址: <code>/get-checksum</code></li>
<li>请求方法: GET</li>
<li>请求参数:
<ul>
<li><code>checksum</code>: 可选用于修复的旧版本生成的checksum也可只传入前8个字符可用来自动刷新时间戳头</li>
</ul>
</li>
<li>响应格式:</li>
</ul>
<pre><code class="language-plaintext">string</code></pre>
<p>说明:</p>
<ul>
<li>如果不提供<code>checksum</code>参数将生成一个新的随机checksum</li>
<li>如果提供<code>checksum</code>参数将尝试修复旧版本的checksum以适配v0.1.3-rc.3之后的版本使用修复失败会返回新的checksum若输入的checksum本来就有效则返回更新tsheader后的checksum</li>
</ul>
<h3>获取当前的tsheader</h3>
<ul>
<li>接口地址: <code>/get-tsheader</code></li>
<li>请求方法: GET</li>
<li>响应格式:</li>
</ul>
<pre><code class="language-plaintext">string</code></pre>
<h3>健康检查接口</h3>
<ul>
<li>接口地址: <code>/health</code><code>/</code>(重定向)</li>
<li>请求方法: GET</li>
<li>认证方式: Bearer Token可选</li>
<li>响应格式: 根据配置返回不同的内容类型(默认、文本或HTML)默认JSON</li>
</ul>
<pre><code class="language-json">{
"status": "success",
"version": "string",
"uptime": number,
"stats": {
"started": "string",
"total_requests": number,
"active_requests": number,
"system": {
"memory": {
"rss": number
},
"cpu": {
"usage": number
}
}
},
"models": ["string"],
"endpoints": ["string"]
}
</code></pre>
<p>注意:<code>stats</code> 字段仅在请求头中包含正确的 <code>AUTH_TOKEN</code> 时才会返回。否则,该字段将被省略。</p>
<h3>获取日志接口</h3>
<ul>
<li>接口地址: <code>/logs</code></li>
<li>请求方法: GET</li>
<li>响应格式: 根据配置返回不同的内容类型(默认、文本或HTML)</li>
</ul>
<h3>获取日志数据</h3>
<ul>
<li>接口地址: <code>/logs</code></li>
<li>请求方法: POST</li>
<li>认证方式: Bearer Token</li>
<li>响应格式:</li>
</ul>
<pre><code class="language-json">{
"total": number,
"logs": [
{
"id": number,
"timestamp": "string",
"model": "string",
"token_info": {
"token": "string",
"checksum": "string",
"profile": {
"usage": {
"premium": {
"requests": number,
"requests_total": number,
"tokens": number,
"max_requests": number,
"max_tokens": number
},
"standard": {
"requests": number,
"requests_total": number,
"tokens": number,
"max_requests": number,
"max_tokens": number
},
"unknown": {
"requests": number,
"requests_total": number,
"tokens": number,
"max_requests": number,
"max_tokens": number
}
},
"user": {
"email": "string",
"name": "string",
"id": "string",
"updated_at": "string"
},
"stripe": {
"membership_type": "free" | "free_trial" | "pro" | "enterprise",
"payment_id": "string",
"days_remaining_on_trial": number
}
}
},
"prompt": "string",
"timing": {
"total": number,
"first": number
},
"stream": boolean,
"status": "string",
"error": "string"
}
],
"timestamp": "string",
"status": "success"
}
</code></pre>
<h3>获取用户信息</h3>
<ul>
<li>接口地址: <code>/userinfo</code></li>
<li>请求方法: POST</li>
<li>认证方式: 请求体中包含token</li>
<li>请求格式:</li>
</ul>
<pre><code class="language-json">{
"token": "string"
}
</code></pre>
<ul>
<li>响应格式:</li>
</ul>
<pre><code class="language-json">{
"usage": {
"premium": {
"requests": number,
"requests_total": number,
"tokens": number,
"max_requests": number,
"max_tokens": number
},
"standard": {
"requests": number,
"requests_total": number,
"tokens": number,
"max_requests": number,
"max_tokens": number
},
"unknown": {
"requests": number,
"requests_total": number,
"tokens": number,
"max_requests": number,
"max_tokens": number
}
},
"user": {
"email": "string",
"name": "string",
"id": "string",
"updated_at": "string"
},
"stripe": {
"membership_type": "free" | "free_trial" | "pro" | "enterprise",
"payment_id": "string",
"days_remaining_on_trial": number
}
}
</code></pre>
<p>如果发生错误,响应格式为:</p>
<pre><code class="language-json">{
"error": "string"
}
</code></pre>
<h3>基础校准</h3>
<ul>
<li>接口地址: <code>/basic-calibration</code></li>
<li>请求方法: POST</li>
<li>认证方式: 请求体中包含token</li>
<li>请求格式:</li>
</ul>
<pre><code class="language-json">{
"token": "string"
}
</code></pre>
<ul>
<li>响应格式:</li>
</ul>
<pre><code class="language-json">{
"status": "success" | "error",
"message": "string",
"user_id": "string",
"create_at": "string",
"checksum_time": number
}
</code></pre>
<p>注意: <code>user_id</code>, <code>create_at</code>, 和 <code>checksum_time</code> 字段在校验失败时可能不存在。</p>
<h2>偷偷写在最后的话</h2>
<p>虽然作者觉得<del></del>收点钱合理,但不强求,要是<strong>主动自愿</strong>发我我肯定收(因为真有人这么做,虽然不是赞助),赞助很合理吧</p>
<p>不是<strong>主动自愿</strong>就算了,不是很缺,给了会很感动罢了。</p>
<p>虽然不是很建议你赞助,但如果你赞助了,大概可以:</p>
<ul>
<li>测试版更新</li>
<li>要求功能</li>
<li>问题更快解决</li>
</ul>
<p>即使如此,我也保留可以拒绝赞助和拒绝要求的权利。</p>
<p>求赞助还是有点不要脸了,接下来是吐槽:</p>
<p>辛辛苦苦做这个也不知道是为了谁好累。其实还有很多功能可以做比如直接传token支持配置其实这个要专门做一个页面这个作为rc.4的计划之一吧。</p>
<p>主要没想做用户管理所以不存在是否接入LinuxDo的问题。虽然那个半成品公益版做好了就是了。</p>
<p>就说这么多,没啥可说的,不管那么多,做就完了。<span>[doge]</span> 自己想象吧。</p>
<p>为什么一直说要跑路呢主要是有时Cursor的Claude太假了堪比gpt-4o-mini我对比发现真没啥差别比以前差远了无力了所以不太想做了。我也感觉很奇怪。</p>
<p>查询额度会在一开始检测导致和完成时的额度有些差别,但是懒得改了,反正差别不大,对话也没响应内容,恰好完成了统一。</p>
<p>有人说少个二维码来着,还是算了。如果觉得好用,给点支持。其实没啥大不了的,没兴趣就不做了。不想那么多了。</p>

View File

@@ -117,7 +117,7 @@ input[type="checkbox"] {
appearance: auto;
}
input[type="checkbox"] + label {
input[type="checkbox"]+label {
cursor: pointer;
color: var(--text-primary);
user-select: none;
@@ -217,7 +217,39 @@ button:disabled {
/* 次要按钮样式 */
button.secondary {
background: var(--text-secondary);
background: transparent;
border: 1px solid var(--primary-color);
color: var(--primary-color);
}
button.secondary:hover {
background: var(--primary-color-alpha);
border-color: var(--primary-dark);
color: var(--primary-dark);
}
button.danger {
background: var(--error-color);
border: none;
}
button.danger:hover {
background: #d32f2f;
/* 深红色 */
box-shadow: 0 4px 12px rgba(244, 67, 54, 0.2);
}
/* 激活状态的按钮 */
button.active {
background: var(--primary-dark);
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
transform: translateY(1px);
}
button.secondary.active {
background: var(--primary-color);
color: white;
border-color: var(--primary-dark);
}
/* 按钮组 */
@@ -227,22 +259,93 @@ button.secondary {
margin: var(--spacing) 0;
}
/* 消息提示 */
/* 按钮组中的按钮间距调整 */
.button-group button {
flex: 1;
min-width: 120px;
}
/* 消息容器 - 固定在顶部中间 */
.message-container {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 9999;
display: flex;
flex-direction: column;
align-items: center;
pointer-events: none;
/* 允许点击穿透 */
}
/* 单个消息样式 */
.message {
padding: 12px;
border-radius: var(--border-radius);
margin: 10px 0;
border: 1px solid transparent;
padding: 12px 20px;
border-radius: 4px;
background: var(--card-background);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
margin-bottom: 10px;
pointer-events: auto;
/* 允许消息本身可以交互 */
min-width: 300px;
max-width: 500px;
display: flex;
align-items: center;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
animation: messageIn 0.3s ease-in-out;
}
.success {
background: var(--success-color);
color: #fff;
.message.success {
background: #f0f9eb;
border: 1px solid #e1f3d8;
}
.error {
background: var(--error-color);
color: #fff;
.message.error {
background: #fef0f0;
border: 1px solid #fde2e2;
}
@keyframes messageIn {
0% {
opacity: 0;
transform: translateY(-20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
@keyframes messageOut {
0% {
opacity: 1;
transform: translateY(0);
}
100% {
opacity: 0;
transform: translateY(-20px);
}
}
/* 深色模式适配 */
@media (prefers-color-scheme: dark) {
.message {
background: #2c2c2c;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.message.success {
background: #294929;
border-color: #1c321c;
}
.message.error {
background: #4d2c2c;
border-color: #321c1c;
}
}
/* 表格样式 */

View File

@@ -24,19 +24,50 @@ function getAuthToken() {
// 消息显示功能
function showMessage(elementId, text, isError = false) {
const msg = document.getElementById(elementId);
msg.className = `message ${isError ? 'error' : 'success'}`;
msg.textContent = text;
let msg = document.getElementById(elementId);
// 如果消息元素不存在,创建一个新的
if (!msg) {
msg = document.createElement('div');
msg.id = elementId;
document.body.appendChild(msg);
}
msg.className = `floating-message ${isError ? 'error' : 'success'}`;
msg.innerHTML = text.replace(/\n/g, '<br>');
}
function showGlobalMessage(text, isError = false) {
showMessage('message', text, isError);
// 3秒后自动清除消息
// 确保消息容器存在
function ensureMessageContainer() {
let container = document.querySelector('.message-container');
if (!container) {
container = document.createElement('div');
container.className = 'message-container';
document.body.appendChild(container);
}
return container;
}
function showGlobalMessage(text, isError = false, timeout = 3000) {
const container = ensureMessageContainer();
const msgElement = document.createElement('div');
msgElement.className = `message ${isError ? 'error' : 'success'}`;
msgElement.textContent = text;
container.appendChild(msgElement);
// 设置淡出动画和移除
setTimeout(() => {
const msg = document.getElementById('message');
msg.textContent = '';
msg.className = 'message';
}, 3000);
msgElement.style.animation = 'messageOut 0.3s ease-in-out';
setTimeout(() => {
msgElement.remove();
// 如果容器为空,也移除容器
if (container.children.length === 0) {
container.remove();
}
}, 300);
}, timeout);
}
// Token 输入框自动填充和事件绑定

View File

@@ -1,127 +0,0 @@
<!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>
.token-container {
display: grid;
gap: var(--spacing);
}
.token-section {
background: var(--card-background);
padding: var(--spacing);
border-radius: var(--border-radius);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.shortcuts {
margin-top: var(--spacing);
padding: 12px;
background: #f8f9fa;
border-radius: 4px;
font-size: 14px;
}
kbd {
background: #eee;
border-radius: 3px;
border: 1px solid #b4b4b4;
padding: 1px 4px;
font-size: 12px;
}
</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="token-container">
<div class="token-section">
<h3>Token 配置</h3>
<div class="button-group">
<button onclick="getTokenInfo()">获取当前配置</button>
<button onclick="updateTokenInfo()" class="secondary">保存更改</button>
</div>
<div class="form-group">
<label>Token 文件内容:</label>
<textarea id="tokens" placeholder="每行一个 token"></textarea>
</div>
<div class="form-group">
<label>Token List 文件内容:</label>
<textarea id="tokenList" placeholder="token,checksum 格式,每行一对"></textarea>
</div>
<div class="shortcuts">
快捷键: <kbd>Ctrl</kbd> + <kbd>S</kbd> 保存更改
</div>
</div>
</div>
<div id="message"></div>
<script>
function showMessage(text, isError = false) {
showGlobalMessage(text, isError);
}
async function getTokenInfo() {
const data = await makeAuthenticatedRequest('/get-tokeninfo');
if (data) {
document.getElementById('tokens').value = data.tokens;
document.getElementById('tokenList').value = data.token_list;
showGlobalMessage('配置获取成功');
}
}
async function updateTokenInfo() {
const tokens = document.getElementById('tokens').value;
const tokenList = document.getElementById('tokenList').value;
if (!tokens) {
showGlobalMessage('Token 文件内容不能为空', true);
return;
}
const data = await makeAuthenticatedRequest('/update-tokeninfo', {
body: JSON.stringify({
tokens: tokens,
token_list: tokenList || undefined
})
});
if (data) {
showGlobalMessage(`更新成功: ${data.message}`);
}
}
// 快捷键支持
document.addEventListener('keydown', function (e) {
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
updateTokenInfo();
}
});
// 初始化 token 处理
initializeTokenHandling('authToken');
</script>
</body>
</html>

603
static/tokens.html Normal file
View File

@@ -0,0 +1,603 @@
<!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>
.token-container {
display: grid;
gap: var(--spacing);
}
.token-section {
background: var(--card-background);
padding: var(--spacing);
border-radius: var(--border-radius);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.shortcuts {
margin-top: var(--spacing);
padding: 12px;
background: var(--disabled-bg);
border-radius: 4px;
font-size: 14px;
color: var(--text-secondary);
}
kbd {
background: var(--card-background);
border-radius: 3px;
border: 1px solid var(--border-color);
padding: 1px 4px;
font-size: 12px;
color: var(--text-primary);
}
.token-table {
width: 100%;
border-collapse: collapse;
margin: 8px 0;
background: var(--card-background);
}
.token-table th,
.token-table td {
padding: 8px;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
.token-table th {
background: var(--disabled-bg);
font-weight: 500;
color: var(--text-primary);
}
.token-list-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.token-list-header button {
padding: 4px 12px;
font-size: 14px;
}
/* Token表格样式优化 */
.token-table td {
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--text-primary);
}
.token-table tr:hover {
background: var(--primary-color-alpha);
}
.token-table tr:hover td {
white-space: normal;
word-break: break-all;
}
/* 操作按钮样式 */
.action-cell {
width: 100px;
text-align: center !important;
}
.action-cell button {
padding: 2px 8px;
font-size: 12px;
white-space: nowrap;
}
/* 提示框样式 */
.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;
}
.modal-header h3 {
margin: 0;
color: var(--text-primary);
}
.modal-footer {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid var(--border-color);
text-align: right;
}
/* 复选框容器样式 */
.checkbox-container {
margin: 8px 0;
}
.checkbox-container label {
display: inline;
margin-left: 8px;
color: var(--text-primary);
}
/* 帮助文本样式 */
.help-text {
color: var(--text-secondary);
}
@media (prefers-color-scheme: dark) {
.modal {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.token-table tr:hover {
background: rgba(144, 202, 249, 0.1);
/* --primary-color in dark mode */
}
}
/* Key结果样式 */
.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;
/* Firefox */
-ms-overflow-style: none;
/* IE and Edge */
}
/* Webkit浏览器的滚动条样式 */
.key-content::-webkit-scrollbar {
height: 6px;
}
.key-content::-webkit-scrollbar-track {
background: var(--disabled-bg);
border-radius: 3px;
}
.key-content::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 3px;
}
.key-content::-webkit-scrollbar-thumb:hover {
background: var(--text-secondary);
}
@media (prefers-color-scheme: dark) {
.key-content::-webkit-scrollbar-track {
background: var(--card-background);
}
.key-content::-webkit-scrollbar-thumb {
background: var(--text-secondary);
}
.key-content::-webkit-scrollbar-thumb:hover {
background: var(--text-primary);
}
}
.model-list {
max-height: 150px;
overflow-y: auto;
padding: 8px;
border: 1px solid var(--border-color);
border-radius: 4px;
margin-top: 8px;
background: var(--card-background);
}
.model-item {
display: flex;
align-items: center;
margin-bottom: 4px;
}
.model-item input[type="checkbox"] {
margin-right: 8px;
}
</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="token-container">
<div class="token-section">
<h3>Token 管理</h3>
<div class="button-group">
<button onclick="getTokenInfo()">获取当前配置</button>
<button onclick="addTokens()" class="secondary">添加Token</button>
<button onclick="deleteTokens()" class="danger">删除Token</button>
</div>
<div class="form-group">
<label>Token 操作:</label>
<textarea id="tokenInput" placeholder="每行一个 token"></textarea>
<div class="help-text">添加模式: 输入要添加的token每行一个
删除模式: 输入要删除的token每行一个</div>
</div>
<div class="form-group">
<div class="token-list-header">
<label>当前Token列表:</label>
<button onclick="copyTokenList()" class="secondary">复制列表</button>
</div>
<table class="token-table">
<thead>
<tr>
<th>Token</th>
<th>Checksum</th>
<th class="action-cell">操作</th>
</tr>
</thead>
<tbody id="tokenTableBody">
</tbody>
</table>
</div>
<div class="shortcuts">
快捷键: <kbd>Ctrl</kbd> + <kbd>Enter</kbd> 执行当前操作
</div>
</div>
</div>
<div id="message"></div>
<!-- 动态key生成对话框 -->
<div class="modal-backdrop" id="keyModal-backdrop" onclick="closeKeyModal()"></div>
<div class="modal" id="keyModal">
<div class="modal-header">
<h3>生成动态Key</h3>
</div>
<div class="form-group">
<label>流第一个块检查:</label>
<select id="enableStreamCheck">
<option value="">跟随全局</option>
<option value="true">启用</option>
<option value="false">禁用</option>
</select>
</div>
<div class="form-group">
<label>包含停止流:</label>
<select id="includeStopStream">
<option value="">跟随全局</option>
<option value="true">启用</option>
<option value="false">禁用</option>
</select>
</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;">
<!-- 模型列表将通过 JavaScript 动态填充 -->
</div>
</div>
<div class="key-result" id="keyResult" style="display: none;" onclick="copyGeneratedKey()">
<div class="key-content" id="keyContent"></div>
</div>
<div class="modal-footer">
<button onclick="closeKeyModal()" class="secondary">取消</button>
<button onclick="generateKey()" class="primary">生成</button>
</div>
</div>
<script>
async function getTokenInfo() {
const data = await makeAuthenticatedRequest('/tokens/get');
if (data) {
const tableBody = document.getElementById('tokenTableBody');
tableBody.innerHTML = data.tokens.map(t => `<tr><td title="${t.token}">${t.token}</td><td title="${t.checksum}">${t.checksum}</td><td class="action-cell"><button onclick="showKeyModal('${t.token}','${t.checksum}')" class="secondary">生成Key</button></td></tr>`).join('');
showGlobalMessage('配置获取成功');
}
}
function copyTokenList() {
const tableBody = document.getElementById('tokenTableBody');
const rows = tableBody.getElementsByTagName('tr');
const tokenList = Array.from(rows).map(row => {
const token = row.cells[0].textContent;
const checksum = row.cells[1].textContent;
return `${token},${checksum}`;
}).join('\n');
navigator.clipboard.writeText(tokenList).then(() => {
showGlobalMessage('Token列表已复制到剪贴板');
}).catch(err => {
showGlobalMessage('复制失败: ' + err, true);
});
}
async function addTokens() {
const tokensInput = document.getElementById('tokenInput').value;
if (!tokensInput) {
showGlobalMessage('请输入要添加的Token', true);
return;
}
// 处理输入的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
};
});
if (tokenList.length === 0) {
showGlobalMessage('没有有效的Token输入', true);
return;
}
const data = await makeAuthenticatedRequest('/tokens/add', {
body: JSON.stringify(tokenList)
});
if (data) {
showGlobalMessage(`添加成功: ${data.message}`);
document.getElementById('tokenInput').value = '';
getTokenInfo(); // 刷新当前配置
}
}
async function deleteTokens() {
const tokensToDelete = document.getElementById('tokenInput').value;
if (!tokensToDelete) {
showGlobalMessage('请输入要删除的Token', true);
return;
}
const tokens = tokensToDelete.trim().split('\n').filter(t => t);
const data = await makeAuthenticatedRequest('/tokens/delete', {
body: JSON.stringify({
tokens: tokens,
expectation: 'detailed'
})
});
if (data) {
let message = '删除操作完成\n';
if (data.failed_tokens?.length) {
message += `\n未找到的Token: ${data.failed_tokens.join('\n')}`;
}
if (data.updated_tokens?.length) {
message += `\n剩余Token: ${data.updated_tokens.join('\n')}`;
}
showGlobalMessage(message, timeout = 30000);
document.getElementById('tokenInput').value = '';
getTokenInfo();
}
}
// 动态key相关函数
let availableModels = [];
let currentToken = '';
let currentChecksum = '';
async function getModels() {
try {
const response = await fetch('/v1/models');
const data = await response.json();
availableModels = data.data.map(model => model.id);
updateModelList();
} catch (error) {
showGlobalMessage('获取模型列表失败', true);
}
}
function updateModelList() {
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 type = document.getElementById('usageCheckType').value;
const container = document.getElementById('modelListContainer');
container.style.display = type === 'custom' ? 'block' : 'none';
}
function showKeyModal(token, checksum) {
currentToken = token;
currentChecksum = checksum;
const modal = document.getElementById('keyModal');
const backdrop = document.getElementById('keyModal-backdrop');
modal.style.display = 'block';
backdrop.style.display = 'block';
document.getElementById('keyResult').style.display = 'none';
// 添加点击事件处理
modal.addEventListener('click', function (event) {
event.stopPropagation(); // 防止点击modal内部时触发backdrop的点击事件
});
// 重置所有选项
document.getElementById('enableStreamCheck').value = '';
document.getElementById('includeStopStream').value = '';
document.getElementById('disableVision').value = '';
document.getElementById('enableSlowPool').value = '';
document.getElementById('usageCheckType').value = '';
document.getElementById('modelListContainer').style.display = 'none';
}
function closeKeyModal() {
document.getElementById('keyModal').style.display = 'none';
document.getElementById('keyModal-backdrop').style.display = 'none';
}
function parseBooleanFromString(value, defaultValue) {
if (value === '') return defaultValue;
return value === 'true';
}
async function generateKey() {
const type = document.getElementById('usageCheckType').value;
let modelIds = '';
if (type === 'custom') {
modelIds = Array.from(document.querySelectorAll('#modelListContainer input:checked'))
.map(input => input.value)
.join(',');
}
const payload = {
auth_token: `${currentToken},${currentChecksum}`,
enable_stream_check: parseBooleanFromString(document.getElementById('enableStreamCheck').value, undefined),
include_stop_stream: parseBooleanFromString(document.getElementById('includeStopStream').value, undefined),
disable_vision: parseBooleanFromString(document.getElementById('disableVision').value, undefined),
enable_slow_pool: parseBooleanFromString(document.getElementById('enableSlowPool').value, undefined),
usage_check_models: type ? {
type: type,
model_ids: type === 'custom' ? modelIds : undefined
} : undefined
};
const data = await makeAuthenticatedRequest('/build-key', {
body: JSON.stringify(payload)
});
if (data && data.key) {
const keyResult = document.getElementById('keyResult');
const keyContent = document.getElementById('keyContent');
keyContent.textContent = data.key;
keyResult.style.display = 'block';
showGlobalMessage('动态Key已生成点击复制');
}
}
function copyGeneratedKey(event) {
// 防止触发滚动条点击事件
if (event && event.target.classList.contains('key-content') && event.offsetX > event.target.clientWidth) {
return;
}
const keyContent = document.getElementById('keyContent').textContent;
navigator.clipboard.writeText(keyContent).then(() => {
showGlobalMessage('Key已复制到剪贴板');
}).catch(() => {
showGlobalMessage('复制失败', true);
});
}
// 快捷键支持
document.addEventListener('keydown', function (e) {
if (e.ctrlKey && e.key === 'Enter') {
e.preventDefault();
const activeElement = document.activeElement;
if (activeElement.id === 'tokenInput') {
// 根据当前焦点确定操作
const action = document.querySelector('.button-group button.active');
if (action) {
action.click();
}
}
}
});
// 初始化 token 处理
initializeTokenHandling('authToken');
// 页面加载完成后获取当前配置和模型列表
document.addEventListener('DOMContentLoaded', () => {
getModels();
getTokenInfo();
});
</script>
</body>
</html>