From e0d37f9da951f1768b4f94e7c331c25e04d52428 Mon Sep 17 00:00:00 2001 From: Huarch Date: Thu, 24 Sep 2026 16:36:12 +0800 Subject: [PATCH] feat(tasks): enforce dynamic execution plans --- .opencode/agents/instruction.md | 3 + .opencode/tools/publish_execution_plan.ts | 11 +- .opencode/tools/request_task_data_access.ts | 28 + openapi/openapi.json | 1114 ++++++++++++++++++- src/generated/server-api.d.ts | 651 ++++++++++- src/product/operational-task-client.ts | 27 +- src/routes/agentTrigger.ts | 4 +- src/routes/chatSessionRoutes.ts | 2 +- src/routes/executionPlan.ts | 55 +- src/server.ts | 23 + tests/opencode/internalBridgeTools.test.ts | 4 +- tests/routes/agentTrigger.test.ts | 3 +- tests/routes/executionPlan.test.ts | 34 +- 13 files changed, 1942 insertions(+), 17 deletions(-) create mode 100644 .opencode/tools/request_task_data_access.ts diff --git a/.opencode/agents/instruction.md b/.opencode/agents/instruction.md index a040d69..18d00cf 100644 --- a/.opencode/agents/instruction.md +++ b/.opencode/agents/instruction.md @@ -29,6 +29,7 @@ Agent 负责: | 发布事件调查计划及节点进度 | `publish_execution_plan` | | 读取工况任务、证据和授权数据 | `get_operational_task_context` | | 查询任务授权范围内的实时或外围数据 | `query_operational_task_data` | +| 请求任务级数据访问并等待人工审核 | `request_task_data_access` | | 发布精简调查报告和关键证据 | `publish_task_report` | | 提议需人工审批的工单或命令 | `propose_task_action` | | 记录用户明确提供的任务证据 | `add_task_human_evidence` | @@ -44,6 +45,8 @@ Agent 负责: ## 执行约束 +事件调查由当前投诉工单或人工请求动态规划,不套用固定流程。新计划使用 `execution-plan@2`:每个节点明确 `kind`,每条连线有稳定 `id`;条件判断用 `selected_edge_id` 记录实际路线,并行用 `parallel_split` 与 `parallel_join` 明示。数据授权或工单审核用 `gate` 节点引用真实 `gate_ref`。如需再次排查或重拟草稿,新增节点并用 `revisits_node_id` 指向上次节点,保留旧节点与证据,不创建环形连线。未获得数据权限时先调用 `request_task_data_access`,发布等待节点并停止越权查询;拒绝后标记证据缺口。只有任务上下文包含批准的来源时才能查询。工单草稿可由人工编辑后确认下发,Agent 不得自行代替人工确认。 + 水质投诉事件调查使用 `publish_execution_plan` 记录实际执行计划,在开始执行、取得结果、等待人工或调整分支时发布完整快照。保留稳定节点 ID 和已完成证据,取消或不适用的分支显式标记。运行中或已完成节点的所有前置节点必须已完成或已跳过。完成节点必须提供真实来源;预设场景输入明确标为合成证据,未实际调用的查询不得写成已执行。工具返回失败表示后端未保存计划,必须按错误修正后重试,不能把失败调用当作已发布或直接结束本轮。现场采样、清洗、复测及回访保持等待人工确认,不能因本轮回复结束而完成。 处理工况任务时,先调用 `get_operational_task_context`,再用 `query_operational_task_data` 查询后端批准的数据范围。`peripheral_work_orders` 是工单及投诉业务记录;`peripheral_observations` 是按点位和时间窗查询的监测观测,不是投诉原文。只把工具实际返回的数据写成系统证据,并直接引用返回的 `evidence.evidence_id`,不得用 `publish_task_report` 重写系统、外部或人工证据;该工具新增证据只允许明确的合成输入。`origin=synthetic` 必须标为合成证据,`truncated=true` 时不能据此断言完整范围没有异常。数据源缺失或不可用时保持相关节点 waiting 并记录缺口。计划可在调查中多次更新,纯计划发布是中间进度,不会结束未失败的任务。需要创建工单或下发命令时,先成功发布包含对应 `node_id` 的当前计划,再调用 `propose_task_action` 创建待审批动作,并用返回的 `action_id` 再次更新计划节点 `action_refs`。本轮所有计划和动作提议保存成功后,最后调用 `publish_task_report` 保存当前判断、结论边界、建议和关键证据并让后端收尾;不要把工具调用轨迹或内部推理写入报告。后端只用当前执行代、当前会话的计划和报告判断继续运行、等待或完成状态。只有后端上下文显示动作具备成功回执时,才能把关联人工节点标为完成。审批、执行与回执均由 TJWater Server 控制。 diff --git a/.opencode/tools/publish_execution_plan.ts b/.opencode/tools/publish_execution_plan.ts index f8d507e..5862005 100644 --- a/.opencode/tools/publish_execution_plan.ts +++ b/.opencode/tools/publish_execution_plan.ts @@ -1,11 +1,12 @@ import { tool } from "@opencode-ai/plugin"; import { getInternalBridgeConfig } from "../internalBridge.js"; -import { executionPlanSchema } from "../../src/routes/executionPlan.js"; +import { newExecutionPlanSchema } from "../../src/routes/executionPlan.js"; export default tool({ description: "Publish the current full execution plan and progress to TJWater Server. Plans may be updated multiple times during investigation; a plan-only publish is intermediate and does not finish the task unless it explicitly records Agent failure. Publish the relevant plan node before proposing an action, then republish the plan with the returned action_id before the final report. Preserve stable node IDs and completed evidence. A running or completed node may only depend on completed or skipped predecessors. Edges are dependencies or labeled conditional branches, never guessed from list order. Human actions stay waiting until externally confirmed. Evidence must identify synthetic inputs, tool results or human confirmation accurately. This records a plan, it does not execute actions.", args: { request_id: tool.schema.string().describe("Stable UUID idempotency key for this plan revision"), + schema: tool.schema.enum(["execution-plan@2"]), title: tool.schema.string(), reason: tool.schema.string().describe("Why the plan or node status changed"), nodes: tool.schema.array(tool.schema.object({ @@ -14,12 +15,16 @@ export default tool({ owner: tool.schema.enum(["agent", "human"]), detail: tool.schema.string(), evidence: tool.schema.array(tool.schema.string()), action_refs: tool.schema.array(tool.schema.string()).optional(), + kind: tool.schema.enum(["activity", "decision", "parallel_split", "parallel_join", "gate"]).optional(), + selected_edge_id: tool.schema.string().nullable().optional(), + gate_ref: tool.schema.string().nullable().optional(), + revisits_node_id: tool.schema.string().nullable().optional(), })), - edges: tool.schema.array(tool.schema.object({ from: tool.schema.string(), to: tool.schema.string(), label: tool.schema.string() })), + edges: tool.schema.array(tool.schema.object({ id: tool.schema.string().optional(), from: tool.schema.string(), to: tool.schema.string(), label: tool.schema.string() })), }, async execute(args, context) { const { request_id, ...input } = args; - const plan = executionPlanSchema.parse(input); + const plan = newExecutionPlanSchema.parse(input); const { baseUrl, internalToken } = getInternalBridgeConfig(); const response = await fetch(`${baseUrl}/internal/operational-task/artifacts`, { method: "POST", diff --git a/.opencode/tools/request_task_data_access.ts b/.opencode/tools/request_task_data_access.ts new file mode 100644 index 0000000..47cd232 --- /dev/null +++ b/.opencode/tools/request_task_data_access.ts @@ -0,0 +1,28 @@ +import { tool } from "@opencode-ai/plugin"; +import { getInternalBridgeConfig } from "../internalBridge.js"; + +export default tool({ + description: "Request task-scoped access to one or more finite investigation data sources. This only creates a pending human review record. Do not query the requested sources until the approved scope appears in task context; after denial, mark the evidence gap and continue with available facts.", + args: { + request_id: tool.schema.string().uuid(), + node_id: tool.schema.string().min(1), + scopes: tool.schema.array(tool.schema.enum([ + "workbench_runtime", "scada_devices", "scada_readings", "peripheral_points", + "peripheral_work_orders", "peripheral_observations", "peripheral_test_data", + ])).min(1), + reason: tool.schema.string().min(1), + }, + async execute(args, context) { + const { baseUrl, internalToken } = getInternalBridgeConfig(); + const response = await fetch(`${baseUrl}/internal/operational-task/access-requests`, { + method: "POST", + headers: { "Content-Type": "application/json", "x-agent-internal-token": internalToken }, + body: JSON.stringify({ session_id: context.sessionID, request: { + requestId: args.request_id, nodeId: args.node_id, scopes: args.scopes, reason: args.reason, + } }), + }); + const text = await response.text(); + if (!response.ok) throw new Error(`task data access request failed (${response.status}): ${text}`); + return text; + }, +}); diff --git a/openapi/openapi.json b/openapi/openapi.json index c5dc38c..83e2a80 100644 --- a/openapi/openapi.json +++ b/openapi/openapi.json @@ -1904,6 +1904,18 @@ "title": "From", "type": "string" }, + "id": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Id" + }, "to": { "minLength": 1, "title": "To", @@ -1943,7 +1955,10 @@ "type": "string" }, "schema": { - "const": "execution-plan@1", + "enum": [ + "execution-plan@1", + "execution-plan@2" + ], "title": "Schema", "type": "string" }, @@ -1986,11 +2001,41 @@ "title": "Evidence Refs", "type": "array" }, + "gate_ref": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Gate Ref" + }, "id": { "minLength": 1, "title": "Id", "type": "string" }, + "kind": { + "anyOf": [ + { + "enum": [ + "activity", + "decision", + "parallel_split", + "parallel_join", + "gate" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Kind" + }, "owner": { "enum": [ "agent", @@ -1999,6 +2044,30 @@ "title": "Owner", "type": "string" }, + "revisits_node_id": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Revisits Node Id" + }, + "selected_edge_id": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Selected Edge Id" + }, "status": { "enum": [ "pending", @@ -2235,6 +2304,27 @@ "title": "ExternalFeedbackResult", "type": "object" }, + "ExternalSourceWithdrawalCommand": { + "additionalProperties": false, + "properties": { + "request_id": { + "format": "uuid", + "title": "Request Id", + "type": "string" + }, + "source_event_id": { + "minLength": 1, + "title": "Source Event Id", + "type": "string" + } + }, + "required": [ + "request_id", + "source_event_id" + ], + "title": "ExternalSourceWithdrawalCommand", + "type": "object" + }, "ExternalWorkOrderSnapshot": { "additionalProperties": false, "properties": { @@ -2556,6 +2646,30 @@ ], "title": "Decision Comment" }, + "edited_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Edited At" + }, + "edited_by": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Edited By" + }, "evidence_refs": { "items": { "minLength": 1, @@ -3359,6 +3473,13 @@ "OperationalTaskDetail": { "additionalProperties": false, "properties": { + "access_requests": { + "items": { + "$ref": "#/components/schemas/TaskAccessRequest" + }, + "title": "Access Requests", + "type": "array" + }, "actions": { "items": { "$ref": "#/components/schemas/HumanApprovalAction" @@ -3633,6 +3754,15 @@ "title": "Kind", "type": "string" }, + "lifecycle_status": { + "default": "active", + "enum": [ + "active", + "resolved" + ], + "title": "Lifecycle Status", + "type": "string" + }, "notification_id": { "format": "uuid", "title": "Notification Id", @@ -3674,6 +3804,30 @@ ], "title": "Required Permission" }, + "resolution_reason": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Resolution Reason" + }, + "resolved_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Resolved At" + }, "status": { "enum": [ "unread", @@ -3682,6 +3836,29 @@ "title": "Status", "type": "string" }, + "subject_id": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Subject Id" + }, + "subject_type": { + "default": "task", + "enum": [ + "task", + "run", + "access_request", + "action" + ], + "title": "Subject Type", + "type": "string" + }, "task_id": { "format": "uuid", "title": "Task Id", @@ -3796,6 +3973,11 @@ "OperationalTaskPermissions": { "additionalProperties": false, "properties": { + "can_decide_access": { + "default": false, + "title": "Can Decide Access", + "type": "boolean" + }, "can_decide_commands": { "title": "Can Decide Commands", "type": "boolean" @@ -5955,6 +6137,247 @@ "title": "SourceGisFeatureProperties", "type": "object" }, + "TaskAccessRequest": { + "additionalProperties": false, + "properties": { + "access_request_id": { + "format": "uuid", + "title": "Access Request Id", + "type": "string" + }, + "approved_scopes": { + "items": { + "enum": [ + "workbench_runtime", + "scada_devices", + "scada_readings", + "peripheral_points", + "peripheral_work_orders", + "peripheral_observations", + "peripheral_test_data" + ], + "type": "string" + }, + "title": "Approved Scopes", + "type": "array" + }, + "comment": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Comment" + }, + "decided_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Decided At" + }, + "decided_by": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Decided By" + }, + "node_id": { + "minLength": 1, + "title": "Node Id", + "type": "string" + }, + "reason": { + "minLength": 1, + "title": "Reason", + "type": "string" + }, + "requested_at": { + "format": "date-time", + "title": "Requested At", + "type": "string" + }, + "requested_by": { + "minLength": 1, + "title": "Requested By", + "type": "string" + }, + "scopes": { + "items": { + "enum": [ + "workbench_runtime", + "scada_devices", + "scada_readings", + "peripheral_points", + "peripheral_work_orders", + "peripheral_observations", + "peripheral_test_data" + ], + "type": "string" + }, + "title": "Scopes", + "type": "array" + }, + "status": { + "enum": [ + "pending", + "approved", + "rejected" + ], + "title": "Status", + "type": "string" + }, + "task_id": { + "format": "uuid", + "title": "Task Id", + "type": "string" + }, + "version": { + "minimum": 1.0, + "title": "Version", + "type": "integer" + } + }, + "required": [ + "access_request_id", + "task_id", + "node_id", + "scopes", + "reason", + "status", + "approved_scopes", + "version", + "requested_by", + "requested_at" + ], + "title": "TaskAccessRequest", + "type": "object" + }, + "TaskAccessRequestCreate": { + "additionalProperties": false, + "properties": { + "node_id": { + "minLength": 1, + "title": "Node Id", + "type": "string" + }, + "reason": { + "minLength": 1, + "title": "Reason", + "type": "string" + }, + "request_id": { + "format": "uuid", + "title": "Request Id", + "type": "string" + }, + "scopes": { + "items": { + "enum": [ + "workbench_runtime", + "scada_devices", + "scada_readings", + "peripheral_points", + "peripheral_work_orders", + "peripheral_observations", + "peripheral_test_data" + ], + "type": "string" + }, + "maxItems": 7, + "minItems": 1, + "title": "Scopes", + "type": "array" + }, + "source_session_id": { + "minLength": 1, + "title": "Source Session Id", + "type": "string" + } + }, + "required": [ + "request_id", + "source_session_id", + "node_id", + "scopes", + "reason" + ], + "title": "TaskAccessRequestCreate", + "type": "object" + }, + "TaskAccessRequestDecision": { + "additionalProperties": false, + "properties": { + "approved_scopes": { + "items": { + "enum": [ + "workbench_runtime", + "scada_devices", + "scada_readings", + "peripheral_points", + "peripheral_work_orders", + "peripheral_observations", + "peripheral_test_data" + ], + "type": "string" + }, + "title": "Approved Scopes", + "type": "array" + }, + "comment": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Comment" + }, + "decision": { + "enum": [ + "approved", + "rejected" + ], + "title": "Decision", + "type": "string" + }, + "expected_version": { + "minimum": 1.0, + "title": "Expected Version", + "type": "integer" + }, + "request_id": { + "format": "uuid", + "title": "Request Id", + "type": "string" + } + }, + "required": [ + "request_id", + "expected_version", + "decision" + ], + "title": "TaskAccessRequestDecision", + "type": "object" + }, "TaskAlertCommand": { "additionalProperties": false, "properties": { @@ -6258,6 +6681,18 @@ "TaskSource": { "additionalProperties": false, "properties": { + "deleted_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Deleted At" + }, "event_kind": { "anyOf": [ { @@ -6486,6 +6921,52 @@ "title": "WorkOrderActivity", "type": "object" }, + "WorkOrderConfirmDispatchCommand": { + "additionalProperties": false, + "properties": { + "expected_version": { + "minimum": 1.0, + "title": "Expected Version", + "type": "integer" + }, + "request_id": { + "format": "uuid", + "title": "Request Id", + "type": "string" + } + }, + "required": [ + "request_id", + "expected_version" + ], + "title": "WorkOrderConfirmDispatchCommand", + "type": "object" + }, + "WorkOrderDraftUpdateCommand": { + "additionalProperties": false, + "properties": { + "expected_version": { + "minimum": 1.0, + "title": "Expected Version", + "type": "integer" + }, + "payload": { + "$ref": "#/components/schemas/CreateWorkOrderPayload" + }, + "request_id": { + "format": "uuid", + "title": "Request Id", + "type": "string" + } + }, + "required": [ + "request_id", + "expected_version", + "payload" + ], + "title": "WorkOrderDraftUpdateCommand", + "type": "object" + }, "WorkbenchAlertSource": { "additionalProperties": false, "properties": { @@ -24073,6 +24554,250 @@ ] } }, + "/api/v1/operational-tasks/{task_id}/access-requests": { + "post": { + "operationId": "post_operational_tasks_task_id_access_requests", + "parameters": [ + { + "in": "path", + "name": "task_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Task Id", + "type": "string" + } + }, + { + "in": "header", + "name": "X-Project-Id", + "required": true, + "schema": { + "title": "X-Project-Id", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TaskAccessRequestCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TaskAccessRequest" + } + } + }, + "description": "Successful Response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "Authentication required" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "Insufficient permission" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "Resource not found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "Resource conflict" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "Validation error" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "Dependency unavailable" + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ], + "summary": "Request Operational Task Access", + "tags": [ + "Operational Tasks" + ] + } + }, + "/api/v1/operational-tasks/{task_id}/access-requests/{access_request_id}/decision": { + "post": { + "operationId": "post_operational_tasks_task_id_access_requests_access_request_id_decision", + "parameters": [ + { + "in": "path", + "name": "task_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Task Id", + "type": "string" + } + }, + { + "in": "path", + "name": "access_request_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Access Request Id", + "type": "string" + } + }, + { + "in": "header", + "name": "X-Project-Id", + "required": true, + "schema": { + "title": "X-Project-Id", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TaskAccessRequestDecision" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TaskAccessRequest" + } + } + }, + "description": "Successful Response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "Authentication required" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "Insufficient permission" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "Resource not found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "Resource conflict" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "Validation error" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "Dependency unavailable" + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ], + "summary": "Decide Operational Task Access", + "tags": [ + "Operational Tasks" + ] + } + }, "/api/v1/operational-tasks/{task_id}/actions": { "post": { "operationId": "post_operational_tasks_task_id_actions", @@ -24190,6 +24915,149 @@ ] } }, + "/api/v1/operational-tasks/{task_id}/actions/{action_id}/confirm-dispatch": { + "post": { + "operationId": "post_operational_tasks_task_id_actions_action_id_confirm_dispatch", + "parameters": [ + { + "in": "path", + "name": "task_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Task Id", + "type": "string" + } + }, + { + "in": "path", + "name": "action_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Action Id", + "type": "string" + } + }, + { + "in": "header", + "name": "Authorization", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + }, + { + "in": "header", + "name": "X-Project-Id", + "required": true, + "schema": { + "title": "X-Project-Id", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkOrderConfirmDispatchCommand" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HumanApprovalAction" + } + } + }, + "description": "Successful Response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "Authentication required" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "Insufficient permission" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "Resource not found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "Resource conflict" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "Validation error" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "Dependency unavailable" + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ], + "summary": "Confirm Operational Work Order Dispatch", + "tags": [ + "Operational Tasks" + ] + } + }, "/api/v1/operational-tasks/{task_id}/actions/{action_id}/decision": { "post": { "operationId": "post_operational_tasks_task_id_actions_action_id_decision", @@ -24317,6 +25185,133 @@ ] } }, + "/api/v1/operational-tasks/{task_id}/actions/{action_id}/draft": { + "patch": { + "operationId": "patch_operational_tasks_task_id_actions_action_id_draft", + "parameters": [ + { + "in": "path", + "name": "task_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Task Id", + "type": "string" + } + }, + { + "in": "path", + "name": "action_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Action Id", + "type": "string" + } + }, + { + "in": "header", + "name": "X-Project-Id", + "required": true, + "schema": { + "title": "X-Project-Id", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkOrderDraftUpdateCommand" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HumanApprovalAction" + } + } + }, + "description": "Successful Response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "Authentication required" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "Insufficient permission" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "Resource not found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "Resource conflict" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "Validation error" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "Dependency unavailable" + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ], + "summary": "Update Operational Work Order Draft", + "tags": [ + "Operational Tasks" + ] + } + }, "/api/v1/operational-tasks/{task_id}/actions/{action_id}/execute": { "post": { "operationId": "post_operational_tasks_task_id_actions_action_id_execute", @@ -26189,6 +27184,123 @@ ] } }, + "/api/v1/operational-tasks/{task_id}/withdraw-external-source": { + "post": { + "operationId": "post_operational_tasks_task_id_withdraw_external_source", + "parameters": [ + { + "in": "path", + "name": "task_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Task Id", + "type": "string" + } + }, + { + "in": "header", + "name": "X-Project-Id", + "required": true, + "schema": { + "title": "X-Project-Id", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExternalSourceWithdrawalCommand" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OperationalTaskDetail" + } + } + }, + "description": "Successful Response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "Authentication required" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "Insufficient permission" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "Resource not found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "Resource conflict" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "Validation error" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "Dependency unavailable" + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ], + "summary": "Withdraw Operational Task External Source", + "tags": [ + "Operational Tasks" + ] + } + }, "/api/v1/patterns": { "delete": { "description": "从网络中删除指定的模式", diff --git a/src/generated/server-api.d.ts b/src/generated/server-api.d.ts index 41badb9..bafd55b 100644 --- a/src/generated/server-api.d.ts +++ b/src/generated/server-api.d.ts @@ -2327,6 +2327,40 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/operational-tasks/{task_id}/access-requests": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Request Operational Task Access */ + post: operations["post_operational_tasks_task_id_access_requests"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/operational-tasks/{task_id}/access-requests/{access_request_id}/decision": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Decide Operational Task Access */ + post: operations["post_operational_tasks_task_id_access_requests_access_request_id_decision"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v1/operational-tasks/{task_id}/actions": { parameters: { query?: never; @@ -2344,6 +2378,23 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/operational-tasks/{task_id}/actions/{action_id}/confirm-dispatch": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Confirm Operational Work Order Dispatch */ + post: operations["post_operational_tasks_task_id_actions_action_id_confirm_dispatch"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v1/operational-tasks/{task_id}/actions/{action_id}/decision": { parameters: { query?: never; @@ -2361,6 +2412,23 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/operational-tasks/{task_id}/actions/{action_id}/draft": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** Update Operational Work Order Draft */ + patch: operations["patch_operational_tasks_task_id_actions_action_id_draft"]; + trace?: never; + }; "/api/v1/operational-tasks/{task_id}/actions/{action_id}/execute": { parameters: { query?: never; @@ -2616,6 +2684,23 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/operational-tasks/{task_id}/withdraw-external-source": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Withdraw Operational Task External Source */ + post: operations["post_operational_tasks_task_id_withdraw_external_source"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v1/patterns": { parameters: { query?: never; @@ -5983,6 +6068,8 @@ export interface components { condition?: string | null; /** From */ from: string; + /** Id */ + id?: string | null; /** To */ to: string; }; @@ -5996,9 +6083,9 @@ export interface components { reason: string; /** * Schema - * @constant + * @enum {string} */ - schema: "execution-plan@1"; + schema: "execution-plan@1" | "execution-plan@2"; /** Title */ title: string; }; @@ -6010,13 +6097,21 @@ export interface components { detail: string; /** Evidence Refs */ evidence_refs?: string[]; + /** Gate Ref */ + gate_ref?: string | null; /** Id */ id: string; + /** Kind */ + kind?: ("activity" | "decision" | "parallel_split" | "parallel_join" | "gate") | null; /** * Owner * @enum {string} */ owner: "agent" | "human"; + /** Revisits Node Id */ + revisits_node_id?: string | null; + /** Selected Edge Id */ + selected_edge_id?: string | null; /** * Status * @enum {string} @@ -6103,6 +6198,16 @@ export interface components { source_revision: number; task: components["schemas"]["OperationalTaskDetail"]; }; + /** ExternalSourceWithdrawalCommand */ + ExternalSourceWithdrawalCommand: { + /** + * Request Id + * Format: uuid + */ + request_id: string; + /** Source Event Id */ + source_event_id: string; + }; /** ExternalWorkOrderSnapshot */ ExternalWorkOrderSnapshot: { /** Activity */ @@ -6230,6 +6335,10 @@ export interface components { decided_by?: string | null; /** Decision Comment */ decision_comment?: string | null; + /** Edited At */ + edited_at?: string | null; + /** Edited By */ + edited_by?: string | null; /** Evidence Refs */ evidence_refs: string[]; /** Executed At */ @@ -6552,6 +6661,8 @@ export interface components { }; /** OperationalTaskDetail */ OperationalTaskDetail: { + /** Access Requests */ + access_requests?: components["schemas"]["TaskAccessRequest"][]; /** Actions */ actions: components["schemas"]["HumanApprovalAction"][]; active_run?: components["schemas"]["OperationalTaskRun"] | null; @@ -6644,6 +6755,12 @@ export interface components { * @enum {string} */ kind: "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"; + /** + * Lifecycle Status + * @default active + * @enum {string} + */ + lifecycle_status: "active" | "resolved"; /** * Notification Id * Format: uuid @@ -6655,11 +6772,23 @@ export interface components { read_by?: string | null; /** Required Permission */ required_permission?: string | null; + /** Resolution Reason */ + resolution_reason?: string | null; + /** Resolved At */ + resolved_at?: string | null; /** * Status * @enum {string} */ status: "unread" | "read"; + /** Subject Id */ + subject_id?: string | null; + /** + * Subject Type + * @default task + * @enum {string} + */ + subject_type: "task" | "run" | "access_request" | "action"; /** * Task Id * Format: uuid @@ -6694,6 +6823,11 @@ export interface components { }; /** OperationalTaskPermissions */ OperationalTaskPermissions: { + /** + * Can Decide Access + * @default false + */ + can_decide_access: boolean; /** Can Decide Commands */ can_decide_commands: boolean; /** Can Decide Work Orders */ @@ -7536,6 +7670,82 @@ export interface components { /** Source Fid */ source_fid: number; }; + /** TaskAccessRequest */ + TaskAccessRequest: { + /** + * Access Request Id + * Format: uuid + */ + access_request_id: string; + /** Approved Scopes */ + approved_scopes: ("workbench_runtime" | "scada_devices" | "scada_readings" | "peripheral_points" | "peripheral_work_orders" | "peripheral_observations" | "peripheral_test_data")[]; + /** Comment */ + comment?: string | null; + /** Decided At */ + decided_at?: string | null; + /** Decided By */ + decided_by?: string | null; + /** Node Id */ + node_id: string; + /** Reason */ + reason: string; + /** + * Requested At + * Format: date-time + */ + requested_at: string; + /** Requested By */ + requested_by: string; + /** Scopes */ + scopes: ("workbench_runtime" | "scada_devices" | "scada_readings" | "peripheral_points" | "peripheral_work_orders" | "peripheral_observations" | "peripheral_test_data")[]; + /** + * Status + * @enum {string} + */ + status: "pending" | "approved" | "rejected"; + /** + * Task Id + * Format: uuid + */ + task_id: string; + /** Version */ + version: number; + }; + /** TaskAccessRequestCreate */ + TaskAccessRequestCreate: { + /** Node Id */ + node_id: string; + /** Reason */ + reason: string; + /** + * Request Id + * Format: uuid + */ + request_id: string; + /** Scopes */ + scopes: ("workbench_runtime" | "scada_devices" | "scada_readings" | "peripheral_points" | "peripheral_work_orders" | "peripheral_observations" | "peripheral_test_data")[]; + /** Source Session Id */ + source_session_id: string; + }; + /** TaskAccessRequestDecision */ + TaskAccessRequestDecision: { + /** Approved Scopes */ + approved_scopes?: ("workbench_runtime" | "scada_devices" | "scada_readings" | "peripheral_points" | "peripheral_work_orders" | "peripheral_observations" | "peripheral_test_data")[]; + /** Comment */ + comment?: string | null; + /** + * Decision + * @enum {string} + */ + decision: "approved" | "rejected"; + /** Expected Version */ + expected_version: number; + /** + * Request Id + * Format: uuid + */ + request_id: string; + }; /** TaskAlertCommand */ TaskAlertCommand: { /** Alert Id */ @@ -7650,6 +7860,8 @@ export interface components { }; /** TaskSource */ TaskSource: { + /** Deleted At */ + deleted_at?: string | null; /** Event Kind */ event_kind?: string | null; /** External Id */ @@ -7742,6 +7954,27 @@ export interface components { /** Title */ title: string; }; + /** WorkOrderConfirmDispatchCommand */ + WorkOrderConfirmDispatchCommand: { + /** Expected Version */ + expected_version: number; + /** + * Request Id + * Format: uuid + */ + request_id: string; + }; + /** WorkOrderDraftUpdateCommand */ + WorkOrderDraftUpdateCommand: { + /** Expected Version */ + expected_version: number; + payload: components["schemas"]["CreateWorkOrderPayload"]; + /** + * Request Id + * Format: uuid + */ + request_id: string; + }; /** WorkbenchAlertSource */ WorkbenchAlertSource: { /** Run Id */ @@ -19772,6 +20005,171 @@ export interface operations { }; }; }; + post_operational_tasks_task_id_access_requests: { + parameters: { + query?: never; + header: { + "X-Project-Id": string; + }; + path: { + task_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TaskAccessRequestCreate"]; + }; + }; + responses: { + /** @description Successful Response */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TaskAccessRequest"]; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Insufficient permission */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Resource not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Resource conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Dependency unavailable */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProblemDetails"]; + }; + }; + }; + }; + post_operational_tasks_task_id_access_requests_access_request_id_decision: { + parameters: { + query?: never; + header: { + "X-Project-Id": string; + }; + path: { + task_id: string; + access_request_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TaskAccessRequestDecision"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TaskAccessRequest"]; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Insufficient permission */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Resource not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Resource conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Dependency unavailable */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProblemDetails"]; + }; + }; + }; + }; post_operational_tasks_task_id_actions: { parameters: { query?: never; @@ -19854,6 +20252,90 @@ export interface operations { }; }; }; + post_operational_tasks_task_id_actions_action_id_confirm_dispatch: { + parameters: { + query?: never; + header: { + Authorization?: string | null; + "X-Project-Id": string; + }; + path: { + task_id: string; + action_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["WorkOrderConfirmDispatchCommand"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HumanApprovalAction"]; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Insufficient permission */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Resource not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Resource conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Dependency unavailable */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProblemDetails"]; + }; + }; + }; + }; post_operational_tasks_task_id_actions_action_id_decision: { parameters: { query?: never; @@ -19937,6 +20419,89 @@ export interface operations { }; }; }; + patch_operational_tasks_task_id_actions_action_id_draft: { + parameters: { + query?: never; + header: { + "X-Project-Id": string; + }; + path: { + task_id: string; + action_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["WorkOrderDraftUpdateCommand"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HumanApprovalAction"]; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Insufficient permission */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Resource not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Resource conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Dependency unavailable */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProblemDetails"]; + }; + }; + }; + }; post_operational_tasks_task_id_actions_action_id_execute: { parameters: { query?: never; @@ -21171,6 +21736,88 @@ export interface operations { }; }; }; + post_operational_tasks_task_id_withdraw_external_source: { + parameters: { + query?: never; + header: { + "X-Project-Id": string; + }; + path: { + task_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ExternalSourceWithdrawalCommand"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OperationalTaskDetail"]; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Insufficient permission */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Resource not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Resource conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProblemDetails"]; + }; + }; + /** @description Dependency unavailable */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProblemDetails"]; + }; + }; + }; + }; get_patterns: { parameters: { query?: { diff --git a/src/product/operational-task-client.ts b/src/product/operational-task-client.ts index 4bf8ba0..978271b 100644 --- a/src/product/operational-task-client.ts +++ b/src/product/operational-task-client.ts @@ -151,6 +151,12 @@ export const humanTaskEvidenceCommandSchema = z.object({ export type TaskEvidence = z.infer; export type TaskReport = z.infer; export type OperationalTaskDataQuery = z.infer; +export const expressAccessRequestSchema = z.object({ + requestId: z.string().uuid(), + nodeId: z.string().min(1), + scopes: z.array(taskDataSourceSchema).min(1).max(7), + reason: z.string().min(1), +}); export type HumanTaskEvidenceCommand = z.infer; export type OperationalTaskContext = Record; @@ -354,6 +360,18 @@ export class OperationalTaskClient { return { evidence, snapshot }; } + async requestDataAccess(runtimeSessionId: string, taskId: string, input: { + requestId: string; sourceSessionId: string; nodeId: string; scopes: OperationalTaskDataQuery["source"][]; reason: string; + }) { + return this.request(runtimeSessionId, `/api/v1/operational-tasks/${encodeURIComponent(taskId)}/access-requests`, { + method: "POST", + body: JSON.stringify({ + request_id: input.requestId, source_session_id: input.sourceSessionId, + node_id: input.nodeId, scopes: input.scopes, reason: input.reason, + }), + }); + } + async proposeAction(runtimeSessionId: string, taskId: string, input: ProposedTaskAction) { const result = await this.request(runtimeSessionId, `/api/v1/operational-tasks/${encodeURIComponent(taskId)}/actions`, { method: "POST", @@ -475,7 +493,7 @@ const toServerPlan = (input: Record) => { const nodes = Array.isArray(input.nodes) ? input.nodes : []; const edges = Array.isArray(input.edges) ? input.edges : []; return { - schema: "execution-plan@1", + schema: input.schema === "execution-plan@2" ? "execution-plan@2" : "execution-plan@1", title: input.title, reason: input.reason, nodes: nodes.map((value) => { @@ -488,6 +506,12 @@ const toServerPlan = (input: Record) => { detail: node.detail, evidence_refs: Array.isArray(node.evidence) ? node.evidence : [], action_refs: Array.isArray(node.action_refs) ? node.action_refs : [], + ...(input.schema === "execution-plan@2" ? { + kind: node.kind, + selected_edge_id: node.selected_edge_id ?? null, + gate_ref: node.gate_ref ?? null, + revisits_node_id: node.revisits_node_id ?? null, + } : {}), }; }), edges: edges.map((value) => { @@ -496,6 +520,7 @@ const toServerPlan = (input: Record) => { from: edge.from, to: edge.to, ...(typeof edge.label === "string" && edge.label.trim() ? { condition: edge.label } : {}), + ...(input.schema === "execution-plan@2" ? { id: edge.id } : {}), }; }), }; diff --git a/src/routes/agentTrigger.ts b/src/routes/agentTrigger.ts index 8ed615e..164cc05 100644 --- a/src/routes/agentTrigger.ts +++ b/src/routes/agentTrigger.ts @@ -33,8 +33,8 @@ export const buildAgentTriggerPrompt = (trigger: AgentTriggerContext) => [ event_id: trigger.trigger_id, })}`, "这是合成演练数据,不得表述为生产事实。", - "开始调查前调用 publish_execution_plan 发布本次调查的结构化计划,明确并行依赖和条件分支;执行前、取得结果后、遇到阻塞或修改计划时再次发布完整快照。保留节点 ID、已完成节点及证据,重试用新节点,不要按时间推算进度。", + "根据这张工单的事实动态规划本次调查,不套用预设路线。开始调查前调用 publish_execution_plan 发布 execution-plan@2,明确路线选择、并行分叉与汇合、人工审核点;执行前、取得结果后、遇到阻塞或修改计划时再次发布完整快照。保留节点 ID、已完成节点及证据,重试用新节点,不要按时间推算进度。", "每个完成节点必须列明实际依据。预设证据标注为合成场景输入,不得声称执行过未调用的数据查询工具。现场采样、清洗、复测、回访等人工工作保持 waiting,写清等待内容,不得因回复结束标记完成。", "外围样板随事件提供的合成场景输入(不是 Agent 实时查询结果):上游三个模拟水质监测点的数据质量均为 valid,演示指标均在场景阈值内,且相邻片区没有同类投诉;投诉集中在演示小区,二次供水水箱最近一次清洗记录待核验。", - "请按‘上游数据质量正常、上游水质未见异常 → 疑似二次供水环节问题(待现场核验)’形成阶段性判断,并给出处置、复测和回访建议。不得将疑似判断写成已确认事故。", + "依据实际取得的证据判断可能原因,区分合成场景输入、已查询数据和待现场核验事项;给出处置、复测和回访建议。不得将疑似判断写成已确认事故。", ].join("\n"); diff --git a/src/routes/chatSessionRoutes.ts b/src/routes/chatSessionRoutes.ts index b371531..1cc1a1a 100644 --- a/src/routes/chatSessionRoutes.ts +++ b/src/routes/chatSessionRoutes.ts @@ -105,7 +105,7 @@ export const registerChatSessionRoutes = ( body: JSON.stringify({ session_id: record.sessionId, model, - message: `执行工况任务${workflow ? `:${workflow}` : ""}。先读取后端任务上下文,再按任务类型更新计划、证据和报告。`, + message: `执行工况任务${workflow ? `:${workflow}` : ""}。先读取后端任务上下文,依据当前工单或人工请求动态规划 execution-plan@2;按实际选择的路线更新计划、证据和报告,数据权限不足时先申请并等待审核。`, }), }).then(async (response) => { const reader = response.body?.getReader(); diff --git a/src/routes/executionPlan.ts b/src/routes/executionPlan.ts index 4add5e8..9fd49cc 100644 --- a/src/routes/executionPlan.ts +++ b/src/routes/executionPlan.ts @@ -2,6 +2,7 @@ import { z } from "zod"; const id = z.string().regex(/^[a-zA-Z0-9_-]{1,64}$/); export const executionPlanSchema = z.object({ + schema: z.enum(["execution-plan@1", "execution-plan@2"]).default("execution-plan@1"), title: z.string().trim().min(1).max(120), reason: z.string().trim().min(1).max(1000), nodes: z.array(z.object({ @@ -12,18 +13,27 @@ export const executionPlanSchema = z.object({ detail: z.string().max(2000), evidence: z.array(z.string().min(1).max(1000)).max(12), action_refs: z.array(z.string().uuid()).max(12).default([]), + kind: z.enum(["activity", "decision", "parallel_split", "parallel_join", "gate"]).optional(), + selected_edge_id: id.nullable().optional(), + gate_ref: z.string().uuid().nullable().optional(), + revisits_node_id: id.nullable().optional(), })).min(1).max(40), - edges: z.array(z.object({ from: id, to: id, label: z.string().max(120) })).max(80), + edges: z.array(z.object({ id: id.optional(), from: id, to: id, label: z.string().max(120) })).max(80), }).superRefine((plan, context) => { const ids = new Set(plan.nodes.map((node) => node.id)); if (ids.size !== plan.nodes.length) context.addIssue({ code: "custom", message: "Duplicate node ID" }); const edges = new Set(); + const edgeIds = new Set(); for (const edge of plan.edges) { const key = `${edge.from}:${edge.to}`; if (!ids.has(edge.from) || !ids.has(edge.to) || edge.from === edge.to || edges.has(key)) { context.addIssue({ code: "custom", message: "Invalid or duplicate dependency" }); } edges.add(key); + if (plan.schema === "execution-plan@2") { + if (!edge.id || edgeIds.has(edge.id)) context.addIssue({ code: "custom", message: "V2 edges require unique IDs" }); + if (edge.id) edgeIds.add(edge.id); + } } const visiting = new Set(); const visited = new Set(); @@ -37,6 +47,35 @@ export const executionPlanSchema = z.object({ return false; }; if (plan.nodes.some((node) => visit(node.id))) context.addIssue({ code: "custom", message: "Dependencies must be acyclic; use a new node for a retry" }); + if (plan.schema === "execution-plan@2") { + if (plan.nodes.filter((node) => !plan.edges.some((edge) => edge.to === node.id)).length !== 1) { + context.addIssue({ code: "custom", message: "V2 plan requires one start node" }); + } + const nodeById = new Map(plan.nodes.map((node) => [node.id, node])); + for (const node of plan.nodes) { + const outgoing = plan.edges.filter((edge) => edge.from === node.id); + const incoming = plan.edges.filter((edge) => edge.to === node.id); + if (!node.kind) context.addIssue({ code: "custom", message: "V2 nodes require kind" }); + if (node.kind === "decision") { + if (outgoing.length < 2 || outgoing.some((edge) => !edge.label.trim())) context.addIssue({ code: "custom", message: "Decision routes must be labeled" }); + if (node.selected_edge_id && !outgoing.some((edge) => edge.id === node.selected_edge_id)) context.addIssue({ code: "custom", message: "Selected route must leave its decision" }); + if (node.status === "completed" && !node.selected_edge_id) context.addIssue({ code: "custom", message: "Completed decision requires a selected route" }); + if (node.selected_edge_id) { + for (const edge of outgoing) { + const targetStatus = nodeById.get(edge.to)?.status; + if (edge.id === node.selected_edge_id && ["skipped", "cancelled"].includes(targetStatus ?? "")) context.addIssue({ code: "custom", message: "Selected route cannot be skipped" }); + if (edge.id !== node.selected_edge_id && ["running", "completed", "waiting"].includes(targetStatus ?? "")) context.addIssue({ code: "custom", message: "Unchosen route cannot be active" }); + } + } + } else if (node.selected_edge_id) context.addIssue({ code: "custom", message: "Only decisions select routes" }); + if (node.kind !== "decision" && node.kind !== "parallel_split" && outgoing.length > 1) context.addIssue({ code: "custom", message: "Only decisions or parallel splits may branch" }); + if (node.kind === "parallel_split" && outgoing.length < 2) context.addIssue({ code: "custom", message: "Parallel split requires two routes" }); + if (node.kind === "parallel_join" && incoming.length < 2) context.addIssue({ code: "custom", message: "Parallel join requires two inputs" }); + if (node.kind === "gate" && !node.gate_ref) context.addIssue({ code: "custom", message: "Gate requires a persisted reference" }); + if (node.kind !== "gate" && node.gate_ref) context.addIssue({ code: "custom", message: "Only gates reference approvals" }); + if (node.revisits_node_id && !ids.has(node.revisits_node_id)) context.addIssue({ code: "custom", message: "Revisited node must exist" }); + } + } for (const node of plan.nodes) { if (node.status === "running" || node.status === "completed") { const hasIncompletePredecessor = plan.edges @@ -50,11 +89,21 @@ export const executionPlanSchema = z.object({ message: "Running or completed nodes require completed or skipped predecessors", }); } - if (node.status === "completed" && node.evidence.length === 0 && node.action_refs.length === 0) context.addIssue({ code: "custom", message: "Completed nodes require evidence or an approved action receipt" }); + if (node.status === "completed" && node.evidence.length === 0 && node.action_refs.length === 0 + && !(plan.schema === "execution-plan@2" && ["parallel_split", "parallel_join", "gate"].includes(node.kind ?? ""))) { + context.addIssue({ code: "custom", message: "Completed nodes require evidence or an approved action receipt" }); + } if ((node.status === "waiting" || node.status === "failed") && !node.detail.trim()) context.addIssue({ code: "custom", message: "Waiting or failed nodes require a reason" }); } }); +// Stored v1 plans remain readable, but every newly published task plan must +// opt into the graph and approval-gate guarantees of v2. +export const newExecutionPlanSchema = executionPlanSchema.refine( + (plan) => plan.schema === "execution-plan@2", + { message: "New task execution plans must use execution-plan@2", path: ["schema"] }, +); + export const executionPlanNeedsTaskPublish = (input: unknown) => !(input && typeof input === "object" && (input as Record).server_published === true); @@ -84,5 +133,5 @@ export function createExecutionPlanSnapshot(input: unknown, messages: unknown[], } } } - return { ...plan, schema_version: "execution-plan@1", session_id: sessionId, revision: history.length + 1, updated_at: new Date().toISOString() }; + return { ...plan, schema_version: plan.schema, session_id: sessionId, revision: history.length + 1, updated_at: new Date().toISOString() }; } diff --git a/src/server.ts b/src/server.ts index d634616..efdf66a 100644 --- a/src/server.ts +++ b/src/server.ts @@ -19,6 +19,7 @@ import { getRuntimeSessionContext } from "./runtime/sessionContext.js"; import { operationalTaskClient, operationalTaskDataQuerySchema, + expressAccessRequestSchema, humanTaskEvidenceCommandSchema, proposedTaskActionSchema, taskArtifactBridgeSchema, @@ -153,6 +154,28 @@ app.post("/internal/operational-task/data-query", async (req, res) => { } }); +app.post("/internal/operational-task/access-requests", async (req, res) => { + const bridge = readOperationalTaskBridgeContext(req); + if (!bridge || bridge.context.purpose !== "task_execution") { + res.status(403).json({ message: "task execution context required" }); + return; + } + const parsed = expressAccessRequestSchema.safeParse(req.body?.request); + if (!parsed.success) { + res.status(400).json({ message: "invalid access request" }); + return; + } + try { + res.json(await operationalTaskClient.requestDataAccess(bridge.runtimeSessionId, bridge.taskId, { + ...parsed.data, sourceSessionId: bridge.context.clientSessionId, + })); + } catch (error) { + const status = error && typeof error === "object" && "status" in error && typeof error.status === "number" + ? error.status : 502; + res.status(status).json({ message: error instanceof Error ? error.message : "access request failed" }); + } +}); + app.post("/internal/operational-task/actions", async (req, res) => { const bridge = readOperationalTaskBridgeContext(req); if (!bridge) { diff --git a/tests/opencode/internalBridgeTools.test.ts b/tests/opencode/internalBridgeTools.test.ts index f6be146..d7e0012 100644 --- a/tests/opencode/internalBridgeTools.test.ts +++ b/tests/opencode/internalBridgeTools.test.ts @@ -96,7 +96,7 @@ describe("OpenCode internal bridge tools", () => { "-e", [ 'import tool from "./.opencode/tools/publish_execution_plan.ts";', - 'const result = await tool.execute({ request_id: "60000000-0000-4000-8000-000000000001", title: "调查", reason: "开始", nodes: [{ id: "read", title: "读取", status: "completed", owner: "agent", detail: "完成", evidence: ["evidence-1"], action_refs: [] }], edges: [] }, { sessionID: "runtime-session-1" });', + 'const result = await tool.execute({ request_id: "60000000-0000-4000-8000-000000000001", schema: "execution-plan@2", title: "调查", reason: "开始", nodes: [{ id: "read", title: "读取", status: "completed", owner: "agent", detail: "完成", evidence: ["evidence-1"], action_refs: [], kind: "activity" }], edges: [] }, { sessionID: "runtime-session-1" });', "process.stdout.write(result);", ].join("\n"), ], { @@ -141,7 +141,7 @@ describe("OpenCode internal bridge tools", () => { "-e", [ 'import tool from "./.opencode/tools/publish_execution_plan.ts";', - 'await tool.execute({ request_id: "60000000-0000-4000-8000-000000000001", title: "调查", reason: "开始", nodes: [{ id: "read", title: "读取", status: "completed", owner: "agent", detail: "完成", evidence: ["evidence-1"], action_refs: [] }], edges: [] }, { sessionID: "runtime-session-1" });', + 'await tool.execute({ request_id: "60000000-0000-4000-8000-000000000001", schema: "execution-plan@2", title: "调查", reason: "开始", nodes: [{ id: "read", title: "读取", status: "completed", owner: "agent", detail: "完成", evidence: ["evidence-1"], action_refs: [], kind: "activity" }], edges: [] }, { sessionID: "runtime-session-1" });', ].join("\n"), ], { cwd: process.cwd(), diff --git a/tests/routes/agentTrigger.test.ts b/tests/routes/agentTrigger.test.ts index 8e903e8..22bc4ad 100644 --- a/tests/routes/agentTrigger.test.ts +++ b/tests/routes/agentTrigger.test.ts @@ -32,7 +32,8 @@ describe("agent event trigger", () => { expect(prompt).toContain("水质监测点"); expect(prompt).toContain("二次供水"); expect(prompt).toContain("合成演练数据"); - expect(prompt).toContain("疑似二次供水环节问题(待现场核验)"); + expect(prompt).toContain("动态规划本次调查"); + expect(prompt).not.toContain("请按‘上游数据质量正常"); expect(prompt).toContain(trigger.trigger_id); }); diff --git a/tests/routes/executionPlan.test.ts b/tests/routes/executionPlan.test.ts index a54416e..b56edb2 100644 --- a/tests/routes/executionPlan.test.ts +++ b/tests/routes/executionPlan.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "bun:test"; -import { createExecutionPlanSnapshot, executionPlanNeedsTaskPublish, executionPlanSchema, readExecutionPlans } from "../../src/routes/executionPlan.js"; +import { createExecutionPlanSnapshot, executionPlanNeedsTaskPublish, executionPlanSchema, newExecutionPlanSchema, readExecutionPlans } from "../../src/routes/executionPlan.js"; import { streamPromptResponse } from "../../src/routes/chatStream.js"; import type { OpencodeRuntimeAdapter } from "../../src/runtime/opencode.js"; @@ -13,6 +13,38 @@ const plan = { }; describe("execution plans", () => { + it("requires v2 for newly published task plans while retaining legacy reads", () => { + const legacy = { + title: "Legacy plan", + reason: "Stored before graph plans", + nodes: [{ id: "inspect", title: "Inspect", status: "pending", owner: "agent", detail: "", evidence: [] }], + edges: [], + }; + + expect(executionPlanSchema.parse(legacy).schema).toBe("execution-plan@1"); + expect(newExecutionPlanSchema.safeParse(legacy).success).toBe(false); + }); + + it("accepts explicit v2 branches and structural joins without invented evidence", () => { + const v2 = { + schema: "execution-plan@2", title: "动态调查", reason: "工单请求现场及数据并行核验", + nodes: [ + { id: "split", title: "并行启动", status: "completed", owner: "agent", detail: "同时核查", evidence: [], kind: "parallel_split" }, + { id: "field", title: "现场采样", status: "waiting", owner: "human", detail: "等待采样", evidence: [], kind: "activity" }, + { id: "data", title: "监测数据", status: "completed", owner: "agent", detail: "已核对", evidence: ["e-1"], kind: "activity" }, + { id: "join", title: "证据汇合", status: "waiting", owner: "agent", detail: "等待现场", evidence: [], kind: "parallel_join" }, + ], + edges: [ + { id: "field-route", from: "split", to: "field", label: "现场" }, + { id: "data-route", from: "split", to: "data", label: "数据" }, + { id: "field-join", from: "field", to: "join", label: "" }, + { id: "data-join", from: "data", to: "join", label: "" }, + ], + }; + const parsed = executionPlanSchema.parse(v2); + expect(createExecutionPlanSnapshot(parsed, [], "session-1").schema_version).toBe("execution-plan@2"); + expect(executionPlanSchema.safeParse({ ...v2, edges: [{ ...v2.edges[0], id: "data-route" }, ...v2.edges.slice(1)] }).success).toBe(false); + }); it("rejects missing dependencies, cycles, duplicate IDs and unsupported completion", () => { for (const invalid of [ { ...plan, edges: [{ from: "unknown", to: "sample", label: "" }] },