/** * API 大锅饭 - 管理 API 路由 * 提供 Key 管理的 RESTful API 和用户端查询 API */ import { createKey, listKeys, getKey, deleteKey, updateKeyLimit, resetKeyUsage, toggleKey, updateKeyName, regenerateKey, getStats, validateKey, KEY_PREFIX, setConfigGetter, updateBonusRemaining, applyDailyLimitToAllKeys, getAllKeyIds } from './key-manager.js'; import { getUserCredentials, addUserCredential, migrateUserCredentials, getAllUsersCredentials, syncCredentialBonuses, getBonusDetails, getConfig, updateConfig, getAllUserApiKeys } from './user-data-manager.js'; import path from 'path'; import { existsSync } from 'fs'; import { promises as fs } from 'fs'; import multer from 'multer'; import { batchImportKiroRefreshTokensStream, importAwsCredentials } from '../../auth/oauth-handlers.js'; import { autoLinkProviderConfigs, getProviderPoolManager } from '../../services/service-manager.js'; import { CONFIG } from '../../core/config-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; } console.log('[API Potluck] Handling request:', method, path); // 验证管理员权限 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(); const config = getConfig(); sendJson(res, 200, { success: true, data: { keys, stats, config } }); return true; } // GET /api/potluck/config - 获取配置 if (method === 'GET' && path === '/api/potluck/config') { const config = getConfig(); sendJson(res, 200, { success: true, data: config }); return true; } // PUT /api/potluck/config - 更新配置 if (method === 'PUT' && path === '/api/potluck/config') { const body = await parseRequestBody(req); const { defaultDailyLimit, bonusPerCredential, bonusValidityDays, persistInterval } = body; // 验证参数 if (defaultDailyLimit !== undefined && (typeof defaultDailyLimit !== 'number' || defaultDailyLimit < 1)) { sendJson(res, 400, { success: false, error: { message: 'defaultDailyLimit must be a positive number' } }); return true; } if (bonusPerCredential !== undefined && (typeof bonusPerCredential !== 'number' || bonusPerCredential < 0)) { sendJson(res, 400, { success: false, error: { message: 'bonusPerCredential must be a non-negative number' } }); return true; } if (bonusValidityDays !== undefined && (typeof bonusValidityDays !== 'number' || bonusValidityDays < 1)) { sendJson(res, 400, { success: false, error: { message: 'bonusValidityDays must be a positive number' } }); return true; } if (persistInterval !== undefined && (typeof persistInterval !== 'number' || persistInterval < 1000)) { sendJson(res, 400, { success: false, error: { message: 'persistInterval must be at least 1000ms' } }); return true; } const newConfig = await updateConfig({ defaultDailyLimit, bonusPerCredential, bonusValidityDays, persistInterval }); sendJson(res, 200, { success: true, message: 'Config updated successfully', data: newConfig }); return true; } // POST /api/potluck/keys/apply-limit - 批量应用每日限额到所有 Key if (method === 'POST' && path === '/api/potluck/keys/apply-limit') { const config = getConfig(); const result = await applyDailyLimitToAllKeys(config.defaultDailyLimit); sendJson(res, 200, { success: true, message: `已将每日限额 ${config.defaultDailyLimit} 应用到 ${result.updated}/${result.total} 个 Key`, data: result }); return true; } // POST /api/potluck/keys/apply-bonus - 批量同步所有用户的资源包 if (method === 'POST' && path === '/api/potluck/keys/apply-bonus') { const allKeyIds = getAllKeyIds(); let totalSynced = 0; let totalBonusUpdated = 0; for (const apiKey of allKeyIds) { try { // 获取用户凭据并检查健康状态 const credentials = getUserCredentials(apiKey); if (credentials.length === 0) continue; // 构建带健康状态的凭证列表(从主服务同步) const credentialsWithHealth = []; for (const cred of credentials) { const healthResult = await syncCredentialHealthFromPool(apiKey, cred); credentialsWithHealth.push({ id: cred.id, isHealthy: healthResult.isHealthy, addedAt: cred.addedAt }); } // 同步资源包 const bonusSync = await syncCredentialBonuses(apiKey, credentialsWithHealth); await updateBonusRemaining(apiKey, bonusSync.bonusRemaining); totalSynced++; if (bonusSync.added > 0 || bonusSync.removed > 0) { totalBonusUpdated++; } } catch (error) { console.warn(`[API Potluck] Failed to sync bonus for ${apiKey.substring(0, 12)}...:`, error.message); } } sendJson(res, 200, { success: true, message: `已同步 ${totalSynced} 个用户的资源包,${totalBonusUpdated} 个有变更`, data: { totalKeys: allKeyIds.length, synced: totalSynced, updated: totalBonusUpdated } }); 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; } // POST /api/potluck/keys/:keyId/regenerate - 重新生成 Key if (method === 'POST' && subPath === '/regenerate') { const result = await regenerateKey(keyId); if (!result) { sendJson(res, 404, { success: false, error: { message: 'Key not found' } }); return true; } sendJson(res, 200, { success: true, message: 'Key regenerated successfully', data: { oldKey: result.oldKey, newKey: result.newKey, keyData: result.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; } } /** * 从请求中提取 Potluck API Key * @param {http.IncomingMessage} req - HTTP 请求对象 * @returns {string|null} */ function extractApiKeyFromRequest(req) { // 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 const xApiKey = req.headers['x-api-key']; if (xApiKey && xApiKey.startsWith(KEY_PREFIX)) { return xApiKey; } return null; } /** * 处理用户端 API 请求 - 用户通过自己的 API Key 查询使用量 * @param {string} method - HTTP 方法 * @param {string} path - 请求路径 * @param {http.IncomingMessage} req - HTTP 请求对象 * @param {http.ServerResponse} res - HTTP 响应对象 * @returns {Promise} - 是否处理了请求 */ export async function handlePotluckUserApiRoutes(method, path, req, res) { // 只处理 /api/potluckuser 开头的请求 if (!path.startsWith('/api/potluckuser')) { return false; } console.log('[API Potluck User] Handling request:', method, path); try { // 从请求中提取 API Key const apiKey = extractApiKeyFromRequest(req); if (!apiKey) { sendJson(res, 401, { success: false, error: { message: 'API Key required. Please provide your API Key in Authorization header (Bearer maki_xxx) or x-api-key header.', code: 'API_KEY_REQUIRED' } }); return true; } // 验证 API Key const validation = await validateKey(apiKey); if (!validation.valid && validation.reason !== 'quota_exceeded') { const errorMessages = { 'invalid_format': 'Invalid API key format', 'not_found': 'API key not found', 'disabled': 'API key has been disabled' }; sendJson(res, 401, { success: false, error: { message: errorMessages[validation.reason] || 'Invalid API key', code: validation.reason } }); return true; } // GET /api/potluckuser/usage - 获取当前用户的使用量信息 if (method === 'GET' && path === '/api/potluckuser/usage') { const keyData = await getKey(apiKey); if (!keyData) { sendJson(res, 404, { success: false, error: { message: 'Key not found', code: 'KEY_NOT_FOUND' } }); return true; } // 计算使用百分比 const usagePercent = keyData.dailyLimit > 0 ? Math.round((keyData.todayUsage / keyData.dailyLimit) * 100) : 0; // 获取资源包详情 const bonusDetails = getBonusDetails(apiKey); const bonusTotal = bonusDetails.bonuses.length * bonusDetails.bonusPerCredential; const bonusUsed = bonusDetails.bonuses.reduce((sum, b) => sum + b.usedCount, 0); // 返回用户友好的使用量信息(隐藏敏感信息) sendJson(res, 200, { success: true, data: { name: keyData.name, enabled: keyData.enabled, usage: { today: keyData.todayUsage, limit: keyData.dailyLimit, remaining: Math.max(0, keyData.dailyLimit - keyData.todayUsage), percent: usagePercent, resetDate: keyData.lastResetDate }, bonusRemaining: keyData.bonusRemaining || 0, bonusTotal: bonusTotal, bonusUsed: bonusUsed, total: keyData.totalUsage, lastUsedAt: keyData.lastUsedAt, createdAt: keyData.createdAt, // 显示部分遮蔽的 Key ID maskedKey: `${apiKey.substring(0, 12)}...${apiKey.substring(apiKey.length - 4)}` } }); return true; } // POST /api/potluckuser/upload - 上传授权文件 if (method === 'POST' && path === '/api/potluckuser/upload') { return await handleUserUpload(req, res, apiKey); } // POST /api/potluckuser/regenerate-key - 用户重置自己的 API Key if (method === 'POST' && path === '/api/potluckuser/regenerate-key') { const result = await regenerateKey(apiKey); if (!result) { sendJson(res, 404, { success: false, error: { message: 'Key not found' } }); return true; } // 同时迁移用户的凭据数据到新 Key await migrateUserCredentials(apiKey, result.newKey); sendJson(res, 200, { success: true, message: 'API Key regenerated successfully', data: { newKey: result.newKey, maskedNewKey: `${result.newKey.substring(0, 12)}...${result.newKey.substring(result.newKey.length - 4)}` } }); return true; } // POST /api/potluckuser/kiro/batch-import-tokens - 批量导入 Kiro refresh token if (method === 'POST' && path === '/api/potluckuser/kiro/batch-import-tokens') { return await handleKiroBatchImportTokens(req, res, apiKey); } // POST /api/potluckuser/kiro/import-aws-credentials - 导入 AWS SSO 凭据 if (method === 'POST' && path === '/api/potluckuser/kiro/import-aws-credentials') { return await handleKiroImportAwsCredentials(req, res, apiKey); } // GET /api/potluckuser/credentials - 获取用户的凭据列表 if (method === 'GET' && path === '/api/potluckuser/credentials') { const credentials = getUserCredentials(apiKey); const bonusDetails = getBonusDetails(apiKey); // 将资源包信息附加到对应凭证 const credentialsWithBonus = credentials.map(cred => { const bonus = bonusDetails.bonuses.find(b => b.credentialId === cred.id); return { ...cred, bonus: bonus ? { usedCount: bonus.usedCount, remaining: bonus.remaining, total: bonusDetails.bonusPerCredential, expiresAt: bonus.expiresAt } : null }; }); sendJson(res, 200, { success: true, data: credentialsWithBonus }); return true; } // POST /api/potluckuser/credentials/check-all - 批量检查所有凭据健康状态 if (method === 'POST' && path === '/api/potluckuser/credentials/check-all') { const results = await checkUserCredentialsHealth(apiKey); const credentials = getUserCredentials(apiKey); const bonusDetails = getBonusDetails(apiKey); // 将资源包信息附加到对应凭证 const credentialsWithBonus = credentials.map(cred => { const healthResult = results.find(r => r.id === cred.id); const bonus = bonusDetails.bonuses.find(b => b.credentialId === cred.id); return { ...cred, isHealthy: healthResult?.isHealthy, healthMessage: healthResult?.message, bonus: bonus ? { usedCount: bonus.usedCount, remaining: bonus.remaining, total: bonusDetails.bonusPerCredential, expiresAt: bonus.expiresAt } : null }; }); sendJson(res, 200, { success: true, data: { results, credentials: credentialsWithBonus } }); return true; } // 处理凭据相关的路由 const credentialMatch = path.match(/^\/api\/potluckuser\/credentials\/([^\/]+)(\/.*)?$/); if (credentialMatch) { const credentialId = decodeURIComponent(credentialMatch[1]); const subPath = credentialMatch[2] || ''; // POST /api/potluckuser/credentials/:id/health - 检查凭据健康状态 if (method === 'POST' && subPath === '/health') { return await handleCredentialHealthCheck(req, res, apiKey, credentialId); } } // 未匹配的用户端路由 sendJson(res, 404, { success: false, error: { message: 'User API endpoint not found' } }); return true; } catch (error) { console.error('[API Potluck] User API error:', error); sendJson(res, 500, { success: false, error: { message: error.message || 'Internal server error' } }); return true; } } /** * 提供商映射 */ const providerMap = { 'gemini-cli-oauth': 'gemini', 'gemini-antigravity': 'antigravity', 'claude-kiro-oauth': 'kiro', 'openai-qwen-oauth': 'qwen', 'openai-iflow': 'iflow' }; /** * 配置 multer 用于用户上传 */ const userUploadStorage = multer.diskStorage({ destination: async (req, file, cb) => { try { // 先使用临时目录 const uploadPath = path.join(process.cwd(), 'configs', 'temp'); await fs.mkdir(uploadPath, { recursive: true }); cb(null, uploadPath); } catch (error) { cb(error); } }, filename: (req, file, cb) => { const timestamp = Date.now(); const sanitizedName = file.originalname.replace(/[^a-zA-Z0-9.-]/g, '_'); cb(null, `${timestamp}_${sanitizedName}`); } }); const userUploadFileFilter = (req, file, cb) => { const allowedTypes = ['.json', '.txt', '.key', '.pem', '.p12', '.pfx']; const ext = path.extname(file.originalname).toLowerCase(); if (allowedTypes.includes(ext)) { cb(null, true); } else { cb(new Error('Unsupported file type'), false); } }; const userUpload = multer({ storage: userUploadStorage, fileFilter: userUploadFileFilter, limits: { fileSize: 5 * 1024 * 1024 // 5MB 限制 } }); /** * 处理用户上传授权文件(带自动绑定和凭据关联功能) * @param {http.IncomingMessage} req * @param {http.ServerResponse} res * @param {string} apiKey - 用户的 API Key * @returns {Promise} */ async function handleUserUpload(req, res, apiKey) { return new Promise((resolve) => { userUpload.single('file')(req, res, async (err) => { if (err) { console.error('[API Potluck User] File upload error:', err.message); sendJson(res, 400, { success: false, error: err.message }); resolve(true); return; } if (!req.file) { sendJson(res, 400, { success: false, error: 'No file uploaded' }); resolve(true); return; } try { const providerType = req.body?.provider || 'common'; const provider = providerMap[providerType] || providerType; const tempFilePath = req.file.path; // 根据 provider 确定目标目录 let targetDir = path.join(process.cwd(), 'configs', provider); // kiro 类型需要子文件夹 if (provider === 'kiro') { const timestamp = Date.now(); const originalNameWithoutExt = path.parse(req.file.originalname).name; const subFolder = `${timestamp}_${originalNameWithoutExt}`; targetDir = path.join(targetDir, subFolder); } await fs.mkdir(targetDir, { recursive: true }); const targetFilePath = path.join(targetDir, req.file.filename); await fs.rename(tempFilePath, targetFilePath); const relativePath = path.relative(process.cwd(), targetFilePath).replace(/\\/g, '/'); // 将凭据关联到用户 const credentialInfo = { path: relativePath, provider: providerType, authMethod: 'file-upload' }; const credential = await addUserCredential(apiKey, credentialInfo); // 自动从主服务同步健康状态 const healthResult = await syncCredentialHealthFromPool(apiKey, credential); // 触发自动绑定 try { await autoLinkProviderConfigs(CONFIG); } catch (linkError) { console.warn('[API Potluck User] Auto-link failed:', linkError.message); } console.log(`[API Potluck User] File uploaded, linked and health checked: ${relativePath} (provider: ${providerType}, health: ${healthResult.message})`); sendJson(res, 200, { success: true, message: 'File uploaded successfully', filePath: relativePath, originalName: req.file.originalname, provider: provider, health: healthResult }); resolve(true); } catch (error) { console.error('[API Potluck User] File processing error:', error); sendJson(res, 500, { success: false, error: error.message }); resolve(true); } }); }); } /** * 处理 Kiro 批量导入 Refresh Token * @param {http.IncomingMessage} req * @param {http.ServerResponse} res * @param {string} apiKey - 用户的 API Key */ async function handleKiroBatchImportTokens(req, res, apiKey) { try { const body = await parseRequestBody(req); const { refreshTokens, region } = body; if (!refreshTokens || !Array.isArray(refreshTokens) || refreshTokens.length === 0) { sendJson(res, 400, { success: false, error: 'refreshTokens array is required and must not be empty' }); return true; } console.log(`[API Potluck User] Starting batch import of ${refreshTokens.length} tokens (user: ${apiKey.substring(0, 12)}...)`); // 设置 SSE 响应头 res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no' }); // 发送 SSE 事件的辅助函数 const sendSSE = (event, data) => { res.write(`event: ${event}\n`); res.write(`data: ${JSON.stringify(data)}\n\n`); }; // 发送开始事件 sendSSE('start', { total: refreshTokens.length }); // 执行流式批量导入 const result = await batchImportKiroRefreshTokensStream( refreshTokens, region || 'us-east-1', async (progress) => { // 每处理完一个 token 发送进度更新 sendSSE('progress', progress); // 成功的凭据关联到用户并执行健康检查 if (progress.current && progress.current.success && progress.current.path) { try { const credentialInfo = { path: progress.current.path.replace(/\\/g, '/'), provider: 'claude-kiro-oauth', authMethod: 'refresh-token' }; const credential = await addUserCredential(apiKey, credentialInfo); // 自动从主服务同步健康状态 await syncCredentialHealthFromPool(apiKey, credential); console.log(`[API Potluck User] Credential linked and health synced: ${credentialInfo.path}`); } catch (linkError) { console.warn('[API Potluck User] Failed to link/check credential:', linkError.message); } } } ); console.log(`[API Potluck User] Completed: ${result.success} success, ${result.failed} failed`); // 发送完成事件 sendSSE('complete', { success: true, total: result.total, successCount: result.success, failedCount: result.failed, details: result.details }); res.end(); return true; } catch (error) { console.error('[API Potluck User] Kiro Batch Import Error:', error); if (res.headersSent) { res.write(`event: error\n`); res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`); res.end(); } else { sendJson(res, 500, { success: false, error: error.message }); } return true; } } /** * 处理 Kiro 导入 AWS 凭据 * @param {http.IncomingMessage} req * @param {http.ServerResponse} res * @param {string} apiKey - 用户的 API Key */ async function handleKiroImportAwsCredentials(req, res, apiKey) { try { const body = await parseRequestBody(req); const { credentials } = body; if (!credentials || typeof credentials !== 'object') { sendJson(res, 400, { success: false, error: 'credentials object is required' }); return true; } // 验证必需字段 const missingFields = []; if (!credentials.clientId) missingFields.push('clientId'); if (!credentials.clientSecret) missingFields.push('clientSecret'); if (!credentials.accessToken) missingFields.push('accessToken'); if (!credentials.refreshToken) missingFields.push('refreshToken'); if (missingFields.length > 0) { sendJson(res, 400, { success: false, error: `Missing required fields: ${missingFields.join(', ')}` }); return true; } console.log(`[API Potluck User] Starting AWS credentials import (user: ${apiKey.substring(0, 12)}...)`); const result = await importAwsCredentials(credentials); if (result.success) { console.log(`[API Potluck User] Successfully imported credentials to: ${result.path}`); // 将凭据路径关联到用户 const credentialInfo = { path: result.path, provider: 'claude-kiro-oauth', authMethod: credentials.authMethod || 'builder-id' }; const credential = await addUserCredential(apiKey, credentialInfo); // 自动从主服务同步健康状态 const healthResult = await syncCredentialHealthFromPool(apiKey, credential); console.log(`[API Potluck User] Health sync result: ${healthResult.message}`); sendJson(res, 200, { success: true, path: result.path, message: 'AWS credentials imported successfully', health: healthResult }); } else { const statusCode = result.error === 'duplicate' ? 409 : 500; sendJson(res, statusCode, { success: false, error: result.error, existingPath: result.existingPath || null }); } return true; } catch (error) { console.error('[API Potluck User] Kiro AWS Import Error:', error); sendJson(res, 500, { success: false, error: error.message }); return true; } } /** * 从主服务同步凭据健康状态(不触发实际检查,不存储到本地) * @param {string} apiKey - 用户的 API Key(保留参数以兼容调用) * @param {Object} credential - 凭据对象 * @returns {Promise<{isHealthy: boolean|null, message: string}>} */ async function syncCredentialHealthFromPool(apiKey, credential) { const fullPath = path.join(process.cwd(), credential.path); // 检查文件是否存在 if (!existsSync(fullPath)) { return { isHealthy: false, message: '凭据文件不存在' }; } // 从 ProviderPoolManager 获取该凭据对应的 provider 状态 const poolManager = getProviderPoolManager(); if (poolManager && credential.provider) { // 在 providerStatus 中查找匹配的配置 const providerPool = poolManager.providerStatus[credential.provider]; if (providerPool && providerPool.length > 0) { // 通过凭据路径匹配 provider 配置 const normalizedCredPath = credential.path.replace(/\\/g, '/'); const matchedProvider = providerPool.find(p => { const configPath = p.config.kiroOAuthCredsFile || p.config.oauthCredsFile || ''; const normalizedConfigPath = configPath.replace(/\\/g, '/'); return normalizedConfigPath === normalizedCredPath || normalizedConfigPath.endsWith(normalizedCredPath) || normalizedCredPath.endsWith(normalizedConfigPath); }); if (matchedProvider) { const config = matchedProvider.config; const isHealthy = config.isHealthy && !config.isDisabled; let message = '健康检查:正常'; if (config.isDisabled) { message = '已禁用'; } else if (!config.isHealthy) { message = config.lastErrorMessage || '健康检查:异常'; } return { isHealthy, message }; } } } // 未在主服务中找到匹配的配置,检查文件有效性 try { const content = await fs.readFile(fullPath, 'utf8'); const credData = JSON.parse(content); // 检查 expiresAt 字段 if (credData.expiresAt) { const expiresAt = new Date(credData.expiresAt); const now = new Date(); if (expiresAt < now) { return { isHealthy: false, message: '凭据已过期' }; } } // 文件存在且未过期,但未在主服务中注册 return { isHealthy: null, message: '未注册到服务' }; } catch (parseError) { return { isHealthy: false, message: '凭据文件格式错误' }; } } /** * 处理凭据健康检查 * @param {http.IncomingMessage} req * @param {http.ServerResponse} res * @param {string} apiKey - 用户的 API Key * @param {string} credentialId - 凭据 ID */ async function handleCredentialHealthCheck(req, res, apiKey, credentialId) { try { const credentials = getUserCredentials(apiKey); const credential = credentials.find(c => c.id === credentialId); if (!credential) { sendJson(res, 404, { success: false, error: { message: 'Credential not found' } }); return true; } console.log(`[API Potluck User] Syncing health for credential: ${credential.path}`); const result = await syncCredentialHealthFromPool(apiKey, credential); sendJson(res, 200, { success: true, data: result }); return true; } catch (error) { console.error('[API Potluck User] Health check error:', error); sendJson(res, 500, { success: false, error: error.message }); return true; } } // ============ 定时健康检查 ============ const HEALTH_CHECK_INTERVAL = 5 * 60 * 1000; // 5 分钟 let healthCheckTimer = null; /** * 批量同步所有用户的凭据健康状态(从主服务同步) * @returns {Promise<{total: number, checked: number, healthy: number, unhealthy: number}>} */ async function checkAllCredentialsHealth() { const allUsers = getAllUsersCredentials(); let total = 0, checked = 0, healthy = 0, unhealthy = 0; for (const { apiKey, credentials } of allUsers) { for (const credential of credentials) { total++; try { const result = await syncCredentialHealthFromPool(apiKey, credential); checked++; if (result.isHealthy) { healthy++; } else if (result.isHealthy === false) { unhealthy++; } // isHealthy === null 表示未注册到服务,不计入健康/不健康 } catch (error) { console.warn(`[API Potluck] Health sync failed for ${credential.path}:`, error.message); } } } return { total, checked, healthy, unhealthy }; } /** * 同步单个用户的所有凭据健康状态(从主服务同步) * 同时更新资源包状态和 Key 的 bonusRemaining * @param {string} apiKey - 用户的 API Key * @returns {Promise>} */ async function checkUserCredentialsHealth(apiKey) { const credentials = getUserCredentials(apiKey); const results = []; for (const credential of credentials) { try { const result = await syncCredentialHealthFromPool(apiKey, credential); results.push({ id: credential.id, isHealthy: result.isHealthy, message: result.message, addedAt: credential.addedAt // 传递 addedAt 用于资源包初始化 }); } catch (error) { results.push({ id: credential.id, isHealthy: null, message: '同步失败: ' + error.message, addedAt: credential.addedAt }); } } // 同步资源包状态并更新 Key 的 bonusRemaining const bonusSync = await syncCredentialBonuses(apiKey, results); await updateBonusRemaining(apiKey, bonusSync.bonusRemaining); return results; } /** * 启动定时健康检查 */ export function startHealthCheckScheduler() { if (healthCheckTimer) { clearInterval(healthCheckTimer); } // 启动后延迟 30 秒执行第一次同步 setTimeout(async () => { console.log('[API Potluck] Running initial health sync from pool...'); const result = await checkAllCredentialsHealth(); console.log(`[API Potluck] Health sync complete: ${result.healthy}/${result.total} healthy`); }, 30000); // 定时同步 healthCheckTimer = setInterval(async () => { console.log('[API Potluck] Running scheduled health sync from pool...'); const result = await checkAllCredentialsHealth(); console.log(`[API Potluck] Health sync complete: ${result.healthy}/${result.total} healthy`); }, HEALTH_CHECK_INTERVAL); console.log(`[API Potluck] Health sync scheduler started (interval: ${HEALTH_CHECK_INTERVAL / 1000}s)`); } /** * 停止定时健康检查 */ export function stopHealthCheckScheduler() { if (healthCheckTimer) { clearInterval(healthCheckTimer); healthCheckTimer = null; console.log('[API Potluck] Health sync scheduler stopped'); } } // 导出批量检查函数供 API 使用 export { checkUserCredentialsHealth };