feat(provider): 添加健康状态跟踪并持久化到JSON文件

添加provider健康状态跟踪功能,包括使用次数、错误计数和最后使用时间
将provider状态持久化到JSON文件,确保重启后状态不丢失
重构provider选择逻辑,将状态管理移至config对象
This commit is contained in:
hex2077 2025-08-30 14:02:13 +08:00
parent 3109eb21c5
commit b60593085e
2 changed files with 125 additions and 32 deletions

View file

@ -3,46 +3,86 @@
{
"OPENAI_API_KEY": "sk-openai-key1",
"OPENAI_BASE_URL": "https://api.openai.com/v1",
"uuid": "2f579c65-d3c5-41b1-9985-9f6e3d7bf39c"
"uuid": "2f579c65-d3c5-41b1-9985-9f6e3d7bf39c",
"isHealthy": true,
"lastUsed": null,
"usageCount": 0,
"errorCount": 0,
"lastErrorTime": null
},
{
"OPENAI_API_KEY": "sk-openai-key2",
"OPENAI_BASE_URL": "https://api.openai.com/v1",
"uuid": "e284628d-302f-456d-91f3-6095386fb3b8"
"uuid": "e284628d-302f-456d-91f3-6095386fb3b8",
"isHealthy": true,
"lastUsed": null,
"usageCount": 0,
"errorCount": 0,
"lastErrorTime": null
}
],
"gemini-cli": [
"gemini-cli-oauth": [
{
"GEMINI_OAUTH_CREDS_FILE_PATH": "./credentials1.json",
"PROJECT_ID": "your-project-id-1",
"uuid": "ac200154-26b8-4f5f-8650-e8cc738b06e3"
"uuid": "ac200154-26b8-4f5f-8650-e8cc738b06e3",
"isHealthy": true,
"lastUsed": null,
"usageCount": 0,
"errorCount": 0,
"lastErrorTime": null
},
{
"GEMINI_OAUTH_CREDS_FILE_PATH": "./credentials2.json",
"PROJECT_ID": "your-project-id-2",
"uuid": "4f8afcc2-a9bb-4b96-bb50-3b9667a71f54"
"uuid": "4f8afcc2-a9bb-4b96-bb50-3b9667a71f54",
"isHealthy": true,
"lastUsed": null,
"usageCount": 0,
"errorCount": 0,
"lastErrorTime": null
}
],
"claude-custom": [
{
"CLAUDE_API_KEY": "sk-claude-key1",
"CLAUDE_BASE_URL": "https://api.anthropic.com",
"uuid": "bb87047a-3b1d-4249-adbb-1087ecd58128"
"uuid": "bb87047a-3b1d-4249-adbb-1087ecd58128",
"isHealthy": true,
"lastUsed": null,
"usageCount": 0,
"errorCount": 0,
"lastErrorTime": null
},
{
"CLAUDE_API_KEY": "sk-claude-key2",
"CLAUDE_BASE_URL": "https://api.anthropic.com",
"uuid": "7c2002c6-122a-4db0-af06-8a0ff433801a"
"uuid": "7c2002c6-122a-4db0-af06-8a0ff433801a",
"isHealthy": true,
"lastUsed": null,
"usageCount": 0,
"errorCount": 0,
"lastErrorTime": null
}
],
"kiro-api": [
"claude-kiro-oauth": [
{
"KIRO_OAUTH_CREDS_FILE_PATH": "./kiro_creds1.json",
"uuid": "2c69d0ac-b86f-43d8-9d17-0d300afc5cfd"
"uuid": "2c69d0ac-b86f-43d8-9d17-0d300afc5cfd",
"isHealthy": true,
"lastUsed": null,
"usageCount": 0,
"errorCount": 0,
"lastErrorTime": null
},
{
"KIRO_OAUTH_CREDS_FILE_PATH": "./kiro_creds2.json",
"uuid": "7482abe6-8083-4288-bb7d-d8ecb7c461e2"
"uuid": "7482abe6-8083-4288-bb7d-d8ecb7c461e2",
"isHealthy": true,
"lastUsed": null,
"usageCount": 0,
"errorCount": 0,
"lastErrorTime": null
}
]
}

View file

@ -1,3 +1,5 @@
import * as fs from 'fs'; // Import fs module
/**
* Manages a pool of API service providers, handling their health and selection.
*/
@ -19,15 +21,21 @@ export class ProviderPoolManager {
for (const providerType in this.providerPools) {
this.providerStatus[providerType] = [];
this.roundRobinIndex[providerType] = 0; // Initialize round-robin index for each type
this.providerPools[providerType].forEach((providerConfig, index) => {
this.providerPools[providerType].forEach((providerConfig) => {
// Ensure initial health and usage stats are present in the config
providerConfig.isHealthy = providerConfig.isHealthy !== undefined ? providerConfig.isHealthy : true;
providerConfig.lastUsed = providerConfig.lastUsed !== undefined ? providerConfig.lastUsed : null;
providerConfig.usageCount = providerConfig.usageCount !== undefined ? providerConfig.usageCount : 0;
providerConfig.errorCount = providerConfig.errorCount !== undefined ? providerConfig.errorCount : 0;
if (providerConfig.lastErrorTime && typeof providerConfig.lastErrorTime === 'string') {
providerConfig.lastErrorTime = new Date(providerConfig.lastErrorTime);
} else if (providerConfig.lastErrorTime === undefined) {
providerConfig.lastErrorTime = null;
}
this.providerStatus[providerType].push({
config: providerConfig,
uuid: providerConfig.uuid,
isHealthy: true,
lastUsed: null,
usageCount: 0,
errorCount: 0,
lastErrorTime: null, // New: Timestamp of the last error
uuid: providerConfig.uuid, // Still keep uuid at the top level for easy access
});
});
}
@ -42,7 +50,7 @@ export class ProviderPoolManager {
*/
selectProvider(providerType) {
const availableProviders = this.providerStatus[providerType] || [];
const healthyProviders = availableProviders.filter(p => p.isHealthy);
const healthyProviders = availableProviders.filter(p => p.config.isHealthy);
if (healthyProviders.length === 0) {
console.warn(`[ProviderPoolManager] No healthy providers available for type: ${providerType}`);
@ -65,9 +73,11 @@ export class ProviderPoolManager {
}
if (selected) {
selected.lastUsed = new Date();
selected.usageCount++; // Increment usage count
selected.config.lastUsed = new Date();
selected.config.usageCount++; // Increment usage count
console.log(`[ProviderPoolManager] Selected provider for ${providerType} (round-robin): ${JSON.stringify(selected.config)}`);
this._saveProviderPoolsToJson(providerType); // Persist changes
return selected.config;
}
@ -84,15 +94,16 @@ export class ProviderPoolManager {
if (pool) {
const provider = pool.find(p => p.uuid === providerConfig.uuid);
if (provider) {
provider.errorCount++;
provider.lastErrorTime = new Date(); // Update last error time
provider.config.errorCount++;
provider.config.lastErrorTime = new Date(); // Update last error time in config
if (provider.errorCount >= this.maxErrorCount) {
provider.isHealthy = false;
console.warn(`[ProviderPoolManager] Marked provider as unhealthy: ${JSON.stringify(providerConfig)} for type ${providerType}. Total errors: ${provider.errorCount}`);
if (provider.config.errorCount >= this.maxErrorCount) {
provider.config.isHealthy = false;
console.warn(`[ProviderPoolManager] Marked provider as unhealthy: ${JSON.stringify(providerConfig)} for type ${providerType}. Total errors: ${provider.config.errorCount}`);
} else {
console.warn(`[ProviderPoolManager] Provider ${JSON.stringify(providerConfig)} for type ${providerType} error count: ${provider.errorCount}/${this.maxErrorCount}. Still healthy.`);
console.warn(`[ProviderPoolManager] Provider ${JSON.stringify(providerConfig)} for type ${providerType} error count: ${provider.config.errorCount}/${this.maxErrorCount}. Still healthy.`);
}
this._saveProviderPoolsToJson(providerType); // Persist changes
}
}
}
@ -105,11 +116,13 @@ export class ProviderPoolManager {
markProviderHealthy(providerType, providerConfig) {
const pool = this.providerStatus[providerType];
if (pool) {
const provider = pool.find(p => p.uuid === providerConfig.uuid);a
const provider = pool.find(p => p.uuid === providerConfig.uuid);
if (provider) {
provider.isHealthy = true;
provider.errorCount = 0; // Reset error count on health recovery
provider.config.isHealthy = true;
provider.config.errorCount = 0; // Reset error count on health recovery
provider.config.lastErrorTime = null; // Reset lastErrorTime when healthy
console.log(`[ProviderPoolManager] Marked provider as healthy: ${JSON.stringify(providerConfig)} for type ${providerType}`);
this._saveProviderPoolsToJson(providerType); // Persist changes
}
}
}
@ -126,8 +139,8 @@ export class ProviderPoolManager {
const providerConfig = providerStatus.config;
// Only attempt to health check unhealthy providers after a certain interval
if (!providerStatus.isHealthy && providerStatus.lastErrorTime &&
(now.getTime() - providerStatus.lastErrorTime.getTime() < this.healthCheckInterval)) {
if (!providerStatus.config.isHealthy && providerStatus.config.lastErrorTime &&
(now.getTime() - providerStatus.config.lastErrorTime.getTime() < this.healthCheckInterval)) {
console.log(`[ProviderPoolManager] Skipping health check for ${JSON.stringify(providerConfig)} (${providerType}). Last error too recent.`);
continue;
}
@ -137,7 +150,7 @@ export class ProviderPoolManager {
// For now, if a provider was unhealthy and enough time has passed,
// we optimistically mark it healthy and reset error count.
// A more robust system would involve actual API calls or pings here.
if (!providerStatus.isHealthy) {
if (!providerStatus.config.isHealthy) {
// Only reset and mark healthy if it was unhealthy and we are attempting a check after interval
this.markProviderHealthy(providerType, providerConfig);
console.log(`[ProviderPoolManager] Health check for ${JSON.stringify(providerConfig)} (${providerType}): Marked Healthy (re-evaluation)`);
@ -154,4 +167,44 @@ export class ProviderPoolManager {
}
}
}
/**
* Saves the current provider pools configuration to the JSON file.
* @private
*/
async _saveProviderPoolsToJson(providerTypeToUpdate) {
try {
const filePath = 'provider_pools.json';
let currentPools = {};
try {
const fileContent = await fs.promises.readFile(filePath, 'utf8');
currentPools = JSON.parse(fileContent);
} catch (readError) {
if (readError.code === 'ENOENT') {
console.log('[ProviderPoolManager] provider_pools.json does not exist, creating new file.');
} else {
throw readError;
}
}
if (this.providerStatus[providerTypeToUpdate]) {
currentPools[providerTypeToUpdate] = this.providerStatus[providerTypeToUpdate].map(p => {
if (p.config.lastUsed instanceof Date) {
p.config.lastUsed = p.config.lastUsed.toISOString();
}
if (p.config.lastErrorTime instanceof Date) {
p.config.lastErrorTime = p.config.lastErrorTime.toISOString();
}
return p.config;
});
} else {
console.warn(`[ProviderPoolManager] Attempted to save unknown providerType: ${providerTypeToUpdate}`);
}
await fs.promises.writeFile(filePath, JSON.stringify(currentPools, null, 2), 'utf8');
console.log(`[ProviderPoolManager] provider_pools.json for ${providerTypeToUpdate} updated successfully.`);
} catch (error) {
console.error('[ProviderPoolManager] Failed to write provider_pools.json:', error);
}
}
}