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; export const PolicyStatus = z.enum(["draft", "published", "superseded", "rolled_back", "archived"]); export type PolicyStatus = z.infer; /** 策略版本:草案 → 预览 → 授权发布 → 版本化/回滚;同一 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; /** 新建总是草案;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; /** 只有草案可改内容;发布/回滚走动作端点 */ export const PolicyRevisionUpdate = PolicyRevisionCreate.omit({ policyKey: true, kind: true }).partial(); export type PolicyRevisionUpdate = z.infer; export const SubscriptionMediaKind = z.enum(["movie", "series", "variety"]); export type SubscriptionMediaKind = z.infer; export const SubscriptionMode = z.enum(["once", "continuous"]); export type SubscriptionMode = z.infer; /** 仅本季 / 持续后续季(REQ §6.5) */ export const SeasonScope = z.enum(["current_season", "following_seasons"]); export type SeasonScope = z.infer; export const SubscriptionOrigin = z.enum(["user", "agent", "policy", "import"]); export type SubscriptionOrigin = z.infer; /** 默认优先级:即时 > 追更 > 新作品 > 升级(REQ §6.8) */ export const AcquisitionPriority = z.enum(["immediate", "tracking", "new_work", "upgrade"]); export type AcquisitionPriority = z.infer; export const SubscriptionStatus = z.enum(["active", "paused", "completed", "cancelled"]); export type SubscriptionStatus = z.infer; 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; 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; 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; /** Slot 状态(REQ §6.1) */ export const SlotStatus = z.enum([ "empty", "reserved", "downloading", "verifying", "satisfied", "upgrade_pending", "attention", ]); export type SlotStatus = z.infer; /** 独立收录目标:硬条件 / 软偏好 / 回退 / 升级阈值 / 完成条件 */ 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; 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; /** 规则可改;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; /** 偏好类型:soft 影响排序;hard 硬排除;version 版本偏好;target 独立目标配置(REQ §6.7) */ export const PreferenceKind = z.enum(["soft", "hard", "version", "target"]); export type PreferenceKind = z.infer; export const PreferenceOrigin = z.enum(["manual", "conversation", "agent_inferred"]); export type PreferenceOrigin = z.infer; export const PreferenceStatus = z.enum(["active", "disabled", "deleted"]); export type PreferenceStatus = z.infer; 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; /** 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; export const PreferenceTagUpdate = PreferenceTagCreate.omit({ origin: true }) .partial() .extend({ status: PreferenceStatus.optional() }); export type PreferenceTagUpdate = z.infer; /** 手动取消标签留下的抑制记录: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; 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; /** 唯一允许的更新是解除抑制 */ export const PreferenceSuppressionUpdate = z.object({ active: z.literal(false), reason: z.string().optional() }); export type PreferenceSuppressionUpdate = z.infer;