Some checks failed
docker-images / resolve-build-targets (push) Successful in 5s
docker-images / build-and-push (admin) (push) Successful in 30s
docker-images / submit-indexnow (push) Has been cancelled
docker-images / build-and-push (frontend) (push) Has been cancelled
docker-images / build-and-push (backend) (push) Has been cancelled
128 lines
3.4 KiB
TypeScript
128 lines
3.4 KiB
TypeScript
import type { WorkerJobRecord } from "@/lib/types";
|
|
|
|
type WorkerProgressShape = {
|
|
phase?: string;
|
|
message?: string;
|
|
total_chunks?: number;
|
|
processed_chunks?: number;
|
|
total_batches?: number;
|
|
current_batch?: number;
|
|
batch_size?: number;
|
|
percent?: number;
|
|
};
|
|
|
|
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
return null;
|
|
}
|
|
|
|
return value as Record<string, unknown>;
|
|
}
|
|
|
|
function asNumber(value: unknown): number | null {
|
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
return value;
|
|
}
|
|
|
|
if (typeof value === "string") {
|
|
const parsed = Number(value);
|
|
return Number.isFinite(parsed) ? parsed : null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function asText(value: unknown): string | null {
|
|
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
}
|
|
|
|
export function getWorkerProgress(
|
|
job: Pick<WorkerJobRecord, "result">,
|
|
): WorkerProgressShape | null {
|
|
const result = asRecord(job.result);
|
|
if (!result) {
|
|
return null;
|
|
}
|
|
|
|
const nested = asRecord(result.progress);
|
|
const source = nested ?? result;
|
|
const percent = asNumber(source.percent);
|
|
const totalChunks = asNumber(source.total_chunks);
|
|
const processedChunks = asNumber(source.processed_chunks);
|
|
const totalBatches = asNumber(source.total_batches);
|
|
const currentBatch = asNumber(source.current_batch);
|
|
const batchSize = asNumber(source.batch_size);
|
|
const phase = asText(source.phase) ?? asText(result.phase) ?? undefined;
|
|
const message = asText(source.message) ?? asText(result.message) ?? undefined;
|
|
|
|
if (
|
|
percent === null &&
|
|
totalChunks === null &&
|
|
processedChunks === null &&
|
|
totalBatches === null &&
|
|
currentBatch === null &&
|
|
batchSize === null &&
|
|
!phase &&
|
|
!message
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
phase,
|
|
message,
|
|
total_chunks: totalChunks ?? undefined,
|
|
processed_chunks: processedChunks ?? undefined,
|
|
total_batches: totalBatches ?? undefined,
|
|
current_batch: currentBatch ?? undefined,
|
|
batch_size: batchSize ?? undefined,
|
|
percent: percent ?? undefined,
|
|
};
|
|
}
|
|
|
|
export function formatWorkerProgress(
|
|
job: Pick<WorkerJobRecord, "result">,
|
|
): string | null {
|
|
const progress = getWorkerProgress(job);
|
|
if (!progress) {
|
|
return null;
|
|
}
|
|
|
|
const percentText =
|
|
typeof progress.percent === "number"
|
|
? `${Math.max(0, Math.min(100, Math.round(progress.percent)))}%`
|
|
: null;
|
|
const chunkText =
|
|
typeof progress.processed_chunks === "number" &&
|
|
typeof progress.total_chunks === "number"
|
|
? `${progress.processed_chunks}/${progress.total_chunks} 分块`
|
|
: null;
|
|
const batchText =
|
|
typeof progress.current_batch === "number" &&
|
|
typeof progress.total_batches === "number" &&
|
|
progress.total_batches > 0
|
|
? `第 ${progress.current_batch}/${progress.total_batches} 批`
|
|
: null;
|
|
|
|
const details = [percentText, chunkText, batchText]
|
|
.filter(Boolean)
|
|
.join(" · ");
|
|
|
|
if (progress.message && details) {
|
|
return `${progress.message} ${details}`;
|
|
}
|
|
|
|
return progress.message ?? (details || null);
|
|
}
|
|
|
|
export function getWorkerProgressPercent(
|
|
job: Pick<WorkerJobRecord, "result">,
|
|
): number | null {
|
|
const progress = getWorkerProgress(job);
|
|
if (typeof progress?.percent !== "number") {
|
|
return null;
|
|
}
|
|
|
|
return Math.max(0, Math.min(100, Math.round(progress.percent)));
|
|
}
|