一个目录里十份档案,只读了一份
下载文件夹里平铺着十份档案的文件。扫描器取了第一个匹配的清单,剩下九份、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 条从头到尾就是个假象。
但认领只用来合并,不用来放行广播。 抽广播的时候要拿每条帖子的作者编号和档案主人对一遍, 答错了就是把第三方的内容写进别人的档案。 而「两条记录已经共享同一个作品编号」这件事,对「这条帖子是谁发的」一个字都没说。 授予要严,证伪可松——所以那些档案的广播照旧被拒。
那条告警的第一版,永远不可能响
「认领了哪几份」这条告警的第一版是这么写的:名单在用到账号的时候顺便填, 而告警在活儿开跑之前就推出去了。
也就是说它只可能报零。
一条永远不会响的告警比没有更糟——它看起来像有人在看着。
改成开工前就从档案清单里直接算出来。这个形状在这个项目里反复出现, 最近一次是一道永远为真的守卫, 而两次都是靠把它改坏、看有没有测试变红发现的。