- Waiting plans (approval / disk space / automation off / queued) are
active-segment rows on /download-tasks/board, merged by created_at,
counted, with approve / cancel actions; ?plan= deep links and legacy
/downloads/plans routes redirect to the panel
- Download detail shows "why this download" (source, purpose, replaced
version, reasons); raw evaluation in tech info
- Failed / cancelled / expired plans appear in the entity download
history; rejected / revoked / expired approvals close draft plans
- New GET /acquisition-plans/{id}/why, POST /acquisition-plans/{id}/cancel
- User-facing copy no longer says 下载计划; glossary updated
Co-Authored-By: Claude Opus 5.5 <[email protected]>
143 lines
6.2 KiB
TypeScript
143 lines
6.2 KiB
TypeScript
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<typeof DownloadBoardState>;
|
|
|
|
export const DOWNLOAD_BOARD_STATUSES: Record<DownloadBoardState, readonly DownloadStatus[]> = {
|
|
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<typeof DownloadBoardQuery>;
|
|
|
|
/** 单个种子的实时数据(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<typeof DownloadLive>;
|
|
|
|
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<typeof DownloadBoardItem>;
|
|
|
|
/**
|
|
* 计划在等什么(DL-3):approval = 等管理员批准(有待处理的审批);space = 下载盘超过暂停线或用量未知;
|
|
* automation_off = 自动化总开关关闭 / 只读对账;queued = 条件已满足、等提交任务执行。
|
|
* 每日下载额度在生成计划时核对,超额不生成计划,所以没有"等额度"的计划。
|
|
*/
|
|
export const PlanWait = z.enum(["approval", "space", "automation_off", "queued"]);
|
|
export type PlanWait = z.infer<typeof PlanWait>;
|
|
|
|
/** 还没提交到下载器的计划,作为「进行中」的一行(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<typeof DownloadBoardPlanItem>;
|
|
|
|
export const DownloadBoardRow = z.discriminatedUnion("kind", [DownloadBoardItem, DownloadBoardPlanItem]);
|
|
export type DownloadBoardRow = z.infer<typeof DownloadBoardRow>;
|
|
|
|
/** 下载的来由(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<typeof DownloadWhy>;
|
|
|
|
export const DownloaderTransfer = z.object({
|
|
instanceId: Id,
|
|
name: z.string(),
|
|
reachable: z.boolean(),
|
|
dlSpeed: NonNegInt,
|
|
upSpeed: NonNegInt,
|
|
});
|
|
export type DownloaderTransfer = z.infer<typeof DownloaderTransfer>;
|
|
|
|
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<typeof DownloadBoard>;
|