mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 20:08:18 +08:00
fix(workflow): hard-delete + blank-draft compile + canvas blank pane
This commit is contained in:
parent
f8499eefa7
commit
844a64ad37
@ -5,7 +5,6 @@ import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
|||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@ -66,6 +65,6 @@ public class TriggerEntity {
|
|||||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||||
private LocalDateTime updateTime;
|
private LocalDateTime updateTime;
|
||||||
|
|
||||||
@TableLogic
|
// Hard-delete only (project convention); column kept for schema compat.
|
||||||
private Integer deleted;
|
private Integer deleted;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -94,12 +94,26 @@ public class WorkflowController {
|
|||||||
if (row == null) {
|
if (row == null) {
|
||||||
return ResponseEntity.badRequest().body(R.fail("workflow not found: " + id));
|
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()
|
return ResponseEntity.badRequest()
|
||||||
.body(R.fail("workflow has no draft to compile: " + id));
|
.body(R.fail("workflow has no draft to compile: " + id));
|
||||||
}
|
}
|
||||||
WorkflowCompiler.Result result = compiler.compile(row.getDraftJson(),
|
WorkflowCompiler.Result result;
|
||||||
new PublishContext(0L, row.getWorkspaceId()), aclPort);
|
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()) {
|
if (!result.ok()) {
|
||||||
return ResponseEntity.unprocessableEntity()
|
return ResponseEntity.unprocessableEntity()
|
||||||
.body(buildCompileFailure(result.errors()));
|
.body(buildCompileFailure(result.errors()));
|
||||||
@ -119,6 +133,12 @@ public class WorkflowController {
|
|||||||
return ResponseEntity.ok(R.ok(outcome));
|
return ResponseEntity.ok(R.ok(outcome));
|
||||||
} catch (WorkflowCompileFailedException e) {
|
} catch (WorkflowCompileFailedException e) {
|
||||||
return ResponseEntity.unprocessableEntity().body(buildCompileFailure(e.errors()));
|
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) {
|
} catch (IllegalArgumentException | IllegalStateException e) {
|
||||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(R.fail(e.getMessage()));
|
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(R.fail(e.getMessage()));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
|||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@ -56,6 +55,10 @@ public class WorkflowEntity {
|
|||||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||||
private LocalDateTime updateTime;
|
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;
|
private Integer deleted;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
|||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@ -56,6 +55,6 @@ public class WorkflowRunEntity {
|
|||||||
@TableField(fill = FieldFill.INSERT)
|
@TableField(fill = FieldFill.INSERT)
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
@TableLogic
|
// Hard-delete only (project convention); column kept for schema compat.
|
||||||
private Integer deleted;
|
private Integer deleted;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
@ -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;
|
||||||
@ -27,12 +27,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!graph.nodes.length && !graph.parseError" class="canvas-empty">
|
<div v-if="graph.parseError" class="canvas-error">
|
||||||
{{ t('workflows.canvas.empty') }}
|
<div class="canvas-error-icon">⚠</div>
|
||||||
|
<div class="canvas-error-msg">{{ t('workflows.canvas.parseError', { msg: graph.parseError }) }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="!graph.nodes.length" class="canvas-empty">
|
||||||
|
<div class="canvas-empty-illustration">
|
||||||
|
<svg width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="3" y="3" width="7" height="7" rx="1.5"/>
|
||||||
|
<rect x="14" y="3" width="7" height="7" rx="1.5"/>
|
||||||
|
<rect x="3" y="14" width="7" height="7" rx="1.5"/>
|
||||||
|
<rect x="14" y="14" width="7" height="7" rx="1.5"/>
|
||||||
|
<path d="M10 6.5h4"/>
|
||||||
|
<path d="M10 17.5h4"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="canvas-empty-text">{{ t('workflows.canvas.empty') }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<VueFlow
|
<VueFlow
|
||||||
v-else-if="!graph.parseError"
|
v-else
|
||||||
:id="canvasId"
|
:id="canvasId"
|
||||||
class="canvas-flow"
|
class="canvas-flow"
|
||||||
:nodes="graph.nodes"
|
:nodes="graph.nodes"
|
||||||
@ -235,11 +250,46 @@ onBeforeUnmount(() => {
|
|||||||
.canvas-empty {
|
.canvas-empty {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
opacity: 0.7;
|
padding: 32px 24px;
|
||||||
padding: 24px;
|
color: var(--mc-text-secondary, #666);
|
||||||
|
background: var(--mc-bg-sunken, transparent);
|
||||||
|
min-height: 280px;
|
||||||
|
}
|
||||||
|
.canvas-empty-illustration {
|
||||||
|
color: var(--mc-text-tertiary, #999);
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
.canvas-empty-text {
|
||||||
|
text-align: center;
|
||||||
|
max-width: 320px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.canvas-error {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 32px 24px;
|
||||||
|
background: var(--mc-danger-bg, rgba(255, 80, 80, 0.08));
|
||||||
|
color: var(--mc-danger, #c0392b);
|
||||||
|
min-height: 280px;
|
||||||
|
}
|
||||||
|
.canvas-error-icon {
|
||||||
|
font-size: 36px;
|
||||||
|
}
|
||||||
|
.canvas-error-msg {
|
||||||
|
font-family: 'JetBrains Mono', Consolas, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 480px;
|
||||||
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
.canvas-flow {
|
.canvas-flow {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
@ -384,7 +384,30 @@ async function onCreateSubmit(payload: { name: string; description: string }) {
|
|||||||
const created = res.data as unknown as WorkflowSummary
|
const created = res.data as unknown as WorkflowSummary
|
||||||
createDialogOpen.value = false
|
createDialogOpen.value = false
|
||||||
await reload()
|
await reload()
|
||||||
if (created?.id) await select(created.id)
|
if (created?.id) {
|
||||||
|
await select(created.id)
|
||||||
|
// Seed a starter step so the canvas renders something the user
|
||||||
|
// can immediately edit, instead of opening on a blank slate.
|
||||||
|
// The compile button also has something real to validate.
|
||||||
|
if (!draftJson.value || !draftJson.value.trim()) {
|
||||||
|
draftJson.value = JSON.stringify({
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
name: 'first-step',
|
||||||
|
agentName: 'agent-name',
|
||||||
|
mode: { type: 'sequential' },
|
||||||
|
promptTemplate: 'Hello {{ inputs.payload }}',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}, null, 2)
|
||||||
|
try {
|
||||||
|
await workflowApi.saveDraft(created.id, draftJson.value)
|
||||||
|
} catch (e) {
|
||||||
|
// Non-fatal: the user can still hit Save Draft manually.
|
||||||
|
console.warn('seed draft save failed', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setStatus(t('workflows.status.createFailed', { msg: (e as Error).message }), 'err')
|
setStatus(t('workflows.status.createFailed', { msg: (e as Error).message }), 'err')
|
||||||
} finally {
|
} finally {
|
||||||
@ -838,10 +861,21 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
.canvas-pane {
|
.canvas-pane {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: 1fr 240px;
|
flex-direction: row;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
min-height: 360px;
|
min-height: 420px;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
.canvas-pane > .workflow-canvas {
|
||||||
|
/* fill available width when no inspector is visible — earlier the
|
||||||
|
pane reserved 240px for an inspector via grid-template-columns
|
||||||
|
even when it was v-if hidden, leaving the canvas confined. */
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.canvas-pane > .canvas-inspector {
|
||||||
|
flex: 0 0 240px;
|
||||||
}
|
}
|
||||||
.canvas-inspector {
|
.canvas-inspector {
|
||||||
background: var(--mc-bg-elevated, rgba(0, 0, 0, 0.02));
|
background: var(--mc-bg-elevated, rgba(0, 0, 0, 0.02));
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user