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

+1
View File
@@ -25,6 +25,7 @@ export {
resolveAutoFill,
} from "./fill.ts";
export { type AcquisitionMode, determineMode, type ModeDecision, type ModeInput } from "./mode.ts";
export { type ScopedAcquisitionPolicy, selectAcquisitionPolicy } from "./policy.ts";
export {
DEFAULT_PRIORITY_ORDER,
type PriorityClass,
+30
View File
@@ -0,0 +1,30 @@
import type { AcquisitionPolicyScope } from "@umt/contracts";
export interface ScopedAcquisitionPolicy {
id: string;
scope: AcquisitionPolicyScope;
}
/** 输入已按发布时间排序;分类明确命中优先于类型默认,显式选择不回退到别的策略。 */
export function selectAcquisitionPolicy<T extends ScopedAcquisitionPolicy>(
policies: readonly T[],
input: { explicitId?: string | null; mediaKind: "movie" | "series" | "variety"; category: string | null },
): T | undefined {
if (input.explicitId) return policies.find((p) => p.id === input.explicitId);
const type = input.mediaKind === "movie" ? "movie" : "tv";
let selected: T | undefined;
let specificity = -1;
for (const p of policies) {
if (p.scope.mediaKinds?.length && !p.scope.mediaKinds.includes(input.mediaKind)) continue;
const categories = p.scope.categories;
if (categories && Object.values(categories).some((v) => v?.length)) {
if (!input.category || !categories[type]?.includes(input.category)) continue;
}
const score = categories?.[type]?.length ? 2 : p.scope.mediaKinds?.length ? 1 : 0;
if (score > specificity) {
selected = p;
specificity = score;
}
}
return selected;
}
+3 -9
View File
@@ -1,3 +1,4 @@
import type { ContentType } from "@umt/contracts";
import type { CompiledGlobalWord } from "../identify-words/apply.ts";
/** 解析器版本:规则变化时递增,缓存/识别结果按此失效 */
@@ -35,15 +36,8 @@ export type Cut =
| "Ultimate"
| "Special Edition";
/** 综艺内容类型(REQ §5.1);v0.1 只识别标记,不做分段建模 */
export type ContentType =
| "unknown"
| "main"
| "extra"
| "behind_the_scenes"
| "performance_only"
| "preview"
| "reaction";
// 内容类型在 contracts 定义一次;领域与接口使用相同语义。
export type { ContentType };
export type Segment = "full" | "upper" | "middle" | "lower";
+77
View File
@@ -0,0 +1,77 @@
import type { ParsedRelease } from "../release-parser/types.ts";
import type { HardField } from "../slot/types.ts";
import { type AtomCatalog, testAtom } from "./evaluate.ts";
import type { RuleContext, RuleExpr, RuleGroupSpec } from "./types.ts";
// 只对有明确事实字段的条件判断未知;正则文本缺词仍是已知不命中。
const fields: Record<string, HardField> = {
"4K": "resolution",
"1080P": "resolution",
"720P": "resolution",
BLU: "source",
BLURAY: "source",
REMUX: "source",
WEBDL: "source",
H265: "codec",
H264: "codec",
FREE: "promotion",
};
type PartialResult = { hit: boolean | null; missing: HardField[] };
function partial(e: RuleExpr, r: ParsedRelease, ctx: RuleContext, catalog: AtomCatalog): PartialResult {
if (e.op === "atom") {
const id = e.id.toUpperCase();
const f = fields[id];
const value =
f === "resolution"
? r.resolution
: f === "source"
? r.source
: f === "codec"
? r.videoCodec
: ctx.downloadVolumeFactor;
if (f && value === undefined) return { hit: null, missing: [f] };
const custom = catalog.get(id);
const missing: HardField[] = [];
if (custom?.minSeeders !== undefined && ctx.seeders === undefined) missing.push("seeders");
if (custom?.free && ctx.downloadVolumeFactor === undefined) missing.push("promotion");
if (custom?.unitSizeMB && r.sizeBytes === undefined) missing.push("size");
const input = { r, ctx, text: [r.raw ?? "", ctx.subtitle ?? "", ...(ctx.labels ?? [])].join(" ") };
// 已知的正则条件失败时,其他缺失字段不会改变结论。
if (custom && missing.length) {
const known = { ...custom, minSeeders: undefined, free: undefined, unitSizeMB: undefined };
const knownCatalog = new Map(catalog).set(id, known);
if (!testAtom(id, input, knownCatalog).hit) return { hit: false, missing: [] };
return { hit: null, missing };
}
return { hit: testAtom(id, input, catalog).hit, missing: [] };
}
if (e.op === "not") {
const result = partial(e.arg, r, ctx, catalog);
return { ...result, hit: result.hit === null ? null : !result.hit };
}
const results = e.args.map((a) => partial(a, r, ctx, catalog));
const decisive = e.op !== "and";
if (results.some((x) => x.hit === decisive)) return { hit: decisive, missing: [] };
const unknown = results.filter((x) => x.hit === null);
return unknown.length ? { hit: null, missing: unknown.flatMap((x) => x.missing) } : { hit: !decisive, missing: [] };
}
/** 未知的高档不能被当成不命中而落入低档,NOT 也不能把未知当成事实。 */
export function unknownRuleFields(
groups: readonly RuleGroupSpec[],
r: ParsedRelease,
ctx: RuleContext,
catalog: AtomCatalog,
): HardField[] {
const missing: HardField[] = [];
for (const group of groups) {
for (const expr of group.hardExcludes ?? []) missing.push(...partial(expr, r, ctx, catalog).missing);
for (const layer of group.layers) {
const result = partial(layer.expr, r, ctx, catalog);
missing.push(...result.missing);
if (result.hit === true) break;
}
}
return [...new Set(missing)];
}
+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;