From 16bbe51cbe1d18f8c913d41585dddd0c5cad157a Mon Sep 17 00:00:00 2001 From: alka Date: Wed, 8 Apr 2026 21:50:11 +0800 Subject: [PATCH 1/5] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=A9=BA=E6=96=87?= =?UTF-8?q?=E6=9C=AC=E8=A2=AB=E7=BF=BB=E8=AF=91=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- entrypoints/main/dom.ts | 18 +++++++++++++----- entrypoints/main/trans.ts | 8 ++++++-- entrypoints/utils/translateApi.ts | 6 ++++++ 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/entrypoints/main/dom.ts b/entrypoints/main/dom.ts index 5805fd3..93e9664 100644 --- a/entrypoints/main/dom.ts +++ b/entrypoints/main/dom.ts @@ -33,7 +33,11 @@ export function grabAllNode(rootNode: Node): Element[] { NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, { acceptNode: (node: Node): number => { - if (node instanceof Text) return NodeFilter.FILTER_ACCEPT; + if (node instanceof Text) { + // 检查文本节点是否有实际内容(去除空白后) + const textContent = node.textContent?.replace(/[\s\u3000]/g, '') || ''; + return textContent.length > 0 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP; + } if (!(node instanceof Element)) return NodeFilter.FILTER_SKIP; @@ -59,13 +63,17 @@ export function grabAllNode(rootNode: Node): Element[] { for (const child of node.childNodes) { if (child.nodeType === Node.ELEMENT_NODE) { hasElement = true; - // 检查子元素是否包含文本 - if (child.textContent?.trim()) { + // 检查子元素是否包含文本(去除空白后) + const childText = child.textContent?.replace(/[\s\u3000]/g, '') || ''; + if (childText.length > 0) { hasNonEmptyElement = true; } } - if (child.nodeType === Node.TEXT_NODE && child.textContent?.trim()) { - hasText = true; + if (child.nodeType === Node.TEXT_NODE) { + const textContent = child.textContent?.replace(/[\s\u3000]/g, '') || ''; + if (textContent.length > 0) { + hasText = true; + } } } diff --git a/entrypoints/main/trans.ts b/entrypoints/main/trans.ts index a7dc91d..3a0570d 100644 --- a/entrypoints/main/trans.ts +++ b/entrypoints/main/trans.ts @@ -252,7 +252,9 @@ export function handleSingleTranslation(node: any, slide: boolean) { function bilingualTranslate(node: any, nodeOuterHTML: any) { - if (detectlang(node.textContent.replace(/[\s\u3000]/g, '')) === config.to) return; + const cleanedText = node.textContent.replace(/[\s\u3000]/g, ''); + if (!cleanedText || cleanedText.length === 0) return; + if (detectlang(cleanedText) === config.to) return; let origin = node.textContent; let spinner = insertLoadingSpinner(node); @@ -272,7 +274,9 @@ function bilingualTranslate(node: any, nodeOuterHTML: any) { export function singleTranslate(node: any) { - if (detectlang(node.textContent.replace(/[\s\u3000]/g, '')) === config.to) return; + const cleanedText = node.textContent.replace(/[\s\u3000]/g, ''); + if (!cleanedText || cleanedText.length === 0) return; + if (detectlang(cleanedText) === config.to) return; let origin = servicesType.isMachine(config.service) ? node.innerHTML : LLMStandardHTML(node); let spinner = insertLoadingSpinner(node); diff --git a/entrypoints/utils/translateApi.ts b/entrypoints/utils/translateApi.ts index 6bd2526..d32b43b 100644 --- a/entrypoints/utils/translateApi.ts +++ b/entrypoints/utils/translateApi.ts @@ -30,6 +30,12 @@ export async function translateText(origin: string, context: string = document.t useCache = config.useCache, } = options; + // 检查 origin 是否为空或只有空白字符 + const cleanedOrigin = origin?.replace(/[\s\u3000]/g, '') || ''; + if (!cleanedOrigin || cleanedOrigin.length === 0) { + return origin || ''; + } + // 如果目标语言与当前文本语言相同,直接返回原文 if (detectlang(origin.replace(/[\s\u3000]/g, '')) === config.to) { return origin; From b88e8dcebb0ba6157c18dce7f1eacda28049161d Mon Sep 17 00:00:00 2001 From: alka Date: Wed, 15 Apr 2026 22:14:17 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=E6=B7=BB=E5=8A=A0{{context}}=E5=8F=98?= =?UTF-8?q?=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/Main.vue | 2 +- entrypoints/service/azure-openai.ts | 4 +-- entrypoints/service/claude.ts | 4 +-- entrypoints/service/common.ts | 4 +-- entrypoints/service/coze.ts | 2 +- entrypoints/service/custom.ts | 4 +-- entrypoints/service/deepseek.ts | 6 ++-- entrypoints/service/gemini.ts | 2 +- entrypoints/service/grok.ts | 6 ++-- entrypoints/service/infini.ts | 2 +- entrypoints/service/minimax.ts | 4 +-- entrypoints/service/newapi.ts | 4 +-- entrypoints/service/tongyi.ts | 2 +- entrypoints/service/yiyan.ts | 4 +-- entrypoints/service/zhipu.ts | 2 +- entrypoints/utils/option.ts | 10 +++++- entrypoints/utils/template.ts | 47 ++++++++++++++--------------- 17 files changed, 58 insertions(+), 51 deletions(-) diff --git a/components/Main.vue b/components/Main.vue index a4f1bce..ff7ed00 100644 --- a/components/Main.vue +++ b/components/Main.vue @@ -570,7 +570,7 @@ user diff --git a/entrypoints/service/azure-openai.ts b/entrypoints/service/azure-openai.ts index e7325bf..a6cc842 100644 --- a/entrypoints/service/azure-openai.ts +++ b/entrypoints/service/azure-openai.ts @@ -29,8 +29,8 @@ async function azureOpenai(message: any) { const resp = await fetch(endpoint, { method: method.POST, headers, - body: commonMsgTemplate(message.origin) - }); + body: commonMsgTemplate(message.origin, message.context) + }); if (!resp.ok) { const errorText = await resp.text(); diff --git a/entrypoints/service/claude.ts b/entrypoints/service/claude.ts index 1ed139d..002e1c1 100644 --- a/entrypoints/service/claude.ts +++ b/entrypoints/service/claude.ts @@ -17,8 +17,8 @@ async function claude(message: any) { const resp = await fetch(url, { method: method.POST, headers, - body: claudeMsgTemplate(message.origin) - }); + body: claudeMsgTemplate(message.origin, message.context) + }); if (!resp.ok) { throw new Error(`请求失败: ${resp.status} ${resp.statusText} body: ${await resp.text()}`); diff --git a/entrypoints/service/common.ts b/entrypoints/service/common.ts index 68293d4..1b70301 100644 --- a/entrypoints/service/common.ts +++ b/entrypoints/service/common.ts @@ -22,7 +22,7 @@ async function common(message: any) { const resp = await fetch(url, { method: method.POST, headers, - body: commonMsgTemplate(message.origin) + body: commonMsgTemplate(message.origin, message.context) }); if (!resp.ok) { @@ -37,4 +37,4 @@ async function common(message: any) { } } -export default common; \ No newline at end of file +export default common; diff --git a/entrypoints/service/coze.ts b/entrypoints/service/coze.ts index c4dbbe1..a7bc0c4 100644 --- a/entrypoints/service/coze.ts +++ b/entrypoints/service/coze.ts @@ -16,7 +16,7 @@ async function coze( message: any) { const resp = await fetch(url, { method: method.POST, headers: headers, - body: cozeTemplate(message.origin) + body: cozeTemplate(message.origin, message.context) }); if (resp.ok) { diff --git a/entrypoints/service/custom.ts b/entrypoints/service/custom.ts index f9471b2..d93f5b2 100644 --- a/entrypoints/service/custom.ts +++ b/entrypoints/service/custom.ts @@ -13,7 +13,7 @@ async function custom(message: any) { const resp = await fetch(config.custom, { method: method.POST, headers: headers, - body: commonMsgTemplate(message.origin) + body: commonMsgTemplate(message.origin, message.context) }); if (resp.ok) { @@ -25,4 +25,4 @@ async function custom(message: any) { } } -export default custom; \ No newline at end of file +export default custom; diff --git a/entrypoints/service/deepseek.ts b/entrypoints/service/deepseek.ts index c7c30f2..d13e089 100644 --- a/entrypoints/service/deepseek.ts +++ b/entrypoints/service/deepseek.ts @@ -15,8 +15,8 @@ async function deepseek(message: any) { const resp = await fetch(url, { method: method.POST, headers, - body: deepseekMsgTemplate(message.origin) - }); + body: deepseekMsgTemplate(message.origin, message.context) + }); if (!resp.ok) { throw new Error(`翻译失败: ${resp.status} ${resp.statusText} body: ${await resp.text()}`); @@ -30,4 +30,4 @@ async function deepseek(message: any) { } } -export default deepseek; \ No newline at end of file +export default deepseek; diff --git a/entrypoints/service/gemini.ts b/entrypoints/service/gemini.ts index 9098e33..7497f4f 100644 --- a/entrypoints/service/gemini.ts +++ b/entrypoints/service/gemini.ts @@ -15,7 +15,7 @@ async function gemini(message: any) { const resp = await fetch(url, { method: method.POST, headers: {'Content-Type': 'application/json'}, - body: geminiMsgTemplate(message.origin), + body: geminiMsgTemplate(message.origin, message.context), }); if (resp.ok) { let result = await resp.json(); diff --git a/entrypoints/service/grok.ts b/entrypoints/service/grok.ts index 73bc1b4..47e91cc 100644 --- a/entrypoints/service/grok.ts +++ b/entrypoints/service/grok.ts @@ -20,8 +20,8 @@ async function grok(message: any) { const resp = await fetch(url, { method: method.POST, headers, - body: commonMsgTemplate(message.origin) - }); + body: commonMsgTemplate(message.origin, message.context) + }); if (!resp.ok) { throw new Error(`翻译失败: ${resp.status} ${resp.statusText} body: ${await resp.text()}`); @@ -35,4 +35,4 @@ async function grok(message: any) { } } -export default grok; \ No newline at end of file +export default grok; diff --git a/entrypoints/service/infini.ts b/entrypoints/service/infini.ts index 5af405b..d6783c6 100644 --- a/entrypoints/service/infini.ts +++ b/entrypoints/service/infini.ts @@ -16,7 +16,7 @@ async function infini(message: any) { const resp = await fetch(`https://cloud.infini-ai.com/maas/${model}/nvidia/chat/completions`, { method: method.POST, headers: headers, - body: commonMsgTemplate(message.origin) + body: commonMsgTemplate(message.origin, message.context) }); if (resp.ok) { diff --git a/entrypoints/service/minimax.ts b/entrypoints/service/minimax.ts index 7166e78..b95ebce 100644 --- a/entrypoints/service/minimax.ts +++ b/entrypoints/service/minimax.ts @@ -17,7 +17,7 @@ async function minimax(message: any) { const resp = await fetch(url, { method: method.POST, headers: headers, - body: minimaxTemplate(message.origin) + body: minimaxTemplate(message.origin, message.context) }) if (resp.ok) { let result = await resp.json(); @@ -30,4 +30,4 @@ async function minimax(message: any) { } -export default minimax; \ No newline at end of file +export default minimax; diff --git a/entrypoints/service/newapi.ts b/entrypoints/service/newapi.ts index 87ac161..459fd91 100644 --- a/entrypoints/service/newapi.ts +++ b/entrypoints/service/newapi.ts @@ -30,7 +30,7 @@ async function newapi(message: any) { const resp = await fetch(url, { method: method.POST, headers, - body: commonMsgTemplate(message.origin) + body: commonMsgTemplate(message.origin, message.context) }); if (!resp.ok) { @@ -50,4 +50,4 @@ async function newapi(message: any) { } } -export default newapi; \ No newline at end of file +export default newapi; diff --git a/entrypoints/service/tongyi.ts b/entrypoints/service/tongyi.ts index aafe7fa..d0c79cc 100644 --- a/entrypoints/service/tongyi.ts +++ b/entrypoints/service/tongyi.ts @@ -16,7 +16,7 @@ async function tongyi(message: any) { const resp = await fetch(url, { method: method.POST, headers: headers, - body: tongyiMsgTemplate(message.origin) + body: tongyiMsgTemplate(message.origin, message.context) }); if (resp.ok) { diff --git a/entrypoints/service/yiyan.ts b/entrypoints/service/yiyan.ts index da762c5..684d256 100644 --- a/entrypoints/service/yiyan.ts +++ b/entrypoints/service/yiyan.ts @@ -23,7 +23,7 @@ async function yiyan(message: any) { const resp = await fetch(url, { method: method.POST, headers: {'Content-Type': 'application/json'}, - body: yiyanMsgTemplate(message.origin) + body: yiyanMsgTemplate(message.origin, message.context) }); if (resp.ok) { @@ -67,4 +67,4 @@ async function getSecret() { } else throw new Error(res.error_description || '文心一言获取 token 失败'); } -export default yiyan; \ No newline at end of file +export default yiyan; diff --git a/entrypoints/service/zhipu.ts b/entrypoints/service/zhipu.ts index 18ad7d3..5d910c5 100644 --- a/entrypoints/service/zhipu.ts +++ b/entrypoints/service/zhipu.ts @@ -28,7 +28,7 @@ async function zhipu(message: any) { const resp = await fetch(urls[services.zhipu], { method: method.POST, headers: headers, - body: commonMsgTemplate(message.origin) + body: commonMsgTemplate(message.origin, message.context) }); if (resp.ok) { diff --git a/entrypoints/utils/option.ts b/entrypoints/utils/option.ts index 8293314..7fd7524 100644 --- a/entrypoints/utils/option.ts +++ b/entrypoints/utils/option.ts @@ -400,8 +400,16 @@ export const defaultOption = { deeplx: "http://localhost:1188/translate", system_role: "You are a professional, authentic machine translation engine.", - user_role: `Translate the following text into {{to}}, If translation is unnecessary (e.g. proper nouns, codes, etc.), return the original text. NO explanations. NO notes: + user_role: `Context: +\`\`\` +{{context}} +\`\`\` +Use the following context only for disambiguation, and do not translate or explain the context itself. + +Translate the following text into {{to}}.If translation is unnecessary (e.g. proper nouns, codes, etc.), return the original text. NO explanations. NO notes: + +Original text: {{origin}}`, count: 0, useCache: true, diff --git a/entrypoints/utils/template.ts b/entrypoints/utils/template.ts index 865b492..a298a8f 100644 --- a/entrypoints/utils/template.ts +++ b/entrypoints/utils/template.ts @@ -2,8 +2,15 @@ import {customModelString, defaultOption, services} from "./option"; import {config} from "@/entrypoints/utils/config"; +function renderUserTemplate(origin: string, context: string = "") { + return (config.user_role[config.service] || defaultOption.user_role) + .replace('{{to}}', config.to) + .replace('{{origin}}', origin) + .replace('{{context}}', context); +} + // openai 格式的消息模板(通用模板) -export function commonMsgTemplate(origin: string) { +export function commonMsgTemplate(origin: string, context: string = "") { // 检测是否使用自定义模型 let model = config.model[config.service] === customModelString ? config.customModel[config.service] : config.model[config.service] @@ -11,8 +18,7 @@ export function commonMsgTemplate(origin: string) { model = model.replace(/(.*)/g, ""); let system = config.system_role[config.service] || defaultOption.system_role; - let user = (config.user_role[config.service] || defaultOption.user_role) - .replace('{{to}}', config.to).replace('{{origin}}', origin); + let user = renderUserTemplate(origin, context); return JSON.stringify({ 'model': model, @@ -25,7 +31,7 @@ export function commonMsgTemplate(origin: string) { } // deepseek -export function deepseekMsgTemplate(origin: string) { +export function deepseekMsgTemplate(origin: string, context: string = "") { // 检测是否使用自定义模型 let model = config.model[config.service] === customModelString ? config.customModel[config.service] : config.model[config.service] @@ -33,8 +39,7 @@ export function deepseekMsgTemplate(origin: string) { model = model.replace(/(.*)/g, ""); let system = config.system_role[config.service] || defaultOption.system_role; - let user = (config.user_role[config.service] || defaultOption.user_role) - .replace('{{to}}', config.to).replace('{{origin}}', origin); + let user = renderUserTemplate(origin, context); const payload: any = { 'model': model, @@ -53,9 +58,8 @@ export function deepseekMsgTemplate(origin: string) { } // gemini -export function geminiMsgTemplate(origin: string) { - let user = (config.user_role[config.service] || defaultOption.user_role) - .replace('{{to}}', config.to).replace('{{origin}}', origin); +export function geminiMsgTemplate(origin: string, context: string = "") { + let user = renderUserTemplate(origin, context); return JSON.stringify({ "contents": [ @@ -65,15 +69,14 @@ export function geminiMsgTemplate(origin: string) { } // claude -export function claudeMsgTemplate(origin: string) { +export function claudeMsgTemplate(origin: string, context: string = "") { let model = config.model[services.claude]; if (model === "claude-3-5-haiku") model = "claude-3-5-haiku-20241022"; else if (model === "claude-3-5-sonnet") model = "claude-3-5-sonnet-20241022"; else if (model === "claude-3-opus") model = "claude-3-opus-20240229"; let system = config.system_role[config.service] || defaultOption.system_role; - let user = (config.user_role[config.service] || defaultOption.user_role) - .replace('{{to}}', config.to).replace('{{origin}}', origin); + let user = renderUserTemplate(origin, context); return JSON.stringify({ model: model, @@ -87,12 +90,11 @@ export function claudeMsgTemplate(origin: string) { } // 通义千问 -export function tongyiMsgTemplate(origin: string) { +export function tongyiMsgTemplate(origin: string, context: string = "") { let model = config.model[config.service] === customModelString ? config.customModel[config.service] : config.model[config.service] const normalTemplate = () => { let system = config.system_role[config.service] || defaultOption.system_role; - let user = (config.user_role[config.service] || defaultOption.user_role) - .replace('{{to}}', config.to).replace('{{origin}}', origin); + let user = renderUserTemplate(origin, context); return JSON.stringify({ "model": model, @@ -131,9 +133,8 @@ export function tongyiMsgTemplate(origin: string) { } // 文心一言 -export function yiyanMsgTemplate(origin: string) { - let user = (config.user_role[config.service] || defaultOption.user_role) - .replace('{{to}}', config.to).replace('{{origin}}', origin); +export function yiyanMsgTemplate(origin: string, context: string = "") { + let user = renderUserTemplate(origin, context); return JSON.stringify({ 'temperature': 0.7, @@ -144,11 +145,10 @@ export function yiyanMsgTemplate(origin: string) { }) } -export function minimaxTemplate(origin: string) { +export function minimaxTemplate(origin: string, context: string = "") { let system = config.system_role[config.service] || defaultOption.system_role; - let user = (config.user_role[config.service] || defaultOption.user_role) - .replace('{{to}}', config.to).replace('{{origin}}', origin); + let user = renderUserTemplate(origin, context); return JSON.stringify({ model: "MiniMax-Text-01", @@ -161,11 +161,10 @@ export function minimaxTemplate(origin: string) { }) } -export function cozeTemplate(origin: string) { +export function cozeTemplate(origin: string, context: string = "") { let system = config.system_role[config.service] || defaultOption.system_role; - let user = (config.user_role[config.service] || defaultOption.user_role) - .replace('{{to}}', config.to).replace('{{origin}}', origin); + let user = renderUserTemplate(origin, context); return JSON.stringify({ bot_id: config.robot_id[config.service], From 2c6eb52686d09a94c6f5b5e05c1833bf5e6c65e5 Mon Sep 17 00:00:00 2001 From: alka Date: Wed, 15 Apr 2026 22:23:40 +0800 Subject: [PATCH 3/5] =?UTF-8?q?add:=20=E6=B7=BB=E5=8A=A0=E4=BA=86url?= =?UTF-8?q?=E4=BD=9C=E4=B8=BAcontext?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- entrypoints/main/dom.ts | 5 +++-- entrypoints/main/trans.ts | 9 +++++---- entrypoints/utils/context.ts | 23 +++++++++++++++++++++++ entrypoints/utils/floatingBall.ts | 2 +- entrypoints/utils/translateApi.ts | 5 +++-- 5 files changed, 35 insertions(+), 9 deletions(-) create mode 100644 entrypoints/utils/context.ts diff --git a/entrypoints/main/dom.ts b/entrypoints/main/dom.ts index 93e9664..46abb61 100644 --- a/entrypoints/main/dom.ts +++ b/entrypoints/main/dom.ts @@ -1,6 +1,7 @@ import { getMainDomain, selectCompatFn } from "@/entrypoints/main/compat"; import { html } from 'js-beautify'; import { handleBtnTranslation } from "@/entrypoints/main/trans"; +import { buildTranslationContext } from "@/entrypoints/utils/context"; // 直接翻译的标签集合(块级元素) const directSet = new Set([ @@ -373,7 +374,7 @@ function handleFirstLineText(node: any): boolean { while (child) { if (child.nodeType === Node.TEXT_NODE && child.textContent.trim()) { browser.runtime.sendMessage({ - context: document.title, + context: buildTranslationContext(), origin: child.textContent }) .then((text: string) => child.textContent = text) @@ -469,4 +470,4 @@ export function smashTruncationStyle(node: any) { checkAndRemoveStyle(node, 'webkitLineClamp'); node.style.webkitLineClamp = 'unset'; node.style.maxHeight = 'unset'; -} \ No newline at end of file +} diff --git a/entrypoints/main/trans.ts b/entrypoints/main/trans.ts index 3a0570d..058b35a 100644 --- a/entrypoints/main/trans.ts +++ b/entrypoints/main/trans.ts @@ -8,6 +8,7 @@ import { detectlang, throttle } from "@/entrypoints/utils/common"; import { getMainDomain, replaceCompatFn } from "@/entrypoints/main/compat"; import { config } from "@/entrypoints/utils/config"; import { translateText, cancelAllTranslations } from '@/entrypoints/utils/translateApi'; +import { buildTranslationContext } from '@/entrypoints/utils/context'; let hoverTimer: any; // 鼠标悬停计时器 let htmlSet = new Set(); // 防抖 @@ -260,7 +261,7 @@ function bilingualTranslate(node: any, nodeOuterHTML: any) { let spinner = insertLoadingSpinner(node); // 使用队列管理的翻译API - translateText(origin, document.title) + translateText(origin, buildTranslationContext()) .then((text: string) => { spinner.remove(); htmlSet.delete(nodeOuterHTML); @@ -282,7 +283,7 @@ export function singleTranslate(node: any) { let spinner = insertLoadingSpinner(node); // 使用队列管理的翻译API - translateText(origin, document.title) + translateText(origin, buildTranslationContext()) .then((text: string) => { spinner.remove(); @@ -315,7 +316,7 @@ export const handleBtnTranslation = throttle((node: any) => { config.count++ && storage.setItem('local:config', JSON.stringify(config)); - browser.runtime.sendMessage({ context: document.title, origin: origin }) + browser.runtime.sendMessage({ context: buildTranslationContext(), origin: origin }) .then((text: string) => { cache.localSetDual(origin, text); node.innerText = text; @@ -335,4 +336,4 @@ function bilingualAppendChild(node: any, text: string) { newNode.append(text); smashTruncationStyle(node); node.appendChild(newNode); -} \ No newline at end of file +} diff --git a/entrypoints/utils/context.ts b/entrypoints/utils/context.ts new file mode 100644 index 0000000..526587a --- /dev/null +++ b/entrypoints/utils/context.ts @@ -0,0 +1,23 @@ +export interface TranslationContextOptions { + title?: string; + url?: string; +} + +export function buildTranslationContext(options: TranslationContextOptions = {}) { + const title = options.title ?? document.title; + const rawUrl = options.url ?? location.href; + + let normalizedUrl = rawUrl; + try { + const parsedUrl = new URL(rawUrl); + parsedUrl.hash = ''; + normalizedUrl = parsedUrl.toString(); + } catch { + normalizedUrl = rawUrl; + } + + return [ + `Page title: ${title}`.trim(), + `URL: ${normalizedUrl}`.trim(), + ].join('\n'); +} diff --git a/entrypoints/utils/floatingBall.ts b/entrypoints/utils/floatingBall.ts index 331286b..56cf9f4 100644 --- a/entrypoints/utils/floatingBall.ts +++ b/entrypoints/utils/floatingBall.ts @@ -249,4 +249,4 @@ export function toggleFloatingBallPosition() { // 保存配置到存储 saveConfig(); -} \ No newline at end of file +} diff --git a/entrypoints/utils/translateApi.ts b/entrypoints/utils/translateApi.ts index d32b43b..1bd72d5 100644 --- a/entrypoints/utils/translateApi.ts +++ b/entrypoints/utils/translateApi.ts @@ -9,6 +9,7 @@ import { config } from './config'; import { cache } from './cache'; import { detectlang } from './common'; import { storage } from '@wxt-dev/storage'; +import { buildTranslationContext } from './context'; // 调试相关 const isDev = process.env.NODE_ENV === 'development'; @@ -22,7 +23,7 @@ const isDev = process.env.NODE_ENV === 'development'; * @param options 翻译选项 * @returns 翻译结果的Promise */ -export async function translateText(origin: string, context: string = document.title, options: TranslateOptions = {}): Promise { +export async function translateText(origin: string, context: string = buildTranslationContext(), options: TranslateOptions = {}): Promise { const { maxRetries = 3, retryDelay = 1000, @@ -133,4 +134,4 @@ export interface TranslateOptions { timeout?: number; /** 是否使用缓存 */ useCache?: boolean; -} \ No newline at end of file +} From ec09c1001e85e5b73513220a8600cb2c733cfc0d Mon Sep 17 00:00:00 2001 From: alka Date: Wed, 15 Apr 2026 23:10:04 +0800 Subject: [PATCH 4/5] =?UTF-8?q?add:=20=E6=B7=BB=E5=8A=A0=E6=9B=B4=E5=A4=9A?= =?UTF-8?q?=E7=A7=8D=E7=B1=BB=E7=9A=84context?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- entrypoints/main/context.ts | 75 +++++++++++++++++++++++++++++++++++++ entrypoints/main/dom.ts | 4 +- entrypoints/main/trans.ts | 8 ++-- 3 files changed, 81 insertions(+), 6 deletions(-) create mode 100644 entrypoints/main/context.ts diff --git a/entrypoints/main/context.ts b/entrypoints/main/context.ts new file mode 100644 index 0000000..f7c2957 --- /dev/null +++ b/entrypoints/main/context.ts @@ -0,0 +1,75 @@ +import { buildTranslationContext } from "@/entrypoints/utils/context"; + +const MAX_CONTEXT_LENGTH = 120; +const CONTEXT_TAGS = 'h1, h2, h3, h4, h5, h6, p, li, dd, blockquote, figcaption, div, label'; + +function compactText(text: string | null | undefined, maxLength: number = MAX_CONTEXT_LENGTH) { + const compact = (text || "").replace(/\s+/g, ' ').trim(); + if (!compact) return ""; + return compact.length > maxLength ? `${compact.slice(0, maxLength)}...` : compact; +} + +function findNearbyNodeText(nodes: Element[], startIndex: number, step: -1 | 1) { + let index = startIndex + step; + while (index >= 0 && index < nodes.length) { + const text = compactText(nodes[index]?.textContent); + if (text) return text; + index += step; + } + return ""; +} + +function findNearbyHeading(node: Element) { + let current: Element | null = node; + while (current) { + let sibling: Element | null = current.previousElementSibling; + while (sibling) { + if (/^H[1-6]$/.test(sibling.tagName)) { + const heading = compactText(sibling.textContent, 100); + if (heading) return heading; + } + sibling = sibling.previousElementSibling; + } + current = current.parentElement; + } + + const headings = Array.from(document.querySelectorAll('h1, h2, h3')); + for (let index = headings.length - 1; index >= 0; index -= 1) { + const heading = headings[index]; + const position = heading.compareDocumentPosition(node); + if (position & Node.DOCUMENT_POSITION_FOLLOWING) { + const text = compactText(heading.textContent, 100); + if (text) return text; + } + } + + return ""; +} + +function collectContextNodes() { + return Array.from(document.querySelectorAll(CONTEXT_TAGS)).filter(node => { + if (!(node instanceof Element)) return false; + const text = compactText(node.textContent); + if (!text) return false; + if (node.classList.contains('notranslate') || node.classList.contains('sr-only')) return false; + if (node.closest('header, footer, nav, aside, script, style, noscript')) return false; + if (node.matches('button, input, textarea, select, code, pre')) return false; + return true; + }); +} + +export function buildNodeTranslationContext(node: Element) { + const nodes = collectContextNodes(); + const index = nodes.indexOf(node); + + const previousText = index >= 0 ? findNearbyNodeText(nodes, index, -1) : ""; + const nextText = index >= 0 ? findNearbyNodeText(nodes, index, 1) : ""; + const nearbyHeading = findNearbyHeading(node); + + return [ + buildTranslationContext(), + nearbyHeading ? `Nearby heading: ${nearbyHeading}` : "", + previousText ? `Previous text: ${previousText}` : "", + nextText ? `Next text: ${nextText}` : "", + ].filter(Boolean).join('\n'); +} diff --git a/entrypoints/main/dom.ts b/entrypoints/main/dom.ts index 46abb61..1a33f20 100644 --- a/entrypoints/main/dom.ts +++ b/entrypoints/main/dom.ts @@ -1,7 +1,7 @@ import { getMainDomain, selectCompatFn } from "@/entrypoints/main/compat"; import { html } from 'js-beautify'; import { handleBtnTranslation } from "@/entrypoints/main/trans"; -import { buildTranslationContext } from "@/entrypoints/utils/context"; +import { buildNodeTranslationContext } from "@/entrypoints/main/context"; // 直接翻译的标签集合(块级元素) const directSet = new Set([ @@ -374,7 +374,7 @@ function handleFirstLineText(node: any): boolean { while (child) { if (child.nodeType === Node.TEXT_NODE && child.textContent.trim()) { browser.runtime.sendMessage({ - context: buildTranslationContext(), + context: buildNodeTranslationContext(node), origin: child.textContent }) .then((text: string) => child.textContent = text) diff --git a/entrypoints/main/trans.ts b/entrypoints/main/trans.ts index 058b35a..266e0d8 100644 --- a/entrypoints/main/trans.ts +++ b/entrypoints/main/trans.ts @@ -8,7 +8,7 @@ import { detectlang, throttle } from "@/entrypoints/utils/common"; import { getMainDomain, replaceCompatFn } from "@/entrypoints/main/compat"; import { config } from "@/entrypoints/utils/config"; import { translateText, cancelAllTranslations } from '@/entrypoints/utils/translateApi'; -import { buildTranslationContext } from '@/entrypoints/utils/context'; +import { buildNodeTranslationContext } from '@/entrypoints/main/context'; let hoverTimer: any; // 鼠标悬停计时器 let htmlSet = new Set(); // 防抖 @@ -261,7 +261,7 @@ function bilingualTranslate(node: any, nodeOuterHTML: any) { let spinner = insertLoadingSpinner(node); // 使用队列管理的翻译API - translateText(origin, buildTranslationContext()) + translateText(origin, buildNodeTranslationContext(node)) .then((text: string) => { spinner.remove(); htmlSet.delete(nodeOuterHTML); @@ -283,7 +283,7 @@ export function singleTranslate(node: any) { let spinner = insertLoadingSpinner(node); // 使用队列管理的翻译API - translateText(origin, buildTranslationContext()) + translateText(origin, buildNodeTranslationContext(node)) .then((text: string) => { spinner.remove(); @@ -316,7 +316,7 @@ export const handleBtnTranslation = throttle((node: any) => { config.count++ && storage.setItem('local:config', JSON.stringify(config)); - browser.runtime.sendMessage({ context: buildTranslationContext(), origin: origin }) + browser.runtime.sendMessage({ context: buildNodeTranslationContext(node), origin: origin }) .then((text: string) => { cache.localSetDual(origin, text); node.innerText = text; From e0b30d494329e0f2b463a7f2a8e0b31ac4a7531c Mon Sep 17 00:00:00 2001 From: alka Date: Wed, 15 Apr 2026 23:13:01 +0800 Subject: [PATCH 5/5] fix: typo --- entrypoints/utils/option.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/entrypoints/utils/option.ts b/entrypoints/utils/option.ts index 7fd7524..05875b9 100644 --- a/entrypoints/utils/option.ts +++ b/entrypoints/utils/option.ts @@ -405,7 +405,7 @@ export const defaultOption = { {{context}} \`\`\` -Use the following context only for disambiguation, and do not translate or explain the context itself. +Use the above context only for disambiguation, and do not translate or explain the context itself. Translate the following text into {{to}}.If translation is unnecessary (e.g. proper nouns, codes, etc.), return the original text. NO explanations. NO notes: