mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(workflow): wire await_approval into the approval inbox
This commit is contained in:
parent
247caaac3d
commit
e3af03645b
@ -260,6 +260,78 @@ public class ApprovalWorkflowService implements ApplicationRunner {
|
||||
toolCallPayload, siblingToolCalls, agentId, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Workflow-scoped approval request — creates a {@code mate_tool_approval}
|
||||
* row keyed to a workflow run + step instead of a conversation, so an
|
||||
* {@code await_approval} step is visible in the same approval inbox the
|
||||
* tool-approval flow uses. Returns the row's auto-generated long id; the
|
||||
* caller (typically {@code AwaitApprovalStepAdapter}) writes that id back
|
||||
* onto {@code mate_workflow_run_pause.external_approval_id} so a future
|
||||
* approval-resolve callback can map "approval X resolved → resume run Y".
|
||||
*
|
||||
* <p>The approval row's {@code conversationId} is set to
|
||||
* {@code "workflow:run:{runId}"} as a synthetic key — that lets the
|
||||
* existing {@link ApprovalService#findPendingByConversation} surface the
|
||||
* workflow approval to operator UIs without needing a parallel query
|
||||
* surface. {@code toolName} is set to {@code "workflow:{kind}"} so the
|
||||
* inbox can group / filter workflow approvals from tool approvals.
|
||||
*
|
||||
* <p>v0 keeps the resume path through {@code WorkflowResumeController}
|
||||
* with the pauseToken; this method does not yet wire a resolve→resume
|
||||
* callback. The approval row's purpose for v0 is operator visibility
|
||||
* and a stable foreign key for the pause record.
|
||||
*/
|
||||
public Long requestWorkflowApproval(long workspaceId,
|
||||
long runId,
|
||||
Long stepId,
|
||||
String approvalKind,
|
||||
String approvalMessage,
|
||||
java.util.List<String> approverChannels,
|
||||
Integer timeoutSecs) {
|
||||
try {
|
||||
ToolApprovalEntity entity = new ToolApprovalEntity();
|
||||
// pendingId is the string handle the existing approval pipeline
|
||||
// uses for resolve / get; "wf-" prefix lets future code branch
|
||||
// on workflow-scoped vs tool-scoped approvals at a glance. The
|
||||
// pending_id column is VARCHAR(32) so we trim a no-dashes UUID
|
||||
// down to fit ("wf-" + 24 hex chars = 27 chars; collisions of
|
||||
// 24 hex chars per workflow are astronomically rare and we
|
||||
// also fall back to UNIQUE-key violation handling).
|
||||
String shortId = java.util.UUID.randomUUID().toString()
|
||||
.replace("-", "").substring(0, 24);
|
||||
entity.setPendingId("wf-" + shortId);
|
||||
entity.setConversationId("workflow:run:" + runId);
|
||||
String kind = approvalKind == null || approvalKind.isBlank() ? "manual" : approvalKind.trim();
|
||||
entity.setToolName("workflow:" + kind);
|
||||
entity.setSummary(approvalMessage == null ? "" : approvalMessage);
|
||||
// Encode approver channels in tool_arguments so the inbox UI can
|
||||
// render which channels were asked. Plain JSON to keep parsing
|
||||
// trivial on the read path.
|
||||
try {
|
||||
if (approverChannels != null && !approverChannels.isEmpty()) {
|
||||
entity.setToolArguments(objectMapper.writeValueAsString(
|
||||
java.util.Map.of(
|
||||
"runId", runId,
|
||||
"stepId", stepId,
|
||||
"approverChannels", approverChannels)));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[ApprovalWorkflow] failed to encode approverChannels: {}", e.getMessage());
|
||||
}
|
||||
entity.setStatus("PENDING");
|
||||
entity.setCreatedAt(LocalDateTime.now());
|
||||
entity.setExpireAt(LocalDateTime.now().plusSeconds(
|
||||
timeoutSecs != null && timeoutSecs > 0 ? timeoutSecs : 30 * 60));
|
||||
approvalMapper.insert(entity);
|
||||
log.info("[ApprovalWorkflow] requested workflow approval row id={}, runId={}, workspace={}, kind={}",
|
||||
entity.getId(), runId, workspaceId, kind);
|
||||
return entity.getId();
|
||||
} catch (Exception e) {
|
||||
log.warn("[ApprovalWorkflow] requestWorkflowApproval failed: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a pending approval (approve / deny) following the RFC-067 §4.2 two-phase
|
||||
* contract: snapshot → DB UPDATE conditional on {@code status='PENDING'} →
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
package vip.mate.workflow.runtime.mode;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import vip.mate.approval.ApprovalWorkflowService;
|
||||
import vip.mate.workflow.compiler.ir.StepMode;
|
||||
import vip.mate.workflow.compiler.ir.WorkflowStep;
|
||||
import vip.mate.workflow.model.WorkflowRunPauseEntity;
|
||||
@ -37,16 +39,24 @@ import java.util.UUID;
|
||||
* declares a {@code timeoutSecs}; otherwise it stays {@code null} and the
|
||||
* resumer treats the pause as open-ended.
|
||||
*
|
||||
* <p>The {@code external_approval_id} column on the pause row is reserved
|
||||
* for a future integration that bridges workflow pauses into the
|
||||
* tool-approval inbox; v0 leaves it null and routes resolution through the
|
||||
* pause-token path above.
|
||||
* <p>The {@code external_approval_id} column on the pause row links to the
|
||||
* {@code mate_tool_approval} row created via
|
||||
* {@link ApprovalWorkflowService#requestWorkflowApproval} so the workflow
|
||||
* pause is visible in the same approval inbox the tool-approval flow uses.
|
||||
* Resolution still goes through {@code WorkflowResumeController} +
|
||||
* pauseToken — the approval row is for operator visibility today; v1 wires
|
||||
* the resolve→resume callback so an inbox decision can also fire the
|
||||
* resumer.
|
||||
*/
|
||||
@Component
|
||||
public class AwaitApprovalStepAdapter implements StepAdapter {
|
||||
|
||||
private final WorkflowRunPauseMapper pauseMapper;
|
||||
private final WorkflowRunStepMapper stepMapper;
|
||||
/** Optional — not all test contexts wire the approval module up. The
|
||||
* adapter falls back to a no-op approval row when null. */
|
||||
@Autowired(required = false)
|
||||
private ApprovalWorkflowService approvalService;
|
||||
|
||||
public AwaitApprovalStepAdapter(WorkflowRunPauseMapper pauseMapper,
|
||||
WorkflowRunStepMapper stepMapper) {
|
||||
@ -77,6 +87,8 @@ public class AwaitApprovalStepAdapter implements StepAdapter {
|
||||
String pauseToken = UUID.randomUUID().toString();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
// Insert the pause row first so we have a stable id to reference
|
||||
// even if the approval-service call below fails.
|
||||
WorkflowRunPauseEntity pause = new WorkflowRunPauseEntity();
|
||||
pause.setRunId(context.runId());
|
||||
pause.setStepId(stepRow.getId());
|
||||
@ -88,6 +100,35 @@ public class AwaitApprovalStepAdapter implements StepAdapter {
|
||||
}
|
||||
pauseMapper.insert(pause);
|
||||
|
||||
// Bridge into the approval inbox: create a mate_tool_approval row so
|
||||
// the workflow pause shows up alongside tool approvals, then write
|
||||
// the row id back as external_approval_id for the future
|
||||
// resolve→resume callback. Failures here are non-fatal — the run
|
||||
// is still resolvable via pauseToken + WorkflowResumeController.
|
||||
if (approvalService != null) {
|
||||
try {
|
||||
Long approvalId = approvalService.requestWorkflowApproval(
|
||||
context.workspaceId(),
|
||||
context.runId(),
|
||||
stepRow.getId(),
|
||||
cfg.approvalKind(),
|
||||
cfg.approvalMessage(),
|
||||
cfg.approverChannels(),
|
||||
cfg.timeoutSecs());
|
||||
if (approvalId != null) {
|
||||
pause.setExternalApprovalId(approvalId);
|
||||
pauseMapper.updateById(pause);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Non-fatal — log and continue. The pause row is the
|
||||
// canonical record for v0; the approval row is a parallel
|
||||
// visibility surface that can rebuild later if needed.
|
||||
org.slf4j.LoggerFactory.getLogger(AwaitApprovalStepAdapter.class)
|
||||
.warn("await_approval failed to create approval row for run {}: {}",
|
||||
context.runId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return StepResult.paused(pauseToken,
|
||||
"awaiting " + (cfg.approvalKind() == null ? "approval" : cfg.approvalKind()));
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user