feat(workbench): integrate operational workflows and map state
This commit is contained in:
@@ -0,0 +1,395 @@
|
||||
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()
|
||||
}).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())
|
||||
}).strict();
|
||||
const planInputSchema = z.object({
|
||||
schema: z.literal("execution-plan@1"),
|
||||
title: z.string().min(1),
|
||||
reason: z.string().min(1),
|
||||
nodes: z.array(planNodeSchema).min(1).max(100),
|
||||
edges: z.array(z.object({
|
||||
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,
|
||||
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 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()
|
||||
}).strict();
|
||||
const externalWorkOrderStageSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
sequence: z.number().int().positive(),
|
||||
title: z.string().min(1),
|
||||
status: z.enum(["done", "current", "pending", "blocked", "skipped"]),
|
||||
owner: z.string().min(1),
|
||||
time_label: z.string().min(1),
|
||||
description: z.string().min(1)
|
||||
}).strict();
|
||||
const externalWorkOrderSubtaskSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
title: z.string().min(1),
|
||||
summary: z.string().min(1),
|
||||
status: z.enum(["pending", "running", "blocked", "completed", "cancelled"]),
|
||||
scheduled_at: workbenchDateTimeSchema,
|
||||
location: z.string().min(1),
|
||||
assignee: z.string().min(1),
|
||||
duration_minutes: z.number().int().positive(),
|
||||
reply_window_minutes: z.number().int().nonnegative(),
|
||||
stages: z.array(externalWorkOrderStageSchema).min(1),
|
||||
reply_requirements: z.array(z.string().min(1))
|
||||
}).strict();
|
||||
const externalWorkOrderSnapshotSchema = 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(),
|
||||
subtasks: z.array(externalWorkOrderSubtaskSchema).min(1).nullable(),
|
||||
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),
|
||||
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"]),
|
||||
title: z.string().min(1),
|
||||
description: z.string().min(1),
|
||||
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 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 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();
|
||||
}
|
||||
Reference in New Issue
Block a user