mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-09 03:46:16 -08:00
101 lines
3.1 KiB
TypeScript
101 lines
3.1 KiB
TypeScript
import assert from 'node:assert/strict'
|
|
import { setTimeout as delay } from 'node:timers/promises'
|
|
|
|
interface RemoteOptions {
|
|
key: string
|
|
request?: typeof fetch
|
|
signal?: AbortSignal
|
|
timeoutMs?: number
|
|
retries?: number
|
|
wait?: (ms: number) => Promise<unknown>
|
|
}
|
|
|
|
function object(value: unknown): Record<string, unknown> {
|
|
return value !== null && typeof value === 'object'
|
|
? (value as Record<string, unknown>)
|
|
: {}
|
|
}
|
|
|
|
export async function translateChunk(
|
|
content: string,
|
|
options: RemoteOptions,
|
|
): Promise<string> {
|
|
assert(options.key, 'Missing DEEPSEEK_API_KEY')
|
|
const retries = options.retries ?? 3
|
|
assert(
|
|
Number.isInteger(retries) && retries > 0 && retries <= 5,
|
|
'Invalid retry limit',
|
|
)
|
|
const timeoutMs = options.timeoutMs ?? 60000
|
|
assert(
|
|
Number.isFinite(timeoutMs) && timeoutMs > 0,
|
|
'Invalid request timeout',
|
|
)
|
|
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
options.signal?.throwIfAborted()
|
|
const controller = new AbortController()
|
|
const timeout = setTimeout(
|
|
() => controller.abort(new Error('Translation request timed out')),
|
|
timeoutMs,
|
|
)
|
|
const signal = options.signal
|
|
? AbortSignal.any([controller.signal, options.signal])
|
|
: controller.signal
|
|
let retry = false
|
|
try {
|
|
const response = await (options.request ?? fetch)(
|
|
'https://api.deepseek.com/v1/chat/completions',
|
|
{
|
|
method: 'POST',
|
|
signal,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${options.key}`,
|
|
},
|
|
body: JSON.stringify({
|
|
model: 'deepseek-chat',
|
|
temperature: 0.2,
|
|
messages: [
|
|
{
|
|
role: 'user',
|
|
content: `Translate this Chinese technical Markdown to English. Preserve structure, inline code, URLs and every ARTPLAYER_KEEP marker exactly once. Return only Markdown, without an outer code fence or explanations.\n\n${content}`,
|
|
},
|
|
],
|
|
}),
|
|
},
|
|
)
|
|
retry = response.status === 429 || response.status >= 500
|
|
if (!response.ok)
|
|
await response.body?.cancel()
|
|
assert(response.ok, `Translation HTTP ${response.status}`)
|
|
const data: unknown = await response.json().catch((error: unknown) => {
|
|
if (signal.aborted)
|
|
throw error
|
|
assert.fail('Invalid translation JSON response')
|
|
})
|
|
const choices = object(data).choices
|
|
const text = object(
|
|
object(Array.isArray(choices) ? choices[0] : undefined).message,
|
|
).content
|
|
assert(
|
|
typeof text === 'string' && text.trim(),
|
|
'Invalid or empty translation response',
|
|
)
|
|
return text.trim()
|
|
}
|
|
catch (error) {
|
|
if (options.signal?.aborted)
|
|
throw error
|
|
if (!(error instanceof assert.AssertionError))
|
|
retry = true
|
|
if (!retry || attempt === retries)
|
|
throw error
|
|
}
|
|
finally {
|
|
clearTimeout(timeout)
|
|
}
|
|
await (options.wait ?? delay)(500 * attempt)
|
|
}
|
|
throw new Error('Translation retries exhausted')
|
|
}
|