fix(download): 连续 3 轮查不到才判移除(M1)
qB 列表与按 hash 都查不到时先记 missing_since / missing_count,连续 MISSING_CONFIRM_ROUNDS(3) 轮才判 removed;中途重新出现或只是去掉管理标签即清零。相关测试改为同步多轮后再断言移除。 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
1 parent
cba5de731b
commit
d96f0ff19f
4 files changed
+25
-6
No files matched your search
@@ -718,6 +718,9 @@ export interface SyncStats {
|
|||||||
thresholdResumed: number;
|
thresholdResumed: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 连续多少轮同步(列表 + 按 hash 查)都查不到才判 removed;与资产对账的 missingConfirmAfter 同口径 */
|
||||||
|
export const MISSING_CONFIRM_ROUNDS = 3;
|
||||||
|
|
||||||
export async function syncDownloads(d: AoDeps): Promise<SyncStats> {
|
export async function syncDownloads(d: AoDeps): Promise<SyncStats> {
|
||||||
const stats: SyncStats = {
|
const stats: SyncStats = {
|
||||||
instances: 0,
|
instances: 0,
|
||||||
@@ -797,13 +800,20 @@ async function syncInstance(d: AoDeps, topo: InstanceTopology, stats: SyncStats)
|
|||||||
// 管理标签被移除:停止后续自动动作,保留历史与资产
|
// 管理标签被移除:停止后续自动动作,保留历史与资产
|
||||||
await trx
|
await trx
|
||||||
.updateTable("download_task")
|
.updateTable("download_task")
|
||||||
.set({ managed: false, tags: still.tags, last_synced_at: now })
|
.set({ managed: false, tags: still.tags, last_synced_at: now, missing_since: null, missing_count: 0 })
|
||||||
|
.where("id", "=", row.id)
|
||||||
|
.execute();
|
||||||
|
} else if (row.missing_count + 1 < MISSING_CONFIRM_ROUNDS) {
|
||||||
|
// 列表与按 hash 都查不到:先记缺席(qB 刚重启、任务还没加载完时会短暂返回空 / 部分列表)
|
||||||
|
await trx
|
||||||
|
.updateTable("download_task")
|
||||||
|
.set({ missing_since: row.missing_since ?? now, missing_count: row.missing_count + 1, last_synced_at: now })
|
||||||
.where("id", "=", row.id)
|
.where("id", "=", row.id)
|
||||||
.execute();
|
.execute();
|
||||||
} else {
|
} else {
|
||||||
await trx
|
await trx
|
||||||
.updateTable("download_task")
|
.updateTable("download_task")
|
||||||
.set({ status: "removed", removed_at: now, last_synced_at: now })
|
.set({ status: "removed", removed_at: now, last_synced_at: now, missing_count: row.missing_count + 1 })
|
||||||
.where("id", "=", row.id)
|
.where("id", "=", row.id)
|
||||||
.execute();
|
.execute();
|
||||||
await emitStatus(d, trx, row.id, row.status, "removed", null);
|
await emitStatus(d, trx, row.id, row.status, "removed", null);
|
||||||
@@ -892,6 +902,8 @@ async function observeLocked(
|
|||||||
last_progress_at: lastProgressAt,
|
last_progress_at: lastProgressAt,
|
||||||
no_progress_since: noProgressSince,
|
no_progress_since: noProgressSince,
|
||||||
last_synced_at: now,
|
last_synced_at: now,
|
||||||
|
missing_since: null,
|
||||||
|
missing_count: 0,
|
||||||
...(becameComplete ? { completed_at: now } : {}),
|
...(becameComplete ? { completed_at: now } : {}),
|
||||||
})
|
})
|
||||||
.where("id", "=", row.id)
|
.where("id", "=", row.id)
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import { createQbClient } from "@umt/adapters";
|
|||||||
import { createMockQb, createTempTree, type MockQb, makeTorrent, type TempTree } from "@umt/testkit";
|
import { createMockQb, createTempTree, type MockQb, makeTorrent, type TempTree } from "@umt/testkit";
|
||||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||||
import { evaluateAcquisition } from "../../src/modules/acquisition-organize/services/acquisition.ts";
|
import { evaluateAcquisition } from "../../src/modules/acquisition-organize/services/acquisition.ts";
|
||||||
import { submitDownload, syncDownloads } from "../../src/modules/acquisition-organize/services/downloads.ts";
|
import {
|
||||||
|
MISSING_CONFIRM_ROUNDS,
|
||||||
|
submitDownload,
|
||||||
|
syncDownloads,
|
||||||
|
} from "../../src/modules/acquisition-organize/services/downloads.ts";
|
||||||
import { executeRecord, planImport } from "../../src/modules/acquisition-organize/services/organize.ts";
|
import { executeRecord, planImport } from "../../src/modules/acquisition-organize/services/organize.ts";
|
||||||
import {
|
import {
|
||||||
applyEpisodeTotal,
|
applyEpisodeTotal,
|
||||||
@@ -159,7 +163,7 @@ describe.skipIf(!hasPg)("订阅增强(真实 PG + mock qB + 临时目录)", () =
|
|||||||
// 用户在 qB 里删掉 E02:确认移除 → 计划失败释放单元 → Slot 到期重评 → 延后的 E02 另一版本得到计划
|
// 用户在 qB 里删掉 E02:确认移除 → 计划失败释放单元 → Slot 到期重评 → 延后的 E02 另一版本得到计划
|
||||||
const qc = createQbClient({ instanceId: "t", baseUrl: qb.url, username: "admin", password: "adminadmin" });
|
const qc = createQbClient({ instanceId: "t", baseUrl: qb.url, username: "admin", password: "adminadmin" });
|
||||||
await qc.delete([e2.hash], { deleteFiles: false });
|
await qc.delete([e2.hash], { deleteFiles: false });
|
||||||
await syncDownloads(h.d);
|
for (let i = 0; i < MISSING_CONFIRM_ROUNDS; i++) await syncDownloads(h.d);
|
||||||
expect(await planRow(p2)).toMatchObject({ status: "failed", status_reason: "download_removed" });
|
expect(await planRow(p2)).toMatchObject({ status: "failed", status_reason: "download_removed" });
|
||||||
expect((await slotRow(slotId)).status).toBe("attention");
|
expect((await slotRow(slotId)).status).toBe("attention");
|
||||||
h.engine.enqueued.length = 0;
|
h.engine.enqueued.length = 0;
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ describe.skipIf(!hasPg)("E15 Agent 质量(fake 模型 + 真实能力)", () => {
|
|||||||
name: "Dune.Part.Two.2024.1080p.BluRay.x264-GRP",
|
name: "Dune.Part.Two.2024.1080p.BluRay.x264-GRP",
|
||||||
status: "failed",
|
status: "failed",
|
||||||
last_error: JSON.stringify({ message: "qB 状态 missingFiles" }),
|
last_error: JSON.stringify({ message: "qB 状态 missingFiles" }),
|
||||||
|
// 已连续缺席两轮:下一轮同步即确认移除(多轮缺席才判移除)
|
||||||
|
missing_count: 2,
|
||||||
})
|
})
|
||||||
.returning("id")
|
.returning("id")
|
||||||
.executeTakeFirstOrThrow()
|
.executeTakeFirstOrThrow()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createQbClient } from "@umt/adapters";
|
|||||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||||
import type { createAcquisitionOrganizeModule } from "../../src/modules/acquisition-organize/index.ts";
|
import type { createAcquisitionOrganizeModule } from "../../src/modules/acquisition-organize/index.ts";
|
||||||
import { evaluateAcquisition } from "../../src/modules/acquisition-organize/services/acquisition.ts";
|
import { evaluateAcquisition } from "../../src/modules/acquisition-organize/services/acquisition.ts";
|
||||||
import { syncDownloads } from "../../src/modules/acquisition-organize/services/downloads.ts";
|
import { MISSING_CONFIRM_ROUNDS, syncDownloads } from "../../src/modules/acquisition-organize/services/downloads.ts";
|
||||||
import { torrentFor } from "./fixtures/modules.ts";
|
import { torrentFor } from "./fixtures/modules.ts";
|
||||||
import { createE2E, type E2E, pgAvailable, seedBase, waitFor } from "./harness.ts";
|
import { createE2E, type E2E, pgAvailable, seedBase, waitFor } from "./harness.ts";
|
||||||
|
|
||||||
@@ -206,7 +206,8 @@ describe.skipIf(!hasPg)("E8 AT24 标签移除 / 任务消失", () => {
|
|||||||
});
|
});
|
||||||
await ext.delete([ids.D.hash], { deleteFiles: false });
|
await ext.delete([ids.D.hash], { deleteFiles: false });
|
||||||
const externalDeletes = e.qb.callsTo("torrents/delete").length;
|
const externalDeletes = e.qb.callsTo("torrents/delete").length;
|
||||||
await syncDownloads(deps());
|
// 连续多轮查不到才确认移除
|
||||||
|
for (let i = 0; i < MISSING_CONFIRM_ROUNDS; i++) await syncDownloads(deps());
|
||||||
const row = await dt("D");
|
const row = await dt("D");
|
||||||
expect(row.status).toBe("removed");
|
expect(row.status).toBe("removed");
|
||||||
expect(row.removed_at).not.toBeNull();
|
expect(row.removed_at).not.toBeNull();
|
||||||
|
|||||||
Reference in new issue
Block a user