diff --git a/mateclaw-server/src/main/java/vip/mate/trigger/model/TriggerEntity.java b/mateclaw-server/src/main/java/vip/mate/trigger/model/TriggerEntity.java new file mode 100644 index 00000000..aa295eaf --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/trigger/model/TriggerEntity.java @@ -0,0 +1,71 @@ +package vip.mate.trigger.model; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableLogic; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * Trigger row. {@code patternVersion} is a lamport counter that fire callbacks + * compare against the row on every fire; mismatches mean another instance has + * updated the cron expression and the local schedule must self-cancel. + */ +@Data +@TableName("mate_trigger") +public class TriggerEntity { + + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + private Long workspaceId; + + @TableField(value = "name", updateStrategy = FieldStrategy.ALWAYS) + private String name; + + /** Pattern flavour: cron / webhook / channel_message / agent_lifecycle / content_match / workflow_completion. */ + private String patternType; + + @TableField(value = "pattern_json", updateStrategy = FieldStrategy.ALWAYS) + private String patternJson; + + /** Routing target type: agent or workflow. */ + private String targetType; + + private Long targetId; + + @TableField(value = "payload_template", updateStrategy = FieldStrategy.ALWAYS) + private String payloadTemplate; + + private Integer rateLimitPerMin; + + private Integer dedupWindowSecs; + + private Boolean botSelfFilter; + + private Boolean enabled; + + private Long fireCount; + + private Long maxFires; + + @TableField(value = "last_fired_at", updateStrategy = FieldStrategy.ALWAYS) + private LocalDateTime lastFiredAt; + + /** Lamport counter — bump on every cron expression / payload template change. */ + private Long patternVersion; + + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updateTime; + + @TableLogic + private Integer deleted; +} diff --git a/mateclaw-server/src/main/java/vip/mate/trigger/model/TriggerEventEntity.java b/mateclaw-server/src/main/java/vip/mate/trigger/model/TriggerEventEntity.java new file mode 100644 index 00000000..d76907a1 --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/trigger/model/TriggerEventEntity.java @@ -0,0 +1,31 @@ +package vip.mate.trigger.model; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * Trigger dedup-window row. {@code dedupKey} carries envelope.eventId, falling + * back to a content sha256 when the upstream channel did not provide a stable + * id. {@code expiresAt} is set on insert to {@code receivedAt + dedupWindowSecs} + * so the cleanup task can sweep expired rows. + */ +@Data +@TableName("mate_trigger_event") +public class TriggerEventEntity { + + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + private Long triggerId; + + private String dedupKey; + + /** Filled by DB DEFAULT CURRENT_TIMESTAMP when left null on insert. */ + private LocalDateTime receivedAt; + + private LocalDateTime expiresAt; +} diff --git a/mateclaw-server/src/main/java/vip/mate/trigger/repository/TriggerEventMapper.java b/mateclaw-server/src/main/java/vip/mate/trigger/repository/TriggerEventMapper.java new file mode 100644 index 00000000..2fd612d6 --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/trigger/repository/TriggerEventMapper.java @@ -0,0 +1,9 @@ +package vip.mate.trigger.repository; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import vip.mate.trigger.model.TriggerEventEntity; + +@Mapper +public interface TriggerEventMapper extends BaseMapper { +} diff --git a/mateclaw-server/src/main/java/vip/mate/trigger/repository/TriggerMapper.java b/mateclaw-server/src/main/java/vip/mate/trigger/repository/TriggerMapper.java new file mode 100644 index 00000000..54f49629 --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/trigger/repository/TriggerMapper.java @@ -0,0 +1,9 @@ +package vip.mate.trigger.repository; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import vip.mate.trigger.model.TriggerEntity; + +@Mapper +public interface TriggerMapper extends BaseMapper { +} diff --git a/mateclaw-server/src/main/java/vip/mate/workflow/model/WorkflowEntity.java b/mateclaw-server/src/main/java/vip/mate/workflow/model/WorkflowEntity.java new file mode 100644 index 00000000..7a1ced5e --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/workflow/model/WorkflowEntity.java @@ -0,0 +1,61 @@ +package vip.mate.workflow.model; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableLogic; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * Stable workflow identity. The current draft is stored inline (1:1 with the + * workflow row) so PK uniqueness automatically guarantees a single draft; + * published snapshots live in {@code mate_workflow_revision}. + */ +@Data +@TableName("mate_workflow") +public class WorkflowEntity { + + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + private Long workspaceId; + + private String name; + + private String description; + + private Boolean enabled; + + /** Inline draft graph_json; null when there is no active draft. */ + @TableField(value = "draft_json", updateStrategy = FieldStrategy.ALWAYS) + private String draftJson; + + @TableField(value = "draft_schema_version", updateStrategy = FieldStrategy.ALWAYS) + private String draftSchemaVersion; + + @TableField(value = "draft_updated_by", updateStrategy = FieldStrategy.ALWAYS) + private Long draftUpdatedBy; + + @TableField(value = "draft_updated_at", updateStrategy = FieldStrategy.ALWAYS) + private LocalDateTime draftUpdatedAt; + + /** Pointer to the most recently published revision; null if never published. */ + @TableField(value = "latest_revision_id", updateStrategy = FieldStrategy.ALWAYS) + private Long latestRevisionId; + + private Long createdBy; + + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updateTime; + + @TableLogic + private Integer deleted; +} diff --git a/mateclaw-server/src/main/java/vip/mate/workflow/model/WorkflowPayloadEntity.java b/mateclaw-server/src/main/java/vip/mate/workflow/model/WorkflowPayloadEntity.java new file mode 100644 index 00000000..22233c11 --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/workflow/model/WorkflowPayloadEntity.java @@ -0,0 +1,48 @@ +package vip.mate.workflow.model; + +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * Payload body addressed by a stable URI. Small payloads (< 256KB) live + * inline in {@code contentBytes}; larger payloads point at filesystem or + * object storage via {@code storageKind} + {@code storageRef}. {@code sha256} + * is for tamper detection only — v0 does not deduplicate across runs. + */ +@Data +@TableName("mate_workflow_payload") +public class WorkflowPayloadEntity { + + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + private String payloadUri; + + private Long workspaceId; + + @TableField(value = "content_bytes", updateStrategy = FieldStrategy.ALWAYS) + private byte[] contentBytes; + + /** Storage flavour: inline / fs / s3 / oss. */ + private String storageKind; + + @TableField(value = "storage_ref", updateStrategy = FieldStrategy.ALWAYS) + private String storageRef; + + @TableField(value = "content_type", updateStrategy = FieldStrategy.ALWAYS) + private String contentType; + + @TableField(value = "sha256", updateStrategy = FieldStrategy.ALWAYS) + private String sha256; + + @TableField(value = "size_bytes", updateStrategy = FieldStrategy.ALWAYS) + private Long sizeBytes; + + private LocalDateTime createdAt; +} diff --git a/mateclaw-server/src/main/java/vip/mate/workflow/model/WorkflowRevisionEntity.java b/mateclaw-server/src/main/java/vip/mate/workflow/model/WorkflowRevisionEntity.java new file mode 100644 index 00000000..733766b9 --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/workflow/model/WorkflowRevisionEntity.java @@ -0,0 +1,41 @@ +package vip.mate.workflow.model; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * Immutable published snapshot of a workflow. The {@code revision} column is + * monotonic per workflow; rows are append-only after publish. + */ +@Data +@TableName("mate_workflow_revision") +public class WorkflowRevisionEntity { + + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + private Long workflowId; + + private Integer revision; + + @TableField(value = "graph_json", updateStrategy = FieldStrategy.ALWAYS) + private String graphJson; + + private String schemaVersion; + + @TableField(value = "published_note", updateStrategy = FieldStrategy.ALWAYS) + private String publishedNote; + + @TableField(value = "published_by", updateStrategy = FieldStrategy.ALWAYS) + private Long publishedBy; + + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; +} diff --git a/mateclaw-server/src/main/java/vip/mate/workflow/model/WorkflowRunEntity.java b/mateclaw-server/src/main/java/vip/mate/workflow/model/WorkflowRunEntity.java new file mode 100644 index 00000000..4f31b9f1 --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/workflow/model/WorkflowRunEntity.java @@ -0,0 +1,61 @@ +package vip.mate.workflow.model; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableLogic; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * Workflow run instance. Run is locked to a specific revision for stability + * even when later revisions are published. Initial input and final output are + * stored as payload URIs to avoid bloating the run row. + */ +@Data +@TableName("mate_workflow_run") +public class WorkflowRunEntity { + + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + private Long workflowId; + + private Long revisionId; + + private Long workspaceId; + + /** State machine value: pending / running / paused / succeeded / failed / cancelled / timed_out. */ + private String state; + + @TableField(value = "triggered_by", updateStrategy = FieldStrategy.ALWAYS) + private String triggeredBy; + + @TableField(value = "triggered_meta", updateStrategy = FieldStrategy.ALWAYS) + private String triggeredMeta; + + @TableField(value = "initial_input_ref", updateStrategy = FieldStrategy.ALWAYS) + private String initialInputRef; + + @TableField(value = "final_output_ref", updateStrategy = FieldStrategy.ALWAYS) + private String finalOutputRef; + + @TableField(value = "error_message", updateStrategy = FieldStrategy.ALWAYS) + private String errorMessage; + + @TableField(value = "started_at", updateStrategy = FieldStrategy.ALWAYS) + private LocalDateTime startedAt; + + @TableField(value = "completed_at", updateStrategy = FieldStrategy.ALWAYS) + private LocalDateTime completedAt; + + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + @TableLogic + private Integer deleted; +} diff --git a/mateclaw-server/src/main/java/vip/mate/workflow/model/WorkflowRunPauseEntity.java b/mateclaw-server/src/main/java/vip/mate/workflow/model/WorkflowRunPauseEntity.java new file mode 100644 index 00000000..0f5fe913 --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/workflow/model/WorkflowRunPauseEntity.java @@ -0,0 +1,51 @@ +package vip.mate.workflow.model; + +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * Durable workflow pause row. Holds the resume token and links back to the + * external approval row (or other callback source) so that resume can be + * triggered idempotently after a JVM restart. + */ +@Data +@TableName("mate_workflow_run_pause") +public class WorkflowRunPauseEntity { + + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + private Long runId; + + private Long stepId; + + /** Source of the pause: await_approval, external_callback, etc. */ + private String pauseKind; + + /** Random server-generated token used as the resume entry key. */ + private String pauseToken; + + @TableField(value = "external_approval_id", updateStrategy = FieldStrategy.ALWAYS) + private Long externalApprovalId; + + private LocalDateTime pausedAt; + + @TableField(value = "resume_deadline", updateStrategy = FieldStrategy.ALWAYS) + private LocalDateTime resumeDeadline; + + @TableField(value = "resume_payload_ref", updateStrategy = FieldStrategy.ALWAYS) + private String resumePayloadRef; + + @TableField(value = "resumed_at", updateStrategy = FieldStrategy.ALWAYS) + private LocalDateTime resumedAt; + + /** Outcome on resume: approved / rejected / timeout / cancelled. */ + @TableField(value = "resume_outcome", updateStrategy = FieldStrategy.ALWAYS) + private String resumeOutcome; +} diff --git a/mateclaw-server/src/main/java/vip/mate/workflow/model/WorkflowRunStepEntity.java b/mateclaw-server/src/main/java/vip/mate/workflow/model/WorkflowRunStepEntity.java new file mode 100644 index 00000000..e4d0d706 --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/workflow/model/WorkflowRunStepEntity.java @@ -0,0 +1,68 @@ +package vip.mate.workflow.model; + +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * Per-step execution row. {@code stepIndex} is the zero-based index in the + * revision's steps array; {@code iterationIndex} is reserved for fan_out + * iterations (and future loop bodies). + */ +@Data +@TableName("mate_workflow_run_step") +public class WorkflowRunStepEntity { + + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + private Long runId; + + private Integer stepIndex; + + @TableField(value = "iteration_index", updateStrategy = FieldStrategy.ALWAYS) + private Integer iterationIndex; + + @TableField(value = "step_name", updateStrategy = FieldStrategy.ALWAYS) + private String stepName; + + @TableField(value = "agent_id", updateStrategy = FieldStrategy.ALWAYS) + private Long agentId; + + private String state; + + @TableField(value = "input_ref", updateStrategy = FieldStrategy.ALWAYS) + private String inputRef; + + @TableField(value = "output_ref", updateStrategy = FieldStrategy.ALWAYS) + private String outputRef; + + @TableField(value = "output_summary", updateStrategy = FieldStrategy.ALWAYS) + private String outputSummary; + + @TableField(value = "output_content_type", updateStrategy = FieldStrategy.ALWAYS) + private String outputContentType; + + @TableField(value = "error_message", updateStrategy = FieldStrategy.ALWAYS) + private String errorMessage; + + @TableField(value = "duration_ms", updateStrategy = FieldStrategy.ALWAYS) + private Long durationMs; + + @TableField(value = "token_input", updateStrategy = FieldStrategy.ALWAYS) + private Integer tokenInput; + + @TableField(value = "token_output", updateStrategy = FieldStrategy.ALWAYS) + private Integer tokenOutput; + + @TableField(value = "started_at", updateStrategy = FieldStrategy.ALWAYS) + private LocalDateTime startedAt; + + @TableField(value = "completed_at", updateStrategy = FieldStrategy.ALWAYS) + private LocalDateTime completedAt; +} diff --git a/mateclaw-server/src/main/java/vip/mate/workflow/repository/WorkflowMapper.java b/mateclaw-server/src/main/java/vip/mate/workflow/repository/WorkflowMapper.java new file mode 100644 index 00000000..c4c6b662 --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/workflow/repository/WorkflowMapper.java @@ -0,0 +1,9 @@ +package vip.mate.workflow.repository; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import vip.mate.workflow.model.WorkflowEntity; + +@Mapper +public interface WorkflowMapper extends BaseMapper { +} diff --git a/mateclaw-server/src/main/java/vip/mate/workflow/repository/WorkflowPayloadMapper.java b/mateclaw-server/src/main/java/vip/mate/workflow/repository/WorkflowPayloadMapper.java new file mode 100644 index 00000000..8b50cd2a --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/workflow/repository/WorkflowPayloadMapper.java @@ -0,0 +1,9 @@ +package vip.mate.workflow.repository; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import vip.mate.workflow.model.WorkflowPayloadEntity; + +@Mapper +public interface WorkflowPayloadMapper extends BaseMapper { +} diff --git a/mateclaw-server/src/main/java/vip/mate/workflow/repository/WorkflowRevisionMapper.java b/mateclaw-server/src/main/java/vip/mate/workflow/repository/WorkflowRevisionMapper.java new file mode 100644 index 00000000..857009f0 --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/workflow/repository/WorkflowRevisionMapper.java @@ -0,0 +1,9 @@ +package vip.mate.workflow.repository; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import vip.mate.workflow.model.WorkflowRevisionEntity; + +@Mapper +public interface WorkflowRevisionMapper extends BaseMapper { +} diff --git a/mateclaw-server/src/main/java/vip/mate/workflow/repository/WorkflowRunMapper.java b/mateclaw-server/src/main/java/vip/mate/workflow/repository/WorkflowRunMapper.java new file mode 100644 index 00000000..2fd57eb1 --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/workflow/repository/WorkflowRunMapper.java @@ -0,0 +1,9 @@ +package vip.mate.workflow.repository; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import vip.mate.workflow.model.WorkflowRunEntity; + +@Mapper +public interface WorkflowRunMapper extends BaseMapper { +} diff --git a/mateclaw-server/src/main/java/vip/mate/workflow/repository/WorkflowRunPauseMapper.java b/mateclaw-server/src/main/java/vip/mate/workflow/repository/WorkflowRunPauseMapper.java new file mode 100644 index 00000000..5a9c2094 --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/workflow/repository/WorkflowRunPauseMapper.java @@ -0,0 +1,9 @@ +package vip.mate.workflow.repository; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import vip.mate.workflow.model.WorkflowRunPauseEntity; + +@Mapper +public interface WorkflowRunPauseMapper extends BaseMapper { +} diff --git a/mateclaw-server/src/main/java/vip/mate/workflow/repository/WorkflowRunStepMapper.java b/mateclaw-server/src/main/java/vip/mate/workflow/repository/WorkflowRunStepMapper.java new file mode 100644 index 00000000..f7f78e03 --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/workflow/repository/WorkflowRunStepMapper.java @@ -0,0 +1,9 @@ +package vip.mate.workflow.repository; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import vip.mate.workflow.model.WorkflowRunStepEntity; + +@Mapper +public interface WorkflowRunStepMapper extends BaseMapper { +} diff --git a/mateclaw-server/src/main/java/vip/mate/workspace/conversation/ConversationService.java b/mateclaw-server/src/main/java/vip/mate/workspace/conversation/ConversationService.java index fea4f499..6de391be 100644 --- a/mateclaw-server/src/main/java/vip/mate/workspace/conversation/ConversationService.java +++ b/mateclaw-server/src/main/java/vip/mate/workspace/conversation/ConversationService.java @@ -556,6 +556,7 @@ public class ConversationService { case "text" -> appendSegment(text, part.getText()); case "thinking", "tool_call", "parse_error" -> { /* skip — frontend reads these from contentParts directly */ } case "file" -> appendSegment(text, renderFilePart(part)); + case "image", "video", "audio", "model3d" -> appendSegment(text, renderMediaPart(part)); default -> appendSegment(text, part.getText()); } } @@ -614,6 +615,36 @@ public class ConversationService { return "[附件] " + name + "(路径: " + path + ")"; } + /** + * Render an image/video/audio/3D-model content part for the LLM prompt. + *

+ * Without this marker, media parts are invisible in the rendered text — the LLM + * sees only the user's accompanying text and has no idea an attachment was sent. + * That fails closed when the multimodal Media injection in {@code BaseAgent} is + * upstream-stripped (model heuristic claims vision but the actual provider drops + * the image), leaving the agent to ask "which image?" for an attachment the user + * already uploaded. The path lets file-reading tools ({@code read_file}, + * {@code extract_document_text}, {@code detect_file_type}) work as a fallback. + */ + private String renderMediaPart(MessageContentPart part) { + String label = switch (part.getType()) { + case "image" -> "[图片]"; + case "video" -> "[视频]"; + case "audio" -> "[音频]"; + case "model3d" -> "[3D 模型]"; + default -> "[附件]"; + }; + String name = safe(part.getFileName()); + if (name.isBlank()) { + name = "未命名"; + } + String path = safe(part.getPath()); + if (path.isBlank()) { + return label + " " + name; + } + return label + " " + name + "(路径: " + path + ")"; + } + private void appendSegment(StringBuilder builder, String text) { String safeText = safe(text); if (safeText.isBlank()) { diff --git a/mateclaw-server/src/main/resources/db/migration/h2/V96__workflow_foundations.sql b/mateclaw-server/src/main/resources/db/migration/h2/V96__workflow_foundations.sql new file mode 100644 index 00000000..2990d072 --- /dev/null +++ b/mateclaw-server/src/main/resources/db/migration/h2/V96__workflow_foundations.sql @@ -0,0 +1,173 @@ +-- V96: Foundational schema for the workflow runtime. +-- Eight tables establish workflow identity (workflow + immutable revisions), +-- run state (run + per-step rows + durable pause rows for await_approval), +-- payload URI storage with inline / filesystem fallback, and trigger +-- definitions paired with a dedup-window table for envelope-based event +-- governance. H2 dialect uses CLOB for MEDIUMTEXT and BLOB for LONGBLOB; +-- secondary indexes are emitted as separate CREATE INDEX statements per +-- project convention. + +-- 1. Stable workflow identity + draft (1:1 with workflow row). +CREATE TABLE IF NOT EXISTS mate_workflow ( + id BIGINT NOT NULL PRIMARY KEY, + workspace_id BIGINT NOT NULL, + name VARCHAR(128) NOT NULL, + description VARCHAR(1024), + enabled BOOLEAN NOT NULL DEFAULT TRUE, + draft_json CLOB, + draft_schema_version VARCHAR(8), + draft_updated_by BIGINT, + draft_updated_at TIMESTAMP, + latest_revision_id BIGINT, + created_by BIGINT, + create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + deleted INT NOT NULL DEFAULT 0 +); +CREATE UNIQUE INDEX IF NOT EXISTS uk_workflow_workspace_name + ON mate_workflow (workspace_id, name, deleted); + +-- 2. Immutable published revisions; integer revision is monotonic per workflow. +CREATE TABLE IF NOT EXISTS mate_workflow_revision ( + id BIGINT NOT NULL PRIMARY KEY, + workflow_id BIGINT NOT NULL, + revision INT NOT NULL, + graph_json CLOB NOT NULL, + schema_version VARCHAR(8) NOT NULL, + published_note VARCHAR(512), + published_by BIGINT, + create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE UNIQUE INDEX IF NOT EXISTS uk_workflow_revision + ON mate_workflow_revision (workflow_id, revision); + +-- 3. Workflow run instance; payload bodies live behind URIs in mate_workflow_payload. +CREATE TABLE IF NOT EXISTS mate_workflow_run ( + id BIGINT NOT NULL PRIMARY KEY, + workflow_id BIGINT NOT NULL, + revision_id BIGINT NOT NULL, + workspace_id BIGINT NOT NULL, + state VARCHAR(16) NOT NULL, + triggered_by VARCHAR(32), + triggered_meta CLOB, + initial_input_ref VARCHAR(256), + final_output_ref VARCHAR(256), + error_message VARCHAR(2048), + started_at TIMESTAMP, + completed_at TIMESTAMP, + create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + deleted INT NOT NULL DEFAULT 0 +); +CREATE INDEX IF NOT EXISTS idx_workflow_run_started + ON mate_workflow_run (workflow_id, started_at); + +-- 4. Per-step run row; iteration_index reserved for fan_out (and future loop). +CREATE TABLE IF NOT EXISTS mate_workflow_run_step ( + id BIGINT NOT NULL PRIMARY KEY, + run_id BIGINT NOT NULL, + step_index INT NOT NULL, + iteration_index INT, + step_name VARCHAR(128), + agent_id BIGINT, + state VARCHAR(16), + input_ref VARCHAR(256), + output_ref VARCHAR(256), + output_summary VARCHAR(512), + output_content_type VARCHAR(64), + error_message VARCHAR(2048), + duration_ms BIGINT, + token_input INT, + token_output INT, + started_at TIMESTAMP, + completed_at TIMESTAMP +); +CREATE INDEX IF NOT EXISTS idx_workflow_run_step + ON mate_workflow_run_step (run_id, step_index, iteration_index); + +-- 5. Durable pause rows so await_approval can resume across restarts. +-- pause_token is the resume entry key; external_approval_id ties back to +-- ApprovalWorkflowService rows so the approval callback can find the pause. +CREATE TABLE IF NOT EXISTS mate_workflow_run_pause ( + id BIGINT NOT NULL PRIMARY KEY, + run_id BIGINT NOT NULL, + step_id BIGINT NOT NULL, + pause_kind VARCHAR(32) NOT NULL, + pause_token VARCHAR(128) NOT NULL, + external_approval_id BIGINT, + paused_at TIMESTAMP NOT NULL, + resume_deadline TIMESTAMP, + resume_payload_ref VARCHAR(256), + resumed_at TIMESTAMP, + resume_outcome VARCHAR(32) +); +CREATE UNIQUE INDEX IF NOT EXISTS uk_workflow_pause_run_step + ON mate_workflow_run_pause (run_id, step_id); +CREATE UNIQUE INDEX IF NOT EXISTS uk_workflow_pause_token + ON mate_workflow_run_pause (pause_token); +CREATE INDEX IF NOT EXISTS idx_workflow_pause_external_approval + ON mate_workflow_run_pause (external_approval_id); +CREATE INDEX IF NOT EXISTS idx_workflow_pause_open_deadline + ON mate_workflow_run_pause (resumed_at, resume_deadline); + +-- 6. Payload URI storage. Inline blob for < 256KB; storage_kind=fs/s3/oss +-- carries the external object key in storage_ref. sha256 is for tamper +-- detection only — v0 does not deduplicate across runs. +CREATE TABLE IF NOT EXISTS mate_workflow_payload ( + id BIGINT NOT NULL PRIMARY KEY, + payload_uri VARCHAR(256) NOT NULL, + workspace_id BIGINT NOT NULL, + content_bytes BLOB, + storage_kind VARCHAR(16) NOT NULL, + storage_ref VARCHAR(512), + content_type VARCHAR(64), + sha256 CHAR(64), + size_bytes BIGINT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE UNIQUE INDEX IF NOT EXISTS uk_workflow_payload_uri + ON mate_workflow_payload (payload_uri); +CREATE INDEX IF NOT EXISTS idx_workflow_payload_workspace_created + ON mate_workflow_payload (workspace_id, created_at); + +-- 7. Trigger definitions. pattern_version is a lamport counter that fire +-- callbacks compare against on every fire to detect that another instance +-- has updated the cron expression and self-cancel the local schedule. +CREATE TABLE IF NOT EXISTS mate_trigger ( + id BIGINT NOT NULL PRIMARY KEY, + workspace_id BIGINT NOT NULL, + name VARCHAR(128), + pattern_type VARCHAR(32) NOT NULL, + pattern_json CLOB NOT NULL, + target_type VARCHAR(16) NOT NULL, + target_id BIGINT NOT NULL, + payload_template CLOB, + rate_limit_per_min INT NOT NULL DEFAULT 60, + dedup_window_secs INT NOT NULL DEFAULT 60, + bot_self_filter BOOLEAN NOT NULL DEFAULT TRUE, + enabled BOOLEAN NOT NULL DEFAULT TRUE, + fire_count BIGINT NOT NULL DEFAULT 0, + max_fires BIGINT NOT NULL DEFAULT 0, + last_fired_at TIMESTAMP, + pattern_version BIGINT NOT NULL DEFAULT 1, + create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + deleted INT NOT NULL DEFAULT 0 +); +CREATE INDEX IF NOT EXISTS idx_trigger_workspace_enabled + ON mate_trigger (workspace_id, enabled, deleted); +CREATE INDEX IF NOT EXISTS idx_trigger_target + ON mate_trigger (target_type, target_id); + +-- 8. Event dedup window. dedup_key is envelope.eventId, falling back to +-- sourceHash when the upstream channel did not provide a stable id. +CREATE TABLE IF NOT EXISTS mate_trigger_event ( + id BIGINT NOT NULL PRIMARY KEY, + trigger_id BIGINT NOT NULL, + dedup_key VARCHAR(128) NOT NULL, + received_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + expires_at TIMESTAMP NOT NULL +); +CREATE UNIQUE INDEX IF NOT EXISTS uk_trigger_dedup + ON mate_trigger_event (trigger_id, dedup_key); +CREATE INDEX IF NOT EXISTS idx_trigger_event_expires + ON mate_trigger_event (expires_at); diff --git a/mateclaw-server/src/main/resources/db/migration/mysql/V96__workflow_foundations.sql b/mateclaw-server/src/main/resources/db/migration/mysql/V96__workflow_foundations.sql new file mode 100644 index 00000000..edc23508 --- /dev/null +++ b/mateclaw-server/src/main/resources/db/migration/mysql/V96__workflow_foundations.sql @@ -0,0 +1,162 @@ +-- V96: Foundational schema for the workflow runtime. +-- Eight tables establish workflow identity (workflow + immutable revisions), +-- run state (run + per-step rows + durable pause rows for await_approval), +-- payload URI storage with inline / filesystem fallback, and trigger +-- definitions paired with a dedup-window table for envelope-based event +-- governance. CREATE TABLE IF NOT EXISTS is itself idempotent on MySQL. + +-- 1. Stable workflow identity + draft (1:1 with workflow row). +CREATE TABLE IF NOT EXISTS mate_workflow ( + id BIGINT NOT NULL PRIMARY KEY, + workspace_id BIGINT NOT NULL, + name VARCHAR(128) NOT NULL, + description VARCHAR(1024), + enabled TINYINT NOT NULL DEFAULT 1, + draft_json MEDIUMTEXT, + draft_schema_version VARCHAR(8), + draft_updated_by BIGINT, + draft_updated_at DATETIME(3), + latest_revision_id BIGINT, + created_by BIGINT, + create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + deleted INT NOT NULL DEFAULT 0, + UNIQUE KEY uk_workflow_workspace_name (workspace_id, name, deleted) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci + COMMENT = 'Workflow definition with stable identity and inline draft.'; + +-- 2. Immutable published revisions; integer revision is monotonic per workflow. +CREATE TABLE IF NOT EXISTS mate_workflow_revision ( + id BIGINT NOT NULL PRIMARY KEY, + workflow_id BIGINT NOT NULL, + revision INT NOT NULL, + graph_json MEDIUMTEXT NOT NULL, + schema_version VARCHAR(8) NOT NULL, + published_note VARCHAR(512), + published_by BIGINT, + create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + UNIQUE KEY uk_workflow_revision (workflow_id, revision) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci + COMMENT = 'Immutable published workflow revisions.'; + +-- 3. Workflow run instance; payload bodies live behind URIs in mate_workflow_payload. +CREATE TABLE IF NOT EXISTS mate_workflow_run ( + id BIGINT NOT NULL PRIMARY KEY, + workflow_id BIGINT NOT NULL, + revision_id BIGINT NOT NULL, + workspace_id BIGINT NOT NULL, + state VARCHAR(16) NOT NULL, + triggered_by VARCHAR(32), + triggered_meta MEDIUMTEXT, + initial_input_ref VARCHAR(256), + final_output_ref VARCHAR(256), + error_message VARCHAR(2048), + started_at DATETIME(3), + completed_at DATETIME(3), + create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + deleted INT NOT NULL DEFAULT 0, + KEY idx_workflow_run_started (workflow_id, started_at) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci + COMMENT = 'Workflow run instances locked to a specific revision.'; + +-- 4. Per-step run row; iteration_index reserved for fan_out (and future loop). +CREATE TABLE IF NOT EXISTS mate_workflow_run_step ( + id BIGINT NOT NULL PRIMARY KEY, + run_id BIGINT NOT NULL, + step_index INT NOT NULL, + iteration_index INT, + step_name VARCHAR(128), + agent_id BIGINT, + state VARCHAR(16), + input_ref VARCHAR(256), + output_ref VARCHAR(256), + output_summary VARCHAR(512), + output_content_type VARCHAR(64), + error_message VARCHAR(2048), + duration_ms BIGINT, + token_input INT, + token_output INT, + started_at DATETIME(3), + completed_at DATETIME(3), + KEY idx_workflow_run_step (run_id, step_index, iteration_index) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci + COMMENT = 'Per-step run rows with input/output references and timings.'; + +-- 5. Durable pause rows so await_approval can resume across restarts. +CREATE TABLE IF NOT EXISTS mate_workflow_run_pause ( + id BIGINT NOT NULL PRIMARY KEY, + run_id BIGINT NOT NULL, + step_id BIGINT NOT NULL, + pause_kind VARCHAR(32) NOT NULL, + pause_token VARCHAR(128) NOT NULL, + external_approval_id BIGINT, + paused_at DATETIME(3) NOT NULL, + resume_deadline DATETIME(3), + resume_payload_ref VARCHAR(256), + resumed_at DATETIME(3), + resume_outcome VARCHAR(32), + UNIQUE KEY uk_workflow_pause_run_step (run_id, step_id), + UNIQUE KEY uk_workflow_pause_token (pause_token), + KEY idx_workflow_pause_external_approval (external_approval_id), + KEY idx_workflow_pause_open_deadline (resumed_at, resume_deadline) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci + COMMENT = 'Durable workflow pause rows for await_approval resume.'; + +-- 6. Payload URI storage. Inline blob for < 256KB; storage_kind=fs/s3/oss +-- carries the external object key in storage_ref. +CREATE TABLE IF NOT EXISTS mate_workflow_payload ( + id BIGINT NOT NULL PRIMARY KEY, + payload_uri VARCHAR(256) NOT NULL, + workspace_id BIGINT NOT NULL, + content_bytes LONGBLOB, + storage_kind VARCHAR(16) NOT NULL, + storage_ref VARCHAR(512), + content_type VARCHAR(64), + sha256 CHAR(64), + size_bytes BIGINT, + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + UNIQUE KEY uk_workflow_payload_uri (payload_uri), + KEY idx_workflow_payload_workspace_created (workspace_id, created_at) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci + COMMENT = 'Payload bodies addressed by stable URIs.'; + +-- 7. Trigger definitions. pattern_version is a lamport counter that fire +-- callbacks compare against on every fire to detect that another instance +-- has updated the cron expression and self-cancel the local schedule. +CREATE TABLE IF NOT EXISTS mate_trigger ( + id BIGINT NOT NULL PRIMARY KEY, + workspace_id BIGINT NOT NULL, + name VARCHAR(128), + pattern_type VARCHAR(32) NOT NULL, + pattern_json MEDIUMTEXT NOT NULL, + target_type VARCHAR(16) NOT NULL, + target_id BIGINT NOT NULL, + payload_template MEDIUMTEXT, + rate_limit_per_min INT NOT NULL DEFAULT 60, + dedup_window_secs INT NOT NULL DEFAULT 60, + bot_self_filter TINYINT NOT NULL DEFAULT 1, + enabled TINYINT NOT NULL DEFAULT 1, + fire_count BIGINT NOT NULL DEFAULT 0, + max_fires BIGINT NOT NULL DEFAULT 0, + last_fired_at DATETIME(3), + pattern_version BIGINT NOT NULL DEFAULT 1, + create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + deleted INT NOT NULL DEFAULT 0, + KEY idx_trigger_workspace_enabled (workspace_id, enabled, deleted), + KEY idx_trigger_target (target_type, target_id) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci + COMMENT = 'Workflow / agent trigger definitions with pattern versioning.'; + +-- 8. Event dedup window. dedup_key is envelope.eventId, falling back to +-- sourceHash when the upstream channel did not provide a stable id. +CREATE TABLE IF NOT EXISTS mate_trigger_event ( + id BIGINT NOT NULL PRIMARY KEY, + trigger_id BIGINT NOT NULL, + dedup_key VARCHAR(128) NOT NULL, + received_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + expires_at DATETIME(3) NOT NULL, + UNIQUE KEY uk_trigger_dedup (trigger_id, dedup_key), + KEY idx_trigger_event_expires (expires_at) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci + COMMENT = 'Per-trigger event dedup window with TTL-style expiry.';