mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 20:08:18 +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);
|
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
|
* Resolve a pending approval (approve / deny) following the RFC-067 §4.2 two-phase
|
||||||
* contract: snapshot → DB UPDATE conditional on {@code status='PENDING'} →
|
* contract: snapshot → DB UPDATE conditional on {@code status='PENDING'} →
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
package vip.mate.workflow.runtime.mode;
|
package vip.mate.workflow.runtime.mode;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
import vip.mate.approval.ApprovalWorkflowService;
|
||||||
import vip.mate.workflow.compiler.ir.StepMode;
|
import vip.mate.workflow.compiler.ir.StepMode;
|
||||||
import vip.mate.workflow.compiler.ir.WorkflowStep;
|
import vip.mate.workflow.compiler.ir.WorkflowStep;
|
||||||
import vip.mate.workflow.model.WorkflowRunPauseEntity;
|
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
|
* declares a {@code timeoutSecs}; otherwise it stays {@code null} and the
|
||||||
* resumer treats the pause as open-ended.
|
* resumer treats the pause as open-ended.
|
||||||
*
|
*
|
||||||
* <p>The {@code external_approval_id} column on the pause row is reserved
|
* <p>The {@code external_approval_id} column on the pause row links to the
|
||||||
* for a future integration that bridges workflow pauses into the
|
* {@code mate_tool_approval} row created via
|
||||||
* tool-approval inbox; v0 leaves it null and routes resolution through the
|
* {@link ApprovalWorkflowService#requestWorkflowApproval} so the workflow
|
||||||
* pause-token path above.
|
* 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
|
@Component
|
||||||
public class AwaitApprovalStepAdapter implements StepAdapter {
|
public class AwaitApprovalStepAdapter implements StepAdapter {
|
||||||
|
|
||||||
private final WorkflowRunPauseMapper pauseMapper;
|
private final WorkflowRunPauseMapper pauseMapper;
|
||||||
private final WorkflowRunStepMapper stepMapper;
|
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,
|
public AwaitApprovalStepAdapter(WorkflowRunPauseMapper pauseMapper,
|
||||||
WorkflowRunStepMapper stepMapper) {
|
WorkflowRunStepMapper stepMapper) {
|
||||||
@ -77,6 +87,8 @@ public class AwaitApprovalStepAdapter implements StepAdapter {
|
|||||||
String pauseToken = UUID.randomUUID().toString();
|
String pauseToken = UUID.randomUUID().toString();
|
||||||
LocalDateTime now = LocalDateTime.now();
|
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();
|
WorkflowRunPauseEntity pause = new WorkflowRunPauseEntity();
|
||||||
pause.setRunId(context.runId());
|
pause.setRunId(context.runId());
|
||||||
pause.setStepId(stepRow.getId());
|
pause.setStepId(stepRow.getId());
|
||||||
@ -88,6 +100,35 @@ public class AwaitApprovalStepAdapter implements StepAdapter {
|
|||||||
}
|
}
|
||||||
pauseMapper.insert(pause);
|
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,
|
return StepResult.paused(pauseToken,
|
||||||
"awaiting " + (cfg.approvalKind() == null ? "approval" : cfg.approvalKind()));
|
"awaiting " + (cfg.approvalKind() == null ? "approval" : cfg.approvalKind()));
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user