248 lines
9.1 KiB
TypeScript
248 lines
9.1 KiB
TypeScript
import { z } from "zod";
|
|
import { EntityBase, Id, JsonObject, SERVER_MANAGED, Timestamp } from "./common";
|
|
|
|
// 规则域:订阅、策略版本、收录 Slot、偏好标签与抑制记录(REQ §6)
|
|
|
|
export const PolicyKind = z.enum(["acquisition", "preference", "retention", "cleanup", "instance", "automation"]);
|
|
export type PolicyKind = z.infer<typeof PolicyKind>;
|
|
export const PolicyStatus = z.enum(["draft", "published", "superseded", "rolled_back", "archived"]);
|
|
export type PolicyStatus = z.infer<typeof PolicyStatus>;
|
|
|
|
/** 策略版本:草案 → 预览 → 授权发布 → 版本化/回滚;同一 policyKey 只有一个 published */
|
|
export const PolicyRevision = EntityBase.extend({
|
|
policyKey: z.string().min(1),
|
|
revision: z.number().int().min(1),
|
|
kind: PolicyKind,
|
|
status: PolicyStatus,
|
|
title: z.string().min(1),
|
|
summary: z.string().nullable(),
|
|
scope: JsonObject,
|
|
/** 结构化规则数据;不是可执行脚本或工作流 DSL */
|
|
rules: JsonObject,
|
|
preview: JsonObject.nullable(),
|
|
basedOnRevisionId: Id.nullable(),
|
|
createdBy: z.string().nullable(),
|
|
publishedAt: Timestamp.nullable(),
|
|
publishedBy: z.string().nullable(),
|
|
});
|
|
export type PolicyRevision = z.infer<typeof PolicyRevision>;
|
|
/** 新建总是草案;revision 由服务端分配 */
|
|
export const PolicyRevisionCreate = PolicyRevision.pick({
|
|
policyKey: true,
|
|
kind: true,
|
|
title: true,
|
|
summary: true,
|
|
scope: true,
|
|
rules: true,
|
|
basedOnRevisionId: true,
|
|
}).partial({ summary: true, scope: true, basedOnRevisionId: true });
|
|
export type PolicyRevisionCreate = z.infer<typeof PolicyRevisionCreate>;
|
|
/** 只有草案可改内容;发布/回滚走动作端点 */
|
|
export const PolicyRevisionUpdate = PolicyRevisionCreate.omit({ policyKey: true, kind: true }).partial();
|
|
export type PolicyRevisionUpdate = z.infer<typeof PolicyRevisionUpdate>;
|
|
|
|
export const SubscriptionMediaKind = z.enum(["movie", "series", "variety"]);
|
|
export type SubscriptionMediaKind = z.infer<typeof SubscriptionMediaKind>;
|
|
export const SubscriptionMode = z.enum(["once", "continuous"]);
|
|
export type SubscriptionMode = z.infer<typeof SubscriptionMode>;
|
|
/** 仅本季 / 持续后续季(REQ §6.5) */
|
|
export const SeasonScope = z.enum(["current_season", "following_seasons"]);
|
|
export type SeasonScope = z.infer<typeof SeasonScope>;
|
|
export const SubscriptionOrigin = z.enum(["user", "agent", "policy", "import"]);
|
|
export type SubscriptionOrigin = z.infer<typeof SubscriptionOrigin>;
|
|
/** 默认优先级:即时 > 追更 > 新作品 > 升级(REQ §6.8) */
|
|
export const AcquisitionPriority = z.enum(["immediate", "tracking", "new_work", "upgrade"]);
|
|
export type AcquisitionPriority = z.infer<typeof AcquisitionPriority>;
|
|
export const SubscriptionStatus = z.enum(["active", "paused", "completed", "cancelled"]);
|
|
export type SubscriptionStatus = z.infer<typeof SubscriptionStatus>;
|
|
|
|
export const Subscription = EntityBase.extend({
|
|
mediaEntityId: Id.nullable(),
|
|
seasonUnitId: Id.nullable(),
|
|
/** 作品尚未建档时的检索词 */
|
|
titleQuery: z.string().nullable(),
|
|
mediaKind: SubscriptionMediaKind,
|
|
mode: SubscriptionMode,
|
|
seasonScope: SeasonScope,
|
|
origin: SubscriptionOrigin,
|
|
priority: AcquisitionPriority,
|
|
status: SubscriptionStatus,
|
|
policyRevisionId: Id.nullable(),
|
|
params: JsonObject,
|
|
statusReason: z.string().nullable(),
|
|
lastCheckedAt: Timestamp.nullable(),
|
|
createdBy: z.string().nullable(),
|
|
});
|
|
export type Subscription = z.infer<typeof Subscription>;
|
|
export const SubscriptionCreate = Subscription.pick({
|
|
mediaEntityId: true,
|
|
seasonUnitId: true,
|
|
titleQuery: true,
|
|
mediaKind: true,
|
|
mode: true,
|
|
seasonScope: true,
|
|
origin: true,
|
|
priority: true,
|
|
policyRevisionId: true,
|
|
params: true,
|
|
})
|
|
.partial({
|
|
mediaEntityId: true,
|
|
seasonUnitId: true,
|
|
titleQuery: true,
|
|
mode: true,
|
|
seasonScope: true,
|
|
origin: true,
|
|
priority: true,
|
|
policyRevisionId: true,
|
|
params: true,
|
|
})
|
|
.refine((v) => !!v.mediaEntityId || !!v.titleQuery, { message: "mediaEntityId 与 titleQuery 至少给一个" });
|
|
export type SubscriptionCreate = z.infer<typeof SubscriptionCreate>;
|
|
export const SubscriptionUpdate = Subscription.pick({
|
|
mode: true,
|
|
seasonScope: true,
|
|
priority: true,
|
|
status: true,
|
|
policyRevisionId: true,
|
|
params: true,
|
|
statusReason: true,
|
|
}).partial();
|
|
export type SubscriptionUpdate = z.infer<typeof SubscriptionUpdate>;
|
|
|
|
/** Slot 状态(REQ §6.1) */
|
|
export const SlotStatus = z.enum([
|
|
"empty",
|
|
"reserved",
|
|
"downloading",
|
|
"verifying",
|
|
"satisfied",
|
|
"upgrade_pending",
|
|
"attention",
|
|
]);
|
|
export type SlotStatus = z.infer<typeof SlotStatus>;
|
|
|
|
/** 独立收录目标:硬条件 / 软偏好 / 回退 / 升级阈值 / 完成条件 */
|
|
export const AcquisitionSlot = EntityBase.extend({
|
|
mediaEntityId: Id,
|
|
contentUnitId: Id.nullable(),
|
|
subscriptionId: Id.nullable(),
|
|
policyRevisionId: Id.nullable(),
|
|
/** 用户命名,如 archival / streaming */
|
|
name: z.string().min(1).max(64),
|
|
status: SlotStatus,
|
|
hardConditions: JsonObject,
|
|
softPreferences: JsonObject,
|
|
fallback: JsonObject,
|
|
upgradeThreshold: JsonObject,
|
|
completion: JsonObject,
|
|
/** 同组 Slot 在 requireDistinctAsset=true 时必须由不同物理资产满足(M2 校验) */
|
|
distinctGroup: z.string().nullable(),
|
|
requireDistinctAsset: z.boolean(),
|
|
currentAssetId: Id.nullable(),
|
|
reservedPlanId: Id.nullable(),
|
|
statusReason: z.string().nullable(),
|
|
lastEvaluatedAt: Timestamp.nullable(),
|
|
nextEvaluateAt: Timestamp.nullable(),
|
|
revision: z.number().int().min(1),
|
|
});
|
|
export type AcquisitionSlot = z.infer<typeof AcquisitionSlot>;
|
|
export const AcquisitionSlotCreate = AcquisitionSlot.pick({
|
|
mediaEntityId: true,
|
|
contentUnitId: true,
|
|
subscriptionId: true,
|
|
policyRevisionId: true,
|
|
name: true,
|
|
hardConditions: true,
|
|
softPreferences: true,
|
|
fallback: true,
|
|
upgradeThreshold: true,
|
|
completion: true,
|
|
distinctGroup: true,
|
|
requireDistinctAsset: true,
|
|
}).partial({
|
|
contentUnitId: true,
|
|
subscriptionId: true,
|
|
policyRevisionId: true,
|
|
hardConditions: true,
|
|
softPreferences: true,
|
|
fallback: true,
|
|
upgradeThreshold: true,
|
|
completion: true,
|
|
distinctGroup: true,
|
|
requireDistinctAsset: true,
|
|
});
|
|
export type AcquisitionSlotCreate = z.infer<typeof AcquisitionSlotCreate>;
|
|
/** 规则可改;status/currentAsset/reservedPlan 只由状态机推进,不经 Update */
|
|
export const AcquisitionSlotUpdate = AcquisitionSlotCreate.omit({
|
|
mediaEntityId: true,
|
|
contentUnitId: true,
|
|
name: true,
|
|
})
|
|
.partial()
|
|
.extend({ expectedRevision: z.number().int().min(1) });
|
|
export type AcquisitionSlotUpdate = z.infer<typeof AcquisitionSlotUpdate>;
|
|
|
|
/** 偏好类型:soft 影响排序;hard 硬排除;version 版本偏好;target 独立目标配置(REQ §6.7) */
|
|
export const PreferenceKind = z.enum(["soft", "hard", "version", "target"]);
|
|
export type PreferenceKind = z.infer<typeof PreferenceKind>;
|
|
export const PreferenceOrigin = z.enum(["manual", "conversation", "agent_inferred"]);
|
|
export type PreferenceOrigin = z.infer<typeof PreferenceOrigin>;
|
|
export const PreferenceStatus = z.enum(["active", "disabled", "deleted"]);
|
|
export type PreferenceStatus = z.infer<typeof PreferenceStatus>;
|
|
|
|
export const PreferenceTag = EntityBase.extend({
|
|
label: z.string().min(1).max(80),
|
|
kind: PreferenceKind,
|
|
scope: JsonObject,
|
|
rule: JsonObject,
|
|
/** 语义指纹(kind+scope+rule 规范化哈希),用于抑制匹配 */
|
|
fingerprint: z.string().min(1),
|
|
origin: PreferenceOrigin,
|
|
description: z.string().nullable(),
|
|
status: PreferenceStatus,
|
|
revision: z.number().int().min(1),
|
|
evidence: JsonObject,
|
|
policyRevisionId: Id.nullable(),
|
|
});
|
|
export type PreferenceTag = z.infer<typeof PreferenceTag>;
|
|
/** fingerprint 由服务端计算 */
|
|
export const PreferenceTagCreate = PreferenceTag.pick({
|
|
label: true,
|
|
kind: true,
|
|
scope: true,
|
|
rule: true,
|
|
origin: true,
|
|
description: true,
|
|
evidence: true,
|
|
}).partial({ scope: true, rule: true, origin: true, description: true, evidence: true });
|
|
export type PreferenceTagCreate = z.infer<typeof PreferenceTagCreate>;
|
|
export const PreferenceTagUpdate = PreferenceTagCreate.omit({ origin: true })
|
|
.partial()
|
|
.extend({ status: PreferenceStatus.optional() });
|
|
export type PreferenceTagUpdate = z.infer<typeof PreferenceTagUpdate>;
|
|
|
|
/** 手动取消标签留下的抑制记录:Agent 不得据同一旧证据自动加回 */
|
|
export const PreferenceSuppression = EntityBase.extend({
|
|
preferenceTagId: Id.nullable(),
|
|
fingerprint: z.string().min(1),
|
|
reason: z.string().nullable(),
|
|
evidence: JsonObject,
|
|
suppressedBy: z.string().min(1),
|
|
active: z.boolean(),
|
|
liftedAt: Timestamp.nullable(),
|
|
liftedBy: z.string().nullable(),
|
|
});
|
|
export type PreferenceSuppression = z.infer<typeof PreferenceSuppression>;
|
|
export const PreferenceSuppressionCreate = PreferenceSuppression.omit({
|
|
...SERVER_MANAGED,
|
|
active: true,
|
|
liftedAt: true,
|
|
liftedBy: true,
|
|
suppressedBy: true,
|
|
}).partial({ preferenceTagId: true, reason: true, evidence: true });
|
|
export type PreferenceSuppressionCreate = z.infer<typeof PreferenceSuppressionCreate>;
|
|
/** 唯一允许的更新是解除抑制 */
|
|
export const PreferenceSuppressionUpdate = z.object({ active: z.literal(false), reason: z.string().optional() });
|
|
export type PreferenceSuppressionUpdate = z.infer<typeof PreferenceSuppressionUpdate>;
|