refactor(core): migrate to Bun and converge acquisition policies and Agent boundaries
This commit is contained in:
1 parent
3f1c6d02f6
commit
27c0f27434
268 files changed
+7468
-7852
No files matched your search
@@ -1,6 +1,8 @@
|
||||
import type { ParsedRelease, ReleaseSource, Resolution } from "../release-parser/types.ts";
|
||||
import { catalogOf, evaluateRuleChain } from "../rules/evaluate.ts";
|
||||
import type { RuleChainResult } from "../rules/types.ts";
|
||||
import { unknownRuleFields } from "../rules/unknown.ts";
|
||||
import { seasonReleaseKey, upgradeSpec } from "./season-release.ts";
|
||||
import type {
|
||||
EvaluationContext,
|
||||
HardField,
|
||||
@@ -310,6 +312,8 @@ export function describeViolation(v: Violation): string {
|
||||
return `优先级没有提高(现有 ${JSON.stringify(e)},这个资源 ${JSON.stringify(a)})`;
|
||||
case "upgrade.not_full_pack":
|
||||
return "全集洗版只接受整季包";
|
||||
case "upgrade.different_season_release":
|
||||
return "与当季选定的成套版本不同,等待同套资源";
|
||||
case "upgrade.no_improvable_units":
|
||||
return "这个资源包含的季集都没有可升级的";
|
||||
default:
|
||||
@@ -348,6 +352,37 @@ function tierFailures(rule: SlotTierRule, r: ParsedRelease, ctx: EvaluationConte
|
||||
return out;
|
||||
}
|
||||
|
||||
function unknownTierFields(spec: SlotSpec, r: ParsedRelease, ctx: EvaluationContext): HardField[] {
|
||||
for (const tier of spec.soft?.tiers ?? []) {
|
||||
if (!tierFailures(tier, r, ctx).length) break;
|
||||
const missing: HardField[] = [];
|
||||
const filled = { ...r };
|
||||
if (!r.resolution && tier.resolutions?.length) {
|
||||
missing.push("resolution");
|
||||
filled.resolution = tier.resolutions[0];
|
||||
}
|
||||
if (!r.source && (tier.sources?.length || tier.excludeSources?.length || tier.requireEncode)) {
|
||||
missing.push("source");
|
||||
filled.source =
|
||||
tier.sources?.[0] ??
|
||||
(tier.requireEncode
|
||||
? "BluRay"
|
||||
: (Object.keys(DEFAULT_SOURCE_TIERS) as ReleaseSource[]).find((s) => !tier.excludeSources?.includes(s)));
|
||||
filled.isEncode = tier.requireEncode ?? filled.source === "BluRay";
|
||||
}
|
||||
if (!r.videoCodec && tier.codecs?.length) {
|
||||
missing.push("codec");
|
||||
filled.videoCodec = tier.codecs[0];
|
||||
}
|
||||
if (!r.releaseGroup && tier.groups?.length) {
|
||||
missing.push("group");
|
||||
filled.releaseGroup = { name: tier.groups.join("@"), source: "known" };
|
||||
}
|
||||
if (missing.length && !tierFailures(tier, filled, ctx).length) return missing;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 优先级阶梯(对应 MP 规则组 `A > B > C`):逐层匹配,第 k 层(0 起)命中 → pri = 100 - k;
|
||||
* 未配置阶梯 → pri = 100、level = null(不参与排序差异);配置了但都不命中 → pri = 0。
|
||||
@@ -465,9 +500,21 @@ export function evaluateCandidate(
|
||||
current?: QualityFacts | null,
|
||||
ctx: EvaluationContext = {},
|
||||
): SlotEvaluation {
|
||||
if (current) spec = upgradeSpec(spec);
|
||||
const hard = checkHardConditions(spec, r, ctx);
|
||||
const { missing, accepted } = hard;
|
||||
const chain = ruleChainOf(spec, r, ctx);
|
||||
if (hard.violations.length === 0) {
|
||||
const unknown = spec.rules ? unknownRuleFields(spec.rules.groups, r, ctx, catalogOf(spec.rules.atoms)) : [];
|
||||
if (chain?.decidingGroup === undefined || chain.decidingGroup === null)
|
||||
unknown.push(...unknownTierFields(spec, r, ctx));
|
||||
if (unknown.length)
|
||||
return {
|
||||
decision: "unknown_field",
|
||||
missing: [...new Set([...hard.missing, ...unknown])],
|
||||
trace: unknown.map((f) => `组合规则需要补充:${f}`),
|
||||
};
|
||||
}
|
||||
const { missing, accepted } = hard;
|
||||
const withRules = chain ? { rules: chain } : {};
|
||||
const violations = [...hard.violations, ...chainViolations(chain, spec)];
|
||||
const ladder = effectiveLadder(spec, r, ctx, chain);
|
||||
@@ -524,6 +571,53 @@ export function evaluateCandidate(
|
||||
const up = spec.upgrade;
|
||||
if (!up?.enabled) return reject({ field: "upgrade", code: "upgrade_disabled" });
|
||||
if (up.mode === "full_pack" && (r.episodes || r.airDate)) return reject({ field: "upgrade", code: "not_full_pack" });
|
||||
if (r.isEncode && up.encodeGroupAllowlist?.length) {
|
||||
if (!r.releaseGroup) return { decision: "unknown_field", missing: ["group"], trace: ["合格压制组需要查证发布组"] };
|
||||
const groups = r.releaseGroup ? r.releaseGroup.name.split("@").map(lc) : [];
|
||||
if (!up.encodeGroupAllowlist.some((g) => groups.includes(lc(g))))
|
||||
return reject({
|
||||
field: "upgrade",
|
||||
code: "encode_not_qualified",
|
||||
expected: up.encodeGroupAllowlist,
|
||||
actual: r.releaseGroup?.name,
|
||||
});
|
||||
}
|
||||
if (up.consistency === "season_release") {
|
||||
const key = seasonReleaseKey(r);
|
||||
const targetKey = ctx.seasonReleaseTarget ? seasonReleaseKey(ctx.seasonReleaseTarget) : null;
|
||||
if (key && !targetKey)
|
||||
return { decision: "unknown_field", missing: ["current_asset"], trace: ["当季成套目标的规格未知"] };
|
||||
if (!key)
|
||||
return {
|
||||
decision: "unknown_field",
|
||||
missing: [
|
||||
...(!r.resolution ? ["resolution" as const] : []),
|
||||
...(!r.source ? ["source" as const] : []),
|
||||
...(!r.videoCodec ? ["codec" as const] : []),
|
||||
...(!r.releaseGroup?.name ? ["group" as const] : []),
|
||||
],
|
||||
trace: ["成套洗版需要完整版本规格"],
|
||||
};
|
||||
if (key !== targetKey) return reject({ field: "upgrade", code: "different_season_release" });
|
||||
const fromPri = currentPri(spec, current);
|
||||
if (key !== seasonReleaseKey(current) && ladder.pri === fromPri) {
|
||||
const from = qualityTier(current, spec);
|
||||
if (tier.source < from.source || tier.resolution < from.resolution)
|
||||
return reject({ field: "upgrade", code: "lower_tier", expected: from, actual: tier });
|
||||
return {
|
||||
decision: "upgrade",
|
||||
tier,
|
||||
fromTier: from,
|
||||
softRank: rank,
|
||||
reasons: [...reasons, "season_release_aligned", ...priReasons],
|
||||
pri: ladder.pri,
|
||||
fromPri,
|
||||
ladder,
|
||||
trace: ["保持优先级,向当季同套版本对齐", ...ladder.trace],
|
||||
...withRules,
|
||||
};
|
||||
}
|
||||
}
|
||||
const basis = up.basis ?? (tiered ? "priority" : "tier");
|
||||
if (basis === "priority") {
|
||||
const fromPri = currentPri(spec, current);
|
||||
@@ -559,16 +653,6 @@ export function evaluateCandidate(
|
||||
dSource === 0 && dRes === 0 ? "same_tier" : dSource < 0 || dRes < 0 ? "lower_tier" : "below_min_improvement";
|
||||
return reject({ field: "upgrade", code, expected: { minTierSteps: minSteps, from }, actual: tier });
|
||||
}
|
||||
if (r.isEncode && up.encodeGroupAllowlist?.length) {
|
||||
const groups = r.releaseGroup ? r.releaseGroup.name.split("@").map(lc) : [];
|
||||
if (!up.encodeGroupAllowlist.some((g) => groups.includes(lc(g))))
|
||||
return reject({
|
||||
field: "upgrade",
|
||||
code: "encode_not_qualified",
|
||||
expected: up.encodeGroupAllowlist,
|
||||
actual: r.releaseGroup?.name,
|
||||
});
|
||||
}
|
||||
return {
|
||||
decision: "upgrade",
|
||||
tier,
|
||||
|
||||
@@ -59,6 +59,7 @@ export {
|
||||
type SubscriptionGroupResult,
|
||||
subscriptionGroup,
|
||||
} from "./overview.ts";
|
||||
export { seasonReleaseKey, selectSeasonRelease, upgradeSpec } from "./season-release.ts";
|
||||
export {
|
||||
canTransition,
|
||||
type SlotEvent,
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { ParsedRelease } from "../release-parser/types.ts";
|
||||
import { compareRank, currentPri, qualityTier, softRank } from "./evaluate.ts";
|
||||
import type { QualityFacts, SlotSpec } from "./types.ts";
|
||||
|
||||
/** 缺少关键规格时不能声称同套;集号、文件名、种子 ID 不属于版本身份。 */
|
||||
export function seasonReleaseKey(q: QualityFacts): string | null {
|
||||
if (!q.resolution || !q.source || !q.videoCodec || !q.releaseGroup?.name) return null;
|
||||
return JSON.stringify([
|
||||
q.resolution,
|
||||
q.source,
|
||||
q.videoCodec,
|
||||
q.releaseGroup.name
|
||||
.toLowerCase()
|
||||
.split("@")
|
||||
.map((g) => g.trim())
|
||||
.sort(),
|
||||
[...(q.hdr ?? [])].sort(),
|
||||
q.cut ?? "default",
|
||||
]);
|
||||
}
|
||||
|
||||
export function upgradeSpec(spec: SlotSpec): SlotSpec {
|
||||
return {
|
||||
...spec,
|
||||
...(spec.upgradeRules ? { rules: spec.upgradeRules } : {}),
|
||||
...(spec.upgradeHard ? { hard: spec.upgradeHard } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/** 优先级、软偏好、规范化规格形成稳定顺序;同档对齐不允许质量降档。 */
|
||||
export function selectSeasonRelease(spec: SlotSpec, versions: readonly QualityFacts[]): QualityFacts | undefined {
|
||||
let target: QualityFacts | undefined;
|
||||
const rank = (q: QualityFacts) => {
|
||||
const tier = qualityTier(q, spec);
|
||||
return [
|
||||
currentPri(spec, q),
|
||||
tier.source,
|
||||
tier.resolution,
|
||||
...softRank(spec, { raw: "", hdr: [], audio: [], ...q } as unknown as ParsedRelease),
|
||||
];
|
||||
};
|
||||
for (const q of versions) {
|
||||
const key = seasonReleaseKey(q);
|
||||
if (!key) continue;
|
||||
const compared = target ? compareRank(rank(q), rank(target)) : 1;
|
||||
if (compared > 0 || (compared === 0 && key < seasonReleaseKey(target!)!)) target = q;
|
||||
}
|
||||
return target;
|
||||
}
|
||||
@@ -91,6 +91,10 @@ export interface SlotSoftPreferences {
|
||||
|
||||
export interface SlotUpgradePolicy {
|
||||
enabled: boolean;
|
||||
/** 洗版阶段显式规则组;未设时沿用既有洗版默认链。 */
|
||||
ruleGroupIds?: string[];
|
||||
/** 追更可混合,洗版按季向同组、同来源和编码的版本收敛。 */
|
||||
consistency?: "season_release";
|
||||
/** 最小改进:来源等级至少提升几级,默认 1(同等级不反复更换) */
|
||||
minTierSteps?: number;
|
||||
/** 来源等级表;默认 REMUX/原盘/蓝光二压=3,WEB-DL=2,WEBRip/HDTV/HDRip=1,DVD=0 */
|
||||
@@ -128,6 +132,9 @@ export interface SlotSpec {
|
||||
hard: SlotHardConditions;
|
||||
/** 规则组:硬排除 → 拒绝;各组须命中某层(ladderRequired);pri 取最后一个带层的组(覆盖 soft.tiers 的 pri) */
|
||||
rules?: SlotRuleChain;
|
||||
/** 服务入口解析的洗版规则,不参与首次补齐。 */
|
||||
upgradeRules?: SlotRuleChain;
|
||||
upgradeHard?: SlotHardConditions;
|
||||
unknownPolicy?: UnknownPolicy | Partial<Record<HardField, UnknownPolicy>>;
|
||||
soft?: SlotSoftPreferences;
|
||||
upgrade?: SlotUpgradePolicy;
|
||||
@@ -151,6 +158,8 @@ export type QualityFacts = Pick<
|
||||
|
||||
/** 评估上下文:站点字段与折算口径(不在标题解析里的事实由调用方给) */
|
||||
export interface EvaluationContext {
|
||||
/** 季级成套目标,由同一收录版本的有效资产及在途计划确定。 */
|
||||
seasonReleaseTarget?: QualityFacts;
|
||||
siteId?: string;
|
||||
/** 副标题/标签等参与关键词匹配的文本 */
|
||||
subtitle?: string;
|
||||
|
||||
Reference in new issue
Block a user