diff --git a/.gitignore b/.gitignore index 852f2a4..02fb6b0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules/ .env .DS_Store *.txt +server-key.json diff --git a/key-manager.js b/key-manager.js index ae552ab..9031527 100644 --- a/key-manager.js +++ b/key-manager.js @@ -10,7 +10,9 @@ class KeyManager { key: key, success: 0, fail: 0, - deprecated: false // 是否被废弃 + deprecated: false, // 是否被废弃 + depleted: false, // 是否余额用尽 + balance: null // 最近一次余额信息 { totalAllowance, used, remaining, fetchedAt } })); this.algorithm = algorithm; // 'weighted' or 'simple' @@ -18,6 +20,7 @@ class KeyManager { this.endpointStats = {}; // 端点统计 { endpoint: { success: 0, fail: 0 } } this.removeOn402 = removeOn402; // 是否在402时移除key this.deprecatedKeys = []; // 已废弃的key列表 + this.skipThreshold = 0; // 剩余≤阈值则视为用尽 logInfo(`KeyManager initialized with ${this.keys.length} keys, algorithm: ${this.algorithm}, removeOn402: ${this.removeOn402}`); } @@ -27,7 +30,7 @@ class KeyManager { */ selectKey() { // 获取未废弃的key - const activeKeys = this.keys.filter(k => !k.deprecated); + const activeKeys = this.keys.filter(k => !k.deprecated && !k.depleted); if (activeKeys.length === 0) { throw new Error('No active keys available - all keys have been deprecated'); @@ -126,6 +129,38 @@ class KeyManager { logDebug(`Endpoint stats updated: ${endpoint}, success=${this.endpointStats[endpoint].success}, fail=${this.endpointStats[endpoint].fail}`); } + /** + * 更新余额与用尽状态(按索引) + * @param {number} index + * @param {object} balanceInfo { totalAllowance, used, remaining, fetchedAt } + */ + setBalanceByIndex(index, balanceInfo) { + if (typeof index !== 'number' || index < 0 || index >= this.keys.length) return; + const k = this.keys[index]; + if (!k) return; + k.balance = balanceInfo || null; + if (balanceInfo && typeof balanceInfo.remaining === 'number') { + k.depleted = balanceInfo.remaining <= this.skipThreshold; + } + } + + /** + * 设置跳过阈值(剩余≤n 视为用尽)并重算状态 + */ + setSkipThreshold(n) { + const val = Number(n); + this.skipThreshold = Number.isFinite(val) && val >= 0 ? val : 0; + for (const k of this.keys) { + if (k.balance && typeof k.balance.remaining === 'number') { + k.depleted = k.balance.remaining <= this.skipThreshold; + } + } + } + + getSkipThreshold() { + return this.skipThreshold; + } + /** * 废弃一个key * @param {string} key - 要废弃的key @@ -167,9 +202,10 @@ class KeyManager { * 获取统计信息 */ getStats() { - // 分离活跃的key和废弃的key - const activeKeys = this.keys.filter(k => !k.deprecated); - const deprecatedKeys = this.keys.filter(k => k.deprecated); + // 分离活跃的key和废弃的key(保留原始索引以便在UI中定位) + const indexedKeys = this.keys.map((k, idx) => ({ ...k, __index: idx })); + const activeKeys = indexedKeys.filter(k => !k.deprecated); + const deprecatedKeys = indexedKeys.filter(k => k.deprecated); return { algorithm: this.algorithm, @@ -181,7 +217,9 @@ class KeyManager { total: keyObj.success + keyObj.fail, successRate: keyObj.success + keyObj.fail > 0 ? ((keyObj.success / (keyObj.success + keyObj.fail)) * 100).toFixed(2) + '%' - : 'N/A' + : 'N/A', + index: keyObj.__index, + depleted: !!keyObj.depleted })), deprecatedKeys: deprecatedKeys.map(keyObj => ({ key: this.maskKey(keyObj.key), @@ -191,7 +229,8 @@ class KeyManager { successRate: keyObj.success + keyObj.fail > 0 ? ((keyObj.success / (keyObj.success + keyObj.fail)) * 100).toFixed(2) + '%' : 'N/A', - deprecatedAt: this.deprecatedKeys.find(dk => dk.key === keyObj.key)?.deprecatedAt || 'Unknown' + deprecatedAt: this.deprecatedKeys.find(dk => dk.key === keyObj.key)?.deprecatedAt || 'Unknown', + index: keyObj.__index })), endpoints: Object.entries(this.endpointStats) .filter(([_, stats]) => stats.success > 0 || stats.fail > 0) diff --git a/routes.js b/routes.js index 0818b6b..2fab60e 100644 --- a/routes.js +++ b/routes.js @@ -9,6 +9,7 @@ import { AnthropicResponseTransformer } from './transformers/response-anthropic. import { OpenAIResponseTransformer } from './transformers/response-openai.js'; import { getApiKey, recordRequestResult } from './auth.js'; import { getKeyManager } from './key-manager.js'; +import { isServerKeySet, setServerKey } from './server-auth.js'; const router = express.Router(); @@ -103,9 +104,12 @@ async function handleChatCompletions(req, res) { logInfo(`Routing to ${model.type} endpoint: ${endpoint.base_url}`); // Get API key (will auto-refresh if needed) + // When server access key is enabled, Authorization header is reserved for server auth. + // To pass a client-provided upstream token (client mode), use X-Endpoint-Authorization. let authHeader; try { - authHeader = await getApiKey(req.headers.authorization); + const clientUpstreamAuth = req.headers['x-endpoint-authorization'] || null; + authHeader = await getApiKey(clientUpstreamAuth); } catch (error) { logError('Failed to get API key', error); return res.status(500).json({ @@ -268,7 +272,8 @@ async function handleDirectResponses(req, res) { const clientAuthFromXApiKey = req.headers['x-api-key'] ? `Bearer ${req.headers['x-api-key']}` : null; - authHeader = await getApiKey(req.headers.authorization || clientAuthFromXApiKey); + const clientUpstreamAuth = req.headers['x-endpoint-authorization'] || null; + authHeader = await getApiKey(clientUpstreamAuth || clientAuthFromXApiKey); } catch (error) { logError('Failed to get API key', error); return res.status(500).json({ @@ -407,7 +412,8 @@ async function handleDirectMessages(req, res) { const clientAuthFromXApiKey = req.headers['x-api-key'] ? `Bearer ${req.headers['x-api-key']}` : null; - authHeader = await getApiKey(req.headers.authorization || clientAuthFromXApiKey); + const clientUpstreamAuth = req.headers['x-endpoint-authorization'] || null; + authHeader = await getApiKey(clientUpstreamAuth || clientAuthFromXApiKey); } catch (error) { logError('Failed to get API key', error); return res.status(500).json({ @@ -523,6 +529,45 @@ router.get('/status', (req, res) => { logInfo('GET /status'); try { + // First-visit setup: if no server key yet, present setup form + if (!isServerKeySet()) { + return res.send(` + + +
+ +First-time setup: Set a server access key. After saving, all endpoints require this key via header Authorization: Bearer <key>. The /status page remains open.
Use header Authorization: Bearer <key> for all API requests (except /status).