refactor(core): migrate to Bun and converge acquisition policies and Agent boundaries

This commit is contained in:
root committed 2026-10-13 14:37:03 +08:00
1 parent 3f1c6d02f6
commit 27c0f27434
268 files changed
+7468 -7852

No files matched your search

+95 -11
View File
@@ -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,
+1
View File
@@ -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;
}
+9
View File
@@ -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;