chrome-plugin-manga-translator

Round 9 audit — 对着承诺再读一遍

前八轮每轮只问一个问题:钱花在哪、有没有人授权、界面上说的话是不是真的。 第八轮修完之后,两个新的审查同时被派出去,一个只查钱,一个只查体验。

结论:这一轮找到的不是新的洞,而是同一批句子在别处又说错了一遍。


P1 — 本地模型的用户,每一屏都在被告知要花钱

现象

一个用 Ollama 的用户打开弹窗,看到:

本页 2 张可翻译图片,翻译会调用模型服务,会产生费用。

页面上写着:

翻译这 2 张图会产生调用费用(缓存命中不计费)

HUD 的按钮写着:

重新翻译失败项(会再次计费)

设置页写着:

页面内后续图片出现时自动继续翻译(会产生费用)

而引导页写的是:

翻译会在你的电脑上完成,图片不会离开本机,也不产生服务商费用。

根因

「会不会花钱」取决于用哪个服务商,而这个扩展一直支持两个世界:

服务商 图片去哪 账单
MiniMax / SiliconFlow / OpenRouter / Gemini / OpenAI 离开这台机器 有
Ollama / LM Studio 留在这台机器 没有

代码里没有「本地服务商」这个概念——providers/base.ts 里有 KEYLESS_LOCAL_PROVIDERS(不需要 Key 的那一组),但没有「不收钱的 那一组」。于是所有关于钱的句子都写死成「会产生费用」。

引导页是唯一一处做对了的:它单独判断了服务商。设置页没有。

这不是措辞问题。这是扩展唯一一句用来建立信任的话(关于钱的那句),对 一群用户来说,从头到尾都是错的——而这群用户恰好是最不该被吓跑的人。

修法

新增 src/shared/local-providers.ts:本地服务商只有一份名单, providers/base.ts 也从那里取,两边不会各说各话。

所有关于钱的句子都改成带服务商参数:

describeFee(provider)          // 会产生费用 / 不产生服务商费用,图片也不会离开这台机器
describeFeeShort(provider)     // 括号里用的短版
describeRequestCount(provider) // 一张图会变成几次请求
describeRetryFailedCost(provider)
describeRetryFee(provider)     // 按钮上用的短版

未知的服务商按云端算。 新加一个服务商时,如果它其实不收钱,产品至少 不会在用户面前先说”免费”。


P1 — 一次请求变成多次请求,理由只说了两个

共享的那句话原来写着:

一张图可能变成多次请求:长条图会拆成多片分别请求,模型没读出文字时还会用 OCR 再分批试一次。实际请求次数可能高于图片数。

第三个理由没写:请求超时或失败时会自动重试一次(retryWithBackoff(fn, 2))。 超时是服务商已经收了钱的请求——扩展自己也是这么记的(isAbandonedRequest)。 一张超时的图 therefore 是两次请求,而那句话只说了”可能高于”一个来源。

顺带修掉的:OCR 兜底对本地模型是不做的,那句话却对所有人说本地模型 也会走 OCR。现在它只对云端服务商说。


P1 — 页面上那个会花钱的按钮,点一下就花了

弹窗里同一个动作是这样的:

重新翻译失败的 3 张 → 弹一个确认框,写明会重新请求多少张

页面上的 HUD 是这样的:

重新翻译失败项(会再次计费) → 点一下就发请求

HUD 是读者在翻章过程中、在旁边、随手会点的那一个。它连张数都没写。

修法


P1 — 页面换了图片地址,扩展就再问一次

观察器的规则是「只问从来没问过的图」,而「这张图」是用地址认的:

img.src || `img-${offsetLeft}-${offsetTop}-${width}-${height}`

两种常见的页面做法会让这条记录失效:

  1. 响应式站点把缩略图换成大图——同一个元素,地址变了;
  2. <img> 只写 srcset 不写 src——img.src 是空的,于是退化成 “在页面上的位置”,而位置会随着页面回流而变。

两种情况下观察器都会认为这是一张新图,再问一次——同一张图,两次请求。

修法

地址和元素都记。元素是这一页上这张图片的稳定身份;地址能抓住”页面把 元素重建了一遍”的情况。两个记录都说”问过了”,才算问过了。

顺带:getImageKey 改用 currentSrc(浏览器真正取的那个地址), srcset 的图片从此有了一个稳定的地址。


P1 — 缓存的承诺,三处说得不一样

地方 原来
商店页面 翻译缓存:相同图片不重复请求,节省额度
设置页 翻译过的漫画会缓存在本机,重复打开同一页不会再次调用模型
审核说明 再次翻译同一页预期不再发出任何服务商请求(之前翻译失败的图片没有进缓存,会重新请求)

审核说明是对的:失败的翻译不进缓存,所以下次打开还会再请求一次。 另外两处没有这个例外,所以它们在承诺一件做不到的事。

修法

失败不能进缓存——把”失败”当成”翻译好了”缓存起来,缓存里就会躺着一个 永远翻不出来的图。所以是改承诺,不是改代码:

翻译缓存:翻译成功的图片不重复请求,节省额度

设置页同一句话也照抄。商店页面的这句话在 docs/store-claims.md 里登记过,改文案要同步改那张表——检查脚本会把 两者对不上这件事变成构建失败。


P2 — 剩下的几条

  1. 设置页的第一行承诺了一个不存在的后端。 「可直接使用构建时预置的 后端」——上架的那个构建里一个都没有(引导页知道这件事,设置页不知道)。 现在这一行按”这个构建有没有预置后端”来写。
  2. 同一个动作有三个名字。 弹窗按钮「强制重翻」、快捷键列表「忽略缓存 重新翻译当前页面」、商店页面「忽略缓存重翻」——同一个屏幕里,隔着 40 个像素。现在统一成「忽略缓存重翻」,并且加了一条测试把 public/manifest.json 里的描述和代码里的描述钉在一起。
  3. 设置页的输入框没有边框。 border-white/20 压在近黑的背景上是 1.7:1,WCAG 1.4.11 要的是 3:1。弹窗的选择框早先修过同样的问题, 设置页有十个控件没修——包括贴 API Key 的那个框。
  4. 已经翻过的页面,按了按钮之后没有下一步。 摘要写着「本页 N 张图片 已经翻译过了」,而能做事的那颗按钮(忽略缓存重翻)在下面。现在摘要 直接指过去。
  5. 弹窗里的用量数字把本机调用算成了账单。 「本月 N 次 API 调用」—— 本地调用也在里面,而它不花钱。设置页早就写清楚了这一点,弹窗(本地用户 真正会看的地方)没有。现在本地服务商写「次模型调用(本机模型,不产生 费用)」。
  6. 「测试配置」的探测遇到 408 没有计数。 规则说”被放弃的请求算一次 账单”,探测器没有遵守。补上了。
  7. 网络错误(status 0)不计数。 客户端分不清”请求还没发出去就断了” 和”服务商已经跑完模型才断了”——不计数是少报一次,计数是多报一次。 这条规则以前只存在于代码里,现在写在了规则本身旁边。

检查过、决定不改的


这次验证的方式

测试 去掉修复后
本地服务商的句子不说”产生费用” 句子变成「会产生费用」
费用说明里出现 OCR 兜底 本地服务商也被告知会走 OCR
页面换了地址的图不再问第二次 请求数 1 → 2
清单里的描述与 manifest 一致 描述对不上,测试失败
弹窗对本地服务商说「次模型调用」 弹窗写着「次 API 调用」
设置页第一行按”有没有预置后端”来写 无后端时仍承诺有

模式

前八轮的模式是”规则要写在执行它的代码旁边”。这一轮补上的是它的另一半:

一句话要知道自己在对谁说。

「会产生费用」这句话对 MiniMax 的用户是对的,对 Ollama 的用户是错的。 而扩展一直只准备了一种说法,于是要么对一半人说错,要么对另一半人说 对——这次是前者。