From 4d24edd94c219d8a66b0611effdc37d86ab83991 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 3 Oct 2026 20:46:40 +0800 Subject: [PATCH] =?UTF-8?q?fix(acquisition):=20=E9=9B=86=E6=95=B0=E5=BE=85?= =?UTF-8?q?=E7=A1=AE=E8=AE=A4=E7=9A=84=E8=B5=84=E6=BA=90=E5=9C=A8=E8=AE=A2?= =?UTF-8?q?=E9=98=85=E4=BE=A7=E5=86=99=E5=80=99=E9=80=89=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 评估跳过 episode_unresolved 时,给仍在追踪的收录版本各写一条 skipped 候选评估 (原因 episode_unresolved),作品「为什么选 / 不选」与订阅记录能看到"集数待确认"。 确认后重发评估按 (收录版本, 资源) 覆盖;换算到别的季不再进入原收录版本时, 先清掉旧的待确认记录,不留陈旧原因。 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei --- .../services/acquisition.ts | 40 ++++++++++++++++- .../episode-resolution-evaluate.test.ts | 43 +++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/apps/server/src/modules/acquisition-organize/services/acquisition.ts b/apps/server/src/modules/acquisition-organize/services/acquisition.ts index c8b1929b..2fbb2c5e 100644 --- a/apps/server/src/modules/acquisition-organize/services/acquisition.ts +++ b/apps/server/src/modules/acquisition-organize/services/acquisition.ts @@ -34,6 +34,7 @@ import { hashJson, jsonb, startOfDay, toApi } from "../util.ts"; import { type AoDeps, automationBlock, + CANDIDATE_ARTIFACT_TITLE, downloaderPathOf, type Exec, enqueueTracked, @@ -364,7 +365,13 @@ async function evaluateRelease(c: Ctx, rel: SourceReleaseRow, eventUnits: readon if (rel.availability !== "active") return skip(`release_${rel.availability}`); const entity = await resolveEntity(db, rel.media_entity_id); if (!entity || entity.status === "archived") return skip("entity_unavailable"); - if (entity.kind !== "movie" && episodeUnresolved(rel)) return skip("episode_unresolved"); + if (entity.kind !== "movie" && episodeUnresolved(rel)) { + // 订阅侧可见:相关收录版本各记一条候选评估;确认后重发 release.resolved 正常评估时按 (收录版本, 资源) 覆盖 + await noteEpisodeUnresolved(c, rel, entity.id, onlySlotId); + return skip("episode_unresolved"); + } + // 已确认的资源:先清掉之前的"集数待确认"记录(换算到别的季时下面可能不再进入该收录版本,不会被覆盖) + if (entity.kind !== "movie" && episodeResolutionOf(rel)) await clearEpisodeUnresolvedNotes(c, rel.id, entity.id); const parsed = parsedOf(rel); const units = await releaseUnits( db, @@ -523,6 +530,37 @@ async function evaluateRelease(c: Ctx, rel: SourceReleaseRow, eventUnits: readon } } +/** 集数待确认:给仍在追踪的收录版本各写一条 skipped 候选评估(原因 episode_unresolved) */ +async function noteEpisodeUnresolved(c: Ctx, rel: SourceReleaseRow, entityId: string, onlySlotId?: string) { + let q = c.d.ctx.db + .selectFrom("acquisition_slot as s") + .leftJoin("subscription as sub", "sub.id", "s.subscription_id") + .select("s.id") + .where("s.media_entity_id", "=", entityId) + .where((eb) => eb.or([eb("s.subscription_id", "is", null), eb("sub.status", "in", [...TRACKING_SUB_STATUSES])])); + if (onlySlotId) q = q.where("s.id", "=", onlySlotId); + for (const s of await q.execute()) + await note(c, { + slotId: s.id, + sourceReleaseId: rel.id, + title: rel.title, + planId: null, + decision: "skipped", + mode: null, + rejected: [{ code: "episode_unresolved", message: "集数待确认,确认后自动重新评估" }], + reasons: ["episode_unresolved"], + }); +} + +async function clearEpisodeUnresolvedNotes(c: Ctx, sourceReleaseId: string, entityId: string) { + await sql` + delete from artifact + where title = ${CANDIDATE_ARTIFACT_TITLE} and subject_type = 'acquisition_slot' + and subject_id in (select id from acquisition_slot where media_entity_id = ${entityId}) + and content->>'sourceReleaseId' = ${sourceReleaseId} + and content->'reasons' ? 'episode_unresolved'`.execute(c.d.ctx.db); +} + /** 站点字段 → 评估上下文(做种数、促销下载系数、副标题/标签、站点 id、发布后分钟数) */ export function contextOf(rel: SourceReleaseRow, now: Date = new Date()): EvaluationContext { const promo = (rel.promotion ?? {}) as { kind?: string; raw?: { downloadFactor?: number | null } }; diff --git a/apps/server/test/acquisition-organize/episode-resolution-evaluate.test.ts b/apps/server/test/acquisition-organize/episode-resolution-evaluate.test.ts index 85c65d47..392a6c09 100644 --- a/apps/server/test/acquisition-organize/episode-resolution-evaluate.test.ts +++ b/apps/server/test/acquisition-organize/episode-resolution-evaluate.test.ts @@ -156,4 +156,47 @@ describe.skipIf(!hasPg)("订阅评估跳过集数待确认的资源", () => { expect(ev.plans).toHaveLength(0); expect(ev.skipped).toEqual([{ sourceReleaseId: id, reason: "episode_unresolved" }]); }); + + it("集数待确认在订阅侧可见:写候选记录 episode_unresolved;确认后正常评估并覆盖", async () => { + const slot = await h.db + .selectFrom("acquisition_slot") + .select("id") + .where("media_entity_id", "=", entityId) + .executeTakeFirstOrThrow(); + const candidate = async (releaseId: string) => + ( + await h.db + .selectFrom("artifact") + .select("content") + .where("title", "=", "candidate_evaluation") + .where("subject_id", "=", slot.id) + .execute() + ) + .map( + (r) => r.content as { sourceReleaseId: string; decision: string; reasons: string[]; planId: string | null }, + ) + .find((c) => c.sourceReleaseId === releaseId); + const id = await seed(LILITH_40); + await evaluateAcquisition(h.d, { sourceReleaseId: id }); + expect(await candidate(id)).toMatchObject({ decision: "skipped", reasons: ["episode_unresolved"] }); + + // 人工确认为 S02E12(与 `- 24` 的换算结果相同),重发评估:正常建计划,记录被覆盖 + const fixed = resolved("[Lilith-Raws] Kusuriya no Hitorigoto - 24 [Baha][WEB-DL][1080p][AVC AAC][CHT][MP4]"); + const cols = toParsedColumns(fixed.parsed); + await h.db + .updateTable("source_release") + .set({ + parsed: JSON.stringify(cols.parsed), + parser_version: cols.parserVersion, + episode_resolution: JSON.stringify(fixed.resolution), + }) + .where("id", "=", id) + .execute(); + const ev = await evaluateAcquisition(h.d, { sourceReleaseId: id }); + expect(ev.plans, JSON.stringify(ev)).toHaveLength(1); + const c = await candidate(id); + expect(c?.decision).toBe("accept"); + expect(c?.planId).toBe(ev.plans[0]); + expect(c?.reasons).not.toContain("episode_unresolved"); + }); });