fix: preserve investigation execution history by round
This commit is contained in:
@@ -138,6 +138,76 @@ describe("Agent API client sessions", () => {
|
|||||||
await expect(createAgentApiClient("http://agent.local").loadSession("missing")).resolves.toBeNull();
|
await expect(createAgentApiClient("http://agent.local").loadSession("missing")).resolves.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("loads a task execution history through its exact task and run binding", async () => {
|
||||||
|
const fetchMock = vi.fn(async (..._args: Parameters<typeof fetch>) => Response.json({
|
||||||
|
id: "execution-session",
|
||||||
|
session_id: "execution-session",
|
||||||
|
title: "南桥管道爆管调查 · 执行记录",
|
||||||
|
purpose: "task_execution",
|
||||||
|
task_id: "50000000-0000-4000-8000-000000000001",
|
||||||
|
created_at: "2026-10-30T01:00:00Z",
|
||||||
|
updated_at: "2026-10-30T01:05:00Z",
|
||||||
|
messages: [{ id: "assistant", role: "assistant", parts: [{ type: "text", text: "调查完成" }] }],
|
||||||
|
is_streaming: false,
|
||||||
|
run_status: "completed"
|
||||||
|
}));
|
||||||
|
vi.stubGlobal("fetch", fetchMock);
|
||||||
|
const client = createAgentApiClient("http://agent.local", {
|
||||||
|
getAccessToken: async () => "token",
|
||||||
|
getProjectId: async () => "project"
|
||||||
|
});
|
||||||
|
|
||||||
|
const loaded = await client.loadTaskExecutionSession(
|
||||||
|
"50000000-0000-4000-8000-000000000001",
|
||||||
|
"70000000-0000-4000-8000-000000000001",
|
||||||
|
"execution-session"
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(loaded).toMatchObject({
|
||||||
|
sessionId: "execution-session",
|
||||||
|
title: "南桥管道爆管调查 · 执行记录",
|
||||||
|
purpose: "task_execution",
|
||||||
|
taskId: "50000000-0000-4000-8000-000000000001",
|
||||||
|
runStatus: "completed"
|
||||||
|
});
|
||||||
|
const url = new URL(String(fetchMock.mock.calls[0]?.[0]));
|
||||||
|
expect(url.pathname).toBe("/api/v1/task/50000000-0000-4000-8000-000000000001/execution-session");
|
||||||
|
expect(url.searchParams.get("run_id")).toBe("70000000-0000-4000-8000-000000000001");
|
||||||
|
expect(url.searchParams.get("session_id")).toBe("execution-session");
|
||||||
|
const headers = new Headers(fetchMock.mock.calls[0]?.[1]?.headers);
|
||||||
|
expect(headers.get("Authorization")).toBe("Bearer token");
|
||||||
|
expect(headers.get("X-Project-Id")).toBe("project");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates a debug branch bound to an exact task execution history", async () => {
|
||||||
|
const fetchMock = vi.fn(async (..._args: Parameters<typeof fetch>) => Response.json({
|
||||||
|
session_id: "debug-session",
|
||||||
|
title: "南桥管道爆管调查 · 调试",
|
||||||
|
purpose: "task_collaboration",
|
||||||
|
task_id: "50000000-0000-4000-8000-000000000001",
|
||||||
|
parent_session_id: "execution-session",
|
||||||
|
created: true
|
||||||
|
}, { status: 201 }));
|
||||||
|
vi.stubGlobal("fetch", fetchMock);
|
||||||
|
const client = createAgentApiClient("http://agent.local", {
|
||||||
|
getAccessToken: async () => "token",
|
||||||
|
getProjectId: async () => "project"
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(client.createTaskExecutionDebugSession(
|
||||||
|
"50000000-0000-4000-8000-000000000001",
|
||||||
|
"70000000-0000-4000-8000-000000000001",
|
||||||
|
"execution-session"
|
||||||
|
)).resolves.toMatchObject({ session_id: "debug-session", purpose: "task_collaboration" });
|
||||||
|
|
||||||
|
const [url, init] = fetchMock.mock.calls[0] ?? [];
|
||||||
|
expect(new URL(String(url)).pathname).toBe("/api/v1/task/50000000-0000-4000-8000-000000000001/execution-session/debug");
|
||||||
|
expect(JSON.parse(String(init?.body))).toEqual({
|
||||||
|
run_id: "70000000-0000-4000-8000-000000000001",
|
||||||
|
session_id: "execution-session"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("falls back when a candidate is not the agent service", async () => {
|
it("falls back when a candidate is not the agent service", async () => {
|
||||||
const fetchMock = vi
|
const fetchMock = vi
|
||||||
.fn()
|
.fn()
|
||||||
|
|||||||
@@ -174,6 +174,8 @@ export type AgentApiClient = InteractionRequestApi & {
|
|||||||
moveSessionToTopic: (sessionId: string, topicId: string | null) => Promise<void>;
|
moveSessionToTopic: (sessionId: string, topicId: string | null) => Promise<void>;
|
||||||
loadOlderMessages: (sessionId: string, before: string, signal?: AbortSignal) => Promise<AgentHistoryMessagePage>;
|
loadOlderMessages: (sessionId: string, before: string, signal?: AbortSignal) => Promise<AgentHistoryMessagePage>;
|
||||||
createTaskCollaborationSession: (taskId: string) => Promise<AgentChatSession>;
|
createTaskCollaborationSession: (taskId: string) => Promise<AgentChatSession>;
|
||||||
|
createTaskExecutionDebugSession: (taskId: string, runId: string, sessionId: string) => Promise<AgentChatSession>;
|
||||||
|
loadTaskExecutionSession: (taskId: string, runId: string, sessionId: string, signal?: AbortSignal) => Promise<AgentLoadedChatSession | null>;
|
||||||
loadSession: (sessionId: string, options?: { messageLimit?: number; signal?: AbortSignal }) => Promise<AgentLoadedChatSession | null>;
|
loadSession: (sessionId: string, options?: { messageLimit?: number; signal?: AbortSignal }) => Promise<AgentLoadedChatSession | null>;
|
||||||
streamSession: (
|
streamSession: (
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
@@ -365,6 +367,30 @@ export function createAgentApiClient(
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async createTaskExecutionDebugSession(taskId, runId, sessionId) {
|
||||||
|
return requestJson<AgentChatSession>(
|
||||||
|
`/task/${encodeURIComponent(taskId)}/execution-session/debug`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ run_id: runId, session_id: sessionId })
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadTaskExecutionSession(taskId, runId, sessionId, signal) {
|
||||||
|
const query = new URLSearchParams({ run_id: runId, session_id: sessionId });
|
||||||
|
const response = await request(
|
||||||
|
`/task/${encodeURIComponent(taskId)}/execution-session?${query}`,
|
||||||
|
{ signal }
|
||||||
|
);
|
||||||
|
const text = await response.text();
|
||||||
|
const data = text ? JSON.parse(text) : null;
|
||||||
|
if (response.status === 404) return null;
|
||||||
|
if (!response.ok) throw new Error(getResponseErrorMessage(data, response.status));
|
||||||
|
return toLoadedSession(data);
|
||||||
|
},
|
||||||
|
|
||||||
async getFrontendActionRegistry() {
|
async getFrontendActionRegistry() {
|
||||||
return requestJson<unknown>("/frontend-action-registry");
|
return requestJson<unknown>("/frontend-action-registry");
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
|
Eye,
|
||||||
History,
|
History,
|
||||||
LoaderCircle,
|
LoaderCircle,
|
||||||
Pause,
|
Pause,
|
||||||
@@ -153,6 +154,7 @@ type AgentCommandPanelProps = {
|
|||||||
onRetryPromptSuggestions?: () => void;
|
onRetryPromptSuggestions?: () => void;
|
||||||
activeSessionId?: string | null;
|
activeSessionId?: string | null;
|
||||||
qaOnly?: boolean;
|
qaOnly?: boolean;
|
||||||
|
executionHistory?: boolean;
|
||||||
statusLabel?: string;
|
statusLabel?: string;
|
||||||
connectionStatus?: AgentConnectionStatus;
|
connectionStatus?: AgentConnectionStatus;
|
||||||
streaming?: boolean;
|
streaming?: boolean;
|
||||||
@@ -220,6 +222,7 @@ export function AgentCommandPanel({
|
|||||||
onRetryPromptSuggestions,
|
onRetryPromptSuggestions,
|
||||||
activeSessionId,
|
activeSessionId,
|
||||||
qaOnly = false,
|
qaOnly = false,
|
||||||
|
executionHistory = false,
|
||||||
connectionStatus = "connecting",
|
connectionStatus = "connecting",
|
||||||
streaming = false,
|
streaming = false,
|
||||||
inputDelivery = "steer",
|
inputDelivery = "steer",
|
||||||
@@ -561,6 +564,7 @@ export function AgentCommandPanel({
|
|||||||
onResumeSpeech={resumeSpeech}
|
onResumeSpeech={resumeSpeech}
|
||||||
onStopSpeech={stopSpeech}
|
onStopSpeech={stopSpeech}
|
||||||
qaOnly={qaOnly}
|
qaOnly={qaOnly}
|
||||||
|
showProcess={!qaOnly || executionHistory}
|
||||||
onLocateAnswerFeatures={onLocateAnswerFeatures}
|
onLocateAnswerFeatures={onLocateAnswerFeatures}
|
||||||
onSubmitAnswerForm={onSubmitPrompt}
|
onSubmitAnswerForm={onSubmitPrompt}
|
||||||
onAsk={(selection) => {
|
onAsk={(selection) => {
|
||||||
@@ -603,6 +607,15 @@ export function AgentCommandPanel({
|
|||||||
: "border-t border-[var(--surface-divider)] p-3"
|
: "border-t border-[var(--surface-divider)] p-3"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{executionHistory ? (
|
||||||
|
<div role="note" className="agent-panel-control mb-1 flex min-h-14 items-center gap-2.5 rounded-xl px-3 py-2 text-sm">
|
||||||
|
<Eye className="size-4 shrink-0 text-muted-foreground" aria-hidden />
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="font-medium text-foreground">执行记录自动同步</p>
|
||||||
|
<p className="text-xs leading-5 text-muted-foreground">发送消息后进入独立调试会话,系统执行记录不会被修改。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
{centeredComposer && !promptReference ? (
|
{centeredComposer && !promptReference ? (
|
||||||
<ConversationEmptyState className="agent-chat-empty-state">
|
<ConversationEmptyState className="agent-chat-empty-state">
|
||||||
<h2 className="text-2xl font-medium tracking-tight text-foreground">有什么可以帮你?</h2>
|
<h2 className="text-2xl font-medium tracking-tight text-foreground">有什么可以帮你?</h2>
|
||||||
@@ -1265,6 +1278,7 @@ type BackendMessageListProps = {
|
|||||||
onResumeSpeech: () => void;
|
onResumeSpeech: () => void;
|
||||||
onStopSpeech: () => void;
|
onStopSpeech: () => void;
|
||||||
qaOnly: boolean;
|
qaOnly: boolean;
|
||||||
|
showProcess: boolean;
|
||||||
onLocateAnswerFeatures?: (target: AnswerUiMapTarget) => Promise<unknown> | void;
|
onLocateAnswerFeatures?: (target: AnswerUiMapTarget) => Promise<unknown> | void;
|
||||||
onSubmitAnswerForm: (text: string) => Promise<void> | void;
|
onSubmitAnswerForm: (text: string) => Promise<void> | void;
|
||||||
onAsk?: (selection: AnswerUiSelection) => void;
|
onAsk?: (selection: AnswerUiSelection) => void;
|
||||||
@@ -1329,6 +1343,7 @@ function BackendMessage({
|
|||||||
onResumeSpeech,
|
onResumeSpeech,
|
||||||
onStopSpeech,
|
onStopSpeech,
|
||||||
qaOnly,
|
qaOnly,
|
||||||
|
showProcess,
|
||||||
onLocateAnswerFeatures,
|
onLocateAnswerFeatures,
|
||||||
onSubmitAnswerForm,
|
onSubmitAnswerForm,
|
||||||
onAsk,
|
onAsk,
|
||||||
@@ -1366,10 +1381,9 @@ function BackendMessage({
|
|||||||
processMessages.some(
|
processMessages.some(
|
||||||
(source) =>
|
(source) =>
|
||||||
getAnswerPresentation(source).process.length > 0 ||
|
getAnswerPresentation(source).process.length > 0 ||
|
||||||
(!qaOnly &&
|
(showProcess && source.progress?.length) ||
|
||||||
(source.progress?.length ||
|
(!qaOnly && !hasPendingMessageInput(source)
|
||||||
(!hasPendingMessageInput(source) &&
|
&& (source.todos?.todos.length || source.permissions?.length || source.questions?.length)),
|
||||||
(source.todos?.todos.length || source.permissions?.length || source.questions?.length)))),
|
|
||||||
);
|
);
|
||||||
const hasProcessError = processMessages.some((source) => source.progress?.some((item) => item.status === "error"));
|
const hasProcessError = processMessages.some((source) => source.progress?.some((item) => item.status === "error"));
|
||||||
return (
|
return (
|
||||||
@@ -1485,6 +1499,7 @@ function BackendMessage({
|
|||||||
<AgentMessageProcessContent
|
<AgentMessageProcessContent
|
||||||
messages={processMessages}
|
messages={processMessages}
|
||||||
qaOnly={qaOnly}
|
qaOnly={qaOnly}
|
||||||
|
showProgress={showProcess}
|
||||||
runningMessageId={!completed ? message.id : undefined}
|
runningMessageId={!completed ? message.id : undefined}
|
||||||
/>
|
/>
|
||||||
</AgentProcessDisclosure>
|
</AgentProcessDisclosure>
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ import { AgentMessageDetails, AgentMessageProgress } from "./agent-message-detai
|
|||||||
export function AgentMessageProcessContent({
|
export function AgentMessageProcessContent({
|
||||||
messages,
|
messages,
|
||||||
qaOnly,
|
qaOnly,
|
||||||
|
showProgress,
|
||||||
runningMessageId
|
runningMessageId
|
||||||
}: {
|
}: {
|
||||||
messages: AgentChatMessage[];
|
messages: AgentChatMessage[];
|
||||||
qaOnly: boolean;
|
qaOnly: boolean;
|
||||||
|
showProgress: boolean;
|
||||||
runningMessageId?: string;
|
runningMessageId?: string;
|
||||||
}) {
|
}) {
|
||||||
const progress = messages.flatMap((message) => (message.progress ?? []).map((item) => ({
|
const progress = messages.flatMap((message) => (message.progress ?? []).map((item) => ({
|
||||||
@@ -18,7 +20,7 @@ export function AgentMessageProcessContent({
|
|||||||
})));
|
})));
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!qaOnly ? <AgentMessageProgress progress={progress} running={Boolean(runningMessageId)} /> : null}
|
{showProgress ? <AgentMessageProgress progress={progress} running={Boolean(runningMessageId)} /> : null}
|
||||||
{messages.map((message) => {
|
{messages.map((message) => {
|
||||||
const { process } = getAnswerPresentation(message);
|
const { process } = getAnswerPresentation(message);
|
||||||
const running = message.id === runningMessageId;
|
const running = message.id === runningMessageId;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
|||||||
import {
|
import {
|
||||||
fetchOperationalTaskEvidenceSnapshot,
|
fetchOperationalTaskEvidenceSnapshot,
|
||||||
fetchOperationalTaskNotifications,
|
fetchOperationalTaskNotifications,
|
||||||
|
fetchOperationalTaskRuns,
|
||||||
parseOperationalTask,
|
parseOperationalTask,
|
||||||
parseOperationalTaskNotification
|
parseOperationalTaskNotification
|
||||||
} from "./operational-task-client";
|
} from "./operational-task-client";
|
||||||
@@ -39,6 +40,38 @@ function task(generation: number | null | undefined = 2) {
|
|||||||
afterEach(() => vi.unstubAllGlobals());
|
afterEach(() => vi.unstubAllGlobals());
|
||||||
|
|
||||||
describe("operational task contract", () => {
|
describe("operational task contract", () => {
|
||||||
|
it("loads task runs used to resolve a selected report round", async () => {
|
||||||
|
const run = {
|
||||||
|
run_id: id,
|
||||||
|
task_id: id,
|
||||||
|
generation: 1,
|
||||||
|
execution_request_id: id,
|
||||||
|
status: "succeeded",
|
||||||
|
revision: 2,
|
||||||
|
agent_session_id: "execution-session-1",
|
||||||
|
agent_instance_id: "agent-supply-1",
|
||||||
|
lease_expires_at: null,
|
||||||
|
heartbeat_at: time,
|
||||||
|
started_at: time,
|
||||||
|
finished_at: time,
|
||||||
|
failure_code: null,
|
||||||
|
failure_detail: null,
|
||||||
|
dispatch_attempts: 1,
|
||||||
|
next_dispatch_at: time,
|
||||||
|
created_at: time,
|
||||||
|
updated_at: time
|
||||||
|
};
|
||||||
|
const fetchMock = vi.fn(async (..._args: Parameters<typeof fetch>) =>
|
||||||
|
Response.json({ items: [run], next_cursor: null }));
|
||||||
|
vi.stubGlobal("fetch", fetchMock);
|
||||||
|
|
||||||
|
await expect(fetchOperationalTaskRuns(id)).resolves.toEqual([run]);
|
||||||
|
const url = new URL(String(fetchMock.mock.calls[0]?.[0]));
|
||||||
|
expect(url.pathname).toBe(`/api/v1/operational-tasks/${id}/runs`);
|
||||||
|
expect(url.searchParams.get("limit")).toBe("100");
|
||||||
|
expect(url.searchParams.get("offset")).toBe("0");
|
||||||
|
});
|
||||||
|
|
||||||
it("accepts execution generation on both plan and report", () => {
|
it("accepts execution generation on both plan and report", () => {
|
||||||
const parsed = parseOperationalTask(task());
|
const parsed = parseOperationalTask(task());
|
||||||
expect(parsed.latest_plan).toHaveProperty("execution_generation", 2);
|
expect(parsed.latest_plan).toHaveProperty("execution_generation", 2);
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const operationalTaskRunSchema = z.object({
|
|||||||
status: z.enum(["queued", "claimed", "running", "succeeded", "failed", "timed_out", "cancelled"]),
|
status: z.enum(["queued", "claimed", "running", "succeeded", "failed", "timed_out", "cancelled"]),
|
||||||
revision: z.number().int().positive(),
|
revision: z.number().int().positive(),
|
||||||
agent_session_id: z.string().min(1).nullable(),
|
agent_session_id: z.string().min(1).nullable(),
|
||||||
|
agent_instance_id: z.string().min(1).nullable().optional(),
|
||||||
lease_expires_at: workbenchDateTimeSchema.nullable(),
|
lease_expires_at: workbenchDateTimeSchema.nullable(),
|
||||||
heartbeat_at: workbenchDateTimeSchema.nullable(),
|
heartbeat_at: workbenchDateTimeSchema.nullable(),
|
||||||
started_at: workbenchDateTimeSchema.nullable(),
|
started_at: workbenchDateTimeSchema.nullable(),
|
||||||
@@ -39,6 +40,10 @@ const operationalTaskRunSchema = z.object({
|
|||||||
created_at: workbenchDateTimeSchema,
|
created_at: workbenchDateTimeSchema,
|
||||||
updated_at: workbenchDateTimeSchema
|
updated_at: workbenchDateTimeSchema
|
||||||
}).strict();
|
}).strict();
|
||||||
|
const operationalTaskRunPageSchema = z.object({
|
||||||
|
items: z.array(operationalTaskRunSchema),
|
||||||
|
next_cursor: z.string().min(1).nullable()
|
||||||
|
}).strict();
|
||||||
const taskSummarySchema = z.object({
|
const taskSummarySchema = z.object({
|
||||||
project_id: z.string().uuid(),
|
project_id: z.string().uuid(),
|
||||||
task_id: z.string().uuid(),
|
task_id: z.string().uuid(),
|
||||||
@@ -359,6 +364,11 @@ const taskDataSnapshotSchema = z.object({
|
|||||||
|
|
||||||
export type OperationalTaskSummary = z.infer<typeof taskSummarySchema>;
|
export type OperationalTaskSummary = z.infer<typeof taskSummarySchema>;
|
||||||
export type OperationalTask = z.infer<typeof operationalTaskSchema>;
|
export type OperationalTask = z.infer<typeof operationalTaskSchema>;
|
||||||
|
export type OperationalTaskRun = z.infer<typeof operationalTaskRunSchema>;
|
||||||
|
export type TaskExecutionHistoryTarget = {
|
||||||
|
executionGeneration: number | null;
|
||||||
|
sessionId: string;
|
||||||
|
};
|
||||||
export type OperationalTaskPlan = z.infer<typeof planInputSchema>;
|
export type OperationalTaskPlan = z.infer<typeof planInputSchema>;
|
||||||
export type OperationalTaskAction = z.infer<typeof actionSchema>;
|
export type OperationalTaskAction = z.infer<typeof actionSchema>;
|
||||||
export type OperationalTaskAccessRequest = z.infer<typeof accessRequestSchema>;
|
export type OperationalTaskAccessRequest = z.infer<typeof accessRequestSchema>;
|
||||||
@@ -423,6 +433,18 @@ export async function fetchOperationalTaskReports(taskId: string, offset: number
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchOperationalTaskRuns(taskId: string, getAccessToken?: WorkbenchAccess) {
|
||||||
|
const runs: OperationalTaskRun[] = [];
|
||||||
|
const limit = 100;
|
||||||
|
for (let offset = 0; ; offset += limit) {
|
||||||
|
const page = operationalTaskRunPageSchema.parse(await operationalTaskRequest(
|
||||||
|
`/operational-tasks/${encodeURIComponent(taskId)}/runs?limit=${limit}&offset=${offset}`, getAccessToken,
|
||||||
|
));
|
||||||
|
runs.push(...page.items);
|
||||||
|
if (page.items.length < limit) return runs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchOperationalTaskPresentationCursor(taskId: string, getAccessToken?: WorkbenchAccess, signal?: AbortSignal) {
|
export async function fetchOperationalTaskPresentationCursor(taskId: string, getAccessToken?: WorkbenchAccess, signal?: AbortSignal) {
|
||||||
return investigationPresentationCursorSchema.parse(await operationalTaskRequest(
|
return investigationPresentationCursorSchema.parse(await operationalTaskRequest(
|
||||||
`/operational-tasks/${encodeURIComponent(taskId)}/presentation-cursor`, getAccessToken, { signal }
|
`/operational-tasks/${encodeURIComponent(taskId)}/presentation-cursor`, getAccessToken, { signal }
|
||||||
|
|||||||
@@ -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", () => {
|
describe("operational task approval and evidence", () => {
|
||||||
it("shows the selected v2 route first and keeps unchosen work available", () => {
|
it("shows the selected v2 route first and keeps unchosen work available", () => {
|
||||||
const node = (id: string, title: string, status: "completed" | "waiting" | "skipped") => ({
|
const node = (id: string, title: string, status: "completed" | "waiting" | "skipped") => ({
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import type {
|
|||||||
OperationalTaskPlan,
|
OperationalTaskPlan,
|
||||||
OperationalTaskDataSnapshot
|
OperationalTaskDataSnapshot
|
||||||
} from "../api/operational-task-client";
|
} from "../api/operational-task-client";
|
||||||
|
import type { TaskExecutionHistoryTarget } from "../api/operational-task-client";
|
||||||
|
|
||||||
type ExecutionNode = {
|
type ExecutionNode = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -65,6 +66,7 @@ type AgentExecutionPlanPanelProps = {
|
|||||||
onLocate?: (targets: import("../api/investigation-report").InvestigationMapTarget[]) => void;
|
onLocate?: (targets: import("../api/investigation-report").InvestigationMapTarget[]) => void;
|
||||||
task: OperationalTask;
|
task: OperationalTask;
|
||||||
onContinue: (discussion?: InvestigationDiscussion) => void;
|
onContinue: (discussion?: InvestigationDiscussion) => void;
|
||||||
|
onViewExecutionHistory?: (target: TaskExecutionHistoryTarget, knownRunId?: string) => Promise<void> | void;
|
||||||
onDecision: (actionId: string, expectedVersion: number, decision: OperationalTaskDecision, comment?: string) => Promise<unknown>;
|
onDecision: (actionId: string, expectedVersion: number, decision: OperationalTaskDecision, comment?: string) => Promise<unknown>;
|
||||||
onExecute: (actionId: string, expectedVersion: number) => Promise<unknown>;
|
onExecute: (actionId: string, expectedVersion: number) => Promise<unknown>;
|
||||||
onDraftUpdate?: (actionId: string, expectedVersion: number, payload: Record<string, unknown>) => Promise<unknown>;
|
onDraftUpdate?: (actionId: string, expectedVersion: number, payload: Record<string, unknown>) => Promise<unknown>;
|
||||||
@@ -82,6 +84,7 @@ export function AgentExecutionPlanPanel({
|
|||||||
onLocate,
|
onLocate,
|
||||||
task,
|
task,
|
||||||
onContinue,
|
onContinue,
|
||||||
|
onViewExecutionHistory,
|
||||||
onDecision,
|
onDecision,
|
||||||
onExecute,
|
onExecute,
|
||||||
onDraftUpdate,
|
onDraftUpdate,
|
||||||
@@ -95,7 +98,8 @@ export function AgentExecutionPlanPanel({
|
|||||||
focusActionId
|
focusActionId
|
||||||
}: AgentExecutionPlanPanelProps) {
|
}: AgentExecutionPlanPanelProps) {
|
||||||
if (task.task_type === "event_investigation") return <InvestigationTaskReport key={task.task_id} task={task}
|
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}
|
renderAction={(action) => <HumanActionCard key={action.action_id} task={task} action={action} onDecision={onDecision}
|
||||||
onExecute={onExecute} onDraftUpdate={onDraftUpdate} onConfirmDispatch={onConfirmDispatch} onReconcile={onReconcile} onRefresh={onRefresh} />}
|
onExecute={onExecute} onDraftUpdate={onDraftUpdate} onConfirmDispatch={onConfirmDispatch} onReconcile={onReconcile} onRefresh={onRefresh} />}
|
||||||
renderEvidence={(item) => <EvidenceDisclosure key={item.evidence_id} item={item} onLoadSnapshot={onLoadEvidenceSnapshot} />}
|
renderEvidence={(item) => <EvidenceDisclosure key={item.evidence_id} item={item} onLoadSnapshot={onLoadEvidenceSnapshot} />}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState, type ReactNode } from "react";
|
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 { Button } from "@/shared/ui/button";
|
||||||
import { Disclosure } from "@/shared/ui/disclosure";
|
import { Disclosure } from "@/shared/ui/disclosure";
|
||||||
import { StatusBadge } from "@/shared/ui/status";
|
import { StatusBadge } from "@/shared/ui/status";
|
||||||
@@ -11,7 +11,8 @@ import type {
|
|||||||
OperationalTaskAction,
|
OperationalTaskAction,
|
||||||
OperationalTaskEvidence,
|
OperationalTaskEvidence,
|
||||||
OperationalTaskReport,
|
OperationalTaskReport,
|
||||||
OperationalTaskReportPage
|
OperationalTaskReportPage,
|
||||||
|
TaskExecutionHistoryTarget
|
||||||
} from "../api/operational-task-client";
|
} from "../api/operational-task-client";
|
||||||
import {
|
import {
|
||||||
investigationDiagram,
|
investigationDiagram,
|
||||||
@@ -24,11 +25,13 @@ import { useReportUpdateMotion } from "../hooks/use-report-update-motion";
|
|||||||
import { ExpandableReading } from "@/shared/ui/reading-workspace";
|
import { ExpandableReading } from "@/shared/ui/reading-workspace";
|
||||||
import { InvestigationReportBlocks } from "./investigation-report-blocks";
|
import { InvestigationReportBlocks } from "./investigation-report-blocks";
|
||||||
import { InvestigationReportNavigation, type InvestigationReportSection } from "./investigation-report-navigation";
|
import { InvestigationReportNavigation, type InvestigationReportSection } from "./investigation-report-navigation";
|
||||||
|
import { showMapNotice } from "@/features/map/core";
|
||||||
|
|
||||||
type ReportSelection = { snapshot: OperationalTaskReport; diagram: InvestigationDiagram | null };
|
type ReportSelection = { snapshot: OperationalTaskReport; diagram: InvestigationDiagram | null };
|
||||||
type Props = {
|
type Props = {
|
||||||
task: OperationalTask;
|
task: OperationalTask;
|
||||||
onContinue: (discussion?: InvestigationDiscussion) => Promise<boolean | void> | boolean | void;
|
onContinue: (discussion?: InvestigationDiscussion) => Promise<boolean | void> | boolean | void;
|
||||||
|
onViewExecutionHistory?: (target: TaskExecutionHistoryTarget, knownRunId?: string) => Promise<void> | void;
|
||||||
renderAction: (action: OperationalTaskAction) => ReactNode;
|
renderAction: (action: OperationalTaskAction) => ReactNode;
|
||||||
renderEvidence: (evidence: OperationalTaskEvidence) => ReactNode;
|
renderEvidence: (evidence: OperationalTaskEvidence) => ReactNode;
|
||||||
onLoadReports?: (offset: number) => Promise<OperationalTaskReportPage>;
|
onLoadReports?: (offset: number) => Promise<OperationalTaskReportPage>;
|
||||||
@@ -126,6 +129,7 @@ function investigationTimeline(
|
|||||||
export function InvestigationTaskReport({
|
export function InvestigationTaskReport({
|
||||||
task,
|
task,
|
||||||
onContinue,
|
onContinue,
|
||||||
|
onViewExecutionHistory,
|
||||||
renderAction,
|
renderAction,
|
||||||
renderEvidence,
|
renderEvidence,
|
||||||
onLoadReports,
|
onLoadReports,
|
||||||
@@ -139,8 +143,24 @@ export function InvestigationTaskReport({
|
|||||||
const [historyOffset, setHistoryOffset] = useState<number | null>(0);
|
const [historyOffset, setHistoryOffset] = useState<number | null>(0);
|
||||||
const [historyLoading, setHistoryLoading] = useState(false);
|
const [historyLoading, setHistoryLoading] = useState(false);
|
||||||
const [historyError, setHistoryError] = useState<string | null>(null);
|
const [historyError, setHistoryError] = useState<string | null>(null);
|
||||||
|
const [openingExecutionHistory, setOpeningExecutionHistory] = useState(false);
|
||||||
const live = selection === null;
|
const live = selection === null;
|
||||||
const snapshot = selection?.snapshot ?? task.latest_report;
|
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 report = snapshot?.report;
|
||||||
const progress = task.latest_progress?.execution_generation === task.execution_generation ? task.latest_progress : null;
|
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
|
// A current final report owns its archived graph. During investigation, a
|
||||||
@@ -221,6 +241,25 @@ export function InvestigationTaskReport({
|
|||||||
setHistoryLoading(false);
|
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 (
|
return (
|
||||||
<ExpandableReading title={snapshot?.report.title ?? task.title}
|
<ExpandableReading title={snapshot?.report.title ?? task.title}
|
||||||
@@ -229,7 +268,7 @@ export function InvestigationTaskReport({
|
|||||||
<header className="investigation-report-header">
|
<header className="investigation-report-header">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="text-xs text-[var(--ink-muted)]">
|
<p className="text-xs text-[var(--ink-muted)]">
|
||||||
调查任务 · 第 {task.execution_generation} 轮
|
{displayedGeneration == null ? "调查任务 · 历史轮次" : `调查任务 · 第 ${displayedGeneration} 轮`}
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-1 flex flex-wrap items-center gap-2">
|
<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>
|
<h2 className="text-base font-semibold leading-6 text-[var(--ink)]">{task.title}</h2>
|
||||||
@@ -247,17 +286,31 @@ export function InvestigationTaskReport({
|
|||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-xs leading-5 text-[var(--ink-muted)]">{task.summary}</p>
|
<p className="mt-1 text-xs leading-5 text-[var(--ink-muted)]">{task.summary}</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<div className="flex flex-wrap items-center justify-end gap-2">
|
||||||
variant="outline"
|
<Button
|
||||||
size="sm"
|
variant="outline"
|
||||||
title="打开调查协作对话,引用此任务,不会直接发送。"
|
size="sm"
|
||||||
onClick={() =>
|
disabled={!onViewExecutionHistory || !executionHistoryTarget || openingExecutionHistory}
|
||||||
onContinue({ context: snapshot ? { report_revision: snapshot.revision } : {}, reportGeneration: snapshot?.execution_generation })
|
title={onViewExecutionHistory && executionHistoryTarget ? "在左侧 Agent 中打开所选轮次自动同步的调查执行记录,并可创建调试会话。" : "所选轮次还没有可查看的执行记录。"}
|
||||||
}
|
onClick={() => void openExecutionHistory()}
|
||||||
>
|
>
|
||||||
<MessageSquare size={14} aria-hidden />
|
{openingExecutionHistory
|
||||||
Agent 协作
|
? <LoaderCircle size={14} className="animate-spin" aria-hidden />
|
||||||
</Button>
|
: <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>
|
</header>
|
||||||
|
|
||||||
<InvestigationReportNavigation reportRef={reportRef} sections={sections} />
|
<InvestigationReportNavigation reportRef={reportRef} sections={sections} />
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
import { AnimatePresence, motion } from "motion/react";
|
import { AnimatePresence, motion } from "motion/react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { AgentExecutionPlanPanel } from "./agent-execution-plan-panel";
|
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 {
|
import type {
|
||||||
ExternalWorkOrderSnapshot,
|
ExternalWorkOrderSnapshot,
|
||||||
OperationalTaskDataSnapshot
|
OperationalTaskDataSnapshot
|
||||||
@@ -114,6 +114,7 @@ type ConditionDetailPanelProps = {
|
|||||||
dataSource?: WorkbenchRuntimeDataSource;
|
dataSource?: WorkbenchRuntimeDataSource;
|
||||||
workOrderDataSource?: WorkbenchWorkOrderDataSource;
|
workOrderDataSource?: WorkbenchWorkOrderDataSource;
|
||||||
onContinueConversation: (discussion?: import("../api/investigation-report").InvestigationDiscussion) => Promise<boolean | void> | boolean | void;
|
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>;
|
onTaskDecision?: (taskId: string, actionId: string, expectedVersion: number, decision: OperationalTaskDecision, comment?: string) => Promise<unknown>;
|
||||||
onTaskExecute?: (taskId: string, actionId: string, expectedVersion: number) => Promise<unknown>;
|
onTaskExecute?: (taskId: string, actionId: string, expectedVersion: number) => Promise<unknown>;
|
||||||
onTaskDraftUpdate?: (taskId: string, actionId: string, expectedVersion: number, payload: Record<string, unknown>) => Promise<unknown>;
|
onTaskDraftUpdate?: (taskId: string, actionId: string, expectedVersion: number, payload: Record<string, unknown>) => Promise<unknown>;
|
||||||
@@ -168,6 +169,7 @@ export function ConditionDetailPanel({
|
|||||||
dataSource = "mock",
|
dataSource = "mock",
|
||||||
workOrderDataSource = "mock",
|
workOrderDataSource = "mock",
|
||||||
onContinueConversation,
|
onContinueConversation,
|
||||||
|
onViewTaskExecutionHistory,
|
||||||
onTaskDecision,
|
onTaskDecision,
|
||||||
onTaskExecute,
|
onTaskExecute,
|
||||||
onTaskDraftUpdate,
|
onTaskDraftUpdate,
|
||||||
@@ -196,6 +198,7 @@ export function ConditionDetailPanel({
|
|||||||
dataSource={dataSource}
|
dataSource={dataSource}
|
||||||
workOrderDataSource={workOrderDataSource}
|
workOrderDataSource={workOrderDataSource}
|
||||||
onContinueConversation={onContinueConversation}
|
onContinueConversation={onContinueConversation}
|
||||||
|
onViewTaskExecutionHistory={onViewTaskExecutionHistory}
|
||||||
onTaskDecision={onTaskDecision}
|
onTaskDecision={onTaskDecision}
|
||||||
onTaskExecute={onTaskExecute}
|
onTaskExecute={onTaskExecute}
|
||||||
onTaskDraftUpdate={onTaskDraftUpdate}
|
onTaskDraftUpdate={onTaskDraftUpdate}
|
||||||
@@ -232,6 +235,7 @@ function ConditionRecordDetailPanel({
|
|||||||
dataSource,
|
dataSource,
|
||||||
workOrderDataSource,
|
workOrderDataSource,
|
||||||
onContinueConversation,
|
onContinueConversation,
|
||||||
|
onViewTaskExecutionHistory,
|
||||||
onTaskDecision,
|
onTaskDecision,
|
||||||
onTaskExecute,
|
onTaskExecute,
|
||||||
onTaskDraftUpdate,
|
onTaskDraftUpdate,
|
||||||
@@ -249,6 +253,7 @@ function ConditionRecordDetailPanel({
|
|||||||
dataSource: WorkbenchRuntimeDataSource;
|
dataSource: WorkbenchRuntimeDataSource;
|
||||||
workOrderDataSource: WorkbenchWorkOrderDataSource;
|
workOrderDataSource: WorkbenchWorkOrderDataSource;
|
||||||
onContinueConversation: ConditionDetailPanelProps["onContinueConversation"];
|
onContinueConversation: ConditionDetailPanelProps["onContinueConversation"];
|
||||||
|
onViewTaskExecutionHistory?: ConditionDetailPanelProps["onViewTaskExecutionHistory"];
|
||||||
onTaskDecision?: ConditionDetailPanelProps["onTaskDecision"];
|
onTaskDecision?: ConditionDetailPanelProps["onTaskDecision"];
|
||||||
onTaskExecute?: ConditionDetailPanelProps["onTaskExecute"];
|
onTaskExecute?: ConditionDetailPanelProps["onTaskExecute"];
|
||||||
onTaskDraftUpdate?: ConditionDetailPanelProps["onTaskDraftUpdate"];
|
onTaskDraftUpdate?: ConditionDetailPanelProps["onTaskDraftUpdate"];
|
||||||
@@ -321,6 +326,13 @@ function ConditionRecordDetailPanel({
|
|||||||
onReportViewed={onReportViewed ? (generation, revision) => onReportViewed(condition.operationalTask!.task_id, generation, revision) : undefined}
|
onReportViewed={onReportViewed ? (generation, revision) => onReportViewed(condition.operationalTask!.task_id, generation, revision) : undefined}
|
||||||
focusActionId={focusActionId}
|
focusActionId={focusActionId}
|
||||||
onContinue={onContinueConversation}
|
onContinue={onContinueConversation}
|
||||||
|
onViewExecutionHistory={onViewTaskExecutionHistory
|
||||||
|
? (target, knownRunId) => onViewTaskExecutionHistory(
|
||||||
|
condition.operationalTask!.task_id,
|
||||||
|
target,
|
||||||
|
knownRunId
|
||||||
|
)
|
||||||
|
: undefined}
|
||||||
onDecision={(actionId, version, decision, comment) => {
|
onDecision={(actionId, version, decision, comment) => {
|
||||||
if (!onTaskDecision) return Promise.reject(new Error("当前未配置人工确认接口。"));
|
if (!onTaskDecision) return Promise.reject(new Error("当前未配置人工确认接口。"));
|
||||||
return onTaskDecision(condition.operationalTask!.task_id, actionId, version, decision, comment);
|
return onTaskDecision(condition.operationalTask!.task_id, actionId, version, decision, comment);
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import type {
|
|||||||
WorkbenchRuntimeDataSource,
|
WorkbenchRuntimeDataSource,
|
||||||
WorkbenchWorkOrderDataSource
|
WorkbenchWorkOrderDataSource
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
import type { TaskExecutionHistoryTarget } from "../api/operational-task-client";
|
||||||
import { createConditionConversationPrompt } from "../utils/scheduled-condition-prompts";
|
import { createConditionConversationPrompt } from "../utils/scheduled-condition-prompts";
|
||||||
import type { OperationalTaskDecision } from "../api/operational-task-client";
|
import type { OperationalTaskDecision } from "../api/operational-task-client";
|
||||||
import type { InvestigationDiscussion } from "../api/investigation-report";
|
import type { InvestigationDiscussion } from "../api/investigation-report";
|
||||||
@@ -85,6 +86,7 @@ type ScheduledConditionFeedProps = {
|
|||||||
onLoadHistorySession: (sessionId: string) => void | Promise<void>;
|
onLoadHistorySession: (sessionId: string) => void | Promise<void>;
|
||||||
onSubmitPrompt: (prompt: string) => void | Promise<void>;
|
onSubmitPrompt: (prompt: string) => void | Promise<void>;
|
||||||
onOpenTaskCollaboration?: (taskId: string, discussion?: InvestigationDiscussion) => 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>;
|
onTaskDecision?: (taskId: string, actionId: string, expectedVersion: number, decision: OperationalTaskDecision, comment?: string) => Promise<unknown>;
|
||||||
onTaskExecute?: (taskId: string, actionId: string, expectedVersion: number) => Promise<unknown>;
|
onTaskExecute?: (taskId: string, actionId: string, expectedVersion: number) => Promise<unknown>;
|
||||||
onTaskDraftUpdate?: (taskId: string, actionId: string, expectedVersion: number, payload: Record<string, unknown>) => Promise<unknown>;
|
onTaskDraftUpdate?: (taskId: string, actionId: string, expectedVersion: number, payload: Record<string, unknown>) => Promise<unknown>;
|
||||||
@@ -125,6 +127,7 @@ export function ScheduledConditionFeed({
|
|||||||
onLoadHistorySession,
|
onLoadHistorySession,
|
||||||
onSubmitPrompt,
|
onSubmitPrompt,
|
||||||
onOpenTaskCollaboration,
|
onOpenTaskCollaboration,
|
||||||
|
onViewTaskExecutionHistory,
|
||||||
onTaskDecision,
|
onTaskDecision,
|
||||||
onTaskExecute,
|
onTaskExecute,
|
||||||
onTaskDraftUpdate,
|
onTaskDraftUpdate,
|
||||||
@@ -554,6 +557,7 @@ export function ScheduledConditionFeed({
|
|||||||
dataSource={dataSource}
|
dataSource={dataSource}
|
||||||
workOrderDataSource={workOrderDataSource}
|
workOrderDataSource={workOrderDataSource}
|
||||||
onContinueConversation={(discussion) => handleContinueConversation(selectedCondition, discussion)}
|
onContinueConversation={(discussion) => handleContinueConversation(selectedCondition, discussion)}
|
||||||
|
onViewTaskExecutionHistory={onViewTaskExecutionHistory}
|
||||||
onTaskDecision={onTaskDecision}
|
onTaskDecision={onTaskDecision}
|
||||||
onTaskExecute={onTaskExecute}
|
onTaskExecute={onTaskExecute}
|
||||||
onTaskDraftUpdate={onTaskDraftUpdate}
|
onTaskDraftUpdate={onTaskDraftUpdate}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export type AgentSessionRuntime = {
|
|||||||
topicId?: string | null;
|
topicId?: string | null;
|
||||||
purpose: AgentSessionPurpose;
|
purpose: AgentSessionPurpose;
|
||||||
taskId?: string;
|
taskId?: string;
|
||||||
|
taskExecutionHistory?: { taskId: string; runId: string; sessionId: string };
|
||||||
draft: string;
|
draft: string;
|
||||||
reference?: AnswerUiSelection;
|
reference?: AnswerUiSelection;
|
||||||
taskDiscussion?: { context: AgentTaskDiscussionContext; reportGeneration?: number | null };
|
taskDiscussion?: { context: AgentTaskDiscussionContext; reportGeneration?: number | null };
|
||||||
|
|||||||
@@ -200,7 +200,12 @@ export function useWorkbenchAgent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [resources, scopeKey, scopeClient, selectedModel, approvalMode]);
|
}, [resources, scopeKey, scopeClient, selectedModel, approvalMode]);
|
||||||
const interactionRequests = useInteractionRequests(scopeClient, scopeKey, sessionId, continueInteraction);
|
const interactionRequests = useInteractionRequests(
|
||||||
|
scopeClient,
|
||||||
|
scopeKey,
|
||||||
|
sessionPurpose === "task_execution" ? null : sessionId,
|
||||||
|
continueInteraction
|
||||||
|
);
|
||||||
const refreshInteractionRequests = interactionRequests.refresh;
|
const refreshInteractionRequests = interactionRequests.refresh;
|
||||||
|
|
||||||
|
|
||||||
@@ -510,7 +515,9 @@ export function useWorkbenchAgent({
|
|||||||
|
|
||||||
const streaming = runtimeRunning(runtime);
|
const streaming = runtimeRunning(runtime);
|
||||||
const { data: pendingInputs = [], mutate: refreshPendingInputs } = useSWR(
|
const { data: pendingInputs = [], mutate: refreshPendingInputs } = useSWR(
|
||||||
sessionId ? ["agent", "response-inputs", getAccessToken, scopeKey, sessionId] : null,
|
sessionId && sessionPurpose !== "task_execution"
|
||||||
|
? ["agent", "response-inputs", getAccessToken, scopeKey, sessionId]
|
||||||
|
: null,
|
||||||
() => clientRef.current.listResponseInputs(sessionId!),
|
() => clientRef.current.listResponseInputs(sessionId!),
|
||||||
{ refreshInterval: streaming ? 1000 : 0, shouldRetryOnError: false }
|
{ refreshInterval: streaming ? 1000 : 0, shouldRetryOnError: false }
|
||||||
);
|
);
|
||||||
@@ -776,6 +783,118 @@ export function useWorkbenchAgent({
|
|||||||
return id;
|
return id;
|
||||||
}, [activateRuntime, getAccessToken, getProjectId, projectScopeKey, resources]);
|
}, [activateRuntime, getAccessToken, getProjectId, projectScopeKey, resources]);
|
||||||
|
|
||||||
|
const openTaskExecutionHistory = useCallback(async (taskId: string, runId: string, executionSessionId: string) => {
|
||||||
|
const normalizedTaskId = taskId.trim();
|
||||||
|
const normalizedRunId = runId.trim();
|
||||||
|
const normalizedSessionId = executionSessionId.trim();
|
||||||
|
if (!normalizedTaskId || !normalizedRunId || !normalizedSessionId) {
|
||||||
|
throw new Error("调查任务缺少可用的执行记录标识。");
|
||||||
|
}
|
||||||
|
const projectClient = createAgentApiClient(undefined, {
|
||||||
|
getAccessToken: getWorkbenchTokenProvider(getAccessToken),
|
||||||
|
getProjectId
|
||||||
|
});
|
||||||
|
const loaded = await projectClient.loadTaskExecutionSession(
|
||||||
|
normalizedTaskId,
|
||||||
|
normalizedRunId,
|
||||||
|
normalizedSessionId,
|
||||||
|
AbortSignal.timeout(15_000)
|
||||||
|
);
|
||||||
|
if (!loaded) throw new Error("调查执行记录不存在或尚未持久化。");
|
||||||
|
let target = agentRuntimes(resources).find((entry) =>
|
||||||
|
entry.scope === projectScopeKey && entry.sessionId === loaded.sessionId
|
||||||
|
);
|
||||||
|
if (!target) {
|
||||||
|
target = createAgentRuntime(
|
||||||
|
resources,
|
||||||
|
projectScopeKey,
|
||||||
|
projectClient,
|
||||||
|
createWorkbenchAgentTransport(projectClient, getAccessToken, getProjectId),
|
||||||
|
loaded
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
target.client = projectClient;
|
||||||
|
target.chat.messages = toAgentUiMessages(loaded.messages);
|
||||||
|
target.title = loaded.title;
|
||||||
|
target.purpose = "task_execution";
|
||||||
|
target.taskId = normalizedTaskId;
|
||||||
|
target.loadedAt = Date.now();
|
||||||
|
target.updatedAt = loaded.updatedAt;
|
||||||
|
target.runStatus = loaded.runStatus;
|
||||||
|
target.error = undefined;
|
||||||
|
}
|
||||||
|
target.taskDiscussion = undefined;
|
||||||
|
target.taskExecutionHistory = {
|
||||||
|
taskId: normalizedTaskId,
|
||||||
|
runId: normalizedRunId,
|
||||||
|
sessionId: normalizedSessionId
|
||||||
|
};
|
||||||
|
target.draft = "";
|
||||||
|
activateRuntime(target);
|
||||||
|
setStatusLabel(loaded.runStatus === "running" ? "调查执行记录自动同步中" : "调查执行记录已打开");
|
||||||
|
return loaded.sessionId;
|
||||||
|
}, [activateRuntime, getAccessToken, getProjectId, projectScopeKey, resources]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const target = runtimeRef.current;
|
||||||
|
const binding = target?.taskExecutionHistory;
|
||||||
|
if (sessionPurpose !== "task_execution" || !target || !binding || target.sessionId !== binding.sessionId) return;
|
||||||
|
let stopped = false;
|
||||||
|
let refreshing = false;
|
||||||
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
const controller = new AbortController();
|
||||||
|
const schedule = (delay = 2_000) => {
|
||||||
|
if (!stopped) timer = setTimeout(() => void refresh(), delay);
|
||||||
|
};
|
||||||
|
const refresh = async () => {
|
||||||
|
if (stopped || refreshing || resources.activeAgent !== target || target.purpose !== "task_execution") return;
|
||||||
|
refreshing = true;
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = undefined;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const loaded = await target.client.loadTaskExecutionSession(
|
||||||
|
binding.taskId,
|
||||||
|
binding.runId,
|
||||||
|
binding.sessionId,
|
||||||
|
controller.signal
|
||||||
|
);
|
||||||
|
if (!loaded || stopped || resources.activeAgent !== target || target.purpose !== "task_execution") {
|
||||||
|
refreshing = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
target.chat.messages = mergeHistoryMessages(target.chat.messages, toAgentUiMessages(loaded.messages));
|
||||||
|
target.title = loaded.title;
|
||||||
|
target.updatedAt = loaded.updatedAt;
|
||||||
|
target.loadedAt = Date.now();
|
||||||
|
target.runStatus = loaded.runStatus;
|
||||||
|
target.connectionError = undefined;
|
||||||
|
setSessionTitle(loaded.title);
|
||||||
|
setStatusLabel(loaded.runStatus === "running" ? "调查执行记录自动同步中" : "调查执行记录已同步");
|
||||||
|
resources.notify();
|
||||||
|
refreshing = false;
|
||||||
|
if (loaded.runStatus === "running" || loaded.isStreaming) schedule();
|
||||||
|
} catch (error) {
|
||||||
|
refreshing = false;
|
||||||
|
if (controller.signal.aborted || stopped) return;
|
||||||
|
target.connectionError = error instanceof Error ? error.message : "调查执行记录同步失败";
|
||||||
|
setStatusLabel("调查执行记录同步中断,正在重试");
|
||||||
|
resources.notify();
|
||||||
|
schedule(4_000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const refreshOnFocus = () => void refresh();
|
||||||
|
window.addEventListener("focus", refreshOnFocus);
|
||||||
|
schedule();
|
||||||
|
return () => {
|
||||||
|
stopped = true;
|
||||||
|
controller.abort();
|
||||||
|
if (timer) clearTimeout(timer);
|
||||||
|
window.removeEventListener("focus", refreshOnFocus);
|
||||||
|
};
|
||||||
|
}, [resources, sessionId, sessionPurpose]);
|
||||||
|
|
||||||
const renameHistorySession = useCallback(
|
const renameHistorySession = useCallback(
|
||||||
async (nextSessionId: string, title: string) => {
|
async (nextSessionId: string, title: string) => {
|
||||||
const normalizedTitle = title.trim();
|
const normalizedTitle = title.trim();
|
||||||
@@ -890,6 +1009,42 @@ export function useWorkbenchAgent({
|
|||||||
if (!normalizedPrompt) {
|
if (!normalizedPrompt) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let activeSessionId = sessionIdRef.current;
|
||||||
|
if (runtimeRef.current?.purpose === "task_execution") {
|
||||||
|
const target = runtimeRef.current;
|
||||||
|
const binding = target.taskExecutionHistory;
|
||||||
|
if (!binding) throw new Error("调查执行记录缺少调试分支信息,请重新打开后再试。");
|
||||||
|
setStatusLabel("正在创建调查调试分支");
|
||||||
|
try {
|
||||||
|
const branch = await target.client.createTaskExecutionDebugSession(
|
||||||
|
binding.taskId,
|
||||||
|
binding.runId,
|
||||||
|
binding.sessionId
|
||||||
|
);
|
||||||
|
const loaded = await target.client.loadSession(branch.session_id, {
|
||||||
|
messageLimit: 40,
|
||||||
|
signal: AbortSignal.timeout(15_000)
|
||||||
|
});
|
||||||
|
if (!loaded) throw new Error("调查调试分支创建后无法读取。");
|
||||||
|
if (runtimeRef.current !== target) throw new Error("当前 Agent 会话已切换,请在新会话中重新发送。");
|
||||||
|
target.sessionId = loaded.sessionId;
|
||||||
|
target.title = loaded.title;
|
||||||
|
target.purpose = "task_collaboration";
|
||||||
|
target.taskId = binding.taskId;
|
||||||
|
target.taskExecutionHistory = undefined;
|
||||||
|
target.chat.messages = mergeHistoryMessages(target.chat.messages, toAgentUiMessages(loaded.messages));
|
||||||
|
target.messagePage = loaded.messagePage;
|
||||||
|
target.loadedAt = Date.now();
|
||||||
|
target.updatedAt = loaded.updatedAt;
|
||||||
|
target.runStatus = loaded.runStatus;
|
||||||
|
activeSessionId = loaded.sessionId;
|
||||||
|
activateRuntime(target);
|
||||||
|
setStatusLabel("调查调试会话已创建");
|
||||||
|
} catch (error) {
|
||||||
|
showMapNotice({ tone: "error", title: "调查调试会话创建失败", message: error instanceof Error ? error.message : "请稍后重试。" });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (streaming || interactionRequests.awaitingInput) {
|
if (streaming || interactionRequests.awaitingInput) {
|
||||||
const owningRuntime = runtimeRef.current;
|
const owningRuntime = runtimeRef.current;
|
||||||
@@ -925,8 +1080,8 @@ export function useWorkbenchAgent({
|
|||||||
{ text: normalizedPrompt },
|
{ text: normalizedPrompt },
|
||||||
{
|
{
|
||||||
body: {
|
body: {
|
||||||
session_id: sessionIdRef.current ?? undefined,
|
session_id: activeSessionId ?? undefined,
|
||||||
topic_id: !sessionIdRef.current ? runtimeRef.current?.topicId ?? undefined : undefined,
|
topic_id: !activeSessionId ? runtimeRef.current?.topicId ?? undefined : undefined,
|
||||||
model: selectedModel || undefined,
|
model: selectedModel || undefined,
|
||||||
approval_mode: approvalMode,
|
approval_mode: approvalMode,
|
||||||
capabilities: frontendActionEnabledRef.current ? ["frontend-action@1", "host-question@1"] : ["host-question@1"],
|
capabilities: frontendActionEnabledRef.current ? ["frontend-action@1", "host-question@1"] : ["host-question@1"],
|
||||||
@@ -935,7 +1090,7 @@ export function useWorkbenchAgent({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[approvalMode, chat, inputDelivery, interactionRequests.awaitingInput, refreshInteractionRequests, refreshPendingInputs, selectedModel, streaming, taskDiscussion]
|
[activateRuntime, approvalMode, chat, inputDelivery, interactionRequests.awaitingInput, refreshInteractionRequests, refreshPendingInputs, selectedModel, streaming, taskDiscussion]
|
||||||
);
|
);
|
||||||
|
|
||||||
const triggerEvent = useCallback(async (trigger: AgentEventTriggerContext, prompt: string) => {
|
const triggerEvent = useCallback(async (trigger: AgentEventTriggerContext, prompt: string) => {
|
||||||
@@ -1060,6 +1215,7 @@ export function useWorkbenchAgent({
|
|||||||
startNewSession,
|
startNewSession,
|
||||||
loadHistorySession,
|
loadHistorySession,
|
||||||
openTaskCollaboration,
|
openTaskCollaboration,
|
||||||
|
openTaskExecutionHistory,
|
||||||
renameHistorySession,
|
renameHistorySession,
|
||||||
deleteHistorySession,
|
deleteHistorySession,
|
||||||
sessionHistory,
|
sessionHistory,
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import { FeaturePopover } from "./components/feature-popover";
|
|||||||
import { ScheduledConditionFeed } from "./components/scheduled-condition-feed";
|
import { ScheduledConditionFeed } from "./components/scheduled-condition-feed";
|
||||||
import { TaskConversationReview } from "./components/task-conversation-review";
|
import { TaskConversationReview } from "./components/task-conversation-review";
|
||||||
import type { InvestigationDiscussion } from "./api/investigation-report";
|
import type { InvestigationDiscussion } from "./api/investigation-report";
|
||||||
|
import { fetchOperationalTaskRuns, type TaskExecutionHistoryTarget } from "./api/operational-task-client";
|
||||||
import { WorkbenchAgentPanels } from "./components/workbench-agent-panels";
|
import { WorkbenchAgentPanels } from "./components/workbench-agent-panels";
|
||||||
import { OperationalTimeline } from "./operational-timeline/operational-timeline";
|
import { OperationalTimeline } from "./operational-timeline/operational-timeline";
|
||||||
import {
|
import {
|
||||||
@@ -1013,6 +1014,29 @@ function MapWorkbenchContent({
|
|||||||
openAgentPanelForViewport();
|
openAgentPanelForViewport();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleViewTaskExecutionHistory(
|
||||||
|
taskId: string,
|
||||||
|
target: TaskExecutionHistoryTarget,
|
||||||
|
knownRunId?: string
|
||||||
|
) {
|
||||||
|
mapController.clearScadaAnalysis();
|
||||||
|
let runId = knownRunId;
|
||||||
|
if (!runId) {
|
||||||
|
const runs = await fetchOperationalTaskRuns(taskId, getAccessToken);
|
||||||
|
runId = runs.find((run) => (target.executionGeneration == null
|
||||||
|
|| run.generation === target.executionGeneration)
|
||||||
|
&& run.agent_session_id === target.sessionId)?.run_id;
|
||||||
|
}
|
||||||
|
if (!runId) {
|
||||||
|
throw new Error(target.executionGeneration == null
|
||||||
|
? "所选历史报告没有匹配的执行记录。"
|
||||||
|
: `第 ${target.executionGeneration} 轮调查没有匹配的执行记录。`);
|
||||||
|
}
|
||||||
|
await agent.openTaskExecutionHistory(taskId, runId, target.sessionId);
|
||||||
|
readingWorkspace?.close();
|
||||||
|
openAgentPanelForViewport();
|
||||||
|
}
|
||||||
|
|
||||||
async function handleDeleteAgentHistorySession(sessionId: string) {
|
async function handleDeleteAgentHistorySession(sessionId: string) {
|
||||||
const clearsActiveAnalysis = sessionId === agent.sessionId;
|
const clearsActiveAnalysis = sessionId === agent.sessionId;
|
||||||
const deleted = await agent.deleteHistorySession(sessionId);
|
const deleted = await agent.deleteHistorySession(sessionId);
|
||||||
@@ -1240,14 +1264,15 @@ function MapWorkbenchContent({
|
|||||||
promptSuggestionsLoading: promptSuggestions.loading,
|
promptSuggestionsLoading: promptSuggestions.loading,
|
||||||
promptSuggestionsUnavailable: promptSuggestions.unavailable,
|
promptSuggestionsUnavailable: promptSuggestions.unavailable,
|
||||||
activeSessionId: agent.sessionId,
|
activeSessionId: agent.sessionId,
|
||||||
qaOnly: agent.sessionPurpose === "task_collaboration",
|
qaOnly: agent.sessionPurpose === "task_collaboration" || agent.sessionPurpose === "task_execution",
|
||||||
|
executionHistory: agent.sessionPurpose === "task_execution",
|
||||||
statusLabel: agent.statusLabel,
|
statusLabel: agent.statusLabel,
|
||||||
connectionStatus: agent.connectionStatus,
|
connectionStatus: agent.connectionStatus,
|
||||||
streaming: agent.streaming,
|
streaming: agent.streaming,
|
||||||
inputDelivery: agent.inputDelivery,
|
inputDelivery: agent.inputDelivery,
|
||||||
onInputDeliveryChange: agent.setInputDelivery,
|
onInputDeliveryChange: agent.setInputDelivery,
|
||||||
pendingInputs: agent.pendingInputs,
|
pendingInputs: agent.pendingInputs,
|
||||||
interactionRequests: agent.interactionRequests,
|
interactionRequests: agent.sessionPurpose === "task_execution" ? undefined : agent.interactionRequests,
|
||||||
onPendingInputDeliveryChange: agent.updateInputDelivery,
|
onPendingInputDeliveryChange: agent.updateInputDelivery,
|
||||||
onPendingInputEdit: (id) => agent.withdrawInput(id, true),
|
onPendingInputEdit: (id) => agent.withdrawInput(id, true),
|
||||||
onPendingInputCancel: (id) => agent.withdrawInput(id, false),
|
onPendingInputCancel: (id) => agent.withdrawInput(id, false),
|
||||||
@@ -1542,6 +1567,7 @@ function MapWorkbenchContent({
|
|||||||
onLoadHistorySession={handleLoadAgentHistorySession}
|
onLoadHistorySession={handleLoadAgentHistorySession}
|
||||||
onSubmitPrompt={agent.submitPrompt}
|
onSubmitPrompt={agent.submitPrompt}
|
||||||
onOpenTaskCollaboration={handleOpenTaskCollaboration}
|
onOpenTaskCollaboration={handleOpenTaskCollaboration}
|
||||||
|
onViewTaskExecutionHistory={handleViewTaskExecutionHistory}
|
||||||
onTaskDecision={operationalTasks.decideAction}
|
onTaskDecision={operationalTasks.decideAction}
|
||||||
onTaskExecute={operationalTasks.executeAction}
|
onTaskExecute={operationalTasks.executeAction}
|
||||||
onTaskDraftUpdate={operationalTasks.updateWorkOrderDraft}
|
onTaskDraftUpdate={operationalTasks.updateWorkOrderDraft}
|
||||||
@@ -1699,6 +1725,7 @@ function MapWorkbenchContent({
|
|||||||
onLoadHistorySession={handleLoadAgentHistorySession}
|
onLoadHistorySession={handleLoadAgentHistorySession}
|
||||||
onSubmitPrompt={agent.submitPrompt}
|
onSubmitPrompt={agent.submitPrompt}
|
||||||
onOpenTaskCollaboration={handleOpenTaskCollaboration}
|
onOpenTaskCollaboration={handleOpenTaskCollaboration}
|
||||||
|
onViewTaskExecutionHistory={handleViewTaskExecutionHistory}
|
||||||
onTaskDecision={operationalTasks.decideAction}
|
onTaskDecision={operationalTasks.decideAction}
|
||||||
onTaskExecute={operationalTasks.executeAction}
|
onTaskExecute={operationalTasks.executeAction}
|
||||||
onTaskDraftUpdate={operationalTasks.updateWorkOrderDraft}
|
onTaskDraftUpdate={operationalTasks.updateWorkOrderDraft}
|
||||||
|
|||||||
@@ -0,0 +1,261 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
import type { OperationalTask, OperationalTaskSummary } from "../../src/features/workbench/api/operational-task-client";
|
||||||
|
import { mockAgentApi } from "./support/mock-agent-api";
|
||||||
|
|
||||||
|
const projectId = "324ff7ee-c2d4-5184-a84b-48fb14a2933e";
|
||||||
|
const taskId = "82000000-0000-4000-8000-000000000011";
|
||||||
|
const runId = "82500000-0000-4000-8000-000000000011";
|
||||||
|
const previousRunId = "82500000-0000-4000-8000-000000000010";
|
||||||
|
const sessionId = "investigation-execution-session";
|
||||||
|
const previousSessionId = "investigation-execution-session-previous";
|
||||||
|
const now = "2026-10-30T08:00:00Z";
|
||||||
|
|
||||||
|
const latestRun: NonNullable<OperationalTask["latest_run"]> = {
|
||||||
|
run_id: runId,
|
||||||
|
task_id: taskId,
|
||||||
|
generation: 2,
|
||||||
|
execution_request_id: "85000000-0000-4000-8000-000000000011",
|
||||||
|
status: "succeeded",
|
||||||
|
revision: 4,
|
||||||
|
agent_session_id: sessionId,
|
||||||
|
agent_instance_id: "agent-supply-1",
|
||||||
|
lease_expires_at: null,
|
||||||
|
heartbeat_at: now,
|
||||||
|
started_at: now,
|
||||||
|
finished_at: now,
|
||||||
|
failure_code: null,
|
||||||
|
failure_detail: null,
|
||||||
|
dispatch_attempts: 1,
|
||||||
|
next_dispatch_at: now,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now
|
||||||
|
};
|
||||||
|
|
||||||
|
const previousRun: NonNullable<OperationalTask["latest_run"]> = {
|
||||||
|
...latestRun,
|
||||||
|
run_id: previousRunId,
|
||||||
|
generation: 1,
|
||||||
|
execution_request_id: "85000000-0000-4000-8000-000000000010",
|
||||||
|
agent_session_id: previousSessionId,
|
||||||
|
created_at: "2026-10-29T08:00:00Z",
|
||||||
|
updated_at: "2026-10-29T08:05:00Z"
|
||||||
|
};
|
||||||
|
|
||||||
|
const latestReport: NonNullable<OperationalTask["latest_report"]> = {
|
||||||
|
revision: 2,
|
||||||
|
execution_generation: 2,
|
||||||
|
source_session_id: sessionId,
|
||||||
|
published_at: now,
|
||||||
|
published_by: "agent",
|
||||||
|
report: {
|
||||||
|
title: "南桥爆管调查报告",
|
||||||
|
current_conclusion: "第二轮已完成现场记录复核",
|
||||||
|
conclusion_boundary: "以第二轮调查数据为准",
|
||||||
|
evidence_refs: [],
|
||||||
|
recommendations: [],
|
||||||
|
pending_items: []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const previousReport: NonNullable<OperationalTask["latest_report"]> = {
|
||||||
|
...latestReport,
|
||||||
|
revision: 1,
|
||||||
|
execution_generation: 1,
|
||||||
|
source_session_id: previousSessionId,
|
||||||
|
published_at: "2026-10-29T08:05:00Z",
|
||||||
|
report: {
|
||||||
|
...latestReport.report,
|
||||||
|
current_conclusion: "第一轮完成压力与流量核对",
|
||||||
|
conclusion_boundary: "尚未取得现场阀门记录"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const task: OperationalTask = {
|
||||||
|
project_id: projectId,
|
||||||
|
task_id: taskId,
|
||||||
|
task_type: "event_investigation",
|
||||||
|
workflow: "burst-report",
|
||||||
|
lifecycle_status: "completed",
|
||||||
|
active_run: null,
|
||||||
|
latest_run: latestRun,
|
||||||
|
status: "completed",
|
||||||
|
priority: "normal",
|
||||||
|
revision: 2,
|
||||||
|
execution_generation: 2,
|
||||||
|
execution_request_id: "85000000-0000-4000-8000-000000000011",
|
||||||
|
title: "南桥爆管调查",
|
||||||
|
summary: "核对压力、流量和现场反馈",
|
||||||
|
source: { kind: "operator", external_id: null, external_revision: null, event_kind: "burst" },
|
||||||
|
trigger: { kind: "manual", occurred_at: now, detail: "调试调查执行历史" },
|
||||||
|
scheduled_for: null,
|
||||||
|
agent_session_id: sessionId,
|
||||||
|
agent_lease_expires_at: null,
|
||||||
|
current_plan_revision: null,
|
||||||
|
current_report_revision: 2,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
latest_plan: null,
|
||||||
|
latest_report: latestReport,
|
||||||
|
latest_progress: null,
|
||||||
|
evidence: [],
|
||||||
|
actions: [],
|
||||||
|
access_requests: [],
|
||||||
|
permissions: {
|
||||||
|
can_publish_artifacts: false,
|
||||||
|
can_propose_actions: false,
|
||||||
|
can_sync_external_feedback: false,
|
||||||
|
can_decide_work_orders: false,
|
||||||
|
can_execute_work_orders: false,
|
||||||
|
can_decide_commands: false,
|
||||||
|
can_execute_commands: false,
|
||||||
|
can_decide_access: false
|
||||||
|
},
|
||||||
|
work_order_snapshot: null
|
||||||
|
};
|
||||||
|
|
||||||
|
const summary = (item: OperationalTask): OperationalTaskSummary => {
|
||||||
|
const { latest_plan, latest_report, latest_progress, evidence, actions, access_requests, permissions, work_order_snapshot, ...value } = item;
|
||||||
|
void latest_plan; void latest_report; void latest_progress; void evidence;
|
||||||
|
void actions; void access_requests; void permissions; void work_order_snapshot;
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
test("调查任务执行记录自动刷新并可创建调试会话追加消息", async ({ page }, testInfo) => {
|
||||||
|
await page.emulateMedia({ reducedMotion: "reduce" });
|
||||||
|
await mockAgentApi(page);
|
||||||
|
let executionHistoryReads = 0;
|
||||||
|
let debugBranchCreates = 0;
|
||||||
|
const openedBindings: Array<{ runId: string | null; sessionId: string | null }> = [];
|
||||||
|
let debugBranchPayload: unknown;
|
||||||
|
await page.route(`http://127.0.0.1:8787/api/v1/task/${taskId}/execution-session?**`, async (route) => {
|
||||||
|
executionHistoryReads += 1;
|
||||||
|
const refreshed = executionHistoryReads > 1;
|
||||||
|
const query = new URL(route.request().url()).searchParams;
|
||||||
|
openedBindings.push({ runId: query.get("run_id"), sessionId: query.get("session_id") });
|
||||||
|
await route.fulfill({
|
||||||
|
headers: { "Access-Control-Allow-Origin": "*" },
|
||||||
|
json: {
|
||||||
|
id: previousSessionId,
|
||||||
|
session_id: previousSessionId,
|
||||||
|
title: "南桥爆管调查 · 第一轮执行记录",
|
||||||
|
purpose: "task_execution",
|
||||||
|
task_id: taskId,
|
||||||
|
status: "active",
|
||||||
|
created_at: now,
|
||||||
|
updated_at: refreshed ? "2026-10-30T08:01:00Z" : now,
|
||||||
|
messages: [
|
||||||
|
{ id: "execution-user", role: "user", parts: [{ type: "text", text: "执行后台调查任务" }] },
|
||||||
|
{ id: "execution-assistant", role: "assistant", parts: [
|
||||||
|
{ type: "data-progress", data: { id: "pressure-flow", phase: "tool", title: "核对第一轮压力与流量", detail: "查询监测记录并校验时间范围。", status: "completed" } },
|
||||||
|
{ type: "text", text: "第一轮已完成压力与流量证据核对" }
|
||||||
|
] },
|
||||||
|
...(refreshed ? [{ id: "execution-refresh", role: "assistant", parts: [{ type: "text", text: "第一轮自动同步发现现场阀门记录" }] }] : [])
|
||||||
|
],
|
||||||
|
is_streaming: !refreshed,
|
||||||
|
run_status: refreshed ? "completed" : "running",
|
||||||
|
read_only: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
page.on("request", (request) => {
|
||||||
|
if (request.method() === "POST" && request.url().endsWith(`/task/${taskId}/execution-session/debug`)) {
|
||||||
|
debugBranchCreates += 1;
|
||||||
|
debugBranchPayload = request.postDataJSON();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await page.route("**/runtime-config.js", (route) => route.fulfill({
|
||||||
|
contentType: "text/javascript",
|
||||||
|
body: `globalThis.__TJWATER_CONFIG__=${JSON.stringify({
|
||||||
|
TJWATER_AUTH_MODE: "disabled",
|
||||||
|
TJWATER_SERVER_API_BASE_URL: "http://server.test",
|
||||||
|
TJWATER_MAP_URL: "http://map.test/geoserver",
|
||||||
|
TJWATER_GEOSERVER_WORKSPACE: "tjwater_next"
|
||||||
|
})}`
|
||||||
|
}));
|
||||||
|
await page.route("http://server.test/api/v1/projects**", (route) => route.fulfill({ json: {
|
||||||
|
items: [{ project_id: projectId, gs_workspace: "tjwater_next", status: "active" }], total: 1, limit: 1000, offset: 0
|
||||||
|
} }));
|
||||||
|
await page.route("http://server.test/api/v1/scada-devices**", (route) => route.fulfill({ json: { items: [], total: 0, limit: 1000, offset: 0 } }));
|
||||||
|
await page.route("http://server.test/api/v1/workbench/sync-snapshot", (route) => route.fulfill({ json: {
|
||||||
|
schema: "workbench-sync-snapshot@1",
|
||||||
|
event_cursor: 0,
|
||||||
|
runtime: {
|
||||||
|
availability: "not_configured", source: "none", scenario_availability: "not_configured",
|
||||||
|
artifact_availability: "not_configured", schedule_availability: "not_configured",
|
||||||
|
work_order_availability: "not_configured", alert_availability: "not_configured",
|
||||||
|
generated_at: now, conditions: [], alerts: [], scenarios: [], latest_analysis: null, work_orders: []
|
||||||
|
},
|
||||||
|
tasks: [summary(task)],
|
||||||
|
notifications: [],
|
||||||
|
generated_at: now
|
||||||
|
} }));
|
||||||
|
await page.route("http://server.test/api/v1/workbench/events**", (route) => route.fulfill({ contentType: "text/event-stream", body: ": keepalive\n\n" }));
|
||||||
|
await page.route("http://server.test/api/v1/operational-task-notifications**", (route) => route.fulfill({ json: { items: [], limit: 100, offset: 0, next_offset: null } }));
|
||||||
|
await page.route("http://server.test/api/v1/operational-tasks**", (route) => {
|
||||||
|
const path = new URL(route.request().url()).pathname;
|
||||||
|
if (path.endsWith("/operational-tasks/schedules")) return route.fulfill({ json: { items: [] } });
|
||||||
|
if (path.endsWith(`/operational-tasks/${taskId}/reports`)) return route.fulfill({ json: {
|
||||||
|
items: [latestReport, previousReport], next_offset: null
|
||||||
|
} });
|
||||||
|
if (path.endsWith(`/operational-tasks/${taskId}/runs`)) return route.fulfill({ json: {
|
||||||
|
items: [latestRun, previousRun], next_cursor: null
|
||||||
|
} });
|
||||||
|
if (path.endsWith(`/operational-tasks/${taskId}`)) return route.fulfill({ json: task });
|
||||||
|
if (path.endsWith("/operational-tasks")) return route.fulfill({ json: { items: [summary(task)], limit: 50, offset: 0, next_offset: null } });
|
||||||
|
return route.fulfill({ status: 404, json: {} });
|
||||||
|
});
|
||||||
|
await page.route("http://map.test/**", (route) => route.fulfill({ status: 204 }));
|
||||||
|
|
||||||
|
await page.goto("/");
|
||||||
|
await page.getByRole("button", { name: "工况任务", exact: true }).click();
|
||||||
|
const taskPanel = page.getByRole("region", { name: "工况任务", exact: true });
|
||||||
|
const expand = taskPanel.getByRole("button", { name: "展开工况任务" });
|
||||||
|
if (await expand.isVisible()) await expand.click();
|
||||||
|
await taskPanel.getByRole("button", { name: /调查任务/ }).click();
|
||||||
|
await taskPanel.getByText("南桥爆管调查", { exact: true }).first().click();
|
||||||
|
await expect(taskPanel.getByText("调查任务 · 第 2 轮", { exact: true })).toBeVisible();
|
||||||
|
await taskPanel.getByRole("button", { name: "展开报告版本" }).click();
|
||||||
|
await page.getByRole("dialog", { name: "报告版本" }).getByRole("button", { name: /第 1 轮 · 报告第 1 版/ }).click();
|
||||||
|
await expect(taskPanel.getByText("调查任务 · 第 1 轮", { exact: true })).toBeVisible();
|
||||||
|
|
||||||
|
const executionHistory = taskPanel.getByRole("button", { name: "执行记录" });
|
||||||
|
await expect(executionHistory).toBeVisible();
|
||||||
|
await executionHistory.click();
|
||||||
|
|
||||||
|
const agentPanel = page.getByRole("complementary", { name: "Agent 命令面板" });
|
||||||
|
await expect(agentPanel.getByRole("note").getByText("执行记录自动同步", { exact: true })).toBeVisible();
|
||||||
|
await expect(agentPanel).toContainText("第一轮已完成压力与流量证据核对");
|
||||||
|
const processToggle = agentPanel.getByRole("button", { name: "查看处理过程", exact: true }).first();
|
||||||
|
await expect(processToggle).toBeVisible();
|
||||||
|
await processToggle.click();
|
||||||
|
await expect(agentPanel.getByText("核对第一轮压力与流量", { exact: true })).toBeVisible();
|
||||||
|
await expect(agentPanel.locator("textarea")).toBeVisible();
|
||||||
|
await expect(agentPanel).toContainText("第一轮自动同步发现现场阀门记录", { timeout: 5_000 });
|
||||||
|
expect(executionHistoryReads).toBeGreaterThanOrEqual(2);
|
||||||
|
expect(openedBindings).toEqual(expect.arrayContaining([
|
||||||
|
{ runId: previousRunId, sessionId: previousSessionId }
|
||||||
|
]));
|
||||||
|
|
||||||
|
await agentPanel.locator("textarea").fill("继续核对失败前最后一次工具调用");
|
||||||
|
await agentPanel.getByRole("button", { name: "发送 Agent 指令" }).click();
|
||||||
|
await expect.poll(() => debugBranchCreates).toBe(1);
|
||||||
|
expect(debugBranchPayload).toEqual({ run_id: previousRunId, session_id: previousSessionId });
|
||||||
|
await expect(agentPanel).toContainText("继续核对失败前最后一次工具调用");
|
||||||
|
await expect(agentPanel.getByRole("note").getByText("执行记录自动同步", { exact: true })).toHaveCount(0);
|
||||||
|
await testInfo.attach("desktop-execution-history", {
|
||||||
|
body: await agentPanel.screenshot(), contentType: "image/png"
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "折叠 Agent 面板", exact: true }).click();
|
||||||
|
await page.setViewportSize({ width: 375, height: 720 });
|
||||||
|
const openTasks = page.getByRole("button", { name: "打开工况任务", exact: true });
|
||||||
|
if (await openTasks.isVisible()) await openTasks.click();
|
||||||
|
const mobilePanel = page.getByRole("region", { name: "工况任务", exact: true });
|
||||||
|
const mobileExpand = mobilePanel.getByRole("button", { name: "展开工况任务" });
|
||||||
|
if (await mobileExpand.isVisible()) await mobileExpand.click();
|
||||||
|
await mobilePanel.getByRole("button", { name: /调查任务/ }).click();
|
||||||
|
await expect(mobilePanel.getByRole("button", { name: "执行记录" })).toBeVisible();
|
||||||
|
await testInfo.attach("mobile-execution-history-button", {
|
||||||
|
body: await page.getByRole("region", { name: "工况任务抽屉" }).screenshot(), contentType: "image/png"
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -21,6 +21,7 @@ export async function mockAgentApi(page: Page, options: MockAgentApiOptions = {}
|
|||||||
icon: "bolt" as const
|
icon: "bolt" as const
|
||||||
};
|
};
|
||||||
const triggeredSessions = new Map<string, { sessionId: string; triggerId: string }>();
|
const triggeredSessions = new Map<string, { sessionId: string; triggerId: string }>();
|
||||||
|
const executionDebugSessions = new Map<string, { taskId: string; sourceSessionId: string }>();
|
||||||
await page.route("http://127.0.0.1:8787/api/v1/**", async (route) => {
|
await page.route("http://127.0.0.1:8787/api/v1/**", async (route) => {
|
||||||
if (route.request().method() === "OPTIONS") {
|
if (route.request().method() === "OPTIONS") {
|
||||||
await route.fulfill({
|
await route.fulfill({
|
||||||
@@ -70,6 +71,58 @@ export async function mockAgentApi(page: Page, options: MockAgentApiOptions = {}
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const executionHistoryMatch = pathname.match(/\/task\/([^/]+)\/execution-session$/);
|
||||||
|
if (executionHistoryMatch && route.request().method() === "GET") {
|
||||||
|
const url = new URL(route.request().url());
|
||||||
|
const sessionId = url.searchParams.get("session_id") ?? "task-execution-session";
|
||||||
|
await route.fulfill({
|
||||||
|
headers: corsHeaders,
|
||||||
|
json: {
|
||||||
|
id: sessionId,
|
||||||
|
session_id: sessionId,
|
||||||
|
title: "水质投诉动态调查 · 执行记录",
|
||||||
|
purpose: "task_execution",
|
||||||
|
task_id: executionHistoryMatch[1],
|
||||||
|
status: "active",
|
||||||
|
created_at: "2026-09-22T02:00:00Z",
|
||||||
|
updated_at: "2026-09-22T02:10:00Z",
|
||||||
|
messages: [
|
||||||
|
{ id: "execution-user", role: "user", parts: [{ type: "text", text: "执行后台调查任务" }] },
|
||||||
|
{ id: "execution-assistant", role: "assistant", parts: [{ type: "text", text: "已完成压力与流量证据核对" }] }
|
||||||
|
],
|
||||||
|
is_streaming: false,
|
||||||
|
run_status: "completed",
|
||||||
|
read_only: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const executionDebugMatch = pathname.match(/\/task\/([^/]+)\/execution-session\/debug$/);
|
||||||
|
if (executionDebugMatch && route.request().method() === "POST") {
|
||||||
|
const body = (route.request().postDataJSON() ?? {}) as { session_id?: string };
|
||||||
|
const sessionId = `execution-debug-${executionDebugMatch[1]}`;
|
||||||
|
const created = !executionDebugSessions.has(sessionId);
|
||||||
|
executionDebugSessions.set(sessionId, {
|
||||||
|
taskId: executionDebugMatch[1],
|
||||||
|
sourceSessionId: body.session_id ?? "task-execution-session"
|
||||||
|
});
|
||||||
|
await route.fulfill({
|
||||||
|
status: created ? 201 : 200,
|
||||||
|
headers: corsHeaders,
|
||||||
|
json: {
|
||||||
|
session_id: sessionId,
|
||||||
|
title: "水质投诉动态调查 · 调试",
|
||||||
|
purpose: "task_collaboration",
|
||||||
|
task_id: executionDebugMatch[1],
|
||||||
|
parent_session_id: body.session_id,
|
||||||
|
status: "active",
|
||||||
|
created
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (pathname.endsWith("/session") && route.request().method() === "POST") {
|
if (pathname.endsWith("/session") && route.request().method() === "POST") {
|
||||||
const body = (route.request().postDataJSON() ?? {}) as {
|
const body = (route.request().postDataJSON() ?? {}) as {
|
||||||
trigger_context?: { trigger_id?: string };
|
trigger_context?: { trigger_id?: string };
|
||||||
@@ -98,25 +151,34 @@ export async function mockAgentApi(page: Page, options: MockAgentApiOptions = {}
|
|||||||
const sessionMatch = pathname.match(/\/session\/([^/]+)$/);
|
const sessionMatch = pathname.match(/\/session\/([^/]+)$/);
|
||||||
if (sessionMatch && route.request().method() === "GET") {
|
if (sessionMatch && route.request().method() === "GET") {
|
||||||
const collaboration = sessionMatch[1].startsWith("collaboration-");
|
const collaboration = sessionMatch[1].startsWith("collaboration-");
|
||||||
|
const executionDebug = executionDebugSessions.get(sessionMatch[1]);
|
||||||
const stored = options.sessions?.find((session) => (session.session_id ?? session.id) === sessionMatch[1]);
|
const stored = options.sessions?.find((session) => (session.session_id ?? session.id) === sessionMatch[1]);
|
||||||
await route.fulfill({
|
await route.fulfill({
|
||||||
headers: corsHeaders,
|
headers: corsHeaders,
|
||||||
json: {
|
json: {
|
||||||
id: sessionMatch[1],
|
id: sessionMatch[1],
|
||||||
session_id: sessionMatch[1],
|
session_id: sessionMatch[1],
|
||||||
title: stored?.title ?? (collaboration ? "任务协作问答" : "水质投诉事件调查"),
|
title: stored?.title ?? (executionDebug ? "水质投诉动态调查 · 调试" : collaboration ? "任务协作问答" : "水质投诉事件调查"),
|
||||||
purpose: stored?.purpose ?? (collaboration ? "task_collaboration" : "ordinary_chat"),
|
purpose: stored?.purpose ?? (executionDebug || collaboration ? "task_collaboration" : "ordinary_chat"),
|
||||||
task_id: collaboration ? sessionMatch[1].slice("collaboration-".length) : undefined,
|
task_id: executionDebug?.taskId ?? (collaboration ? sessionMatch[1].slice("collaboration-".length) : undefined),
|
||||||
status: "active",
|
status: "active",
|
||||||
created_at: "2026-09-22T02:00:00Z",
|
created_at: "2026-09-22T02:00:00Z",
|
||||||
updated_at: "2026-09-22T02:00:00Z",
|
updated_at: "2026-09-22T02:00:00Z",
|
||||||
messages: [],
|
messages: executionDebug ? [
|
||||||
|
{ id: "execution-user", role: "user", parts: [{ type: "text", text: "执行后台调查任务" }] },
|
||||||
|
{ id: "execution-assistant", role: "assistant", parts: [{ type: "text", text: "已完成压力与流量证据核对" }] }
|
||||||
|
] : [],
|
||||||
is_streaming: false
|
is_streaming: false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (/\/session\/[^/]+\/interactions$/.test(pathname)) {
|
||||||
|
await route.fulfill({ headers: corsHeaders, json: { items: [] } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (pathname.endsWith("/responses")) {
|
if (pathname.endsWith("/responses")) {
|
||||||
await route.fulfill({
|
await route.fulfill({
|
||||||
status: 200,
|
status: 200,
|
||||||
|
|||||||
Reference in New Issue
Block a user