import { z } from "zod"; import { DecisionKind } from "./agent.ts"; import { CursorQuery, Id, NonNegInt, Timestamp } from "./common.ts"; import { type DownloadStatus, DownloadTask, PlanPurpose, PlanStatus } from "./execution.ts"; import { OrganizationStatus } from "./organization.ts"; // 下载面板(DL-2):下载任务 + 下载器实时数据(速度、剩余时间、连接数)+ 分组计数 + 下载器总速度。 // 实时数据来自 qB torrents/info 与 transfer/info,服务端短 TTL 缓存(轮询不直打 qB)。 /** * 进行中 = 排队/提交中/下载中/暂停/无进展/出错/缺失 + 还没提交、在等条件的计划(DL-3); * 已完成 = 下载完成(含做种)/入库中/已入库;已移除不出现 */ export const DownloadBoardState = z.enum(["active", "completed"]); export type DownloadBoardState = z.infer; export const DOWNLOAD_BOARD_STATUSES: Record = { active: ["queued", "submitting", "downloading", "paused", "stalled", "failed", "missing"], completed: ["completed", "importing", "imported"], }; export const DownloadBoardQuery = CursorQuery.extend({ state: DownloadBoardState.default("active"), instanceId: Id.optional(), /** 深链(DL-3):只返回这个计划对应的行——等待中的计划,或它的下载任务(不分进行中 / 已完成) */ planId: Id.optional(), }); export type DownloadBoardQuery = z.infer; /** 单个种子的实时数据(qB torrents/info);下载器不可达或已不在下载器里时为 null */ export const DownloadLive = z.object({ /** 字节/秒 */ dlSpeed: NonNegInt, upSpeed: NonNegInt, /** 剩余秒数;qB 的 8640000(无穷)与已完成记为 null */ etaSec: NonNegInt.nullable(), progress: z.number().min(0).max(1), rawState: z.string(), /** 已连接的做种 / 下载用户 */ seeds: NonNegInt, leechs: NonNegInt, /** Tracker 报告的总数(未知为 null) */ seedsTotal: NonNegInt.nullable(), leechsTotal: NonNegInt.nullable(), ratio: z.number().nullable(), /** 下载器记录的完成时间 */ completedAt: Timestamp.nullable(), }); export type DownloadLive = z.infer; export const DownloadBoardItem = DownloadTask.extend({ kind: z.literal("task"), live: DownloadLive.nullable(), /** 已识别的作品 */ media: z.object({ id: Id, title: z.string(), year: z.number().int().nullable() }).nullable(), /** 最近一次整理结果:状态 + 原因码(人话见 reasons 表);有未关闭的待处理事项时带上,前端链过去 */ organize: z .object({ recordId: Id, status: OrganizationStatus, reason: z.string().nullable(), decision: z.object({ id: Id, kind: DecisionKind }).nullable(), }) .nullable(), }); export type DownloadBoardItem = z.infer; /** * 计划在等什么(DL-3):approval = 等管理员批准(有待处理的审批);space = 下载盘超过暂停线或用量未知; * automation_off = 自动化总开关关闭 / 只读对账;queued = 条件已满足、等提交任务执行。 * 每日下载额度在生成计划时核对,超额不生成计划,所以没有"等额度"的计划。 */ /** volume_unknown:下载目录读不到容量(配置问题,不是空间不够) */ export const PlanWait = z.enum(["approval", "space", "volume_unknown", "automation_off", "queued"]); export type PlanWait = z.infer; /** 还没提交到下载器的计划,作为「进行中」的一行(id 是计划 id) */ export const DownloadBoardPlanItem = z.object({ kind: z.literal("plan"), id: Id, createdAt: Timestamp, /** 资源标题 */ name: z.string(), sizeBytes: NonNegInt.nullable(), purpose: PlanPurpose, status: PlanStatus, media: z.object({ id: Id, title: z.string(), year: z.number().int().nullable() }).nullable(), wait: PlanWait, /** 原因码(提交任务的延后原因等;人话见 reasons 表,原码进技术信息) */ waitReason: z.string().nullable(), /** 待批准时的审批(批准 / 拒绝回传 payloadHash) */ approval: z.object({ id: Id, payloadHash: z.string(), decisionId: Id.nullable() }).nullable(), }); export type DownloadBoardPlanItem = z.infer; export const DownloadBoardRow = z.discriminatedUnion("kind", [DownloadBoardItem, DownloadBoardPlanItem]); export type DownloadBoardRow = z.infer; /** 下载的来由(DL-3「为什么下这个」):来源、目的、替换的版本、选中理由的原始要点 */ export const DownloadWhy = z.object({ planId: Id, source: z.discriminatedUnion("kind", [ z.object({ kind: z.literal("subscription"), id: Id, name: z.string() }), z.object({ kind: z.literal("agent"), runId: Id, name: z.string() }), z.object({ kind: z.literal("manual") }), z.object({ kind: z.literal("auto") }), ]), purpose: PlanPurpose, /** 收录版本名 */ slotName: z.string().nullable(), /** 升级 / 换源时被替换的版本(规格短句) */ replaces: z.object({ assetId: Id, label: z.string() }).nullable(), /** evaluation.reasons 原串(hard_conditions_met、source_tier_improved、tier:xxx…) */ reasons: z.array(z.string()), /** 资源的促销类型(free / 2xfree / 50% …)与做种数 */ promotion: z.string().nullable(), seeders: NonNegInt.nullable(), siteName: z.string().nullable(), /** 原始评估结果(技术信息里显示) */ evaluation: z.record(z.string(), z.unknown()), }); export type DownloadWhy = z.infer; export const DownloaderTransfer = z.object({ instanceId: Id, name: z.string(), reachable: z.boolean(), dlSpeed: NonNegInt, upSpeed: NonNegInt, }); export type DownloaderTransfer = z.infer; export const DownloadBoard = z.object({ /** 进行中:下载任务与等待中的计划按创建时间一起倒序;已完成只有下载任务 */ items: z.array(DownloadBoardRow), nextCursor: z.string().nullable(), counts: z.object({ active: NonNegInt, completed: NonNegInt }), /** 下载器总速度(transfer/info,含非本系统管理的任务);不可达的实例速度记 0 */ transfer: z.object({ dlSpeed: NonNegInt, upSpeed: NonNegInt, instances: z.array(DownloaderTransfer) }), /** 实时数据的取样时间(缓存命中时早于请求时间);没有启用的下载器时为 null */ liveAt: Timestamp.nullable(), }); export type DownloadBoard = z.infer;