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 index aa295eaf..f6e7f632 100644 --- a/mateclaw-server/src/main/java/vip/mate/trigger/model/TriggerEntity.java +++ b/mateclaw-server/src/main/java/vip/mate/trigger/model/TriggerEntity.java @@ -5,7 +5,6 @@ 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; @@ -66,6 +65,6 @@ public class TriggerEntity { @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; - @TableLogic + // Hard-delete only (project convention); column kept for schema compat. private Integer deleted; } diff --git a/mateclaw-server/src/main/java/vip/mate/workflow/api/WorkflowController.java b/mateclaw-server/src/main/java/vip/mate/workflow/api/WorkflowController.java index 8762d710..e376f172 100644 --- a/mateclaw-server/src/main/java/vip/mate/workflow/api/WorkflowController.java +++ b/mateclaw-server/src/main/java/vip/mate/workflow/api/WorkflowController.java @@ -94,12 +94,26 @@ public class WorkflowController { if (row == null) { return ResponseEntity.badRequest().body(R.fail("workflow not found: " + id)); } - if (row.getDraftJson() == null) { + // The parser throws WorkflowParseException for null/blank/whitespace + // input, which would otherwise bubble up to the global handler as a + // 500. A blank draft is a normal user state ("just created, nothing + // typed yet"), so we surface a friendly 400 here. + if (row.getDraftJson() == null || row.getDraftJson().trim().isEmpty()) { return ResponseEntity.badRequest() .body(R.fail("workflow has no draft to compile: " + id)); } - WorkflowCompiler.Result result = compiler.compile(row.getDraftJson(), - new PublishContext(0L, row.getWorkspaceId()), aclPort); + WorkflowCompiler.Result result; + try { + result = compiler.compile(row.getDraftJson(), + new PublishContext(0L, row.getWorkspaceId()), aclPort); + } catch (vip.mate.workflow.compiler.WorkflowParseException e) { + // Malformed JSON / structurally invalid graph → render as a + // single-error compile failure so the UI's existing errors + // panel handles it without a stack trace dialog. + return ResponseEntity.unprocessableEntity().body(buildCompileFailure(List.of( + new vip.mate.workflow.compiler.CompileError( + "graph.parse_failed", "/", e.getMessage())))); + } if (!result.ok()) { return ResponseEntity.unprocessableEntity() .body(buildCompileFailure(result.errors())); @@ -119,6 +133,12 @@ public class WorkflowController { return ResponseEntity.ok(R.ok(outcome)); } catch (WorkflowCompileFailedException e) { return ResponseEntity.unprocessableEntity().body(buildCompileFailure(e.errors())); + } catch (vip.mate.workflow.compiler.WorkflowParseException e) { + // Same surface as a compile error so the UI errors panel + // handles a malformed / blank draft without a 500 dialog. + return ResponseEntity.unprocessableEntity().body(buildCompileFailure(List.of( + new vip.mate.workflow.compiler.CompileError( + "graph.parse_failed", "/", e.getMessage())))); } catch (IllegalArgumentException | IllegalStateException e) { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(R.fail(e.getMessage())); } 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 index 7a1ced5e..07300ec8 100644 --- a/mateclaw-server/src/main/java/vip/mate/workflow/model/WorkflowEntity.java +++ b/mateclaw-server/src/main/java/vip/mate/workflow/model/WorkflowEntity.java @@ -5,7 +5,6 @@ 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; @@ -56,6 +55,10 @@ public class WorkflowEntity { @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; - @TableLogic + // The `deleted` column stays on the table for schema compatibility but + // is no longer logical-deleted — see contributing.md, the project moved + // to hard-delete project-wide. deleteById() now performs a real DELETE, + // and the unique key on (workspace_id, name, deleted) no longer collides + // when a name is recreated and re-deleted. private Integer deleted; } 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 index 4f31b9f1..3b0964f7 100644 --- a/mateclaw-server/src/main/java/vip/mate/workflow/model/WorkflowRunEntity.java +++ b/mateclaw-server/src/main/java/vip/mate/workflow/model/WorkflowRunEntity.java @@ -5,7 +5,6 @@ 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; @@ -56,6 +55,6 @@ public class WorkflowRunEntity { @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; - @TableLogic + // Hard-delete only (project convention); column kept for schema compat. private Integer deleted; } diff --git a/mateclaw-server/src/main/resources/db/migration/h2/V97__workflow_purge_tombstones.sql b/mateclaw-server/src/main/resources/db/migration/h2/V97__workflow_purge_tombstones.sql new file mode 100644 index 00000000..1202c754 --- /dev/null +++ b/mateclaw-server/src/main/resources/db/migration/h2/V97__workflow_purge_tombstones.sql @@ -0,0 +1,16 @@ +-- The workflow / trigger entities originally shipped with @TableLogic, which +-- caused deleteById() to soft-update `deleted=1`. The project convention is +-- hard-delete everywhere (see contributing.md), and the soft-delete path +-- collided with the (workspace_id, name, deleted) unique key whenever a name +-- was recreated and re-deleted: the second update tried to write a tombstone +-- that already existed. +-- +-- The entity annotations are removed in this same change set so deleteById() +-- now performs a real DELETE. This migration purges any tombstones that the +-- old soft-delete path may have written, because the annotation-driven query +-- filter is no longer applied — a stale `deleted=1` row would otherwise show +-- up in list endpoints. + +DELETE FROM mate_workflow WHERE deleted <> 0; +DELETE FROM mate_workflow_run WHERE deleted <> 0; +DELETE FROM mate_trigger WHERE deleted <> 0; diff --git a/mateclaw-server/src/main/resources/db/migration/mysql/V97__workflow_purge_tombstones.sql b/mateclaw-server/src/main/resources/db/migration/mysql/V97__workflow_purge_tombstones.sql new file mode 100644 index 00000000..8b3a9125 --- /dev/null +++ b/mateclaw-server/src/main/resources/db/migration/mysql/V97__workflow_purge_tombstones.sql @@ -0,0 +1,9 @@ +-- See the matching H2 file for context. The workflow / trigger entities +-- moved off @TableLogic to align with the project's hard-delete convention; +-- this migration drops any tombstones the old soft-delete path persisted so +-- list endpoints don't expose them after the annotation-driven filter is +-- removed. + +DELETE FROM mate_workflow WHERE deleted <> 0; +DELETE FROM mate_workflow_run WHERE deleted <> 0; +DELETE FROM mate_trigger WHERE deleted <> 0; diff --git a/mateclaw-ui/src/components/workflow/WorkflowCanvas.vue b/mateclaw-ui/src/components/workflow/WorkflowCanvas.vue index c3c3a019..56a6aba8 100644 --- a/mateclaw-ui/src/components/workflow/WorkflowCanvas.vue +++ b/mateclaw-ui/src/components/workflow/WorkflowCanvas.vue @@ -27,12 +27,27 @@ -