lin7zhi's picture
Upload folder using huggingface_hub
97ec0e5 verified
let authToken = localStorage.getItem('authToken');
let oauthPort = null;
const CLIENT_ID = '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com';
const SCOPES = [
'https://www.googleapis.com/auth/cloud-platform',
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/cclog',
'https://www.googleapis.com/auth/experimentsandconfigs'
].join(' ');
function showToast(message, type = 'info', title = '') {
const icons = { success: '✅', error: '❌', warning: '⚠️', info: 'ℹ️' };
const titles = { success: '成功', error: '错误', warning: '警告', info: '提示' };
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.innerHTML = `
<div class="toast-icon">${icons[type]}</div>
<div class="toast-content">
<div class="toast-title">${title || titles[type]}</div>
<div class="toast-message">${message}</div>
</div>
`;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'slideOut 0.3s ease';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
function showConfirm(message, title = '确认操作') {
return new Promise((resolve) => {
const modal = document.createElement('div');
modal.className = 'modal';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-title">${title}</div>
<div class="modal-message">${message}</div>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="this.closest('.modal').remove(); window.modalResolve(false)">取消</button>
<button class="btn btn-danger" onclick="this.closest('.modal').remove(); window.modalResolve(true)">确定</button>
</div>
</div>
`;
document.body.appendChild(modal);
modal.onclick = (e) => { if (e.target === modal) { modal.remove(); resolve(false); } };
window.modalResolve = resolve;
});
}
function showLoading(text = '处理中...') {
const overlay = document.createElement('div');
overlay.className = 'loading-overlay';
overlay.id = 'loadingOverlay';
overlay.innerHTML = `<div class="spinner"></div><div class="loading-text">${text}</div>`;
document.body.appendChild(overlay);
}
function hideLoading() {
const overlay = document.getElementById('loadingOverlay');
if (overlay) overlay.remove();
}
if (authToken) {
showMainContent();
loadTokens();
loadConfig();
}
document.getElementById('login').addEventListener('submit', async (e) => {
e.preventDefault();
const btn = e.target.querySelector('button[type="submit"]');
if (btn.disabled) return;
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
btn.disabled = true;
btn.classList.add('loading');
const originalText = btn.textContent;
btn.textContent = '登录中';
try {
const response = await fetch('/admin/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (data.success) {
authToken = data.token;
localStorage.setItem('authToken', authToken);
showToast('登录成功,欢迎回来!', 'success');
showMainContent();
loadTokens();
} else {
showToast(data.message || '用户名或密码错误', 'error');
}
} catch (error) {
showToast('登录失败: ' + error.message, 'error');
} finally {
btn.disabled = false;
btn.classList.remove('loading');
btn.textContent = originalText;
}
});
function showOAuthModal() {
showToast('点击后请在新窗口完成授权', 'info', '提示');
const modal = document.createElement('div');
modal.className = 'modal form-modal';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-title">🔐 OAuth授权登录</div>
<div class="oauth-steps">
<p><strong>📝 授权流程:</strong></p>
<p>1️⃣ 点击下方按钮打开Google授权页面</p>
<p>2️⃣ 完成授权后,复制浏览器地址栏的完整URL</p>
<p>3️⃣ 粘贴URL到下方输入框并提交</p>
</div>
<button type="button" onclick="openOAuthWindow()" class="btn btn-success" style="width: 100%; margin-bottom: 16px;">🔐 打开授权页面</button>
<input type="text" id="modalCallbackUrl" placeholder="粘贴完整的回调URL (http://localhost:xxxxx/oauth-callback?code=...)">
<div class="modal-actions">
<button class="btn btn-secondary" onclick="this.closest('.modal').remove()">取消</button>
<button class="btn btn-success" onclick="processOAuthCallbackModal()">✅ 提交</button>
</div>
</div>
`;
document.body.appendChild(modal);
modal.onclick = (e) => { if (e.target === modal) modal.remove(); };
}
function showManualModal() {
const modal = document.createElement('div');
modal.className = 'modal form-modal';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-title">✏️ 手动填入Token</div>
<div class="form-row">
<input type="text" id="modalAccessToken" placeholder="Access Token (必填)">
<input type="text" id="modalRefreshToken" placeholder="Refresh Token (必填)">
<input type="number" id="modalExpiresIn" placeholder="过期时间(秒)" value="3599">
</div>
<p style="font-size: 0.85rem; color: var(--text-light); margin-bottom: 16px;">💡 提示:过期时间默认3599秒(约1小时)</p>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="this.closest('.modal').remove()">取消</button>
<button class="btn btn-success" onclick="addTokenFromModal()">✅ 添加</button>
</div>
</div>
`;
document.body.appendChild(modal);
modal.onclick = (e) => { if (e.target === modal) modal.remove(); };
}
function openOAuthWindow() {
oauthPort = Math.floor(Math.random() * 10000) + 50000;
const redirectUri = `http://localhost:${oauthPort}/oauth-callback`;
const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?` +
`access_type=offline&client_id=${CLIENT_ID}&prompt=consent&` +
`redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&` +
`scope=${encodeURIComponent(SCOPES)}&state=${Date.now()}`;
window.open(authUrl, '_blank');
}
async function processOAuthCallbackModal() {
const modal = document.querySelector('.form-modal');
const callbackUrl = document.getElementById('modalCallbackUrl').value.trim();
if (!callbackUrl) {
showToast('请输入回调URL', 'warning');
return;
}
showLoading('正在处理授权...');
try {
const url = new URL(callbackUrl);
const code = url.searchParams.get('code');
const port = new URL(url.origin).port || (url.protocol === 'https:' ? 443 : 80);
if (!code) {
hideLoading();
showToast('URL中未找到授权码,请检查URL是否完整', 'error');
return;
}
const response = await fetch('/admin/oauth/exchange', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({ code, port })
});
const result = await response.json();
if (result.success) {
const account = result.data;
const addResponse = await fetch('/admin/tokens', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify(account)
});
const addResult = await addResponse.json();
hideLoading();
if (addResult.success) {
modal.remove();
showToast('Token添加成功!', 'success');
loadTokens();
} else {
showToast('Token添加失败: ' + addResult.message, 'error');
}
} else {
hideLoading();
showToast('Token交换失败: ' + result.message, 'error');
}
} catch (error) {
hideLoading();
showToast('处理失败: ' + error.message, 'error');
}
}
async function addTokenFromModal() {
const modal = document.querySelector('.form-modal');
const accessToken = document.getElementById('modalAccessToken').value.trim();
const refreshToken = document.getElementById('modalRefreshToken').value.trim();
const expiresIn = parseInt(document.getElementById('modalExpiresIn').value);
if (!accessToken || !refreshToken) {
showToast('请填写完整的Token信息', 'warning');
return;
}
showLoading('正在添加Token...');
try {
const response = await fetch('/admin/tokens', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({ access_token: accessToken, refresh_token: refreshToken, expires_in: expiresIn })
});
const data = await response.json();
hideLoading();
if (data.success) {
modal.remove();
showToast('Token添加成功!', 'success');
loadTokens();
} else {
showToast(data.message || '添加失败', 'error');
}
} catch (error) {
hideLoading();
showToast('添加失败: ' + error.message, 'error');
}
}
function showMainContent() {
document.getElementById('loginForm').classList.add('hidden');
document.getElementById('mainContent').classList.remove('hidden');
}
function switchTab(tab) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
event.target.classList.add('active');
if (tab === 'tokens') {
document.getElementById('tokensPage').classList.remove('hidden');
document.getElementById('settingsPage').classList.add('hidden');
} else if (tab === 'settings') {
document.getElementById('tokensPage').classList.add('hidden');
document.getElementById('settingsPage').classList.remove('hidden');
loadConfig();
}
}
async function logout() {
const confirmed = await showConfirm('确定要退出登录吗?', '退出确认');
if (!confirmed) return;
localStorage.removeItem('authToken');
authToken = null;
document.getElementById('loginForm').classList.remove('hidden');
document.getElementById('mainContent').classList.add('hidden');
showToast('已退出登录', 'info');
}
async function loadTokens() {
try {
const response = await fetch('/admin/tokens', {
headers: { 'Authorization': `Bearer ${authToken}` }
});
if (response.status === 401) {
logout();
return;
}
const data = await response.json();
if (data.success) {
renderTokens(data.data);
} else {
showToast('加载失败: ' + (data.message || '未知错误'), 'error');
}
} catch (error) {
showToast('加载Token失败: ' + error.message, 'error');
}
}
function renderTokens(tokens) {
document.getElementById('totalTokens').textContent = tokens.length;
document.getElementById('enabledTokens').textContent = tokens.filter(t => t.enable).length;
document.getElementById('disabledTokens').textContent = tokens.filter(t => !t.enable).length;
const tokenList = document.getElementById('tokenList');
if (tokens.length === 0) {
tokenList.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">📦</div>
<div class="empty-state-text">暂无Token</div>
<div class="empty-state-hint">点击上方按钮添加您的第一个Token</div>
</div>
`;
return;
}
tokenList.innerHTML = tokens.map(token => `
<div class="token-card">
<div class="token-header">
<span class="status ${token.enable ? 'enabled' : 'disabled'}">
${token.enable ? '✅ 启用' : '❌ 禁用'}
</span>
<span class="token-id">#${token.refresh_token.substring(0, 8)}</span>
</div>
<div class="token-info">
<div class="info-row">
<span class="info-label">🎫 Access</span>
<span class="info-value">${token.access_token_suffix}</span>
</div>
<div class="info-row">
<span class="info-label">📦 Project</span>
<span class="info-value">${token.projectId || 'N/A'}</span>
</div>
<div class="info-row">
<span class="info-label">⏰ 过期</span>
<span class="info-value">${new Date(token.timestamp + token.expires_in * 1000).toLocaleString('zh-CN', {month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit'})}</span>
</div>
</div>
<div class="token-actions">
<button class="btn ${token.enable ? 'btn-warning' : 'btn-success'}" onclick="toggleToken('${token.refresh_token}', ${!token.enable})">
${token.enable ? '⏸️ 禁用' : '▶️ 启用'}
</button>
<button class="btn btn-danger" onclick="deleteToken('${token.refresh_token}')">🗑️ 删除</button>
</div>
</div>
`).join('');
}
async function toggleToken(refreshToken, enable) {
const action = enable ? '启用' : '禁用';
const confirmed = await showConfirm(`确定要${action}这个Token吗?`, `${action}确认`);
if (!confirmed) return;
showLoading(`正在${action}Token...`);
try {
const response = await fetch(`/admin/tokens/${encodeURIComponent(refreshToken)}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({ enable })
});
const data = await response.json();
hideLoading();
if (data.success) {
showToast(`Token已${enable ? '启用' : '禁用'}`, 'success');
loadTokens();
} else {
showToast(data.message || '操作失败', 'error');
}
} catch (error) {
hideLoading();
showToast('操作失败: ' + error.message, 'error');
}
}
async function deleteToken(refreshToken) {
const confirmed = await showConfirm('删除后无法恢复,确定要删除这个Token吗?', '⚠️ 删除确认');
if (!confirmed) return;
showLoading('正在删除Token...');
try {
const response = await fetch(`/admin/tokens/${encodeURIComponent(refreshToken)}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${authToken}` }
});
const data = await response.json();
hideLoading();
if (data.success) {
showToast('Token已删除', 'success');
loadTokens();
} else {
showToast(data.message || '删除失败', 'error');
}
} catch (error) {
hideLoading();
showToast('删除失败: ' + error.message, 'error');
}
}
async function loadConfig() {
try {
const response = await fetch('/admin/config', {
headers: { 'Authorization': `Bearer ${authToken}` }
});
const data = await response.json();
if (data.success) {
const form = document.getElementById('configForm');
const { env, json } = data.data;
// 加载 .env 配置
Object.entries(env).forEach(([key, value]) => {
const input = form.elements[key];
if (input) input.value = value || '';
});
// 加载 config.json 配置
if (json.server) {
if (form.elements['PORT']) form.elements['PORT'].value = json.server.port || '';
if (form.elements['HOST']) form.elements['HOST'].value = json.server.host || '';
if (form.elements['MAX_REQUEST_SIZE']) form.elements['MAX_REQUEST_SIZE'].value = json.server.maxRequestSize || '';
}
if (json.defaults) {
if (form.elements['DEFAULT_TEMPERATURE']) form.elements['DEFAULT_TEMPERATURE'].value = json.defaults.temperature ?? '';
if (form.elements['DEFAULT_TOP_P']) form.elements['DEFAULT_TOP_P'].value = json.defaults.topP ?? '';
if (form.elements['DEFAULT_TOP_K']) form.elements['DEFAULT_TOP_K'].value = json.defaults.topK ?? '';
if (form.elements['DEFAULT_MAX_TOKENS']) form.elements['DEFAULT_MAX_TOKENS'].value = json.defaults.maxTokens ?? '';
}
if (json.other) {
if (form.elements['TIMEOUT']) form.elements['TIMEOUT'].value = json.other.timeout ?? '';
if (form.elements['MAX_IMAGES']) form.elements['MAX_IMAGES'].value = json.other.maxImages ?? '';
if (form.elements['USE_NATIVE_AXIOS']) form.elements['USE_NATIVE_AXIOS'].value = json.other.useNativeAxios ? 'true' : 'false';
if (form.elements['SKIP_PROJECT_ID_FETCH']) form.elements['SKIP_PROJECT_ID_FETCH'].value = json.other.skipProjectIdFetch ? 'true' : 'false';
}
}
} catch (error) {
showToast('加载配置失败: ' + error.message, 'error');
}
}
document.getElementById('configForm').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const allConfig = Object.fromEntries(formData);
// 分离敏感和非敏感配置
const sensitiveKeys = ['API_KEY', 'ADMIN_USERNAME', 'ADMIN_PASSWORD', 'JWT_SECRET', 'PROXY', 'SYSTEM_INSTRUCTION', 'IMAGE_BASE_URL'];
const envConfig = {};
const jsonConfig = {
server: {},
api: {},
defaults: {},
other: {}
};
Object.entries(allConfig).forEach(([key, value]) => {
if (sensitiveKeys.includes(key)) {
envConfig[key] = value;
} else {
// 映射到 config.json 结构
if (key === 'PORT') jsonConfig.server.port = parseInt(value);
else if (key === 'HOST') jsonConfig.server.host = value;
else if (key === 'MAX_REQUEST_SIZE') jsonConfig.server.maxRequestSize = value;
else if (key === 'API_URL') jsonConfig.api.url = value;
else if (key === 'API_MODELS_URL') jsonConfig.api.modelsUrl = value;
else if (key === 'API_NO_STREAM_URL') jsonConfig.api.noStreamUrl = value;
else if (key === 'API_HOST') jsonConfig.api.host = value;
else if (key === 'API_USER_AGENT') jsonConfig.api.userAgent = value;
else if (key === 'DEFAULT_TEMPERATURE') jsonConfig.defaults.temperature = parseFloat(value);
else if (key === 'DEFAULT_TOP_P') jsonConfig.defaults.topP = parseFloat(value);
else if (key === 'DEFAULT_TOP_K') jsonConfig.defaults.topK = parseInt(value);
else if (key === 'DEFAULT_MAX_TOKENS') jsonConfig.defaults.maxTokens = parseInt(value);
else if (key === 'USE_NATIVE_AXIOS') jsonConfig.other.useNativeAxios = value !== 'false';
else if (key === 'TIMEOUT') jsonConfig.other.timeout = parseInt(value);
else if (key === 'MAX_IMAGES') jsonConfig.other.maxImages = parseInt(value);
else if (key === 'SKIP_PROJECT_ID_FETCH') jsonConfig.other.skipProjectIdFetch = value === 'true';
else envConfig[key] = value;
}
});
showLoading('正在保存配置...');
try {
const response = await fetch('/admin/config', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({ env: envConfig, json: jsonConfig })
});
const data = await response.json();
hideLoading();
if (data.success) {
showToast(data.message, 'success');
} else {
showToast(data.message || '保存失败', 'error');
}
} catch (error) {
hideLoading();
showToast('保存失败: ' + error.message, 'error');
}
});