diff --git a/.gitignore b/.gitignore index 327f719..e05a051 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ input_system_prompt.txt token-store.json usage-cache.json *_oauth_creds.json -*-auth-token.json \ No newline at end of file +*-auth-token.json +api-potluck-keys.json \ No newline at end of file diff --git a/src/api-potluck/api-routes.js b/src/api-potluck/api-routes.js new file mode 100644 index 0000000..3819aaa --- /dev/null +++ b/src/api-potluck/api-routes.js @@ -0,0 +1,277 @@ +/** + * API 大锅饭 - 管理 API 路由 + * 提供 Key 管理的 RESTful API + */ + +import { + createKey, + listKeys, + getKey, + deleteKey, + updateKeyLimit, + resetKeyUsage, + toggleKey, + updateKeyName, + getStats +} from './key-manager.js'; + +/** + * 解析请求体 + * @param {http.IncomingMessage} req + * @returns {Promise} + */ +function parseRequestBody(req) { + return new Promise((resolve, reject) => { + let body = ''; + req.on('data', chunk => { + body += chunk.toString(); + }); + req.on('end', () => { + try { + resolve(body ? JSON.parse(body) : {}); + } catch (error) { + reject(new Error('Invalid JSON format')); + } + }); + req.on('error', reject); + }); +} + +/** + * 发送 JSON 响应 + * @param {http.ServerResponse} res + * @param {number} statusCode + * @param {Object} data + */ +function sendJson(res, statusCode, data) { + res.writeHead(statusCode, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(data)); +} + +/** + * 验证管理员 Token + * @param {http.IncomingMessage} req + * @returns {Promise} + */ +async function checkAdminAuth(req) { + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return false; + } + + // 动态导入 ui-manager 中的 token 验证逻辑 + try { + const { existsSync, readFileSync } = await import('fs'); + const { promises: fs } = await import('fs'); + const path = await import('path'); + + const TOKEN_STORE_FILE = path.join(process.cwd(), 'configs', 'token-store.json'); + + if (!existsSync(TOKEN_STORE_FILE)) { + return false; + } + + const content = readFileSync(TOKEN_STORE_FILE, 'utf8'); + const tokenStore = JSON.parse(content); + const token = authHeader.substring(7); + const tokenInfo = tokenStore.tokens[token]; + + if (!tokenInfo) { + return false; + } + + // 检查是否过期 + if (Date.now() > tokenInfo.expiryTime) { + return false; + } + + return true; + } catch (error) { + console.error('[API Potluck] Auth check error:', error.message); + return false; + } +} + +/** + * 处理 Potluck 管理 API 请求 + * @param {string} method - HTTP 方法 + * @param {string} path - 请求路径 + * @param {http.IncomingMessage} req - HTTP 请求对象 + * @param {http.ServerResponse} res - HTTP 响应对象 + * @returns {Promise} - 是否处理了请求 + */ +export async function handlePotluckApiRoutes(method, path, req, res) { + // 只处理 /api/potluck 开头的请求 + if (!path.startsWith('/api/potluck')) { + return false; + } + + // 验证管理员权限 + const isAuthed = await checkAdminAuth(req); + if (!isAuthed) { + sendJson(res, 401, { + success: false, + error: { message: 'Unauthorized: Please login first', code: 'UNAUTHORIZED' } + }); + return true; + } + + try { + // GET /api/potluck/stats - 获取统计信息 + if (method === 'GET' && path === '/api/potluck/stats') { + const stats = await getStats(); + sendJson(res, 200, { success: true, data: stats }); + return true; + } + + // GET /api/potluck/keys - 获取所有 Key 列表 + if (method === 'GET' && path === '/api/potluck/keys') { + const keys = await listKeys(); + const stats = await getStats(); + sendJson(res, 200, { + success: true, + data: { + keys, + stats + } + }); + return true; + } + + // POST /api/potluck/keys - 创建新 Key + if (method === 'POST' && path === '/api/potluck/keys') { + const body = await parseRequestBody(req); + const { name, dailyLimit } = body; + const keyData = await createKey(name, dailyLimit); + sendJson(res, 201, { + success: true, + message: 'API Key created successfully', + data: keyData + }); + return true; + } + + // 处理带 keyId 的路由 + const keyIdMatch = path.match(/^\/api\/potluck\/keys\/([^\/]+)(\/.*)?$/); + if (keyIdMatch) { + const keyId = decodeURIComponent(keyIdMatch[1]); + const subPath = keyIdMatch[2] || ''; + + // GET /api/potluck/keys/:keyId - 获取单个 Key 详情 + if (method === 'GET' && !subPath) { + const keyData = await getKey(keyId); + if (!keyData) { + sendJson(res, 404, { success: false, error: { message: 'Key not found' } }); + return true; + } + sendJson(res, 200, { success: true, data: keyData }); + return true; + } + + // DELETE /api/potluck/keys/:keyId - 删除 Key + if (method === 'DELETE' && !subPath) { + const deleted = await deleteKey(keyId); + if (!deleted) { + sendJson(res, 404, { success: false, error: { message: 'Key not found' } }); + return true; + } + sendJson(res, 200, { success: true, message: 'Key deleted successfully' }); + return true; + } + + // PUT /api/potluck/keys/:keyId/limit - 更新每日限额 + if (method === 'PUT' && subPath === '/limit') { + const body = await parseRequestBody(req); + const { dailyLimit } = body; + + if (typeof dailyLimit !== 'number' || dailyLimit < 0) { + sendJson(res, 400, { + success: false, + error: { message: 'Invalid dailyLimit value' } + }); + return true; + } + + const keyData = await updateKeyLimit(keyId, dailyLimit); + if (!keyData) { + sendJson(res, 404, { success: false, error: { message: 'Key not found' } }); + return true; + } + sendJson(res, 200, { + success: true, + message: 'Daily limit updated successfully', + data: keyData + }); + return true; + } + + // POST /api/potluck/keys/:keyId/reset - 重置当天调用次数 + if (method === 'POST' && subPath === '/reset') { + const keyData = await resetKeyUsage(keyId); + if (!keyData) { + sendJson(res, 404, { success: false, error: { message: 'Key not found' } }); + return true; + } + sendJson(res, 200, { + success: true, + message: 'Usage reset successfully', + data: keyData + }); + return true; + } + + // POST /api/potluck/keys/:keyId/toggle - 切换启用/禁用状态 + if (method === 'POST' && subPath === '/toggle') { + const keyData = await toggleKey(keyId); + if (!keyData) { + sendJson(res, 404, { success: false, error: { message: 'Key not found' } }); + return true; + } + sendJson(res, 200, { + success: true, + message: `Key ${keyData.enabled ? 'enabled' : 'disabled'} successfully`, + data: keyData + }); + return true; + } + + // PUT /api/potluck/keys/:keyId/name - 更新 Key 名称 + if (method === 'PUT' && subPath === '/name') { + const body = await parseRequestBody(req); + const { name } = body; + + if (!name || typeof name !== 'string') { + sendJson(res, 400, { + success: false, + error: { message: 'Invalid name value' } + }); + return true; + } + + const keyData = await updateKeyName(keyId, name); + if (!keyData) { + sendJson(res, 404, { success: false, error: { message: 'Key not found' } }); + return true; + } + sendJson(res, 200, { + success: true, + message: 'Name updated successfully', + data: keyData + }); + return true; + } + } + + // 未匹配的 potluck 路由 + sendJson(res, 404, { success: false, error: { message: 'Potluck API endpoint not found' } }); + return true; + + } catch (error) { + console.error('[API Potluck] API error:', error); + sendJson(res, 500, { + success: false, + error: { message: error.message || 'Internal server error' } + }); + return true; + } +} diff --git a/src/api-potluck/index.js b/src/api-potluck/index.js new file mode 100644 index 0000000..4530c20 --- /dev/null +++ b/src/api-potluck/index.js @@ -0,0 +1,33 @@ +/** + * API 大锅饭 - 模块入口 + * 导出所有功能供外部使用 + */ + +// Key 管理 +export { + createKey, + listKeys, + getKey, + deleteKey, + updateKeyLimit, + resetKeyUsage, + toggleKey, + updateKeyName, + validateKey, + incrementUsage, + getStats, + KEY_PREFIX, + DEFAULT_DAILY_LIMIT +} from './key-manager.js'; + +// 中间件 +export { + extractPotluckKey, + isPotluckRequest, + potluckAuthMiddleware, + recordPotluckUsage, + sendPotluckError +} from './middleware.js'; + +// API 路由 +export { handlePotluckApiRoutes } from './api-routes.js'; diff --git a/src/api-potluck/key-manager.js b/src/api-potluck/key-manager.js new file mode 100644 index 0000000..c122f39 --- /dev/null +++ b/src/api-potluck/key-manager.js @@ -0,0 +1,297 @@ +/** + * API 大锅饭 - Key 管理模块 + * 使用内存缓存 + 写锁 + 定期持久化,解决并发安全问题 + */ + +import { promises as fs } from 'fs'; +import { existsSync, readFileSync, writeFileSync } from 'fs'; +import path from 'path'; +import crypto from 'crypto'; + +// 配置 +const KEYS_STORE_FILE = path.join(process.cwd(), 'configs', 'api-potluck-keys.json'); +const KEY_PREFIX = 'maki_'; +const DEFAULT_DAILY_LIMIT = 1000; +const PERSIST_INTERVAL = 5000; // 5秒持久化一次 + +// 内存缓存 +let keyStore = null; +let isDirty = false; +let isWriting = false; +let persistTimer = null; + +/** + * 初始化:从文件加载数据到内存 + */ +function ensureLoaded() { + if (keyStore !== null) return; + try { + if (existsSync(KEYS_STORE_FILE)) { + const content = readFileSync(KEYS_STORE_FILE, 'utf8'); + keyStore = JSON.parse(content); + } else { + keyStore = { keys: {} }; + syncWriteToFile(); + } + } catch (error) { + console.error('[API Potluck] Failed to load key store:', error.message); + keyStore = { keys: {} }; + } + // 启动定期持久化 + if (!persistTimer) { + persistTimer = setInterval(persistIfDirty, PERSIST_INTERVAL); + // 进程退出时保存 + process.on('beforeExit', () => persistIfDirty()); + process.on('SIGINT', () => { persistIfDirty(); process.exit(0); }); + process.on('SIGTERM', () => { persistIfDirty(); process.exit(0); }); + } +} + +/** + * 同步写入文件(仅初始化时使用) + */ +function syncWriteToFile() { + try { + const dir = path.dirname(KEYS_STORE_FILE); + if (!existsSync(dir)) { + require('fs').mkdirSync(dir, { recursive: true }); + } + writeFileSync(KEYS_STORE_FILE, JSON.stringify(keyStore, null, 2), 'utf8'); + } catch (error) { + console.error('[API Potluck] Sync write failed:', error.message); + } +} + +/** + * 异步持久化(带写锁) + */ +async function persistIfDirty() { + if (!isDirty || isWriting || keyStore === null) return; + isWriting = true; + try { + const dir = path.dirname(KEYS_STORE_FILE); + if (!existsSync(dir)) { + await fs.mkdir(dir, { recursive: true }); + } + // 写入临时文件再重命名,防止写入中断导致文件损坏 + const tempFile = KEYS_STORE_FILE + '.tmp'; + await fs.writeFile(tempFile, JSON.stringify(keyStore, null, 2), 'utf8'); + await fs.rename(tempFile, KEYS_STORE_FILE); + isDirty = false; + } catch (error) { + console.error('[API Potluck] Persist failed:', error.message); + } finally { + isWriting = false; + } +} + +/** + * 标记数据已修改 + */ +function markDirty() { + isDirty = true; +} + +/** + * 生成随机 API Key + */ +function generateApiKey() { + return `${KEY_PREFIX}${crypto.randomBytes(16).toString('hex')}`; +} + +/** + * 获取今天的日期字符串 (YYYY-MM-DD) + */ +function getTodayDateString() { + const now = new Date(); + return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`; +} + +/** + * 检查并重置过期的每日计数 + */ +function checkAndResetDailyCount(keyData) { + const today = getTodayDateString(); + if (keyData.lastResetDate !== today) { + keyData.todayUsage = 0; + keyData.lastResetDate = today; + } + return keyData; +} + +/** + * 创建新的 API Key + */ +export async function createKey(name = '', dailyLimit = DEFAULT_DAILY_LIMIT) { + ensureLoaded(); + const apiKey = generateApiKey(); + const now = new Date().toISOString(); + const today = getTodayDateString(); + + const keyData = { + id: apiKey, + name: name || `Key-${Object.keys(keyStore.keys).length + 1}`, + createdAt: now, + dailyLimit, + todayUsage: 0, + totalUsage: 0, + lastResetDate: today, + lastUsedAt: null, + enabled: true + }; + + keyStore.keys[apiKey] = keyData; + markDirty(); + await persistIfDirty(); // 创建操作立即持久化 + + console.log(`[API Potluck] Created key: ${apiKey.substring(0, 12)}...`); + return keyData; +} + +/** + * 获取所有 Key 列表 + */ +export async function listKeys() { + ensureLoaded(); + const keys = []; + for (const [keyId, keyData] of Object.entries(keyStore.keys)) { + const updated = checkAndResetDailyCount({ ...keyData }); + keys.push({ + ...updated, + maskedKey: `${keyId.substring(0, 12)}...${keyId.substring(keyId.length - 4)}` + }); + } + return keys; +} + +/** + * 获取单个 Key 详情 + */ +export async function getKey(keyId) { + ensureLoaded(); + const keyData = keyStore.keys[keyId]; + if (!keyData) return null; + return checkAndResetDailyCount({ ...keyData }); +} + +/** + * 删除 Key + */ +export async function deleteKey(keyId) { + ensureLoaded(); + if (!keyStore.keys[keyId]) return false; + delete keyStore.keys[keyId]; + markDirty(); + await persistIfDirty(); // 删除操作立即持久化 + console.log(`[API Potluck] Deleted key: ${keyId.substring(0, 12)}...`); + return true; +} + +/** + * 更新 Key 的每日限额 + */ +export async function updateKeyLimit(keyId, newLimit) { + ensureLoaded(); + if (!keyStore.keys[keyId]) return null; + keyStore.keys[keyId].dailyLimit = newLimit; + markDirty(); + return keyStore.keys[keyId]; +} + +/** + * 重置 Key 的当天调用次数 + */ +export async function resetKeyUsage(keyId) { + ensureLoaded(); + if (!keyStore.keys[keyId]) return null; + keyStore.keys[keyId].todayUsage = 0; + keyStore.keys[keyId].lastResetDate = getTodayDateString(); + markDirty(); + return keyStore.keys[keyId]; +} + +/** + * 切换 Key 的启用/禁用状态 + */ +export async function toggleKey(keyId) { + ensureLoaded(); + if (!keyStore.keys[keyId]) return null; + keyStore.keys[keyId].enabled = !keyStore.keys[keyId].enabled; + markDirty(); + return keyStore.keys[keyId]; +} + +/** + * 更新 Key 名称 + */ +export async function updateKeyName(keyId, newName) { + ensureLoaded(); + if (!keyStore.keys[keyId]) return null; + keyStore.keys[keyId].name = newName; + markDirty(); + return keyStore.keys[keyId]; +} + +/** + * 验证 API Key 是否有效且有配额 + */ +export async function validateKey(apiKey) { + ensureLoaded(); + if (!apiKey || !apiKey.startsWith(KEY_PREFIX)) { + return { valid: false, reason: 'invalid_format' }; + } + const keyData = keyStore.keys[apiKey]; + if (!keyData) return { valid: false, reason: 'not_found' }; + if (!keyData.enabled) return { valid: false, reason: 'disabled' }; + + // 直接在内存中检查和重置 + checkAndResetDailyCount(keyData); + if (keyData.todayUsage >= keyData.dailyLimit) { + return { valid: false, reason: 'quota_exceeded', keyData }; + } + return { valid: true, keyData }; +} + +/** + * 增加 Key 的使用次数(原子操作,直接修改内存) + */ +export async function incrementUsage(apiKey) { + ensureLoaded(); + const keyData = keyStore.keys[apiKey]; + if (!keyData) return null; + + checkAndResetDailyCount(keyData); + keyData.todayUsage += 1; + keyData.totalUsage += 1; + keyData.lastUsedAt = new Date().toISOString(); + markDirty(); + // 不立即持久化,由定时器批量写入 + return keyData; +} + +/** + * 获取统计信息 + */ +export async function getStats() { + ensureLoaded(); + const keys = Object.values(keyStore.keys); + let enabledKeys = 0, todayTotalUsage = 0, totalUsage = 0; + + for (const key of keys) { + checkAndResetDailyCount(key); + if (key.enabled) enabledKeys++; + todayTotalUsage += key.todayUsage; + totalUsage += key.totalUsage; + } + + return { + totalKeys: keys.length, + enabledKeys, + disabledKeys: keys.length - enabledKeys, + todayTotalUsage, + totalUsage + }; +} + +// 导出常量 +export { KEY_PREFIX, DEFAULT_DAILY_LIMIT }; diff --git a/src/api-potluck/middleware.js b/src/api-potluck/middleware.js new file mode 100644 index 0000000..089b247 --- /dev/null +++ b/src/api-potluck/middleware.js @@ -0,0 +1,152 @@ +/** + * API 大锅饭 - 中间件模块 + * 负责请求拦截和配额检查 + */ + +import { validateKey, incrementUsage, KEY_PREFIX } from './key-manager.js'; + +/** + * 从请求中提取 Potluck API Key + * 支持多种认证方式: + * 1. Authorization: Bearer maki_xxx + * 2. x-api-key: maki_xxx + * 3. x-goog-api-key: maki_xxx + * 4. URL query: ?key=maki_xxx + * + * @param {http.IncomingMessage} req - HTTP 请求对象 + * @param {URL} requestUrl - 解析后的 URL 对象 + * @returns {string|null} 提取到的 API Key,如果不是 potluck key 则返回 null + */ +export function extractPotluckKey(req, requestUrl) { + // 1. 检查 Authorization header + const authHeader = req.headers['authorization']; + if (authHeader && authHeader.startsWith('Bearer ')) { + const token = authHeader.substring(7); + if (token.startsWith(KEY_PREFIX)) { + return token; + } + } + + // 2. 检查 x-api-key header (Claude style) + const xApiKey = req.headers['x-api-key']; + if (xApiKey && xApiKey.startsWith(KEY_PREFIX)) { + return xApiKey; + } + + // 3. 检查 x-goog-api-key header (Gemini style) + const googApiKey = req.headers['x-goog-api-key']; + if (googApiKey && googApiKey.startsWith(KEY_PREFIX)) { + return googApiKey; + } + + // 4. 检查 URL query parameter + const queryKey = requestUrl.searchParams.get('key'); + if (queryKey && queryKey.startsWith(KEY_PREFIX)) { + return queryKey; + } + + return null; +} + +/** + * 检查请求是否使用 Potluck Key + * @param {http.IncomingMessage} req - HTTP 请求对象 + * @param {URL} requestUrl - 解析后的 URL 对象 + * @returns {boolean} + */ +export function isPotluckRequest(req, requestUrl) { + return extractPotluckKey(req, requestUrl) !== null; +} + +/** + * Potluck 认证中间件 + * 验证 Potluck API Key 并检查配额 + * + * @param {http.IncomingMessage} req - HTTP 请求对象 + * @param {URL} requestUrl - 解析后的 URL 对象 + * @returns {Promise<{authorized: boolean, error?: Object, keyData?: Object, apiKey?: string}>} + */ +export async function potluckAuthMiddleware(req, requestUrl) { + const apiKey = extractPotluckKey(req, requestUrl); + + if (!apiKey) { + // 不是 potluck 请求,返回 null 让原有逻辑处理 + return { authorized: null }; + } + + // 验证 Key + const validation = await validateKey(apiKey); + + if (!validation.valid) { + const errorMessages = { + 'invalid_format': 'Invalid API key format', + 'not_found': 'API key not found', + 'disabled': 'API key has been disabled', + 'quota_exceeded': 'Daily quota exceeded for this API key' + }; + + const statusCodes = { + 'invalid_format': 401, + 'not_found': 401, + 'disabled': 403, + 'quota_exceeded': 429 + }; + + return { + authorized: false, + error: { + statusCode: statusCodes[validation.reason] || 401, + message: errorMessages[validation.reason] || 'Authentication failed', + code: validation.reason, + keyData: validation.keyData + } + }; + } + + return { + authorized: true, + keyData: validation.keyData, + apiKey: apiKey + }; +} + +/** + * 记录 Potluck 请求使用 + * 在请求成功处理后调用 + * + * @param {string} apiKey - API Key + * @returns {Promise} + */ +export async function recordPotluckUsage(apiKey) { + if (!apiKey || !apiKey.startsWith(KEY_PREFIX)) { + return null; + } + return incrementUsage(apiKey); +} + +/** + * 创建 Potluck 错误响应 + * @param {http.ServerResponse} res - HTTP 响应对象 + * @param {Object} error - 错误信息 + */ +export function sendPotluckError(res, error) { + const response = { + error: { + message: error.message, + code: error.code, + type: 'potluck_error' + } + }; + + // 如果是配额超限,添加额外信息 + if (error.code === 'quota_exceeded' && error.keyData) { + response.error.quota = { + used: error.keyData.todayUsage, + limit: error.keyData.dailyLimit, + resetDate: error.keyData.lastResetDate + }; + } + + res.writeHead(error.statusCode, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(response)); +} diff --git a/src/common.js b/src/common.js index 7b1928a..b71e4ba 100644 --- a/src/common.js +++ b/src/common.js @@ -490,6 +490,15 @@ export async function handleContentGenerationRequest(req, res, service, endpoint } else { await handleUnaryRequest(res, service, model, processedRequestBody, fromProvider, toProvider, CONFIG.PROMPT_LOG_MODE, PROMPT_LOG_FILENAME, providerPoolManager, actualUuid, actualCustomName); } + + // ============== API 大锅饭插件 - 开始 ============== + if (CONFIG.potluckApiKey) { + try { + const { recordPotluckUsage } = await import('./api-potluck/index.js'); + await recordPotluckUsage(CONFIG.potluckApiKey); + } catch (e) { /* 静默失败,不影响主流程 */ } + } + // ============== API 大锅饭插件 - 结束 ============== } /** diff --git a/src/request-handler.js b/src/request-handler.js index bcc1de8..eb5c4e0 100644 --- a/src/request-handler.js +++ b/src/request-handler.js @@ -8,6 +8,10 @@ import { MODEL_PROVIDER } from './common.js'; import { PROMPT_LOG_FILENAME } from './config-manager.js'; import { handleOllamaRequest, handleOllamaShow } from './ollama-handler.js'; +// ============== API 大锅饭插件 - 开始 ============== +import { handlePotluckApiRoutes, potluckAuthMiddleware, sendPotluckError } from './api-potluck/index.js'; +// ============== API 大锅饭插件 - 结束 ============== + /** * Parse request body as JSON */ @@ -54,7 +58,8 @@ export function createRequestHandler(config, providerPoolManager) { } // Serve static files for UI (除了登录页面需要认证) - if (path.startsWith('/static/') || path === '/' || path === '/favicon.ico' || path === '/index.html' || path.startsWith('/app/') || path === '/login.html') { + // ============== API 大锅饭插件: 添加 /potluck.html ============== + if (path.startsWith('/static/') || path === '/' || path === '/favicon.ico' || path === '/index.html' || path.startsWith('/app/') || path === '/login.html' || path === '/potluck.html') { const served = await serveStaticFiles(path, res); if (served) return; } @@ -62,6 +67,11 @@ export function createRequestHandler(config, providerPoolManager) { const uiHandled = await handleUIApiRequests(method, path, req, res, currentConfig, providerPoolManager); if (uiHandled) return; + // ============== API 大锅饭插件 - 开始 ============== + const potluckRouteHandled = await handlePotluckApiRoutes(method, path, req, res); + if (potluckRouteHandled) return; + // ============== API 大锅饭插件 - 结束 ============== + // Ollama show endpoint with model name if (method === 'POST' && path === '/ollama/api/show') { await handleOllamaShow(req, res); @@ -144,7 +154,16 @@ export function createRequestHandler(config, providerPoolManager) { } // Check authentication for API requests (before Ollama handling to allow unauthenticated Ollama endpoints) - if (!isAuthorized(req, requestUrl, currentConfig.REQUIRED_API_KEY)) { + // ============== API 大锅饭插件 - 开始 ============== + const potluckAuth = await potluckAuthMiddleware(req, requestUrl); + if (potluckAuth.authorized === false) { + sendPotluckError(res, potluckAuth.error); + return; + } else if (potluckAuth.authorized === true) { + currentConfig.potluckApiKey = potluckAuth.apiKey; + console.log(`[API Potluck] Authorized with key: ${potluckAuth.apiKey.substring(0, 12)}...`); + } else if (!isAuthorized(req, requestUrl, currentConfig.REQUIRED_API_KEY)) { + // ============== API 大锅饭插件 - 结束 ============== res.writeHead(401, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: 'Unauthorized: API key is invalid or missing.' } })); return; @@ -217,4 +236,4 @@ export function createRequestHandler(config, providerPoolManager) { handleError(res, error, currentConfig.MODEL_PROVIDER); } }; -} \ No newline at end of file +} diff --git a/static/potluck.html b/static/potluck.html new file mode 100644 index 0000000..6af88f7 --- /dev/null +++ b/static/potluck.html @@ -0,0 +1,276 @@ + + + + + + API 大锅饭 - Key 管理 + + + +
+
+

🍲 API 大锅饭

+ +
+
+
总 Key 数
0
+
已启用
0
+
今日总调用
0
+
累计调用
0
+
+
+
+

Key 列表

+
+ + +
+
+
+
+
+ + + + +
+ + + \ No newline at end of file