Update subtitle.ts
Browse files- subtitle.ts +77 -16
subtitle.ts
CHANGED
|
@@ -43,7 +43,7 @@ const config = {
|
|
| 43 |
url: Deno.env.get("OPENAI_API_URL_3") || "https://translate.doi9.top",
|
| 44 |
apiKey: Deno.env.get("OPENAI_API_KEY_3") || "123",
|
| 45 |
model: Deno.env.get("OPENAI_MODEL_3") || "gpt-4.1-mini",
|
| 46 |
-
maxLinesPerRequest:
|
| 47 |
maxWorkers: 3,
|
| 48 |
minInterval: 300
|
| 49 |
},
|
|
@@ -52,7 +52,7 @@ const config = {
|
|
| 52 |
url: Deno.env.get("OPENAI_API_URL_2") || "https://tbai.xin",
|
| 53 |
apiKey: Deno.env.get("OPENAI_API_KEY_2") || "sk-8Pgi3KczqqCo4Hg0XJjDelRFnRHSII6nKTJMpCjWdGVfJJVA",
|
| 54 |
model: Deno.env.get("OPENAI_MODEL_2") || "gpt-4.1-nano",
|
| 55 |
-
maxLinesPerRequest:
|
| 56 |
maxWorkers: 3,
|
| 57 |
minInterval: 300
|
| 58 |
},
|
|
@@ -121,6 +121,15 @@ interface SubtitleEntry {
|
|
| 121 |
originalSubtitles?: SubtitleEntry[];
|
| 122 |
}
|
| 123 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
class AdaptiveRateLimiter {
|
| 125 |
private queue: Array<{
|
| 126 |
fn: () => Promise<any>,
|
|
@@ -374,7 +383,7 @@ class ChatGPT_WorkerPool {
|
|
| 374 |
state.runningWorkers++;
|
| 375 |
state.lastRunTime = now;
|
| 376 |
const { task, resolve, reject } = state.queue.shift()!;
|
| 377 |
-
console.log(`[WorkerPool] 🚀 [${providerConfig.name}]
|
| 378 |
|
| 379 |
this.singleTranslationTask(task, 0, providerIndex)
|
| 380 |
.then(resolve)
|
|
@@ -471,13 +480,13 @@ class ChatGPT_WorkerPool {
|
|
| 471 |
console.log(`[WorkerPool-Cache] HIT for task ${task.subtitleId}`);
|
| 472 |
return cachedResult;
|
| 473 |
}
|
| 474 |
-
console.log(`[WorkerPool-Cache] MISS for task ${task.subtitleId}`);
|
| 475 |
} catch (cacheError) {
|
| 476 |
console.warn(`[WorkerPool-Cache] Cache check failed: ${cacheError.message}`);
|
| 477 |
}
|
| 478 |
|
| 479 |
let systemPrompt = '';
|
| 480 |
-
if (provider.model.includes('glm-')) {
|
| 481 |
systemPrompt = `You are a text translation API. Your task is to translate the user's text from ${task.sourceLanguage} to ${task.targetLanguage}.
|
| 482 |
RULES:
|
| 483 |
1. Translate the text inside the square brackets for each numbered item.
|
|
@@ -515,7 +524,7 @@ Output:
|
|
| 515 |
task.signal.addEventListener('abort', onAbort, { once: true });
|
| 516 |
|
| 517 |
try {
|
| 518 |
-
console.log(`[Worker]
|
| 519 |
|
| 520 |
if (task.signal.aborted) throw new Error("Aborted before fetch");
|
| 521 |
|
|
@@ -538,14 +547,28 @@ Output:
|
|
| 538 |
task.signal.removeEventListener('abort', onAbort);
|
| 539 |
|
| 540 |
if (!response.ok) {
|
| 541 |
-
if ([429, 500, 502, 503, 504].includes(response.status)) {
|
| 542 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 543 |
}
|
|
|
|
|
|
|
| 544 |
const errorBody = await response.text();
|
| 545 |
-
throw new
|
|
|
|
| 546 |
}
|
| 547 |
|
| 548 |
const result = await response.json();
|
|
|
|
| 549 |
const translation = result.choices[0]?.message?.content?.trim();
|
| 550 |
|
| 551 |
// === 缓存成功结果 ===
|
|
@@ -554,7 +577,7 @@ Output:
|
|
| 554 |
const textHash = await sha256(task.text.trim());
|
| 555 |
const cacheKey = `chatgpt-worker-${task.sourceLanguage}-${task.targetLanguage}-${textHash}`;
|
| 556 |
translationCache.set(cacheKey, translation);
|
| 557 |
-
console.log(`[WorkerPool-Cache] STORED result for task ${task.subtitleId}`);
|
| 558 |
} catch (cacheError) {
|
| 559 |
console.warn(`[WorkerPool-Cache] Failed to cache result: ${cacheError.message}`);
|
| 560 |
}
|
|
@@ -569,14 +592,35 @@ Output:
|
|
| 569 |
throw error;
|
| 570 |
}
|
| 571 |
|
| 572 |
-
const statusCode = parseInt(error.message.match(/API Error (\d{3})/)?.[1] || '0');
|
| 573 |
-
const isRetryableServerError = [429, 500, 502, 503, 504].includes(statusCode);
|
| 574 |
-
const isFatalApiClientError = statusCode >= 400 && statusCode < 500 && !isRetryableServerError;
|
| 575 |
-
const isNetworkOrTimeoutError = error.message.includes("timed out") || (error instanceof TypeError && error.message.includes("fetch"));
|
| 576 |
-
const isEmptyResponseError = error.message === "EMPTY_RESPONSE";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 577 |
|
| 578 |
// 分支 1: 可重试错误
|
| 579 |
if ((isRetryableServerError || isNetworkOrTimeoutError || isEmptyResponseError) && retryCount < MAX_RETRIES) {
|
|
|
|
|
|
|
| 580 |
const delay = Math.pow(2, retryCount) * 1500 + Math.random() * 1000;
|
| 581 |
console.warn(`[Worker] Retrying on ${provider.name} for ID: ${task.subtitleId} (Attempt ${retryCount + 2}) after ${delay.toFixed(0)}ms. Reason: ${error.message}`);
|
| 582 |
await new Promise(r => setTimeout(r, delay));
|
|
@@ -777,11 +821,28 @@ async function processSingleBatchWithSmartFallback(
|
|
| 777 |
const formatRegex = /^\s*\d+\.\s*\[.*]\s*$/;
|
| 778 |
if (!translatedParts.every(p => formatRegex.test(p.trim()))) {
|
| 779 |
console.warn(`[SmartFallback] Format error detected in ${batchId}.`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 780 |
throw new Error("Format error detected.");
|
| 781 |
}
|
| 782 |
|
| 783 |
// --- 4. 如果所有检查都通过,就是成功! ---
|
| 784 |
-
console.log(`[SmartFallback] ✅
|
| 785 |
return batch.map((original, index) => ({
|
| 786 |
...original,
|
| 787 |
translatedText: translatedParts[index].replace(/^\d+\.\s*\[?/, '').replace(/]?\s*$/, '').trim(),
|
|
|
|
| 43 |
url: Deno.env.get("OPENAI_API_URL_3") || "https://translate.doi9.top",
|
| 44 |
apiKey: Deno.env.get("OPENAI_API_KEY_3") || "123",
|
| 45 |
model: Deno.env.get("OPENAI_MODEL_3") || "gpt-4.1-mini",
|
| 46 |
+
maxLinesPerRequest: 50,
|
| 47 |
maxWorkers: 3,
|
| 48 |
minInterval: 300
|
| 49 |
},
|
|
|
|
| 52 |
url: Deno.env.get("OPENAI_API_URL_2") || "https://tbai.xin",
|
| 53 |
apiKey: Deno.env.get("OPENAI_API_KEY_2") || "sk-8Pgi3KczqqCo4Hg0XJjDelRFnRHSII6nKTJMpCjWdGVfJJVA",
|
| 54 |
model: Deno.env.get("OPENAI_MODEL_2") || "gpt-4.1-nano",
|
| 55 |
+
maxLinesPerRequest: 50,
|
| 56 |
maxWorkers: 3,
|
| 57 |
minInterval: 300
|
| 58 |
},
|
|
|
|
| 121 |
originalSubtitles?: SubtitleEntry[];
|
| 122 |
}
|
| 123 |
|
| 124 |
+
class ApiError extends Error {
|
| 125 |
+
constructor(message, status, isRetryable = false) {
|
| 126 |
+
super(message);
|
| 127 |
+
this.name = 'ApiError';
|
| 128 |
+
this.status = status; // 存储原始状态码
|
| 129 |
+
this.isRetryable = isRetryable; // 明确标记是否可重试
|
| 130 |
+
}
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
class AdaptiveRateLimiter {
|
| 134 |
private queue: Array<{
|
| 135 |
fn: () => Promise<any>,
|
|
|
|
| 383 |
state.runningWorkers++;
|
| 384 |
state.lastRunTime = now;
|
| 385 |
const { task, resolve, reject } = state.queue.shift()!;
|
| 386 |
+
console.log(`[WorkerPool] 🚀 [${providerConfig.name}] 翻译任务启动. (正在运行: ${state.runningWorkers}, 队列剩余: ${state.queue.length})`);
|
| 387 |
|
| 388 |
this.singleTranslationTask(task, 0, providerIndex)
|
| 389 |
.then(resolve)
|
|
|
|
| 480 |
console.log(`[WorkerPool-Cache] HIT for task ${task.subtitleId}`);
|
| 481 |
return cachedResult;
|
| 482 |
}
|
| 483 |
+
// console.log(`[WorkerPool-Cache] MISS for task ${task.subtitleId}`);
|
| 484 |
} catch (cacheError) {
|
| 485 |
console.warn(`[WorkerPool-Cache] Cache check failed: ${cacheError.message}`);
|
| 486 |
}
|
| 487 |
|
| 488 |
let systemPrompt = '';
|
| 489 |
+
if (provider.model.toLowerCase().includes('glm-')) {
|
| 490 |
systemPrompt = `You are a text translation API. Your task is to translate the user's text from ${task.sourceLanguage} to ${task.targetLanguage}.
|
| 491 |
RULES:
|
| 492 |
1. Translate the text inside the square brackets for each numbered item.
|
|
|
|
| 524 |
task.signal.addEventListener('abort', onAbort, { once: true });
|
| 525 |
|
| 526 |
try {
|
| 527 |
+
console.log(`[Worker] 第 #${retryCount + 1} 次尝试, ID: ${task.subtitleId} 使用 [${provider.name}] 翻译。`);
|
| 528 |
|
| 529 |
if (task.signal.aborted) throw new Error("Aborted before fetch");
|
| 530 |
|
|
|
|
| 547 |
task.signal.removeEventListener('abort', onAbort);
|
| 548 |
|
| 549 |
if (!response.ok) {
|
| 550 |
+
// if ([429, 500, 502, 503, 504].includes(response.status)) {
|
| 551 |
+
// throw new Error("RETRYABLE_PROXY_ERROR");
|
| 552 |
+
// }
|
| 553 |
+
// const errorBody = await response.text();
|
| 554 |
+
// throw new Error(`API Error: ${response.status}, Body: ${errorBody.substring(0, 100)}`);
|
| 555 |
+
|
| 556 |
+
const status = response.status;
|
| 557 |
+
const retryableStatusCodes = [429, 500, 502, 503, 504];
|
| 558 |
+
|
| 559 |
+
if (retryableStatusCodes.includes(status)) {
|
| 560 |
+
// 对于可重试错误,将 isRetryable 设为 true
|
| 561 |
+
throw new ApiError(`Retryable API Error: ${status}`, status, true);
|
| 562 |
}
|
| 563 |
+
|
| 564 |
+
// 对于其他 API 错误
|
| 565 |
const errorBody = await response.text();
|
| 566 |
+
throw new ApiError(`API Error: ${status}, Body: ${errorBody.substring(0, 100)}`, status, false);
|
| 567 |
+
|
| 568 |
}
|
| 569 |
|
| 570 |
const result = await response.json();
|
| 571 |
+
// console.log("完整响应结构:", JSON.stringify(result, null, 2));
|
| 572 |
const translation = result.choices[0]?.message?.content?.trim();
|
| 573 |
|
| 574 |
// === 缓存成功结果 ===
|
|
|
|
| 577 |
const textHash = await sha256(task.text.trim());
|
| 578 |
const cacheKey = `chatgpt-worker-${task.sourceLanguage}-${task.targetLanguage}-${textHash}`;
|
| 579 |
translationCache.set(cacheKey, translation);
|
| 580 |
+
// console.log(`[WorkerPool-Cache] STORED result for task ${task.subtitleId}`);
|
| 581 |
} catch (cacheError) {
|
| 582 |
console.warn(`[WorkerPool-Cache] Failed to cache result: ${cacheError.message}`);
|
| 583 |
}
|
|
|
|
| 592 |
throw error;
|
| 593 |
}
|
| 594 |
|
| 595 |
+
// const statusCode = parseInt(error.message.match(/API Error (\d{3})/)?.[1] || '0');
|
| 596 |
+
// const isRetryableServerError = [429, 500, 502, 503, 504].includes(statusCode);
|
| 597 |
+
// const isFatalApiClientError = statusCode >= 400 && statusCode < 500 && !isRetryableServerError;
|
| 598 |
+
// const isNetworkOrTimeoutError = error.message.includes("timed out") || (error instanceof TypeError && error.message.includes("fetch"));
|
| 599 |
+
// const isEmptyResponseError = error.message === "EMPTY_RESPONSE";
|
| 600 |
+
|
| 601 |
+
let statusCode = 0;
|
| 602 |
+
let isRetryableServerError = false;
|
| 603 |
+
let isFatalApiClientError = false;
|
| 604 |
+
let isNetworkOrTimeoutError = false;
|
| 605 |
+
let isEmptyResponseError = false;
|
| 606 |
+
|
| 607 |
+
// 使用 instanceof 进行清晰、可靠的错误类型判断
|
| 608 |
+
if (error instanceof ApiError) {
|
| 609 |
+
// 这是我们定义的 API 错误,直接从属性读取信息,不再解析字符串!
|
| 610 |
+
statusCode = error.status;
|
| 611 |
+
isRetryableServerError = error.isRetryable;
|
| 612 |
+
isFatalApiClientError = !isRetryableServerError && statusCode >= 400 && statusCode < 500;
|
| 613 |
+
} else {
|
| 614 |
+
// 这是其他类型的错误(网络、超时、代码bug等)
|
| 615 |
+
// 保持原有的字符串检查逻辑来分类这些非 API 错误
|
| 616 |
+
isNetworkOrTimeoutError = error.message.includes("timed out") || (error instanceof TypeError && error.message.includes("fetch"));
|
| 617 |
+
isEmptyResponseError = error.message === "EMPTY_RESPONSE";
|
| 618 |
+
}
|
| 619 |
|
| 620 |
// 分支 1: 可重试错误
|
| 621 |
if ((isRetryableServerError || isNetworkOrTimeoutError || isEmptyResponseError) && retryCount < MAX_RETRIES) {
|
| 622 |
+
// if ((error.message === "RETRYABLE_PROXY_ERROR" || error.message === "EMPTY_RESPONSE" || error.message.includes("timed out")||
|
| 623 |
+
// (error instanceof TypeError && error.message.includes("fetch"))) && retryCount < MAX_RETRIES) {
|
| 624 |
const delay = Math.pow(2, retryCount) * 1500 + Math.random() * 1000;
|
| 625 |
console.warn(`[Worker] Retrying on ${provider.name} for ID: ${task.subtitleId} (Attempt ${retryCount + 2}) after ${delay.toFixed(0)}ms. Reason: ${error.message}`);
|
| 626 |
await new Promise(r => setTimeout(r, delay));
|
|
|
|
| 821 |
const formatRegex = /^\s*\d+\.\s*\[.*]\s*$/;
|
| 822 |
if (!translatedParts.every(p => formatRegex.test(p.trim()))) {
|
| 823 |
console.warn(`[SmartFallback] Format error detected in ${batchId}.`);
|
| 824 |
+
|
| 825 |
+
console.error(`[SmartFallback-Triage] Mismatch for ${batchId} (received: , expected: ${batch.length}). Activating final fallback.`);
|
| 826 |
+
console.error(`\n\n\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!`);
|
| 827 |
+
console.error(`[CRIME SCENE] BATCH #${batchId} FAILED: Mismatch Detected!`);
|
| 828 |
+
console.error(`!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!`);
|
| 829 |
+
console.error(` - Batch ID: ${batchId}`);
|
| 830 |
+
console.error(` - EXPECTED LINES: ${batch.length}`);
|
| 831 |
+
console.error(` - RECEIVED PARTS: `);
|
| 832 |
+
console.error("\n--- MERGED ORIGINAL TEXT (SENT TO API) ---");
|
| 833 |
+
console.error(mergedText);
|
| 834 |
+
console.error("\n--- RECEIVED TRANSLATED TEXT (FROM API) ---");
|
| 835 |
+
console.error(translatedMergedText);
|
| 836 |
+
console.error("\n--- SPLIT PARTS (FOR DEBUGGING) ---");
|
| 837 |
+
console.error(translatedParts);
|
| 838 |
+
console.error(`!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!`);
|
| 839 |
+
console.error(`[ACTION] Activating line-by-line fallback for this batch...`);
|
| 840 |
+
console.error(`!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n\n\n`);
|
| 841 |
throw new Error("Format error detected.");
|
| 842 |
}
|
| 843 |
|
| 844 |
// --- 4. 如果所有检查都通过,就是成功! ---
|
| 845 |
+
console.log(`[SmartFallback] ✅ 批量翻译成功: ${batchId}`);
|
| 846 |
return batch.map((original, index) => ({
|
| 847 |
...original,
|
| 848 |
translatedText: translatedParts[index].replace(/^\d+\.\s*\[?/, '').replace(/]?\s*$/, '').trim(),
|