Jaasomn
Initial deployment
ceb3821
/**
* 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<Object>}
*/
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<boolean>}
*/
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<boolean>} - 是否处理了请求
*/
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<boolean>} - 是否处理了请求
*/
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<boolean>}
*/
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<Array<{id: string, isHealthy: boolean, message: string}>>}
*/
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 };