1. 项目概述
1.1 零成本建翻译插件的核心思路
我这段时间一直在折腾一件事:不花一分钱,给自己搞一个顺手的网页翻译工具。起因很简单,日常看技术文档、读英文论文、刷开源项目 README,总得在浏览器和翻译软件之间来回切换,复制粘贴太烦了。市面上现成的翻译插件确实不少,但要么有字数限制,要么需要注册账号,要么用起来总觉得不够“自己的味道”。
正好大模型 API 的热度一路走高,DeepSeek、智谱这些厂商都在推免费额度,DeepSeek 的新用户甚至直接送了几百万 token。我的想法很直接:既然大模型翻译质量比传统机翻好一大截,免费 API 额度又够用,为什么不能自己用油猴脚本或者浏览器扩展,把这些能力包成一个翻译插件?
这个项目本质上是三件事的拼装:一个能触发翻译的浏览器脚本、一个调用大模型 API 的后端逻辑、一个把译文回填到网页里的渲染方案。听起来简单,做起来也确实不算难,但细节里坑不少,尤其是 API 鉴权、跨域请求、DOM 节点替换这几块,我踩了一路的雷,今天一并整理出来。
适合谁参考?想动手折腾 AI 工具但不想花钱买会员的朋友,前端刚入门想练练手的新人,或者单纯觉得现成翻译插件不够好用、想定制一个符合自己阅读习惯的译者。我会把从选型到上线、再到排查问题的全部过程写清楚,你照着做就能复现。
1.2 老翻译插件的问题在哪里
先聊聊我为什么非自己造轮子不可。市面上主流的翻译插件,核心痛点其实不是翻译质量,而是“流程割裂”。你装了一个插件,点一下翻译,它把整个页面变成双语对照,或者干脆替换成纯译文,然后你想看英文原句,还得再点一下切换。阅读过程中,中英文来回横跳,思路经常断。
更现实的问题是成本和质量不对等。免费翻译服务(比如各家网页翻译)虽然不花钱,但翻译专业术语时经常生硬得让人头大;付费 API 又按字符计费,长文档翻译一次可能烧掉不少钱。而大模型 API 的免费额度把这些事情变成了可能——DeepSeek 的免费额度我实测下来,翻译几百篇技术文档都用不完。
还有一个让很多人忽略的点:隐私。网页翻译要把整个页面内容发送给翻译服务商。用大厂公共翻译接口,等于把你在看的每一篇文档都交给对方。自建插件 + 免费大模型 API,虽然也不是完全本地化,但至少链路是自己可控的,数据去向更透明,心里踏实不少。
2. 技术选型与方案权衡
2.1 为什么最终选定油猴脚本 + 大模型 API
最开始我纠结过两条路线:正经做浏览器扩展(Manifest V3),还是搞油猴脚本。
浏览器扩展的优势很明确:权限体系完整、可以后台运行、UI 可以做得精致。但劣势同样扎心——发布到 Chrome 应用商店要交 5 美元注册费,而且 Manifest V3 对远程代码限制非常严格,每次改逻辑都得走审核。自己本地加载未打包扩展虽然免费,但每次打开浏览器都会提示“请停用以开发者模式运行的扩展程序”,体验很割裂。
油猴脚本(Tampermonkey / Violentmonkey)就不一样了。它本质上是一个用户脚本管理容器,脚本以纯 JavaScript 形式分发,改完刷新页面就能生效,不需要任何注册费,也没有加载未打包扩展的提示。对于翻译这种纯前端交互场景,油猴脚本的 API 足够用,GM_xmlhttpRequest 还能绕过跨域限制直接请求大模型 API,简直是为这个需求量身定做的。
大模型 API 这边,我的筛选标准是三条:有免费额度且够用、上下文窗口不要太抠门、API 风格对开发者友好。综合下来,DeepSeek-V3 和智谱 GLM 系列是首选。DeepSeek 的免费 token 额度大,智谱的 GLM-4-Flash 干脆就是永久免费的,两者都是 OpenAI 兼容的接口格式,代码写一次,换厂商就换一个 Base URL 的事。
2.2 免费大模型 API 选型对比
我实测对比了目前主流的几家免费/低价 API,列个表给大家参考:
| 厂商 | 模型名 | 免费额度 | 上下文窗口 | 接口风格 |
|---|---|---|---|---|
| DeepSeek | deepseek-chat | 注册赠送数百万 token | 64K~128K | OpenAI 兼容 |
| 智谱 AI | glm-4-flash | 永久免费 | 128K | OpenAI 兼容 |
| 硅基流动 | Qwen2.5-7B 等 | 注册赠额度 | 32K | OpenAI 兼容 |
| Moonshot | moonshot-v1-8k | 注册赠额度 | 8K | OpenAI 兼容 |
选型逻辑很直白。翻译任务依赖的是语义理解和上下文连贯,模型参数量太小的免费模型(比如 7B 以下)长段翻译时容易出现句子断裂、术语前后不一致的问题。DeepSeek 和智谱这些模型在中文上的表现明显更好,因为它们的中文训练语料占比高,对于中英翻译的语序调整、长句拆分处理得更自然。
接口风格统一用 OpenAI 兼容格式,是为了降低后期切换成本。你不想被某一家厂商绑定一辈子,也不想每次换 API 都要重写大段请求逻辑。OpenAI 兼容的 Chat Completions 接口,请求体结构基本是固定的 messages 数组 + model 字段,只有 Base URL 和 API Key 不同。脚本里把这两项抽成配置,换模型就是改配置的事。
2.3 自建插件相比现有插件的优势
这个方案的竞争力体现在三个维度。
第一是零成本。市面上一款靠谱的 AI 翻译插件,会员费一年少说几百块。自建方案用的是免费 API 额度,而且就算额度用完了,充值成本也远低于订阅制会员——DeepSeek 的 API 价格我记得是百万 token 才几块钱,翻译一篇万字长文的花费大概是一厘钱级别,几乎可以忽略不计。
第二是可控性强。你可以完全决定翻译的触发方式、提示词策略、译文展示形式。比如我在脚本里写了一个小功能:鼠标选中一段英文,按快捷键 Alt+T 就只翻译选中的部分,弹出一个小气泡显示译文。这种交互细节,现成插件基本不会给你做,但你可以在自己的脚本里随意实现。
第三个优势是“直觉级”隐私掌控。自建之后你清楚知道每一段文本发往哪里、被谁处理了。想彻底离线就用本地模型部署,想兼顾效果就用云端免费 API。这种掌控感,用现成工具是体会不到的。
3. 从零搭建翻译插件的完整流程
3.1 注册 API Key 与配置环境
第一步,拿到大模型 API 的访问凭证。以 DeepSeek 为例,打开官网注册账号,进入控制台创建一个 API Key。创建时注意 Key 只显示一次,务必立刻复制保存,丢了就得重新生成。智谱 AI 的流程类似,它的开放平台里有专门的 API Key 管理页面。
拿到 Key 之后不要直接硬编码在脚本里,这一点很重要。油猴脚本是明文 JavaScript,任何人打开你的脚本都能看到内容。如果你把 Key 写死在脚本里,再传到一个公开的脚本分享站,这 Key 基本就相当于公之于众了。我的做法是利用油猴脚本的 GM_registerMenuCommand 做一个设置面板,Key 存在 localStorage 里,既不用每次写死,也不会在代码里泄露。
配置环境这块,油猴脚本本身不需要“安装环境”,浏览器装好 Tampermonkey 扩展就行。这里有个小建议:用 Violentmonkey 替代 Tampermonkey 也完全可以,它在开源社区里维护更活跃,对 GM_xmlhttpRequest 的实现也更干净。两者的 API 基本互通,下面代码在任一环境都能跑。
3.2 核心代码实现:请求、翻译与回填
整个脚本的骨架分四个部分:配置管理、翻译请求、DOM 文本提取、译文渲染。我直接给出精简版核心代码,你可以在自己的脚本里扩展:
// ==UserScript== // @name 网页智能翻译 // @namespace translator // @version 0.1.0 // @description 调用免费大模型 API 翻译网页内容的油猴脚本 // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @grant GM_getValue // @grant GM_setValue // ==/UserScript== const CONFIG = { apiBase: 'https://api.deepseek.com/v1/chat/completions', model: 'deepseek-chat', apiKey: '', targetLang: '中文', maxTextLength: 6000 }; // 初始化配置与设置菜单 function initConfig() { CONFIG.apiKey = GM_getValue('apiKey', ''); GM_registerMenuCommand('设置 API Key', () => { const key = prompt('请输入你的 API Key:'); if (key) { GM_setValue('apiKey', key.trim()); CONFIG.apiKey = key.trim(); } }); } // 调用大模型翻译 function translateText(text, onSuccess, onError) { const prompt = `你是一位专业的翻译引擎。请将以下内容翻译为${CONFIG.targetLang}。\n只输出翻译结果,不要任何解释。\n\n内容:\n${text}`; GM_xmlhttpRequest({ method: 'POST', url: CONFIG.apiBase, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${CONFIG.apiKey}` }, data: JSON.stringify({ model: CONFIG.model, messages: [ { role: 'system', content: 'You are a professional translator.' }, { role: 'user', content: prompt } ], temperature: 0.3, stream: false }), onload: (res) => { if (res.status === 200) { const result = JSON.parse(res.responseText); onSuccess(result.choices[0].message.content.trim()); } else { onError(`请求失败,状态码:${res.status}`); } }, onerror: (err) => onError('请求发生网络错误') }); }这段代码核心就两个动作:构造 prompt,发送请求。temperature: 0.3是翻译任务的一个关键参数——翻译需要忠实原意,随机性太高会导致翻译结果不稳定,同样的句子翻两次说法不一样。0.2 到 0.4 之间是比较稳妥的区间。如果你的 API 支持temperature且默认值偏高,一定要显式压低。
3.3 让脚本识别网页正文并实现翻译替换
拿到译文之后,最麻烦的问题是“怎么把译文放回正确的位置”。整页翻译和选中翻译是两种不同的 DOM 操作策略。
整页翻译的贪心做法是把document.body.innerText全量发给大模型,让它整页翻完再把 body 的 HTML 替换掉。这种做法最大的副作用是网页结构几乎全毁,样式、事件绑定全都没了,原文和译文也没有对照关系。实际体验很糟糕。
更好的方案是按块翻译。遍历 body 下的文本节点,把连续的短文本节点合并成逻辑段落,逐段调用 API。这样做的好处是:译文回填时只替换对应文本节点的textContent,样式完全不动,阴影、布局都保留。代价是 API 请求次数变多,翻译长文时需要排队请求,速度慢一些。我最终用的是折中方案:把单个文本节点长度超过 60 字符的优先处理,短文本合并成组再请求,这样既减少请求次数,又能保持页面结构。
用户界面交互上我做了一个悬浮球。右下角一个小圆钮,点一下弹出操作面板:整页翻译、恢复原文、选中翻译三个按钮。恢复原文的思路是在翻译前先把整个 body 的 innerHTML 快照存到变量里,想还原就document.body.innerHTML = snapshot。选中翻译则监听鼠标松开事件,读取window.getSelection().toString(),把选中文本送去翻译,译文用临时气泡展示。
这里有一个极其重要的细节:翻译替换时一定要用textContent而不是innerHTML。网页里的文本节点有些包含嵌套标签,直接改 innerHTML 会把译文中的尖括号、特殊字符当作 HTML 解析,轻则译文显示错乱,重则直接破坏页面结构。我第一次写的时候就在这里翻过车,翻译一个包含<div>字样代码示例的页面,结果整页布局直接崩了。
3.4 界面改造与交互体验打磨
翻译插件的 UI 不需要多复杂,但也不能丑到影响使用。我的设计是右下角一个 40x40 的圆形按钮,半透明毛玻璃质感,悬浮在所有页面之上。这个按钮用 fixed 定位,z-index 设为 2147483647(油猴脚本常用最大级),确保任何页面上都能看到、点到。
点击按钮弹出操作面板,面板里除了三个功能按钮,还要显示当前的 API 配置摘要和剩余请求状态。我这里没有做配额查询,因为各厂商的配额查询接口差异较大,写通用代码收益不高。如果你想加,可以针对自己用的厂商单独实现一个余额查询函数,展示在面板里。
图标可以用简单的 SVG 内联,不需要引入外部图片资源。整页翻译按钮点击后,按钮颜色变为橙色并显示“翻译中”,所有请求结束后恢复原样。这样用户能直观感知任务是否进行中。我实测翻译一篇含 30 个文本块的长文,请求耗时大约 20 到 40 秒,如果不做状态反馈,用户很可能以为脚本卡住了。
操作面板的样式我用GM_addStyle注入,这样不会污染页面的原有样式体系。所有按钮确认一次点击只触发一次翻译,防止用户连点导致 API 请求堆积,浪费额度还容易触发厂商限流。
4. 常见问题与排障手册
4.1 API 请求报错的排查思路
翻译脚本最大的不稳定因素就是 API 调用,我把高频报错整理成了速查表:
| 报错信息 | 原因 | 解决办法 |
|---|---|---|
| 401 Unauthorized | API Key 错误或过期 | 检查 Key 是否复制完整,注意别带多余空格 |
| 403 Forbidden | 账户被风控或额度用尽 | 登录控制台查看配额,确认没有触发滥用规则 |
| 400 Bad Request | 请求参数格式不正确 | 检查 JSON 结构,确认 model 字段名与厂商文档一致 |
| 429 Too Many Requests | 请求过于频繁 | 在脚本里加个简单的并发限流,比如每次请求间隔 500ms |
| context length exceeded | 单次输入超长 | 降低 maxTextLength 配置,或把长文本切成多段分批翻译 |
Debug 技巧:所有 GM_xmlhttpRequest 的响应在 onload 里都能拿到,打印res.responseText能看到完整错误信息。很多厂商在返回体里会写清楚具体是哪个字段出问题,排查效率高很多。我在脚本里加了一个隐藏的日志面板,把最近的 API 返回体都记录在里面,出问题时按 Ctrl+Shift+L 就能快速调出查看。
网络错误也不容忽视。有些网页的 CSP(内容安全策略)策略比较严格,可能会阻止油猴脚本发起的请求。不过 GM_xmlhttpRequest 在油猴中跑在独立的沙箱环境里,绝大多数情况下能绕过 CSP 限制,只在极少数的暴力 CORP 策略下才会被拦,这种情况只能换浏览器尝试。
4.2 翻译质量不理想的优化技巧
免费模型和头部商业模型在翻译质量上有差距,这是客观事实。但通过 prompt 优化和参数调整,这个差距能缩小到可用范围内。我实践的优化方案有这几条:
第一,在 system prompt 里写明“你是专业译者,擅长科技文档翻译”,能显著提升术语翻译的准确度。第二,用 few-shot 示例引导格式——在 prompt 中给出一个英译中的例子对,模型会照着格式输出,翻译的语序和用词会顺畅很多。第三,temperature固定为 0.2~0.3,上面说过了,这是翻译稳定性的生命线。
还有一个容易被忽略的点:不要翻译“空的”文本。有些文本节点的内容只是空格、换行符、标签残片,发给 API 纯属浪费额度。在提取文本时做一个净化预处理,过滤掉长度小于 2 且没有任何字母数字的内容。具体代码就三行:
function isMeaningfulText(text) { return text && text.trim().length >= 2 && /[a-zA-Z0-9\u4e00-\u9fa5]/.test(text); }4.3 页面样式错乱与被反爬识别
翻译回填后页面 CSS 错乱,最常见的原因是网页用了:lang()选择器或字体族设置,中文译文的渲染和原文不同。解决办法:在注入的样式中显式覆盖字体族,比如font-family: system-ui, -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;,中文支持基本都有。
被反爬识别这个坑比较冷门,但值得注意。少数网站会监测页面文本变化,一旦检测到 DOM 被大规模改写就弹出人机验证或直接封会话。遇到这种站点,我的建议是别硬刚,干脆放弃整页翻译,只保留选中翻译和阅读模式翻译两种交互,降低被检测到的频率。毕竟一个脚本工具,没必要跟网站的防护机制死磕。
另外,有些网站用的是 Canvas 字体内嵌文本,文本节点存在内部状态里,textContent改了界面不变。这种没法用简单办法处理,碰到了随手记一下站点名单,绕过去就行,别在这上面浪费时间。
5. 进阶玩法与扩展方向
5.1 一键接入本地大模型,实现完全离线翻译
免费 API 用着确实香,但对数据敏感的场景,总归还是觉得云端兜底不稳妥。近一年本地大模型部署的门槛降了很多,Ollama 一条命令就能拉起 Qwen2.5、Llama 3 这些开源模型,我的 16G 内存笔记本跑 7B 量化模型,翻译速度大概是每秒 15 到 25 个 token,虽说不算快,但配合阅读节奏刚好够用。
接入方式比你想的简单。本地 Ollama 默认监听 11434 端口,接口也是 OpenAI 兼容格式。脚本里只需要把apiBase换成http://127.0.0.1:11434/v1/chat/completions,model改成本地模型名,API Key 随便填个占位符比如ollama,整个逻辑不用改一行代码。实测下来,7B 量化模型的翻译质量在简单的说明文档、代码注释场景已经相当能打,但在复杂的文学文本、俚语表达上明显不如云端大模型,需要有所预期。
这种“云端免费 API + 本地模型热切换”的架构,是我比较推荐的组合方式。日常查阅资料用云端,翻译敏感文档时一键切本地,两种模式共用一个 UI,体验一致,数据流向完全可控。
5.2 增加术语库与自定义翻译风格
翻译技术文档时最头疼的是术语不一致。同一个词,上一段翻译成“接口”,下一段翻译成“API”,阅读时很割裂。大模型本身能根据上下文保持一致,但在跨页面的多次翻译中,缺乏记忆能力。
想要更强的术语一致性,可以在 prompt 里注入术语表。在脚本配置里加一个 JSON 字段,记录你常用的中英对照词条,每次请求前把术语表拼进 system prompt。实际效果我测试过,翻译同一篇文档,带术语表的版本术语一致率能提高 40% 以上。知识库文件太长可以单独存成 Markdown 格式,翻译前动态读取并截取前若干条加入 prompt,注意上下文窗口别撑爆。
自定义风格也是同理。有些人喜欢译文简短干练,有人喜欢保留原文结构,这些偏好都可以通过改 prompt 表达。我琢磨出一个通用模板:
翻译时遵循以下规则: 1. 术语按术语表翻译,未收录的专有名词保留英文 2. 译文力求通顺,避免机器翻译的僵化语序 3. 代码块、变量名、URL 保持原样,不翻译 4. 保持原文的段落结构和标点习惯把这些规则拼接进 system message,模型就能按你的口味来。而且每个用户完全可以维护一份属于自己的“翻译规范”,这种定制程度是现成工具给不了的。
5.3 从油猴脚本升级为浏览器扩展
当你把脚本调到顺手、功能也稳定了,想发布给更多人用,油猴脚本的分发渠道是有限的。虽然 GreasyFork 可以发布脚本,但普通用户装起来总得多一步“安装插件管理器”,传播门槛偏高。此时可以考虑把它封装成 Manifest V3 的浏览器扩展。
升级核心是把 GM_xmlhttpRequest 替换为后台 service worker 的fetch,油猴 API 换成 Chrome API。好消息是翻译核心逻辑和 DOM 处理部分可以 90% 复用,只要把接口适配层重写一遍即可。MV3 的权限申请要注意,跨域请求需要声明host_permissions,需要明确的域名列表。如果只想针对特定站点翻译,这个列表可以大幅收敛,浏览器商店审核也更容易过。
实际上,我在把脚本打包成扩展的过程中,最大的工作量不是代码改造,而是被商店审核折腾——隐私政策、图标素材、权限说明都要准备齐全。如果是自用或者小圈子分发,油猴脚本的轻便优势明显;想大规模发布,扩展的正式感更合适。两条路互为补充,看你的具体诉求。
6. 实操过程中的独家心得
做到最后,我把这段时间折腾的核心教训浓缩成几件事,供你少走弯路。
第一件事,一定要先想清楚你要“整页翻译”还是“段落翻译”,再动手写代码。我一开始贪心做整页百分百翻译效果,结果被各种奇怪的布局问题折磨了两天,后来降到按文本块翻译,效率和稳定性都大幅提升。大部分阅读场景,我们只需要理解每一段的含义,并不需要一个“完全中文版页面”。
第二件事,免费 API 不等于无限额度,请求策略要克制。我没做任何限流的时候,一次手滑触发了整个页面的文本节点全量翻译,分钟级就把一天试用额度的四分之一耗完了。后来加了防抖、去重、暂停按钮之后,额度消耗速度降了一个数量级。省着用,才是零成本的正确打开方式。
第三件事,prompt 工程在这个项目里比任何代码优化都重要。同样的模型、同样的 API,一个精心设计的 prompt 和一个随手写的 prompt,翻译效果可以天差地别。多花点时间打磨措辞、测试不同写法,性价比远高于折腾更复杂的脚本逻辑。
最后想说的是,这个项目最大的价值不只是省了钱,而是让我重新理解了“工具”这个词——它不再是一个固定的、别人定义好功能的商品,而是可以被任何人按需裁剪、组合、改造的数字积木。API 是积木,脚本是积木,模型也是积木。虽然这一路踩了不少坑,但当你亲手把一个只服务于你自己的翻译工具跑起来,那种“一切尽在掌握”的感觉,是花钱买现成工具永远体会不到的。你要不要也试一把?