feat(downloads): fold download plans into download panel (DL-3)
- 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]>
This commit is contained in:
1 parent
c0522f2531
commit
a9147e647d
50 files changed
+1535
-348
No files matched your search
@@ -1,13 +1,16 @@
|
||||
import { z } from "zod";
|
||||
import { DecisionKind } from "./agent.ts";
|
||||
import { CursorQuery, Id, NonNegInt, Timestamp } from "./common.ts";
|
||||
import { type DownloadStatus, DownloadTask } from "./execution.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>;
|
||||
|
||||
@@ -19,6 +22,8 @@ export const DOWNLOAD_BOARD_STATUSES: Record<DownloadBoardState, readonly Downlo
|
||||
export const DownloadBoardQuery = CursorQuery.extend({
|
||||
state: DownloadBoardState.default("active"),
|
||||
instanceId: Id.optional(),
|
||||
/** 深链(DL-3):只返回这个计划对应的行——等待中的计划,或它的下载任务(不分进行中 / 已完成) */
|
||||
planId: Id.optional(),
|
||||
});
|
||||
export type DownloadBoardQuery = z.infer<typeof DownloadBoardQuery>;
|
||||
|
||||
@@ -44,6 +49,7 @@ export const DownloadLive = z.object({
|
||||
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(),
|
||||
@@ -59,6 +65,61 @@ export const DownloadBoardItem = DownloadTask.extend({
|
||||
});
|
||||
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(),
|
||||
@@ -69,7 +130,8 @@ export const DownloaderTransfer = z.object({
|
||||
export type DownloaderTransfer = z.infer<typeof DownloaderTransfer>;
|
||||
|
||||
export const DownloadBoard = z.object({
|
||||
items: z.array(DownloadBoardItem),
|
||||
/** 进行中:下载任务与等待中的计划按创建时间一起倒序;已完成只有下载任务 */
|
||||
items: z.array(DownloadBoardRow),
|
||||
nextCursor: z.string().nullable(),
|
||||
counts: z.object({ active: NonNegInt, completed: NonNegInt }),
|
||||
/** 下载器总速度(transfer/info,含非本系统管理的任务);不可达的实例速度记 0 */
|
||||
|
||||
Reference in new issue
Block a user