feat(控制台): 去掉待处理中心,问题回到发生处
- 侧栏与命令面板没有「待处理」;/console/inbox?kind= 按问题类型跳到整理记录「需要处理」、下载面板、运行状态或订阅 - 整理记录加「需要处理」筛选,行内写原因与推荐动作(指定作品 / 重新整理 / 让智能体识别 / 替换·保留两者·跳过 / 忽略), 详情页同样可处理;处理按钮从原待处理页抽成 components/decisions/decision-actions.tsx 复用 - 铃铛只放等你批准的智能体操作与系统健康;首页「要我处理」与运行状态「需要关注」放汇总计数,运行状态加智能体用量 - 设置 → 智能体:预算卡改为「用量与限制」(只剩单次工具调用与时长,附花费统计),自动调查卡加「整理识别不到作品时叫智能体识别」 - 集数待确认的原因显示在作品页集数映射(可确认建议规则 / 智能体建议)与订阅详情;未识别资源在媒体库「未识别」里直接选候选或指定 - 下载、站点、任务里指向待处理的链接改到对应页面 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
1 parent
1c1a954703
commit
fa76d7563c
36 files changed
+1209
-921
No files matched your search
@@ -0,0 +1,107 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import type { PublicOrganizationRecord, RecordAttention } from "@umt/contracts";
|
||||
import { createElement, type ReactNode } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { MemoryRouter } from "react-router";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { attentionLine, primaryAction, RecordAttentionActions } from "./record-attention.tsx";
|
||||
|
||||
// 整理记录「需要处理」的行内处理(2026-10-06 去掉待处理中心):原因一句话 + 推荐动作,一键执行。
|
||||
|
||||
const D = "11111111-1111-4111-8111-111111111111";
|
||||
const R = "22222222-2222-4222-8222-222222222222";
|
||||
|
||||
const render = (node: ReactNode) =>
|
||||
renderToStaticMarkup(
|
||||
createElement(QueryClientProvider, { client: new QueryClient() }, createElement(MemoryRouter, null, node)),
|
||||
);
|
||||
|
||||
const decision = (over: Partial<NonNullable<RecordAttention["decision"]>> = {}) => ({
|
||||
id: D,
|
||||
kind: "identity_ambiguous" as const,
|
||||
status: "open" as const,
|
||||
title: "没识别出作品:Foo.2019.1080p",
|
||||
summary: null,
|
||||
options: [
|
||||
{ key: "candidate_0", label: "Foo (2019)", input: { candidate: { mediaEntityId: R } } },
|
||||
{ key: "select_entity", label: "选择作品(本地)", input: { requires: "mediaEntityId" } },
|
||||
{ key: "select_tmdb", label: "选择作品(TMDB)", input: { requires: "tmdb" } },
|
||||
{ key: "provisional", label: "作为临时作品入库", input: {} },
|
||||
{ key: "dismiss", label: "忽略" },
|
||||
],
|
||||
agentRunId: null,
|
||||
evidence: { downloadTaskId: R, torrentName: "Foo.2019.1080p" },
|
||||
...over,
|
||||
});
|
||||
|
||||
type Rec = Pick<PublicOrganizationRecord, "id" | "action" | "status" | "attention" | "reason">;
|
||||
const rec = (attention: RecordAttention | null, over: Partial<Rec> = {}): Rec => ({
|
||||
id: R,
|
||||
action: "copy",
|
||||
status: "failed",
|
||||
reason: null,
|
||||
attention,
|
||||
...over,
|
||||
});
|
||||
const html = (r: Rec) => render(createElement(RecordAttentionActions, { r, onReorganize: () => {} }));
|
||||
|
||||
describe("整理记录行内处理", () => {
|
||||
it("没识别出作品:主操作指定作品,可让智能体识别、重新整理、忽略", () => {
|
||||
const r = rec({ topic: "identity", decision: decision(), agentIdentify: true });
|
||||
expect(primaryAction(r)).toBe("assign");
|
||||
expect(attentionLine(r)).toBe("没识别出作品。指定作品,或让智能体识别");
|
||||
const out = html(r);
|
||||
for (const t of ["指定作品", "让智能体识别", "重新整理", "忽略", "Foo (2019)"]) expect(out).toContain(t);
|
||||
// 「选择作品(本地 / TMDB)」并进「指定作品」,不单列;临时作品不在行内
|
||||
expect(out).not.toContain("选择作品(本地)");
|
||||
expect(out).not.toContain("作为临时作品入库");
|
||||
});
|
||||
|
||||
it("智能体识别中:显示进度与会话入口,不再给指定 / 识别按钮", () => {
|
||||
const r = rec({
|
||||
topic: "identity",
|
||||
decision: decision({ status: "investigating", agentRunId: D }),
|
||||
agentIdentify: true,
|
||||
});
|
||||
expect(attentionLine(r)).toContain("智能体正在识别作品");
|
||||
const out = html(r);
|
||||
expect(out).toContain("智能体识别中");
|
||||
expect(out).toContain(`/console/agent/${D}`);
|
||||
expect(out).not.toContain("让智能体识别");
|
||||
expect(out).not.toContain("指定作品");
|
||||
});
|
||||
|
||||
it("目标已存在:按事项选项给替换 / 保留两者 / 跳过", () => {
|
||||
const r = rec({
|
||||
topic: "conflict",
|
||||
decision: decision({
|
||||
kind: "path_conflict",
|
||||
summary: "媒体库里已有同名文件",
|
||||
options: [
|
||||
{ key: "replace", label: "替换" },
|
||||
{ key: "keep_both", label: "保留两者" },
|
||||
{ key: "skip", label: "跳过" },
|
||||
],
|
||||
}),
|
||||
agentIdentify: false,
|
||||
});
|
||||
expect(primaryAction(r)).toBe("options");
|
||||
expect(attentionLine(r)).toBe("媒体库里已有同名文件");
|
||||
const out = html(r);
|
||||
for (const t of ["替换", "保留两者", "跳过", "忽略"]) expect(out).toContain(t);
|
||||
expect(out).not.toContain("让智能体识别");
|
||||
});
|
||||
|
||||
it("其它失败:重新整理为主操作", () => {
|
||||
const r = rec({ topic: "other", decision: null, agentIdentify: false });
|
||||
expect(primaryAction(r)).toBe("reorganize");
|
||||
const out = html(r);
|
||||
expect(out).toContain("重新整理");
|
||||
expect(out).toContain("忽略");
|
||||
});
|
||||
|
||||
it("不需要处理的记录不渲染", () => {
|
||||
expect(html(rec(null, { status: "succeeded" }))).toBe("");
|
||||
expect(attentionLine(rec(null))).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,130 @@
|
||||
import type { PublicOrganizationRecord } from "@umt/contracts";
|
||||
import { BellOff, Bot, ChevronRight, LoaderCircle, Wand2 } from "lucide-react";
|
||||
import { Link } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { useAgentIdentifyRecord, useDismissRecordAttention } from "~/api/records.ts";
|
||||
import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx";
|
||||
import { consolePath } from "~/components/console/nav.ts";
|
||||
import {
|
||||
AssignWorkButton,
|
||||
canAssignWork,
|
||||
OptionButton,
|
||||
plainOptions,
|
||||
} from "~/components/decisions/decision-actions.tsx";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { humanizeReason } from "~/lib/reasons.ts";
|
||||
import { canReorganize } from "./record-parts.ts";
|
||||
|
||||
// 整理记录「需要处理」的行内处理(2026-10-06 去掉待处理中心后,整理侧的问题在这里就地处理):
|
||||
// 没识别出作品 → 指定作品(主操作)/ 让智能体识别 / 忽略;识别中显示进度与会话入口;
|
||||
// 目标已存在 / 路径冲突 → 事项给的选项(替换 / 保留两者 / 跳过);
|
||||
// 其余失败 / 部分完成 → 重新整理 / 忽略。执行都走已有端点(/decisions/{id}/resolve、整理向导、忽略)。
|
||||
|
||||
type Rec = Pick<PublicOrganizationRecord, "id" | "action" | "status" | "attention" | "reason">;
|
||||
|
||||
/** 一句话说清这条记录卡在哪(事项摘要优先,其次记录原因) */
|
||||
export function attentionLine(r: Rec): string | null {
|
||||
const a = r.attention;
|
||||
if (!a) return null;
|
||||
if (a.decision?.status === "investigating") return "智能体正在识别作品,认出来会自动重新整理";
|
||||
if (a.topic === "identity") return "没识别出作品。指定作品,或让智能体识别";
|
||||
return a.decision?.summary ? humanizeReason(a.decision.summary) : null;
|
||||
}
|
||||
|
||||
/** 推荐的主动作(给列表文字提示与测试用):identity → 指定作品;conflict → 事项选项;other → 重新整理 */
|
||||
export function primaryAction(r: Rec): "assign" | "options" | "reorganize" | null {
|
||||
const a = r.attention;
|
||||
if (!a) return null;
|
||||
if (a.topic === "identity" && a.decision && canAssignWork(a.decision)) return "assign";
|
||||
if (a.topic === "conflict" && a.decision && plainOptions(a.decision).length) return "options";
|
||||
return canReorganize(r) ? "reorganize" : null;
|
||||
}
|
||||
|
||||
export function RecordAttentionActions({
|
||||
r,
|
||||
onReorganize,
|
||||
compact = false,
|
||||
}: {
|
||||
r: Rec;
|
||||
onReorganize: () => void;
|
||||
compact?: boolean;
|
||||
}) {
|
||||
const identify = useAgentIdentifyRecord();
|
||||
const dismiss = useDismissRecordAttention();
|
||||
const a = r.attention;
|
||||
if (!a) return null;
|
||||
const d = a.decision;
|
||||
const investigating = d?.status === "investigating";
|
||||
const primary = primaryAction(r);
|
||||
return (
|
||||
// 行点击会进详情;按钮区不冒泡
|
||||
// biome-ignore lint/a11y/noStaticElementInteractions: 只拦截冒泡,不是交互元素
|
||||
// biome-ignore lint/a11y/useKeyWithClickEvents: 同上
|
||||
<div className="flex flex-wrap items-center gap-1.5" onClick={(e) => e.stopPropagation()}>
|
||||
{investigating && d?.agentRunId ? (
|
||||
<Button asChild variant="ghost" size="sm">
|
||||
<Link to={consolePath("agent", d.agentRunId)}>
|
||||
<LoaderCircle className="animate-spin" />
|
||||
智能体识别中
|
||||
<ChevronRight />
|
||||
</Link>
|
||||
</Button>
|
||||
) : null}
|
||||
{primary === "assign" && d && !investigating ? <AssignWorkButton d={d} /> : null}
|
||||
{primary === "options" && d
|
||||
? plainOptions(d).map((o, i) => (
|
||||
<OptionButton key={o.key} d={d} option={o} variant={i === 0 ? "default" : "outline"} />
|
||||
))
|
||||
: null}
|
||||
{a.topic === "identity" && d && !investigating
|
||||
? plainOptions(d)
|
||||
.filter((o) => !compact && o.key !== "provisional")
|
||||
.slice(0, 2)
|
||||
.map((o) => <OptionButton key={o.key} d={d} option={o} />)
|
||||
: null}
|
||||
{canReorganize(r) && !investigating && (!compact || primary === "reorganize") ? (
|
||||
<Button size="sm" variant={primary === "reorganize" ? "default" : "outline"} onClick={onReorganize}>
|
||||
<Wand2 />
|
||||
重新整理
|
||||
</Button>
|
||||
) : null}
|
||||
{a.agentIdentify && !investigating ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={identify.isPending}
|
||||
onClick={() =>
|
||||
identify.mutateAsync(r.id).then(
|
||||
(x) => toast.success(x.queued ? "已让智能体识别,认出来会自动重新整理" : "智能体已经在识别了"),
|
||||
(err) => toast.error(errorMessage(err, "操作失败")),
|
||||
)
|
||||
}
|
||||
>
|
||||
<Bot />
|
||||
让智能体识别
|
||||
</Button>
|
||||
) : null}
|
||||
<ConfirmDialog
|
||||
trigger={
|
||||
<Button variant="ghost" size="sm">
|
||||
<BellOff />
|
||||
忽略
|
||||
</Button>
|
||||
}
|
||||
title="忽略这条记录的问题?"
|
||||
description="不再算需要处理,文件和记录都不动;之后可以随时重新整理。"
|
||||
confirmLabel="忽略"
|
||||
onConfirm={() =>
|
||||
dismiss.mutateAsync(r.id).then(
|
||||
() => toast.success("已忽略"),
|
||||
(err) => {
|
||||
toast.error(errorMessage(err, "操作失败"));
|
||||
throw err;
|
||||
},
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user