import { z } from "zod"; import { investigationReportSchema, investigationDiagramSchema } from "./investigation-report"; import { investigationPresentationCursorSchema } from "./investigation-presentation"; import type { AccessTokenProvider } from "@/shared/auth/keycloak-auth"; import { env } from "@/shared/config/env"; import { createWorkbenchHeaders, findOwnedWorkbenchProject, getServerError } from "./project-client"; const taskStatusSchema = z.enum(["queued", "running", "waiting_human", "completed", "failed", "cancelled"]); const taskPrioritySchema = z.enum(["normal", "urgent"]); const workbenchDateTimeSchema = z.string().datetime({ offset: true }); const taskSourceSchema = z.object({ kind: z.enum(["schedule", "external_system", "operator", "agent"]), external_id: z.string().min(1).nullable(), external_revision: z.number().int().positive().nullable(), event_kind: z.string().min(1).nullable(), deleted_at: workbenchDateTimeSchema.nullish() }).strict(); const taskTriggerSchema = z.object({ kind: z.enum(["schedule", "external_sync", "event", "manual"]), occurred_at: workbenchDateTimeSchema, detail: z.string().min(1) }).strict(); const operationalTaskRunSchema = z.object({ run_id: z.string().uuid(), task_id: z.string().uuid(), generation: z.number().int().positive(), execution_request_id: z.string().uuid(), status: z.enum(["queued", "claimed", "running", "succeeded", "failed", "timed_out", "cancelled"]), revision: z.number().int().positive(), agent_session_id: z.string().min(1).nullable(), lease_expires_at: workbenchDateTimeSchema.nullable(), heartbeat_at: workbenchDateTimeSchema.nullable(), started_at: workbenchDateTimeSchema.nullable(), finished_at: workbenchDateTimeSchema.nullable(), failure_code: z.string().min(1).nullable(), failure_detail: z.string().min(1).nullable(), dispatch_attempts: z.number().int().nonnegative(), next_dispatch_at: workbenchDateTimeSchema, created_at: workbenchDateTimeSchema, updated_at: workbenchDateTimeSchema }).strict(); const taskSummarySchema = z.object({ project_id: z.string().uuid(), task_id: z.string().uuid(), task_type: z.enum(["scheduled_flow", "external_work_order", "event_investigation"]), workflow: z.string().min(1), lifecycle_status: z.enum(["open", "waiting_human", "completed", "failed", "cancelled"]), active_run: operationalTaskRunSchema.nullable(), latest_run: operationalTaskRunSchema.nullable().optional(), status: taskStatusSchema, priority: taskPrioritySchema, revision: z.number().int().positive(), execution_generation: z.number().int().positive(), execution_request_id: z.string().uuid(), title: z.string().min(1), summary: z.string().min(1), source: taskSourceSchema, trigger: taskTriggerSchema, scheduled_for: workbenchDateTimeSchema.nullable(), agent_session_id: z.string().min(1).nullable(), agent_lease_expires_at: workbenchDateTimeSchema.nullable(), current_plan_revision: z.number().int().positive().nullable(), current_report_revision: z.number().int().positive().nullable(), created_at: workbenchDateTimeSchema, updated_at: workbenchDateTimeSchema }).strict(); const evidenceSchema = z.object({ evidence_id: z.string().min(1), kind: z.enum(["system_data", "external_feedback", "human_input", "synthetic"]), summary: z.string().min(1), source_label: z.string().min(1), source_ref: z.string().min(1).nullable(), observed_at: workbenchDateTimeSchema, recorded_at: workbenchDateTimeSchema, recorded_by: z.string().min(1), has_snapshot: z.boolean() }).strict(); const planNodeSchema = z.object({ id: z.string().min(1), title: z.string().min(1), owner: z.enum(["agent", "human"]), status: z.enum(["pending", "waiting", "running", "completed", "failed", "skipped", "cancelled"]), detail: z.string().min(1), evidence_refs: z.array(z.string().min(1)), action_refs: z.array(z.string().uuid()), kind: z.enum(["activity", "decision", "parallel_split", "parallel_join", "gate"]).nullish(), selected_edge_id: z.string().min(1).nullish(), gate_ref: z.string().uuid().nullish(), revisits_node_id: z.string().min(1).nullish() }).strict(); const planInputSchema = z.object({ schema: z.enum(["execution-plan@1", "execution-plan@2"]), title: z.string().min(1), reason: z.string().min(1), nodes: z.array(planNodeSchema).min(1).max(100), edges: z.array(z.object({ id: z.string().min(1).nullish(), from: z.string().min(1), to: z.string().min(1), condition: z.string().min(1).nullable() }).strict()).max(300) }).strict(); const planSnapshotSchema = z.object({ revision: z.number().int().positive(), execution_generation: z.number().int().positive().nullish(), source_session_id: z.string().min(1), published_at: workbenchDateTimeSchema, published_by: z.string().min(1), plan: planInputSchema }).strict(); const reportInputSchema = investigationReportSchema; const reportRevisionSchema = z.object({ revision: z.number().int().positive(), execution_generation: z.number().int().positive().nullish(), source_session_id: z.string().min(1), published_at: workbenchDateTimeSchema, published_by: z.string().min(1), report: reportInputSchema }).strict(); const progressSnapshotSchema = z.object({ revision: z.number().int().positive(), run_id: z.string().uuid(), execution_generation: z.number().int().positive(), published_at: workbenchDateTimeSchema, source_session_id: z.string().min(1), progress: z.object({ summary: z.string().trim().min(1), diagram: investigationDiagramSchema.nullable() }).strict() }).strict(); const reportPageSchema = z.object({ items: z.array(reportRevisionSchema), next_offset: z.number().int().nonnegative().nullable(), }).strict(); const actionReceiptSchema = z.object({ adapter: z.string().min(1), receipt_id: z.string().min(1), submitted_at: workbenchDateTimeSchema, completed_at: workbenchDateTimeSchema.nullable(), external_status: z.string().min(1), detail: z.string().min(1) }).strict(); const actionSchema = z.object({ action_id: z.string().uuid(), task_id: z.string().uuid(), version: z.number().int().positive(), kind: z.enum(["create_work_order", "issue_command"]), reason: z.string().min(1), payload: z.record(z.string(), z.unknown()), evidence_refs: z.array(z.string().min(1)), node_id: z.string().min(1).nullable(), required_approval_permission: z.string().min(1), required_execution_permission: z.string().min(1), approval_status: z.enum(["pending", "approved", "rejected"]), execution_status: z.enum(["not_submitted", "submitted", "succeeded", "failed", "unsupported", "unknown"]), execution_request_id: z.string().uuid().nullable(), proposed_by: z.string().min(1), proposed_at: workbenchDateTimeSchema, edited_by: z.string().min(1).nullish(), edited_at: workbenchDateTimeSchema.nullish(), decided_by: z.string().min(1).nullable(), decided_at: workbenchDateTimeSchema.nullable(), decision_comment: z.string().min(1).nullable(), executed_by: z.string().min(1).nullable(), executed_at: workbenchDateTimeSchema.nullable(), receipt: actionReceiptSchema.nullable(), execution_detail: z.string().min(1).nullable() }).strict(); const accessRequestSchema = z.object({ access_request_id: z.string().uuid(), task_id: z.string().uuid(), node_id: z.string().min(1), scopes: z.array(z.string().min(1)), reason: z.string().min(1), status: z.enum(["pending", "approved", "rejected"]), approved_scopes: z.array(z.string().min(1)), version: z.number().int().positive(), requested_by: z.string().min(1), requested_at: workbenchDateTimeSchema, decided_by: z.string().min(1).nullable(), decided_at: workbenchDateTimeSchema.nullable(), comment: z.string().min(1).nullable() }).strict(); const taskPermissionsSchema = z.object({ can_publish_artifacts: z.boolean(), can_propose_actions: z.boolean(), can_sync_external_feedback: z.boolean(), can_decide_work_orders: z.boolean(), can_execute_work_orders: z.boolean(), can_decide_commands: z.boolean(), can_execute_commands: z.boolean(), can_decide_access: z.boolean().default(false) }).strict(); const externalWorkOrderActivitySchema = z.object({ sequence: z.number().int().positive(), occurred_at: workbenchDateTimeSchema, actor: z.string().min(1), kind: z.enum(["created", "updated", "feedback"]), title: z.string().min(1), detail: z.string().min(1) }).strict(); const externalWorkOrderSnapshotSchema = z.preprocess((value) => { if (value && typeof value === "object" && !Array.isArray(value)) { const { subtasks: _legacySubtasks, ...flat } = value as Record; return flat; } return value; }, z.object({ work_order_id: z.string().uuid(), revision: z.number().int().positive(), title: z.string().min(1), summary: z.string().min(1), status: z.enum(["draft", "open", "in_progress", "awaiting_reply", "completed", "cancelled"]), priority: z.enum(["low", "normal", "high", "urgent"]), assignee: z.string().min(1).nullable(), scheduled_at: workbenchDateTimeSchema.nullable(), work_order_type: z.enum(["general", "water_quality_complaint", "burst_report"]), location: z.string().min(1).nullable(), scenario_code: z.string().min(1).nullable(), feedback_requirements: z.array(z.string().min(1)).default([]), activity: z.array(externalWorkOrderActivitySchema).default([]), origin: z.literal("synthetic"), updated_at: workbenchDateTimeSchema }).strict()); const operationalTaskSchema = taskSummarySchema.extend({ latest_plan: planSnapshotSchema.nullable(), latest_report: reportRevisionSchema.nullable(), latest_progress: progressSnapshotSchema.nullish(), evidence: z.array(evidenceSchema), actions: z.array(actionSchema), access_requests: z.array(accessRequestSchema).default([]), permissions: taskPermissionsSchema, work_order_snapshot: externalWorkOrderSnapshotSchema.nullable().default(null) }).strict(); const notificationSchema = z.object({ notification_id: z.string().uuid(), task_id: z.string().uuid(), kind: z.enum([ "task_created", "action_pending", "report_updated", "task_completed", "task_failed", "run_delivery_degraded", "access_approval_pending", "action_approval_pending", "human_input_required", "external_coordination_failed" ]), title: z.string().min(1), description: z.string().min(1), lifecycle_status: z.enum(["active", "resolved"]).default("active"), subject_type: z.enum(["task", "run", "access_request", "action"]).default("task"), subject_id: z.string().uuid().nullable().default(null), report_reference: z.object({ execution_generation: z.number().int().positive(), revision: z.number().int().positive() }).strict().nullish(), resolved_at: workbenchDateTimeSchema.nullable().default(null), resolution_reason: z.string().min(1).nullable().default(null), status: z.enum(["unread", "read"]), version: z.number().int().positive(), required_permission: z.string().min(1).nullable(), created_at: workbenchDateTimeSchema, read_at: workbenchDateTimeSchema.nullable(), read_by: z.string().min(1).nullable() }).strict(); const notificationPageSchema = z.object({ items: z.array(notificationSchema), limit: z.number().int().positive(), offset: z.number().int().nonnegative(), next_offset: z.number().int().nonnegative().nullable() }).strict(); const scheduleSchema = z.object({ schedule_id: z.string().uuid(), schedule_key: z.string().min(1), title: z.string().min(1), summary: z.string().min(1), interval_seconds: z.number().int().min(60).max(31_536_000), next_run_at: workbenchDateTimeSchema, priority: taskPrioritySchema, enabled: z.boolean(), version: z.number().int().positive(), created_at: workbenchDateTimeSchema, updated_at: workbenchDateTimeSchema }).strict(); const schedulePageSchema = z.object({ items: z.array(scheduleSchema) }).strict(); const taskDataSnapshotSchema = z.object({ // Snapshot sources include product MCP queries; access-request scopes remain unchanged. source: z.enum(["workbench_runtime", "scada_devices", "scada_readings", "peripheral_points", "peripheral_work_orders", "peripheral_observations", "peripheral_test_data", "get_analysis_run", "get_element_gis_properties", "get_element_properties", "get_model_baseline_at_time", "get_network_options", "get_network_schema", "get_scada_device", "get_sensor_placement_run", "get_source_gis_properties", "get_task_context", "get_task_evidence", "get_workbench_runtime", "list_analysis_results", "list_analysis_runs", "list_elements", "list_scada_devices", "list_sensor_placement_runs", "query_analysis_series", "query_analysis_values", "query_element_history", "query_model_baseline_field", "query_model_baseline_series", "query_peripheral_data", "query_scada_readings", "run_burst_detection", "run_simulation", "run_contaminant_simulation", "analyze_valve_closure", "analyze_flushing", "analyze_pressure_regulation", "optimize_sensor_placement", "run_burst_location", "analyze_valve_isolation", "run_leakage_identification", ]), source_ref: z.string().min(1), queried_at: workbenchDateTimeSchema, origin: z.enum(["system_data", "synthetic"]), data: z.unknown(), truncated: z.boolean() }).strict(); export type OperationalTaskSummary = z.infer; export type OperationalTask = z.infer; export type OperationalTaskPlan = z.infer; export type OperationalTaskAction = z.infer; export type OperationalTaskAccessRequest = z.infer; export type OperationalTaskEvidence = z.infer; export type OperationalTaskNotification = z.infer; export type OperationalTaskReport = z.infer; export type OperationalTaskReportPage = z.infer; export type OperationalTaskProgress = z.infer; export type OperationalTaskSchedule = z.infer; export type OperationalTaskDataSnapshot = z.infer; export type ExternalWorkOrderSnapshot = z.infer; export type OperationalTaskDecision = "approved" | "rejected"; export function parseOperationalTask(value: unknown) { return operationalTaskSchema.parse(value); } export function parseOperationalTaskSummary(value: unknown) { return taskSummarySchema.parse(value); } export function parseOperationalTaskNotification(value: unknown) { return notificationSchema.parse(value); } export type CreateOperationalTaskInput = { request_id: string; task_type: OperationalTask["task_type"]; workflow: string; title: string; summary: string; priority: OperationalTask["priority"]; source: OperationalTask["source"]; trigger: OperationalTask["trigger"]; scheduled_for?: string; agent_session_id?: string; }; const SERVER_API_BASE_URL = env.TJWATER_SERVER_API_BASE_URL.replace(/\/$/, ""); async function operationalTaskRequest(path: string, getAccessToken?: AccessTokenProvider, init?: RequestInit) { const { project, accessToken } = await findOwnedWorkbenchProject(getAccessToken, init?.signal ?? undefined); const headers = new Headers(createWorkbenchHeaders(accessToken, project.project_id)); if (init?.body) headers.set("Content-Type", "application/json"); const response = await fetch(`${SERVER_API_BASE_URL}/api/v1${path}`, { ...init, headers }); const payload: unknown = await response.json().catch(() => null); if (!response.ok) throw new Error(getServerError(payload, response.status, "工况任务请求")); return payload; } export async function fetchOperationalTask(taskId: string, getAccessToken?: AccessTokenProvider, signal?: AbortSignal) { return operationalTaskSchema.parse(await operationalTaskRequest( `/operational-tasks/${encodeURIComponent(taskId)}`, getAccessToken, { signal } )); } export async function fetchOperationalTaskReports(taskId: string, offset: number, getAccessToken?: AccessTokenProvider) { return reportPageSchema.parse(await operationalTaskRequest( `/operational-tasks/${encodeURIComponent(taskId)}/reports?limit=20&offset=${offset}`, getAccessToken, )); } export async function fetchOperationalTaskPresentationCursor(taskId: string, getAccessToken?: AccessTokenProvider, signal?: AbortSignal) { return investigationPresentationCursorSchema.parse(await operationalTaskRequest( `/operational-tasks/${encodeURIComponent(taskId)}/presentation-cursor`, getAccessToken, { signal } )); } export async function createOperationalTask(input: CreateOperationalTaskInput, getAccessToken?: AccessTokenProvider) { return operationalTaskSchema.parse(await operationalTaskRequest( "/operational-tasks", getAccessToken, { method: "POST", body: JSON.stringify(input) } )); } export async function decideOperationalTaskAction(taskId: string, actionId: string, input: { request_id: string; expected_version: number; decision: OperationalTaskDecision; comment?: string; }, getAccessToken?: AccessTokenProvider) { return actionSchema.parse(await operationalTaskRequest( `/operational-tasks/${encodeURIComponent(taskId)}/actions/${encodeURIComponent(actionId)}/decision`, getAccessToken, { method: "POST", body: JSON.stringify(input) } )); } export async function executeOperationalTaskAction(taskId: string, actionId: string, input: { request_id: string; expected_version: number; }, getAccessToken?: AccessTokenProvider) { return actionSchema.parse(await operationalTaskRequest( `/operational-tasks/${encodeURIComponent(taskId)}/actions/${encodeURIComponent(actionId)}/execute`, getAccessToken, { method: "POST", body: JSON.stringify(input) } )); } export async function updateOperationalWorkOrderDraft(taskId: string, actionId: string, input: { request_id: string; expected_version: number; payload: Record; }, getAccessToken?: AccessTokenProvider) { return actionSchema.parse(await operationalTaskRequest( `/operational-tasks/${encodeURIComponent(taskId)}/actions/${encodeURIComponent(actionId)}/draft`, getAccessToken, { method: "PATCH", body: JSON.stringify(input) } )); } export async function confirmOperationalWorkOrderDispatch(taskId: string, actionId: string, input: { request_id: string; expected_version: number; }, getAccessToken?: AccessTokenProvider) { return actionSchema.parse(await operationalTaskRequest( `/operational-tasks/${encodeURIComponent(taskId)}/actions/${encodeURIComponent(actionId)}/confirm-dispatch`, getAccessToken, { method: "POST", body: JSON.stringify(input) } )); } export async function decideOperationalTaskAccess(taskId: string, accessRequestId: string, input: { request_id: string; expected_version: number; decision: "approved" | "rejected"; approved_scopes: string[]; comment?: string; }, getAccessToken?: AccessTokenProvider) { return accessRequestSchema.parse(await operationalTaskRequest( `/operational-tasks/${encodeURIComponent(taskId)}/access-requests/${encodeURIComponent(accessRequestId)}/decision`, getAccessToken, { method: "POST", body: JSON.stringify(input) } )); } export async function fetchOperationalTaskNotifications(getAccessToken?: AccessTokenProvider, signal?: AbortSignal) { return notificationPageSchema.parse(await operationalTaskRequest( "/operational-task-notifications?unread_only=false&limit=100&offset=0", getAccessToken, { signal } )).items; } export async function acknowledgeOperationalTaskNotification(notificationId: string, version: number, requestId: string, getAccessToken?: AccessTokenProvider) { return notificationSchema.parse(await operationalTaskRequest( `/operational-task-notifications/${encodeURIComponent(notificationId)}/acknowledge`, getAccessToken, { method: "POST", body: JSON.stringify({ request_id: requestId, expected_version: version }) } )); } export async function fetchOperationalTaskSchedules(getAccessToken?: AccessTokenProvider, signal?: AbortSignal) { return schedulePageSchema.parse(await operationalTaskRequest( "/operational-tasks/schedules", getAccessToken, { signal } )).items; } export async function createOperationalTaskSchedule(input: { request_id: string; schedule_key: string; title: string; summary: string; interval_seconds: number; next_run_at: string; priority: "normal" | "urgent"; }, getAccessToken?: AccessTokenProvider) { return scheduleSchema.parse(await operationalTaskRequest( "/operational-tasks/schedules", getAccessToken, { method: "POST", body: JSON.stringify(input) } )); } export async function reconcileOperationalTaskAction(taskId: string, actionId: string, input: { request_id: string; expected_version: number; }, getAccessToken?: AccessTokenProvider) { return actionSchema.parse(await operationalTaskRequest( `/operational-tasks/${encodeURIComponent(taskId)}/actions/${encodeURIComponent(actionId)}/reconcile`, getAccessToken, { method: "POST", body: JSON.stringify(input) } )); } export async function fetchOperationalTaskEvidenceSnapshot(taskId: string, evidenceId: string, getAccessToken?: AccessTokenProvider) { return taskDataSnapshotSchema.parse(await operationalTaskRequest( `/operational-tasks/${encodeURIComponent(taskId)}/evidence/${encodeURIComponent(evidenceId)}/snapshot`, getAccessToken )); } export function createOperationalRequestId() { return crypto.randomUUID(); }