// ==UserScript==
// @name 大模型中文翻译助手
// @name:en LLM powered WebPage Translator to Chinese
// @namespace http://tampermonkey.net/
// @version 1.3.14
// @description 选中文本后调用 OpenAI Compatible API 将其翻译为中文,支持历史记录、收藏夹及整页翻译
// @description:en Select text and call OpenAI Compatible API to translate it to Chinese, supports history, favorites and full page translation
// @author tzh
// @match *://*/*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// 可配置的设置
const defaultSettings = {
apiEndpoint: 'https://api.deepseek.com/v1/chat/completions', // 默认 OpenAI API 端点
apiKey: '', // 设置您的 API 密钥
model: 'deepseek-chat', // 可以根据需要更改为其他模型
systemPrompt: '你是一个翻译助手。我会为你提供待翻译的文本,以及之前已经翻译过的上下文(如果有)。请参考这些上下文,将文本准确地翻译成中文,保持原文的意思、风格和格式。在充分保留原文意思的情况下使用符合中文习惯的表达。只返回翻译结果,不需要解释。',
useStreaming: false, // 默认启用流式响应
temperature: 0.3, // 控制翻译结果的随机性,值越低越准确,值越高越有创意
maxHistoryItems: 50, // 最大历史记录数
maxFavoritesItems: 100, // 最大收藏数
showSourceLanguage: false, // 是否显示源语言
autoDetectLanguage: true, // 是否自动检测语言
detectArticleContent: true, // 是否自动识别文章主体内容
contextSize: 3, // 翻译时使用的上下文数量
useTranslationContext: true, // 是否启用翻译上下文
fullPageTranslationSelector: 'body', // 整页翻译时的选择器
fullPageMaxSegmentLength: 2000, // 整页翻译时的最大分段长度
excludeSelectors: 'script, style, noscript, iframe, img, svg, canvas', // 翻译时排除的元素
apiConfigs: [ // 多API配置
{
name: 'DeepSeek',
apiEndpoint: 'https://api.deepseek.com/v1/chat/completions',
apiKey: '',
model: 'deepseek-chat',
}
],
currentApiIndex: 0, // 当前使用的API索引
currentTab: 'general', // 默认显示普通设置标签
};
// 初始化设置
let settings = GM_getValue('translatorSettings', defaultSettings);
// 全文翻译相关状态
let isTranslatingFullPage = false; // 是否正在进行全文翻译
let isTranslationPaused = false; // 是否已暂停翻译
let translationSegments = []; // 存储页面分段内容
let lastTranslatedIndex = -1; // 最后翻译的段落索引
let originalTexts = []; // 存储原始文本
// 确保设置中包含apiConfigs字段
if (!settings.apiConfigs) {
settings.apiConfigs = [
{
name: '默认API',
apiEndpoint: settings.apiEndpoint,
apiKey: settings.apiKey,
model: settings.model,
}
];
settings.currentApiIndex = 0;
GM_setValue('translatorSettings', settings);
}
// 从当前选择的API配置中同步主要设置
function syncApiSettings() {
if (settings.apiConfigs && settings.apiConfigs.length > 0 && settings.currentApiIndex >= 0 && settings.currentApiIndex < settings.apiConfigs.length) {
const currentApi = settings.apiConfigs[settings.currentApiIndex];
const oldEndpoint = settings.apiEndpoint;
const oldModel = settings.model;
settings.apiEndpoint = currentApi.apiEndpoint;
settings.apiKey = currentApi.apiKey;
settings.model = currentApi.model;
// 检查是否有变化
if (oldEndpoint !== settings.apiEndpoint || oldModel !== settings.model) {
console.log(`API设置已同步 - 索引: ${settings.currentApiIndex}, 端点: ${settings.apiEndpoint}, 模型: ${settings.model} (变更: ${oldEndpoint} -> ${settings.apiEndpoint}, ${oldModel} -> ${settings.model})`);
} else {
console.log(`API设置已同步 - 索引: ${settings.currentApiIndex}, 端点: ${settings.apiEndpoint}, 模型: ${settings.model} (无变更)`);
}
GM_setValue('translatorSettings', settings);
} else {
console.warn('无法同步API设置 - 无效的配置或索引', {
hasConfigs: Boolean(settings.apiConfigs),
configsLength: settings.apiConfigs ? settings.apiConfigs.length : 0,
currentIndex: settings.currentApiIndex
});
}
}
// 初始同步
syncApiSettings();
let translationHistory = GM_getValue('translationHistory', []);
let translationFavorites = GM_getValue('translationFavorites', []);
// 跟踪当前活跃的翻译按钮
let activeTranslateButton = null;
let lastSelectedText = '';
let lastSelectionRect = null;
// 监听整个文档的mousedown事件,防止点击翻译按钮时丢失选择
document.addEventListener('mousedown', function (e) {
// 检查点击是否发生在翻译按钮上
if (e.target.classList.contains('translate-button') || e.target.closest('.translate-button')) {
e.stopPropagation();
e.preventDefault();
}
}, true); // 使用捕获阶段,确保在其他处理程序之前执行
// 语言检测函数
function detectLanguage(text) {
return new Promise((resolve) => {
// 简单的语言检测逻辑
const chineseRegex = /[\u4e00-\u9fa5]/;
const englishRegex = /[a-zA-Z]/;
const japaneseRegex = /[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF]/;
const koreanRegex = /[\uAC00-\uD7AF\u1100-\u11FF]/;
if (chineseRegex.test(text)) resolve('中文');
else if (englishRegex.test(text)) resolve('英语');
else if (japaneseRegex.test(text)) resolve('日语');
else if (koreanRegex.test(text)) resolve('韩语');
else resolve('未知');
});
}
// 创建设置面板
function createSettingsPanel() {
const settingsPanel = document.createElement('div');
settingsPanel.id = 'translator-settings-panel';
settingsPanel.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 500px;
background-color: white;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
z-index: 10000;
height: 90vh;
display: none;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 14px;
line-height: 1.5;
color: #333;
`;
// 创建Tab栏
const tabsHtml = `
<div class="settings-header" style="padding: 0; flex: 0 0 auto;">
<div class="settings-tabs" style="border-bottom: 1px solid #eee;">
<div class="tab-buttons" style="display: flex;">
<button id="general-tab-btn" class="tab-btn ${settings.currentTab === 'general' ? 'active' : ''}"
style="flex: 1; padding: 12px 15px; background: none; border: none; border-bottom: 2px solid ${settings.currentTab === 'general' ? '#4285f4' : 'transparent'};
cursor: pointer; color: ${settings.currentTab === 'general' ? '#4285f4' : '#666'}; font-size: 14px; font-weight: 500; outline: none;">
翻译设置
</button>
<button id="api-tab-btn" class="tab-btn ${settings.currentTab === 'api' ? 'active' : ''}"
style="flex: 1; padding: 12px 15px; background: none; border: none; border-bottom: 2px solid ${settings.currentTab === 'api' ? '#4285f4' : 'transparent'};
cursor: pointer; color: ${settings.currentTab === 'api' ? '#4285f4' : '#666'}; font-size: 14px; font-weight: 500; outline: none;">
API 管理
</button>
</div>
</div>
</div>
`;
// 创建内容区域的容器
const contentContainerHtml = `
<div class="settings-content" style="flex: 1 1 auto; overflow-y: auto; padding: 20px;">
<div id="general-tab" class="tab-content" style="display: ${settings.currentTab === 'general' ? 'block' : 'none'};">
<h2 style="margin: 0 0 20px 0; font-size: 18px; font-weight: 500; color: #333;">翻译设置</h2>
<div style="margin-bottom: 20px;">
<label for="systemPrompt" style="display: block; margin-bottom: 8px; color: #333; font-weight: 500;">系统提示词:</label>
<textarea id="systemPrompt" style="width: 100%; height: 100px; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; line-height: 1.5; resize: vertical; font-family: inherit;">${settings.systemPrompt}</textarea>
</div>
<div style="margin-bottom: 20px;">
<label for="temperature" style="display: block; margin-bottom: 8px; color: #333; font-weight: 500;">随机性(Temperature)</label>
<div style="display: flex; align-items: center;">
<input type="range" id="temperature" min="0" max="1" step="0.1" value="${settings.temperature}"
style="flex-grow: 1; height: 4px; background: #ddd; border-radius: 2px; outline: none; -webkit-appearance: none;">
<span id="temperature-value" style="margin-left: 15px; min-width: 30px; color: #666;">${settings.temperature}</span>
</div>
<div style="font-size: 12px; color: #666; margin-top: 8px;">
值越低翻译越准确,值越高结果越具有创意性。翻译任务建议使用较低的值。
</div>
</div>
<div style="margin-bottom: 15px;">
<label style="display: flex; align-items: center; color: #333;">
<input type="checkbox" id="showSourceLanguage" ${settings.showSourceLanguage ? 'checked' : ''}
style="margin: 0 8px 0 0; width: 16px; height: 16px;">
<span style="font-weight: 500;">显示原文</span>
</label>
<div style="font-size: 12px; color: #666; margin-top: 8px; margin-left: 24px;">
启用后将在翻译结果上方显示原文
</div>
</div>
<div style="margin-bottom: 15px;">
<label style="display: flex; align-items: center; color: #333;">
<input type="checkbox" id="useStreaming" ${settings.useStreaming ? 'checked' : ''}
style="margin: 0 8px 0 0; width: 16px; height: 16px;">
<span style="font-weight: 500;">启用流式响应(实时显示翻译结果)</span>
</label>
<div style="font-size: 12px; color: #666; margin-top: 8px; margin-left: 24px;">
如果遇到翻译失败问题,可以尝试关闭此选项
</div>
</div>
<div style="margin-bottom: 15px;">
<label style="display: flex; align-items: center; color: #333;">
<input type="checkbox" id="useTranslationContext" ${settings.useTranslationContext ? 'checked' : ''}
style="margin: 0 8px 0 0; width: 16px; height: 16px;">
<span style="font-weight: 500;">启用翻译上下文</span>
</label>
<div style="font-size: 12px; color: #666; margin-top: 8px; margin-left: 24px;">
启用后将使用之前翻译过的内容作为上下文,提高翻译连贯性
</div>
</div>
<div style="margin-bottom: 20px;">
<label for="contextSize" style="display: block; margin-bottom: 8px; color: #333; font-weight: 500;">上下文数量:</label>
<div style="display: flex; align-items: center;">
<input type="number" id="contextSize" min="1" max="10" value="${settings.contextSize}"
style="width: 60px; padding: 6px 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
<span style="margin-left: 10px; color: #666;">段落</span>
</div>
<div style="font-size: 12px; color: #666; margin-top: 8px;">
使用前面已翻译段落作为上下文提升翻译连贯性,建议设置1-5之间,设置较大值会降低翻译速度,消耗更多API配额
</div>
</div>
<div style="margin-bottom: 15px;">
<label style="display: flex; align-items: center; color: #333;">
<input type="checkbox" id="detectArticleContent" ${settings.detectArticleContent ? 'checked' : ''}
style="margin: 0 8px 0 0; width: 16px; height: 16px;">
<span style="font-weight: 500;">智能识别文章主体内容</span>
</label>
<div style="font-size: 12px; color: #666; margin-top: 8px; margin-left: 24px;">
启用后将自动识别文章主要内容区域,避免翻译无关内容
</div>
</div>
<div style="margin-bottom: 20px;">
<label for="fullPageTranslationSelector" style="display: block; margin-bottom: 8px; color: #333; font-weight: 500;">整页翻译选择器:</label>
<input type="text" id="fullPageTranslationSelector" value="${settings.fullPageTranslationSelector}"
style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
<div style="font-size: 12px; color: #666; margin-top: 8px;">
CSS选择器,用于指定翻译哪些区域的内容
</div>
</div>
<div style="margin-bottom: 20px;">
<label for="excludeSelectors" style="display: block; margin-bottom: 8px; color: #333; font-weight: 500;">排除翻译的元素:</label>
<input type="text" id="excludeSelectors" value="${settings.excludeSelectors}"
style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
<div style="font-size: 12px; color: #666; margin-top: 8px;">
CSS选择器,指定要排除翻译的元素
</div>
</div>
</div>
<div id="api-tab" class="tab-content" style="display: ${settings.currentTab === 'api' ? 'block' : 'none'};">
<h2 style="margin: 0 0 20px 0; font-size: 18px; font-weight: 500; color: #333;">API 管理</h2>
<button id="add-api-btn" style="width: 100%; padding: 12px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; font-size: 14px; font-weight: 500; cursor: pointer; margin-bottom: 20px;">
+ 添加新 API
</button>
<div id="api-list">
${settings.apiConfigs.map((api, index) => `
<div class="api-item" style="margin-bottom: 15px; padding: 15px; border: 1px solid #ddd; border-radius: 4px; position: relative; ${index === settings.currentApiIndex ? 'background-color: #f0f8ff; border-color: #4285f4;' : ''}">
<div style="position: absolute; top: 15px; right: 15px;">
${index === settings.currentApiIndex ?
'<span style="color: #4CAF50; font-weight: 500;">✓ 当前使用</span>' :
`<button class="use-api-btn" data-index="${index}" style="padding: 4px 12px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; margin-right: 8px; font-size: 12px;">使用</button>`
}
<button class="edit-api-btn" data-index="${index}" style="padding: 4px 12px; background-color: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer; margin-right: 8px; font-size: 12px;">编辑</button>
${settings.apiConfigs.length > 1 ?
`<button class="delete-api-btn" data-index="${index}" style="padding: 4px 12px; background-color: #f44336; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">删除</button>` : ''
}
</div>
<div style="margin-bottom: 8px;"><strong style="color: #333;">名称:</strong> <span style="color: #666;">${api.name}</span></div>
<div style="margin-bottom: 8px;"><strong style="color: #333;">端点:</strong> <span style="color: #666;">${api.apiEndpoint}</span></div>
<div style="margin-bottom: 8px;"><strong style="color: #333;">密钥:</strong> <span style="color: #666;">${api.apiKey ? '******' + api.apiKey.substring(api.apiKey.length - 4) : '未设置'}</span></div>
<div><strong style="color: #333;">模型:</strong> <span style="color: #666;">${api.model}</span></div>
</div>
`).join('')}
</div>
</div>
</div>
`;
// 创建API编辑表单
const apiFormHtml = `
<div id="api-form" style="display: none; padding: 20px; overflow-y: auto; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: white;">
<h2 id="api-form-title" style="margin: 0 0 20px 0; font-size: 18px; font-weight: 500; color: #333;">添加 API</h2>
<input type="hidden" id="api-form-index" value="-1">
<div style="margin-bottom: 20px;">
<label for="api-name" style="display: block; margin-bottom: 8px; color: #333; font-weight: 500;">API 名称:</label>
<input type="text" id="api-name" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;" placeholder="例如:OpenAI、Azure、DeepSeek">
</div>
<div style="margin-bottom: 20px;">
<label for="api-endpoint" style="display: block; margin-bottom: 8px; color: #333; font-weight: 500;">API 端点:</label>
<input type="text" id="api-endpoint" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;" placeholder="例如:https://api.openai.com/v1/chat/completions">
</div>
<div style="margin-bottom: 20px;">
<label for="api-key" style="display: block; margin-bottom: 8px; color: #333; font-weight: 500;">API 密钥:</label>
<input type="password" id="api-key" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;" placeholder="输入您的API密钥">
<div style="font-size: 12px; color: #666; margin-top: 8px;">
编辑现有API时,如不需要修改密钥请留空
</div>
</div>
<div style="margin-bottom: 20px;">
<label for="api-model" style="display: block; margin-bottom: 8px; color: #333; font-weight: 500;">模型名称:</label>
<input type="text" id="api-model" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;" placeholder="例如:gpt-3.5-turbo">
</div>
<div style="text-align: right;">
<button id="cancel-api-form" style="padding: 8px 20px; background: #f5f5f5; color: #333; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; margin-right: 10px; cursor: pointer;">取消</button>
<button id="save-api-form" style="padding: 8px 20px; background-color: #4285f4; color: white; border: none; border-radius: 4px; font-size: 14px; cursor: pointer;">保存</button>
</div>
</div>
`;
// 创建底部按钮区域
const footerHtml = `
<div class="settings-footer" style="padding: 15px 20px; border-top: 1px solid #eee; text-align: right; background: white; border-radius: 0 0 8px 8px; flex: 0 0 auto;">
<button id="cancel-settings" style="padding: 8px 20px; background: #f5f5f5; color: #333; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; margin-right: 10px; cursor: pointer;">取消</button>
<button id="save-settings" style="padding: 8px 20px; background-color: #4285f4; color: white; border: none; border-radius: 4px; font-size: 14px; cursor: pointer;">保存</button>
</div>
`;
settingsPanel.innerHTML = tabsHtml + contentContainerHtml + apiFormHtml + footerHtml;
document.body.appendChild(settingsPanel);
// 添加温度滑块值变化的事件监听器
document.getElementById('temperature').addEventListener('input', function() {
document.getElementById('temperature-value').textContent = this.value;
});
// 工具函数:更新设置面板视图
function updateSettingsPanelView() {
// 更新标签页状态
document.getElementById('general-tab').style.display = settings.currentTab === 'general' ? 'block' : 'none';
document.getElementById('api-tab').style.display = settings.currentTab === 'api' ? 'block' : 'none';
document.getElementById('api-form').style.display = 'none';
// 更新标签按钮样式
const generalTabBtn = document.getElementById('general-tab-btn');
const apiTabBtn = document.getElementById('api-tab-btn');
if (settings.currentTab === 'general') {
generalTabBtn.style.borderBottom = '2px solid #4285f4';
generalTabBtn.style.color = '#4285f4';
apiTabBtn.style.borderBottom = '2px solid transparent';
apiTabBtn.style.color = '#666';
} else {
apiTabBtn.style.borderBottom = '2px solid #4285f4';
apiTabBtn.style.color = '#4285f4';
generalTabBtn.style.borderBottom = '2px solid transparent';
generalTabBtn.style.color = '#666';
}
// 更新表单值
document.getElementById('systemPrompt').value = settings.systemPrompt;
document.getElementById('temperature').value = settings.temperature;
document.getElementById('temperature-value').textContent = settings.temperature;
document.getElementById('showSourceLanguage').checked = settings.showSourceLanguage;
document.getElementById('useStreaming').checked = settings.useStreaming;
document.getElementById('useTranslationContext').checked = settings.useTranslationContext;
document.getElementById('contextSize').value = settings.contextSize;
document.getElementById('detectArticleContent').checked = settings.detectArticleContent;
document.getElementById('fullPageTranslationSelector').value = settings.fullPageTranslationSelector;
document.getElementById('excludeSelectors').value = settings.excludeSelectors;
// 更新API列表
updateApiList();
}
// 事件处理程序
document.getElementById('general-tab-btn').addEventListener('click', function() {
settings.currentTab = 'general';
updateSettingsPanelView();
});
document.getElementById('api-tab-btn').addEventListener('click', function() {
settings.currentTab = 'api';
updateSettingsPanelView();
});
// 添加API按钮事件
document.getElementById('add-api-btn').addEventListener('click', function() {
document.getElementById('api-tab').style.display = 'none';
document.getElementById('api-form').style.display = 'block';
document.getElementById('api-form-title').textContent = '添加 API';
document.getElementById('api-form-index').value = '-1';
document.getElementById('api-name').value = '';
document.getElementById('api-endpoint').value = '';
document.getElementById('api-key').value = '';
document.getElementById('api-model').value = '';
});
// API表单取消按钮事件
document.getElementById('cancel-api-form').addEventListener('click', function() {
document.getElementById('api-form').style.display = 'none';
document.getElementById('api-tab').style.display = 'block';
});
// API表单保存按钮事件
document.getElementById('save-api-form').addEventListener('click', function() {
const index = parseInt(document.getElementById('api-form-index').value);
const apiConfig = {
name: document.getElementById('api-name').value,
apiEndpoint: document.getElementById('api-endpoint').value,
model: document.getElementById('api-model').value
};
const apiKey = document.getElementById('api-key').value;
if (apiKey) {
apiConfig.apiKey = apiKey;
} else if (index !== -1) {
apiConfig.apiKey = settings.apiConfigs[index].apiKey;
}
if (index === -1) {
settings.apiConfigs.push(apiConfig);
} else {
settings.apiConfigs[index] = apiConfig;
}
document.getElementById('api-form').style.display = 'none';
document.getElementById('api-tab').style.display = 'block';
updateApiList();
});
// 设置面板的取消和保存按钮事件
document.getElementById('cancel-settings').addEventListener('click', function() {
settingsPanel.style.display = 'none';
});
document.getElementById('save-settings').addEventListener('click', function() {
settings = {
...settings,
systemPrompt: document.getElementById('systemPrompt').value,
useStreaming: document.getElementById('useStreaming').checked,
showSourceLanguage: document.getElementById('showSourceLanguage').checked,
detectArticleContent: document.getElementById('detectArticleContent').checked,
temperature: parseFloat(document.getElementById('temperature').value),
fullPageTranslationSelector: document.getElementById('fullPageTranslationSelector').value,
excludeSelectors: document.getElementById('excludeSelectors').value,
useTranslationContext: document.getElementById('useTranslationContext').checked,
contextSize: parseInt(document.getElementById('contextSize').value) || 3,
currentTab: settings.currentTab
};
GM_setValue('translatorSettings', settings);
settingsPanel.style.display = 'none';
});
// 绑定API列表项的事件处理程序
function bindApiListEvents() {
document.querySelectorAll('.use-api-btn').forEach(button => {
button.addEventListener('click', function() {
const index = parseInt(this.dataset.index);
settings.currentApiIndex = index;
updateApiList();
});
});
document.querySelectorAll('.edit-api-btn').forEach(button => {
button.addEventListener('click', function() {
const index = parseInt(this.dataset.index);
const api = settings.apiConfigs[index];
document.getElementById('api-form-title').textContent = '编辑 API';
document.getElementById('api-form-index').value = index;
document.getElementById('api-name').value = api.name;
document.getElementById('api-endpoint').value = api.apiEndpoint;
document.getElementById('api-key').value = '';
document.getElementById('api-model').value = api.model;
// 在内容区域显示API表单,而不是隐藏内容区域
document.getElementById('api-form').style.display = 'block';
document.getElementById('api-tab').style.display = 'none';
});
});
document.querySelectorAll('.delete-api-btn').forEach(button => {
button.addEventListener('click', function() {
const index = parseInt(this.dataset.index);
if (confirm('确定要删除这个API配置吗?')) {
settings.apiConfigs.splice(index, 1);
if (settings.currentApiIndex === index) {
settings.currentApiIndex = 0;
} else if (settings.currentApiIndex > index) {
settings.currentApiIndex--;
}
updateApiList();
}
});
});
}
// 更新API列表的函数
function updateApiList() {
const apiListHtml = settings.apiConfigs.map((api, index) => `
<div class="api-item" style="margin-bottom: 15px; padding: 15px; border: 1px solid #ddd; border-radius: 4px; position: relative; ${index === settings.currentApiIndex ? 'background-color: #f0f8ff; border-color: #4285f4;' : ''}">
<div style="position: absolute; top: 15px; right: 15px;">
${index === settings.currentApiIndex ?
'<span style="color: #4CAF50; font-weight: 500;">✓ 当前使用</span>' :
`<button class="use-api-btn" data-index="${index}" style="padding: 4px 12px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; margin-right: 8px; font-size: 12px;">使用</button>`
}
<button class="edit-api-btn" data-index="${index}" style="padding: 4px 12px; background-color: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer; margin-right: 8px; font-size: 12px;">编辑</button>
${settings.apiConfigs.length > 1 ?
`<button class="delete-api-btn" data-index="${index}" style="padding: 4px 12px; background-color: #f44336; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">删除</button>` : ''
}
</div>
<div style="margin-bottom: 8px;"><strong style="color: #333;">名称:</strong> <span style="color: #666;">${api.name}</span></div>
<div style="margin-bottom: 8px;"><strong style="color: #333;">端点:</strong> <span style="color: #666;">${api.apiEndpoint}</span></div>
<div style="margin-bottom: 8px;"><strong style="color: #333;">密钥:</strong> <span style="color: #666;">${api.apiKey ? '******' + api.apiKey.substring(api.apiKey.length - 4) : '未设置'}</span></div>
<div><strong style="color: #333;">模型:</strong> <span style="color: #666;">${api.model}</span></div>
</div>
`).join('');
document.getElementById('api-list').innerHTML = apiListHtml;
bindApiListEvents();
}
// 显示和隐藏面板的方法
const show = () => {
settingsPanel.style.display = 'flex';
settingsPanel.style.flexDirection = 'column';
updateSettingsPanelView();
};
const hide = () => {
settingsPanel.style.display = 'none';
};
return {
panel: settingsPanel,
show: show,
hide: hide,
updateView: updateSettingsPanelView
};
}
function createSettingsButton() {
const settingsButton = document.createElement('div');
settingsButton.id = 'translator-settings-button';
settingsButton.innerHTML = '⚙️';
settingsButton.title = '翻译设置';
settingsButton.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
width: 40px;
height: 40px;
background-color: rgba(66, 133, 244, 0.8);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
cursor: pointer;
z-index: 9999;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
`;
document.body.appendChild(settingsButton);
const settingsPanel = createSettingsPanel();
settingsButton.addEventListener('click', function() {
// 显示面板并设置flex布局
settingsPanel.panel.style.display = 'flex';
settingsPanel.panel.style.flexDirection = 'column';
// 强制确保有一个有效的当前标签
if (!settings.currentTab || (settings.currentTab !== 'general' && settings.currentTab !== 'api')) {
settings.currentTab = 'general';
}
// 由于没有updateSettingsPanelView函数,直接更新当前标签的显示
const generalTab = document.getElementById('general-tab');
const apiTab = document.getElementById('api-tab');
if (generalTab && apiTab) {
generalTab.style.display = settings.currentTab === 'general' ? 'block' : 'none';
apiTab.style.display = settings.currentTab === 'api' ? 'block' : 'none';
}
});
return settingsButton;
}
// 创建翻译整页按钮
function createTranslatePageButton() {
const button = document.createElement('div');
button.id = 'translate-page-button';
button.innerHTML = '翻译整页';
button.title = '翻译当前页面内容';
button.style.cssText = `
position: fixed;
bottom: 20px;
right: 70px;
padding: 8px 12px;
background-color: rgba(66, 133, 244, 0.8);
color: white;
border-radius: 20px;
font-size: 14px;
cursor: pointer;
z-index: 9999;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
display: flex;
align-items: center;
justify-content: center;
`;
document.body.appendChild(button);
button.addEventListener('click', function () {
if (isTranslatingFullPage) {
alert('正在翻译中,请稍候...');
return;
}
translateFullPage();
});
return button;
}
// 创建翻译进度条
function createProgressBar() {
const progressContainer = document.createElement('div');
progressContainer.id = 'translation-progress-container';
progressContainer.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 4px;
background-color: #f3f3f3;
z-index: 10001;
display: none;
`;
const progressBar = document.createElement('div');
progressBar.id = 'translation-progress-bar';
progressBar.style.cssText = `
height: 100%;
width: 0%;
background-color: #4285f4;
transition: width 0.3s;
`;
progressContainer.appendChild(progressBar);
document.body.appendChild(progressContainer);
return progressContainer;
}
// 深度优先遍历DOM树
function extractTextNodesFromElement(element, textSegments, depth = 0, excludeElements = null, isInReferencesSection = false, referencesSectionElement = null) {
// 如果没有传入排除元素,使用全局的
if (excludeElements === null) {
excludeElements = settings.excludeSelectors ?
Array.from(document.querySelectorAll(settings.excludeSelectors)) : [];
}
// 检查是否进入了参考文献区域
if (element === referencesSectionElement) {
isInReferencesSection = true;
}
// 如果这个元素在排除列表中,跳过
if (excludeElements.includes(element)) {
return;
}
try {
// 只对元素节点检查样式
if (element.nodeType === Node.ELEMENT_NODE) {
// 检查元素是否隐藏
const style = window.getComputedStyle(element);
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
return;
}
// 特殊处理参考文献条目
// 参考文献条目通常以数字加方括号开头,如 [1], [2] 等
if (isInReferencesSection && /^\s*\[\d+\]/.test(element.textContent)) {
console.log("检测到参考文献条目:", element.textContent.substring(0, 50));
textSegments.push({
text: element.textContent,
element: element,
htmlContent: element.innerHTML,
isReferenceItem: true,
isInReferencesSection: true,
original: element.textContent
});
return; // 参考文献条目作为整体处理,不再递归子节点
}
// 对于块级元素,我们在处理完所有子节点后添加换行标记
const isBlockElement = style.display === 'block' ||
style.display === 'flex' ||
style.display === 'grid' ||
element.tagName === 'DIV' ||
element.tagName === 'P' ||
element.tagName === 'SECTION' ||
element.tagName === 'ARTICLE' ||
element.tagName === 'LI';
// 递归处理子节点
for (let i = 0; i < element.childNodes.length; i++) {
extractTextNodesFromElement(element.childNodes[i], textSegments, depth + 1, excludeElements, isInReferencesSection, referencesSectionElement);
}
// 添加换行标记
if (isBlockElement && element.textContent.trim() && element.childNodes.length > 0) {
textSegments.push({ isNewLine: true });
}
} else if (element.nodeType === Node.TEXT_NODE) {
// 文本节点
const text = element.textContent.trim();
if (text) {
textSegments.push({
text: text,
node: element,
original: element.textContent,
parentElement: element.parentElement,
isInReferencesSection: isInReferencesSection
});
}
}
} catch (error) {
console.warn("处理元素时出错:", error);
}
}
// 合并文本段落,特殊处理参考文献条目
function mergeTextSegments(textSegments) {
// 合并相邻文本段落,特殊处理参考文献条目
const mergedSegments = [];
let currentSegment = { text: '', nodes: [], isReferenceItem: false };
let referenceItems = [];
for (const segment of textSegments) {
// 如果是参考文献条目,单独处理
if (segment.isReferenceItem) {
// 如果当前段落有内容,先保存
if (currentSegment.text.trim()) {
mergedSegments.push({ ...currentSegment });
currentSegment = { text: '', nodes: [], isReferenceItem: false };
}
// 添加参考文献条目
referenceItems.push(segment);
mergedSegments.push({
text: segment.text,
element: segment.element,
htmlContent: segment.htmlContent,
isReferenceItem: true,
nodes: [{ node: segment.element, original: segment.original }]
});
continue;
}
// 如果是参考文献区域但非条目,可能需要特殊处理
if (segment.isInReferencesSection && !segment.isNewLine) {
// 可能属于参考文献区域的普通文本,保持段落结构
if (currentSegment.isInReferencesSection !== true) {
// 如果当前段落不是参考文献区域,创建新段落
if (currentSegment.text.trim()) {
mergedSegments.push({ ...currentSegment });
}
currentSegment = {
text: segment.text,
nodes: segment.node ? [{ node: segment.node, original: segment.original }] : [],
isInReferencesSection: true
};
} else {
// 属于同一参考文献区域段落,合并文本
currentSegment.text += (currentSegment.text ? ' ' : '') + segment.text;
if (segment.node) {
currentSegment.nodes.push({
node: segment.node,
original: segment.original,
parentElement: segment.parentElement
});
}
}
continue;
}
if (segment.isNewLine) {
if (currentSegment.text.trim()) {
mergedSegments.push({ ...currentSegment });
currentSegment = { text: '', nodes: [], isReferenceItem: false };
}
continue;
}
currentSegment.text += (currentSegment.text ? ' ' : '') + segment.text;
if (segment.node) {
currentSegment.nodes.push({
node: segment.node,
original: segment.original,
parentElement: segment.parentElement
});
}
// 如果当前段落已经足够长,创建新段落
if (currentSegment.text.length >= settings.fullPageMaxSegmentLength) {
mergedSegments.push({ ...currentSegment });
currentSegment = { text: '', nodes: [], isReferenceItem: false };
}
}
// 添加最后一个段落
if (currentSegment.text.trim()) {
mergedSegments.push(currentSegment);
}
console.log(`提取到${mergedSegments.length}个文本段落,其中参考文献条目${referenceItems.length}个`);
return mergedSegments;
}
// 修改extractPageContent函数,使用合并函数
function extractPageContent() {
console.log("开始提取页面内容");
// 如果启用了自动识别文章主体,尝试识别
if (settings.detectArticleContent) {
const rawSegments = detectMainContent();
if (rawSegments) {
console.log("成功识别到文章主体内容");
return mergeTextSegments(rawSegments);
} else {
console.log("未能识别出文章主体,回退到常规翻译模式");
}
}
const contentSelector = settings.fullPageTranslationSelector || 'body';
const contentElement = document.querySelector(contentSelector);
if (!contentElement) {
console.error(`未找到匹配选择器的元素: ${contentSelector}`);
return [];
}
// 获取所有需要排除的元素
const excludeElements = settings.excludeSelectors ?
Array.from(document.querySelectorAll(settings.excludeSelectors)) : [];
// 检测参考文献区域
const referencesElements = Array.from(document.querySelectorAll('h2, h3, h4')).filter(el =>
el.textContent.toLowerCase().includes('reference') ||
el.textContent.toLowerCase().includes('bibliography') ||
el.textContent.includes('参考文献')
);
let isInReferencesSection = false;
let referencesSectionElement = null;
if (referencesElements.length > 0) {
referencesSectionElement = referencesElements[0];
console.log("检测到参考文献区域:", referencesSectionElement.textContent);
}
// 存储提取的文本段落
const textSegments = [];
// 深度优先遍历DOM树
extractTextNodesFromElement(contentElement, textSegments);
// 合并相邻文本段落
return mergeTextSegments(textSegments);
}
// 识别网页中的文章主体内容
function detectMainContent() {
// 可能的文章主体容器选择器
const possibleArticleSelectors = [
'article',
'.article',
'.post',
'.content',
'.post-content',
'.article-content',
'.entry-content',
'.main-content',
'main',
'#main',
'#content',
'.story',
'[itemprop="articleBody"]',
'[role="main"]',
// 添加Divi主题常用的内容容器选择器
'.et_pb_post_content',
'.et_pb_module.et_pb_post_content',
'.et_pb_text_inner',
'.et_pb_post_content_0_tb_body',
'.et_builder_inner_content',
// 添加更多通用选择器
'.single-content',
'.single-post-content',
'.page-content',
'.post-text'
];
// 尝试这些选择器,寻找包含最多内容的元素
let bestElement = null;
let maxTextLength = 0;
for (const selector of possibleArticleSelectors) {
const elements = document.querySelectorAll(selector);
for (const element of elements) {
// 计算文本内容长度
const textLength = element.textContent.trim().length;
// 如果比之前找到的更长,更新最佳元素
if (textLength > maxTextLength) {
maxTextLength = textLength;
bestElement = element;
}
}
}
// 如果找到了合适的元素
if (bestElement && maxTextLength > 300) { // 降低阈值,从500改为300个字符,更容易识别较短的文章
console.log(`检测到文章主体: ${bestElement.tagName}${bestElement.id ? '#'+bestElement.id : ''}${bestElement.className ? '.'+bestElement.className.replace(/\s+/g, '.') : ''}`);
// 获取所有需要排除的元素
const excludeElements = settings.excludeSelectors ?
Array.from(document.querySelectorAll(settings.excludeSelectors)) : [];
// 检测参考文献区域
const referencesElements = Array.from(bestElement.querySelectorAll('h2, h3, h4')).filter(el =>
el.textContent.toLowerCase().includes('reference') ||
el.textContent.toLowerCase().includes('bibliography') ||
el.textContent.includes('参考文献')
);
let isInReferencesSection = false;
let referencesSectionElement = null;
if (referencesElements.length > 0) {
referencesSectionElement = referencesElements[0];
console.log("在文章主体中检测到参考文献区域:", referencesSectionElement.textContent);
}
// 存储提取的文本段落
const textSegments = [];
// 深度优先遍历DOM树
extractTextNodesFromElement(bestElement, textSegments, 0, excludeElements, isInReferencesSection, referencesSectionElement);
return textSegments.length > 0 ? textSegments : null;
}
// 尝试基于内容区域比例的检测策略
if (!bestElement) {
console.log("尝试使用内容区域比例检测策略");
// 获取所有段落和文本块
const textBlocks = Array.from(document.querySelectorAll('p, article, .post, .content, div > h1 + p, div > h2 + p'));
// 按照父元素对文本块进行分组
const contentGroups = {};
for (const block of textBlocks) {
if (!block.textContent.trim()) continue;
// 获取所有父元素,直到body
let parent = block.parentElement;
while (parent && parent.tagName !== 'BODY') {
const key = parent.tagName + (parent.id ? '#' + parent.id : '') +
(parent.className ? '.' + parent.className.replace(/\s+/g, '.') : '');
contentGroups[key] = contentGroups[key] || { element: parent, textLength: 0 };
contentGroups[key].textLength += block.textContent.trim().length;
parent = parent.parentElement;
}
}
// 找出包含最多文本内容的容器
let bestContentGroup = null;
let maxGroupTextLength = 0;
for (const key in contentGroups) {
if (contentGroups[key].textLength > maxGroupTextLength) {
maxGroupTextLength = contentGroups[key].textLength;
bestContentGroup = contentGroups[key];
}
}
if (bestContentGroup && maxGroupTextLength > 300) {
console.log(`通过内容区域比例检测到文章主体: ${bestContentGroup.element.tagName}${bestContentGroup.element.id ? '#'+bestContentGroup.element.id : ''}${bestContentGroup.element.className ? '.'+bestContentGroup.element.className.replace(/\s+/g, '.') : ''}`);
// 获取所有需要排除的元素
const excludeElements = settings.excludeSelectors ?
Array.from(document.querySelectorAll(settings.excludeSelectors)) : [];
// 存储提取的文本段落
const textSegments = [];
// 深度优先遍历DOM树
extractTextNodesFromElement(bestContentGroup.element, textSegments, 0, excludeElements, false, null);
return textSegments.length > 0 ? textSegments : null;
}
}
return null;
}
// 翻译整个页面
function translateFullPage() {
// 确保使用最新的API设置
syncApiSettings();
console.log('全文翻译 - 确保使用最新API设置:', settings.apiEndpoint, settings.model);
if (!settings.apiKey) {
alert('请先在设置中配置API密钥');
const settingsPanel = document.getElementById('translator-settings-panel') || createSettingsPanel();
settingsPanel.style.display = 'block';
return;
}
// 如果当前已经在翻译中但被暂停了,则恢复翻译
if (isTranslatingFullPage && isTranslationPaused) {
isTranslationPaused = false;
const pauseBtn = document.getElementById('pause-translation-button');
if (pauseBtn) {
pauseBtn.textContent = '暂停翻译';
pauseBtn.title = '暂停当前的翻译任务';
}
// 恢复翻译,从最后翻译的段落的下一段开始
translateNextSegment(lastTranslatedIndex + 1);
return;
}
// 如果当前正在翻译且未暂停,则不做任何操作
if (isTranslatingFullPage && !isTranslationPaused) {
alert('正在翻译中,请稍候...');
return;
}
// 开始新的翻译任务
isTranslatingFullPage = true;
isTranslationPaused = false;
lastTranslatedIndex = -1;
// 清除可能存在的暂停/停止按钮
removeControlButtons();
// 清除可能存在的状态提示元素
const existingStatusElement = document.getElementById('translation-status');
if (existingStatusElement) {
existingStatusElement.remove();
}
// 提取页面内容
translationSegments = extractPageContent();
if (translationSegments.length === 0) {
alert('未找到可翻译的内容');
isTranslatingFullPage = false;
return;
}
// 显示进度条
const progressContainer = document.getElementById('translation-progress-container') || createProgressBar();
const progressBar = document.getElementById('translation-progress-bar');
progressContainer.style.display = 'block';
progressBar.style.width = '0%';
// 创建一个状态提示
const statusElement = document.createElement('div');
statusElement.id = 'translation-status';
statusElement.style.cssText = `
position: fixed;
top: 10px;
left: 50%;
transform: translateX(-50%);
background-color: rgba(66, 133, 244, 0.9);
color: white;
padding: 8px 15px;
border-radius: 20px;
font-size: 14px;
z-index: 10001;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
transition: background-color 0.3s;
min-width: 200px;
text-align: center;
`;
statusElement.textContent = `正在翻译 (0/${translationSegments.length})`;
document.body.appendChild(statusElement);
// 记录所有段落的原始文本,用于恢复
originalTexts = translationSegments.map(segment => ({
nodes: segment.nodes.map(n => ({ node: n.node, text: n.original }))
}));
// 创建用于切换原文/译文的按钮
createToggleButton();
// 创建暂停和停止按钮
createControlButtons();
// 开始翻译第一个段落
translateNextSegment(0);
}
// 创建暂停和停止按钮
function createControlButtons() {
// 创建暂停按钮
const pauseButton = document.createElement('div');
pauseButton.id = 'pause-translation-button';
pauseButton.style.cssText = `
position: fixed;
bottom: 20px;
right: 310px;
padding: 8px 12px;
background-color: rgba(255, 152, 0, 0.8);
color: white;
border-radius: 20px;
font-size: 14px;
cursor: pointer;
z-index: 9999;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
`;
pauseButton.textContent = '暂停翻译';
pauseButton.title = '暂停当前的翻译任务';
document.body.appendChild(pauseButton);
// 创建停止按钮
const stopButton = document.createElement('div');
stopButton.id = 'stop-translation-button';
stopButton.style.cssText = `
position: fixed;
bottom: 20px;
right: 240px;
padding: 8px 12px;
background-color: rgba(244, 67, 54, 0.8);
color: white;
border-radius: 20px;
font-size: 14px;
cursor: pointer;
z-index: 9999;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
`;
stopButton.textContent = '停止翻译';
stopButton.title = '停止当前的翻译任务';
document.body.appendChild(stopButton);
// 绑定暂停按钮事件
pauseButton.addEventListener('click', function() {
if (!isTranslatingFullPage) return;
isTranslationPaused = !isTranslationPaused;
if (isTranslationPaused) {
pauseButton.textContent = '继续翻译';
pauseButton.title = '继续未完成的翻译任务';
// 更新状态提示
const statusElement = document.getElementById('translation-status');
if (statusElement) {
statusElement.textContent = `翻译已暂停 (${lastTranslatedIndex + 1}/${translationSegments.length})`;
}
// 显示切换按钮,允许在暂停时切换查看原文/译文
const toggleButton = document.getElementById('toggle-translation-button');
if (toggleButton) toggleButton.style.display = 'block';
} else {
pauseButton.textContent = '暂停翻译';
pauseButton.title = '暂停当前的翻译任务';
// 恢复翻译
translateNextSegment(lastTranslatedIndex + 1);
}
});
// 绑定停止按钮事件
stopButton.addEventListener('click', function() {
if (!isTranslatingFullPage) return;
// 确认是否要停止翻译
if (confirm('确定要停止翻译吗?已翻译的内容将保留。')) {
// 停止翻译
isTranslatingFullPage = false;
isTranslationPaused = false;
// 更新状态提示
const statusElement = document.getElementById('translation-status');
if (statusElement) {
statusElement.textContent = `翻译已停止 (${lastTranslatedIndex + 1}/${translationSegments.length})`;
// 添加关闭按钮
if (!statusElement.querySelector('.close-btn')) {
const closeButton = document.createElement('span');
closeButton.className = 'close-btn';
closeButton.style.cssText = `
margin-left: 10px;
cursor: pointer;
font-weight: bold;
`;
closeButton.textContent = '×';
closeButton.addEventListener('click', function() {
statusElement.remove();
const progressContainer = document.getElementById('translation-progress-container');
if (progressContainer) progressContainer.style.display = 'none';
});
statusElement.appendChild(closeButton);
}
}
// 显示切换按钮
const toggleButton = document.getElementById('toggle-translation-button');
if (toggleButton) toggleButton.style.display = 'block';
// 删除控制按钮
removeControlButtons();
}
});
}
// 移除控制按钮
function removeControlButtons() {
const pauseButton = document.getElementById('pause-translation-button');
if (pauseButton) pauseButton.remove();
const stopButton = document.getElementById('stop-translation-button');
if (stopButton) stopButton.remove();
}
// 创建切换按钮
function createToggleButton() {
// 检查是否已存在切换按钮
let toggleButton = document.getElementById('toggle-translation-button');
if (!toggleButton) {
toggleButton = document.createElement('div');
toggleButton.id = 'toggle-translation-button';
toggleButton.style.cssText = `
position: fixed;
bottom: 20px;
right: 180px;
padding: 8px 12px;
background-color: rgba(66, 133, 244, 0.8);
color: white;
border-radius: 20px;
font-size: 14px;
cursor: pointer;
z-index: 9999;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
display: none;
`;
toggleButton.textContent = '查看原文';
toggleButton.dataset.showing = 'translation';
document.body.appendChild(toggleButton);
// 用于跟踪当前显示状态
let isShowingTranslation = true;
// 创建映射,用于跟踪每个节点的翻译状态
const nodeTranslationMap = new Map();
toggleButton.addEventListener('click', function () {
isShowingTranslation = !isShowingTranslation;
toggleButton.textContent = isShowingTranslation ? '查看原文' : '查看译文';
toggleButton.dataset.showing = isShowingTranslation ? 'translation' : 'original';
if (isShowingTranslation) {
// 恢复译文
translationSegments.forEach((segment, index) => {
// 特殊处理参考文献条目
if (segment.isReferenceItem && segment.translation && segment.element) {
segment.element.innerHTML = segment.translation;
return;
}
if (segment.translation) {
if (segment.nodes.length === 1) {
// 单节点情况,直接应用翻译
const nodeInfo = segment.nodes[0];
if (nodeInfo.node && nodeInfo.node.nodeType === Node.TEXT_NODE) {
nodeInfo.node.textContent = segment.translation;
// 记录这个节点已经被翻译过
nodeTranslationMap.set(nodeInfo.node, true);
}
} else {
// 多节点情况,使用比例分配
const totalOriginalLength = segment.nodes.reduce(
(sum, nodeInfo) => sum + (nodeInfo.original ? nodeInfo.original.length : 0), 0);
if (totalOriginalLength > 0) {
let startPos = 0;
for (let i = 0; i < segment.nodes.length; i++) {
const nodeInfo = segment.nodes[i];
if (nodeInfo.node && nodeInfo.node.nodeType === Node.TEXT_NODE && nodeInfo.original) {
// 计算该节点在原始文本中的比例
const ratio = nodeInfo.original.length / totalOriginalLength;
// 计算应该分配给该节点的翻译文本长度
const chunkLength = Math.round(segment.translation.length * ratio);
// 提取翻译文本的一部分
let chunk = '';
if (i === segment.nodes.length - 1) {
// 最后一个节点,获取剩余所有文本
chunk = segment.translation.substring(startPos);
} else {
// 非最后节点,按比例获取
chunk = segment.translation.substring(startPos, startPos + chunkLength);
startPos += chunkLength;
}
// 更新节点文本
nodeInfo.node.textContent = chunk;
// 记录这个节点已经被翻译过
nodeTranslationMap.set(nodeInfo.node, true);
}
}
}
}
}
});
} else {
// 恢复原文 - 使用原始数据而不是依赖当前DOM状态
nodeTranslationMap.clear(); // 清除翻译状态记录
// 先处理参考文献条目
translationSegments.forEach((segment) => {
if (segment.isReferenceItem && segment.element && segment.originalHtml) {
segment.element.innerHTML = segment.originalHtml;
}
});
// 再处理普通段落
originalTexts.forEach((originalSegment) => {
originalSegment.nodes.forEach(nodeInfo => {
if (nodeInfo.node && nodeInfo.node.nodeType === Node.TEXT_NODE) {
nodeInfo.node.textContent = nodeInfo.text;
}
});
});
}
});
}
return toggleButton;
}
// 添加一个专门用于翻译参考文献条目的函数
function translateReferenceItem(referenceItem, callback) {
if (!referenceItem || !referenceItem.text) {
callback(null);
return;
}
console.log("翻译参考文献条目:", referenceItem.text.substring(0, 50) + "...");
// 如果有HTML内容,使用更精确的方法
if (referenceItem.htmlContent) {
// 提取HTML中的纯文本部分
const tempDiv = document.createElement('div');
tempDiv.innerHTML = referenceItem.htmlContent;
// 递归处理HTML元素,只翻译文本节点
function processNode(node, callback) {
if (node.nodeType === Node.TEXT_NODE) {
const text = node.textContent.trim();
if (text && text.length > 1) {
// 翻译文本节点
translateText(text, function (result) {
if (result.type === 'complete' || result.type === 'stream-end') {
node.textContent = result.content;
callback(true);
} else {
// 保持原文
callback(false);
}
}, false);
} else {
callback(true); // 空文本,直接继续
}
} else if (node.nodeType === Node.ELEMENT_NODE) {
// 对于元素节点,递归处理其子节点
const childNodes = Array.from(node.childNodes);
processNodeSequentially(childNodes, 0, function () {
callback(true);
});
} else {
callback(true); // 其他类型节点,直接继续
}
}
// 按顺序处理节点列表
function processNodeSequentially(nodes, index, finalCallback) {
if (index >= nodes.length) {
finalCallback();
return;
}
processNode(nodes[index], function (success) {
// 无论成功与否,继续处理下一个节点
setTimeout(() => processNodeSequentially(nodes, index + 1, finalCallback), 10);
});
}
// 开始处理整个HTML片段
processNodeSequentially(Array.from(tempDiv.childNodes), 0, function () {
// 处理完成后,返回完整的翻译后HTML
callback(tempDiv.innerHTML);
});
} else {
// 如果没有HTML内容,直接翻译文本
translateText(referenceItem.text, function (result) {
if (result.type === 'complete' || result.type === 'stream-end') {
callback(result.content);
} else {
callback(null);
}
}, false);
}
}
// 创建翻译按钮
function createTranslateButton() {
const button = document.createElement('div');
button.className = 'translate-button';
button.innerHTML = '翻译';
button.style.cssText = `
position: absolute;
background-color: rgba(66, 133, 244, 0.8);
color: white;
padding: 5px 10px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
z-index: 9999;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
opacity: 0.8;
transition: opacity 0.2s;
`;
button.addEventListener('mouseover', function () {
button.style.opacity = '1';
});
button.addEventListener('mouseout', function () {
button.style.opacity = '0.8';
});
return button;
}
// 创建历史记录面板
function createHistoryPanel() {
const panel = document.createElement('div');
panel.id = 'translator-history-panel';
panel.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 600px;
max-height: 80vh;
background-color: white;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
z-index: 10000;
display: none;
padding: 20px;
overflow-y: auto;
`;
panel.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
<h2 style="margin: 0;">翻译历史</h2>
<div style="display: flex; gap: 10px;">
<button id="clear-history" style="padding: 5px 10px; color: #666;">清除历史</button>
<div id="close-history" style="cursor: pointer; font-size: 20px; color: #666;">×</div>
</div>
</div>
<div id="history-list"></div>
`;
document.body.appendChild(panel);
// 清除历史记录按钮
document.getElementById('clear-history').addEventListener('click', function () {
if (confirm('确定要清除所有翻译历史吗?')) {
translationHistory = [];
GM_setValue('translationHistory', []);
updateHistoryList();
}
});
// 关闭按钮
document.getElementById('close-history').addEventListener('click', function () {
panel.style.display = 'none';
});
return panel;
}
// 创建收藏夹面板
function createFavoritesPanel() {
const panel = document.createElement('div');
panel.id = 'translator-favorites-panel';
panel.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 600px;
max-height: 80vh;
background-color: white;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
z-index: 10000;
display: none;
padding: 20px;
overflow-y: auto;
`;
panel.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
<h2 style="margin: 0;">收藏夹</h2>
<div style="display: flex; gap: 10px;">
<button id="clear-favorites" style="padding: 5px 10px; color: #666;">清除收藏</button>
<div id="close-favorites" style="cursor: pointer; font-size: 20px; color: #666;">×</div>
</div>
</div>
<div id="favorites-list"></div>
`;
document.body.appendChild(panel);
// 清除收藏按钮
document.getElementById('clear-favorites').addEventListener('click', function () {
if (confirm('确定要清除所有收藏吗?')) {
translationFavorites = [];
GM_setValue('translationFavorites', []);
updateFavoritesList();
}
});
// 关闭按钮
document.getElementById('close-favorites').addEventListener('click', function () {
panel.style.display = 'none';
});
return panel;
}
// 更新历史记录列表
function updateHistoryList() {
const historyList = document.getElementById('history-list');
if (!historyList) return;
if (translationHistory.length === 0) {
historyList.innerHTML = '<div style="text-align: center; color: #666; padding: 20px;">暂无翻译历史</div>';
return;
}
// 清除之前的内容
historyList.innerHTML = '';
// 为每个历史记录创建DOM元素
translationHistory.forEach((item, index) => {
const historyItem = document.createElement('div');
historyItem.style.cssText = 'border-bottom: 1px solid #eee; padding: 10px 0;';
const header = document.createElement('div');
header.style.cssText = 'display: flex; justify-content: space-between; margin-bottom: 5px;';
const timestamp = document.createElement('span');
timestamp.style.color = '#666';
timestamp.textContent = item.timestamp;
const buttons = document.createElement('div');
const copyBtn = document.createElement('button');
copyBtn.textContent = '复制';
copyBtn.style.cssText = 'padding: 2px 5px; margin-right: 5px;';
copyBtn.addEventListener('click', function () {
copyTranslation(index);
});
const favoriteBtn = document.createElement('button');
favoriteBtn.textContent = '收藏';
favoriteBtn.style.cssText = 'padding: 2px 5px;';
favoriteBtn.addEventListener('click', function () {
addToFavorites(translationHistory[index]);
favoriteBtn.textContent = '已收藏';
favoriteBtn.disabled = true;
});
buttons.appendChild(copyBtn);
buttons.appendChild(favoriteBtn);
header.appendChild(timestamp);
header.appendChild(buttons);
const sourceDiv = document.createElement('div');
sourceDiv.style.marginBottom = '5px';
sourceDiv.innerHTML = `<strong>原文:</strong>${item.source}`;
const translationDiv = document.createElement('div');
translationDiv.innerHTML = `<strong>译文:</strong>${item.translation}`;
historyItem.appendChild(header);
historyItem.appendChild(sourceDiv);
historyItem.appendChild(translationDiv);
historyList.appendChild(historyItem);
});
}
// 更新收藏夹列表
function updateFavoritesList() {
const favoritesList = document.getElementById('favorites-list');
if (!favoritesList) return;
if (translationFavorites.length === 0) {
favoritesList.innerHTML = '<div style="text-align: center; color: #666; padding: 20px;">暂无收藏内容</div>';
return;
}
// 清除之前的内容
favoritesList.innerHTML = '';
// 为每个收藏项创建DOM元素
translationFavorites.forEach((item, index) => {
const favoriteItem = document.createElement('div');
favoriteItem.style.cssText = 'border-bottom: 1px solid #eee; padding: 10px 0;';
const header = document.createElement('div');
header.style.cssText = 'display: flex; justify-content: space-between; margin-bottom: 5px;';
const timestamp = document.createElement('span');
timestamp.style.color = '#666';
timestamp.textContent = item.timestamp;
const buttons = document.createElement('div');
const copyBtn = document.createElement('button');
copyBtn.textContent = '复制';
copyBtn.style.cssText = 'padding: 2px 5px; margin-right: 5px;';
copyBtn.addEventListener('click', function () {
copyFavorite(index);
});
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '删除';
deleteBtn.style.cssText = 'padding: 2px 5px;';
deleteBtn.addEventListener('click', function () {
removeFromFavorites(index);
});
buttons.appendChild(copyBtn);
buttons.appendChild(deleteBtn);
header.appendChild(timestamp);
header.appendChild(buttons);
const sourceDiv = document.createElement('div');
sourceDiv.style.marginBottom = '5px';
sourceDiv.innerHTML = `<strong>原文:</strong>${item.source}`;
const translationDiv = document.createElement('div');
translationDiv.style.marginBottom = '5px';
translationDiv.innerHTML = `<strong>译文:</strong>${item.translation}`;
const sourceUrlDiv = document.createElement('div');
sourceUrlDiv.style.cssText = 'font-size: 12px; color: #666;';
const sourceTitleDiv = document.createElement('div');
sourceTitleDiv.style.marginBottom = '2px';
sourceTitleDiv.innerHTML = '<strong>来源:</strong>';
const sourceLink = document.createElement('a');
sourceLink.href = item.url;
sourceLink.target = '_blank';
sourceLink.style.cssText = 'color: #4285f4; text-decoration: none;';
sourceLink.textContent = item.title || item.url;
sourceTitleDiv.appendChild(sourceLink);
const urlDiv = document.createElement('div');
urlDiv.style.wordBreak = 'break-all';
urlDiv.textContent = item.url;
sourceUrlDiv.appendChild(sourceTitleDiv);
sourceUrlDiv.appendChild(urlDiv);
favoriteItem.appendChild(header);
favoriteItem.appendChild(sourceDiv);
favoriteItem.appendChild(translationDiv);
favoriteItem.appendChild(sourceUrlDiv);
favoritesList.appendChild(favoriteItem);
});
}
// 添加到历史记录
function addToHistory(source, translation) {
const timestamp = new Date().toLocaleString();
translationHistory.unshift({ source, translation, timestamp });
// 限制历史记录数量
if (translationHistory.length > settings.maxHistoryItems) {
translationHistory.pop();
}
GM_setValue('translationHistory', translationHistory);
updateHistoryList();
}
// 添加到收藏夹
function addToFavorites(item) {
// 添加URL信息
item.url = window.location.href;
item.title = document.title;
// 检查是否已存在
if (!translationFavorites.some(fav => fav.source === item.source)) {
translationFavorites.unshift(item);
// 限制收藏数量
if (translationFavorites.length > settings.maxFavoritesItems) {
translationFavorites.pop();
}
GM_setValue('translationFavorites', translationFavorites);
updateFavoritesList();
}
}
// 从收藏夹移除
function removeFromFavorites(index) {
translationFavorites.splice(index, 1);
GM_setValue('translationFavorites', translationFavorites);
updateFavoritesList();
}
// 复制翻译结果
function copyTranslation(index) {
const item = translationHistory[index];
if (item) {
navigator.clipboard.writeText(item.translation).then(() => {
alert('已复制到剪贴板!');
});
}
}
// 复制收藏的翻译
function copyFavorite(index) {
const item = translationFavorites[index];
if (item) {
navigator.clipboard.writeText(item.translation).then(() => {
alert('已复制到剪贴板!');
});
}
}
// 注册(不可用)菜单命令
GM_registerMenuCommand('翻译整页', translateFullPage);
GM_registerMenuCommand('查看翻译历史', function () {
const panel = document.getElementById('translator-history-panel') || createHistoryPanel();
panel.style.display = 'block';
updateHistoryList();
});
GM_registerMenuCommand('查看收藏夹', function () {
const panel = document.getElementById('translator-favorites-panel') || createFavoritesPanel();
panel.style.display = 'block';
updateFavoritesList();
});
// 创建翻译弹窗
function createTranslationPopup() {
// 主容器
const popup = document.createElement('div');
popup.className = 'translation-popup';
popup.style.cssText = `
position: absolute;
background-color: white;
min-width: 200px;
max-width: 400px;
max-height: 80vh;
display: flex;
flex-direction: column;
padding: 0;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
z-index: 10000;
font-size: 14px;
line-height: 1.5;
overflow: hidden;
user-select: none; /* 防止拖动时选中文本 */
`;
// ===== 顶部区域(可拖动) =====
const header = document.createElement('div');
header.className = 'translation-header';
header.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 15px;
border-bottom: 1px solid #eee;
flex-shrink: 0;
cursor: move; /* 指示可拖动 */
background-color: #f9f9f9; /* 轻微背景色以区分可拖动区域 */
border-radius: 8px 8px 0 0;
position: relative; /* 确保关闭按钮定位正确 */
`;
// 拖动提示图标
const dragHandleIcon = document.createElement('div');
dragHandleIcon.className = 'drag-handle-icon';
dragHandleIcon.innerHTML = '⋮⋮';
dragHandleIcon.style.cssText = `
margin-right: 6px;
color: #999;
font-size: 10px;
transform: rotate(90deg);
display: inline-block;
`;
// 源语言显示区域
const sourceLanguage = document.createElement('div');
sourceLanguage.className = 'source-language';
sourceLanguage.style.cssText = `
color: #666;
font-size: 12px;
margin-left: 8px;
flex-grow: 1;
`;
sourceLanguage.textContent = '源语言: 加载中...';
// 关闭按钮
const closeButton = document.createElement('div');
closeButton.className = 'close-button';
closeButton.innerHTML = '×';
closeButton.style.cssText = `
width: 20px;
height: 20px;
text-align: center;
line-height: 20px;
font-size: 18px;
cursor: pointer;
color: #666;
background-color: white;
border-radius: 50%;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
margin-left: 10px;
`;
// 组装头部
header.appendChild(dragHandleIcon);
header.appendChild(sourceLanguage);
header.appendChild(closeButton);
// ===== 内容区域(可滚动) =====
const contentWrapper = document.createElement('div');
contentWrapper.className = 'content-wrapper';
contentWrapper.style.cssText = `
padding: 10px 15px;
flex-grow: 1;
overflow-y: auto;
max-height: calc(80vh - 90px); /* 减去头部和底部的高度 */
`;
// 创建内容区域
const content = document.createElement('div');
content.className = 'translation-content';
content.style.cssText = `
white-space: pre-wrap;
`;
// 添加内容区域到内容包装器
contentWrapper.appendChild(content);
// ===== 底部区域 =====
const footer = document.createElement('div');
footer.className = 'translation-footer';
footer.style.cssText = `
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 10px 15px;
border-top: 1px solid #eee;
background-color: white;
flex-shrink: 0;
`;
// 复制按钮
const copyButton = document.createElement('button');
copyButton.className = 'copy-button';
copyButton.textContent = '复制译文';
copyButton.style.cssText = `
padding: 5px 10px;
background-color: #4285f4;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
`;
// 收藏按钮
const favoriteButton = document.createElement('button');
favoriteButton.className = 'favorite-button';
favoriteButton.textContent = '收藏';
favoriteButton.style.cssText = `
padding: 5px 10px;
background-color: #fbbc05;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
`;
// 添加按钮到底部
footer.appendChild(copyButton);
footer.appendChild(favoriteButton);
// ===== 组装整个弹窗 =====
popup.appendChild(header);
popup.appendChild(contentWrapper);
popup.appendChild(footer);
// ===== 事件处理 =====
// 绑定事件 - 改进版
let isDragging = false;
let offsetX, offsetY;
let popupRect = null;
// 存储当前的鼠标事件处理函数,用于后续移除
let mouseMoveHandler, mouseUpHandler;
// 关闭按钮事件
closeButton.addEventListener('click', function() {
if (typeof popup.cleanup === 'function') {
popup.cleanup();
}
popup.remove();
});
// 复制按钮事件
copyButton.addEventListener('click', function() {
const translationText = content.textContent;
navigator.clipboard.writeText(translationText)
.then(() => {
copyButton.textContent = '✓ 已复制';
setTimeout(() => {
copyButton.textContent = '复制译文';
}, 2000);
})
.catch(err => {
console.error('无法复制文本: ', err);
});
});
// 收藏按钮事件
favoriteButton.addEventListener('click', function() {
const translationText = content.textContent;
const sourceText = sourceLanguage.textContent.replace('源语言: ', '');
addToFavorites({
source: sourceText,
translation: translationText
});
favoriteButton.textContent = '✓ 已收藏';
setTimeout(() => {
favoriteButton.textContent = '收藏';
}, 2000);
});
// 鼠标按下事件 - 拖动开始
header.addEventListener('mousedown', function(e) {
// 如果点击的是关闭按钮,不启动拖动
if (e.target === closeButton) return;
isDragging = true;
// 获取弹窗的当前位置和尺寸
popupRect = popup.getBoundingClientRect();
// 调整偏移量,只考虑相对于视口的位置,不考虑滚动
offsetX = e.clientX - popupRect.left;
offsetY = e.clientY - popupRect.top;
// 添加临时样式增强拖动体验
popup.style.transition = 'none';
popup.style.opacity = '0.95';
popup.style.boxShadow = '0 5px 15px rgba(0, 0, 0, 0.3)';
document.body.style.cursor = 'move';
// 防止文本选择和其他默认行为
e.preventDefault();
// 临时添加鼠标移动和释放事件处理函数
mouseMoveHandler = handleMouseMove;
mouseUpHandler = handleMouseUp;
document.addEventListener('mousemove', mouseMoveHandler);
document.addEventListener('mouseup', mouseUpHandler);
});
// 鼠标移动处理函数
function handleMouseMove(e) {
if (!isDragging) return;
// 计算新位置,考虑页面滚动偏移量
const left = e.clientX - offsetX + window.scrollX;
const top = e.clientY - offsetY + window.scrollY;
// 防止拖出窗口边界
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
const maxLeft = window.scrollX + windowWidth - 100; // 保留至少100px在视口内
const maxTop = window.scrollY + windowHeight - 50; // 保留至少50px在视口内
// 设置约束后的位置
popup.style.left = `${Math.max(window.scrollX, Math.min(maxLeft, left))}px`;
popup.style.top = `${Math.max(window.scrollY, Math.min(maxTop, top))}px`;
// 防止事件传递给下层元素
e.stopPropagation();
e.preventDefault();
}
// 鼠标释放处理函数
function handleMouseUp() {
if (!isDragging) return;
isDragging = false;
// 恢复正常样式
popup.style.opacity = '1';
popup.style.boxShadow = '0 2px 10px rgba(0, 0, 0, 0.2)';
document.body.style.cursor = 'auto';
// 移除临时事件处理函数
document.removeEventListener('mousemove', mouseMoveHandler);
document.removeEventListener('mouseup', mouseUpHandler);
}
// 清理函数 - 当弹窗被移除时调用
function cleanup() {
// 确保移除所有事件监听器,防止内存泄漏
document.removeEventListener('mousemove', mouseMoveHandler);
document.removeEventListener('mouseup', mouseUpHandler);
}
// 将清理函数和关键元素引用附加到弹窗上,方便外部访问
popup.cleanup = cleanup;
popup.content = content;
popup.sourceLanguage = sourceLanguage;
popup.contentWrapper = contentWrapper;
return popup;
}
// 创建加载动画
function createLoadingAnimation() {
const loading = document.createElement('div');
loading.className = 'loading-animation';
loading.style.cssText = `
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-top-color: #4285f4;
animation: spin 1s ease-in-out infinite;
margin-right: 10px;
`;
const style = document.createElement('style');
style.textContent = `
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`;
document.head.appendChild(style);
return loading;
}
// 调用 API 进行翻译
function translateText(text, callback, retryWithoutStreaming = false, context = null) {
// 确保使用最新的API设置
syncApiSettings();
if (!settings.apiKey) {
callback({
type: 'error',
content: '错误:请先设置 API 密钥'
});
return;
}
// 调试模式
const debugMode = true;
const debugLog = debugMode ? console.log : function () { };
// 创建内容收集器和状态跟踪
let collectedContent = '';
let lastProcessedLength = 0; // 跟踪上次处理的响应长度
let responseBuffer = ''; // 用于存储部分响应,解决跨块JSON问题
// 确定是否使用流式响应
const useStreaming = retryWithoutStreaming ? false : settings.useStreaming;
// 通知回调开始处理
if (useStreaming) {
callback({
type: 'stream-start',
content: ''
});
}
debugLog(`开始翻译请求,文本长度: ${text.length}, 使用流式响应: ${useStreaming}, 模型: ${settings.model}`);
// 准备消息数组
const messages = [
{
role: "system",
content: settings.systemPrompt + " 请保持原文本的段落格式,每个段落之间应当保留一个空行。"
}
];
// 如果有上下文且启用了上下文功能,添加上下文消息
if (settings.useTranslationContext && context && context.length > 0) {
messages.push({
role: "system",
content: "以下是之前已经翻译的上下文,供你参考以保持翻译的一致性和连贯性:"
});
// 添加上下文
context.forEach(ctx => {
messages.push({
role: "user",
content: ctx.original
});
messages.push({
role: "assistant",
content: ctx.translation
});
});
}
// 添加当前要翻译的文本
messages.push({
role: "user",
content: text
});
debugLog(messages)
debugLog(settings.temperature)
GM_xmlhttpRequest({
method: 'POST',
url: settings.apiEndpoint,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${settings.apiKey}`
},
responseType: useStreaming ? 'stream' : '',
timeout: 60000,
data: JSON.stringify({
model: settings.model,
messages: messages,
temperature: settings.temperature,
stream: useStreaming,
top_p: 1,
frequency_penalty: 0,
presence_penalty: 0,
max_tokens: 4000
}),
onloadstart: function () {
if (useStreaming) {
debugLog("流式请求已开始");
} else {
debugLog("标准请求已开始");
debugLog(settings.temperature)
}
},
onprogress: function (response) {
if (!useStreaming) return; // 非流式模式不处理进度更新
try {
// 获取完整响应文本
const responseText = response.responseText || '';
// 如果响应没变长,不处理
if (!responseText || responseText.length <= lastProcessedLength) {
return;
}
debugLog(`进度更新: 响应总长度=${responseText.length}, 上次处理位置=${lastProcessedLength}`);
// 获取新增内容
const newChunk = responseText.substring(lastProcessedLength);
debugLog(`接收新数据: ${newChunk.length}字节`);
// 更新处理位置要放在处理数据前,防止数据处理期间发生新的onprogress事件
lastProcessedLength = responseText.length;
// 追加到缓冲区并处理
responseBuffer += newChunk;
// 不同API可能有不同的流式输出格式,我们需要支持多种格式
// 1. OpenAI风格: "data: {\"id\":\"...\",\"choices\":[{\"delta\":{\"content\":\"文本片段\"}}]}\n"
// 2. 简单文本块风格: 直接是文本内容
let newContent = '';
let dataMatches = [];
// 尝试提取所有"data: {...}"格式的行
const regex = /data: ({.+?})\n/g;
let match;
// 收集所有匹配项
while ((match = regex.exec(responseBuffer)) !== null) {
dataMatches.push(match[0]);
try {
// 提取JSON部分并解析
const jsonStr = match[1];
const data = JSON.parse(jsonStr);
// 从JSON提取文本内容
if (data.choices && data.choices[0]) {
if (data.choices[0].delta && data.choices[0].delta.content) {
// OpenAI风格的delta格式
newContent += data.choices[0].delta.content;
} else if (data.choices[0].text) {
// 有些API使用text字段
newContent += data.choices[0].text;
} else if (data.choices[0].message && data.choices[0].message.content) {
// 完整消息格式
newContent += data.choices[0].message.content;
}
}
} catch (parseError) {
// 解析出错,记录但继续处理
debugLog(`JSON解析失败: ${parseError.message}, 内容: ${match[0].substring(0, 50)}...`);
}
}
// 检查是否找到了标准格式的数据行
if (dataMatches.length > 0) {
// 从缓冲区中移除已处理的部分,但保留可能不完整的最后部分
const lastMatchEndIndex = regex.lastIndex;
if (lastMatchEndIndex > 0 && lastMatchEndIndex < responseBuffer.length) {
responseBuffer = responseBuffer.substring(lastMatchEndIndex);
} else {
responseBuffer = '';
}
debugLog(`找到${dataMatches.length}个数据块,提取了${newContent.length}字符的内容`);
} else {
// 如果没有找到标准格式,尝试按行分割处理
const lines = responseBuffer.split('\n');
// 保留最后一行作为可能的不完整行
responseBuffer = lines.pop() || '';
// 处理每一行
for (const line of lines) {
// 尝试提取"data: "后面的内容
if (line.trim().startsWith('data: ')) {
try {
const content = line.substring(6).trim();
if (content === '[DONE]') {
debugLog("收到流结束标记");
continue;
}
// 尝试解析JSON
const data = JSON.parse(content);
if (data.choices && data.choices[0]) {
if (data.choices[0].delta && data.choices[0].delta.content) {
newContent += data.choices[0].delta.content;
} else if (data.choices[0].text) {
newContent += data.choices[0].text;
} else if (data.choices[0].message && data.choices[0].message.content) {
newContent += data.choices[0].message.content;
}
}
} catch (e) {
// JSON解析失败,可能是普通文本或特殊格式
debugLog(`处理行出错: ${e.message}, 行内容: ${line.substring(0, 50)}...`);
}
} else if (line.trim() && !line.includes('event:') && !line.includes('id:')) {
// 如果不是控制行且非空,可能是直接的文本内容
// 一些API直接发送文本而不是JSON
newContent += line + '\n';
}
}
}
// 如果有新内容
if (newContent) {
collectedContent += newContent;
debugLog(`新增内容: ${newContent.length}字符, 当前总内容: ${collectedContent.length}字符`);
// 发送更新
callback({
type: 'stream-progress',
content: collectedContent
});
} else if (dataMatches.length > 0 || responseBuffer.includes('data:')) {
// 找到了数据行但没有提取到内容,可能是API发送的是控制消息
debugLog("收到数据但未提取到新内容,可能是控制消息");
} else {
debugLog("本次更新没有提取到新内容");
}
} catch (e) {
console.error("处理流数据错误:", e);
debugLog("错误详情:", e.stack);
// 即使出错也更新位置,防止重复处理导致死循环
if (response && response.responseText) {
lastProcessedLength = response.responseText.length;
}
}
},
onload: function (response) {
try {
// 检查HTTP状态码
if (response.status && response.status !== 200) {
// 非200状态码,记录错误但不替换原文
console.error(`API返回非200状态码: ${response.status}`);
let errorMsg = `API返回错误状态码: ${response.status}`;
// 尝试从响应中提取更详细的错误信息
if (response.responseText) {
try {
const errorData = JSON.parse(response.responseText);
if (errorData.error) {
errorMsg += ` - ${errorData.error.message || errorData.error.type || JSON.stringify(errorData.error)}`;
}
} catch (e) {
// 无法解析JSON,使用原始响应内容
if (response.responseText.length < 100) {
errorMsg += ` - ${response.responseText}`;
}
}
}
callback({
type: 'error',
content: errorMsg,
statusCode: response.status
});
return;
}
// 检查是否已经收集到内容(通过流式API)
if (useStreaming && collectedContent) {
// 已收集到了内容 - 直接使用收集到的内容
debugLog("onload: 使用已收集的流式内容,长度:", collectedContent.length);
callback({
type: 'stream-end',
content: collectedContent
});
return;
}
debugLog(`onload: ${useStreaming ? '没有收集到流式内容' : '使用标准响应'}, 尝试处理完整响应`);
// 在流式模式下,如果没有responseText但我们仍然到达onload,这可能是一个API限制
if (useStreaming && (!response || typeof response.responseText !== 'string' || response.responseText.trim() === '')) {
// 检查是否有响应头,可能表明API连接是成功的
if (response && response.responseHeaders) {
// 尝试从缓冲区中恢复,有时onload触发时缓冲区仍有未处理的内容
if (responseBuffer.trim()) {
debugLog("onload: 从缓冲区恢复内容,尝试解析");
try {
// 处理缓冲区中的内容
let content = '';
const lines = responseBuffer.split('\n').filter(line => line.trim());
for (const line of lines) {
if (line.startsWith('data: ') && line !== 'data: [DONE]') {
try {
const jsonStr = line.substring(6).trim();
const data = JSON.parse(jsonStr);
if (data.choices && data.choices[0]) {
if (data.choices[0].delta && data.choices[0].delta.content) {
content += data.choices[0].delta.content;
} else if (data.choices[0].message && data.choices[0].message.content) {
content += data.choices[0].message.content;
}
}
} catch (e) {
debugLog("缓冲区解析出错:", e.message);
}
}
}
if (content) {
// 找到了内容,使用它
callback({
type: 'complete',
content: content.trim()
});
return;
}
} catch (e) {
debugLog("缓冲区处理失败:", e.message);
}
}
if (response.responseHeaders.includes("content-type:text/event-stream")) {
debugLog("onload: 检测到有效的SSE响应头,但无responseText。尝试使用非流式模式重试...");
// 自动重试:使用非流式模式
if (!retryWithoutStreaming) {
debugLog("切换到非流式模式重试翻译请求");
translateText(text, callback, true);
return;
} else {
// 如果这是重试尝试但仍然失败,则报告错误
throw new Error("即使在非流式模式下,API也未能返回翻译内容。请检查API密钥和网络连接。");
}
} else {
// 其他内容类型,可能是API错误
throw new Error(`响应没有包含预期的内容。响应头: ${response.responseHeaders.substring(0, 100)}...`);
}
} else {
// 完全无效的响应
throw new Error("响应对象无效或不包含内容。请检查API端点和密钥是否正确。");
}
}
// 有responseText,处理它
const responseText = response.responseText;
debugLog(`获取到响应文本,长度: ${responseText.length}`);
// 检查是否是SSE格式(以'data: '开头)
if (responseText.trim().startsWith('data: ')) {
debugLog("检测到SSE格式响应,解析数据...");
// 处理SSE格式
const lines = responseText.split('\n');
let fullContent = '';
let processedCount = 0;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line.startsWith('data: ') && line !== 'data: [DONE]') {
try {
// 提取JSON部分
const jsonData = line.substring(6);
const data = JSON.parse(jsonData);
processedCount++;
if (data.choices && data.choices[0]) {
// 对于非流式响应中的SSE格式,提取content
if (data.choices[0].delta && data.choices[0].delta.content) {
fullContent += data.choices[0].delta.content;
}
// 支持普通响应格式
else if (data.choices[0].message && data.choices[0].message.content) {
fullContent += data.choices[0].message.content;
}
// 支持一些API的不同格式
else if (data.choices[0].text) {
fullContent += data.choices[0].text;
}
}
} catch (e) {
console.error("处理单行SSE数据出错:", e, "行内容:", line);
}
}
}
debugLog(`解析了${processedCount}行SSE数据,提取了${fullContent.length}字符的内容`);
if (fullContent) {
callback({
type: 'complete',
content: fullContent.trim()
});
return;
} else {
throw new Error("无法从SSE响应中提取内容");
}
}
// 尝试作为单个JSON对象解析
try {
debugLog("尝试解析为标准JSON响应");
const data = JSON.parse(responseText);
if (data.error) {
callback({
type: 'error',
content: `错误:${data.error.message || JSON.stringify(data.error)}`
});
} else if (data.choices && data.choices[0]) {
// 提取不同格式的内容
let content = '';
if (data.choices[0].message && data.choices[0].message.content) {
content = data.choices[0].message.content;
} else if (data.choices[0].text) {
content = data.choices[0].text;
}
if (content) {
callback({
type: 'complete',
content: content.trim()
});
} else {
throw new Error("API响应中未找到内容字段");
}
} else {
throw new Error("API响应格式不符合预期");
}
} catch (e) {
// JSON解析失败,可能是纯文本响应或其他格式
debugLog("JSON解析失败,尝试其他格式:", e.message);
// 如果响应看起来像纯文本,直接使用
if (responseText && !responseText.startsWith('{') && !responseText.startsWith('[') && !responseText.includes('<!DOCTYPE')) {
debugLog("响应似乎是纯文本,直接返回");
callback({
type: 'complete',
content: responseText.trim()
});
return;
}
// 其他情况下,报告错误
throw new Error(`非标准响应格式: ${e.message}`);
}
} catch (e) {
// 安全地获取响应预览,避免undefined错误
let responsePreview = "无法获取响应内容";
try {
if (response && typeof response.responseText === 'string') {
responsePreview = response.responseText.substring(0, 200);
} else if (response) {
// 尝试获取响应头信息作为调试参考
responsePreview = JSON.stringify(response).substring(0, 300);
}
} catch (previewError) {
responsePreview = `获取响应预览时出错: ${previewError.message}`;
}
console.error("响应处理错误:", e.message);
console.error("错误详情:", e.stack);
console.error("响应信息:", responsePreview);
// 给用户一些有用的建议
let errorMessage = e.message;
if (e.message.includes("API返回了成功响应") || e.message.includes("无法从SSE响应中提取内容")) {
errorMessage += "<br><br>建议:<br>1. 检查API密钥是否有足够的使用额度<br>2. 尝试减少翻译文本长度<br>3. <b>在设置面板中禁用流式响应</b>";
} else if (e.message.includes("非标准响应格式")) {
errorMessage += "<br><br>可能原因:<br>1. API响应格式与脚本不兼容<br>2. API返回了错误信息<br>3. API密钥可能无效";
}
callback({
type: 'error',
content: `解析响应时出错:${errorMessage}<br><br><small>响应信息: ${escapeHtml(responsePreview)}</small>`
});
}
},
onerror: function (error) {
console.error("API请求错误:", error);
// 获取状态码
const statusCode = error.status || 0;
let errorMessage = error.statusText || '无法连接到 API';
// 添加状态码到错误信息
if (statusCode > 0) {
errorMessage = `(${statusCode}) ${errorMessage}`;
}
// 如果是流式请求失败,尝试非流式请求
if (useStreaming && !retryWithoutStreaming) {
debugLog(`流式请求失败,状态码: ${statusCode},尝试使用非流式模式重试...`);
translateText(text, callback, true);
return;
}
callback({
type: 'error',
content: `请求错误:${errorMessage}`,
statusCode: statusCode
});
}
});
}
// 辅助函数:转义HTML,防止XSS
function escapeHtml(text) {
return text
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
// 显示翻译结果
async function showTranslation(text, rect) {
// 确保使用最新的API设置
syncApiSettings();
console.log('显示翻译弹窗 - 确保使用最新API设置:', settings.apiEndpoint, settings.model);
// 移除之前的弹窗,确保清理事件处理函数
const oldPopups = document.querySelectorAll('.translation-popup');
oldPopups.forEach(popup => {
// 如果有清理函数,调用它
if (popup.cleanup && typeof popup.cleanup === 'function') {
popup.cleanup();
}
popup.remove();
});
// 创建新弹窗
const popup = createTranslationPopup();
// 通过附加到弹窗上的引用获取内容元素
const content = popup.content;
const sourceLanguage = popup.sourceLanguage;
const contentWrapper = popup.contentWrapper;
// 添加加载动画
const loadingAnimation = createLoadingAnimation();
content.innerHTML = ''; // 清空内容
content.appendChild(loadingAnimation);
content.appendChild(document.createTextNode('正在翻译...'));
// 先添加到DOM,以便获取尺寸
document.body.appendChild(popup);
// 获取窗口尺寸和弹窗尺寸
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
const popupWidth = popup.offsetWidth;
const popupHeight = popup.offsetHeight;
// 计算初始位置(选中文字的右侧)
let left = window.scrollX + rect.right + 10; // 右侧间隔10px
let top = window.scrollY + rect.top;
// 检查右侧空间是否足够,如果不够则改为左侧显示
if (left + popupWidth > window.scrollX + windowWidth) {
left = window.scrollX + rect.left - popupWidth - 10; // 左侧间隔10px
}
// 如果左侧也没有足够空间,则居中显示
if (left < window.scrollX) {
left = window.scrollX + (windowWidth - popupWidth) / 2;
}
// 确保弹窗不超出视窗底部
if (top + popupHeight > window.scrollY + windowHeight) {
top = window.scrollY + windowHeight - popupHeight - 10; // 底部间隔10px
}
// 确保弹窗不超出视窗顶部
if (top < window.scrollY) {
top = window.scrollY + 10; // 顶部间隔10px
}
// 设置弹窗位置
popup.style.left = `${left}px`;
popup.style.top = `${top}px`;
// 检测语言并翻译
if (settings.autoDetectLanguage) {
try {
const language = await detectLanguage(text);
sourceLanguage.textContent = `源语言: ${language}`;
// 显示原文(如果启用)
if (settings.showSourceLanguage) {
// 创建源文本元素
const sourceText = document.createElement('div');
sourceText.style.cssText = `
margin-bottom: 10px;
padding: 5px;
background-color: #f8f9fa;
border-radius: 4px;
font-size: 13px;
color: #666;
`;
sourceText.textContent = text;
// 在内容前添加源文本
contentWrapper.insertBefore(sourceText, content);
}
// 翻译文本
translateText(text, result => {
// 清除加载动画和加载文本
content.innerHTML = '';
// 确保结果正确显示
if (typeof result === 'object') {
if (result.type === 'error') {
content.textContent = `错误: ${result.content}`;
} else if (result.type === 'complete' || result.type === 'stream-end') {
content.textContent = result.content;
// 添加到历史记录
addToHistory(text, result.content);
} else if (result.content) {
content.textContent = result.content;
} else {
content.textContent = '无法解析翻译结果';
console.error('未能正确解析翻译结果', result);
}
} else if (typeof result === 'string') {
content.textContent = result;
} else {
content.textContent = '翻译过程发生错误';
console.error('翻译回调收到意外类型的结果', result);
}
});
} catch (error) {
console.error('语言检测失败:', error);
sourceLanguage.textContent = '源语言: 检测失败';
// 继续翻译流程
translateText(text, result => {
// ... 与上面相同的翻译处理逻辑 ...
});
}
} else {
sourceLanguage.textContent = '源语言: 未检测';
// 显示原文(如果启用)
if (settings.showSourceLanguage) {
// 创建源文本元素
const sourceText = document.createElement('div');
sourceText.style.cssText = `
margin-bottom: 10px;
padding: 5px;
background-color: #f8f9fa;
border-radius: 4px;
font-size: 13px;
color: #666;
`;
sourceText.textContent = text;
// 在内容前添加源文本
contentWrapper.insertBefore(sourceText, content);
}
// 翻译文本
translateText(text, result => {
// 清除加载动画和加载文本
content.innerHTML = '';
// 确保结果正确显示
if (typeof result === 'object') {
if (result.type === 'error') {
content.textContent = `错误: ${result.content}`;
} else if (result.type === 'complete' || result.type === 'stream-end') {
content.textContent = result.content;
// 添加到历史记录
addToHistory(text, result.content);
} else if (result.content) {
content.textContent = result.content;
} else {
content.textContent = '无法解析翻译结果';
console.error('未能正确解析翻译结果', result);
}
} else if (typeof result === 'string') {
content.textContent = result;
} else {
content.textContent = '翻译过程发生错误';
console.error('翻译回调收到意外类型的结果', result);
}
});
}
// 将选择的文本保存为全局变量,用于收藏功能
lastSelectedText = text;
}
// 监听选择事件
document.addEventListener('mouseup', function (e) {
// 如果点击的是翻译按钮或翻译弹窗,不处理
if (e.target.classList.contains('translate-button') ||
e.target.closest('.translate-button') ||
e.target.classList.contains('translation-popup') ||
e.target.closest('.translation-popup')) {
return;
}
const selection = window.getSelection();
const selectedText = selection.toString().trim();
// 清除之前的翻译按钮
if (activeTranslateButton) {
activeTranslateButton.remove();
activeTranslateButton = null;
}
if (selectedText.length > 0) {
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
// 保存最后选择的文本和位置
lastSelectedText = selectedText;
lastSelectionRect = {
top: rect.top,
left: rect.left,
bottom: rect.bottom,
right: rect.right
};
const translateButton = createTranslateButton();
translateButton.style.top = `${window.scrollY + rect.bottom + 5}px`;
translateButton.style.left = `${window.scrollX + rect.right - 50}px`;
translateButton.addEventListener('click', function (event) {
event.stopPropagation();
event.preventDefault();
// 显示翻译
showTranslation(lastSelectedText, lastSelectionRect);
});
document.body.appendChild(translateButton);
activeTranslateButton = translateButton;
}
});
// 在初始化时创建UI组件
function initUI() {
createSettingsButton();
createTranslatePageButton();
createProgressBar();
}
// 初始化UI
settings.currentTab = 'general'
initUI();
// 重试失败的翻译
function retryFailedTranslations() {
// 确保使用最新的API设置
syncApiSettings();
console.log('重试失败翻译 - 确保使用最新API设置:', settings.apiEndpoint, settings.model);
// 确认有段落需要翻译
if (!translationSegments || translationSegments.length === 0) {
alert('没有找到需要重试的翻译段落');
return;
}
// 收集失败项
const failedSegments = [];
for (let i = 0; i < translationSegments.length; i++) {
if (translationSegments[i].error) {
failedSegments.push(i);
}
}
if (failedSegments.length === 0) {
alert('没有找到失败的翻译段落');
return;
}
// 确认是否重试
if (!confirm(`找到${failedSegments.length}个失败的翻译段落,是否重试?`)) {
return;
}
// 更新状态
const statusElement = document.getElementById('translation-status');
if (statusElement) {
statusElement.textContent = `正在重试失败的翻译 (0/${failedSegments.length})`;
// 移除之前的错误摘要
const oldSummary = statusElement.querySelector('.error-summary');
if (oldSummary) oldSummary.remove();
}
// 重置错误计数
translationSegments.errorCount = 0;
// 开始重试
isTranslatingFullPage = true;
isTranslationPaused = false;
// 创建控制按钮
createControlButtons();
// 递归重试
function retryNext(index) {
if (index >= failedSegments.length) {
// 重试完成
if (statusElement) {
const newErrors = translationSegments.errorCount || 0;
statusElement.textContent = `重试完成 (${failedSegments.length}/${failedSegments.length})${newErrors > 0 ? ` (仍有${newErrors}个错误)` : ''}`;
}
isTranslatingFullPage = false;
return;
}
// 如果暂停,不继续
if (isTranslationPaused || !isTranslatingFullPage) {
return;
}
// 更新状态
if (statusElement) {
statusElement.textContent = `正在重试失败的翻译 (${index + 1}/${failedSegments.length})`;
}
// 获取当前要重试的段落索引
const segmentIndex = failedSegments[index];
// 清除之前的错误标记
if (translationSegments[segmentIndex].nodes && translationSegments[segmentIndex].nodes.length > 0) {
const nodeInfo = translationSegments[segmentIndex].nodes[0];
if (nodeInfo.node && nodeInfo.node.parentElement) {
const errorMark = nodeInfo.node.parentElement.querySelector('.translation-error-mark');
if (errorMark) errorMark.remove();
}
}
// 清除错误状态
delete translationSegments[segmentIndex].error;
// 翻译该段落
translateText(translationSegments[segmentIndex].text, function (result) {
if (result.type === 'complete' || result.type === 'stream-end') {
// 翻译成功,更新
translationSegments[segmentIndex].translation = result.content;
// 更新DOM
if (translationSegments[segmentIndex].nodes && translationSegments[segmentIndex].nodes.length > 0) {
// 复用现有逻辑更新DOM
if (translationSegments[segmentIndex].nodes.length === 1) {
const nodeInfo = translationSegments[segmentIndex].nodes[0];
if (nodeInfo.node && nodeInfo.node.nodeType === Node.TEXT_NODE) {
nodeInfo.node.textContent = result.content;
if (nodeInfo.node.parentElement) {
nodeInfo.node.parentElement.style.fontWeight = 'inherit';
nodeInfo.node.parentElement.dataset.translated = 'true';
}
}
} else {
// 复杂节点结构的处理略过,会在前面代码相同逻辑处理
}
}
// 继续下一个
setTimeout(() => retryNext(index + 1), 50);
} else if (result.type === 'error') {
// 仍然出错
console.error(`重试段落 ${segmentIndex + 1} 翻译失败:`, result.content);
translationSegments[segmentIndex].error = result.content;
// 错误计数
translationSegments.errorCount = (translationSegments.errorCount || 0) + 1;
// 添加错误标记
if (translationSegments[segmentIndex].nodes && translationSegments[segmentIndex].nodes.length > 0) {
const nodeInfo = translationSegments[segmentIndex].nodes[0];
if (nodeInfo.node && nodeInfo.node.parentElement) {
// 在段落旁创建错误标记
const errorMark = document.createElement('span');
errorMark.className = 'translation-error-mark';
errorMark.innerHTML = '⚠️';
errorMark.title = `翻译失败: ${result.content}`;
errorMark.style.cssText = `
color: #ff4d4f;
cursor: pointer;
margin-left: 5px;
font-size: 16px;
`;
errorMark.addEventListener('click', function() {
alert(`翻译错误: ${result.content}`);
});
nodeInfo.node.parentElement.appendChild(errorMark);
// 确保不修改原文内容,防止错误码替换原文
if (nodeInfo.original && nodeInfo.node.nodeType === Node.TEXT_NODE) {
if (nodeInfo.node.textContent !== nodeInfo.original) {
nodeInfo.node.textContent = nodeInfo.original;
}
}
}
}
// 继续下一个
setTimeout(() => retryNext(index + 1), 50);
}
}, false);
}
// 开始重试第一个
retryNext(0);
}
// 递归翻译段落
let translatedCount = 0;
// 添加一些全局错误处理和减速变量
let consecutiveErrors = 0;
let translationDelay = 50; // 初始延迟时间(ms)
function translateNextSegment(index) {
// 如果已暂停,不继续翻译
if (isTranslationPaused) {
return;
}
// 记录最后翻译的索引
lastTranslatedIndex = index - 1;
// 获取进度条和状态元素
const progressBar = document.getElementById('translation-progress-bar');
const statusElement = document.getElementById('translation-status');
if (index >= translationSegments.length) {
// 全部翻译完成
if (progressBar) progressBar.style.width = '100%';
// 获取错误统计
const totalErrors = translationSegments.errorCount || 0;
const errorInfo = totalErrors > 0 ? ` (${totalErrors}个错误)` : '';
if (statusElement) {
statusElement.textContent = `翻译完成 (${translationSegments.length}/${translationSegments.length})${errorInfo}`;
// 如果有错误,添加错误摘要信息
if (totalErrors > 0) {
// 移除之前的错误警告(如果有)
const oldWarning = statusElement.querySelector('.error-warning');
if (oldWarning) oldWarning.remove();
const errorSummary = document.createElement('div');
errorSummary.className = 'error-summary';
errorSummary.style.cssText = `
margin-top: 10px;
color: #ff4d4f;
font-size: 13px;
padding: 8px;
background-color: rgba(255, 77, 79, 0.1);
border-radius: 4px;
`;
// 创建错误摘要内容
let summaryText = `翻译过程中遇到了${totalErrors}个错误。`;
// 添加解决建议
if (totalErrors > 5) {
summaryText += `可能原因:<br>
1. API密钥额度不足或API限制<br>
2. 网络连接不稳定<br>
3. 翻译内容过长或API不兼容<br><br>
建议:<br>
· 检查API设置和密钥<br>
· 在设置中禁用流式响应<br>
· 尝试使用不同的API提供商`;
}
errorSummary.innerHTML = summaryText;
statusElement.appendChild(errorSummary);
// 添加重试错误项按钮
const retryButton = document.createElement('button');
retryButton.textContent = '重试失败项';
retryButton.style.cssText = `
margin-top: 10px;
padding: 5px 10px;
background-color: #ff4d4f;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
`;
retryButton.addEventListener('click', function() {
retryFailedTranslations();
});
errorSummary.appendChild(retryButton);
}
}
// 添加一个关闭按钮
if (statusElement && !statusElement.querySelector('.close-btn')) {
const closeButton = document.createElement('span');
closeButton.className = 'close-btn';
closeButton.style.cssText = `
margin-left: 10px;
cursor: pointer;
font-weight: bold;
`;
closeButton.textContent = '×';
closeButton.addEventListener('click', function () {
statusElement.remove();
const progressContainer = document.getElementById('translation-progress-container');
if (progressContainer) progressContainer.style.display = 'none';
});
statusElement.appendChild(closeButton);
}
// 显示切换按钮
const toggleButton = document.getElementById('toggle-translation-button');
if (toggleButton) toggleButton.style.display = 'block';
// 移除控制按钮
removeControlButtons();
isTranslatingFullPage = false;
return;
}
// 更新进度
const progress = Math.round((index / translationSegments.length) * 100);
if (progressBar) progressBar.style.width = `${progress}%`;
if (statusElement) statusElement.textContent = `正在翻译 (${index}/${translationSegments.length})`;
// 如果段落文本为空,直接跳到下一个
if (!translationSegments[index].text.trim()) {
translateNextSegment(index + 1);
return;
}
// 获取上下文
let context = [];
if (settings.useTranslationContext && settings.contextSize > 0) {
// 从当前段落往前获取已翻译的段落作为上下文
for (let i = index - 1; i >= 0 && context.length < settings.contextSize; i--) {
if (translationSegments[i] && translationSegments[i].translation) {
context.unshift({
original: translationSegments[i].text,
translation: translationSegments[i].translation
});
}
}
}
// 处理参考文献条目 - 特殊处理以保留格式
if (translationSegments[index].isReferenceItem) {
translateReferenceItem(translationSegments[index], function (translatedHtml) {
if (translatedHtml) {
// 保存翻译结果
translationSegments[index].translation = translatedHtml;
// 保存原始HTML以便切换回来
if (!translationSegments[index].originalHtml && translationSegments[index].element) {
translationSegments[index].originalHtml = translationSegments[index].element.innerHTML;
}
// 更新DOM显示翻译结果
if (translationSegments[index].element) {
// 将翻译后的HTML设置到元素中
translationSegments[index].element.innerHTML = translatedHtml;
}
// 更新最后翻译的索引
lastTranslatedIndex = index;
// 继续翻译下一个段落
setTimeout(() => translateNextSegment(index + 1), 50);
} else {
// 翻译失败时继续下一个
console.error(`参考文献条目 ${index + 1} 翻译失败`);
// 更新最后翻译的索引
lastTranslatedIndex = index;
setTimeout(() => translateNextSegment(index + 1), 50);
}
});
return;
}
// 常规段落翻译
translateText(translationSegments[index].text, function (result) {
// 如果已暂停或停止,不处理翻译结果
if (isTranslationPaused || !isTranslatingFullPage) {
return;
}
if (result.type === 'complete' || result.type === 'stream-end') {
// 翻译成功
translatedCount++;
translationSegments[index].translation = result.content;
// 更新DOM中的文本,保留原始样式
if (translationSegments[index].nodes && translationSegments[index].nodes.length > 0) {
// 使用更智能的方式分配翻译结果
if (translationSegments[index].nodes.length === 1) {
// 只有一个节点的简单情况
const nodeInfo = translationSegments[index].nodes[0];
if (nodeInfo.node && nodeInfo.node.nodeType === Node.TEXT_NODE) {
nodeInfo.node.textContent = result.content;
}
} else {
// 多个节点的情况,尝试保留原始结构
// 计算原始内容中每个节点文本的比例
const totalOriginalLength = translationSegments[index].nodes.reduce(
(sum, nodeInfo) => sum + (nodeInfo.original ? nodeInfo.original.length : 0), 0);
if (totalOriginalLength > 0) {
// 按照原始文本长度比例分配翻译后的文本
let startPos = 0;
for (let i = 0; i < translationSegments[index].nodes.length; i++) {
const nodeInfo = translationSegments[index].nodes[i];
if (nodeInfo.node && nodeInfo.node.nodeType === Node.TEXT_NODE && nodeInfo.original) {
// 计算该节点在原始文本中的比例
const ratio = nodeInfo.original.length / totalOriginalLength;
// 计算应该分配给该节点的翻译文本长度
const chunkLength = Math.round(result.content.length * ratio);
// 提取翻译文本的一部分
let chunk = '';
if (i === translationSegments[index].nodes.length - 1) {
// 最后一个节点,获取剩余所有文本
chunk = result.content.substring(startPos);
} else {
// 非最后节点,按比例获取
chunk = result.content.substring(startPos, startPos + chunkLength);
startPos += chunkLength;
}
// 更新节点文本
nodeInfo.node.textContent = chunk;
}
}
} else {
// 回退方案:如果无法计算比例,则将所有文本放在第一个节点
let foundFirstNode = false;
for (let i = 0; i < translationSegments[index].nodes.length; i++) {
const nodeInfo = translationSegments[index].nodes[i];
if (nodeInfo.node && nodeInfo.node.nodeType === Node.TEXT_NODE) {
if (!foundFirstNode) {
nodeInfo.node.textContent = result.content;
foundFirstNode = true;
} else {
nodeInfo.node.textContent = '';
}
}
}
}
}
}
// 更新最后翻译的索引
lastTranslatedIndex = index;
// 重置连续错误计数,因为成功了一次
consecutiveErrors = 0;
// 如果之前增加了延迟,且现在连续成功,则尝试逐步恢复
if (translationDelay > 50 && translatedCount % 5 === 0) {
// 逐步减少延迟,但不低于初始值
translationDelay = Math.max(50, translationDelay * 0.8);
console.log(`连续翻译成功,减少延迟至${translationDelay}ms`);
// 更新延迟信息显示
if (statusElement && statusElement.querySelector('.delay-info')) {
statusElement.querySelector('.delay-info').textContent = `已自动调整延迟至${Math.round(translationDelay)}ms`;
// 如果延迟已经降回正常,移除通知并恢复颜色
if (translationDelay <= 60) {
statusElement.querySelector('.delay-info').remove();
statusElement.style.backgroundColor = 'rgba(66, 133, 244, 0.9)';
}
}
}
// 继续下一个段落
setTimeout(() => translateNextSegment(index + 1), translationDelay);
} else if (result.type === 'error') {
// 翻译出错,记录错误并继续下一个
console.error(`段落 ${index + 1} 翻译失败:`, result.content);
translationSegments[index].error = result.content;
// 更新最后翻译的索引
lastTranslatedIndex = index;
// 错误计数
translationSegments.errorCount = (translationSegments.errorCount || 0) + 1;
consecutiveErrors++;
// 如果连续错误过多,自动增加延迟
if (consecutiveErrors >= 3) {
// 增加延迟,但最大不超过2秒
translationDelay = Math.min(2000, translationDelay * 1.5);
console.log(`检测到连续错误,自动增加翻译延迟至${translationDelay}ms`);
// 更新状态显示
if (statusElement && !statusElement.querySelector('.delay-info')) {
const delayInfo = document.createElement('div');
delayInfo.className = 'delay-info';
delayInfo.style.cssText = `
font-size: 12px;
margin-top: 5px;
`;
delayInfo.textContent = `已自动增加延迟至${translationDelay}ms以减轻API负载`;
statusElement.appendChild(delayInfo);
} else if (statusElement && statusElement.querySelector('.delay-info')) {
statusElement.querySelector('.delay-info').textContent = `已自动增加延迟至${translationDelay}ms以减轻API负载`;
}
// 改变状态颜色提示错误
statusElement.style.backgroundColor = 'rgba(255, 152, 0, 0.9)';
}
// 显示错误提示标记在段落旁
if (translationSegments[index].nodes && translationSegments[index].nodes.length > 0) {
const nodeInfo = translationSegments[index].nodes[0];
if (nodeInfo.node && nodeInfo.node.parentElement) {
// 在段落旁创建错误标记
const errorMark = document.createElement('span');
errorMark.className = 'translation-error-mark';
errorMark.innerHTML = '⚠️';
errorMark.title = `翻译失败: ${result.content}`;
errorMark.style.cssText = `
color: #ff4d4f;
cursor: pointer;
margin-left: 5px;
font-size: 16px;
`;
errorMark.addEventListener('click', function() {
alert(`翻译错误: ${result.content}`);
});
nodeInfo.node.parentElement.appendChild(errorMark);
// 重要:确保不修改原文内容,防止错误码替换原文
// 如果有通过错误处理设置了节点文本,恢复原始文本
if (nodeInfo.original && nodeInfo.node.nodeType === Node.TEXT_NODE) {
if (nodeInfo.node.textContent !== nodeInfo.original) {
nodeInfo.node.textContent = nodeInfo.original;
}
}
}
}
// 更新状态面板显示总体进度和错误信息
if (statusElement) {
const totalErrors = translationSegments.errorCount;
const errorInfo = totalErrors > 0 ? ` (${totalErrors}个错误)` : '';
statusElement.textContent = `正在翻译 (${index + 1}/${translationSegments.length})${errorInfo}`;
// 如果错误超过阈值,添加暂停建议
if (totalErrors > 5 && !statusElement.querySelector('.error-warning')) {
const warningElement = document.createElement('div');
warningElement.className = 'error-warning';
warningElement.style.cssText = `
margin-top: 5px;
color: #ff4d4f;
font-size: 12px;
`;
warningElement.textContent = `检测到多处翻译错误,可能是API限制或网络问题。考虑暂停翻译后再继续。`;
statusElement.appendChild(warningElement);
}
}
// 继续下一个段落
setTimeout(() => translateNextSegment(index + 1), translationDelay);
}
}, false, context); // 添加context参数
}
})();