merge: fix/d-organize
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
commit
fe32520643
12 files changed
+713
-44
No files matched your search
@@ -38,6 +38,9 @@ const REASONS: Record<string, ReasonText> = {
|
||||
entity_not_found: { text: "作品已不存在" },
|
||||
no_library_root: { text: "没有可用的媒体库目录", action: "在设置 → 媒体库与目录里添加" },
|
||||
library_root_offline: { text: "媒体库目录不可访问或未挂载", action: "检查挂载后重试" },
|
||||
target_dir_unavailable: { text: "整理目标目录建不出来", action: "检查目录权限或挂载,恢复后自动重试" },
|
||||
organize_stalled: { text: "整理多次重试仍未完成", action: "检查后重新整理" },
|
||||
excluded_unresolved: { text: "有视频没能确定季集或目标目录,留在原处", action: "确认季集后重新整理" },
|
||||
target_dir_outside_root: { text: "目标目录不在媒体库目录里" },
|
||||
slot_entity_mismatch: { text: "收录版本和作品对不上" },
|
||||
binding_on_other_root: { text: "作品已放在另一个媒体库目录", action: "在作品详情里调整目录" },
|
||||
|
||||
@@ -68,7 +68,8 @@ const ROWS: Record<string, { row: Obj; dbOnly?: string[]; apiOnly?: string[] }>
|
||||
Observation: { row: C.Observation },
|
||||
OrganizationFileResult: { row: C.OrganizationFileResult },
|
||||
OrganizeSourceSnapshot: { row: C.OrganizeSourceSnapshotRow },
|
||||
OrganizationRecord: { row: C.OrganizationRecord, dbOnly: ["changeXid"] },
|
||||
// requeue_count / requeued_at:整理巡检的内部计数,不外露
|
||||
OrganizationRecord: { row: C.OrganizationRecord, dbOnly: ["changeXid", "requeueCount", "requeuedAt"] },
|
||||
OutboxEvent: { row: C.OutboxEvent },
|
||||
PathBinding: { row: C.PathBinding },
|
||||
PathMapping: { row: C.PathMapping },
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { Kysely } from "kysely";
|
||||
import { execDdl } from "../src/migration-helpers.ts";
|
||||
|
||||
// 整理记录卡在 running 的巡检重投(docs/plan/system-review.md T1-2):
|
||||
// - requeue_count:巡检重新入队的次数,超过上限转失败并开待处理
|
||||
// - requeued_at:最近一次巡检重投时间,下一次判定"卡住"从这里起算
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 迁移不依赖应用表类型
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await execDdl(
|
||||
db,
|
||||
`alter table organization_record add column if not exists requeue_count integer not null default 0;
|
||||
alter table organization_record add column if not exists requeued_at timestamptz;`,
|
||||
);
|
||||
}
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 同上
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await execDdl(
|
||||
db,
|
||||
`alter table organization_record drop column if exists requeued_at;
|
||||
alter table organization_record drop column if exists requeue_count;`,
|
||||
);
|
||||
}
|
||||
@@ -1081,6 +1081,8 @@ export interface OrganizationRecordTable {
|
||||
change_xid: Generated<string>;
|
||||
created_at: GeneratedTimestamptz;
|
||||
updated_at: GeneratedTimestamptz;
|
||||
requeue_count: Generated<number>;
|
||||
requeued_at: Timestamptz | null;
|
||||
}
|
||||
|
||||
export interface OrganizeSourceSnapshotTable {
|
||||
|
||||
@@ -142,16 +142,38 @@ export function organizePlanToFileResults(
|
||||
return [...planned, ...excluded];
|
||||
}
|
||||
|
||||
/** 逐文件状态 → 整理记录状态(全部完成后调用;仍有 queued/running 时为 running) */
|
||||
export function organizationStatusFromFiles(statuses: readonly FileResultStatus[]): OrganizationStatus {
|
||||
/**
|
||||
* 被排除但"本该入库"的视频:季集没定下来、没有对应季目录、命名缺字段。文件留在原处没人管,
|
||||
* 不能算整理成功(Move 成功后会移除下载任务)。样例、过小、已在库等预期跳过不在此列。
|
||||
*/
|
||||
export const UNRESOLVED_EXCLUSIONS = ["episode_unresolved", "no_target_dir", "naming_failed"] as const;
|
||||
const UNRESOLVED_SKIP = new Set<string>(UNRESOLVED_EXCLUSIONS.map((r) => `excluded:${r}`));
|
||||
|
||||
/** 文件结果的 skip_reason 是否属于"本该入库却被排除" */
|
||||
export const isUnresolvedExclusion = (skipReason: string | null | undefined): boolean =>
|
||||
!!skipReason && UNRESOLVED_SKIP.has(skipReason);
|
||||
|
||||
/**
|
||||
* 逐文件状态 → 整理记录状态(全部完成后调用;仍有 queued/running 时为 running)。
|
||||
* unresolved:被排除的待确认视频数(isUnresolvedExclusion),有则成功降为 partial。
|
||||
*/
|
||||
export function organizationStatusFromFiles(
|
||||
statuses: readonly FileResultStatus[],
|
||||
opts: { unresolved?: number } = {},
|
||||
): OrganizationStatus {
|
||||
if (statuses.some((s) => s === "queued" || s === "running")) return "running";
|
||||
const done = statuses.filter((s) => s !== "skipped");
|
||||
// 全部跳过(或没有文件)视为成功:没有需要做的事
|
||||
if (done.length === 0) return "succeeded";
|
||||
const ok = done.filter((s) => s === "succeeded").length;
|
||||
if (ok === done.length) return "succeeded";
|
||||
if (done.every((s) => s === "cancelled")) return "cancelled";
|
||||
return ok > 0 ? "partial" : "failed";
|
||||
// 全部跳过(或没有文件)视为成功:没有需要做的事
|
||||
const base: OrganizationStatus =
|
||||
done.length === 0 || ok === done.length
|
||||
? "succeeded"
|
||||
: done.every((s) => s === "cancelled")
|
||||
? "cancelled"
|
||||
: ok > 0
|
||||
? "partial"
|
||||
: "failed";
|
||||
return base === "succeeded" && (opts.unresolved ?? 0) > 0 ? "partial" : base;
|
||||
}
|
||||
|
||||
/** 整理计划 → organize.plan 能力输出(planId / payloadHash 由保存计划的一方给出) */
|
||||
|
||||
@@ -177,6 +177,16 @@ describe("整理计划 → 整理记录", () => {
|
||||
expect(M.organizationStatusFromFiles(["succeeded", "running"])).toBe("running");
|
||||
expect(M.organizationStatusFromFiles(["cancelled"])).toBe("cancelled");
|
||||
expect(M.organizationStatusFromFiles([])).toBe("succeeded");
|
||||
// 被排除的待确认视频(季集 / 季目录 / 命名)让成功降为 partial;预期跳过不影响
|
||||
expect(M.organizationStatusFromFiles(["succeeded", "skipped"], { unresolved: 1 })).toBe("partial");
|
||||
expect(M.organizationStatusFromFiles(["succeeded", "failed"], { unresolved: 1 })).toBe("partial");
|
||||
expect(M.organizationStatusFromFiles(["failed", "skipped"], { unresolved: 1 })).toBe("failed");
|
||||
expect(M.isUnresolvedExclusion("excluded:episode_unresolved")).toBe(true);
|
||||
expect(M.isUnresolvedExclusion("excluded:no_target_dir")).toBe(true);
|
||||
expect(M.isUnresolvedExclusion("excluded:naming_failed")).toBe(true);
|
||||
expect(M.isUnresolvedExclusion("excluded:too_small")).toBe(false);
|
||||
expect(M.isUnresolvedExclusion("already_in_library")).toBe(false);
|
||||
expect(M.isUnresolvedExclusion(null)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user