Compare commits

..

4 Commits

Author SHA1 Message Date
Supra4E8C
30f5300bb4 Update README_CN.md 2025-10-01 16:10:47 +08:00
Supra4E8C
52169200f1 Update README.md 2025-10-01 16:10:22 +08:00
Supra4E8C
80b2597611 0.0.5
为Cli Proxy API主程序兼容做准备
2025-10-01 16:06:12 +08:00
Luis Pater
04f21eea98 change release file name 2025-10-01 02:00:53 +08:00
10 changed files with 424 additions and 2534 deletions

View File

@@ -31,18 +31,18 @@ jobs:
- name: Prepare release assets - name: Prepare release assets
run: | run: |
cd dist cd dist
mv index.html cli-proxy-api-management-center.html mv index.html management.html
ls -lh cli-proxy-api-management-center.html ls -lh management.html
- name: Create Release - name: Create Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
with: with:
files: dist/cli-proxy-api-management-center.html files: dist/management.html
body: | body: |
## CLI Proxy API Management Center - ${{ github.ref_name }} ## CLI Proxy API Management Center - ${{ github.ref_name }}
### Download and Usage ### Download and Usage
1. Download the `cli-proxy-api-management-center.html` file 1. Download the `management.html` file
2. Open it directly in your browser 2. Open it directly in your browser
3. All assets (CSS, JavaScript, images) are bundled into this single file 3. All assets (CSS, JavaScript, images) are bundled into this single file

View File

@@ -9,8 +9,9 @@ https://github.com/router-for-me/CLIProxyAPI
Example URL: Example URL:
https://remote.router-for.me/ https://remote.router-for.me/
Minimum required version: ≥ 5.0.0 Minimum required version: ≥ 6.0.0
Recommended version: ≥ 5.2.6 Recommended version: ≥ 6.0.19
Starting from version 6.0.19, the WebUI has been integrated into the main program and is accessible via `/management.html`.
## Features ## Features

View File

@@ -8,6 +8,7 @@ https://remote.router-for.me/
最低可用版本 ≥ 5.0.0 最低可用版本 ≥ 5.0.0
推荐版本 ≥ 5.2.6 推荐版本 ≥ 5.2.6
自6.0.19起WebUI已经集成在主程序中 可以通过/management.html访问
## 功能特点 ## 功能特点

300
app.js
View File

@@ -2,7 +2,8 @@
class CLIProxyManager { class CLIProxyManager {
constructor() { constructor() {
// 仅保存基础地址(不含 /v0/management请求时自动补齐 // 仅保存基础地址(不含 /v0/management请求时自动补齐
this.apiBase = 'http://localhost:8317'; const detectedBase = this.detectApiBaseFromLocation();
this.apiBase = detectedBase;
this.apiUrl = this.computeApiUrl(this.apiBase); this.apiUrl = this.computeApiUrl(this.apiBase);
this.managementKey = ''; this.managementKey = '';
this.isConnected = false; this.isConnected = false;
@@ -107,6 +108,7 @@ class CLIProxyManager {
this.setupLanguageSwitcher(); this.setupLanguageSwitcher();
this.setupThemeSwitcher(); this.setupThemeSwitcher();
// loadSettings 将在登录成功后调用 // loadSettings 将在登录成功后调用
this.updateLoginConnectionInfo();
} }
// 检查登录状态 // 检查登录状态
@@ -185,6 +187,7 @@ class CLIProxyManager {
document.getElementById('login-page').style.display = 'flex'; document.getElementById('login-page').style.display = 'flex';
document.getElementById('main-page').style.display = 'none'; document.getElementById('main-page').style.display = 'none';
this.isLoggedIn = false; this.isLoggedIn = false;
this.updateLoginConnectionInfo();
} }
// 显示主页面 // 显示主页面
@@ -236,55 +239,40 @@ class CLIProxyManager {
// 处理登录表单提交 // 处理登录表单提交
async handleLogin() { async handleLogin() {
// 获取当前活动的选项卡 const apiBaseInput = document.getElementById('login-api-base');
const activeTab = document.querySelector('.tab-button.active').getAttribute('data-tab'); const managementKeyInput = document.getElementById('login-management-key');
const managementKey = managementKeyInput ? managementKeyInput.value.trim() : '';
let apiUrl, managementKey;
if (!managementKey) {
if (activeTab === 'local') { this.showLoginError(i18n.t('login.error_required'));
// 本地连接从端口号构建URL return;
const port = document.getElementById('local-port').value.trim();
managementKey = document.getElementById('local-management-key').value.trim();
if (!port || !managementKey) {
this.showLoginError(i18n.t('login.error_required'));
return;
}
apiUrl = `http://localhost:${port}`;
} else {
// 远程连接使用完整URL
apiUrl = document.getElementById('remote-api-url').value.trim();
managementKey = document.getElementById('remote-management-key').value.trim();
if (!apiUrl || !managementKey) {
this.showLoginError(i18n.t('login.error_required'));
return;
}
} }
const proxyUrl = document.getElementById('login-proxy-url').value.trim(); if (apiBaseInput && apiBaseInput.value.trim()) {
this.setApiBase(apiBaseInput.value.trim());
}
const submitBtn = document.getElementById('login-submit'); const submitBtn = document.getElementById('login-submit');
const originalText = submitBtn.innerHTML; const originalText = submitBtn ? submitBtn.innerHTML : '';
try { try {
submitBtn.innerHTML = `<div class="loading"></div> ${i18n.t('login.submitting')}`; if (submitBtn) {
submitBtn.disabled = true; submitBtn.innerHTML = `<div class="loading"></div> ${i18n.t('login.submitting')}`;
this.hideLoginError(); submitBtn.disabled = true;
// 如果设置了代理,先保存代理设置
if (proxyUrl) {
localStorage.setItem('proxyUrl', proxyUrl);
} }
this.hideLoginError();
await this.login(apiUrl, managementKey);
this.managementKey = managementKey;
localStorage.setItem('managementKey', this.managementKey);
await this.login(this.apiBase, this.managementKey);
} catch (error) { } catch (error) {
this.showLoginError(`${i18n.t('login.error_title')}: ${error.message}`); this.showLoginError(`${i18n.t('login.error_title')}: ${error.message}`);
} finally { } finally {
submitBtn.innerHTML = originalText; if (submitBtn) {
submitBtn.disabled = false; submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
}
} }
} }
@@ -355,67 +343,30 @@ class CLIProxyManager {
loadLoginSettings() { loadLoginSettings() {
const savedBase = localStorage.getItem('apiBase'); const savedBase = localStorage.getItem('apiBase');
const savedKey = localStorage.getItem('managementKey'); const savedKey = localStorage.getItem('managementKey');
const savedProxy = localStorage.getItem('proxyUrl'); const loginKeyInput = document.getElementById('login-management-key');
const apiBaseInput = document.getElementById('login-api-base');
// 检查元素是否存在(确保在登录页面)
const localPortInput = document.getElementById('local-port');
const remoteApiInput = document.getElementById('remote-api-url');
const localKeyInput = document.getElementById('local-management-key');
const remoteKeyInput = document.getElementById('remote-management-key');
const proxyInput = document.getElementById('login-proxy-url');
// 设置本地端口和远程API地址
if (savedBase) { if (savedBase) {
if (savedBase.includes('localhost')) { this.setApiBase(savedBase);
// 从本地URL中提取端口号 } else {
const match = savedBase.match(/localhost:(\d+)/); this.setApiBase(this.detectApiBaseFromLocation());
if (match && localPortInput) {
localPortInput.value = match[1];
}
} else if (remoteApiInput) {
remoteApiInput.value = savedBase;
}
} }
// 设置密钥 if (apiBaseInput) {
if (localKeyInput && savedKey) { apiBaseInput.value = this.apiBase || '';
localKeyInput.value = savedKey;
} }
if (remoteKeyInput && savedKey) {
remoteKeyInput.value = savedKey; if (loginKeyInput && savedKey) {
loginKeyInput.value = savedKey;
} }
// 设置代理
if (proxyInput && savedProxy) {
proxyInput.value = savedProxy;
}
// 设置实时保存监听器
this.setupLoginAutoSave(); this.setupLoginAutoSave();
} }
// 设置登录页面自动保存
setupLoginAutoSave() { setupLoginAutoSave() {
const localPortInput = document.getElementById('local-port'); const loginKeyInput = document.getElementById('login-management-key');
const remoteApiInput = document.getElementById('remote-api-url'); const apiBaseInput = document.getElementById('login-api-base');
const localKeyInput = document.getElementById('local-management-key'); const resetButton = document.getElementById('login-reset-api-base');
const remoteKeyInput = document.getElementById('remote-management-key');
const proxyInput = document.getElementById('login-proxy-url');
const saveLocalBase = (port) => {
if (port.trim()) {
const apiUrl = `http://localhost:${port}`;
this.setApiBase(apiUrl);
}
};
const saveLocalBaseDebounced = this.debounce(saveLocalBase, 500);
const saveRemoteBase = (val) => {
if (val.trim()) {
this.setApiBase(val);
}
};
const saveRemoteBaseDebounced = this.debounce(saveRemoteBase, 500);
const saveKey = (val) => { const saveKey = (val) => {
if (val.trim()) { if (val.trim()) {
@@ -425,41 +376,32 @@ class CLIProxyManager {
}; };
const saveKeyDebounced = this.debounce(saveKey, 500); const saveKeyDebounced = this.debounce(saveKey, 500);
const saveProxy = (val) => { if (loginKeyInput) {
if (val.trim()) { loginKeyInput.addEventListener('change', (e) => saveKey(e.target.value));
localStorage.setItem('proxyUrl', val); loginKeyInput.addEventListener('input', (e) => saveKeyDebounced(e.target.value));
}
};
const saveProxyDebounced = this.debounce(saveProxy, 500);
// 绑定本地端口输入框
if (localPortInput) {
localPortInput.addEventListener('change', (e) => saveLocalBase(e.target.value));
localPortInput.addEventListener('input', (e) => saveLocalBaseDebounced(e.target.value));
} }
// 绑定远程API输入框 if (apiBaseInput) {
if (remoteApiInput) { const persistBase = (val) => {
remoteApiInput.addEventListener('change', (e) => saveRemoteBase(e.target.value)); const normalized = this.normalizeBase(val);
remoteApiInput.addEventListener('input', (e) => saveRemoteBaseDebounced(e.target.value)); if (normalized) {
this.setApiBase(normalized);
}
};
const persistBaseDebounced = this.debounce(persistBase, 500);
apiBaseInput.addEventListener('change', (e) => persistBase(e.target.value));
apiBaseInput.addEventListener('input', (e) => persistBaseDebounced(e.target.value));
} }
// 绑定本地密钥输入框 if (resetButton) {
if (localKeyInput) { resetButton.addEventListener('click', () => {
localKeyInput.addEventListener('change', (e) => saveKey(e.target.value)); const detected = this.detectApiBaseFromLocation();
localKeyInput.addEventListener('input', (e) => saveKeyDebounced(e.target.value)); this.setApiBase(detected);
} if (apiBaseInput) {
apiBaseInput.value = detected;
// 绑定远程密钥输入框 }
if (remoteKeyInput) { });
remoteKeyInput.addEventListener('change', (e) => saveKey(e.target.value));
remoteKeyInput.addEventListener('input', (e) => saveKeyDebounced(e.target.value));
}
// 绑定代理输入框
if (proxyInput) {
proxyInput.addEventListener('change', (e) => saveProxy(e.target.value));
proxyInput.addEventListener('input', (e) => saveProxyDebounced(e.target.value));
} }
} }
@@ -476,9 +418,6 @@ class CLIProxyManager {
logoutBtn.addEventListener('click', () => this.logout()); logoutBtn.addEventListener('click', () => this.logout());
} }
// 选项卡切换事件
this.setupTabSwitching();
// 密钥可见性切换事件 // 密钥可见性切换事件
this.setupKeyVisibilityToggle(); this.setupKeyVisibilityToggle();
@@ -486,30 +425,6 @@ class CLIProxyManager {
this.bindMainPageEvents(); this.bindMainPageEvents();
} }
// 设置选项卡切换
setupTabSwitching() {
const tabButtons = document.querySelectorAll('.tab-button');
const connectionForms = document.querySelectorAll('.connection-form');
tabButtons.forEach(button => {
button.addEventListener('click', () => {
const targetTab = button.getAttribute('data-tab');
// 更新选项卡状态
tabButtons.forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
// 切换表单
connectionForms.forEach(form => {
form.classList.remove('active');
if (form.id === `${targetTab}-form`) {
form.classList.add('active');
}
});
});
});
}
// 设置密钥可见性切换 // 设置密钥可见性切换
setupKeyVisibilityToggle() { setupKeyVisibilityToggle() {
const toggleButtons = document.querySelectorAll('.toggle-key-visibility'); const toggleButtons = document.querySelectorAll('.toggle-key-visibility');
@@ -729,6 +644,7 @@ class CLIProxyManager {
this.apiUrl = this.computeApiUrl(this.apiBase); this.apiUrl = this.computeApiUrl(this.apiBase);
localStorage.setItem('apiBase', this.apiBase); localStorage.setItem('apiBase', this.apiBase);
localStorage.setItem('apiUrl', this.apiUrl); // 兼容旧字段 localStorage.setItem('apiUrl', this.apiUrl); // 兼容旧字段
this.updateLoginConnectionInfo();
} }
// 加载设置(简化版,仅加载内部状态) // 加载设置(简化版,仅加载内部状态)
@@ -736,22 +652,21 @@ class CLIProxyManager {
const savedBase = localStorage.getItem('apiBase'); const savedBase = localStorage.getItem('apiBase');
const savedUrl = localStorage.getItem('apiUrl'); const savedUrl = localStorage.getItem('apiUrl');
const savedKey = localStorage.getItem('managementKey'); const savedKey = localStorage.getItem('managementKey');
// 只设置内部状态不操作DOM元素
if (savedBase) { if (savedBase) {
this.setApiBase(savedBase); this.setApiBase(savedBase);
} else if (savedUrl) { } else if (savedUrl) {
const base = (savedUrl || '').replace(/\/?v0\/management\/?$/i, ''); const base = (savedUrl || '').replace(/\/?v0\/management\/?$/i, '');
this.setApiBase(base); this.setApiBase(base);
} else { } else {
this.setApiBase(this.apiBase); this.setApiBase(this.detectApiBaseFromLocation());
} }
if (savedKey) { if (savedKey) {
this.managementKey = savedKey; this.managementKey = savedKey;
} }
this.updateLoginConnectionInfo();
} }
// API 请求方法 // API 请求方法
@@ -2191,6 +2106,7 @@ class CLIProxyManager {
showGeminiWebTokenModal() { showGeminiWebTokenModal() {
const inlineSecure1psid = document.getElementById('secure-1psid-input'); const inlineSecure1psid = document.getElementById('secure-1psid-input');
const inlineSecure1psidts = document.getElementById('secure-1psidts-input'); const inlineSecure1psidts = document.getElementById('secure-1psidts-input');
const inlineLabel = document.getElementById('gemini-web-label-input');
const modalBody = document.getElementById('modal-body'); const modalBody = document.getElementById('modal-body');
modalBody.innerHTML = ` modalBody.innerHTML = `
<h3>${i18n.t('auth_login.gemini_web_button')}</h3> <h3>${i18n.t('auth_login.gemini_web_button')}</h3>
@@ -2205,6 +2121,11 @@ class CLIProxyManager {
<input type="text" id="modal-secure-1psidts" placeholder="${i18n.t('auth_login.secure_1psidts_placeholder')}" required> <input type="text" id="modal-secure-1psidts" placeholder="${i18n.t('auth_login.secure_1psidts_placeholder')}" required>
<div class="form-hint">从浏览器开发者工具 → Application → Cookies 中获取</div> <div class="form-hint">从浏览器开发者工具 → Application → Cookies 中获取</div>
</div> </div>
<div class="form-group">
<label for="modal-gemini-web-label">${i18n.t('auth_login.gemini_web_label_label')}</label>
<input type="text" id="modal-gemini-web-label" placeholder="${i18n.t('auth_login.gemini_web_label_placeholder')}">
<div class="form-hint">为此认证文件设置一个标签名称(可选)</div>
</div>
<div class="modal-actions"> <div class="modal-actions">
<button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button> <button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button>
<button class="btn btn-primary" onclick="manager.saveGeminiWebToken()">${i18n.t('common.save')}</button> <button class="btn btn-primary" onclick="manager.saveGeminiWebToken()">${i18n.t('common.save')}</button>
@@ -2215,6 +2136,7 @@ class CLIProxyManager {
const modalSecure1psid = document.getElementById('modal-secure-1psid'); const modalSecure1psid = document.getElementById('modal-secure-1psid');
const modalSecure1psidts = document.getElementById('modal-secure-1psidts'); const modalSecure1psidts = document.getElementById('modal-secure-1psidts');
const modalLabel = document.getElementById('modal-gemini-web-label');
if (modalSecure1psid && inlineSecure1psid) { if (modalSecure1psid && inlineSecure1psid) {
modalSecure1psid.value = inlineSecure1psid.value.trim(); modalSecure1psid.value = inlineSecure1psid.value.trim();
@@ -2222,6 +2144,9 @@ class CLIProxyManager {
if (modalSecure1psidts && inlineSecure1psidts) { if (modalSecure1psidts && inlineSecure1psidts) {
modalSecure1psidts.value = inlineSecure1psidts.value.trim(); modalSecure1psidts.value = inlineSecure1psidts.value.trim();
} }
if (modalLabel && inlineLabel) {
modalLabel.value = inlineLabel.value.trim();
}
if (modalSecure1psid) { if (modalSecure1psid) {
modalSecure1psid.focus(); modalSecure1psid.focus();
@@ -2232,6 +2157,7 @@ class CLIProxyManager {
async saveGeminiWebToken() { async saveGeminiWebToken() {
const secure1psid = document.getElementById('modal-secure-1psid').value.trim(); const secure1psid = document.getElementById('modal-secure-1psid').value.trim();
const secure1psidts = document.getElementById('modal-secure-1psidts').value.trim(); const secure1psidts = document.getElementById('modal-secure-1psidts').value.trim();
const label = document.getElementById('modal-gemini-web-label').value.trim();
if (!secure1psid || !secure1psidts) { if (!secure1psid || !secure1psidts) {
this.showNotification('请填写完整的 Cookie 信息', 'error'); this.showNotification('请填写完整的 Cookie 信息', 'error');
@@ -2239,27 +2165,38 @@ class CLIProxyManager {
} }
try { try {
const requestBody = {
secure_1psid: secure1psid,
secure_1psidts: secure1psidts
};
// 如果提供了 label则添加到请求体中
if (label) {
requestBody.label = label;
}
const response = await this.makeRequest('/gemini-web-token', { const response = await this.makeRequest('/gemini-web-token', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify(requestBody)
secure_1psid: secure1psid,
secure_1psidts: secure1psidts
})
}); });
this.closeModal(); this.closeModal();
this.loadAuthFiles(); // 刷新认证文件列表 this.loadAuthFiles(); // 刷新认证文件列表
const inlineSecure1psid = document.getElementById('secure-1psid-input'); const inlineSecure1psid = document.getElementById('secure-1psid-input');
const inlineSecure1psidts = document.getElementById('secure-1psidts-input'); const inlineSecure1psidts = document.getElementById('secure-1psidts-input');
const inlineLabel = document.getElementById('gemini-web-label-input');
if (inlineSecure1psid) { if (inlineSecure1psid) {
inlineSecure1psid.value = secure1psid; inlineSecure1psid.value = secure1psid;
} }
if (inlineSecure1psidts) { if (inlineSecure1psidts) {
inlineSecure1psidts.value = secure1psidts; inlineSecure1psidts.value = secure1psidts;
} }
if (inlineLabel) {
inlineLabel.value = label;
}
this.showNotification(`${i18n.t('auth_login.gemini_web_saved')}: ${response.file}`, 'success'); this.showNotification(`${i18n.t('auth_login.gemini_web_saved')}: ${response.file}`, 'success');
} catch (error) { } catch (error) {
this.showNotification(`保存失败: ${error.message}`, 'error'); this.showNotification(`保存失败: ${error.message}`, 'error');
@@ -2614,6 +2551,28 @@ class CLIProxyManager {
closeModal() { closeModal() {
document.getElementById('modal').style.display = 'none'; document.getElementById('modal').style.display = 'none';
} }
detectApiBaseFromLocation() {
try {
const { protocol, hostname, port } = window.location;
const normalizedPort = port ? `:${port}` : '';
return this.normalizeBase(`${protocol}//${hostname}${normalizedPort}`);
} catch (error) {
console.warn('无法从当前地址检测 API 基础地址,使用默认设置', error);
return this.normalizeBase(this.apiBase || 'http://localhost:8317');
}
}
updateLoginConnectionInfo() {
const connectionUrlElement = document.getElementById('login-connection-url');
const customInput = document.getElementById('login-api-base');
if (connectionUrlElement) {
connectionUrlElement.textContent = this.apiBase || '-';
}
if (customInput && customInput !== document.activeElement) {
customInput.value = this.apiBase || '';
}
}
} }
// 全局管理器实例 // 全局管理器实例
@@ -2625,6 +2584,19 @@ function setupSiteLogo() {
const loginImg = document.getElementById('login-logo'); const loginImg = document.getElementById('login-logo');
if (!img && !loginImg) return; if (!img && !loginImg) return;
const inlineLogo = typeof window !== 'undefined' ? window.__INLINE_LOGO__ : null;
if (inlineLogo) {
if (img) {
img.src = inlineLogo;
img.style.display = 'inline-block';
}
if (loginImg) {
loginImg.src = inlineLogo;
loginImg.style.display = 'inline-block';
}
return;
}
const candidates = [ const candidates = [
'../logo.svg', '../logo.png', '../logo.jpg', '../logo.jpeg', '../logo.webp', '../logo.gif', '../logo.svg', '../logo.png', '../logo.jpg', '../logo.jpeg', '../logo.webp', '../logo.gif',
'logo.svg', 'logo.png', 'logo.jpg', 'logo.jpeg', 'logo.webp', 'logo.gif', 'logo.svg', 'logo.png', 'logo.jpg', 'logo.jpeg', 'logo.webp', 'logo.gif',

132
build.js Normal file
View File

@@ -0,0 +1,132 @@
'use strict';
const fs = require('fs');
const path = require('path');
const projectRoot = __dirname;
const distDir = path.join(projectRoot, 'dist');
const sourceFiles = {
html: path.join(projectRoot, 'index.html'),
css: path.join(projectRoot, 'styles.css'),
i18n: path.join(projectRoot, 'i18n.js'),
app: path.join(projectRoot, 'app.js')
};
const logoCandidates = ['logo.png', 'logo.jpg', 'logo.jpeg', 'logo.svg', 'logo.webp', 'logo.gif'];
const logoMimeMap = {
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.svg': 'image/svg+xml',
'.webp': 'image/webp',
'.gif': 'image/gif'
};
function readFile(filePath) {
try {
return fs.readFileSync(filePath, 'utf8');
} catch (err) {
console.error(`读取文件失败: ${filePath}`);
throw err;
}
}
function readBinary(filePath) {
try {
return fs.readFileSync(filePath);
} catch (err) {
console.error(`读取文件失败: ${filePath}`);
throw err;
}
}
function escapeForScript(content) {
return content.replace(/<\/(script)/gi, '<\\/$1');
}
function escapeForStyle(content) {
return content.replace(/<\/(style)/gi, '<\\/$1');
}
function ensureDistDir() {
if (fs.existsSync(distDir)) {
fs.rmSync(distDir, { recursive: true, force: true });
}
fs.mkdirSync(distDir);
}
function loadLogoDataUrl() {
for (const candidate of logoCandidates) {
const filePath = path.join(projectRoot, candidate);
if (!fs.existsSync(filePath)) continue;
const ext = path.extname(candidate).toLowerCase();
const mime = logoMimeMap[ext];
if (!mime) {
console.warn(`未知 Logo 文件类型,跳过内联: ${candidate}`);
continue;
}
const buffer = readBinary(filePath);
const base64 = buffer.toString('base64');
return `data:${mime};base64,${base64}`;
}
return null;
}
function build() {
ensureDistDir();
let html = readFile(sourceFiles.html);
const css = escapeForStyle(readFile(sourceFiles.css));
const i18n = escapeForScript(readFile(sourceFiles.i18n));
const app = escapeForScript(readFile(sourceFiles.app));
html = html.replace(
'<link rel="stylesheet" href="styles.css">',
`<style>
${css}
</style>`
);
html = html.replace(
'<script src="i18n.js"></script>',
`<script>
${i18n}
</script>`
);
html = html.replace(
'<script src="app.js"></script>',
`<script>
${app}
</script>`
);
const logoDataUrl = loadLogoDataUrl();
if (logoDataUrl) {
const logoScript = `<script>window.__INLINE_LOGO__ = "${logoDataUrl}";</script>`;
if (html.includes('</body>')) {
html = html.replace('</body>', `${logoScript}\n</body>`);
} else {
html += `\n${logoScript}`;
}
} else {
console.warn('未找到可内联的 Logo 文件,将保持运行时加载。');
}
const outputPath = path.join(distDir, 'index.html');
fs.writeFileSync(outputPath, html, 'utf8');
console.log('构建完成: dist/index.html');
}
try {
build();
} catch (error) {
console.error('构建失败:', error);
process.exit(1);
}

46
i18n.js
View File

@@ -47,19 +47,13 @@ const i18n = {
// 登录页面 // 登录页面
'login.subtitle': '请输入连接信息以访问管理界面', 'login.subtitle': '请输入连接信息以访问管理界面',
'login.tab_local_title': 'Local', 'login.connection_title': '连接地址',
'login.tab_local_subtitle': '在本地运行 Cli Web 服务器', 'login.connection_current': '当前地址',
'login.tab_remote_title': 'Remote', 'login.connection_auto_hint': '系统将自动使用当前访问地址进行连接',
'login.tab_remote_subtitle': '远程连接到 Cli Web 服务器', 'login.custom_connection_label': '自定义连接地址:',
'login.proxy_label': '代理服务器 (可选):', 'login.custom_connection_placeholder': '例如: https://example.com:8317',
'login.proxy_placeholder': 'http://ip:port 或 https://ip:port 或 socks5://user:pass@ip:port', 'login.custom_connection_hint': '默认使用当前访问地址,若需要可手动输入其他地址。',
'login.local_port_label': '端口号:', 'login.use_current_address': '使用当前地址',
'login.local_port_placeholder': '8317',
'login.local_url_hint': '将连接到 http://localhost:端口/v0/management',
'login.api_url_label': 'API 基础地址:',
'login.api_url_placeholder': '例如: http://localhost:8317 或 127.0.0.1:8317',
'login.remote_api_url_placeholder': '例如: https://example.com:8317',
'login.api_url_hint': '将自动补全 /v0/management',
'login.management_key_label': '管理密钥:', 'login.management_key_label': '管理密钥:',
'login.management_key_placeholder': '请输入管理密钥', 'login.management_key_placeholder': '请输入管理密钥',
'login.connect_button': '连接', 'login.connect_button': '连接',
@@ -211,6 +205,8 @@ const i18n = {
'auth_login.secure_1psid_placeholder': '输入 __Secure-1PSID cookie 值', 'auth_login.secure_1psid_placeholder': '输入 __Secure-1PSID cookie 值',
'auth_login.secure_1psidts_label': '__Secure-1PSIDTS Cookie:', 'auth_login.secure_1psidts_label': '__Secure-1PSIDTS Cookie:',
'auth_login.secure_1psidts_placeholder': '输入 __Secure-1PSIDTS cookie 值', 'auth_login.secure_1psidts_placeholder': '输入 __Secure-1PSIDTS cookie 值',
'auth_login.gemini_web_label_label': '标签 (可选):',
'auth_login.gemini_web_label_placeholder': '输入标签名称 (可选)',
'auth_login.gemini_web_saved': 'Gemini Web Token 保存成功', 'auth_login.gemini_web_saved': 'Gemini Web Token 保存成功',
// 使用统计 // 使用统计
@@ -342,21 +338,15 @@ const i18n = {
// Login page // Login page
'login.subtitle': 'Please enter connection information to access the management interface', 'login.subtitle': 'Please enter connection information to access the management interface',
'login.tab_local_title': 'Local', 'login.connection_title': 'Connection Address',
'login.tab_local_subtitle': 'Run Cli Web server on your local machine', 'login.connection_current': 'Current URL',
'login.tab_remote_title': 'Remote', 'login.connection_auto_hint': 'The system will automatically use the current URL for connection',
'login.tab_remote_subtitle': 'Remote connection for a remote Cli Web server', 'login.custom_connection_label': 'Custom Connection URL:',
'login.proxy_label': 'Proxy Server (Optional):', 'login.custom_connection_placeholder': 'Eg: https://example.com:8317',
'login.proxy_placeholder': 'http://ip:port or https://ip:port or socks5://user:pass@ip:port', 'login.custom_connection_hint': 'By default the current URL is used. Override it here if needed.',
'login.local_port_label': 'Port:', 'login.use_current_address': 'Use Current URL',
'login.local_port_placeholder': '8317',
'login.local_url_hint': 'Will connect to http://localhost:port/v0/management',
'login.api_url_label': 'API Base URL:',
'login.api_url_placeholder': 'e.g.: http://localhost:8317 or 127.0.0.1:8317',
'login.remote_api_url_placeholder': 'e.g.: https://example.com:8317',
'login.api_url_hint': 'Will automatically append /v0/management',
'login.management_key_label': 'Management Key:', 'login.management_key_label': 'Management Key:',
'login.management_key_placeholder': 'Please enter management key', 'login.management_key_placeholder': 'Enter the management key',
'login.connect_button': 'Connect', 'login.connect_button': 'Connect',
'login.submit_button': 'Login', 'login.submit_button': 'Login',
'login.submitting': 'Connecting...', 'login.submitting': 'Connecting...',
@@ -506,6 +496,8 @@ const i18n = {
'auth_login.secure_1psid_placeholder': 'Enter __Secure-1PSID cookie value', 'auth_login.secure_1psid_placeholder': 'Enter __Secure-1PSID cookie value',
'auth_login.secure_1psidts_label': '__Secure-1PSIDTS Cookie:', 'auth_login.secure_1psidts_label': '__Secure-1PSIDTS Cookie:',
'auth_login.secure_1psidts_placeholder': 'Enter __Secure-1PSIDTS cookie value', 'auth_login.secure_1psidts_placeholder': 'Enter __Secure-1PSIDTS cookie value',
'auth_login.gemini_web_label_label': 'Label (Optional):',
'auth_login.gemini_web_label_placeholder': 'Enter label name (optional)',
'auth_login.gemini_web_saved': 'Gemini Web Token saved successfully', 'auth_login.gemini_web_saved': 'Gemini Web Token saved successfully',
// Usage Statistics // Usage Statistics

View File

@@ -49,67 +49,39 @@
</div> </div>
</div> </div>
<!-- 选项卡导航 --> <div class="login-body">
<div class="connection-tabs"> <div class="login-connection-info">
<button class="tab-button active" data-tab="local"> <div class="connection-summary">
<i class="fas fa-home"></i> <i class="fas fa-link"></i>
<div class="tab-content"> <div>
<span class="tab-title" data-i18n="login.tab_local_title">Local</span> <h3 data-i18n="login.connection_title">连接地址</h3>
<span class="tab-subtitle" data-i18n="login.tab_local_subtitle">Run CLI Web server on your local machine</span> <p class="connection-url">
<span data-i18n="login.connection_current">当前地址</span>
<span class="connection-url-separator">:</span>
<span id="login-connection-url">-</span>
</p>
</div>
</div> </div>
</button> <p class="form-hint" data-i18n="login.connection_auto_hint">系统将自动使用当前访问地址进行连接</p>
<button class="tab-button" data-tab="remote"> </div>
<i class="fas fa-cloud"></i>
<div class="tab-content">
<span class="tab-title" data-i18n="login.tab_remote_title">Remote</span>
<span class="tab-subtitle" data-i18n="login.tab_remote_subtitle">Remote connection for a remote CLI Web server</span>
</div>
</button>
</div>
<!-- 代理服务器设置(可选) -->
<div class="proxy-settings">
<label data-i18n="login.proxy_label">Proxy Server (Optional):</label>
<input type="text" id="login-proxy-url" data-i18n="login.proxy_placeholder" placeholder="http://ip:port or https://ip:port or socks5://user:pass@ip:port">
</div>
<!-- 本地连接表单 -->
<div id="local-form" class="connection-form active">
<form class="login-form"> <form class="login-form">
<div class="form-group"> <div class="form-group">
<label for="local-port" data-i18n="login.local_port_label">端口号:</label> <label for="login-api-base" data-i18n="login.custom_connection_label">自定义连接地址:</label>
<div class="local-url-group">
<span class="url-prefix">http://localhost:</span>
<input type="number" id="local-port" value="8317" min="1" max="65535" data-i18n="login.local_port_placeholder" placeholder="8317" required>
</div>
<div class="form-hint" data-i18n="login.local_url_hint">将连接到 http://localhost:端口/v0/management</div>
</div>
<div class="form-group">
<label for="local-management-key" data-i18n="login.management_key_label">管理密钥:</label>
<div class="input-group"> <div class="input-group">
<input type="password" id="local-management-key" data-i18n="login.management_key_placeholder" placeholder="请输入管理密钥" required> <input type="text" id="login-api-base" data-i18n="login.custom_connection_placeholder" placeholder="例如: https://example.com:8317">
<button type="button" class="btn btn-secondary toggle-key-visibility"> <button type="button" id="login-reset-api-base" class="btn btn-secondary connection-reset-btn">
<i class="fas fa-eye"></i> <i class="fas fa-location-arrow"></i>
<span data-i18n="login.use_current_address">使用当前地址</span>
</button> </button>
</div> </div>
<p class="form-hint" data-i18n="login.custom_connection_hint">默认使用当前访问地址,若需要可手动输入其他地址。</p>
</div> </div>
</form>
</div>
<!-- 远程连接表单 -->
<div id="remote-form" class="connection-form">
<form class="login-form">
<div class="form-group"> <div class="form-group">
<label for="remote-api-url" data-i18n="login.api_url_label">API 基础地址:</label> <label for="login-management-key" data-i18n="login.management_key_label">管理密钥:</label>
<input type="text" id="remote-api-url" data-i18n="login.remote_api_url_placeholder" placeholder="例如: https://example.com:8317" required>
<div class="form-hint" data-i18n="login.api_url_hint">将自动补全 /v0/management</div>
</div>
<div class="form-group">
<label for="remote-management-key" data-i18n="login.management_key_label">管理密钥:</label>
<div class="input-group"> <div class="input-group">
<input type="password" id="remote-management-key" data-i18n="login.management_key_placeholder" placeholder="请输入管理密钥" required> <input type="password" id="login-management-key" data-i18n="login.management_key_placeholder" placeholder="请输入管理密钥" required>
<button type="button" class="btn btn-secondary toggle-key-visibility"> <button type="button" class="btn btn-secondary toggle-key-visibility">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
</button> </button>
@@ -383,8 +355,20 @@
<div id="openai-providers-list" class="provider-list"></div> <div id="openai-providers-list" class="provider-list"></div>
</div> </div>
</div> </div>
</section>
<!-- 认证文件管理 -->
<section id="auth-files" class="content-section">
<h2 data-i18n="auth_files.title">认证文件管理</h2>
<div class="card" style="margin-bottom: 20px;">
<div class="card-content">
<p class="form-hint" data-i18n="auth_files.description">
这里管理 Qwen 和 Gemini 的认证配置文件。上传 JSON 格式的认证文件以启用相应的 AI 服务。
</p>
</div>
</div>
<!-- Gemini Web Token --> <!-- Gemini Web Token -->
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
@@ -405,19 +389,10 @@
<label for="secure-1psidts-input" data-i18n="auth_login.secure_1psidts_label">__Secure-1PSIDTS Cookie:</label> <label for="secure-1psidts-input" data-i18n="auth_login.secure_1psidts_label">__Secure-1PSIDTS Cookie:</label>
<input type="text" id="secure-1psidts-input" data-i18n="auth_login.secure_1psidts_placeholder" placeholder="输入 __Secure-1PSIDTS cookie 值"> <input type="text" id="secure-1psidts-input" data-i18n="auth_login.secure_1psidts_placeholder" placeholder="输入 __Secure-1PSIDTS cookie 值">
</div> </div>
</div> <div class="form-group">
</div> <label for="gemini-web-label-input" data-i18n="auth_login.gemini_web_label_label">Label (Optional):</label>
</section> <input type="text" id="gemini-web-label-input" data-i18n="auth_login.gemini_web_label_placeholder" placeholder="输入标签名称 (可选)">
</div>
<!-- 认证文件管理 -->
<section id="auth-files" class="content-section">
<h2 data-i18n="auth_files.title">认证文件管理</h2>
<div class="card" style="margin-bottom: 20px;">
<div class="card-content">
<p class="form-hint" data-i18n="auth_files.description">
这里管理 Qwen 和 Gemini 的认证配置文件。上传 JSON 格式的认证文件以启用相应的 AI 服务。
</p>
</div> </div>
</div> </div>
@@ -579,7 +554,7 @@
<!-- 版本信息 --> <!-- 版本信息 -->
<footer class="version-footer"> <footer class="version-footer">
<div class="version-info"> <div class="version-info">
<span data-i18n="footer.version">版本</span>: v0.0.3 <span data-i18n="footer.version">版本</span>: v0.0.5
<span class="separator"></span> <span class="separator"></span>
<span data-i18n="footer.author">作者</span>: Supra4E8C <span data-i18n="footer.author">作者</span>: Supra4E8C
</div> </div>

2074
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,9 +6,7 @@
"scripts": { "scripts": {
"start": "npx serve .", "start": "npx serve .",
"dev": "npx serve . --port 3000", "dev": "npx serve . --port 3000",
"prebuild": "node build-scripts/prepare-html.js", "build": "node build.js",
"build": "webpack --config webpack.config.js",
"postbuild": "rm -f index.build.html",
"lint": "echo '使用浏览器开发者工具检查代码'" "lint": "echo '使用浏览器开发者工具检查代码'"
}, },
"keywords": [ "keywords": [
@@ -20,14 +18,7 @@
"author": "CLI Proxy API WebUI", "author": "CLI Proxy API WebUI",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"css-loader": "^6.8.1", "serve": "^14.2.1"
"html-inline-script-webpack-plugin": "^3.2.1",
"html-loader": "^4.2.0",
"html-webpack-plugin": "^5.5.4",
"serve": "^14.2.1",
"style-loader": "^3.3.3",
"webpack": "^5.102.0",
"webpack-cli": "^5.1.4"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
@@ -35,5 +26,6 @@
"repository": { "repository": {
"type": "git", "type": "git",
"url": "local" "url": "local"
} },
"dependencies": {}
} }

View File

@@ -296,155 +296,6 @@
margin-bottom: 25px; margin-bottom: 25px;
} }
/* 选项卡导航样式 */
.connection-tabs {
display: flex;
margin-bottom: 25px;
margin-top: 10px;
border-radius: 12px;
background: var(--bg-tertiary);
padding: 4px;
border: 1px solid var(--border-primary);
}
.tab-button {
flex: 1;
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
border: none;
background: transparent;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
text-align: left;
color: var(--text-tertiary);
}
.tab-button:hover {
background: var(--accent-tertiary);
}
.tab-button.active {
background: var(--accent-primary);
color: var(--text-inverse);
box-shadow: var(--shadow-primary);
}
.tab-button i {
font-size: 20px;
min-width: 20px;
}
.tab-content {
display: flex;
flex-direction: column;
gap: 2px;
}
.tab-title {
font-weight: 600;
font-size: 16px;
}
.tab-subtitle {
font-size: 12px;
opacity: 0.8;
line-height: 1.3;
}
/* 代理设置样式 */
.proxy-settings {
margin-bottom: 25px;
padding: 16px;
background: var(--bg-tertiary);
border: 1px solid var(--border-primary);
border-radius: 10px;
}
.proxy-settings label {
display: block;
margin-bottom: 8px;
color: var(--text-secondary);
font-weight: 600;
font-size: 14px;
}
.proxy-settings input {
width: 100%;
padding: 12px 16px;
border: 2px solid var(--border-primary);
border-radius: 8px;
font-size: 14px;
transition: all 0.3s ease;
background: var(--bg-secondary);
color: var(--text-primary);
}
.proxy-settings input:focus {
outline: none;
border-color: var(--border-focus);
box-shadow: 0 0 0 3px var(--border-primary);
}
/* 连接表单样式 */
.connection-form {
display: none;
}
.connection-form.active {
display: block;
}
/* 本地URL组合输入框样式 */
.local-url-group {
display: flex;
align-items: center;
border: 2px solid var(--border-primary);
border-radius: 8px;
background: var(--bg-secondary);
transition: all 0.3s ease;
overflow: hidden;
}
.local-url-group:focus-within {
border-color: var(--border-focus);
box-shadow: 0 0 0 3px var(--border-primary);
}
.url-prefix {
padding: 12px 16px;
background: var(--bg-tertiary);
color: var(--text-tertiary);
font-weight: 500;
font-size: 14px;
border-right: 1px solid var(--border-primary);
white-space: nowrap;
}
.local-url-group input {
flex: 1;
padding: 12px 16px;
border: none;
font-size: 14px;
background: transparent;
color: var(--text-primary);
outline: none;
min-width: 80px;
}
.local-url-group input::-webkit-outer-spin-button,
.local-url-group input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.local-url-group input[type="number"] {
-moz-appearance: textfield;
appearance: textfield;
}
.login-title { .login-title {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -475,8 +326,63 @@
text-align: center; text-align: center;
} }
.login-body {
display: flex;
flex-direction: column;
gap: 24px;
}
.login-connection-info {
background: var(--bg-tertiary);
border: 1px solid var(--border-primary);
border-radius: 16px;
padding: 20px;
display: flex;
flex-direction: column;
gap: 12px;
}
[data-theme="dark"] .login-connection-info {
background: rgba(30, 41, 59, 0.7);
border-color: rgba(100, 116, 139, 0.3);
}
.connection-summary {
display: flex;
align-items: center;
gap: 16px;
color: var(--text-secondary);
}
.connection-summary i {
font-size: 24px;
color: var(--primary-color);
}
.connection-url {
font-size: 16px;
color: var(--text-secondary);
}
.connection-url-separator {
margin: 0 8px;
color: var(--text-tertiary);
}
#login-connection-url {
font-family: "Fira Code", "Consolas", "Courier New", monospace;
color: var(--text-primary);
word-break: break-all;
}
[data-theme="dark"] #login-connection-url {
color: var(--text-secondary);
}
.login-form { .login-form {
width: 100%; display: flex;
flex-direction: column;
gap: 20px;
} }
.login-form .form-group { .login-form .form-group {
@@ -644,41 +550,6 @@
height: 50px; height: 50px;
} }
.connection-tabs {
flex-direction: column;
gap: 4px;
}
.tab-button {
flex-direction: row;
gap: 12px;
padding: 14px;
}
.tab-subtitle {
display: none;
}
.proxy-settings {
padding: 12px;
}
.local-url-group {
flex-direction: column;
}
.url-prefix {
border-right: none;
border-bottom: 1px solid #e2e8f0;
border-radius: 8px 8px 0 0;
text-align: center;
}
.local-url-group input {
border-radius: 0 0 8px 8px;
text-align: center;
}
.login-form .input-group { .login-form .input-group {
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
@@ -1847,3 +1718,29 @@ input:checked + .slider:before {
color: var(--text-secondary); color: var(--text-secondary);
} }
.connection-reset-btn {
display: inline-flex;
align-items: center;
gap: 6px;
white-space: nowrap;
}
.connection-reset-btn i {
margin: 0;
}
.connection-reset-btn span {
font-size: 13px;
}
[data-theme="dark"] .connection-reset-btn {
background: rgba(30, 41, 59, 0.9);
border-color: rgba(100, 116, 139, 0.4);
color: #cbd5e1;
}
[data-theme="dark"] .connection-reset-btn:hover {
background: rgba(51, 65, 85, 0.95);
border-color: rgba(100, 116, 139, 0.6);
}