Last updated on

在 fnOS NAS 上搭一套动漫智能推荐 + 自动下载系统


上一篇写了给 NAS 造的夸克自动追番流水线:发个链接就自动下载入库。这篇是它的上层——让 NAS 根据我的观看数据主动推荐,我回一个编号就把整部番下好、刮削好、配好字幕入库。

效果如下,主动触发:

image.png

被动触发:

image.png

系统现状

当前的能力边界:

  • 每周五定时推 6 部候选(异世界 / 恋爱 / 福利各 2,带海报拼图与推荐理由);

  • “看完触发”:检测到某部全集已看完且已完结,立即推 3 部相似;

  • 我回编号(如 1 2 6),后台执行转存 → aria2c 下载 → TMDB 刮削改名 → 配简体外挂字幕 → 入库,每个阶段单独推送进度;

  • 片名直接下;说类型(“推荐点修仙的”)按类型筛;说**“给 X 加字幕”**给已入库的番补外挂字幕;

  • 下载时自动选版本(清晰度 / 体积 / 字幕可用性),生肉自动配简体外挂字幕,同分享内的剧场版一并下到电影库;

  • 完结番下完即从追更列表删除,连载番自动设截止日期到完结停。

落地形态是 fnOS NAS 上的一组 Docker 容器 + cron 脚本 + 一个常驻的 LLM agent(openclaw,负责微信/QQ 收发与技能调度),最终入 Jellyfin,用 Infuse 观看。

一、整体架构与运行时分工

系统数据流 · 五段闭环 ① 口味画像 Jellyfin 完成度 → AniList 标签加权向量 ② 推荐引擎 向量粗筛 → LLM 精判 → 出 Top(货架 2273 部) ③ 审批闸门 微信/QQ 推海报+编号 → 我回编号/报片名/说类型 ④ 下载流水线(autodl) 探测→选版→转存→aria2c→TMDB 改名 → 配简体字幕 / 下剧场版 / 完结判定 ⑤ 媒体库 → Jellyfin / Infuse 剧名/Season NN/剧名 - SxxExx - 集名.mkv + .zh.ass 看完触发 → 推相似(闭环)
图 1 · 五段闭环,看完一部回流到下一轮推荐

架构上有两条原则贯穿始终。

第一,人是闸门。 推荐由系统做,但”下不下”默认等我点头。只有当契合度高过一个保守阈值,才允许先下载、后告知;同时每周自动下载的数量和体积有上限,防止把盘塞爆。

第二,按成本分工运行时。 这套东西如果全用大模型跑会很贵,所以拆成两层:

  • 便宜的定时脚本(cron)干确定性的苦力:拉 Jellyfin 数据、算口味向量、解析目录、向量粗筛、转存、下载、刮削、推送。这些是数据搬运和数学,不需要智能。

  • 大模型(NAS 上常驻的 openclaw agent)只在三个点被调用:① 粗筛后对约 50 个候选做精判并写一句推荐理由;② 解析我发的自然语言意图;③ 对话审批。

具体组件:转存用 Docker 里的 quark-auto-save;下载编排是我自己的 quark_sync.py(每小时 cron);上层编排器 quark_ctl.py 暴露 probe / tmdb / addtask / sync / autodl 等子命令,由 agent 以 sudo 调用;推荐运行时是 anime_reco.py;媒体落在 mergerfs 合并卷上(动漫 / 剧集 / 电影 / 动漫电影,写入策略 mfs 自动落最空的盘)。

二、口味画像:用观看数据建模

推荐的前提是回答一个问题:凭什么判断我会喜欢某部番? 不靠猜,靠我自己的观看记录。

信号来源与权重

数据来自 Jellyfin API:每部剧的 UserDataIsPlayed、未看集数、LastPlayedDate)给出”看没看完、看到哪、最近在看啥”。我按”投入度”给每部样本一个权重,再把它的标签按权重累加成一个向量:

口味向量:投入度加权累加标签 Jellyfin UserData 看完 + 近期 在追 弃番 主力用完成度, 时长信号见下文坑 ×1.0 ×0.8 −0.3 每部 → AniList 标签 Isekai / Romance Ecchi / Magic … 按权重累加 口味画像 归一化向量 + 人话总结 两簇:异世界爽 / 青春恋爱
图 2 · 看完的算正权,弃的算负权
def build_taste_profile(jellyfin):
    vector = defaultdict(float)
    for show in jellyfin.watched_shows():        # 拉 UserData
        weight = (1.0 if show.finished and show.recent
                  else 0.8 if show.in_progress
                  else -0.3 if show.dropped
                  else 0.0)
        for tag in anilist_tags(show):           # 该剧的 AniList 标签
            vector[tag] += weight
    return normalize(vector), summarize_in_words(vector)

实读我的 32 部样本(16 看完 + 16 在追),跑出来两簇势均力敌:一簇是异世界·奇幻爽番(标签 Isekai / Magic / Super Power / Swordplay / Dungeon / Male Protagonist / Female Harem),一簇是青春恋爱·治愈日常Romance / School / Slice of Life / Iyashikei / Kuudere / Cohabitation)。

校准规则

向量是统计,但有些偏好统计不出来,需要我口述成规则、写进打分逻辑。这几条直接影响后面 recommend.py 的权重:

  1. 感情线是核心吸引力——我能为男女主的感情忍受致郁剧情(Re:Zero 是唯一例外,不据此推致郁番)。
  2. 拒绝降智——“无脑爽”不要,要”有质量的爽”,用评分门槛卡掉低分番。
  3. 重口/致郁负权Tragedy / Gore / Horror / Survival / Boys' Love / Yuri / Male Harem 全部减分。
  4. 福利 ≠ 重口Ecchi / Harem / Fanservice + 爽向复仇是正权。
  5. 男主视角 + 异性恋强偏好Male Protagonist +1Female Protagonist −0.8Heterosexual +

三、目录摄取:把”我有哪些番”喂进去

口味是”我喜欢什么类型”,还需要一个货架——来自这位大佬的无私分享:https://www.kdocs.cn/l/cnOBXUamMBJJ

为什么没法直接爬

这一步比预期折腾。该文档是 .otl(在线表格的私有协作格式),我依次试过:

  • 抓 Cookie 调 API:补齐登录态(wps_sid / kso_sid / uid)后,GET 文件元信息 能通,能读到文件名、ID、大小、作者;但 .otl 没有简单导出接口——/download 返回 unSupport (403),各 export 端点全 404。它是私有协作格式,数据不内联在分享页 HTML 里。

  • 逆向渲染协议:weboffice 的渲染走私有协议,前端 preload-o.js 动态构造请求,逆向成本太高,且我对这份文档只有只读权、没有导出/编辑权。

目录摄取:三条路两条死 在线表格 .otl 第三方只读分享 A 抓 Cookie 调 API 导出端点 403/404 ✗ B 逆向渲染协议 私有协议,无导出权 ✗ C 另存为 Word 解析 docx ✓ catalog 2273 部,0 空名 名 + 网盘链接
图 3 · 逆向不值当时,换个数据出口

解法:另存为 docx 后解析

最后走的是最朴素的一条:在浏览器里把表格另存为 .docx,再解析。.docx 本质是个 zip,里面是 OOXML。解析时踩了一个点:

坑|超链接藏在域代码里。 Word 把单元格里的超链接存成 <w:instrText>HYPERLINK "..."</w:instrText> 域代码,而不是可见文字。如果只读单元格的可见文本(w:t 节点),会只拿到番名、丢掉链接。正确做法是把整个单元格序列化成 XML(ET.tostring(tc))再正则搜 HYPERLINK

def parse_catalog(docx):
    rows = []
    for cell in docx.all_table_cells():
        name = cell.visible_text()                  # w:t 拼接
        link = extract_hyperlink(cell.raw_xml())    # 搜整段 XML 的 HYPERLINK 域代码
        if name and link:
            rows.append({"name": name, "quark": link})
    return rows

解析出 2273 部,0 空名,抽验正确。目录以后更新就是重复”另存 docx → 跑解析器”,再 diff 出新增、只富化增量。

四、元数据富化:把每部番打上标签(四轮)

货架有了名字和链接,但推荐需要”每部是什么”——类型、标签、口碑评分。中文名直接搜动漫数据库命中率很差,这一步打了四轮,是整个项目里最反复的部分。

四轮富化:从 96% 推到 100% 2273 部,只有中文名 起点 ① TMDB 中文匹配 命中 96%,拿到日文原名 / 评分 / 类型 / 海报 ② AniList 用日文原名补细标签 命中 ~55%(中文搜 AniList ≈ 0%)→ 剩 873 部 ③ 清洗原名重查 AniList hit 681 / miss 192(白捡 681 部)→ 剩 192 ④ 6 个 LLM Agent 并行查站归类 192 部 0 漏配 → 2273/2273 全归类 96% +55% +681 +192
图 4 · 越往后命中越难,最后用 LLM 兜底

关键工程细节

  • 匹配策略:中文名搜 AniList 命中率几乎为零(连《葬送的芙莉莲》都搜不到)。所以先用 TMDB 做中文匹配(命中 96%),从 TMDB 拿到日文原名,再用日文原名去查 AniList 的细标签。

  • Cloudflare:AniList / TMDB 的请求头 User-Agent 必须是纯浏览器 UA。带 my-script/1.0 这类脚本标识会被 Cloudflare 拦成 404。AniList 的 GraphQL 端点还要求 URL 带尾斜杠。

  • GraphQL 的坑:AniList 批量查询里只要有一个 null,会返回 HTTP 404 但 body 仍带 data——得捕获 404 后照样读 body,否则整批丢弃。

  • 网络位置api.themoviedb.org 在国内直连被墙,TMDB 请求必须在 NAS 上发(NAS 挂了代理);AniList 反而本机能直连。两边分开跑。

  • 节流:第三轮对 873 部批量重查,time.sleep(1.3) 限速,遇到 429 退避 60 秒,每 40 条原子落盘,非原名匹配加 ±3 年的年份校验防误配。

第三轮的动机来自一个判断:“没识别到的番不可能没收录,多半是匹配姿势不对。” 于是把日文原名做清洗(去副标题、括号、季数)重查,免费白捡回 681 部。

第四轮:LLM 并行归类

剩 192 部是硬骨头:2025 新番尚未收录、复杂续季名(“第四季 丧失篇”)、剧场版分章、别名一堆(“蓝箱 / 青春之箱 / 青之箱”)、国创原名是中文。这批用规则匹配不动了,改用 LLM:开 6 个 Agent 并行,每个负责 32 部,读各自的清单文件,用知识 + 实时查站(MAL / AniList / Bangumi / 维基 / 萌娘),产出固定 schema——al_genres(AniList 类型)、al_tags(贴推荐打分词表的英文标签)、al_score(0–100),再合并写回。

192 部:6 个 Agent 并行,固定 schema 输出 192 部 切 6 批 Agent 1 · 32部 Agent 2 · 32部 Agent 3 · 32部 Agent 4 · 32部 Agent 5 · 32部 Agent 6 · 32部 合并 → 按名写回 192 部 0 漏配 / 177 高置信
图 5 · 并行 + 固定输出 schema,方便程序化合并

结果:192 部 0 漏配、177 部高置信,catalog 100% 归类。 抽查准确(幼女战记 → Military/Isekai 81、电锯人 → Gore/Tragedy 83、双城之战 → 89)。

坑|异体字导致漏配。 LLM 把番名逐字写回时,有一条把”亚刻特曼”(异体”奧”)写成了”亚刻特曼”(简体”奥”),按名匹配失败。所以写回数据库前加了一步校验:拿结果的 name 和原清单求交集,列出对不上的(最终只有这 1 条),修正后再写。任何”按名 join”的环节都要先验证主键。

五、推荐引擎

两段式:先用口味向量对全量打分粗筛(几千 → 约 50),再过质量门槛、分三簇出 Top。打分函数大致是:

def score(show, taste):
    s = 0
    for tag in show.al_tags:
        s += taste.weight.get(tag, 0)              # 口味标签加权
    s += GENRE_BONUS(show.al_genres)               # 类型偏好(异世界/恋爱/福利)
    if show.al_score: s += (show.al_score - 70)*0.1  # 口碑微调
    if show.year < 2010: s -= 3                    # 老番降权
    elif show.year < 2015: s -= 1
    if is_sequel_without_prequel(show): return SKIP  # 没看前作的续集不推
    return s

去重与状态管理(anime_reco_state.json):

  • pushed = {番名: 日期}——30 天内不重推,过期可再推;

  • blocked——黑名单永久不推(我说”别再推 X”时执行 anime_reco.py block "X");

  • owned——NAS 库里已有的(不管看没看)一律不推;

  • finished_seen——看完基线,用于”看完触发”判重。

两条触发腿:每周五定时推一次;看完触发是高频轮询 Jellyfin,检测”某部刚从未看完变成全看完, TMDB 状态为 Ended/Canceled”——连载追平最新集不算(还有更新)。

推送做了工程优化:6 部合成一段文字 + 6 张海报用 PIL 拼成一张 2 列长图,每张叠黄色编号对应文字。这是为了绕开两件事:微信连发多图会限流;QQ 偶尔丢单张图。海报由 NAS curlimage.tmdb.org 到本地再发(实测远程 URL 直接喂发送脚本会 fetch failed,必须先落地)。

坑|PIL 的 import。 Pillow 装在 agent 用户的 ~/.local,cron 跑的时候必须带 env HOME=...,否则 import 不到。微信掉线则是另一回事:发送返回 iLink ret=-2 = 会话失效,得我主动给机器人发条消息激活,代码层改不了;所以各推送渠道独立 try,微信挂不影响 QQ。

六、下载流水线:从编号到入库

回一个编号后,autodl 在后台逐步推进。夸克这部分的 API 细节是整条线最容易出错的地方。

下载流水线 · 异步后台 + 阶段通知 autodl 编排器 我(微信/QQ) ① probe 探测分享:几季/字幕组/集数/密码 ② 选最优版本(清晰 > 大文件 > 字幕) ③ addtask + 转存(校验+重试 10×20s) ④ 取直链 + aria2c -x16 下载 ⑤ TMDB 刮削,SxxExx-集名 改名(重试3) ⑥ 配简体外挂字幕 ⑦ 剧场版下到电影库 + 完结判定 "收到,后台下载中"(秒回) "✅ 已转存,开始入库" "📝字幕 / 🎬剧场版 / ✅完成"
图 6 · 每个里程碑单独推一条,长任务不变黑盒

夸克 API 的三个关键点

转存和下载都调夸克的 web API(封装在容器内的 qsync_api.py,因为容器才挂着夸克账号的 cookie):

  1. 转存子文件夹:分享的子目录要用 <分享链接>#/list/share/<folder_fid>-x 这种 URL 形式定位,再 save_file 进自己账号。
  2. token 会话绑定(41020)save_file(fid_list, fid_token_list, ...) 里的 share_fid_token 必须和取它的那次 get_stoken 来自同一个 stoken 会话,否则报 41020 token 校验异常。所以”取 token”和”save”要在同一段脚本/会话里完成,不能跨调用。
  3. 下载直链的 403acc.download([fid]) 返回 (响应, dlcookie) 两段。喂给 aria2c 时,Cookie 头必须是账号 cookie + ”; ” + 这个 per-file dlcookie,少了后半段就 403。
# 转存后不要立刻信"成功"——校验该季账号里确实有文件,防竞态漏季
ok = False
for _ in range(10):                       # 重试 10 次,每次间隔 20s
    run_quark_auto_save()                 # 触发转存
    if account_season_filecount(savepath) > 0:
        ok = True; break
    sleep(20)
if not ok:
    push("❌ 转存失败,交给整点 cron 重试"); return
push("✅ 已转存,开始下载入库…")
run_with_flock(SYNC)                       # 下载/解压/刮削/推送

这个”转存后校验 + 重试”是被一个真实 bug 逼出来的:多季番一次性提交转存会竞态漏季(某部 4 季里 S4 没转上),改成逐季转存、每季校验账号里确有文件再继续。

异步与并发

最初流水线是同步的——回编号后在对话里一直跑到下载完才回,但一部番几十分钟,对话会超时、中途没反馈。改成异步:

def on_approve(nums):
    reply("收到,后台下载中…")            # 立即回
    spawn_detached(f"autodl {nums}")      # setsid 脱离当前进程,后台跑
    end_turn()                            # 结束这轮对话

坑|并发。autodl 和每小时整点的 cron 都会触发下载,没加锁时两个 aria2c 撞一起,对同一文件 -x1 反复重下卡了两小时。加 flock /tmp/quark_sync.lock 解决——但不能双重加锁:外层 flock 包一次、内层 sync 又包同一把锁,会自己等自己、死锁。

坑|TMDB 刮削偶发失败要重试。 改名偶尔撞上 TMDB 的 SSL EOF,导致集名没补上、只剩 SxxExx。加了”刮削失败重试,上限 3 次、间隔 5 秒”,再没出现裸集号。

七、版本选择

同一部番,分享里常并存多个字幕组/画质版本。选择规则是我口述、AI 落地成打分函数的。

选版本:先过滤,再四级排序 多个版本 不同字幕组/画质 过滤掉 只内嵌繁体 / 非中文 且不可换字幕的 剩下按优先级排序 ① 覆盖完整(集数,防分卷缺集) ② 清晰优先(10bit / 超分 / 4K / HEVC) ③ 同清晰选文件大的 ④ 字幕可用性
图 7 · 覆盖完整是底线,清晰和体积决定优劣
def select_version(folders):
    ok = [f for f in folders if sub_ok(f) > 0]   # 滤掉只内嵌繁体/非中文且不可换的
    pool = ok or folders                         # 全是差字幕才退而求其次
    return max(pool, key=lambda f: (
        len(f.episodes),     # ① 覆盖完整(底线): 防选到只有后半季的分卷
        quality_score(f),    # ② 清晰: 关键词 2160/4K > 1080 > 720, +10bit/超分/HEVC
        f.total_size,        # ③ 同清晰选大文件: 1G 系列优先于 512M 系列
        sub_ok(f)))          # ④ 字幕可用性

quality_score 从文件夹名和样本文件名里抓关键词(2160/4K10bit/Hi10pHEVC/x265FLAC);sub_ok 判断字幕可换性——含”简体/外挂/内封”为好,“只内嵌繁体或非中文”为差(不可换,直接过滤)。覆盖完整放在第一位,是因为吃过亏:早期版本按”最大集号”选,结果对一部分卷番选中了只有 14–26 集的后半季,漏了前半。

八、压缩包与解压密码

部分番(尤其里番)在分享里不是裸视频,而是重命名成 .exe 的压缩包(不是真自解压,7z 按文件头识别后缀无所谓),并带解压密码。密码的位置不固定:文件夹名里、同目录某个 txt 里、或就是分享链接/番名本身。

def archive_passwords(name, folder):
    cands  = decoy_passwords_in_folder_name(folder)   # 文件夹名里写的诱饵密码
    cands += read_password_txt(folder)                # 密码.txt
    cands += [name, share_link_id]                     # 番名 / 链接 id
    cands += [p.split("@")[0] for p in cands if "@" in p]  # 变体: ruach@66 → ruach
    return dedup(cands)

def extract(archive, cands):
    for pw in cands:
        if seven_zip(archive, pw):   # 7z x -y -p<pw>,按文件头识别真实格式
            remember_good_password(pw); return True
    return False

解压后按集号改名入库时,要先用一组 SPECIAL_RENCED / NCOP / Menu / SP / Secret Video 这类非正片过滤掉,再按 SxxExx 命名。

坑|诱饵密码。 有个密码标的是 ruach@66,逐个试全部”密码错误”。实际真密码是 ruach@66 是诱饵后缀。于是加了”密码变体”:对 x@y 形式,把 @ 前的部分也作为候选。多密码逐个试、并记住第一个成功的,后续同名压缩包直接用。

九、外挂字幕与剧场版

一个后期补的缺口:高画质 BDRip(如 VCB-Studio)的视频常是无字幕生肉,字幕在单独的”外挂字幕”子文件夹里。早期只下视频、没下字幕,等于下了个看不了的版本。另外同一分享里常还塞着剧场版,也漏了。

这块做成 autodl 的两个后置步骤,且都包了 try/except——失败只跳过,不影响主下载。容器侧加了两个 action:subsfor(在视频文件夹及子目录里找简体字幕、save_file 到临时目录、返回直链)、movieget(剧场版文件夹的主影片 + 简体字幕直链)。宿主机侧下载并归位。

字幕命名有个硬约束:Jellyfin 要求外挂字幕与视频完整同名(含刮削加上的集名),只差后缀和语言标记。所以字幕必须在视频改名之后处理——按集号配对库里已改好名的视频,取那个视频的完整 stem 加 .zh.ass

def fetch_subs(folder, season):
    subs = pick_simplified_subs(folder)             # .sc/.简/chs 优先, .tc/繁次之
    for s in subs:
        video = find_lib_video(season, s.episode)   # 按 SxxExx 找已改名的视频
        if not video or has_any_sub(video):         # 已有字幕(含原版裸.ass)就跳过
            continue
        place(s, dest=f"{video.stem}.zh.ass")        # 完整同名 + .zh, Jellyfin 才认

剧场版判定走正则,但刻意不匹配裸 ZERO/movie——否则会把《Re:Zero》之类 TV 正片误判成剧场版下进电影库。只认”剧场版 / 劇場版 / 劇場 / the movie / gekijou”这类明确标记,命中后在候选里按清晰度+体积选主影片(同时排除 SP/Menu/CM/NC),落到 动漫电影/<片名> (年)/

坑|字幕重复。 有部番原下载就自带裸 .ass(无语言标记),补字幕时只检测了 <stem>.zh.ass 是否存在、没检测裸 <stem>.ass,结果整季又加一遍,每集两条字幕。修法是 has_any_sub() 检测任意形式的同名字幕(裸 .ass / .srt / 带标记 .zh.ass)就跳过。原则:原下载自带、跟视频同包的字幕时轴必对,优先于外部字幕组的。

十、完结判定与任务回收

追更列表早期堆了一批早已完结、却永久挂着、没截止日期的任务。补上 TMDB 状态判定后:

def after_download(show, season):
    info = tmdb_status(show.tmdb_id, season)   # status / next_episode_to_air / 各集播出日
    if info.completed:                         # Ended/Canceled 或本季全播完
        delete_task(show)                      # 下完即删,不长期占用
    elif info.airing:
        set_enddate(show, info.finale + buffer)  # 连载: 到完结日+缓冲自动停
    else:
        set_enddate(show, today + 90*DAY)        # 查不到 tmdb: 兜底 90 天,绝不永久挂

completed 的判定不只看 status,还结合本季各集的播出日期:已播集数 == 总集数、且 next_episode_to_air 为空、无未来集,才算完结。这样对”季终但全剧未完结”的情况也能正确处理。存量也清了一遍:6 个完结番的常驻任务,TMDB 确认后全删。

十一、对话交互

最后一层是自然语言入口。agent 解析意图,路由到对应的轻量技能:

输入动作实现
回编号 126下推荐过的那几部last_push 映射拿链接 → autodl
我想看 XXX目录模糊搜 → 下anime_reco.py findautodl --link
推荐点修仙的按类型筛选推荐recco 中文类型 → AniList 标签映射
给 XXX 加字幕已入库的补简体字幕findquark_ctl subs --link
别再推 X加黑名单anime_reco.py block

每个意图对应一个技能描述文件,agent 按描述触发,秒回 + 后台执行。

十二、传输与运行环境的坑

这套东西跨”本机 ↔ NAS ↔ 容器 ↔ agent 用户”四层,环境差异本身就是一类 bug 源:

  • CJK 经 SSH/heredoc 会乱码:远程执行带中文的脚本时,中文和反斜杠经 heredoc 会被改写。解决办法是 payload 全程走 base64:本地 base64 -w0 → 远程 base64 -d 落地再执行,中文/特殊字符零损耗。

  • 宿主机 python3 对 agent 用户是 “Permission denied”:直接 python3 /tmp/x.py 跑不了,得走 docker exec python3sudo python3 或包成 bash。

  • 容器看不到宿主机大部分路径:转存容器只挂了自己的 config 目录,看不到宿主机的媒体库;所以”转存”在容器里做、“下载落盘/改名/配字幕”在宿主机做,两边用账号 cookie 衔接。

  • FRP 隧道会抖:NAS 经 frp 中转暴露 SSH,偶发 Connection closed by <relay>。狂重试会触发 fail2ban 越封越久,正确做法是缓一下再连。

十三、协作模式与现状

这套系统有十几个脚本、跨容器、带定时任务、会自己收发消息。它的产生方式是一个固定的循环:

迭代循环:需求 → 落地 → 验证 → 下一个 我(安全背景) 需求 / 架构方向 / 优先级 判断 + 验证 + 纠偏 AI(Claude Code) 探查 / 设计 / 编码 / 部署 / 自测 逆向 / 调度 / 修 bug 需求 / 纠偏 实现 / 结果
图 8 · 分工:我定方向与验收,AI 负责实现

值得一提的是几个判断本身的价值——它们是统计和模型给不出、需要人来定的:把”覆盖完整”放在选版第一位、“没识别到的不可能没收录”催生第三轮重查、“完结的别永久挂着”触发任务回收、“内嵌繁体不可换的直接跳过”。这些是需求,不是代码。

工程上每一步都带验证:部署后抽查、批处理后报数核对、出错回滚备份。涉及外部写入(写库、改媒体库、发布)前,都先和原始数据核对主键、留时间戳备份。

现状与待办

  • catalog:2273 部全部归类,推荐池满载;

  • 下载:异步、逐季、转存校验重试、完结判定、外挂字幕 + 剧场版、版本选择规则全部就位;

  • 对话:回编号 / 报片名 / 说类型 / 加字幕 / 拉黑都能走。

待办:把”外挂字幕 + 剧场版”这套拿一部全新番做一次端到端实跑验证;清掉目录里混进来的少量真人影视;给推荐加”探索位”防信息茧房。