豆备 DOUBAK
← 开发日志

一个目录里十份档案,只读了一份

下载文件夹里平铺着十份档案的文件。扫描器取了第一个匹配的清单,剩下九份、573 条捕获无声消失——而校验器只验了那一份,然后说「通过」。

~/downloads/old 是个再普通不过的下载文件夹: 十份档案的清单和段文件全平铺在里面,一个 manifest.json, 外加一堆截图。

找清单的那行代码是这么写的:

readdirSync(dir).find((f) => f.startsWith('index-'))
                 ↑ 第一个匹配的

然后另外单独读了那个 manifest.json, 而且没有任何地方检查这两样是不是同一份档案的。结果是一个自相矛盾的输入

manifest 说   bundle_id = 786e5c
清单第一行是  20260730T102904Z-f4ef8c#000001
              ↑ 完全是另一份档案

九份档案、573 条捕获,一声不吭地没了。 解析照常跑完,产出照常写出来,是一份看起来完全合理的结构化数据——只是少了一大块历史。

校验器也没看出来,而且它说得比谁都肯定

规范仓库那个校验器 读的是 manifest.index.filename——manifest 说清单叫什么,它就去验那一个。 它从来没问过「这个目录里是不是还有别的清单」

于是它验了十份里的一份,然后打印「通过」。 那句话是真的,而且完全没用。

修法不是「拒绝这种目录」

第一反应是报个错让人先把目录整理好。但这和 「解析器会连子目录一起找」是同一件事的两面: 逼人手工整理目录,只会换来一份被漏掉的档案—— 而漏掉是安静的,报错至少是响的。

况且什么都不用猜:段文件的名字里嵌着档案编号,而清单的每一行都写着自己在哪个段里。 所以改成把每一个清单都读进来,一个清单就是一份档案。

两条规矩从这儿掉出来,都是关于「该信谁」的:

  • 档案编号取自清单的文件名,不取自 manifest。 文件名和它里面每一条捕获编号是同源的;而 manifest 是一份关于某个编号的陈述。 陈述可以指错,同源的东西不会。
  • 编号对不上的 manifest,干脆不挂上去。 硬挂的话,另一份档案的抓取状态会被喂进「哪些东西可以断定是被删了」那套判断里—— 也就是拿别人的水位线来决定这里什么算删除。 不挂只是少了一份证据,那是安全的方向。

「这个目录挤了好几份档案」仍然会报出来——十份平铺在一起, 最多只有一份还留着自己的完整性证据。

同一个 bug,在另一个仓库里等着

生成站点那边取图片的代码,写的是一模一样的东西: readdirSync(dir).find(...),只看一层,也不递归。

不一起修的话,它会比之前更糟。解析器现在读全部十份了, 于是结构化数据会引用一批生成器根本导不出来的图—— 而离线保证会恰好在那些本来就已经很乱的档案上失效

「未知」不是一个账号编号

同一天还有一个形状完全一样的问题,主角换成了账号。

标记的降级身份键长这样:d:<账号>:<媒介>:<作品号>, 而账号取不到时填的是字符串 'unknown'没有 manifest 的档案就没有账号,于是:

有 manifest 的   d:82160871:drama:1234
没 manifest 的   d:unknown:drama:1234
                   ↑ 两套永不相交的键空间

这和导入老档案时挖出来的那个 data-cid bug 结构上一模一样,只是换了个字段来劈。 实测把九份没有 manifest 的档案并进去:

2955 个作品  →  2960 条标记
                 ↑ 5 条戏剧记录裂成了两份,
                   状态、评分、标记日期全都一样

只有戏剧,因为戏剧的列表页不带 data-cid; 其他媒介有上游编号,走的是另一套键,怎么都能并回去。

认领,而不是猜

修法不是给它编一个账号,而是认领: 一个目录里如果只有一个已知账号,那些没有 manifest 的档案就归到它名下, 并且报一条告警把它们逐个点名。 (一个目录里有两个账号本来就是硬错误,所以这个前提相当牢。) 零个已知账号时,大家一起用 'unknown'——那时它是唯一的键空间,劈不了任何东西。

修完之后同样的合并给出 2955 个作品 / 2955 条标记,修订数一条不变—— 所以那多出来的 5 条从头到尾就是个假象。

但认领只用来合并,不用来放行广播。 抽广播的时候要拿每条帖子的作者编号和档案主人对一遍, 答错了就是把第三方的内容写进别人的档案。 而「两条记录已经共享同一个作品编号」这件事,对「这条帖子是谁发的」一个字都没说。 授予要严,证伪可松——所以那些档案的广播照旧被拒。

那条告警的第一版,永远不可能响

「认领了哪几份」这条告警的第一版是这么写的:名单在用到账号的时候顺便填, 而告警在活儿开跑之前就推出去了。

也就是说它只可能报零。

一条永远不会响的告警比没有更糟——它看起来像有人在看着。

改成开工前就从档案清单里直接算出来。这个形状在这个项目里反复出现, 最近一次是一道永远为真的守卫, 而两次都是靠把它改坏、看有没有测试变红发现的。

← 更新:页脚写着「零外部请求」,而那一版有 15 页在联网 更早:校验器一直都在,只是没人跑 →