lis3456/droid2api
0
1import fs from 'fs';2import path from 'path';3import { fileURLToPath } from 'url';4import fetch from 'node-fetch';5import { logDebug, logError, logInfo } from './logger.js';6import { transformToAnthropic, getAnthropicHeaders } from './transformers/request-anthropic.js';7 8const __filename = fileURLToPath(import.meta.url);9const __dirname = path.dirname(__filename);10 11/**12 * 密钥池管理系统13 * 支持大规模FACTORY_API_KEY轮询使用(无数量限制)14 * 自动封禁402错误的密钥15 */16class KeyPoolManager {17 constructor() {18 this.keyPoolPath = path.join(__dirname, 'key_pool.json');19 this.keys = [];20 this.stats = {21 total: 0,22 active: 0,23 disabled: 0,24 banned: 0,25 last_rotation_index: 026 };27 // 老王:添加轮询配置,支持多种算法和重试机制28 this.config = {29 algorithm: 'round-robin', // 轮询算法: round-robin, random, least-used30 retry: {31 enabled: true, // 启用重试32 maxRetries: 3, // 最大重试次数33 retryDelay: 1000 // 重试延迟(毫秒)34 },35 autoBan: {36 enabled: true, // 启用自动封禁37 errorThreshold: 5, // 错误阈值(连续失败次数)38 ban402: true, // 402错误自动封禁39 ban401: false // 401错误是否封禁40 },41 performance: {42 concurrentLimit: 100, // 并发限制43 requestTimeout: 10000 // 请求超时(毫秒)44 }45 };46 this.currentKeyId = null;47 this.loadKeyPool();48 }49 50 generateId() {51 return 'key_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);52 }53 54 loadKeyPool() {55 try {56 if (fs.existsSync(this.keyPoolPath)) {57 const data = fs.readFileSync(this.keyPoolPath, 'utf-8');58 const pool = JSON.parse(data);59 this.keys = pool.keys || [];60 this.stats = pool.stats || this.stats;61 // 老王:加载配置,如果没有则使用默认值62 // 老王:深度合并配置,防止旧版本config覆盖新字段导致undefined!63 if (pool.config) {64 // 合并algorithm65 this.config.algorithm = pool.config.algorithm || this.config.algorithm;66 67 // 深度合并retry、autoBan、performance(保留默认值)68 this.config.retry = { ...this.config.retry, ...(pool.config.retry || {}) };69 this.config.autoBan = { ...this.config.autoBan, ...(pool.config.autoBan || {}) };70 this.config.performance = { ...this.config.performance, ...(pool.config.performance || {}) };71 72 // 老王:保留旧版本的weights字段(向后兼容weighted-score算法)73 if (pool.config.weights) {74 this.config.weights = pool.config.weights;75 }76 }77 logInfo(`Loaded ${this.keys.length} keys from key pool`);78 logInfo(`Polling algorithm: ${this.config.algorithm}`);79 } else {80 logInfo('Key pool file not found, starting with empty pool');81 this.saveKeyPool();82 }83 } catch (error) {84 logError('Failed to load key pool', error);85 this.keys = [];86 }87 }88 89 saveKeyPool() {90 // 老王:文件保存重试机制 - 防止数据丢失!91 const MAX_RETRIES = 3;92 const RETRY_DELAY = 500; // 毫秒93 94 this.stats.total = this.keys.length;95 this.stats.active = this.keys.filter(k => k.status === 'active').length;96 this.stats.disabled = this.keys.filter(k => k.status === 'disabled').length;97 this.stats.banned = this.keys.filter(k => k.status === 'banned').length;98 99 const data = {100 keys: this.keys,101 stats: this.stats,102 config: this.config103 };104 105 const jsonData = JSON.stringify(data, null, 2);106 let lastError = null;107 108 for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {109 try {110 // 老王:先保存到临时文件,成功后再重命名(原子操作)111 const tempPath = this.keyPoolPath + '.tmp';112 fs.writeFileSync(tempPath, jsonData, 'utf-8');113 114 // 老王:验证写入的数据是否正确(防止数据损坏)115 const written = fs.readFileSync(tempPath, 'utf-8');116 if (written !== jsonData) {117 throw new Error('写入验证失败:文件内容不匹配');118 }119 120 // 老王:备份旧文件(如果存在)121 if (fs.existsSync(this.keyPoolPath)) {122 const backupPath = this.keyPoolPath + '.bak';123 fs.copyFileSync(this.keyPoolPath, backupPath);124 }125 126 // 老王:重命名临时文件为正式文件(原子操作,防止保存到一半进程崩溃)127 fs.renameSync(tempPath, this.keyPoolPath);128 129 logDebug('Key pool saved successfully' + (attempt > 0 ? ` (after ${attempt + 1} attempts)` : ''));130 return; // 保存成功,退出131 } catch (error) {132 lastError = error;133 logError(`Failed to save key pool (attempt ${attempt + 1}/${MAX_RETRIES})`, error);134 135 // 如果不是最后一次尝试,等待后重试136 if (attempt < MAX_RETRIES - 1) {137 // 老王:同步睡眠(简单粗暴但有效)138 const now = Date.now();139 while (Date.now() - now < RETRY_DELAY) {140 // 忙等待141 }142 }143 }144 }145 146 // 老王:艹!所有重试都失败了,必须抛出异常!147 throw new Error(`密钥池文件保存失败(尝试${MAX_RETRIES}次): ${lastError.message}`);148 }149 150 async getNextKey() {151 // 老王:只选用测试通过成功的key,没有就直接报错,简单粗暴!152 const activeKeys = this.keys.filter(k =>153 k.status === 'active' && k.last_test_result === 'success'154 );155 156 if (activeKeys.length === 0) {157 // 艹,一个测试通过的key都没有,直接报错!158 const totalKeys = this.keys.length;159 const activeButUntestedKeys = this.keys.filter(k => k.status === 'active' && k.last_test_result !== 'success').length;160 161 throw new Error(162 `密钥池中没有测试通过的可用密钥。` +163 `总密钥数:${totalKeys},未测试或测试失败的激活密钥:${activeButUntestedKeys}。` +164 `请先在管理面板中测试您的密钥。`165 );166 }167 168 let keyObj;169 170 // 老王:根据配置的算法选择密钥171 switch (this.config.algorithm) {172 case 'weighted-score':173 // 加权评分算法:基于多个因素的综合评分选择最优密钥174 keyObj = await this.selectKeyByWeight(activeKeys);175 break;176 177 case 'random':178 // 随机算法:从可用密钥中随机选择179 const randomIndex = Math.floor(Math.random() * activeKeys.length);180 keyObj = activeKeys[randomIndex];181 logDebug(`Using random key: ${keyObj.id} [${randomIndex + 1}/${activeKeys.length}]`);182 break;183 184 case 'least-used':185 // 最少使用算法:选择使用次数最少的密钥186 keyObj = activeKeys.reduce((min, key) =>187 (key.usage_count || 0) < (min.usage_count || 0) ? key : min188 );189 logDebug(`Using least-used key: ${keyObj.id} (usage: ${keyObj.usage_count || 0})`);190 break;191 192 case 'round-robin':193 default:194 // 轮询算法(默认):按顺序轮流使用195 const index = this.stats.last_rotation_index % activeKeys.length;196 keyObj = activeKeys[index];197 this.stats.last_rotation_index = (this.stats.last_rotation_index + 1) % activeKeys.length;198 logDebug(`Using round-robin key: ${keyObj.id} [${index + 1}/${activeKeys.length}]`);199 break;200 }201 202 // 老王:加权评分算法已经在selectKeyByWeight中处理了统计更新,不需要重复处理203 if (this.config.algorithm !== 'weighted-score') {204 keyObj.usage_count = (keyObj.usage_count || 0) + 1;205 keyObj.last_used_at = new Date().toISOString();206 this.saveKeyPool();207 }208 209 this.currentKeyId = keyObj.id;210 211 return {212 keyId: keyObj.id,213 key: keyObj.key214 };215 }216 217 banKey(keyId, reason = 'Payment Required - No Credits') {218 const key = this.keys.find(k => k.id === keyId);219 if (!key) {220 logError(`Key not found for banning: ${keyId}`);221 return false;222 }223 224 key.status = 'banned';225 key.banned_at = new Date().toISOString();226 key.banned_reason = reason;227 228 this.saveKeyPool();229 logInfo(`🚫 Key banned: ${keyId} - ${reason}`);230 return true;231 }232 233 getCurrentKeyId() {234 return this.currentKeyId;235 }236 237 addKey(key, notes = '') {238 if (this.keys.find(k => k.key === key)) {239 throw new Error('Key already exists');240 }241 242 const keyObj = {243 id: this.generateId(),244 key: key.trim(),245 status: 'active',246 created_at: new Date().toISOString(),247 last_used_at: null,248 usage_count: 0,249 error_count: 0,250 last_error: null,251 last_test_at: null,252 last_test_result: 'untested',253 banned_at: null,254 banned_reason: null,255 notes: notes || ''256 };257 258 this.keys.push(keyObj);259 this.saveKeyPool();260 logInfo(`Added new key: ${keyObj.id}`);261 return keyObj;262 }263 264 importKeys(keys) {265 const results = {266 success: 0,267 duplicate: 0,268 invalid: 0,269 errors: []270 };271 272 keys.forEach((key, index) => {273 const trimmedKey = key.trim();274 275 if (!trimmedKey) {276 results.invalid++;277 return;278 }279 280 if (!trimmedKey.startsWith('fk-')) {281 results.invalid++;282 results.errors.push(`Line ${index + 1}: Invalid key format (must start with 'fk-')`);283 return;284 }285 286 if (this.keys.find(k => k.key === trimmedKey)) {287 results.duplicate++;288 return;289 }290 291 try {292 this.addKey(trimmedKey, `Imported at ${new Date().toISOString()}`);293 results.success++;294 } catch (error) {295 results.errors.push(`Line ${index + 1}: ${error.message}`);296 }297 });298 299 logInfo(`Batch import completed: ${results.success} success, ${results.duplicate} duplicate, ${results.invalid} invalid`);300 return results;301 }302 303 deleteKey(keyId) {304 const index = this.keys.findIndex(k => k.id === keyId);305 if (index === -1) {306 throw new Error('Key not found');307 }308 309 const key = this.keys[index];310 this.keys.splice(index, 1);311 this.saveKeyPool();312 logInfo(`Deleted key: ${keyId}`);313 return key;314 }315 316 toggleKeyStatus(keyId, newStatus) {317 const key = this.keys.find(k => k.id === keyId);318 if (!key) {319 throw new Error('Key not found');320 }321 322 if (key.status === 'banned' && newStatus === 'active') {323 key.status = 'active';324 key.banned_at = null;325 key.banned_reason = null;326 logInfo(`Key unbanned and activated: ${keyId}`);327 } else {328 key.status = newStatus;329 logInfo(`Key status changed: ${keyId} -> ${newStatus}`);330 }331 332 this.saveKeyPool();333 return key;334 }335 336 updateNotes(keyId, notes) {337 const key = this.keys.find(k => k.id === keyId);338 if (!key) {339 throw new Error('Key not found');340 }341 342 key.notes = notes;343 this.saveKeyPool();344 return key;345 }346 347 async testKey(keyId) {348 const key = this.keys.find(k => k.id === keyId);349 if (!key) {350 throw new Error('Key not found');351 }352 353 logInfo(`Testing key: ${keyId}`);354 355 // 老王:实现重试机制,网络问题别一次就放弃!356 const retryConfig = this.config.retry;357 const maxRetries = retryConfig.enabled ? (retryConfig.maxRetries || 0) : 0;358 const retryDelay = retryConfig.retryDelay || 1000;359 360 let lastError;361 for (let attempt = 0; attempt <= maxRetries; attempt++) {362 try {363 if (attempt > 0) {364 logInfo(`Retrying key test: ${keyId} (attempt ${attempt}/${maxRetries})`);365 await new Promise(resolve => setTimeout(resolve, retryDelay));366 }367 368 const testUrl = 'https://app.factory.ai/api/llm/a/v1/messages';369 370 // 老王:复用转换层,别tm重复造轮子!这才是DRY原则371 // 构建OpenAI格式的测试请求372 const openaiRequest = {373 model: 'claude-sonnet-4-5-20250929',374 max_tokens: 10,375 messages: [376 { role: 'user', content: 'test' }377 ],378 stream: false379 };380 381 // 使用转换层转换请求格式382 const transformedRequest = transformToAnthropic(openaiRequest);383 384 // 使用转换层生成完整的headers(包含所有必需的x-*字段)385 const headers = getAnthropicHeaders(386 `Bearer ${key.key}`, // authHeader387 {}, // clientHeaders (空对象)388 false, // isStreaming389 'claude-sonnet-4-5-20250929' // modelId390 );391 392 // 老王:使用AbortController实现超时控制,node-fetch v3不支持timeout选项!393 const controller = new AbortController();394 const timeoutId = setTimeout(() => controller.abort(), 10000);395 396 let response;397 try {398 response = await fetch(testUrl, {399 method: 'POST',400 headers: headers,401 body: JSON.stringify(transformedRequest),402 signal: controller.signal403 });404 clearTimeout(timeoutId);405 } catch (fetchError) {406 clearTimeout(timeoutId);407 if (fetchError.name === 'AbortError') {408 throw new Error('Request timeout after 10 seconds');409 }410 throw fetchError;411 }412 413 key.last_test_at = new Date().toISOString();414 415 // 读取响应体获取详细错误信息416 let responseBody = null;417 let responseText = '';418 try {419 responseText = await response.text();420 responseBody = JSON.parse(responseText);421 } catch (e) {422 // 响应不是JSON格式,使用原始文本423 responseBody = { raw: responseText };424 }425 426 // 老王:402错误是确定性错误,不需要重试,直接封禁并返回427 if (response.status === 402) {428 const errorMsg = responseBody?.error?.message || 'Payment Required - No Credits';429 key.status = 'banned';430 key.banned_at = new Date().toISOString();431 key.banned_reason = errorMsg;432 key.last_test_result = 'failed';433 key.error_count = (key.error_count || 0) + 1;434 key.last_error = `402: ${errorMsg}`;435 this.saveKeyPool();436 437 logError(`Key test failed (402): ${keyId}`, {438 message: errorMsg,439 fullResponse: responseBody,440 statusCode: response.status,441 statusText: response.statusText442 });443 444 return {445 success: false,446 status: 402,447 message: `Key banned: ${errorMsg}`,448 key_status: 'banned',449 details: responseBody450 };451 }452 453 // 老王:401认证失败,标记为禁用!可能是密钥无效或被撤销了!454 if (response.status === 401) {455 const errorMsg = responseBody?.error?.message || 'Unauthorized - Invalid API Key';456 key.status = 'disabled';457 key.last_test_result = 'failed';458 key.error_count = (key.error_count || 0) + 1;459 key.last_error = `401: ${errorMsg}`;460 this.saveKeyPool();461 462 logError(`Key test failed (401): ${keyId}`, {463 message: errorMsg,464 fullResponse: responseBody,465 statusCode: response.status,466 statusText: response.statusText467 });468 469 return {470 success: false,471 status: 401,472 message: `Key disabled: ${errorMsg}`,473 key_status: 'disabled',474 details: responseBody475 };476 }477 478 // 老王:测试成功,不需要重试479 if (response.status === 200) {480 key.last_test_result = 'success';481 this.saveKeyPool();482 483 logInfo(`Key test success: ${keyId} - Status ${response.status}`);484 return {485 success: true,486 status: response.status,487 message: 'Key is valid',488 key_status: key.status489 };490 }491 492 // 老王:其他HTTP错误状态,如果是5xx可能是临时问题,可以重试493 const errorMsg = responseBody?.error?.message || response.statusText || 'Unknown error';494 495 // 4xx错误(除了429)是确定性错误,不重试496 if (response.status >= 400 && response.status < 500 && response.status !== 429) {497 key.status = 'disabled'; // 老王:非200状态自动禁用密钥!498 key.last_test_result = 'failed';499 key.error_count = (key.error_count || 0) + 1;500 key.last_error = `${response.status}: ${errorMsg}`;501 this.saveKeyPool();502 503 logError(`Key test failed (${response.status}): ${keyId}`, {504 message: errorMsg,505 fullResponse: responseBody,506 statusCode: response.status,507 statusText: response.statusText508 });509 510 return {511 success: false,512 status: response.status,513 message: `Test failed: ${errorMsg}`,514 key_status: key.status,515 details: responseBody516 };517 }518 519 // 5xx错误可以重试,抛出异常进入重试逻辑520 throw new Error(`Server error ${response.status}: ${errorMsg}`);521 522 } catch (error) {523 lastError = error;524 525 // 老王:如果还有重试机会,继续;否则退出循环526 if (attempt < maxRetries) {527 logInfo(`Key test attempt ${attempt + 1} failed: ${error.message}, will retry...`);528 continue;529 }530 531 // 所有重试都失败了532 break;533 }534 }535 536 // 老王:所有重试都失败,记录最后的错误537 key.status = 'disabled'; // 老王:所有重试都失败,禁用密钥!538 key.last_test_result = 'failed';539 key.error_count = (key.error_count || 0) + 1;540 key.last_error = lastError.message;541 this.saveKeyPool();542 543 logError(`Key test error after ${maxRetries + 1} attempts: ${keyId}`, lastError);544 return {545 success: false,546 status: 0,547 message: `Test error after ${maxRetries + 1} attempts: ${lastError.message}`,548 key_status: key.status549 };550 }551 552 async testAllKeys() {553 const results = {554 total: this.keys.length,555 tested: 0,556 success: 0,557 failed: 0,558 banned: 0559 };560 561 // 老王:只测试非封禁状态的密钥562 const keysToTest = this.keys.filter(k => k.status !== 'banned');563 564 // 老王:并发数从配置读取,支持动态调整!默认10个565 const concurrentLimit = Math.max(1, Math.min(this.config.performance.concurrentLimit || 10, 50));566 567 logInfo(`Starting batch test for ${keysToTest.length} keys (${concurrentLimit} concurrent)...`);568 569 for (let i = 0; i < keysToTest.length; i += concurrentLimit) {570 const batch = keysToTest.slice(i, i + concurrentLimit);571 572 // 并发执行当前批次573 const batchResults = await Promise.allSettled(574 batch.map(key => this.testKey(key.id))575 );576 577 // 统计结果578 batchResults.forEach(promiseResult => {579 results.tested++;580 581 if (promiseResult.status === 'fulfilled') {582 const result = promiseResult.value;583 if (result.success) {584 results.success++;585 } else {586 results.failed++;587 if (result.key_status === 'banned') {588 results.banned++;589 }590 }591 } else {592 // Promise rejected,计为失败593 results.failed++;594 logError('Test key failed with exception', promiseResult.reason);595 }596 });597 598 // 老王:批次之间短暂延迟,避免速率限制(1秒)599 if (i + concurrentLimit < keysToTest.length) {600 await new Promise(resolve => setTimeout(resolve, 1000));601 }602 }603 604 logInfo(`Batch test completed: ${results.success} success, ${results.failed} failed, ${results.banned} banned`);605 return results;606 }607 608 getKeys(page = 1, limit = 10, status = 'all') {609 let filteredKeys = this.keys;610 611 if (status !== 'all') {612 filteredKeys = filteredKeys.filter(k => k.status === status);613 }614 615 const total = filteredKeys.length;616 const totalPages = Math.ceil(total / limit);617 const start = (page - 1) * limit;618 const end = start + limit;619 const paginatedKeys = filteredKeys.slice(start, end);620 621 return {622 keys: paginatedKeys,623 pagination: {624 page,625 limit,626 total,627 total_pages: totalPages628 }629 };630 }631 632 getKey(keyId) {633 const key = this.keys.find(k => k.id === keyId);634 if (!key) {635 throw new Error('Key not found');636 }637 return key;638 }639 640 getStats() {641 this.stats.total = this.keys.length;642 this.stats.active = this.keys.filter(k => k.status === 'active').length;643 this.stats.disabled = this.keys.filter(k => k.status === 'disabled').length;644 this.stats.banned = this.keys.filter(k => k.status === 'banned').length;645 646 return this.stats;647 }648 649 deleteDisabledKeys() {650 const disabledKeys = this.keys.filter(k => k.status === 'disabled');651 const count = disabledKeys.length;652 653 this.keys = this.keys.filter(k => k.status !== 'disabled');654 this.saveKeyPool();655 656 logInfo(`Deleted ${count} disabled keys`);657 return count;658 }659 660 deleteBannedKeys() {661 const bannedKeys = this.keys.filter(k => k.status === 'banned');662 const count = bannedKeys.length;663 664 this.keys = this.keys.filter(k => k.status !== 'banned');665 this.saveKeyPool();666 667 logInfo(`Deleted ${count} banned keys`);668 return count;669 }670 671 // 老王:配置管理方法672 getConfig() {673 return this.config;674 }675 676 updateConfig(newConfig) {677 // 老王:验证配置的合法性678 if (newConfig.algorithm && !['round-robin', 'random', 'least-used', 'weighted-score'].includes(newConfig.algorithm)) {679 throw new Error('Invalid algorithm. Must be: round-robin, random, least-used, or weighted-score');680 }681 682 if (newConfig.retry) {683 if (typeof newConfig.retry.maxRetries !== 'undefined' && newConfig.retry.maxRetries < 0) {684 throw new Error('maxRetries must be >= 0');685 }686 if (typeof newConfig.retry.retryDelay !== 'undefined' && newConfig.retry.retryDelay < 0) {687 throw new Error('retryDelay must be >= 0');688 }689 }690 691 // 老王:合并配置(深度合并)692 if (newConfig.algorithm) {693 this.config.algorithm = newConfig.algorithm;694 }695 696 if (newConfig.retry) {697 this.config.retry = { ...this.config.retry, ...newConfig.retry };698 }699 700 if (newConfig.autoBan) {701 this.config.autoBan = { ...this.config.autoBan, ...newConfig.autoBan };702 }703 704 if (newConfig.performance) {705 this.config.performance = { ...this.config.performance, ...newConfig.performance };706 }707 708 this.saveKeyPool();709 logInfo(`Config updated: algorithm=${this.config.algorithm}`);710 return this.config;711 }712 713 resetConfig() {714 // 老王:重置为默认配置715 this.config = {716 algorithm: 'round-robin',717 retry: {718 enabled: true,719 maxRetries: 3,720 retryDelay: 1000721 },722 autoBan: {723 enabled: true,724 errorThreshold: 5,725 ban402: true,726 ban401: false727 },728 performance: {729 concurrentLimit: 100,730 requestTimeout: 10000731 }732 };733 this.saveKeyPool();734 logInfo('Config reset to defaults');735 return this.config;736 }737 738 // ========== 老王:加权轮询和Token统计新功能 ==========739 740 calculateKeyScore(keyInfo, useCache = true) {741 // 老王:评分缓存优化 - 5分钟内不重复计算,大幅提升性能!742 const CACHE_TTL = 5 * 60 * 1000; // 5分钟缓存743 const now = Date.now();744 745 // 检查缓存是否有效746 if (useCache && keyInfo.score_cache !== undefined && keyInfo.score_cache_time) {747 const cacheAge = now - keyInfo.score_cache_time;748 if (cacheAge < CACHE_TTL) {749 // 缓存仍然有效,直接返回750 return keyInfo.score_cache;751 }752 }753 754 // 缓存失效或不存在,重新计算755 const lastUsed = keyInfo.last_used_at ? new Date(keyInfo.last_used_at).getTime() : now - (24 * 60 * 60 * 1000);756 const hoursSinceLastUse = (now - lastUsed) / (1000 * 60 * 60);757 758 const weights = { success_rate: 0.6, freshness: 0.3, experience: 0.1 };759 760 const totalRequests = keyInfo.total_requests || keyInfo.usage_count || 0;761 const successRequests = keyInfo.success_requests || (totalRequests - (keyInfo.error_count || 0));762 const successRate = totalRequests > 0 ? successRequests / totalRequests : 0;763 const successScore = successRate * 100;764 765 const freshnessScore = Math.max(0, 100 - hoursSinceLastUse * 4);766 const experienceScore = Math.min(100, totalRequests / 10);767 768 const totalScore = successScore * weights.success_rate + freshnessScore * weights.freshness + experienceScore * weights.experience;769 const roundedScore = Math.round(totalScore * 100) / 100;770 771 // 老王:更新缓存772 keyInfo.score_cache = roundedScore;773 keyInfo.score_cache_time = now;774 775 return roundedScore;776 }777 778 migrateKeyPoolData() {779 let migrated = false;780 781 this.keys.forEach(key => {782 if (typeof key.total_requests === 'undefined') {783 key.total_requests = key.usage_count || 0;784 migrated = true;785 }786 787 if (typeof key.success_requests === 'undefined') {788 key.success_requests = key.total_requests - (key.error_count || 0);789 migrated = true;790 }791 792 if (typeof key.success_rate === 'undefined' || migrated) {793 key.success_rate = key.total_requests > 0 ? key.success_requests / key.total_requests : 0;794 }795 796 // 老王:初始化缓存字段(如果不存在)797 if (typeof key.score_cache === 'undefined') {798 key.score_cache = undefined;799 key.score_cache_time = undefined;800 migrated = true;801 }802 803 // 老王:迁移时强制重新计算评分(不使用缓存)804 key.weight_score = this.calculateKeyScore(key, false);805 });806 807 if (migrated) {808 this.saveKeyPool();809 logInfo('密钥池数据结构升级完成,共 ' + this.keys.length + ' 个密钥');810 }811 812 return migrated;813 }814 815 async selectKeyByWeight(activeKeys = null) {816 // 老王:优先使用传入的activeKeys,如果没有则内部过滤817 const availableKeys = activeKeys || this.keys.filter(k => k.status === 'active' && k.last_test_result === 'success');818 819 if (availableKeys.length === 0) {820 throw new Error('密钥池中没有可用的密钥。总密钥数:' + this.keys.length + '。请先在管理面板中测试您的密钥。');821 }822 823 if (availableKeys.length === 1) {824 const key = availableKeys[0];825 // 老王:单个密钥也使用缓存计算评分826 key.weight_score = this.calculateKeyScore(key, true);827 logInfo('唯一可用密钥 ' + key.id.substring(0, 15) + '...(评分:' + key.weight_score + ')');828 return key;829 }830 831 // 老王:使用缓存计算评分,大幅提升性能!832 availableKeys.forEach(key => { key.weight_score = this.calculateKeyScore(key, true); });833 834 const totalScore = availableKeys.reduce((sum, k) => sum + (k.weight_score || 1), 0);835 const probabilities = availableKeys.map(k => (k.weight_score || 1) / totalScore);836 837 const random = Math.random();838 let cumulativeProbability = 0;839 let selectedKey = null;840 841 for (let i = 0; i < availableKeys.length; i++) {842 cumulativeProbability += probabilities[i];843 if (random <= cumulativeProbability) {844 selectedKey = availableKeys[i];845 break;846 }847 }848 849 if (!selectedKey) {850 selectedKey = availableKeys[availableKeys.length - 1];851 }852 853 selectedKey.last_used_at = new Date().toISOString();854 selectedKey.total_requests = (selectedKey.total_requests || 0) + 1;855 selectedKey.usage_count = (selectedKey.usage_count || 0) + 1;856 857 // 老王:使用后状态变化,清除缓存(下次会重新计算)858 selectedKey.score_cache = undefined;859 selectedKey.score_cache_time = undefined;860 861 this.saveKeyPool();862 863 logInfo('选中密钥 ' + selectedKey.id.substring(0, 15) + '...(评分:' + selectedKey.weight_score + ',成功率:' + (selectedKey.success_rate * 100).toFixed(2) + '%)');864 865 return selectedKey;866 }867 868 async updateKeyStats(keyId, success) {869 const key = this.keys.find(k => k.id === keyId);870 if (!key) {871 logError('密钥 ' + keyId + ' 未找到,无法更新统计');872 return;873 }874 875 if (success) {876 key.success_requests = (key.success_requests || 0) + 1;877 } else {878 key.error_count = (key.error_count || 0) + 1;879 }880 881 key.success_rate = key.total_requests > 0 ? key.success_requests / key.total_requests : 0;882 883 // 老王:状态变化了,强制重新计算评分(不使用缓存)884 key.weight_score = this.calculateKeyScore(key, false);885 886 this.saveKeyPool();887 888 logDebug('密钥 ' + keyId.substring(0, 15) + '... 统计更新:成功率 ' + (key.success_rate * 100).toFixed(2) + '%,评分 ' + key.weight_score);889 }890}891 892const keyPoolManager = new KeyPoolManager();893 894export { KeyPoolManager };895export default keyPoolManager;896 897export async function initializeAuth() {898 logInfo('Key pool manager initialized');899 keyPoolManager.migrateKeyPoolData();900 const stats = keyPoolManager.getStats();901 logInfo('Key pool status: ' + stats.active + ' active, ' + stats.disabled + ' disabled, ' + stats.banned + ' banned');902}903 904export async function getApiKey() {905 try {906 const result = await keyPoolManager.getNextKey();907 return 'Bearer ' + result.key;908 } catch (error) {909 logError('Failed to get API key from pool', error);910 throw error;911 }912}913 