我是如何用 Codex 做出一个沉浸式 AI 自动翻译插件的
GitHub 项目地址: realreadpaper/translate
先说结论
这个项目最后变成了 Immersive AI Translate:一个面向 Chrome / Edge 的 Manifest V3 浏览器扩展。
它的稳定能力是网页正文沉浸式翻译:点击页面悬浮球后,扩展优先翻译当前视口附近内容,把译文插回原页面,并支持双语、仅原文、仅译文三种阅读模式。
我真正想验证的不是“AI 能不能写一个翻译插件”,而是:当功能从网页扩展到 PDF、YouTube 字幕、ASR/OCR 预留能力时,项目还能不能保持清晰边界。
最后沉淀下来的核心拆法是:
segment:翻译的最小稳定单位。provider:只负责请求模型和解析响应。target:区分网页、PDF、YouTube 字幕。renderer:把结果放回正确位置。- prompt contract:让模型输出成为可验证接口,而不是随缘文本。
阅读路径
这篇按实现顺序展开:
- 先让 Codex 设计边界,而不是直接写 content script。
- 用
segment id解决网页译文回填。 - 用 provider adapter 隔离 DeepSeek、OpenAI-compatible 和传统翻译服务。
- 用 translation target 扩展 PDF 和 YouTube。
- 用更硬的提示词约束模型输出。
- 用测试和 E2E 把边界钉住。
1. 我先让 Codex 做设计,而不是写代码
一开始如果直接说“帮我写一个网页翻译插件”,很容易得到一个 demo:读取页面文字、发给模型、把结果塞回 DOM。这个路径看起来快,但后面一定会被复杂网页、动态内容、模型输出格式和阅读模式拖垮。
所以第一条 prompt 不是实现需求,而是设计约束:
设计一个 Chrome MV3 沉浸式网页翻译扩展。
要求:
- 不破坏原网页 DOM 结构
- 译文必须能按段落稳定回填
- 支持双语、仅原文、仅译文三种模式
- Provider 可替换,默认 DeepSeek,同时支持 OpenAI-compatible
- API Key 保存在浏览器本地
- 先写清楚模块边界、消息协议和测试策略
- 不要直接实现代码Codex 给出的第一版边界非常接近最终结构:
popup/options
-> background service worker
-> provider adapter
-> content script
-> DOM extractor / renderer
-> chrome.storage.local这个拆法的价值是:UI、配置、模型请求、DOM 操作和持久化互相不抢职责。后面加 PDF、YouTube 时,项目没有立刻变成一坨特判。
2. 网页翻译的关键:不要翻译整页 HTML
网页翻译最危险的捷径,是把整页 innerHTML 丢给模型。它会破坏事件、样式、脚本、React/Vue 状态,也很难恢复原文。
我让 Codex 按文本块实现:
Content script
-> DOM TreeWalker 找可见文本节点
-> 跳过 script/style/code/pre/input/hidden
-> 归并成 SourceSegment[]
-> 给真实 DOM 节点写 data-segment-id
-> 等译文回来后插入 data-translation-for 节点核心数据形态很简单:
type SourceSegment = {
id: string;
text: string;
};
type TranslatedSegment = {
id: string;
translatedText: string;
};这个设计解决了三个问题:
- 译文能按
id回到原文附近。 - 阅读模式切换不需要重新请求模型。
- 扩展只拥有自己插入的译文节点,不接管整个网页。
后来悬浮球的“视口优先翻译”也是从这里长出来的:点击后先翻译当前视口附近的 segment,用户滚动时再翻译后续内容。单批默认控制在 6 段,等待时间短,模型也不容易乱返回。
3. Provider 只做模型适配
Provider adapter 不碰 DOM,也不管 popup。它只处理四件事:
validateConfig()translateSegments()normalizeError()getMeta()
Background service worker 才是调度层:
读取 settings
-> 校验 provider 配置
-> 收集 segment
-> 分批
-> 调 provider
-> 把结果发回 content script这样 DeepSeek、OpenAI-compatible、自定义 Base URL、传统翻译服务都可以走同一套任务流。新增 provider 时,不需要碰 dom-extractor 和 segment-renderer。
4. 提示词不是文案,是接口契约
模型返回值必须可机器解析,所以 prompt 写得很硬:
You are a professional translation engine for immersive bilingual reading.
Return only one valid JSON object matching exactly:
{"segments":[{"id":"same id","translatedText":"translation"}]}
Rules:
- Copy each input id byte-for-byte.
- Never translate, shorten, rename, or omit ids.
- Do not return markdown fences, explanations, notes, or extra text.
- If a segment is hard to translate, keep its original text.这里的重点不是“翻译得更好”,而是“返回值能对齐”。浏览器扩展最怕的是:翻译质量还可以,但某个 id 被模型改了,导致译文错位。
即便 prompt 很硬,真实模型仍会返回各种格式:
- Markdown
json代码块 - 顶层数组
{ "segments": [...] }{ "translations": [...] }- 字段名变成
translation/targetText - 近似 JSON,但字符串没包好
所以我让 Codex 加了 parseTranslatedSegments 容错层。它的原则是:优先标准 JSON;能修复就修复;能按源 segment 顺序恢复就恢复;无法可靠恢复才让该批失败。
经验很明确:prompt 负责约束正常路径,parser 负责保护异常路径。
5. PDF 和 YouTube 不能塞进网页逻辑
网页跑通后,我开始加 PDF 和 YouTube。这个阶段如果继续往 content script 里塞判断,很快就会失控。
于是我把 prompt 改成“目标架构”:
把现有网页翻译升级为 translation target 架构。
目标类型:
- html-page
- pdf-document
- youtube-subtitles
要求:
- Provider 批量翻译逻辑复用
- 不把 YouTube/PDF 特判混进 dom-extractor
- 每种 target 有自己的 collector 和 renderer
- anchor 能表达 DOM、PDF block、subtitle cue 的定位信息这个设计让三类内容走同一个翻译调度,但各自保留采集和渲染方式:
html-page
collector: DOM segment
renderer: insert translation node
pdf-document
collector: PDF text block
renderer: PDF translation workspace
youtube-subtitles
collector: timedtext / ASR cue
renderer: video overlayPDF 的边界被定得很窄:只处理独立 PDF 文档,打开扩展自己的 PDF 翻译工作台,优先读取文本层。扫描版 OCR 先保留入口,不假装已经稳定。
YouTube 也先走稳定路径:有字幕轨道时读取 timedtext,提前翻译 cue,再由 overlay 按播放器时间显示。无字幕视频才进入 ASR 实验路径,而且 ASR 不阻塞有字幕视频。
6. 不同内容需要不同提示词
同一个 provider,不同 contentKind,应该用不同 prompt。
网页文本强调保留语气和 UI 标签:
For web page text, preserve the original tone and intent.
Keep URLs, product names, code identifiers, and UI labels stable.PDF 更像技术文档或论文,需要保留公式、引用和参考文献:
The input is from an academic or technical PDF.
Preserve formulas, citations, references, code identifiers,
variable names, model names, dataset names, section numbers,
figure/table labels, and bibliography markers.YouTube 字幕则要短、自然、适合屏幕阅读:
The input is timed subtitle text.
Translate naturally and concisely so each cue remains readable on screen.这比在外层硬调参数更有效,因为它把“译文应该长什么样”直接绑定到内容类型。
7. 广告清理是翻译质量的一部分
真实网页里,污染翻译请求的往往不是正文,而是广告、赞助卡片、动态推荐模块和广告 iframe。
所以翻译前会先跑广告清理:
- 页面打开后先清理一次。
- 用户点击悬浮球时再清理一次。
- 自动翻译和 popup 手动翻译也会先清理。
- 动态插入的广告通过监听继续隐藏。
这不是为了做广告拦截器,而是为了让待翻译 segment 更接近用户真正要读的内容。
8. 测试怎么跟上
这个项目能继续扩展,靠的是测试直接覆盖边界:
dom-extractor:segment 顺序、跳过规则、站点兜底。segment-renderer:双语、仅原文、仅译文。- provider:JSON contract、异常响应、错误归一化。
- PDF:文本块解析、缓存、工作台参数。
- YouTube:字幕轨道、overlay、预取队列、ASR session。
- Playwright:扩展 E2E 和 DeepSeek 联机冒烟测试。
我给 Codex 的实现 prompt 通常会带验证命令:
先补 Vitest 覆盖:
- provider 返回 Markdown json fence 时能解析
- provider 返回 {segments:[...]} 时能解析
- id 缺失但顺序一致时能按 sourceSegments 恢复
然后实现最小解析逻辑。
最后运行对应 test 文件。
不要改无关模块。这样 Codex 不会把“更健壮”理解成重构半个 provider 层。
现在的状态
当前稳定能力仍然是 html-page 网页翻译:
- 悬浮球视口优先翻译。
- Popup 保留全页手动入口。
- 支持双语、仅原文、仅译文。
- DeepSeek 默认配置,支持 OpenAI-compatible、自定义 Base URL 和传统 provider。
实验链路:
- PDF:独立工作台,可翻译可提取文本层的文档,OCR 兜底保留入口。
- YouTube:已有字幕轨道可渲染翻译 overlay,无字幕 ASR 仍在实验中。
收获
这次最大的收获是:AI 写代码很快,但项目能不能长大,取决于边界。
对这个翻译插件来说,关键边界是:
segment是最小稳定翻译单位。provider不碰 DOM。target不污染普通网页链路。renderer只把结果放回正确位置。- prompt 是接口契约的一部分。
Codex 最适合的角色不是一次性生成整个扩展,而是把每条边界变成代码、测试和文档。它负责速度,我负责方向。