- 整理记录「需要处理」:列表 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
122 lines
4.9 KiB
TypeScript
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);
|
|
});
|