chrome-plugin-manga-translator

Round 7 audit — 钱去了哪里

第六轮问的是「产品承诺的东西,代码做得到吗」。第七轮问的更窄,也更贵:

这一轮里,用户没有点任何按钮,账单动了没有?

扩展只有一个地方会不经过用户点击就自己花钱:自动续翻的观察器 (src/content/auto-translate-observer.ts)。它盯着页面,图片一加载就翻。 第四、第五轮把「说了什么」和「记了几次」对齐了;这一轮查的是钱到底 是怎么花出去的,以及每一次花钱用户是不是都点过头。

结论:观察器有一个洞,而且它正好落在最贵的地方。


P0 — 失败过的图会被自动重翻,一轮一次,最多二十轮

现象

一页漫画 20 张,3 张失败了。用户看到的是「3 张失败」和一个按钮: 「重新翻译失败的 3 张」。他没点。

但账单一直在涨。

根因

三处代码各自都合理,合起来是一个洞:

位置 行为 后果
scanImages() 只排除「已经翻译成功」的图(带 PROCESSED_CLASS 或在 processedImages 里) 失败的图既没有标记也不在集合里,重新回到候选池
runTranslation() 每轮非「重试失败项」的翻译开始前清空 failedImages / failedImageKeys 用户那份「要重试的清单」被自动流程擦掉
maybeAutoTranslateNewImages() 用 findTranslatableImages() 判断「还有没有图要翻」 失败的图让观察器永远觉得有事可做

于是:观察器每一轮都把失败的图重新捡起来,重新问一次模型。最多二十轮 (MAX_AUTO_TRANSLATE_FOLLOW_UP_RUNS),每轮都是用户没有授权的请求。

第二轮的伤害更大:用户点「重新翻译失败的 N 张」时,这个 N 已经被自动 流程清空了——按钮要么消失,要么指向一张用户从没见过的图。

修法

引入一个和「成功」分开记账的集合:

attemptedImageKeys —— 问过了的图

规则只有一条:观察器只捡从来没问过的图。

用户侧的语义没有变:失败还是失败,弹窗还是给「重新翻译失败的 N 张」, 点了还是收钱。变的只是没有人替用户点。

顺带堵上的第二个洞

同一轮里发现的:用户点了「取消当前翻译」之后,这一轮没轮到的图从来 没被问过,观察器会把它们当成「新加载的图片」捡起来继续翻。用户刚说了 停,账单继续涨。

现在这一轮没轮到的图也被记成「问过了」——用户自己点的取消、以及致命 错误(密钥无效、被限流)中止的轮次,都一样。它们没有丢:用户下一次点 「翻译当前页」会清掉标记,它们重新获得机会。


P1 — 一次长条图的费用,界面上写的是一个假的确定值

现象

右键翻一张 800×5000 的条漫,页面在进度条下面写着:

这张图是长条,会拆成 5 片分别请求(5 次调用费用)

这是我们自己写上去的句子,而且是错的。

真实的账

也就是最多 2N + 1 次。原来那句「N 次」是把自己说小了——用户以为 花 5 次的钱,实际可能花 11 次。

修法

这句话现在只承诺它能承诺的:片数是真的,请求次数是「可能高于 N 次」。 共享常量 TILING_COST_NOTE 早就是这个口径,页面这句却自己另写了一份。

另一处:整条都没有文字的条漫,从来不写缓存

整图路径故意不缓存「零文字」的答案——那是 1 次请求,让下一次再试一次 几乎不花钱,而且模型看漏的页面还有翻身的机会。这条规则是对的。

但条漫是 N 次请求。同样的规则套在条漫上,代价就是:用户下次打开同一页, 再付 N 次;而没有文字的那些片还会再被问一次,实际可能翻倍。

现在:每一片都回答了「这块没有文字」的条漫也写缓存。这是答案,不是 失败——用户想重来的话还有「忽略缓存重翻」这个出口。

(第八轮把同一条规则补到了整图路径上:一张整页没有文字的图,每次访问都 要再付一次钱,而商店页面写着「相同图片不重复请求」。见 第八轮。)


P2 — 剩下的几条,按影响排序

这一轮没有全部处理。以下是查过、确认存在、但本轮没有动的:

  1. 用量面板把缓存命中和 API 调用混在一起数。 「翻译记录」里既有真 的调用也有缓存命中,两者的钱完全不同。(第七轮补了一半:面板现在会 说明两个数字分别是什么,但它们仍然并排显示。)
  2. ETA 数的是图,不是请求。 一页 20 张图、其中 3 张是长条,ETA 说 「还剩 12 张」,实际可能还有 30 次请求。
  3. 用量面板不区分本地模型和云端模型。 本地模型的「调用」不产生费 用,和云端的放在一起会误导。(第七轮补了说明,但数字仍然合并显示。)
  4. PROVIDER_PREFLIGHT 的注释说这次探测要计费。 它不计费——validateConfig() 只看字符串,不发网络请求;真正会花钱的是「测试配置」按钮,那是另一个 入口。(第七轮改了注释。)
  5. 混合区域路径(hybrid-regions)的请求数是 ceil(气泡数 / 10)。 默认路径不是它(默认是 full-image-vlm),而且设置页没有开关,只能 改配置文件才会走到——所以商店用户碰不到。

已经在第七轮补上的


这次验证的方式

三条回归测试都写成「去掉修复就会失败」:

测试 去掉修复后
失败过的图不会在下一轮被重新请求 请求数 1 → 2
自动的一轮不会清空用户的失败清单 清单 1 → 0
中止的一轮不会把剩下的图交给观察器 请求数增加

外加一条缓存测试:同一条条漫翻第二次,provider 一次都不会被问到。


模式

上一轮的结论是「每修一次,问题就往下一层挪一层;规则要写在执行它的 代码旁边」。这一轮补上一句:

没有人点过的请求,先问「谁授权的」。

自动续翻是这个产品里唯一一个会自己花钱的功能。它的每一条规则都应该能 回答这个问题:观察器为什么翻这张图?——因为这张图从来没被问过。用户 为什么付这笔钱?——因为他看见了清单,因为他点了。