459 lines
19 KiB
TypeScript
459 lines
19 KiB
TypeScript
import { z } from "zod";
|
|
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(),
|
|
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 = z.object({
|
|
title: z.string().min(1),
|
|
current_conclusion: z.string().min(1),
|
|
conclusion_boundary: z.string().min(1),
|
|
evidence_refs: z.array(z.string().min(1)),
|
|
recommendations: z.array(z.string().min(1)),
|
|
pending_items: z.array(z.string().min(1))
|
|
}).strict();
|
|
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 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<string, unknown>;
|
|
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(),
|
|
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),
|
|
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({
|
|
source: z.enum(["workbench_runtime", "scada_devices", "scada_readings", "peripheral_points", "peripheral_work_orders", "peripheral_observations", "peripheral_test_data"]),
|
|
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<typeof taskSummarySchema>;
|
|
export type OperationalTask = z.infer<typeof operationalTaskSchema>;
|
|
export type OperationalTaskPlan = z.infer<typeof planInputSchema>;
|
|
export type OperationalTaskAction = z.infer<typeof actionSchema>;
|
|
export type OperationalTaskAccessRequest = z.infer<typeof accessRequestSchema>;
|
|
export type OperationalTaskEvidence = z.infer<typeof evidenceSchema>;
|
|
export type OperationalTaskNotification = z.infer<typeof notificationSchema>;
|
|
export type OperationalTaskSchedule = z.infer<typeof scheduleSchema>;
|
|
export type OperationalTaskDataSnapshot = z.infer<typeof taskDataSnapshotSchema>;
|
|
export type ExternalWorkOrderSnapshot = z.infer<typeof externalWorkOrderSnapshotSchema>;
|
|
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 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<string, unknown>;
|
|
}, 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=true&limit=100&offset=0",
|
|
getAccessToken,
|
|
{ signal }
|
|
)).items;
|
|
}
|
|
|
|
export async function acknowledgeOperationalTaskNotification(notificationId: string, version: number, requestId: string, getAccessToken?: AccessTokenProvider) {
|
|
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();
|
|
}
|