mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 20:08:18 +08:00
feat(workflow,trigger): wire workflow_completion as a real event source
This commit is contained in:
parent
e3af03645b
commit
87727414d5
@ -0,0 +1,63 @@
|
|||||||
|
package vip.mate.trigger.dispatch;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import vip.mate.trigger.ingest.TriggerEventEnvelope;
|
||||||
|
import vip.mate.trigger.ingest.TriggerEventIngestService;
|
||||||
|
import vip.mate.workflow.runtime.WorkflowCompletionEvent;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bridges {@link WorkflowCompletionEvent} from the workflow module into the
|
||||||
|
* trigger ingest pipeline. Lives in the trigger module so the workflow
|
||||||
|
* runtime stays free of trigger / ingest dependencies — that's how we
|
||||||
|
* dodge the Runner ↔ Dispatcher ↔ Ingest ↔ Runner cycle Spring would
|
||||||
|
* otherwise refuse to construct.
|
||||||
|
*
|
||||||
|
* <p>Each terminal-state run is translated into a {@code workflow_completion}
|
||||||
|
* envelope with a deterministic {@code wf-run-{runId}} eventId, so the
|
||||||
|
* {@code mate_trigger_event} unique constraint dedups any re-publish
|
||||||
|
* (e.g. a runner crash + retry). Failures inside the ingest pipeline are
|
||||||
|
* logged and swallowed — a bad downstream trigger MUST NOT corrupt the
|
||||||
|
* just-completed run.
|
||||||
|
*
|
||||||
|
* <p>The listener fires in the runner thread by default; if a downstream
|
||||||
|
* ingest does heavy work, switch to {@code @Async} once a dedicated
|
||||||
|
* executor is wired.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class WorkflowCompletionEventBridge {
|
||||||
|
|
||||||
|
private final TriggerEventIngestService ingestService;
|
||||||
|
|
||||||
|
@EventListener
|
||||||
|
public void onCompletion(WorkflowCompletionEvent event) {
|
||||||
|
if (event == null) return;
|
||||||
|
try {
|
||||||
|
Map<String, Object> data = new HashMap<>();
|
||||||
|
data.put("sourceWorkflowId", event.workflowId());
|
||||||
|
data.put("revisionId", event.revisionId());
|
||||||
|
data.put("runId", event.runId());
|
||||||
|
data.put("state", event.state());
|
||||||
|
if (event.finalOutputRef() != null) data.put("finalOutputRef", event.finalOutputRef());
|
||||||
|
if (event.errorMessage() != null) data.put("errorMessage", event.errorMessage());
|
||||||
|
TriggerEventEnvelope envelope = new TriggerEventEnvelope(
|
||||||
|
event.workspaceId(),
|
||||||
|
"workflow_completion",
|
||||||
|
"wf-run-" + event.runId(),
|
||||||
|
"system",
|
||||||
|
data);
|
||||||
|
ingestService.ingest(envelope);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[WorkflowCompletionBridge] forwarding run {} completion failed: {}",
|
||||||
|
event.runId(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -140,8 +140,17 @@ public class TriggerPatternMatcher {
|
|||||||
}
|
}
|
||||||
String wantState = textOrNull(pattern, "stateFilter");
|
String wantState = textOrNull(pattern, "stateFilter");
|
||||||
if (wantState != null && !"any".equalsIgnoreCase(wantState)) {
|
if (wantState != null && !"any".equalsIgnoreCase(wantState)) {
|
||||||
Object state = data.get("state");
|
Object stateObj = data.get("state");
|
||||||
if (!(state instanceof String s) || !wantState.equalsIgnoreCase(s)) return false;
|
if (!(stateObj instanceof String actualState)) return false;
|
||||||
|
// The runtime emits "succeeded" / "failed"; pattern authors
|
||||||
|
// commonly type "completed" to mean "non-failed terminal".
|
||||||
|
// Treat the two as equivalent so authors don't have to care
|
||||||
|
// which vocabulary the runner happens to use today.
|
||||||
|
if ("completed".equalsIgnoreCase(wantState)) {
|
||||||
|
if (!"succeeded".equalsIgnoreCase(actualState)) return false;
|
||||||
|
} else if (!wantState.equalsIgnoreCase(actualState)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,24 @@
|
|||||||
|
package vip.mate.workflow.runtime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring application event fired when a workflow run reaches a terminal
|
||||||
|
* state ({@code succeeded} / {@code failed}). The trigger module
|
||||||
|
* subscribes via {@code @EventListener} and pushes the payload through
|
||||||
|
* {@link vip.mate.trigger.ingest.TriggerEventIngestService} so downstream
|
||||||
|
* triggers (e.g. {@code workflow_completion} pattern) can chain off the
|
||||||
|
* outcome.
|
||||||
|
*
|
||||||
|
* <p>Going through the event bus instead of injecting the trigger
|
||||||
|
* service directly into the workflow runner breaks the
|
||||||
|
* Runner ↔ Dispatcher ↔ Ingest ↔ Runner circular dependency that Spring
|
||||||
|
* would otherwise refuse to construct.
|
||||||
|
*/
|
||||||
|
public record WorkflowCompletionEvent(
|
||||||
|
long runId,
|
||||||
|
long workflowId,
|
||||||
|
long revisionId,
|
||||||
|
long workspaceId,
|
||||||
|
String state,
|
||||||
|
String finalOutputRef,
|
||||||
|
String errorMessage
|
||||||
|
) {}
|
||||||
@ -105,6 +105,12 @@ public class WorkflowResumer {
|
|||||||
runRow.setErrorMessage("paused step '" + stepRow.getStepName() + "' " + outcome.token());
|
runRow.setErrorMessage("paused step '" + stepRow.getStepName() + "' " + outcome.token());
|
||||||
runRow.setCompletedAt(LocalDateTime.now());
|
runRow.setCompletedAt(LocalDateTime.now());
|
||||||
runMapper.updateById(runRow);
|
runMapper.updateById(runRow);
|
||||||
|
// Publish the workflow_completion event downstream — same as the
|
||||||
|
// runner's finishFailed path. Without this, runs that end on a
|
||||||
|
// rejected / timed-out approval would never fire their
|
||||||
|
// completion trigger because the resumer skips
|
||||||
|
// runner.continueFromIndex on the failure branch.
|
||||||
|
runner.publishCompletionEvent(runRow, STATE_FAILED, null, runRow.getErrorMessage());
|
||||||
return Outcome.failed(runRow.getId(), runRow.getErrorMessage());
|
return Outcome.failed(runRow.getId(), runRow.getErrorMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
package vip.mate.workflow.runtime;
|
package vip.mate.workflow.runtime;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import vip.mate.workflow.compiler.ir.StepMode;
|
import vip.mate.workflow.compiler.ir.StepMode;
|
||||||
import vip.mate.workflow.compiler.ir.WorkflowGraph;
|
import vip.mate.workflow.compiler.ir.WorkflowGraph;
|
||||||
@ -26,11 +28,20 @@ import java.util.concurrent.TimeUnit;
|
|||||||
* marks the row {@code failed}. The last non-skipped step's output payload
|
* marks the row {@code failed}. The last non-skipped step's output payload
|
||||||
* is recorded as {@code final_output_ref} on success.
|
* is recorded as {@code final_output_ref} on success.
|
||||||
*
|
*
|
||||||
* <p>StateGraph is intentionally not used here — the four base modes
|
* <p><b>v0 runtime decision:</b> StateGraph is intentionally not used here.
|
||||||
* (sequential / fan_out / collect / conditional) are linear plus one bounded
|
* The seven v0 modes (sequential / fan_out / collect / conditional +
|
||||||
* parallel section, which a small executor handles more directly. When
|
* await_approval / dispatch_channel / write_memory) are linear plus one
|
||||||
* {@code await_approval} lands the runtime will switch to a graph-backed
|
* bounded parallel section, which this small executor handles more
|
||||||
* scheduler so pause / resume can survive a JVM restart.
|
* directly than wrapping a graph DSL. {@code await_approval} pause / resume
|
||||||
|
* is implemented via {@link WorkflowResumer} reading the persisted
|
||||||
|
* {@code mate_workflow_run_pause} row, so a JVM restart still recovers the
|
||||||
|
* run. v1 will reassess whether to graduate to a graph-backed scheduler
|
||||||
|
* once {@code loop} / {@code invoke_skill} land — until then, "linear
|
||||||
|
* executor" is the explicit, supported runtime.
|
||||||
|
*
|
||||||
|
* <p>StateGraph remains in use elsewhere for agent-internal control flow
|
||||||
|
* (ReAct / Plan-Execute) — that's the runtime owned by
|
||||||
|
* {@link vip.mate.agent agent module}, not this workflow module.
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@ -49,6 +60,10 @@ public class WorkflowRunner {
|
|||||||
private final WorkflowRunStepMapper stepMapper;
|
private final WorkflowRunStepMapper stepMapper;
|
||||||
private final StepAdapterRegistry adapters;
|
private final StepAdapterRegistry adapters;
|
||||||
private final PayloadStore payloadStore;
|
private final PayloadStore payloadStore;
|
||||||
|
/** Optional — wired in production, may be null in narrow test contexts.
|
||||||
|
* Spring's stock publisher is always available in a full context. */
|
||||||
|
@Autowired(required = false)
|
||||||
|
private ApplicationEventPublisher events;
|
||||||
|
|
||||||
public WorkflowRunner(WorkflowRunMapper runMapper,
|
public WorkflowRunner(WorkflowRunMapper runMapper,
|
||||||
WorkflowRunStepMapper stepMapper,
|
WorkflowRunStepMapper stepMapper,
|
||||||
@ -241,6 +256,7 @@ public class WorkflowRunner {
|
|||||||
runRow.setFinalOutputRef(finalOutputRef);
|
runRow.setFinalOutputRef(finalOutputRef);
|
||||||
runRow.setCompletedAt(LocalDateTime.now());
|
runRow.setCompletedAt(LocalDateTime.now());
|
||||||
runMapper.updateById(runRow);
|
runMapper.updateById(runRow);
|
||||||
|
publishCompletionEvent(runRow, STATE_SUCCEEDED, finalOutputRef, null);
|
||||||
return new WorkflowRunResult(runRow.getId(), STATE_SUCCEEDED, finalOutputRef, null);
|
return new WorkflowRunResult(runRow.getId(), STATE_SUCCEEDED, finalOutputRef, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -249,9 +265,44 @@ public class WorkflowRunner {
|
|||||||
runRow.setErrorMessage(errorMessage);
|
runRow.setErrorMessage(errorMessage);
|
||||||
runRow.setCompletedAt(LocalDateTime.now());
|
runRow.setCompletedAt(LocalDateTime.now());
|
||||||
runMapper.updateById(runRow);
|
runMapper.updateById(runRow);
|
||||||
|
publishCompletionEvent(runRow, STATE_FAILED, null, errorMessage);
|
||||||
return new WorkflowRunResult(runRow.getId(), STATE_FAILED, null, errorMessage);
|
return new WorkflowRunResult(runRow.getId(), STATE_FAILED, null, errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fire a {@code workflow_completion} event into the trigger pipeline so
|
||||||
|
* downstream workflows (or workflows reacting to upstream success /
|
||||||
|
* failure) can chain off this run. Synchronous and best-effort: a
|
||||||
|
* fan-out failure here MUST NOT corrupt the just-completed run state.
|
||||||
|
*
|
||||||
|
* <p>The eventId is keyed on {@code wf-run-{runId}} so a retry of the
|
||||||
|
* same run never duplicate-fires its completion downstream — the
|
||||||
|
* mate_trigger_event UNIQUE(trigger_id, dedup_key) constraint catches
|
||||||
|
* any redundant publish at insert time.
|
||||||
|
*
|
||||||
|
* <p>Package-private so {@link WorkflowResumer} can publish the same
|
||||||
|
* event for resumed runs that end on a rejected / timed-out approval
|
||||||
|
* (those don't go through {@link #finishFailed} since the resumer
|
||||||
|
* writes terminal state directly).
|
||||||
|
*/
|
||||||
|
void publishCompletionEvent(WorkflowRunEntity runRow, String state,
|
||||||
|
String finalOutputRef, String errorMessage) {
|
||||||
|
if (events == null || runRow == null) return;
|
||||||
|
try {
|
||||||
|
events.publishEvent(new WorkflowCompletionEvent(
|
||||||
|
runRow.getId(),
|
||||||
|
runRow.getWorkflowId() == null ? 0L : runRow.getWorkflowId(),
|
||||||
|
runRow.getRevisionId() == null ? 0L : runRow.getRevisionId(),
|
||||||
|
runRow.getWorkspaceId() == null ? 0L : runRow.getWorkspaceId(),
|
||||||
|
state,
|
||||||
|
finalOutputRef,
|
||||||
|
errorMessage));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Workflow run {} completion event publish failed: {}",
|
||||||
|
runRow.getId(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private WorkflowRunResult finishPaused(WorkflowRunEntity runRow, String pauseToken) {
|
private WorkflowRunResult finishPaused(WorkflowRunEntity runRow, String pauseToken) {
|
||||||
runRow.setState(STATE_PAUSED);
|
runRow.setState(STATE_PAUSED);
|
||||||
// Pause leaves the run open — completedAt stays null until resume settles it.
|
// Pause leaves the run open — completedAt stays null until resume settles it.
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
class="step-node"
|
class="step-node"
|
||||||
:class="`mode-${data.modeType}`"
|
:class="`mode-${data.modeType}`"
|
||||||
:data-selected="props.selected"
|
:data-selected="props.selected"
|
||||||
@click="handleClick"
|
@click.stop="handleClick"
|
||||||
>
|
>
|
||||||
<Handle type="target" :position="targetPosition" />
|
<Handle type="target" :position="targetPosition" />
|
||||||
<div class="step-band" />
|
<div class="step-band" />
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user