fix: preserve investigation execution history by round
This commit is contained in:
@@ -149,6 +149,56 @@ it("references the selected report round instead of the latest task round, inclu
|
||||
});
|
||||
});
|
||||
|
||||
it("opens the investigation execution record separately from Agent collaboration", async () => {
|
||||
const onViewExecutionHistory = vi.fn(async () => undefined);
|
||||
taskPanel(taskBase, { onViewExecutionHistory });
|
||||
|
||||
const executionHistory = screen.getByRole("button", { name: "执行记录" });
|
||||
expect(executionHistory).toHaveAttribute("title", "在左侧 Agent 中打开所选轮次自动同步的调查执行记录,并可创建调试会话。");
|
||||
fireEvent.click(executionHistory);
|
||||
|
||||
await waitFor(() => expect(onViewExecutionHistory).toHaveBeenCalledWith({
|
||||
executionGeneration: 1,
|
||||
sessionId: "execution-session"
|
||||
}));
|
||||
});
|
||||
|
||||
it("opens the execution record bound to the selected report round", async () => {
|
||||
const currentReport = {
|
||||
...taskBase.latest_report!,
|
||||
revision: 2,
|
||||
execution_generation: 2,
|
||||
source_session_id: "execution-session-2"
|
||||
};
|
||||
const previousReport = {
|
||||
...taskBase.latest_report!,
|
||||
revision: 1,
|
||||
execution_generation: 1,
|
||||
source_session_id: "execution-session-1"
|
||||
};
|
||||
const onViewExecutionHistory = vi.fn(async () => undefined);
|
||||
taskPanel({
|
||||
...taskBase,
|
||||
execution_generation: 2,
|
||||
current_report_revision: 2,
|
||||
latest_report: currentReport
|
||||
}, {
|
||||
onViewExecutionHistory,
|
||||
onLoadReports: vi.fn(async () => ({ items: [currentReport, previousReport], next_offset: null }))
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "展开报告版本" }));
|
||||
fireEvent.click(await screen.findByRole("button", { name: /第 1 轮 · 报告第 1 版/ }));
|
||||
expect(screen.getByText("调查任务 · 第 1 轮", { exact: true })).toBeVisible();
|
||||
expect(screen.queryByText("调查任务 · 第 2 轮", { exact: true })).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("button", { name: "执行记录", hidden: true }));
|
||||
|
||||
await waitFor(() => expect(onViewExecutionHistory).toHaveBeenCalledWith({
|
||||
executionGeneration: 1,
|
||||
sessionId: "execution-session-1"
|
||||
}));
|
||||
});
|
||||
|
||||
describe("operational task approval and evidence", () => {
|
||||
it("shows the selected v2 route first and keeps unchosen work available", () => {
|
||||
const node = (id: string, title: string, status: "completed" | "waiting" | "skipped") => ({
|
||||
|
||||
@@ -31,6 +31,7 @@ import type {
|
||||
OperationalTaskPlan,
|
||||
OperationalTaskDataSnapshot
|
||||
} from "../api/operational-task-client";
|
||||
import type { TaskExecutionHistoryTarget } from "../api/operational-task-client";
|
||||
|
||||
type ExecutionNode = {
|
||||
id: string;
|
||||
@@ -65,6 +66,7 @@ type AgentExecutionPlanPanelProps = {
|
||||
onLocate?: (targets: import("../api/investigation-report").InvestigationMapTarget[]) => void;
|
||||
task: OperationalTask;
|
||||
onContinue: (discussion?: InvestigationDiscussion) => void;
|
||||
onViewExecutionHistory?: (target: TaskExecutionHistoryTarget, knownRunId?: string) => Promise<void> | void;
|
||||
onDecision: (actionId: string, expectedVersion: number, decision: OperationalTaskDecision, comment?: string) => Promise<unknown>;
|
||||
onExecute: (actionId: string, expectedVersion: number) => Promise<unknown>;
|
||||
onDraftUpdate?: (actionId: string, expectedVersion: number, payload: Record<string, unknown>) => Promise<unknown>;
|
||||
@@ -82,6 +84,7 @@ export function AgentExecutionPlanPanel({
|
||||
onLocate,
|
||||
task,
|
||||
onContinue,
|
||||
onViewExecutionHistory,
|
||||
onDecision,
|
||||
onExecute,
|
||||
onDraftUpdate,
|
||||
@@ -95,7 +98,8 @@ export function AgentExecutionPlanPanel({
|
||||
focusActionId
|
||||
}: AgentExecutionPlanPanelProps) {
|
||||
if (task.task_type === "event_investigation") return <InvestigationTaskReport key={task.task_id} task={task}
|
||||
onContinue={onContinue} onLoadReports={onLoadReports} onReportViewed={onReportViewed} focusActionId={focusActionId} onLocate={onLocate}
|
||||
onContinue={onContinue} onViewExecutionHistory={onViewExecutionHistory}
|
||||
onLoadReports={onLoadReports} onReportViewed={onReportViewed} focusActionId={focusActionId} onLocate={onLocate}
|
||||
renderAction={(action) => <HumanActionCard key={action.action_id} task={task} action={action} onDecision={onDecision}
|
||||
onExecute={onExecute} onDraftUpdate={onDraftUpdate} onConfirmDispatch={onConfirmDispatch} onReconcile={onReconcile} onRefresh={onRefresh} />}
|
||||
renderEvidence={(item) => <EvidenceDisclosure key={item.evidence_id} item={item} onLoadSnapshot={onLoadEvidenceSnapshot} />}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState, type ReactNode } from "react";
|
||||
import { MessageSquare } from "lucide-react";
|
||||
import { History, LoaderCircle, MessageSquare } from "lucide-react";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Disclosure } from "@/shared/ui/disclosure";
|
||||
import { StatusBadge } from "@/shared/ui/status";
|
||||
@@ -11,7 +11,8 @@ import type {
|
||||
OperationalTaskAction,
|
||||
OperationalTaskEvidence,
|
||||
OperationalTaskReport,
|
||||
OperationalTaskReportPage
|
||||
OperationalTaskReportPage,
|
||||
TaskExecutionHistoryTarget
|
||||
} from "../api/operational-task-client";
|
||||
import {
|
||||
investigationDiagram,
|
||||
@@ -24,11 +25,13 @@ import { useReportUpdateMotion } from "../hooks/use-report-update-motion";
|
||||
import { ExpandableReading } from "@/shared/ui/reading-workspace";
|
||||
import { InvestigationReportBlocks } from "./investigation-report-blocks";
|
||||
import { InvestigationReportNavigation, type InvestigationReportSection } from "./investigation-report-navigation";
|
||||
import { showMapNotice } from "@/features/map/core";
|
||||
|
||||
type ReportSelection = { snapshot: OperationalTaskReport; diagram: InvestigationDiagram | null };
|
||||
type Props = {
|
||||
task: OperationalTask;
|
||||
onContinue: (discussion?: InvestigationDiscussion) => Promise<boolean | void> | boolean | void;
|
||||
onViewExecutionHistory?: (target: TaskExecutionHistoryTarget, knownRunId?: string) => Promise<void> | void;
|
||||
renderAction: (action: OperationalTaskAction) => ReactNode;
|
||||
renderEvidence: (evidence: OperationalTaskEvidence) => ReactNode;
|
||||
onLoadReports?: (offset: number) => Promise<OperationalTaskReportPage>;
|
||||
@@ -126,6 +129,7 @@ function investigationTimeline(
|
||||
export function InvestigationTaskReport({
|
||||
task,
|
||||
onContinue,
|
||||
onViewExecutionHistory,
|
||||
renderAction,
|
||||
renderEvidence,
|
||||
onLoadReports,
|
||||
@@ -139,8 +143,24 @@ export function InvestigationTaskReport({
|
||||
const [historyOffset, setHistoryOffset] = useState<number | null>(0);
|
||||
const [historyLoading, setHistoryLoading] = useState(false);
|
||||
const [historyError, setHistoryError] = useState<string | null>(null);
|
||||
const [openingExecutionHistory, setOpeningExecutionHistory] = useState(false);
|
||||
const live = selection === null;
|
||||
const snapshot = selection?.snapshot ?? task.latest_report;
|
||||
const displayedGeneration = live ? task.execution_generation : snapshot?.execution_generation;
|
||||
const fallbackExecutionRun = task.latest_run ?? task.active_run;
|
||||
const executionHistoryTarget: TaskExecutionHistoryTarget | null = snapshot ? {
|
||||
executionGeneration: snapshot.execution_generation ?? null,
|
||||
sessionId: snapshot.source_session_id
|
||||
} : live && fallbackExecutionRun?.agent_session_id ? {
|
||||
executionGeneration: fallbackExecutionRun.generation,
|
||||
sessionId: fallbackExecutionRun.agent_session_id
|
||||
} : null;
|
||||
const knownExecutionRun = executionHistoryTarget
|
||||
? [task.active_run, task.latest_run].find((run) => run
|
||||
&& (executionHistoryTarget.executionGeneration == null
|
||||
|| run.generation === executionHistoryTarget.executionGeneration)
|
||||
&& run.agent_session_id === executionHistoryTarget.sessionId) ?? null
|
||||
: null;
|
||||
const report = snapshot?.report;
|
||||
const progress = task.latest_progress?.execution_generation === task.execution_generation ? task.latest_progress : null;
|
||||
// A current final report owns its archived graph. During investigation, a
|
||||
@@ -221,6 +241,25 @@ export function InvestigationTaskReport({
|
||||
setHistoryLoading(false);
|
||||
}
|
||||
};
|
||||
const openExecutionHistory = async () => {
|
||||
if (!onViewExecutionHistory || !executionHistoryTarget || openingExecutionHistory) return;
|
||||
setOpeningExecutionHistory(true);
|
||||
try {
|
||||
if (knownExecutionRun) {
|
||||
await onViewExecutionHistory(executionHistoryTarget, knownExecutionRun.run_id);
|
||||
} else {
|
||||
await onViewExecutionHistory(executionHistoryTarget);
|
||||
}
|
||||
} catch (error) {
|
||||
showMapNotice({
|
||||
tone: "error",
|
||||
title: "调查执行记录打开失败",
|
||||
message: error instanceof Error ? error.message : "请稍后重试。"
|
||||
});
|
||||
} finally {
|
||||
setOpeningExecutionHistory(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ExpandableReading title={snapshot?.report.title ?? task.title}
|
||||
@@ -229,7 +268,7 @@ export function InvestigationTaskReport({
|
||||
<header className="investigation-report-header">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs text-[var(--ink-muted)]">
|
||||
调查任务 · 第 {task.execution_generation} 轮
|
||||
{displayedGeneration == null ? "调查任务 · 历史轮次" : `调查任务 · 第 ${displayedGeneration} 轮`}
|
||||
</p>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-2">
|
||||
<h2 className="text-base font-semibold leading-6 text-[var(--ink)]">{task.title}</h2>
|
||||
@@ -247,17 +286,31 @@ export function InvestigationTaskReport({
|
||||
</div>
|
||||
<p className="mt-1 text-xs leading-5 text-[var(--ink-muted)]">{task.summary}</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
title="打开调查协作对话,引用此任务,不会直接发送。"
|
||||
onClick={() =>
|
||||
onContinue({ context: snapshot ? { report_revision: snapshot.revision } : {}, reportGeneration: snapshot?.execution_generation })
|
||||
}
|
||||
>
|
||||
<MessageSquare size={14} aria-hidden />
|
||||
Agent 协作
|
||||
</Button>
|
||||
<div className="flex flex-wrap items-center justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={!onViewExecutionHistory || !executionHistoryTarget || openingExecutionHistory}
|
||||
title={onViewExecutionHistory && executionHistoryTarget ? "在左侧 Agent 中打开所选轮次自动同步的调查执行记录,并可创建调试会话。" : "所选轮次还没有可查看的执行记录。"}
|
||||
onClick={() => void openExecutionHistory()}
|
||||
>
|
||||
{openingExecutionHistory
|
||||
? <LoaderCircle size={14} className="animate-spin" aria-hidden />
|
||||
: <History size={14} aria-hidden />}
|
||||
执行记录
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
title="打开调查协作对话,引用此任务,不会直接发送。"
|
||||
onClick={() =>
|
||||
onContinue({ context: snapshot ? { report_revision: snapshot.revision } : {}, reportGeneration: snapshot?.execution_generation })
|
||||
}
|
||||
>
|
||||
<MessageSquare size={14} aria-hidden />
|
||||
Agent 协作
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<InvestigationReportNavigation reportRef={reportRef} sections={sections} />
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { AgentExecutionPlanPanel } from "./agent-execution-plan-panel";
|
||||
import type { OperationalTaskDecision } from "../api/operational-task-client";
|
||||
import type { OperationalTaskDecision, TaskExecutionHistoryTarget } from "../api/operational-task-client";
|
||||
import type {
|
||||
ExternalWorkOrderSnapshot,
|
||||
OperationalTaskDataSnapshot
|
||||
@@ -114,6 +114,7 @@ type ConditionDetailPanelProps = {
|
||||
dataSource?: WorkbenchRuntimeDataSource;
|
||||
workOrderDataSource?: WorkbenchWorkOrderDataSource;
|
||||
onContinueConversation: (discussion?: import("../api/investigation-report").InvestigationDiscussion) => Promise<boolean | void> | boolean | void;
|
||||
onViewTaskExecutionHistory?: (taskId: string, target: TaskExecutionHistoryTarget, knownRunId?: string) => Promise<void> | void;
|
||||
onTaskDecision?: (taskId: string, actionId: string, expectedVersion: number, decision: OperationalTaskDecision, comment?: string) => Promise<unknown>;
|
||||
onTaskExecute?: (taskId: string, actionId: string, expectedVersion: number) => Promise<unknown>;
|
||||
onTaskDraftUpdate?: (taskId: string, actionId: string, expectedVersion: number, payload: Record<string, unknown>) => Promise<unknown>;
|
||||
@@ -168,6 +169,7 @@ export function ConditionDetailPanel({
|
||||
dataSource = "mock",
|
||||
workOrderDataSource = "mock",
|
||||
onContinueConversation,
|
||||
onViewTaskExecutionHistory,
|
||||
onTaskDecision,
|
||||
onTaskExecute,
|
||||
onTaskDraftUpdate,
|
||||
@@ -196,6 +198,7 @@ export function ConditionDetailPanel({
|
||||
dataSource={dataSource}
|
||||
workOrderDataSource={workOrderDataSource}
|
||||
onContinueConversation={onContinueConversation}
|
||||
onViewTaskExecutionHistory={onViewTaskExecutionHistory}
|
||||
onTaskDecision={onTaskDecision}
|
||||
onTaskExecute={onTaskExecute}
|
||||
onTaskDraftUpdate={onTaskDraftUpdate}
|
||||
@@ -232,6 +235,7 @@ function ConditionRecordDetailPanel({
|
||||
dataSource,
|
||||
workOrderDataSource,
|
||||
onContinueConversation,
|
||||
onViewTaskExecutionHistory,
|
||||
onTaskDecision,
|
||||
onTaskExecute,
|
||||
onTaskDraftUpdate,
|
||||
@@ -249,6 +253,7 @@ function ConditionRecordDetailPanel({
|
||||
dataSource: WorkbenchRuntimeDataSource;
|
||||
workOrderDataSource: WorkbenchWorkOrderDataSource;
|
||||
onContinueConversation: ConditionDetailPanelProps["onContinueConversation"];
|
||||
onViewTaskExecutionHistory?: ConditionDetailPanelProps["onViewTaskExecutionHistory"];
|
||||
onTaskDecision?: ConditionDetailPanelProps["onTaskDecision"];
|
||||
onTaskExecute?: ConditionDetailPanelProps["onTaskExecute"];
|
||||
onTaskDraftUpdate?: ConditionDetailPanelProps["onTaskDraftUpdate"];
|
||||
@@ -321,6 +326,13 @@ function ConditionRecordDetailPanel({
|
||||
onReportViewed={onReportViewed ? (generation, revision) => onReportViewed(condition.operationalTask!.task_id, generation, revision) : undefined}
|
||||
focusActionId={focusActionId}
|
||||
onContinue={onContinueConversation}
|
||||
onViewExecutionHistory={onViewTaskExecutionHistory
|
||||
? (target, knownRunId) => onViewTaskExecutionHistory(
|
||||
condition.operationalTask!.task_id,
|
||||
target,
|
||||
knownRunId
|
||||
)
|
||||
: undefined}
|
||||
onDecision={(actionId, version, decision, comment) => {
|
||||
if (!onTaskDecision) return Promise.reject(new Error("当前未配置人工确认接口。"));
|
||||
return onTaskDecision(condition.operationalTask!.task_id, actionId, version, decision, comment);
|
||||
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
WorkbenchRuntimeDataSource,
|
||||
WorkbenchWorkOrderDataSource
|
||||
} from "../types";
|
||||
import type { TaskExecutionHistoryTarget } from "../api/operational-task-client";
|
||||
import { createConditionConversationPrompt } from "../utils/scheduled-condition-prompts";
|
||||
import type { OperationalTaskDecision } from "../api/operational-task-client";
|
||||
import type { InvestigationDiscussion } from "../api/investigation-report";
|
||||
@@ -85,6 +86,7 @@ type ScheduledConditionFeedProps = {
|
||||
onLoadHistorySession: (sessionId: string) => void | Promise<void>;
|
||||
onSubmitPrompt: (prompt: string) => void | Promise<void>;
|
||||
onOpenTaskCollaboration?: (taskId: string, discussion?: InvestigationDiscussion) => Promise<void>;
|
||||
onViewTaskExecutionHistory?: (taskId: string, target: TaskExecutionHistoryTarget, knownRunId?: string) => Promise<void>;
|
||||
onTaskDecision?: (taskId: string, actionId: string, expectedVersion: number, decision: OperationalTaskDecision, comment?: string) => Promise<unknown>;
|
||||
onTaskExecute?: (taskId: string, actionId: string, expectedVersion: number) => Promise<unknown>;
|
||||
onTaskDraftUpdate?: (taskId: string, actionId: string, expectedVersion: number, payload: Record<string, unknown>) => Promise<unknown>;
|
||||
@@ -125,6 +127,7 @@ export function ScheduledConditionFeed({
|
||||
onLoadHistorySession,
|
||||
onSubmitPrompt,
|
||||
onOpenTaskCollaboration,
|
||||
onViewTaskExecutionHistory,
|
||||
onTaskDecision,
|
||||
onTaskExecute,
|
||||
onTaskDraftUpdate,
|
||||
@@ -554,6 +557,7 @@ export function ScheduledConditionFeed({
|
||||
dataSource={dataSource}
|
||||
workOrderDataSource={workOrderDataSource}
|
||||
onContinueConversation={(discussion) => handleContinueConversation(selectedCondition, discussion)}
|
||||
onViewTaskExecutionHistory={onViewTaskExecutionHistory}
|
||||
onTaskDecision={onTaskDecision}
|
||||
onTaskExecute={onTaskExecute}
|
||||
onTaskDraftUpdate={onTaskDraftUpdate}
|
||||
|
||||
Reference in New Issue
Block a user