import { createHash } from "node:crypto"; import type { FakeScript } from "@umt/agent"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { createE2E, type E2E, pgAvailable, runEvents, seedBase, waitFor } from "./harness.ts"; // 整理识别不到作品时自动叫智能体识别(2026-10-06,agent.autoIdentifyOrganize 缺省开): // 用户手动加进 qB 的任务(贴管理标签)下完 → 整理按文件名认不出 → 整理记录失败 + 内部事项 organize_identity // → decision.created → resolveDecision → 识别会话(白名单只有查询类 + organize.assign_identity,不需要持续授权) // → 智能体查到本地作品后 organize.assign_identity → 按该作品自动重新整理入库,事项关闭,记录不再需要处理。 const hasPg = await pgAvailable(); /** 识别会话目标里带着事项 id(脚本从目标里取) */ const cur: { decisionId?: string; entityId?: string } = {}; const identifyScript: FakeScript = { name: "organize-identify", match: (goal) => { const m = /待处理事项 ([0-9a-f-]{36})/.exec(goal); if (m && goal.includes("识别整理记录里的作品")) cur.decisionId = m[1]; return goal.includes("识别整理记录里的作品"); }, steps: [ { text: "先看媒体库里有没有这部。", toolCalls: [{ capability: "inventory.query", input: { query: "Silent Meridian" } }], }, { text: "标题、年份都对得上,指定为媒体库里的《Silent Meridian》。", toolCalls: [ { capability: "organize.assign_identity", input: () => ({ decisionId: cur.decisionId ?? "", mediaEntityId: cur.entityId ?? "", reasons: ["种子名缩写 Smrd 对应 Silent Meridian,年份 2018 一致"], }), }, ], }, ], finalText: "已指定作品并重新整理。", }; describe.skipIf(!hasPg)("整理识别不到作品 → 智能体识别 → 自动重新整理", () => { let e: E2E; beforeAll(async () => { e = await createE2E("orgid", { fakeScripts: [identifyScript] }); await seedBase(e); const ent = await e.db .insertInto("media_entity") .values({ kind: "movie", title: "Silent Meridian", year: 2018, status: "active" }) .returning("id") .executeTakeFirstOrThrow(); cur.entityId = ent.id; await e.settings({ retention: { seedRetentionDays: 0 } }); await e.startWorker(); }, 120_000); afterAll(async () => { await e?.close(); }, 60_000); it("识别失败自动派一次识别会话;指定作品后自动重新整理入库,事项关闭,记录不再需要处理", async () => { const name = "Smrd.2018.1080p.BluRay.x264-QQQ"; const hash = createHash("sha1").update("orgid-1").digest("hex"); e.qb.seed({ hash, name, size: 400_000, progress: 1, tags: ["umt"], save_path: e.cache.root }); await e.qb.complete(hash, { files: [{ path: `${name}/${name}.mkv`, size: 400_000 }], writeFiles: true }); const dec = await waitFor( () => e.db.selectFrom("decision").selectAll().where("dedupe_key", "like", "organize_identity:%").executeTakeFirst(), 60_000, "整理识别不到 → organize_identity 事项", ); const run = await waitFor( () => e.db.selectFrom("agent_run").selectAll().where("dedupe_key", "=", `investigate:${dec.id}`).executeTakeFirst(), 60_000, "识别会话", ); expect(run).toMatchObject({ trigger: "event", mode: "execute", created_by: "system:organize-identify" }); const done = await waitFor( async () => { const r = await e.db.selectFrom("agent_run").selectAll().where("id", "=", run.id).executeTakeFirstOrThrow(); return r.finished_at ? r : null; }, 120_000, "识别会话结束", ); const starts = (await runEvents(e, run.id)) .filter((x) => x.type === "tool_call_start") .map((x) => (x.payload as { capabilityId: string }).capabilityId); expect(starts).toEqual(["inventory.query", "organize.assign_identity"]); expect(done.status).toBe("succeeded"); expect(done.summary).toContain("已恢复"); const closed = await e.db.selectFrom("decision").selectAll().where("id", "=", dec.id).executeTakeFirstOrThrow(); expect(closed.status).toBe("resolved"); // 自动重新整理:同一下载任务的新整理记录成功入库,作品是智能体指定的 const rec = await waitFor( async () => { const r = await e.db .selectFrom("organization_record") .selectAll() .where("status", "=", "succeeded") .where("media_entity_id", "=", cur.entityId!) .executeTakeFirst(); return r ?? null; }, 90_000, "重新整理入库", ); expect(rec.download_task_id).toBeTruthy(); const attention = await e.api("GET", "/organization-records?attention=true"); expect(attention.body.data.items).toEqual([]); }, 300_000); });