293 lines
9.2 KiB
TypeScript
293 lines
9.2 KiB
TypeScript
import { z } from "zod";
|
|
import { EntityBase, Id, JsonObject, NonNegInt, SERVER_MANAGED, Timestamp } from "./common";
|
|
import { AcquisitionPriority } from "./policy";
|
|
|
|
// 执行域:业务任务、下载计划、下载任务、操作账本(REQ §8.4、§11;ADR-002)
|
|
|
|
/** 持久任务类型(任务书 §3.1-8)+ Agent 有界任务 */
|
|
export const TaskKind = z.enum([
|
|
"ingestFeed",
|
|
"resolveObservation",
|
|
"evaluateAcquisition",
|
|
"submitDownload",
|
|
"verifyAndImport",
|
|
"refreshMetadata",
|
|
"reconcileLibrary",
|
|
"cleanupDownloadCache",
|
|
"resolveDecision",
|
|
"agentRun",
|
|
]);
|
|
export type TaskKind = z.infer<typeof TaskKind>;
|
|
export const TaskStatus = z.enum(["pending", "running", "waiting", "succeeded", "failed", "cancelled", "dead"]);
|
|
export type TaskStatus = z.infer<typeof TaskStatus>;
|
|
|
|
export const Task = EntityBase.extend({
|
|
kind: TaskKind,
|
|
handlerVersion: z.number().int().min(1),
|
|
idempotencyKey: z.string().min(1),
|
|
status: TaskStatus,
|
|
/** 数值越小越先执行 */
|
|
priority: z.number().int(),
|
|
input: JsonObject,
|
|
output: JsonObject.nullable(),
|
|
subjectType: z.string().nullable(),
|
|
subjectId: Id.nullable(),
|
|
parentTaskId: Id.nullable(),
|
|
engineRunId: z.string().nullable(),
|
|
attempts: NonNegInt,
|
|
maxAttempts: z.number().int().min(1),
|
|
nextRunAt: Timestamp,
|
|
waitReason: z.string().nullable(),
|
|
cancelRequestedAt: Timestamp.nullable(),
|
|
cancelReason: z.string().nullable(),
|
|
lastError: JsonObject.nullable(),
|
|
startedAt: Timestamp.nullable(),
|
|
finishedAt: Timestamp.nullable(),
|
|
triggeredBy: z.string().nullable(),
|
|
});
|
|
export type Task = z.infer<typeof Task>;
|
|
/** 内部创建输入(幂等键由服务端按语义生成,不能由模型挑选) */
|
|
export const TaskCreate = Task.pick({
|
|
kind: true,
|
|
handlerVersion: true,
|
|
idempotencyKey: true,
|
|
priority: true,
|
|
input: true,
|
|
subjectType: true,
|
|
subjectId: true,
|
|
parentTaskId: true,
|
|
maxAttempts: true,
|
|
nextRunAt: true,
|
|
triggeredBy: true,
|
|
}).partial({
|
|
handlerVersion: true,
|
|
priority: true,
|
|
input: true,
|
|
subjectType: true,
|
|
subjectId: true,
|
|
parentTaskId: true,
|
|
maxAttempts: true,
|
|
nextRunAt: true,
|
|
triggeredBy: true,
|
|
});
|
|
export type TaskCreate = z.infer<typeof TaskCreate>;
|
|
export const TaskUpdate = Task.pick({
|
|
status: true,
|
|
output: true,
|
|
engineRunId: true,
|
|
attempts: true,
|
|
nextRunAt: true,
|
|
waitReason: true,
|
|
lastError: true,
|
|
startedAt: true,
|
|
finishedAt: true,
|
|
}).partial();
|
|
export type TaskUpdate = z.infer<typeof TaskUpdate>;
|
|
/** 取消 = 停止未来步骤,不撤销已完成外部动作(REQ §11.4) */
|
|
export const TaskCancelRequest = z.object({ reason: z.string().min(1).max(500) });
|
|
export type TaskCancelRequest = z.infer<typeof TaskCancelRequest>;
|
|
|
|
export const PlanPurpose = z.enum(["fill", "upgrade", "replace_source"]);
|
|
export type PlanPurpose = z.infer<typeof PlanPurpose>;
|
|
export const PlanStatus = z.enum(["draft", "reserved", "submitted", "completed", "cancelled", "failed", "expired"]);
|
|
export type PlanStatus = z.infer<typeof PlanStatus>;
|
|
|
|
/** 下载计划:与 Slot 预留、预算预留同事务创建;一个 Slot 同时只能有一个 reserved/submitted 计划 */
|
|
export const AcquisitionPlan = EntityBase.extend({
|
|
slotId: Id,
|
|
sourceReleaseId: Id,
|
|
taskId: Id.nullable(),
|
|
purpose: PlanPurpose,
|
|
status: PlanStatus,
|
|
priority: AcquisitionPriority,
|
|
/** evaluateAcquisition 的确定性评估结果与拒绝/接受理由 */
|
|
evaluation: JsonObject,
|
|
reservedBytes: NonNegInt,
|
|
budgetReservation: JsonObject,
|
|
payloadHash: z.string().min(1),
|
|
expectedRevision: z.number().int().min(1),
|
|
idempotencyKey: z.string().min(1),
|
|
expiresAt: Timestamp.nullable(),
|
|
statusReason: z.string().nullable(),
|
|
});
|
|
export type AcquisitionPlan = z.infer<typeof AcquisitionPlan>;
|
|
export const AcquisitionPlanCreate = AcquisitionPlan.omit({ ...SERVER_MANAGED, status: true }).partial({
|
|
taskId: true,
|
|
purpose: true,
|
|
priority: true,
|
|
evaluation: true,
|
|
reservedBytes: true,
|
|
budgetReservation: true,
|
|
expectedRevision: true,
|
|
expiresAt: true,
|
|
statusReason: true,
|
|
});
|
|
export type AcquisitionPlanCreate = z.infer<typeof AcquisitionPlanCreate>;
|
|
export const AcquisitionPlanUpdate = AcquisitionPlan.pick({ status: true, taskId: true, statusReason: true }).partial();
|
|
export type AcquisitionPlanUpdate = z.infer<typeof AcquisitionPlanUpdate>;
|
|
|
|
export const DownloadOrigin = z.enum(["submitted", "adopted"]);
|
|
export type DownloadOrigin = z.infer<typeof DownloadOrigin>;
|
|
export const DownloadStatus = z.enum([
|
|
"queued",
|
|
"submitting",
|
|
"downloading",
|
|
"paused",
|
|
"stalled",
|
|
"completed",
|
|
"importing",
|
|
"imported",
|
|
"failed",
|
|
"removed",
|
|
"missing",
|
|
]);
|
|
export type DownloadStatus = z.infer<typeof DownloadStatus>;
|
|
/** 暂停原因:阈值恢复时只恢复 threshold 暂停项,不恢复用户手动暂停(REQ §8.4.1) */
|
|
export const PauseReason = z.enum(["user", "threshold", "policy", "agent"]);
|
|
export type PauseReason = z.infer<typeof PauseReason>;
|
|
export const TorrentHash = z.string().regex(/^[0-9a-f]{40}$|^[0-9a-f]{64}$/);
|
|
|
|
export const DownloadTask = EntityBase.extend({
|
|
instanceId: Id,
|
|
/** 该下载器实例内的 torrent 标识(v1 hash 或 v2 hash,小写十六进制) */
|
|
torrentHash: TorrentHash,
|
|
infohashV1: z.string().nullable(),
|
|
infohashV2: z.string().nullable(),
|
|
acquisitionPlanId: Id.nullable(),
|
|
sourceReleaseId: Id.nullable(),
|
|
name: z.string(),
|
|
savePath: z.string().nullable(),
|
|
contentPath: z.string().nullable(),
|
|
category: z.string().nullable(),
|
|
tags: z.array(z.string()),
|
|
/** 是否在管理标签范围内;标签移除后暂停后续自动动作 */
|
|
managed: z.boolean(),
|
|
origin: DownloadOrigin,
|
|
status: DownloadStatus,
|
|
pauseReason: PauseReason.nullable(),
|
|
rawState: z.string().nullable(),
|
|
progress: z.number().min(0).max(1),
|
|
sizeBytes: NonNegInt.nullable(),
|
|
downloadedBytes: NonNegInt,
|
|
remainingBytes: NonNegInt.nullable(),
|
|
ratio: z.number().nullable(),
|
|
seedingTimeSec: NonNegInt.nullable(),
|
|
completedAt: Timestamp.nullable(),
|
|
lastProgressAt: Timestamp.nullable(),
|
|
/** 无进展起点:只记录并汇入待处理,不自动换源(REQ §8.4.2) */
|
|
noProgressSince: Timestamp.nullable(),
|
|
lastSyncedAt: Timestamp.nullable(),
|
|
nextCheckAt: Timestamp.nullable(),
|
|
removedAt: Timestamp.nullable(),
|
|
fileList: z.array(z.object({ path: z.string(), size: NonNegInt, progress: z.number().optional() })).nullable(),
|
|
lastError: JsonObject.nullable(),
|
|
});
|
|
export type DownloadTask = z.infer<typeof DownloadTask>;
|
|
export const DownloadTaskCreate = DownloadTask.pick({
|
|
instanceId: true,
|
|
torrentHash: true,
|
|
infohashV1: true,
|
|
infohashV2: true,
|
|
acquisitionPlanId: true,
|
|
sourceReleaseId: true,
|
|
name: true,
|
|
savePath: true,
|
|
category: true,
|
|
tags: true,
|
|
managed: true,
|
|
origin: true,
|
|
}).partial({
|
|
infohashV1: true,
|
|
infohashV2: true,
|
|
acquisitionPlanId: true,
|
|
sourceReleaseId: true,
|
|
savePath: true,
|
|
category: true,
|
|
tags: true,
|
|
managed: true,
|
|
origin: true,
|
|
});
|
|
export type DownloadTaskCreate = z.infer<typeof DownloadTaskCreate>;
|
|
export const DownloadTaskUpdate = DownloadTask.omit({
|
|
...SERVER_MANAGED,
|
|
instanceId: true,
|
|
torrentHash: true,
|
|
}).partial();
|
|
export type DownloadTaskUpdate = z.infer<typeof DownloadTaskUpdate>;
|
|
|
|
export const OperationPhase = z.enum(["decision", "prepare", "transfer", "verify", "commit", "cleanup", "compensate"]);
|
|
export type OperationPhase = z.infer<typeof OperationPhase>;
|
|
export const OperationKind = z.enum([
|
|
"copy",
|
|
"move",
|
|
"rename",
|
|
"mkdir",
|
|
"hardlink",
|
|
"delete_source",
|
|
"delete_target",
|
|
"verify",
|
|
"register",
|
|
"qb_pause",
|
|
"qb_resume",
|
|
"qb_remove",
|
|
"qb_set_tags",
|
|
"other",
|
|
]);
|
|
export type OperationKind = z.infer<typeof OperationKind>;
|
|
export const OperationStatus = z.enum(["planned", "started", "succeeded", "failed", "skipped", "compensated"]);
|
|
export type OperationStatus = z.infer<typeof OperationStatus>;
|
|
|
|
/** 副作用账本:plan → attempt(started, attemptToken) → result;提交时以 attemptToken 做 fencing */
|
|
export const AssetOperation = EntityBase.extend({
|
|
operationId: z.string().min(1),
|
|
taskId: Id.nullable(),
|
|
organizationRecordId: Id.nullable(),
|
|
planFingerprint: z.string().nullable(),
|
|
ordinal: z.number().int().nonnegative(),
|
|
phase: OperationPhase,
|
|
kind: OperationKind,
|
|
status: OperationStatus,
|
|
intent: JsonObject,
|
|
attemptToken: Id.nullable(),
|
|
attempts: NonNegInt,
|
|
result: JsonObject.nullable(),
|
|
error: JsonObject.nullable(),
|
|
mediaAssetId: Id.nullable(),
|
|
downloadTaskId: Id.nullable(),
|
|
startedAt: Timestamp.nullable(),
|
|
finishedAt: Timestamp.nullable(),
|
|
});
|
|
export type AssetOperation = z.infer<typeof AssetOperation>;
|
|
export const AssetOperationCreate = AssetOperation.pick({
|
|
operationId: true,
|
|
taskId: true,
|
|
organizationRecordId: true,
|
|
planFingerprint: true,
|
|
ordinal: true,
|
|
phase: true,
|
|
kind: true,
|
|
intent: true,
|
|
mediaAssetId: true,
|
|
downloadTaskId: true,
|
|
}).partial({
|
|
taskId: true,
|
|
organizationRecordId: true,
|
|
planFingerprint: true,
|
|
ordinal: true,
|
|
intent: true,
|
|
mediaAssetId: true,
|
|
downloadTaskId: true,
|
|
});
|
|
export type AssetOperationCreate = z.infer<typeof AssetOperationCreate>;
|
|
export const AssetOperationUpdate = AssetOperation.pick({
|
|
status: true,
|
|
attemptToken: true,
|
|
attempts: true,
|
|
result: true,
|
|
error: true,
|
|
mediaAssetId: true,
|
|
startedAt: true,
|
|
finishedAt: true,
|
|
}).partial();
|
|
export type AssetOperationUpdate = z.infer<typeof AssetOperationUpdate>;
|