feat: 标准翻译完成
This commit is contained in:
parent
b3003c4606
commit
e7b6c360c1
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue