Files
UHD_Media_Tool/apps/server/test/e2e/organize-identify.test.ts
T
rootandClaude Opus 5.5 1c1a954703 feat(整理): 问题在整理记录就地处理,识别失败自动叫智能体识别
- 整理记录「需要处理」:列表 attention 筛选与统计同口径(没被后来的整理取代,且有未关闭事项或失败 / 部分完成未忽略),
  记录带 attention(原因类别、精简事项、能否让智能体识别);忽略端点记 attention_cleared_at(迁移 0078)并关闭关联事项
- 识别失败(organize_identity)按 agent.autoIdentifyOrganize 自动派一次识别会话;新能力 organize.assign_identity
  指定作品后复用人工指定的钩子路径自动重新整理,只放行本事项的识别会话与用户对话;整理记录里可手动「让智能体识别」
- GET /attention:整理需要处理条数、等你批准的智能体操作、系统健康问题(铃铛 / 首页 / 运行状态共用)
- 原因码文案不再指向待处理;命令面板「待处理」换成「需要处理的整理记录」

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
2026-10-06 16:47:18 +08:00

122 lines
4.9 KiB
TypeScript

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);
});