feat: queue device import review tasks
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -5035,6 +5035,7 @@ export async function completeMasterAgentTask(payload: {
|
||||
draftId: draft.draftId,
|
||||
});
|
||||
}
|
||||
publishBossEvent("devices.updated", { deviceId: draft.deviceId });
|
||||
} else if (task.taskType === "dispatch_execution") {
|
||||
if (!task.dispatchExecutionId || !task.targetProjectId || !task.targetThreadId) {
|
||||
throw new Error("MASTER_AGENT_DISPATCH_EXECUTION_CONTEXT_REQUIRED");
|
||||
@@ -5929,7 +5930,14 @@ export async function getLatestDeviceImportDraft(deviceId: string) {
|
||||
const resolution = draft?.resolutionId
|
||||
? state.deviceImportResolutions.find((item) => item.resolutionId === draft.resolutionId) ?? null
|
||||
: state.deviceImportResolutions.find((item) => item.deviceId === deviceId) ?? null;
|
||||
return { draft, resolution };
|
||||
const reviewTask = draft
|
||||
? state.masterAgentTasks.find(
|
||||
(item) =>
|
||||
item.taskType === "device_import_resolution" &&
|
||||
item.deviceImportDraftId === draft.draftId,
|
||||
) ?? null
|
||||
: null;
|
||||
return { draft, resolution, reviewTask };
|
||||
}
|
||||
|
||||
export async function previewDeviceImportResolution(input: { deviceId: string }) {
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
getRuntimeAiAccountById,
|
||||
getMasterAgentRuntimeAccount,
|
||||
getMasterAgentTask,
|
||||
previewDeviceImportResolution,
|
||||
queueMasterAgentTask,
|
||||
readState,
|
||||
isDispatchableThreadProject,
|
||||
@@ -798,87 +797,6 @@ function buildDeviceImportResolutionPrompt(params: {
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
type DeviceImportResolutionTaskResult =
|
||||
| {
|
||||
ok: true;
|
||||
taskId: string;
|
||||
status: "completed";
|
||||
draft: NonNullable<Awaited<ReturnType<typeof getLatestDeviceImportDraft>>["draft"]>;
|
||||
resolution: NonNullable<Awaited<ReturnType<typeof getLatestDeviceImportDraft>>["resolution"]>;
|
||||
}
|
||||
| {
|
||||
ok: false;
|
||||
taskId: string;
|
||||
status: "failed";
|
||||
draft: Awaited<ReturnType<typeof getLatestDeviceImportDraft>>["draft"];
|
||||
resolution: Awaited<ReturnType<typeof getLatestDeviceImportDraft>>["resolution"];
|
||||
error: string;
|
||||
};
|
||||
|
||||
async function resolveDeviceImportResolutionTask(taskId: string): Promise<DeviceImportResolutionTaskResult> {
|
||||
const task = await getMasterAgentTask(taskId);
|
||||
if (!task) {
|
||||
throw new Error("MASTER_AGENT_TASK_NOT_FOUND");
|
||||
}
|
||||
if (task.taskType !== "device_import_resolution" || !task.deviceImportDraftId) {
|
||||
throw new Error("MASTER_AGENT_TASK_TYPE_INVALID");
|
||||
}
|
||||
|
||||
const draftRecord = await readState();
|
||||
const draft = draftRecord.deviceImportDrafts.find((item) => item.draftId === task.deviceImportDraftId);
|
||||
if (!draft) {
|
||||
throw new Error("DEVICE_IMPORT_DRAFT_NOT_FOUND");
|
||||
}
|
||||
|
||||
try {
|
||||
const proposal = await previewDeviceImportResolution({ deviceId: draft.deviceId });
|
||||
await completeMasterAgentTask({
|
||||
taskId: task.taskId,
|
||||
deviceId: task.deviceId,
|
||||
status: "completed",
|
||||
replyBody: JSON.stringify(
|
||||
{
|
||||
summary: proposal.summary,
|
||||
items: proposal.items.map((item) => ({
|
||||
candidateId: item.candidateId,
|
||||
action: item.action,
|
||||
targetProjectId: item.targetProjectId,
|
||||
reason: item.reason,
|
||||
})),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
});
|
||||
|
||||
const latest = await getLatestDeviceImportDraft(draft.deviceId);
|
||||
return {
|
||||
ok: true as const,
|
||||
taskId: task.taskId,
|
||||
status: "completed" as const,
|
||||
draft: latest.draft!,
|
||||
resolution: latest.resolution!,
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "DEVICE_IMPORT_RESOLUTION_FAILED";
|
||||
await completeMasterAgentTask({
|
||||
taskId: task.taskId,
|
||||
deviceId: task.deviceId,
|
||||
status: "failed",
|
||||
errorMessage: message,
|
||||
});
|
||||
const latest = await getLatestDeviceImportDraft(draft.deviceId);
|
||||
return {
|
||||
ok: false as const,
|
||||
taskId: task.taskId,
|
||||
status: "failed" as const,
|
||||
draft: latest.draft,
|
||||
resolution: latest.resolution,
|
||||
error: message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function queueDeviceImportResolutionTask(params: {
|
||||
deviceId: string;
|
||||
reviewedBy: string;
|
||||
@@ -927,7 +845,20 @@ export async function queueDeviceImportResolutionTask(params: {
|
||||
deviceImportDraftId: draft.draftId,
|
||||
});
|
||||
|
||||
return resolveDeviceImportResolutionTask(task.taskId);
|
||||
const latest = await getLatestDeviceImportDraft(draft.deviceId);
|
||||
return {
|
||||
ok: true as const,
|
||||
taskId: task.taskId,
|
||||
task: {
|
||||
taskId: task.taskId,
|
||||
taskType: task.taskType,
|
||||
status: task.status,
|
||||
deviceId: task.deviceId,
|
||||
deviceImportDraftId: task.deviceImportDraftId,
|
||||
},
|
||||
draft: latest.draft ?? undefined,
|
||||
...(latest.resolution ? { resolution: latest.resolution } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
async function waitForMasterAgentTaskCompletion(taskId: string, timeoutMs = 55_000) {
|
||||
|
||||
Reference in New Issue
Block a user