feat(downloads): live speeds and active/completed split on the download panel (DL-2)

- GET /download-tasks/board?state=active|completed: per-torrent dl/up speed,
  ETA, seeds/leechs, downloader total speed (transfer/info), counts for both
  groups, latest organize result (status, reason code, open decision)
- live qB data cached per instance for 2 s with in-flight coalescing
- panel: total speed line, 进行中 / 已完成 segments remembered locally,
  4 s polling while the page is visible, mobile layout, "—" for zero speed

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-29 21:44:34 +08:00
1 parent 8086250ec2
commit 3b340745bf
17 files changed
+1113 -93

No files matched your search

+10
View File
@@ -8,6 +8,7 @@ import { MetadataEnrichOutput, WebSearchTestInput, WebSearchTestResult } from ".
import { CapabilityDescriptor, KnownCapabilityId } from "./capability.ts";
import { type ApiScope, CursorQuery, cursorPage, HealthStatus, Id, IdParam } from "./common.ts";
import * as content from "./content.ts";
import * as board from "./download-board.ts";
import * as events from "./events.ts";
import * as execution from "./execution.ts";
import * as files from "./files.ts";
@@ -1132,6 +1133,15 @@ export const Api = {
},
R,
),
board: ep({
method: "get",
path: "/download-tasks/board",
scope: "read",
summary:
"下载面板:按进行中 / 已完成分组列出下载任务,附每个种子的实时速度、剩余时间、连接数与下载器总速度(实时数据短时缓存)",
query: board.DownloadBoardQuery,
response: board.DownloadBoard,
}),
pause: ep({
method: "post",
path: "/download-tasks/{id}/pause",
+80
View File
@@ -0,0 +1,80 @@
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 { OrganizationStatus } from "./organization.ts";
// 下载面板(DL-2):下载任务 + 下载器实时数据(速度、剩余时间、连接数)+ 分组计数 + 下载器总速度。
// 实时数据来自 qB torrents/info 与 transfer/info,服务端短 TTL 缓存(轮询不直打 qB)。
/** 进行中 = 排队/提交中/下载中/暂停/无进展/出错/缺失;已完成 = 下载完成(含做种)/入库中/已入库;已移除不出现 */
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(),
});
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({
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>;
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(DownloadBoardItem),
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>;
+1
View File
@@ -9,6 +9,7 @@ export * from "./capabilities-io.ts";
export * from "./capability.ts";
export * from "./common.ts";
export * from "./content.ts";
export * from "./download-board.ts";
export * from "./events.ts";
export * from "./execution.ts";
export * from "./identity.ts";