feat: queue device import review tasks

This commit is contained in:
kris
2026-03-31 22:38:57 +08:00
parent dcbff3cc7d
commit 87ffe19f78
7 changed files with 347 additions and 117 deletions

View File

@@ -2,12 +2,13 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { useRouter } from "next/navigation";
import type { DeviceImportDraft, DeviceImportResolution } from "@/lib/boss-data";
import type { DeviceImportDraft, DeviceImportResolution, MasterAgentTask } from "@/lib/boss-data";
type ImportDraftResponse = {
ok: boolean;
draft?: DeviceImportDraft | null;
resolution?: DeviceImportResolution | null;
reviewTask?: MasterAgentTask | null;
message?: string;
};
@@ -30,6 +31,17 @@ export type DeviceImportDraftViewCopy = {
appliedProjectNames: string[];
};
function isDeviceImportReviewPending(
draft: DeviceImportDraft | null,
resolution: DeviceImportResolution | null,
reviewTask: MasterAgentTask | null,
) {
if (!draft || draft.status !== "pending_resolution" || resolution) {
return false;
}
return reviewTask?.status === "queued" || reviewTask?.status === "running";
}
function groupCandidates(draft: DeviceImportDraft | null) {
const groups = new Map<string, Array<DeviceImportDraft["candidates"][number]>>();
for (const candidate of draft?.candidates ?? []) {
@@ -52,6 +64,7 @@ function joinProjectNames(projectNames: string[]) {
export function describeDeviceImportDraft(
draft: DeviceImportDraft | null,
resolution: DeviceImportResolution | null,
reviewTask: MasterAgentTask | null = null,
): DeviceImportDraftViewCopy {
const candidateCount = draft?.candidates.length ?? 0;
const selectedCount = draft?.selectedCandidateIds.length ?? 0;
@@ -90,10 +103,21 @@ export function describeDeviceImportDraft(
statusBody = "先勾选想导入的线程,再生成导入建议。";
break;
case "pending_resolution":
statusTitle = "建议生成中";
statusBody = "勾选已保存,接下来会生成导入建议。";
if (isDeviceImportReviewPending(draft, resolution, reviewTask)) {
statusTitle = "主 Agent 审核中";
statusBody = "勾选已保存,主 Agent 正在整理导入建议,页面会自动刷新。";
} else if (reviewTask?.status === "failed") {
statusTitle = "建议生成失败";
statusBody = "主 Agent 这次没能生成导入建议。可以稍后重新生成,当前勾选会保留。";
} else {
statusTitle = "建议生成中";
statusBody = "勾选已保存,接下来会生成导入建议。";
}
resultTitle = "导入建议";
resultBody = "导入建议生成后,会先显示每个线程的处理方式和原因。";
resultBody =
reviewTask?.status === "failed"
? "导入建议生成失败后,可以直接重新生成,不需要重新勾选。"
: "导入建议生成后,会先显示每个线程的处理方式和原因。";
break;
case "resolved":
statusTitle = "建议已生成";
@@ -149,6 +173,7 @@ export function DeviceImportDraftManager({
const [feedback, setFeedback] = useState<Feedback | null>(null);
const [draft, setDraft] = useState<DeviceImportDraft | null>(null);
const [resolution, setResolution] = useState<DeviceImportResolution | null>(null);
const [reviewTask, setReviewTask] = useState<MasterAgentTask | null>(null);
const [selectedCandidateIds, setSelectedCandidateIds] = useState<string[]>([]);
const loadDraft = useCallback(async () => {
@@ -158,6 +183,7 @@ export function DeviceImportDraftManager({
const data = (await response.json()) as ImportDraftResponse;
setDraft(data.draft ?? null);
setResolution(data.resolution ?? null);
setReviewTask(data.reviewTask ?? null);
setSelectedCandidateIds(data.draft?.selectedCandidateIds ?? []);
setFeedback(
data.ok
@@ -181,8 +207,21 @@ export function DeviceImportDraftManager({
return () => window.clearTimeout(timer);
}, [loadDraft]);
useEffect(() => {
if (!isDeviceImportReviewPending(draft, resolution, reviewTask)) {
return;
}
const timer = window.setTimeout(() => {
void loadDraft();
}, 2000);
return () => window.clearTimeout(timer);
}, [draft, resolution, reviewTask, loadDraft]);
const groups = useMemo(() => groupCandidates(draft), [draft]);
const copy = useMemo(() => describeDeviceImportDraft(draft, resolution), [draft, resolution]);
const copy = useMemo(
() => describeDeviceImportDraft(draft, resolution, reviewTask),
[draft, resolution, reviewTask],
);
function toggle(candidateId: string) {
setSelectedCandidateIds((current) =>
@@ -211,6 +250,7 @@ export function DeviceImportDraftManager({
}
setDraft(result.draft ?? draft);
setResolution(null);
setReviewTask(null);
setSelectedCandidateIds([]);
setFeedback({ tone: "success", text: "已清空当前勾选,你可以重新选择要导入的线程。" });
} catch (error) {
@@ -242,6 +282,7 @@ export function DeviceImportDraftManager({
}
setDraft(selectResult.draft ?? draft);
setResolution(null);
setReviewTask(null);
setSelectedCandidateIds(selectResult.draft?.selectedCandidateIds ?? selectedCandidateIds);
const reviewResponse = await fetch(`/api/v1/devices/${deviceId}/import-draft/review`, {
@@ -254,6 +295,8 @@ export function DeviceImportDraftManager({
message?: string;
draft?: DeviceImportDraft;
resolution?: DeviceImportResolution;
reviewTask?: MasterAgentTask;
task?: MasterAgentTask;
};
if (!reviewResult.ok) {
setFeedback({ tone: "error", text: reviewResult.message ?? "导入建议生成失败,已保留当前勾选。" });
@@ -261,12 +304,18 @@ export function DeviceImportDraftManager({
}
setDraft(reviewResult.draft ?? selectResult.draft ?? null);
setResolution(reviewResult.resolution ?? null);
setReviewTask(reviewResult.reviewTask ?? reviewResult.task ?? null);
setSelectedCandidateIds(
reviewResult.draft?.selectedCandidateIds ??
selectResult.draft?.selectedCandidateIds ??
selectedCandidateIds,
);
setFeedback({ tone: "success", text: "已生成导入建议,先看推荐理由再应用导入。" });
setFeedback({
tone: reviewResult.resolution ? "success" : "info",
text: reviewResult.resolution
? "已生成导入建议,先看推荐理由再应用导入。"
: "已提交给主 Agent 审核,建议生成后会自动刷新。",
});
} catch (error) {
setFeedback({
tone: "error",
@@ -297,6 +346,7 @@ export function DeviceImportDraftManager({
}
setDraft(result.draft ?? draft);
setResolution(result.resolution ?? resolution);
setReviewTask(null);
setSelectedCandidateIds(result.draft?.selectedCandidateIds ?? draft?.selectedCandidateIds ?? []);
setFeedback({
tone: "success",
@@ -353,6 +403,12 @@ export function DeviceImportDraftManager({
{draft.status}
<br />
{selectedCandidateIds.length}
{reviewTask ? (
<>
<br />
{reviewTask.status}
</>
) : null}
{draft.status === "pending_candidates" ? (
<>
<br />
@@ -451,10 +507,14 @@ export function DeviceImportDraftManager({
<button
type="button"
onClick={() => void reviewSelection()}
disabled={loading || selectedCandidateIds.length === 0}
disabled={loading || selectedCandidateIds.length === 0 || isDeviceImportReviewPending(draft, resolution, reviewTask)}
className="rounded-full bg-[#07C160] px-4 py-2 text-[13px] font-semibold text-white disabled:bg-[#B7E6C9]"
>
{draft?.status === "resolved" || draft?.status === "applied" ? "重新生成导入建议" : "生成导入建议"}
{isDeviceImportReviewPending(draft, resolution, reviewTask)
? "主 Agent 审核中"
: draft?.status === "resolved" || draft?.status === "applied" || reviewTask?.status === "failed"
? "重新生成导入建议"
: "生成导入建议"}
</button>
<button
type="button"