mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-10 04:36:15 -08:00
refactor(docs): [SITE-AI-DOCS-01] make generation offline and translation draft based
This commit is contained in:
1 parent
647f3e7f12
commit
078cd9ef72
27 files changed
+8642
-4263
No files matched your search
+2
-260
@@ -1,262 +1,4 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import dotenv from 'dotenv'
|
||||
import { glob } from 'glob'
|
||||
import { runTranslation } from './documentation/cli.ts'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const rootDir = path.resolve(__dirname, '../packages/artplayer-vitepress')
|
||||
const srcDirs = ['docs/advanced', 'docs/component', 'docs/start']
|
||||
const indexFile = 'docs/index.md'
|
||||
const outputRoot = path.join(rootDir, 'docs/en')
|
||||
const API_URL = 'https://api.deepseek.com/v1/chat/completions'
|
||||
const API_KEY = process.env.DEEPSEEK_API_KEY
|
||||
|
||||
// 配置常量
|
||||
const MAX_CONCURRENT_REQUESTS = 5 // 并发请求数
|
||||
const REQUEST_TIMEOUT = 60000 // 请求超时时间 60秒
|
||||
const MAX_RETRIES = 3 // 最大重试次数
|
||||
const CHUNK_SIZE = 4000 // 分块大小
|
||||
|
||||
if (!API_KEY) {
|
||||
console.error('❌ Missing DEEPSEEK_API_KEY in .env file')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// ==================== Markdown 修复功能 ====================
|
||||
|
||||
/**
|
||||
* 修复 Markdown 代码块闭合问题
|
||||
* - 遇到 ## 标题时补上遗漏的 ```
|
||||
* - 遇到 ::: 时补上遗漏的 ```
|
||||
* - 文件末尾检查是否需要补上
|
||||
*/
|
||||
function fixMarkdownCodeBlocks(content) {
|
||||
const lines = content.split('\n')
|
||||
const fixedLines = []
|
||||
let inCodeBlock = false
|
||||
let fixes = 0
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]
|
||||
|
||||
// 检查是否进入代码块
|
||||
if (line.startsWith('```') && !inCodeBlock) {
|
||||
inCodeBlock = true
|
||||
fixedLines.push(line)
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查是否正常退出代码块
|
||||
if (line.startsWith('```') && inCodeBlock) {
|
||||
inCodeBlock = false
|
||||
fixedLines.push(line)
|
||||
continue
|
||||
}
|
||||
|
||||
// 在代码块中遇到标题(缺少 ```)
|
||||
if (inCodeBlock && line.startsWith('## ')) {
|
||||
fixedLines.push('```')
|
||||
fixedLines.push('')
|
||||
inCodeBlock = false
|
||||
fixes++
|
||||
}
|
||||
|
||||
// 在代码块中遇到 :::(admonition)
|
||||
if (inCodeBlock && line.startsWith(':::')) {
|
||||
fixedLines.push('```')
|
||||
fixedLines.push('')
|
||||
inCodeBlock = false
|
||||
fixes++
|
||||
}
|
||||
|
||||
fixedLines.push(line)
|
||||
}
|
||||
|
||||
// 文件末尾仍在代码块中
|
||||
if (inCodeBlock) {
|
||||
fixedLines.push('```')
|
||||
fixes++
|
||||
}
|
||||
|
||||
return { content: fixedLines.join('\n'), fixes }
|
||||
}
|
||||
|
||||
// ==================== 翻译功能 ====================
|
||||
|
||||
function splitMarkdown(text, maxLen = CHUNK_SIZE) {
|
||||
const lines = text.split('\n')
|
||||
const chunks = []
|
||||
let buffer = ''
|
||||
let insideCodeBlock = false
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.trim().startsWith('```'))
|
||||
insideCodeBlock = !insideCodeBlock
|
||||
if ((`${buffer}\n${line}`).length > maxLen && !insideCodeBlock) {
|
||||
chunks.push(buffer)
|
||||
buffer = ''
|
||||
}
|
||||
buffer += `\n${line}`
|
||||
}
|
||||
|
||||
if (buffer.trim())
|
||||
chunks.push(buffer)
|
||||
return chunks
|
||||
}
|
||||
|
||||
function cleanTranslation(text) {
|
||||
return text
|
||||
.replace(/^```(markdown|md)?/gi, '')
|
||||
.replace(/```$/g, '')
|
||||
.replace(/^(Here(')s|Below is|Translation|The English version|Here is)[::]?\s*/gi, '')
|
||||
.replace(/(Translation completed\.?|End of translation\.?)$/gi, '')
|
||||
.trim()
|
||||
}
|
||||
|
||||
async function callDeepSeekAPI(prompt, tag, chunkId) {
|
||||
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
||||
try {
|
||||
console.log(`🔹 [${tag}] Translating chunk ${chunkId} (attempt ${attempt}/${MAX_RETRIES})`)
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT)
|
||||
|
||||
const res = await fetch(API_URL, {
|
||||
method: 'POST',
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${API_KEY}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'deepseek-chat',
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
temperature: 0.2,
|
||||
}),
|
||||
})
|
||||
clearTimeout(timeout)
|
||||
|
||||
// 处理限流情况
|
||||
if (res.status === 429) {
|
||||
const delay = 2 ** attempt * 1000
|
||||
console.log(`⏳ [${tag}] Rate limited, waiting ${delay}ms...`)
|
||||
await new Promise(r => setTimeout(r, delay))
|
||||
continue
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.text()
|
||||
console.error(`[${tag}] API Error:`, error)
|
||||
throw new Error(`DeepSeek API error: ${res.status}`)
|
||||
}
|
||||
|
||||
const data = await res.json()
|
||||
if (!data.choices || !data.choices[0]) {
|
||||
throw new Error('Invalid API response: missing choices')
|
||||
}
|
||||
return data.choices[0].message?.content?.trim() || ''
|
||||
}
|
||||
catch (err) {
|
||||
if (err.name === 'AbortError') {
|
||||
console.error(`[${tag}] Request timeout for chunk ${chunkId}`)
|
||||
}
|
||||
if (attempt === MAX_RETRIES) {
|
||||
throw err
|
||||
}
|
||||
const delay = 1000 * attempt
|
||||
console.log(`⚠️ [${tag}] Retry ${attempt}/${MAX_RETRIES} after ${delay}ms...`)
|
||||
await new Promise(r => setTimeout(r, delay))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function translateText(text, fileName) {
|
||||
const chunks = splitMarkdown(text, CHUNK_SIZE)
|
||||
let result = ''
|
||||
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
const prompt = `
|
||||
You are a professional English technical translator for documentation.
|
||||
Translate the following Markdown content from Simplified Chinese to fluent English.
|
||||
Keep all Markdown structure, code blocks, and formatting intact.
|
||||
Return ONLY the translated Markdown content — do NOT add any explanations, prefixes, or summaries.
|
||||
|
||||
Text to translate:
|
||||
${chunks[i]}
|
||||
`.trim()
|
||||
|
||||
const raw = await callDeepSeekAPI(prompt, fileName, `${i + 1}/${chunks.length}`)
|
||||
const translated = cleanTranslation(raw)
|
||||
result += `\n\n${translated}`
|
||||
}
|
||||
|
||||
return result.trim()
|
||||
}
|
||||
|
||||
async function processFile(inputPath, outputPath) {
|
||||
const content = fs.readFileSync(inputPath, 'utf8')
|
||||
const fileName = path.basename(inputPath)
|
||||
console.log(`🌍 Translating: ${inputPath}`)
|
||||
|
||||
// 1. 翻译
|
||||
let translated = await translateText(content, fileName)
|
||||
|
||||
// 2. 修复 Markdown 语法
|
||||
const { content: fixed, fixes } = fixMarkdownCodeBlocks(translated)
|
||||
if (fixes > 0) {
|
||||
console.log(`🔧 [${fileName}] Fixed ${fixes} unclosed code blocks`)
|
||||
translated = fixed
|
||||
}
|
||||
|
||||
// 3. 保存
|
||||
fs.mkdirSync(path.dirname(outputPath), { recursive: true })
|
||||
fs.writeFileSync(outputPath, translated, 'utf8')
|
||||
console.log(`✅ Saved: ${outputPath}`)
|
||||
}
|
||||
|
||||
async function runWithConcurrency(tasks, limit = MAX_CONCURRENT_REQUESTS) {
|
||||
const results = []
|
||||
const executing = new Set()
|
||||
for (const task of tasks) {
|
||||
const p = task().finally(() => executing.delete(p))
|
||||
results.push(p)
|
||||
executing.add(p)
|
||||
if (executing.size >= limit) {
|
||||
await Promise.race(executing)
|
||||
}
|
||||
}
|
||||
return Promise.all(results)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('🚀 Starting translation...')
|
||||
fs.rmSync(outputRoot, { recursive: true, force: true })
|
||||
fs.mkdirSync(outputRoot, { recursive: true })
|
||||
|
||||
const indexPath = path.join(rootDir, indexFile)
|
||||
const indexOut = path.join(outputRoot, 'index.md')
|
||||
await processFile(indexPath, indexOut)
|
||||
|
||||
const tasks = []
|
||||
for (const dir of srcDirs) {
|
||||
const fullDir = path.join(rootDir, dir)
|
||||
const files = await glob(`${fullDir}/**/*.md`)
|
||||
for (const file of files) {
|
||||
const relative = path.relative(rootDir, file)
|
||||
const outputFile = path.join(outputRoot, relative.replace(/^docs[\\/]/, ''))
|
||||
tasks.push(() => processFile(file, outputFile))
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`🧠 Total files: ${tasks.length}, concurrency: ${MAX_CONCURRENT_REQUESTS}`)
|
||||
await runWithConcurrency(tasks, MAX_CONCURRENT_REQUESTS)
|
||||
console.log('🎉 Translation complete!')
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('❌ Translation failed:', err)
|
||||
process.exit(1)
|
||||
})
|
||||
await runTranslation(process.argv.slice(2))
|
||||
Reference in new issue
Block a user