locai 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +928 -0
- package/dist/cli.js +718 -0
- package/dist/commands/review.js +357 -0
- package/dist/commands/translator.js +29 -0
- package/dist/config/index.js +52 -0
- package/dist/core/adapters/base-adapter.js +1 -0
- package/dist/core/adapters/factory.js +44 -0
- package/dist/core/adapters/json-adapter.js +20 -0
- package/dist/core/adapters/po-adapter.js +50 -0
- package/dist/core/adapters/properties-adapter.js +31 -0
- package/dist/core/adapters/yaml-adapter.js +24 -0
- package/dist/core/context-processor.js +247 -0
- package/dist/core/detection/framework-detector.js +56 -0
- package/dist/core/detection/structure-detector.js +30 -0
- package/dist/core/fallback-provider.js +442 -0
- package/dist/core/orchestrator.js +438 -0
- package/dist/core/pipeline/Pipeline.js +48 -0
- package/dist/core/pipeline/PipelineStep.js +13 -0
- package/dist/core/pipeline/context.js +21 -0
- package/dist/core/pipeline/steps/CacheReadStep.js +80 -0
- package/dist/core/pipeline/steps/CacheWriteStep.js +32 -0
- package/dist/core/pipeline/steps/ConfidenceCheckStep.js +67 -0
- package/dist/core/pipeline/steps/GlossaryPostStep.js +17 -0
- package/dist/core/pipeline/steps/GlossaryPreStep.js +20 -0
- package/dist/core/pipeline/steps/InputValidationStep.js +26 -0
- package/dist/core/pipeline/steps/QualityCheckStep.js +48 -0
- package/dist/core/pipeline/steps/ToneCheckStep.js +58 -0
- package/dist/core/pipeline/steps/TranslationStep.js +65 -0
- package/dist/core/pipeline/steps/VectorCacheReadStep.js +84 -0
- package/dist/core/pipeline/steps/VectorCacheWriteStep.js +42 -0
- package/dist/core/pipeline/steps/__tests__/ToneCheckStep.test.js +100 -0
- package/dist/core/provider-factory.js +139 -0
- package/dist/providers/base-provider.js +230 -0
- package/dist/providers/dashscope.js +128 -0
- package/dist/providers/deepseek.js +111 -0
- package/dist/providers/gemini.js +135 -0
- package/dist/providers/openai.js +203 -0
- package/dist/providers/xai.js +111 -0
- package/dist/services/embedding-provider.js +50 -0
- package/dist/services/translation-service.js +609 -0
- package/dist/services/vector-store.js +79 -0
- package/dist/utils/ai-context-analyzer.js +460 -0
- package/dist/utils/confidence-scorer.js +277 -0
- package/dist/utils/error-helper.js +386 -0
- package/dist/utils/file-manager.js +357 -0
- package/dist/utils/glossary-manager.js +194 -0
- package/dist/utils/graceful-shutdown.js +118 -0
- package/dist/utils/input-validator.js +663 -0
- package/dist/utils/logger.js +374 -0
- package/dist/utils/object-transformer.js +177 -0
- package/dist/utils/progress-tracker.js +277 -0
- package/dist/utils/prompt-templates.js +415 -0
- package/dist/utils/quality/__tests__/tone-verifier.test.js +91 -0
- package/dist/utils/quality/base-checker.js +73 -0
- package/dist/utils/quality/code-block-checker.js +214 -0
- package/dist/utils/quality/html-tag-checker.js +64 -0
- package/dist/utils/quality/index.js +220 -0
- package/dist/utils/quality/length-checker.js +140 -0
- package/dist/utils/quality/markdown-checker.js +220 -0
- package/dist/utils/quality/placeholder-checker.js +142 -0
- package/dist/utils/quality/punctuation-checker.js +50 -0
- package/dist/utils/quality/quote-balance-checker.js +143 -0
- package/dist/utils/quality/special-characters-checker.js +229 -0
- package/dist/utils/quality/style-guide-checker.js +192 -0
- package/dist/utils/quality/text-sanitizer.js +120 -0
- package/dist/utils/quality/tone-verifier.js +91 -0
- package/dist/utils/rate-limiter.js +398 -0
- package/dist/utils/retry-helper.js +166 -0
- package/dist/utils/state-manager.js +162 -0
- package/dist/utils/statistics-manager.js +51 -0
- package/dist/utils/ui-manager.js +98 -0
- package/package.json +80 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import RetryHelper from "../utils/retry-helper.js";
|
|
2
|
+
import rateLimiter from "../utils/rate-limiter.js";
|
|
3
|
+
class FallbackProvider {
|
|
4
|
+
providers;
|
|
5
|
+
currentIndex;
|
|
6
|
+
providerStats;
|
|
7
|
+
maxRetries;
|
|
8
|
+
reRankInterval;
|
|
9
|
+
operationCount;
|
|
10
|
+
lastErrorTime;
|
|
11
|
+
consecutiveErrors;
|
|
12
|
+
constructor(providers) {
|
|
13
|
+
this.providers = providers;
|
|
14
|
+
this.currentIndex = 0;
|
|
15
|
+
this.providerStats = new Map();
|
|
16
|
+
this.maxRetries = 2;
|
|
17
|
+
this.reRankInterval = 10;
|
|
18
|
+
this.operationCount = 0;
|
|
19
|
+
this.lastErrorTime = null;
|
|
20
|
+
this.consecutiveErrors = 0;
|
|
21
|
+
this.providers.forEach((provider) => {
|
|
22
|
+
const providerName = this._getProviderName(provider);
|
|
23
|
+
this.providerStats.set(providerName, {
|
|
24
|
+
success: 0,
|
|
25
|
+
failure: 0,
|
|
26
|
+
avgResponseTime: 0,
|
|
27
|
+
totalTime: 0,
|
|
28
|
+
lastSuccess: null,
|
|
29
|
+
consecutiveFailures: 0,
|
|
30
|
+
lastError: null,
|
|
31
|
+
disabled: false,
|
|
32
|
+
disabledUntil: null,
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
_calculatePriority(text) {
|
|
37
|
+
if (!text)
|
|
38
|
+
return 1;
|
|
39
|
+
if (text.length < 100)
|
|
40
|
+
return 2;
|
|
41
|
+
if (text.length > 800)
|
|
42
|
+
return 0;
|
|
43
|
+
return 1;
|
|
44
|
+
}
|
|
45
|
+
async translate(text, sourceLang, targetLang, options = {}) {
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
47
|
+
const errors = [];
|
|
48
|
+
const startTime = Date.now();
|
|
49
|
+
const startIndex = this.currentIndex;
|
|
50
|
+
let currentAttempt = 0;
|
|
51
|
+
this._checkAndReRankProviders();
|
|
52
|
+
const availableProviders = this.providers.filter((_, index) => !this._isProviderDisabled(this.providers[index]));
|
|
53
|
+
if (availableProviders.length === 0) {
|
|
54
|
+
this._resetDisabledProviders();
|
|
55
|
+
availableProviders.push(...this.providers);
|
|
56
|
+
}
|
|
57
|
+
const totalProviders = availableProviders.length;
|
|
58
|
+
const maxAttempts = totalProviders * (this.maxRetries + 1);
|
|
59
|
+
while (currentAttempt < maxAttempts) {
|
|
60
|
+
const currentProviderIndex = this.currentIndex % totalProviders;
|
|
61
|
+
const providerData = availableProviders[currentProviderIndex];
|
|
62
|
+
const providerName = this._getProviderName(providerData);
|
|
63
|
+
const currentProvider = providerData.implementation;
|
|
64
|
+
this.operationCount++;
|
|
65
|
+
try {
|
|
66
|
+
if (!this.providerStats.has(providerName)) {
|
|
67
|
+
this.providerStats.set(providerName, {
|
|
68
|
+
success: 0,
|
|
69
|
+
failure: 0,
|
|
70
|
+
avgResponseTime: 0,
|
|
71
|
+
totalTime: 0,
|
|
72
|
+
lastSuccess: null,
|
|
73
|
+
consecutiveFailures: 0,
|
|
74
|
+
lastError: null,
|
|
75
|
+
disabled: false,
|
|
76
|
+
disabledUntil: null,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
const providerStartTime = Date.now();
|
|
80
|
+
const result = await rateLimiter.enqueue(providerName.toLowerCase(), () => RetryHelper.withRetry(() => currentProvider.translate(text, sourceLang, targetLang, options), {
|
|
81
|
+
maxRetries: 0,
|
|
82
|
+
context: `Fallback:${providerName}`,
|
|
83
|
+
logContext: {
|
|
84
|
+
source: sourceLang,
|
|
85
|
+
target: targetLang,
|
|
86
|
+
providerIndex: currentProviderIndex,
|
|
87
|
+
attempt: currentAttempt + 1,
|
|
88
|
+
maxAttempts,
|
|
89
|
+
},
|
|
90
|
+
}), this._calculatePriority(text));
|
|
91
|
+
const responseTime = Date.now() - providerStartTime;
|
|
92
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
93
|
+
const stats = this.providerStats.get(providerName);
|
|
94
|
+
stats.success++;
|
|
95
|
+
stats.consecutiveFailures = 0;
|
|
96
|
+
stats.lastSuccess = new Date();
|
|
97
|
+
const prevTotal = stats.avgResponseTime * (stats.success + stats.failure - 1);
|
|
98
|
+
stats.totalTime = prevTotal + responseTime;
|
|
99
|
+
stats.avgResponseTime = stats.totalTime / (stats.success + stats.failure);
|
|
100
|
+
this.consecutiveErrors = 0;
|
|
101
|
+
this.lastErrorTime = null;
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (this.providerStats.has(providerName)) {
|
|
106
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
107
|
+
const stats = this.providerStats.get(providerName);
|
|
108
|
+
stats.failure++;
|
|
109
|
+
stats.consecutiveFailures++;
|
|
110
|
+
stats.lastError = {
|
|
111
|
+
time: new Date(),
|
|
112
|
+
message: error.message,
|
|
113
|
+
};
|
|
114
|
+
if (stats.consecutiveFailures >= 5) {
|
|
115
|
+
this._disableProvider(providerData, 2 * 60 * 1000);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
this.consecutiveErrors++;
|
|
119
|
+
this.lastErrorTime = Date.now();
|
|
120
|
+
errors.push({
|
|
121
|
+
provider: providerName,
|
|
122
|
+
error: error.message,
|
|
123
|
+
attempt: currentAttempt + 1,
|
|
124
|
+
});
|
|
125
|
+
const safeProviderName = `Provider_${(currentProviderIndex % totalProviders) + 1}`;
|
|
126
|
+
const safeErrorMessage = error.message.includes("API") || error.message.includes("key")
|
|
127
|
+
? "Authentication or API error"
|
|
128
|
+
: error.message.substring(0, 100);
|
|
129
|
+
console.warn(`${safeProviderName} failed (attempt ${currentAttempt + 1}/${maxAttempts}): ${safeErrorMessage}`);
|
|
130
|
+
currentAttempt++;
|
|
131
|
+
this.currentIndex++;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
this.currentIndex = startIndex;
|
|
135
|
+
throw new Error(`All providers failed after ${maxAttempts} attempts (${Date.now() - startTime}ms):\n${JSON.stringify(errors, null, 2)}`);
|
|
136
|
+
}
|
|
137
|
+
async analyze(prompt, options = {}) {
|
|
138
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
139
|
+
const errors = [];
|
|
140
|
+
const startTime = Date.now();
|
|
141
|
+
const savedIndex = this.currentIndex;
|
|
142
|
+
let currentAttempt = 0;
|
|
143
|
+
this._checkAndReRankProviders();
|
|
144
|
+
const availableProviders = this.providers.filter((provider) => !this._isProviderDisabled(provider) &&
|
|
145
|
+
typeof provider.implementation.analyze === "function");
|
|
146
|
+
if (availableProviders.length === 0) {
|
|
147
|
+
this._resetDisabledProviders();
|
|
148
|
+
availableProviders.push(...this.providers.filter((p) => typeof p.implementation.analyze === "function"));
|
|
149
|
+
}
|
|
150
|
+
const totalProviders = availableProviders.length;
|
|
151
|
+
if (totalProviders === 0) {
|
|
152
|
+
throw new Error("No providers support analysis capability");
|
|
153
|
+
}
|
|
154
|
+
const maxAttempts = totalProviders * (this.maxRetries + 1);
|
|
155
|
+
while (currentAttempt < maxAttempts) {
|
|
156
|
+
const currentProviderIndex = currentAttempt % totalProviders;
|
|
157
|
+
const providerData = availableProviders[currentProviderIndex];
|
|
158
|
+
const providerName = this._getProviderName(providerData);
|
|
159
|
+
const currentProvider = providerData.implementation;
|
|
160
|
+
this.operationCount++;
|
|
161
|
+
try {
|
|
162
|
+
if (!this.providerStats.has(providerName)) {
|
|
163
|
+
this.providerStats.set(providerName, {
|
|
164
|
+
success: 0,
|
|
165
|
+
failure: 0,
|
|
166
|
+
avgResponseTime: 0,
|
|
167
|
+
totalTime: 0,
|
|
168
|
+
lastSuccess: null,
|
|
169
|
+
consecutiveFailures: 0,
|
|
170
|
+
lastError: null,
|
|
171
|
+
disabled: false,
|
|
172
|
+
disabledUntil: null,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
const providerStartTime = Date.now();
|
|
176
|
+
const result = await rateLimiter.enqueue(providerName.toLowerCase(), () => RetryHelper.withRetry(() => currentProvider.analyze
|
|
177
|
+
? currentProvider.analyze(prompt, options)
|
|
178
|
+
: Promise.reject(new Error("Analyze not supported")), {
|
|
179
|
+
maxRetries: 0,
|
|
180
|
+
context: `Fallback:${providerName}`,
|
|
181
|
+
logContext: {
|
|
182
|
+
providerIndex: currentProviderIndex,
|
|
183
|
+
attempt: currentAttempt + 1,
|
|
184
|
+
maxAttempts,
|
|
185
|
+
},
|
|
186
|
+
}), this._calculatePriority(prompt));
|
|
187
|
+
const responseTime = Date.now() - providerStartTime;
|
|
188
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
189
|
+
const stats = this.providerStats.get(providerName);
|
|
190
|
+
stats.success++;
|
|
191
|
+
stats.consecutiveFailures = 0;
|
|
192
|
+
stats.lastSuccess = new Date();
|
|
193
|
+
const prevTotal = stats.avgResponseTime * (stats.success + stats.failure - 1);
|
|
194
|
+
stats.totalTime = prevTotal + responseTime;
|
|
195
|
+
stats.avgResponseTime = stats.totalTime / (stats.success + stats.failure);
|
|
196
|
+
this.consecutiveErrors = 0;
|
|
197
|
+
this.lastErrorTime = null;
|
|
198
|
+
this.currentIndex = savedIndex;
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
if (this.providerStats.has(providerName)) {
|
|
203
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
204
|
+
const stats = this.providerStats.get(providerName);
|
|
205
|
+
stats.failure++;
|
|
206
|
+
stats.consecutiveFailures++;
|
|
207
|
+
stats.lastError = {
|
|
208
|
+
time: new Date(),
|
|
209
|
+
message: error.message,
|
|
210
|
+
};
|
|
211
|
+
if (stats.consecutiveFailures >= 5) {
|
|
212
|
+
this._disableProvider(providerData, 2 * 60 * 1000);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
this.consecutiveErrors++;
|
|
216
|
+
this.lastErrorTime = Date.now();
|
|
217
|
+
errors.push({
|
|
218
|
+
provider: providerName,
|
|
219
|
+
error: error.message,
|
|
220
|
+
attempt: currentAttempt + 1,
|
|
221
|
+
});
|
|
222
|
+
const safeProviderName = `Provider_${(currentProviderIndex % totalProviders) + 1}`;
|
|
223
|
+
const safeErrorMessage = error.message.includes("API") || error.message.includes("key")
|
|
224
|
+
? "Authentication or API error"
|
|
225
|
+
: error.message.substring(0, 100);
|
|
226
|
+
console.warn(`${safeProviderName} failed (attempt ${currentAttempt + 1}/${maxAttempts}): ${safeErrorMessage}`);
|
|
227
|
+
currentAttempt++;
|
|
228
|
+
this.currentIndex++;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
this.currentIndex = savedIndex;
|
|
232
|
+
throw new Error(`All providers failed for analysis after ${maxAttempts} attempts (${Date.now() - startTime}ms):\n${JSON.stringify(errors, null, 2)}`);
|
|
233
|
+
}
|
|
234
|
+
async chat(messages, options = {}) {
|
|
235
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
236
|
+
const errors = [];
|
|
237
|
+
const startTime = Date.now();
|
|
238
|
+
const savedIndex = this.currentIndex;
|
|
239
|
+
let currentAttempt = 0;
|
|
240
|
+
this._checkAndReRankProviders();
|
|
241
|
+
const availableProviders = this.providers.filter((provider) => !this._isProviderDisabled(provider) &&
|
|
242
|
+
typeof provider.implementation.chat === "function");
|
|
243
|
+
if (availableProviders.length === 0) {
|
|
244
|
+
this._resetDisabledProviders();
|
|
245
|
+
availableProviders.push(...this.providers.filter((p) => typeof p.implementation.chat === "function"));
|
|
246
|
+
}
|
|
247
|
+
const totalProviders = availableProviders.length;
|
|
248
|
+
if (totalProviders === 0) {
|
|
249
|
+
throw new Error("No providers support chat capability");
|
|
250
|
+
}
|
|
251
|
+
const maxAttempts = totalProviders * (this.maxRetries + 1);
|
|
252
|
+
while (currentAttempt < maxAttempts) {
|
|
253
|
+
const currentProviderIndex = currentAttempt % totalProviders;
|
|
254
|
+
const providerData = availableProviders[currentProviderIndex];
|
|
255
|
+
const providerName = this._getProviderName(providerData);
|
|
256
|
+
const currentProvider = providerData.implementation;
|
|
257
|
+
this.operationCount++;
|
|
258
|
+
try {
|
|
259
|
+
if (!this.providerStats.has(providerName)) {
|
|
260
|
+
this.providerStats.set(providerName, {
|
|
261
|
+
success: 0,
|
|
262
|
+
failure: 0,
|
|
263
|
+
avgResponseTime: 0,
|
|
264
|
+
totalTime: 0,
|
|
265
|
+
lastSuccess: null,
|
|
266
|
+
consecutiveFailures: 0,
|
|
267
|
+
lastError: null,
|
|
268
|
+
disabled: false,
|
|
269
|
+
disabledUntil: null,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
const providerStartTime = Date.now();
|
|
273
|
+
const result = await rateLimiter.enqueue(providerName.toLowerCase(), () => RetryHelper.withRetry(() => currentProvider.chat
|
|
274
|
+
? currentProvider.chat(messages, options)
|
|
275
|
+
: Promise.reject(new Error("Chat not supported")), {
|
|
276
|
+
maxRetries: 0,
|
|
277
|
+
context: `Fallback:${providerName}`,
|
|
278
|
+
logContext: {
|
|
279
|
+
providerIndex: currentProviderIndex,
|
|
280
|
+
attempt: currentAttempt + 1,
|
|
281
|
+
maxAttempts,
|
|
282
|
+
},
|
|
283
|
+
}), 1 // Priority 1 (normal)
|
|
284
|
+
);
|
|
285
|
+
const responseTime = Date.now() - providerStartTime;
|
|
286
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
287
|
+
const stats = this.providerStats.get(providerName);
|
|
288
|
+
stats.success++;
|
|
289
|
+
stats.consecutiveFailures = 0;
|
|
290
|
+
stats.lastSuccess = new Date();
|
|
291
|
+
const prevTotal = stats.avgResponseTime * (stats.success + stats.failure - 1);
|
|
292
|
+
stats.totalTime = prevTotal + responseTime;
|
|
293
|
+
stats.avgResponseTime = stats.totalTime / (stats.success + stats.failure);
|
|
294
|
+
this.consecutiveErrors = 0;
|
|
295
|
+
this.lastErrorTime = null;
|
|
296
|
+
this.currentIndex = savedIndex;
|
|
297
|
+
return result;
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
if (this.providerStats.has(providerName)) {
|
|
301
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
302
|
+
const stats = this.providerStats.get(providerName);
|
|
303
|
+
stats.failure++;
|
|
304
|
+
stats.consecutiveFailures++;
|
|
305
|
+
stats.lastError = {
|
|
306
|
+
time: new Date(),
|
|
307
|
+
message: error.message,
|
|
308
|
+
};
|
|
309
|
+
if (stats.consecutiveFailures >= 5) {
|
|
310
|
+
this._disableProvider(providerData, 2 * 60 * 1000);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
this.consecutiveErrors++;
|
|
314
|
+
this.lastErrorTime = Date.now();
|
|
315
|
+
errors.push({
|
|
316
|
+
provider: providerName,
|
|
317
|
+
error: error.message,
|
|
318
|
+
attempt: currentAttempt + 1,
|
|
319
|
+
});
|
|
320
|
+
const safeProviderName = `Provider_${(currentProviderIndex % totalProviders) + 1}`;
|
|
321
|
+
const safeErrorMessage = error.message.includes("API") || error.message.includes("key")
|
|
322
|
+
? "Authentication or API error"
|
|
323
|
+
: error.message.substring(0, 100);
|
|
324
|
+
console.warn(`${safeProviderName} failed (attempt ${currentAttempt + 1}/${maxAttempts}): ${safeErrorMessage}`);
|
|
325
|
+
currentAttempt++;
|
|
326
|
+
this.currentIndex++;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
this.currentIndex = savedIndex;
|
|
330
|
+
throw new Error(`All providers failed for chat after ${maxAttempts} attempts (${Date.now() - startTime}ms):\n${JSON.stringify(errors, null, 2)}`);
|
|
331
|
+
}
|
|
332
|
+
getStats() {
|
|
333
|
+
const stats = Object.fromEntries(this.providerStats);
|
|
334
|
+
Object.keys(stats).forEach((provider) => {
|
|
335
|
+
const providerStats = stats[provider];
|
|
336
|
+
const total = providerStats.success + providerStats.failure;
|
|
337
|
+
providerStats.successRate = total > 0 ? providerStats.success / total : 0;
|
|
338
|
+
providerStats.totalCalls = total;
|
|
339
|
+
providerStats.avgResponseTimeMs = Math.round(providerStats.avgResponseTime);
|
|
340
|
+
const isDisabled = this._isProviderDisabled(this.providers.find((p) => this._getProviderName(p) === provider));
|
|
341
|
+
providerStats.isDisabled = isDisabled;
|
|
342
|
+
if (isDisabled && providerStats.disabledUntil) {
|
|
343
|
+
providerStats.enablesInMs = Math.max(0, providerStats.disabledUntil - Date.now());
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
return stats;
|
|
347
|
+
}
|
|
348
|
+
reset() {
|
|
349
|
+
this.currentIndex = 0;
|
|
350
|
+
this._resetDisabledProviders();
|
|
351
|
+
}
|
|
352
|
+
resetStats() {
|
|
353
|
+
this.providerStats.clear();
|
|
354
|
+
this.providers.forEach((provider) => {
|
|
355
|
+
const providerName = this._getProviderName(provider);
|
|
356
|
+
this.providerStats.set(providerName, {
|
|
357
|
+
success: 0,
|
|
358
|
+
failure: 0,
|
|
359
|
+
avgResponseTime: 0,
|
|
360
|
+
totalTime: 0,
|
|
361
|
+
lastSuccess: null,
|
|
362
|
+
consecutiveFailures: 0,
|
|
363
|
+
lastError: null,
|
|
364
|
+
disabled: false,
|
|
365
|
+
disabledUntil: null,
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
this.operationCount = 0;
|
|
369
|
+
this.consecutiveErrors = 0;
|
|
370
|
+
this.lastErrorTime = null;
|
|
371
|
+
}
|
|
372
|
+
_checkAndReRankProviders() {
|
|
373
|
+
if (this.operationCount % this.reRankInterval !== 0) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const providerRanks = this.providers.map((provider, index) => {
|
|
377
|
+
const name = this._getProviderName(provider);
|
|
378
|
+
const stats = this.providerStats.get(name) || {
|
|
379
|
+
success: 0,
|
|
380
|
+
failure: 0,
|
|
381
|
+
avgResponseTime: 0,
|
|
382
|
+
consecutiveFailures: 0,
|
|
383
|
+
};
|
|
384
|
+
const total = stats.success + stats.failure;
|
|
385
|
+
let score = 0;
|
|
386
|
+
if (total > 0) {
|
|
387
|
+
const successRate = stats.success / total;
|
|
388
|
+
const responseTimePenalty = stats.avgResponseTime > 0 ? Math.min(0.3, stats.avgResponseTime / 5000) : 0;
|
|
389
|
+
score = successRate * (1 - responseTimePenalty);
|
|
390
|
+
if (stats.consecutiveFailures > 0) {
|
|
391
|
+
score -= Math.min(0.5, stats.consecutiveFailures * 0.1);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return { provider, index, score };
|
|
395
|
+
});
|
|
396
|
+
const hasEnoughData = providerRanks.some((p) => (this.providerStats.get(this._getProviderName(p.provider))?.success || 0) > 2);
|
|
397
|
+
if (!hasEnoughData) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
providerRanks.sort((a, b) => b.score - a.score);
|
|
401
|
+
this.providers = providerRanks.map((p) => p.provider);
|
|
402
|
+
this.currentIndex = 0;
|
|
403
|
+
}
|
|
404
|
+
_disableProvider(provider, timeoutMs = 2 * 60 * 1000) {
|
|
405
|
+
const providerName = this._getProviderName(provider);
|
|
406
|
+
const stats = this.providerStats.get(providerName);
|
|
407
|
+
if (stats) {
|
|
408
|
+
stats.disabled = true;
|
|
409
|
+
stats.disabledUntil = Date.now() + timeoutMs;
|
|
410
|
+
setTimeout(() => {
|
|
411
|
+
stats.disabled = false;
|
|
412
|
+
stats.disabledUntil = null;
|
|
413
|
+
console.log(`Re-enabled provider: ${providerName}`);
|
|
414
|
+
}, timeoutMs);
|
|
415
|
+
console.log(`Temporarily disabled provider ${providerName} for ${timeoutMs / 1000}s due to failures`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
_isProviderDisabled(provider) {
|
|
419
|
+
if (!provider)
|
|
420
|
+
return true;
|
|
421
|
+
const providerName = this._getProviderName(provider);
|
|
422
|
+
const stats = this.providerStats.get(providerName);
|
|
423
|
+
if (!stats)
|
|
424
|
+
return false;
|
|
425
|
+
if (stats.disabled && stats.disabledUntil && Date.now() > stats.disabledUntil) {
|
|
426
|
+
stats.disabled = false;
|
|
427
|
+
stats.disabledUntil = null;
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
return stats.disabled === true;
|
|
431
|
+
}
|
|
432
|
+
_resetDisabledProviders() {
|
|
433
|
+
this.providerStats.forEach((stats) => {
|
|
434
|
+
stats.disabled = false;
|
|
435
|
+
stats.disabledUntil = null;
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
_getProviderName(provider) {
|
|
439
|
+
return provider?.name || "Unknown";
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
export default FallbackProvider;
|