feat: 标准翻译完成

This commit is contained in:
肖应宇 2026-03-10 10:40:46 +08:00
parent b3003c4606
commit e7b6c360c1
2 changed files with 90 additions and 56 deletions

View File

@ -192,54 +192,26 @@ async function performOcr(file, onProgress) {
* @returns {Promise<string>} 翻译后的文本
*/
async function performTranslation(markdown, onProgress) {
// 获取翻译设置
const settings = typeof loadSettings === 'function' ? loadSettings() : {};
// 目标语言固定为中文
const targetLangName = '中文';
const targetLang = settings.targetLanguage || 'zh-CN';
const targetLangName = targetLang === 'custom'
? (settings.customTargetLanguageName || '中文')
: { 'zh-CN': '中文', 'en': 'English', 'ja': '日本語', 'ko': '한국어' }[targetLang] || targetLang;
// 翻译提示词(不依赖 getBuiltInPrompts
const systemPrompt = '你是一个专业的文档翻译助手,擅长将文本精确翻译为简体中文,同时保留原始的 Markdown 格式。';
const userPromptTemplate = '请将以下英文内容翻译成简体中文。请保持原有的 Markdown 格式和结构。只返回翻译后的内容,不要添加任何解释或说明。\n\n${content}';
const selectedModel = settings.selectedTranslationModel || 'mistral';
// 获取 API Key
let apiKey = '';
let modelConfig = null;
if (selectedModel === 'custom') {
// 自定义模型配置
modelConfig = settings.translationModelConfig || settings.customModelConfig || null;
if (!modelConfig) {
throw new Error('请先配置自定义翻译模型');
}
} else {
// 预设模型 - 从 Key 管理器获取 API Key
if (typeof loadModelKeys === 'function') {
const keys = loadModelKeys(selectedModel);
const validKey = keys.find(k => k && k.value && (k.status === 'valid' || k.status === 'untested'));
if (validKey) {
apiKey = validKey.value.trim();
}
}
// 回退到 localStorage
if (!apiKey) {
const legacyKey = localStorage.getItem(`${selectedModel}ApiKeys`) || localStorage.getItem('translationApiKeys');
if (legacyKey) {
apiKey = legacyKey.trim();
}
}
if (!apiKey) {
throw new Error(`请先配置 ${selectedModel} 模型的 API Key`);
}
}
// 强制使用 'aliyun' 模型(后端代理模式,无需前端 API Key
// translation.js 的 predefinedConfigs 只配置了 'aliyun' 指向后端代理
const selectedModel = 'aliyun';
const apiKey = ''; // 后端代理不需要前端 API Key
// 分段翻译长文本
const chunks = splitMarkdownIntoChunks(markdown, 2000);
let translatedText = '';
const totalChunks = chunks.length;
console.log('[performTranslation] 分块数:', totalChunks);
console.log('[performTranslation] 输入markdown长度:', markdown?.length);
onProgress && onProgress(0, totalChunks, '正在翻译...');
// 临时禁用 promptPoolUI避免历史详情页访问不存在的 UI 元素
@ -251,28 +223,24 @@ async function performTranslation(markdown, onProgress) {
onProgress && onProgress(i + 1, totalChunks, `翻译中 (${i + 1}/${totalChunks})`);
try {
// 构建翻译选项
const translateOptions = {};
// 如果是自定义模型,需要传入 modelConfig
if (selectedModel === 'custom' && modelConfig) {
translateOptions.modelConfig = modelConfig;
}
// 调用 translateMarkdown 函数 - 不传 boundPrompt让它使用内置提示词
// 调用 translateMarkdown 函数
// 'aliyun' 模型通过后端代理调用通义千问,无需前端 API Key
const chunkResult = await translateMarkdown(
chunks[i],
targetLangName,
selectedModel,
apiKey,
'[历史详情页翻译]', // logContext
'', // defaultSystemPrompt - 空值会触发内置提示词
'', // defaultUserPromptTemplate - 空值会触发内置提示词
false, // useCustomPrompts
true, // processTablePlaceholders
translateOptions
systemPrompt, // 直接传入中文翻译的系统提示词
userPromptTemplate, // 直接传入中文翻译的用户提示词模板
true, // useCustomPrompts = true使用传入的提示词
true, // processTablePlaceholders
{} // translateOptions - 使用默认配置
);
console.log(`[performTranslation] 第${i + 1}块翻译完成, 结果长度:`, chunkResult?.length);
console.log(`[performTranslation] 第${i + 1}块翻译结果前100字符:`, chunkResult?.substring(0, 100));
translatedText += chunkResult + '\n\n';
} catch (error) {
console.error(`[performTranslation] 翻译第 ${i + 1} 块失败:`, error);
@ -285,6 +253,7 @@ async function performTranslation(markdown, onProgress) {
window.promptPoolUI = originalPromptPoolUI;
}
console.log('[performTranslation] 翻译完成, 最终长度:', translatedText.trim().length);
return translatedText.trim();
}
@ -295,13 +264,19 @@ async function performTranslation(markdown, onProgress) {
* @returns {string[]} 分割后的块数组
*/
function splitMarkdownIntoChunks(markdown, maxChars = 2000) {
console.log('[splitMarkdownIntoChunks] 输入长度:', markdown?.length, 'maxChars:', maxChars);
console.log('[splitMarkdownIntoChunks] 输入前100字符:', markdown?.substring(0, 100));
const chunks = [];
const lines = markdown.split('\n');
console.log('[splitMarkdownIntoChunks] 行数:', lines.length);
let currentChunk = '';
for (const line of lines) {
if (currentChunk.length + line.length + 1 > maxChars) {
if (currentChunk.trim()) {
console.log('[splitMarkdownIntoChunks] 添加块:', chunks.length + 1, '长度:', currentChunk.length);
chunks.push(currentChunk.trim());
}
currentChunk = line + '\n';
@ -311,9 +286,60 @@ function splitMarkdownIntoChunks(markdown, maxChars = 2000) {
}
if (currentChunk.trim()) {
console.log('[splitMarkdownIntoChunks] 添加最后块:', chunks.length + 1, '长度:', currentChunk.length);
chunks.push(currentChunk.trim());
}
console.log('[splitMarkdownIntoChunks] 最终块数:', chunks.length);
chunks.forEach((chunk, i) => console.log(`[splitMarkdownIntoChunks] 块${i + 1} 长度:`, chunk.length));
return chunks.length > 0 ? chunks : [markdown];
}
/**
* 根据标题分割 Markdown 文本为块数组用于分块对比视图
* @param {string} markdown - Markdown 文本
* @returns {string[]} 分割后的块数组
*/
function generateChunks(markdown) {
if (!markdown || typeof markdown !== 'string') return [];
const lines = markdown.split(/\r?\n/);
const chunks = [];
let buffer = [];
let inCode = false;
function flush() {
if (buffer.length) {
chunks.push(buffer.join('\n').trim());
buffer = [];
}
}
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// 处理代码块
if (/^\s*```/.test(line)) {
inCode = !inCode;
buffer.push(line);
continue;
}
if (inCode) {
buffer.push(line);
continue;
}
// 标题作为新分块的起点
if (/^\s*#/.test(line)) {
flush();
}
buffer.push(line);
}
flush();
return chunks.length > 0 ? chunks : [markdown];
}
@ -474,6 +500,13 @@ async function triggerReprocess(includeTranslation) {
showToast(`${msg || '翻译中'} (${current}/${total})`, 'info', 5000);
});
window.data.translation = translationResult;
// 生成分块数据用于"分块对比"视图
const ocrChunks = generateChunks(ocrResult.markdown);
const translatedChunks = generateChunks(translationResult);
window.data.ocrChunks = ocrChunks;
window.data.translatedChunks = translatedChunks;
console.log('[triggerReprocess] 生成分块: ocrChunks=', ocrChunks.length, 'translatedChunks=', translatedChunks.length);
} catch (translationError) {
console.error('[triggerReprocess] 翻译失败:', translationError);
showToast(`翻译失败: ${translationError.message},但 OCR 已完成`, 'warning');
@ -490,7 +523,8 @@ async function triggerReprocess(includeTranslation) {
renderDetail();
}
if (typeof showTab === 'function') {
showTab(includeTranslation ? 'translation' : 'ocr');
// 翻译成功后显示分块对比,否则显示 OCR
showTab(includeTranslation && window.data.translation ? 'chunk-compare' : 'ocr');
}
showToast('处理完成!', 'success');

View File

@ -703,7 +703,7 @@ async function translateMarkdown(
const predefinedConfigs = {
'aliyun': {
// 所有翻译请求都指向后端代理,由后端决定使用哪个模型
endpoint: 'http://localhost:3456/api/llm/dashscope/v1/chat/completions',
endpoint: 'http://localhost:3456/api/llm/tongyi/v1/chat/completions',
modelName: '通义百炼',
headers: { 'Content-Type': 'application/json' },
bodyBuilder: (sys, user) => {