fix(rules): recompute current asset pri under referenced rule groups for upgrades

Slot units and movie assets recorded pri from slot soft.tiers only, so a rule
group ladder would either never upgrade (pri 100) or re-upgrade forever. The
current pri is now re-evaluated from the asset's source release with the same
chain (fallback: pri recorded in the plan evaluation).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-28 15:11:43 +08:00
1 parent d28686ed02
commit 36027a4820
2 files changed
+118 -3

No files matched your search

@@ -12,6 +12,7 @@ import {
checkBudget,
computeUnitGaps,
determineMode,
effectiveLadder,
type EvaluationContext,
episodeKey,
evaluateCandidate,
@@ -20,7 +21,9 @@ import {
parseRelease,
priorityOf,
type QualityFacts,
ruleChainOf,
type SlotEvaluation,
type SlotSpec,
type SlotState,
seasonWildcard,
transition,
@@ -228,6 +231,8 @@ interface Ctx {
rules?: Promise<RuleCatalog>;
/** 作品原语言缓存(规则组 CNSUB/CNVOI 放行) */
langs?: Map<string, Promise<string | undefined>>;
/** 现有资产在规则组下的 pri 缓存 */
assetPri?: Map<string, number | undefined>;
}
async function note(c: Ctx, base: Omit<CandidateEvaluation, "evaluatedAt">) {
@@ -574,6 +579,10 @@ async function evaluateForSlot(c: Ctx, p: SlotInput): Promise<{ planId: string |
if (isMovie) {
if (inflight.length > 0 || BUSY.includes(slot.status)) return defer("slot_busy", []);
const current = await currentFacts(db, slot.current_asset_id);
if (current && slot.current_asset_id && rulesLaddered(spec)) {
const pri = await rulePriOfAsset(c, spec, slot.current_asset_id, ctx.originalLanguage);
if (pri !== undefined) current.pri = pri;
}
ev = evaluateCandidate(spec, parsed, slotSatisfied ? current : null, ctx);
} else {
const cov = await slotCoveredUnits(db, slot, { entityWide: p.slotCount <= 1 });
@@ -623,7 +632,12 @@ async function evaluateForSlot(c: Ctx, p: SlotInput): Promise<{ planId: string |
return defer("units_inflight", reservedHit);
} else {
// 都已覆盖:分集洗版,只取能提升的单元(对照 MP get_best_version_interested_episodes)
const unitCurrent = await unitQuality(db, slot, releaseKeys);
const unitCurrent = await unitQuality(
db,
slot,
releaseKeys,
rulesLaddered(spec) ? (assetId) => rulePriOfAsset(c, spec, assetId, ctx.originalLanguage) : undefined,
);
const withCurrent = releaseKeys.filter((k) => unitCurrent[k]);
const firstCurrent = withCurrent.length
? unitCurrent[withCurrent[0]!]!
@@ -743,11 +757,13 @@ async function unitQuality(
exec: Exec,
slot: AcquisitionSlotRow,
keys: readonly string[],
/** 版本目标引用了带层的规则组时,按规则组重算该单元现有资产的 pri(覆盖入库时按 soft.tiers 记下的值) */
priOf?: (assetId: string) => Promise<number | undefined>,
): Promise<Record<string, QualityFacts | undefined>> {
const rows = await exec
.selectFrom("slot_unit as su")
.innerJoin("content_unit as u", "u.id", "su.content_unit_id")
.select(["u.season_number", "u.episode_number", "su.quality", "su.pri"])
.select(["u.season_number", "u.episode_number", "su.quality", "su.pri", "su.media_asset_id"])
.where("su.slot_id", "=", slot.id)
.where("su.media_asset_id", "is not", null)
.execute();
@@ -757,11 +773,50 @@ async function unitQuality(
if (r.episode_number === null) continue;
const k = episodeKey(r.season_number ?? 1, r.episode_number);
if (!want.has(k)) continue;
out[k] = { ...((r.quality ?? {}) as QualityFacts), ...(r.pri !== null ? { pri: r.pri } : {}) };
const pri = priOf && r.media_asset_id ? ((await priOf(r.media_asset_id)) ?? r.pri) : r.pri;
out[k] = { ...((r.quality ?? {}) as QualityFacts), ...(pri !== null ? { pri } : {}) };
}
return out;
}
const rulesLaddered = (spec: SlotSpec) => !!spec.rules?.groups.some((g) => g.layers.length > 0);
/**
* 现有资产在规则组下的 pri:有来源资源 → 按当前规则组对其标题/副标题/站点事实重算(同 MP 对种子求 pri_order);
* 否则取下载计划评估时记下的 pri。一次评估内按资产缓存。
*/
async function rulePriOfAsset(c: Ctx, spec: SlotSpec, assetId: string, lang?: string): Promise<number | undefined> {
c.assetPri ??= new Map();
const key = `${spec.id ?? ""}:${assetId}`;
if (c.assetPri.has(key)) return c.assetPri.get(key);
const db = c.d.ctx.db;
let pri: number | undefined;
const a = await db
.selectFrom("media_asset")
.select(["source_release_id", "download_task_id"])
.where("id", "=", assetId)
.executeTakeFirst();
const rel = a?.source_release_id
? await db.selectFrom("source_release").selectAll().where("id", "=", a.source_release_id).executeTakeFirst()
: undefined;
if (rel) {
const parsed = parsedOf(rel);
const ctx = { ...contextOf(rel, c.now), ...(lang ? { originalLanguage: lang } : {}) };
pri = effectiveLadder(spec, parsed, ctx, ruleChainOf(spec, parsed, ctx)).pri;
} else if (a?.download_task_id) {
const p = await db
.selectFrom("download_task as t")
.innerJoin("acquisition_plan as p", "p.id", "t.acquisition_plan_id")
.select("p.evaluation")
.where("t.id", "=", a.download_task_id)
.executeTakeFirst();
const v = (p?.evaluation as { pri?: unknown } | undefined)?.pri;
if (typeof v === "number") pri = v;
}
c.assetPri.set(key, pri);
return pri;
}
interface GateOk {
ok: true;
instanceId: string;