mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 11:58:34 +08:00
feat(ui): i18n + step templates for workflow editor; trigger UX polish
This commit is contained in:
parent
20884c1773
commit
2a25f4fcfb
@ -301,6 +301,8 @@ export default {
|
|||||||
security: 'Security',
|
security: 'Security',
|
||||||
tokenUsage: 'Token Usage',
|
tokenUsage: 'Token Usage',
|
||||||
cronJobs: 'Cron Jobs',
|
cronJobs: 'Cron Jobs',
|
||||||
|
workflows: 'Workflows',
|
||||||
|
triggers: 'Triggers',
|
||||||
settings: 'Settings',
|
settings: 'Settings',
|
||||||
logout: 'Logout',
|
logout: 'Logout',
|
||||||
memory: 'Memory',
|
memory: 'Memory',
|
||||||
@ -1904,6 +1906,129 @@ export default {
|
|||||||
goalRequired: 'Goal is required',
|
goalRequired: 'Goal is required',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
workflows: {
|
||||||
|
kicker: 'Workflows',
|
||||||
|
title: 'Workflow Editor',
|
||||||
|
desc: 'Author multi-step workflows. Save the draft as you go, compile to surface diagnostics, then publish a revision when it is ready to run.',
|
||||||
|
newWorkflow: '+ New Workflow',
|
||||||
|
defined: 'Defined ({count})',
|
||||||
|
refresh: 'Refresh',
|
||||||
|
unnamed: '(unnamed)',
|
||||||
|
draftBadge: 'draft',
|
||||||
|
publishedBadge: 'v{rev}',
|
||||||
|
listEmpty: 'No workflows yet — create one to get started.',
|
||||||
|
namePlaceholder: 'Workflow name',
|
||||||
|
descPlaceholder: 'Optional description',
|
||||||
|
actions: {
|
||||||
|
saveMeta: 'Save Meta',
|
||||||
|
saveDraft: 'Save Draft',
|
||||||
|
compile: 'Compile',
|
||||||
|
publish: 'Publish',
|
||||||
|
delete: 'Delete',
|
||||||
|
},
|
||||||
|
bodyPlaceholder: '{"steps":[{"name":"step-a","agentName":"...","mode":{"type":"sequential"},"promptTemplate":"..."}]}',
|
||||||
|
compileErrorsTitle: '{count} compile error(s)',
|
||||||
|
templates: {
|
||||||
|
label: 'Insert template',
|
||||||
|
placeholder: 'Pick a step template',
|
||||||
|
sequential: 'sequential (linear step)',
|
||||||
|
fan_out: 'fan_out (parallel)',
|
||||||
|
collect: 'collect (join fan_out)',
|
||||||
|
conditional: 'conditional (gated)',
|
||||||
|
await_approval: 'await_approval (pause)',
|
||||||
|
dispatch_channel: 'dispatch_channel (broadcast)',
|
||||||
|
write_memory: 'write_memory (persist)',
|
||||||
|
},
|
||||||
|
jsonInvalid: 'JSON parse failed: {msg}',
|
||||||
|
jsonOk: 'JSON syntax OK.',
|
||||||
|
selectHint: 'Select a workflow on the left, or click "New Workflow" to start a fresh draft.',
|
||||||
|
runs: {
|
||||||
|
header: 'Recent runs ({count})',
|
||||||
|
empty: 'No runs yet.',
|
||||||
|
detailTitle: 'Run #{id} — {state}',
|
||||||
|
runHash: 'run #{id}',
|
||||||
|
},
|
||||||
|
prompts: {
|
||||||
|
newName: 'New workflow name:',
|
||||||
|
defaultName: 'untitled-workflow',
|
||||||
|
publishNote: 'Optional publish note:',
|
||||||
|
deleteConfirm: 'Delete workflow "{name}"? This is reversible via the audit log.',
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
metaSaved: 'Metadata saved.',
|
||||||
|
draftSaved: 'Draft saved.',
|
||||||
|
compileOk: 'Compile OK.',
|
||||||
|
published: 'Published.',
|
||||||
|
deleted: 'Deleted.',
|
||||||
|
createFailed: 'Create failed: {msg}',
|
||||||
|
saveFailed: 'Save failed: {msg}',
|
||||||
|
saveDraftFailed: 'Save draft failed: {msg}',
|
||||||
|
deleteFailed: 'Delete failed: {msg}',
|
||||||
|
compileFailed: '{count} compile error(s) — see panel below.',
|
||||||
|
compileFallback: 'Compile / publish failed.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
triggers: {
|
||||||
|
kicker: 'Triggers',
|
||||||
|
title: 'Workflow Triggers',
|
||||||
|
desc: 'Connect cron schedules and channel events to your published workflows. pattern_version increments on every cron expression change so multi-node deployments coordinate correctly.',
|
||||||
|
newTrigger: '+ New Trigger',
|
||||||
|
columns: {
|
||||||
|
name: 'Name',
|
||||||
|
pattern: 'Pattern',
|
||||||
|
target: 'Target Workflow',
|
||||||
|
rate: 'Rate',
|
||||||
|
fires: 'Fires',
|
||||||
|
patternVersion: 'pattern_v',
|
||||||
|
lastFired: 'Last fired',
|
||||||
|
state: 'State',
|
||||||
|
actions: 'Actions',
|
||||||
|
},
|
||||||
|
targetWorkflowSelect: 'Pick a published workflow',
|
||||||
|
targetWorkflowEmpty: 'No published workflows yet.',
|
||||||
|
patternHints: {
|
||||||
|
cron: 'Example: {"cron":"0 0 * * * *","timezone":"UTC"} — every cron change bumps pattern_version.',
|
||||||
|
channel_message: 'Optional channelType (e.g. feishu) and senderEquals narrow the match.',
|
||||||
|
webhook: 'Pass-through match. Ensure the HTTP entry validates the webhook secret.',
|
||||||
|
content_match: 'Requires substring (case-insensitive). Empty strings refuse to fire.',
|
||||||
|
agent_lifecycle: 'Optional agentId and phase (spawned / terminated / crashed).',
|
||||||
|
workflow_completion: 'Optional sourceWorkflowId and stateFilter (completed / failed / any).',
|
||||||
|
},
|
||||||
|
unnamed: '(unnamed)',
|
||||||
|
rateUnit: '{count}/min',
|
||||||
|
enabled: 'enabled',
|
||||||
|
disabled: 'disabled',
|
||||||
|
empty: 'No triggers configured.',
|
||||||
|
actions: {
|
||||||
|
edit: 'Edit',
|
||||||
|
delete: 'Delete',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
save: 'Save',
|
||||||
|
close: 'Close',
|
||||||
|
},
|
||||||
|
formTitleNew: 'New Trigger',
|
||||||
|
formTitleEdit: 'Edit Trigger',
|
||||||
|
fields: {
|
||||||
|
name: 'Name',
|
||||||
|
namePlaceholder: 'hourly-cleanup',
|
||||||
|
patternType: 'Pattern type',
|
||||||
|
patternJson: 'Pattern JSON',
|
||||||
|
patternJsonPlaceholder: '{"cron":"0 0 * * * *","timezone":"UTC"}',
|
||||||
|
targetType: 'Target type',
|
||||||
|
targetId: 'Target id',
|
||||||
|
ratePerMin: 'Rate / min',
|
||||||
|
dedupWindowSecs: 'Dedup window (s)',
|
||||||
|
maxFires: 'Max fires (0 = unlimited)',
|
||||||
|
botSelfFilter: 'Bot-self filter (recommended)',
|
||||||
|
payloadTemplate: 'Payload template (Pebble)',
|
||||||
|
payloadTemplatePlaceholder: '{"who":"{{ event.who }}"}',
|
||||||
|
enabled: 'Enabled',
|
||||||
|
},
|
||||||
|
deleteConfirm: 'Delete trigger "{name}"?',
|
||||||
|
saveFailed: 'Save failed: {msg}',
|
||||||
|
toggleFailed: 'Toggle failed: {msg}',
|
||||||
|
deleteFailed: 'Delete failed: {msg}',
|
||||||
|
},
|
||||||
login: {
|
login: {
|
||||||
kicker: 'AI Operating System',
|
kicker: 'AI Operating System',
|
||||||
subtitle: 'Your intelligent AI assistant',
|
subtitle: 'Your intelligent AI assistant',
|
||||||
|
|||||||
@ -301,6 +301,8 @@ export default {
|
|||||||
security: '安全',
|
security: '安全',
|
||||||
tokenUsage: 'Token 统计',
|
tokenUsage: 'Token 统计',
|
||||||
cronJobs: '定时任务',
|
cronJobs: '定时任务',
|
||||||
|
workflows: '工作流',
|
||||||
|
triggers: '触发器',
|
||||||
settings: '设置',
|
settings: '设置',
|
||||||
logout: '退出登录',
|
logout: '退出登录',
|
||||||
themeLight: '浅色',
|
themeLight: '浅色',
|
||||||
@ -1916,6 +1918,129 @@ export default {
|
|||||||
goalRequired: '执行目标不能为空',
|
goalRequired: '执行目标不能为空',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
workflows: {
|
||||||
|
kicker: '工作流',
|
||||||
|
title: '工作流编辑器',
|
||||||
|
desc: '编排多步骤工作流:边写边存草稿,编译查看诊断,准备好后发布版本。',
|
||||||
|
newWorkflow: '+ 新建工作流',
|
||||||
|
defined: '已定义 ({count})',
|
||||||
|
refresh: '刷新',
|
||||||
|
unnamed: '(未命名)',
|
||||||
|
draftBadge: '草稿',
|
||||||
|
publishedBadge: 'v{rev}',
|
||||||
|
listEmpty: '还没有任何工作流,新建一个开始吧。',
|
||||||
|
namePlaceholder: '工作流名称',
|
||||||
|
descPlaceholder: '可选描述',
|
||||||
|
actions: {
|
||||||
|
saveMeta: '保存元数据',
|
||||||
|
saveDraft: '保存草稿',
|
||||||
|
compile: '编译',
|
||||||
|
publish: '发布',
|
||||||
|
delete: '删除',
|
||||||
|
},
|
||||||
|
bodyPlaceholder: '{"steps":[{"name":"step-a","agentName":"...","mode":{"type":"sequential"},"promptTemplate":"..."}]}',
|
||||||
|
compileErrorsTitle: '{count} 条编译错误',
|
||||||
|
templates: {
|
||||||
|
label: '插入模板',
|
||||||
|
placeholder: '选择步骤模板',
|
||||||
|
sequential: 'sequential(顺序步骤)',
|
||||||
|
fan_out: 'fan_out(并行扇出)',
|
||||||
|
collect: 'collect(汇聚结果)',
|
||||||
|
conditional: 'conditional(条件分支)',
|
||||||
|
await_approval: 'await_approval(等待审批)',
|
||||||
|
dispatch_channel: 'dispatch_channel(消息分发)',
|
||||||
|
write_memory: 'write_memory(写入记忆)',
|
||||||
|
},
|
||||||
|
jsonInvalid: 'JSON 解析失败:{msg}',
|
||||||
|
jsonOk: 'JSON 语法 OK。',
|
||||||
|
selectHint: '在左侧选择一个工作流,或点击「新建工作流」开始。',
|
||||||
|
runs: {
|
||||||
|
header: '近期运行 ({count})',
|
||||||
|
empty: '暂无运行记录。',
|
||||||
|
detailTitle: 'Run #{id} — {state}',
|
||||||
|
runHash: 'run #{id}',
|
||||||
|
},
|
||||||
|
prompts: {
|
||||||
|
newName: '新工作流名称:',
|
||||||
|
defaultName: 'untitled-workflow',
|
||||||
|
publishNote: '可选发布备注:',
|
||||||
|
deleteConfirm: '删除工作流「{name}」?此操作可通过审计日志撤销。',
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
metaSaved: '元数据已保存。',
|
||||||
|
draftSaved: '草稿已保存。',
|
||||||
|
compileOk: '编译通过。',
|
||||||
|
published: '已发布。',
|
||||||
|
deleted: '已删除。',
|
||||||
|
createFailed: '创建失败:{msg}',
|
||||||
|
saveFailed: '保存失败:{msg}',
|
||||||
|
saveDraftFailed: '草稿保存失败:{msg}',
|
||||||
|
deleteFailed: '删除失败:{msg}',
|
||||||
|
compileFailed: '{count} 条编译错误,详见下方面板。',
|
||||||
|
compileFallback: '编译/发布失败。',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
triggers: {
|
||||||
|
kicker: '触发器',
|
||||||
|
title: '工作流触发器',
|
||||||
|
desc: '把 cron 调度和渠道事件接到已发布的工作流上。每次修改 cron 表达式都会让 pattern_version 自增,多节点部署据此协调。',
|
||||||
|
newTrigger: '+ 新建触发器',
|
||||||
|
columns: {
|
||||||
|
name: '名称',
|
||||||
|
pattern: '模式',
|
||||||
|
target: '目标工作流',
|
||||||
|
rate: '速率',
|
||||||
|
fires: '触发次数',
|
||||||
|
patternVersion: 'pattern_v',
|
||||||
|
lastFired: '上次触发',
|
||||||
|
state: '状态',
|
||||||
|
actions: '操作',
|
||||||
|
},
|
||||||
|
targetWorkflowSelect: '选择已发布的工作流',
|
||||||
|
targetWorkflowEmpty: '尚无已发布的工作流。',
|
||||||
|
patternHints: {
|
||||||
|
cron: '示例:{"cron":"0 0 * * * *","timezone":"Asia/Shanghai"} —— 每次修改 cron 都会让 pattern_version 自增。',
|
||||||
|
channel_message: '可填 channelType(如 feishu)和 senderEquals 进一步过滤。',
|
||||||
|
webhook: '透传匹配,请确保 HTTP 入口已校验 webhook 密钥。',
|
||||||
|
content_match: '需要 substring(不区分大小写)。空字符串将拒绝触发。',
|
||||||
|
agent_lifecycle: '可选 agentId 和 phase(spawned / terminated / crashed)。',
|
||||||
|
workflow_completion: '可选 sourceWorkflowId 和 stateFilter(completed / failed / any)。',
|
||||||
|
},
|
||||||
|
unnamed: '(未命名)',
|
||||||
|
rateUnit: '{count}/分钟',
|
||||||
|
enabled: '已启用',
|
||||||
|
disabled: '已停用',
|
||||||
|
empty: '暂无触发器配置。',
|
||||||
|
actions: {
|
||||||
|
edit: '编辑',
|
||||||
|
delete: '删除',
|
||||||
|
cancel: '取消',
|
||||||
|
save: '保存',
|
||||||
|
close: '关闭',
|
||||||
|
},
|
||||||
|
formTitleNew: '新建触发器',
|
||||||
|
formTitleEdit: '编辑触发器',
|
||||||
|
fields: {
|
||||||
|
name: '名称',
|
||||||
|
namePlaceholder: 'hourly-cleanup',
|
||||||
|
patternType: '模式类型',
|
||||||
|
patternJson: '模式 JSON',
|
||||||
|
patternJsonPlaceholder: '{"cron":"0 0 * * * *","timezone":"UTC"}',
|
||||||
|
targetType: '目标类型',
|
||||||
|
targetId: '目标 ID',
|
||||||
|
ratePerMin: '每分钟速率',
|
||||||
|
dedupWindowSecs: '去重窗口(秒)',
|
||||||
|
maxFires: '最大触发数(0 = 不限)',
|
||||||
|
botSelfFilter: 'Bot 自循环过滤(推荐)',
|
||||||
|
payloadTemplate: '载荷模板(Pebble)',
|
||||||
|
payloadTemplatePlaceholder: '{"who":"{{ event.who }}"}',
|
||||||
|
enabled: '启用',
|
||||||
|
},
|
||||||
|
deleteConfirm: '删除触发器「{name}」?',
|
||||||
|
saveFailed: '保存失败:{msg}',
|
||||||
|
toggleFailed: '切换失败:{msg}',
|
||||||
|
deleteFailed: '删除失败:{msg}',
|
||||||
|
},
|
||||||
login: {
|
login: {
|
||||||
kicker: 'AI Operating System',
|
kicker: 'AI Operating System',
|
||||||
subtitle: '你的智能 AI 助手',
|
subtitle: '你的智能 AI 助手',
|
||||||
|
|||||||
@ -4,52 +4,52 @@
|
|||||||
<div class="mc-page-inner triggers-page">
|
<div class="mc-page-inner triggers-page">
|
||||||
<div class="mc-page-header">
|
<div class="mc-page-header">
|
||||||
<div>
|
<div>
|
||||||
<div class="mc-page-kicker">Triggers</div>
|
<div class="mc-page-kicker">{{ t('triggers.kicker') }}</div>
|
||||||
<h1 class="mc-page-title">Workflow Triggers</h1>
|
<h1 class="mc-page-title">{{ t('triggers.title') }}</h1>
|
||||||
<p class="mc-page-desc">
|
<p class="mc-page-desc">{{ t('triggers.desc') }}</p>
|
||||||
Connect cron schedules and channel events to your published workflows.
|
|
||||||
Pattern_version increments on every cron expression change so multi-node
|
|
||||||
deployments coordinate correctly.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-primary" @click="openCreate">+ New Trigger</button>
|
<button class="btn-primary" @click="openCreate">{{ t('triggers.newTrigger') }}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="triggers-table mc-surface-card">
|
<table class="triggers-table mc-surface-card">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>{{ t('triggers.columns.name') }}</th>
|
||||||
<th>Pattern</th>
|
<th>{{ t('triggers.columns.pattern') }}</th>
|
||||||
<th>Target Workflow</th>
|
<th>{{ t('triggers.columns.target') }}</th>
|
||||||
<th>Rate</th>
|
<th>{{ t('triggers.columns.rate') }}</th>
|
||||||
<th>Fires</th>
|
<th>{{ t('triggers.columns.fires') }}</th>
|
||||||
<th>State</th>
|
<th>{{ t('triggers.columns.patternVersion') }}</th>
|
||||||
<th>Actions</th>
|
<th>{{ t('triggers.columns.lastFired') }}</th>
|
||||||
|
<th>{{ t('triggers.columns.state') }}</th>
|
||||||
|
<th>{{ t('triggers.columns.actions') }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="t in triggers" :key="t.id">
|
<tr v-for="row in triggers" :key="row.id">
|
||||||
<td>{{ t.name || '(unnamed)' }}</td>
|
<td>{{ row.name || t('triggers.unnamed') }}</td>
|
||||||
<td>
|
<td>
|
||||||
<code>{{ t.patternType }}</code>
|
<code>{{ row.patternType }}</code>
|
||||||
<div class="pattern-detail">{{ t.patternJson }}</div>
|
<div class="pattern-detail">{{ row.patternJson }}</div>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ t.targetType }}#{{ t.targetId }}</td>
|
<td>{{ formatTarget(row) }}</td>
|
||||||
<td>{{ t.rateLimitPerMin }}/min</td>
|
<td>{{ t('triggers.rateUnit', { count: row.rateLimitPerMin }) }}</td>
|
||||||
<td>{{ t.fireCount }}<span v-if="t.maxFires > 0"> / {{ t.maxFires }}</span></td>
|
<td>{{ row.fireCount }}<span v-if="row.maxFires > 0"> / {{ row.maxFires }}</span></td>
|
||||||
|
<td>{{ row.patternVersion }}</td>
|
||||||
|
<td>{{ formatTime(row.lastFiredAt) }}</td>
|
||||||
<td>
|
<td>
|
||||||
<label class="toggle">
|
<label class="toggle">
|
||||||
<input type="checkbox" :checked="t.enabled" @change="toggleEnabled(t)" />
|
<input type="checkbox" :checked="row.enabled" @change="toggleEnabled(row)" />
|
||||||
<span>{{ t.enabled ? 'enabled' : 'disabled' }}</span>
|
<span>{{ row.enabled ? t('triggers.enabled') : t('triggers.disabled') }}</span>
|
||||||
</label>
|
</label>
|
||||||
</td>
|
</td>
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
<button class="btn-ghost" @click="openEdit(t)">Edit</button>
|
<button class="btn-ghost" @click="openEdit(row)">{{ t('triggers.actions.edit') }}</button>
|
||||||
<button class="btn-danger" @click="remove(t)">Delete</button>
|
<button class="btn-danger" @click="remove(row)">{{ t('triggers.actions.delete') }}</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="!triggers.length">
|
<tr v-if="!triggers.length">
|
||||||
<td colspan="7" class="empty-row">No triggers configured.</td>
|
<td colspan="9" class="empty-row">{{ t('triggers.empty') }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@ -57,12 +57,14 @@
|
|||||||
<!-- Inline form (replaces modal for v0 simplicity) -->
|
<!-- Inline form (replaces modal for v0 simplicity) -->
|
||||||
<section v-if="formOpen" class="form-card mc-surface-card">
|
<section v-if="formOpen" class="form-card mc-surface-card">
|
||||||
<header>
|
<header>
|
||||||
<strong>{{ editing?.id ? 'Edit Trigger' : 'New Trigger' }}</strong>
|
<strong>{{ editing?.id ? t('triggers.formTitleEdit') : t('triggers.formTitleNew') }}</strong>
|
||||||
<button class="btn-ghost" @click="closeForm">Close</button>
|
<button class="btn-ghost" @click="closeForm">{{ t('triggers.actions.close') }}</button>
|
||||||
</header>
|
</header>
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<label>Name <input v-model="formState.name" placeholder="hourly-cleanup" /></label>
|
<label>{{ t('triggers.fields.name') }}
|
||||||
<label>Pattern type
|
<input v-model="formState.name" :placeholder="t('triggers.fields.namePlaceholder')" />
|
||||||
|
</label>
|
||||||
|
<label>{{ t('triggers.fields.patternType') }}
|
||||||
<select v-model="formState.patternType">
|
<select v-model="formState.patternType">
|
||||||
<option value="cron">cron</option>
|
<option value="cron">cron</option>
|
||||||
<option value="channel_message">channel_message</option>
|
<option value="channel_message">channel_message</option>
|
||||||
@ -70,38 +72,55 @@
|
|||||||
<option value="workflow_completion">workflow_completion</option>
|
<option value="workflow_completion">workflow_completion</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label class="span-2">Pattern JSON
|
<label class="span-2">{{ t('triggers.fields.patternJson') }}
|
||||||
<textarea v-model="formState.patternJson" rows="3"
|
<textarea v-model="formState.patternJson" rows="3"
|
||||||
placeholder='{"cron":"0 0 * * * *","timezone":"UTC"}' />
|
:placeholder="t('triggers.fields.patternJsonPlaceholder')" />
|
||||||
|
<small class="pattern-hint">{{ patternHint }}</small>
|
||||||
</label>
|
</label>
|
||||||
<label>Target type
|
<label>{{ t('triggers.fields.targetType') }}
|
||||||
<select v-model="formState.targetType">
|
<select v-model="formState.targetType">
|
||||||
<option value="workflow">workflow</option>
|
<option value="workflow">workflow</option>
|
||||||
<option value="agent">agent</option>
|
<option value="agent">agent</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>Target id <input v-model.number="formState.targetId" type="number" /></label>
|
<label v-if="formState.targetType === 'workflow'">{{ t('triggers.fields.targetId') }}
|
||||||
<label>Rate / min <input v-model.number="formState.rateLimitPerMin" type="number" /></label>
|
<select v-model.number="formState.targetId">
|
||||||
<label>Dedup window (s) <input v-model.number="formState.dedupWindowSecs" type="number" /></label>
|
<option v-if="!availableWorkflows.length" :value="0">
|
||||||
<label>Max fires (0 = unlimited)
|
{{ t('triggers.targetWorkflowEmpty') }}
|
||||||
|
</option>
|
||||||
|
<option v-else v-for="wf in availableWorkflows" :key="wf.id" :value="wf.id">
|
||||||
|
#{{ wf.id }} — {{ wf.name || t('triggers.unnamed') }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label v-else>{{ t('triggers.fields.targetId') }}
|
||||||
|
<input v-model.number="formState.targetId" type="number" />
|
||||||
|
</label>
|
||||||
|
<label>{{ t('triggers.fields.ratePerMin') }}
|
||||||
|
<input v-model.number="formState.rateLimitPerMin" type="number" />
|
||||||
|
</label>
|
||||||
|
<label>{{ t('triggers.fields.dedupWindowSecs') }}
|
||||||
|
<input v-model.number="formState.dedupWindowSecs" type="number" />
|
||||||
|
</label>
|
||||||
|
<label>{{ t('triggers.fields.maxFires') }}
|
||||||
<input v-model.number="formState.maxFires" type="number" />
|
<input v-model.number="formState.maxFires" type="number" />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" v-model="formState.botSelfFilter" />
|
<input type="checkbox" v-model="formState.botSelfFilter" />
|
||||||
Bot-self filter (recommended)
|
{{ t('triggers.fields.botSelfFilter') }}
|
||||||
</label>
|
</label>
|
||||||
<label class="span-2">Payload template (Pebble)
|
<label class="span-2">{{ t('triggers.fields.payloadTemplate') }}
|
||||||
<textarea v-model="formState.payloadTemplate" rows="3"
|
<textarea v-model="formState.payloadTemplate" rows="3"
|
||||||
placeholder='{"who":"{{ event.who }}"}' />
|
:placeholder="t('triggers.fields.payloadTemplatePlaceholder')" />
|
||||||
</label>
|
</label>
|
||||||
<label class="span-2">
|
<label class="span-2">
|
||||||
<input type="checkbox" v-model="formState.enabled" />
|
<input type="checkbox" v-model="formState.enabled" />
|
||||||
Enabled
|
{{ t('triggers.fields.enabled') }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<footer>
|
<footer>
|
||||||
<button class="btn-ghost" @click="closeForm">Cancel</button>
|
<button class="btn-ghost" @click="closeForm">{{ t('triggers.actions.cancel') }}</button>
|
||||||
<button class="btn-primary" :disabled="busy" @click="save">Save</button>
|
<button class="btn-primary" :disabled="busy" @click="save">{{ t('triggers.actions.save') }}</button>
|
||||||
</footer>
|
</footer>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@ -111,12 +130,24 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref, watch } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import { triggerApi, type TriggerSummary } from '@/api'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { triggerApi, type TriggerSummary, workflowApi, type WorkflowSummary } from '@/api'
|
||||||
import { useWorkspaceStore } from '@/stores/useWorkspaceStore'
|
import { useWorkspaceStore } from '@/stores/useWorkspaceStore'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
const workspaceStore = useWorkspaceStore()
|
const workspaceStore = useWorkspaceStore()
|
||||||
const workspaceId = computed(() => workspaceStore.currentWorkspaceId)
|
const workspaceId = computed(() => workspaceStore.currentWorkspaceId)
|
||||||
const triggers = ref<TriggerSummary[]>([])
|
const triggers = ref<TriggerSummary[]>([])
|
||||||
|
const workflows = ref<WorkflowSummary[]>([])
|
||||||
|
|
||||||
|
const availableWorkflows = computed(() =>
|
||||||
|
workflows.value.filter((w) => w.latestRevisionId)
|
||||||
|
)
|
||||||
|
|
||||||
|
const patternHint = computed(() => {
|
||||||
|
const key = `triggers.patternHints.${formState.value.patternType}`
|
||||||
|
return t(key, '')
|
||||||
|
})
|
||||||
|
|
||||||
const formOpen = ref(false)
|
const formOpen = ref(false)
|
||||||
const editing = ref<TriggerSummary | null>(null)
|
const editing = ref<TriggerSummary | null>(null)
|
||||||
@ -162,6 +193,25 @@ async function reload() {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('listTriggers failed', e)
|
console.error('listTriggers failed', e)
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
const res = await workflowApi.list(workspaceId.value)
|
||||||
|
workflows.value = (res.data as unknown as WorkflowSummary[]) ?? []
|
||||||
|
} catch (e) {
|
||||||
|
console.error('listWorkflows failed', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTarget(row: TriggerSummary) {
|
||||||
|
if (row.targetType === 'workflow') {
|
||||||
|
const wf = workflows.value.find((w) => w.id === row.targetId)
|
||||||
|
if (wf?.name) return `${wf.name} (#${row.targetId})`
|
||||||
|
}
|
||||||
|
return `${row.targetType}#${row.targetId}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(iso?: string) {
|
||||||
|
if (!iso) return '-'
|
||||||
|
return iso.replace('T', ' ').slice(0, 19)
|
||||||
}
|
}
|
||||||
|
|
||||||
function openCreate() {
|
function openCreate() {
|
||||||
@ -170,20 +220,20 @@ function openCreate() {
|
|||||||
formOpen.value = true
|
formOpen.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function openEdit(t: TriggerSummary) {
|
function openEdit(row: TriggerSummary) {
|
||||||
editing.value = t
|
editing.value = row
|
||||||
formState.value = {
|
formState.value = {
|
||||||
name: t.name ?? '',
|
name: row.name ?? '',
|
||||||
patternType: t.patternType,
|
patternType: row.patternType,
|
||||||
patternJson: t.patternJson,
|
patternJson: row.patternJson,
|
||||||
targetType: t.targetType,
|
targetType: row.targetType,
|
||||||
targetId: t.targetId,
|
targetId: row.targetId,
|
||||||
rateLimitPerMin: t.rateLimitPerMin ?? 60,
|
rateLimitPerMin: row.rateLimitPerMin ?? 60,
|
||||||
dedupWindowSecs: t.dedupWindowSecs ?? 60,
|
dedupWindowSecs: row.dedupWindowSecs ?? 60,
|
||||||
botSelfFilter: t.botSelfFilter ?? true,
|
botSelfFilter: row.botSelfFilter ?? true,
|
||||||
enabled: t.enabled,
|
enabled: row.enabled,
|
||||||
maxFires: t.maxFires ?? 0,
|
maxFires: row.maxFires ?? 0,
|
||||||
payloadTemplate: t.payloadTemplate ?? '',
|
payloadTemplate: row.payloadTemplate ?? '',
|
||||||
}
|
}
|
||||||
formOpen.value = true
|
formOpen.value = true
|
||||||
}
|
}
|
||||||
@ -208,28 +258,28 @@ async function save() {
|
|||||||
formOpen.value = false
|
formOpen.value = false
|
||||||
await reload()
|
await reload()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
window.alert('Save failed: ' + (e as Error).message)
|
window.alert(t('triggers.saveFailed', { msg: (e as Error).message }))
|
||||||
} finally {
|
} finally {
|
||||||
busy.value = false
|
busy.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleEnabled(t: TriggerSummary) {
|
async function toggleEnabled(row: TriggerSummary) {
|
||||||
try {
|
try {
|
||||||
await triggerApi.update(t.id, { ...t, enabled: !t.enabled })
|
await triggerApi.update(row.id, { ...row, enabled: !row.enabled })
|
||||||
await reload()
|
await reload()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
window.alert('Toggle failed: ' + (e as Error).message)
|
window.alert(t('triggers.toggleFailed', { msg: (e as Error).message }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function remove(t: TriggerSummary) {
|
async function remove(row: TriggerSummary) {
|
||||||
if (!window.confirm(`Delete trigger "${t.name || t.id}"?`)) return
|
if (!window.confirm(t('triggers.deleteConfirm', { name: row.name || String(row.id) }))) return
|
||||||
try {
|
try {
|
||||||
await triggerApi.delete(t.id)
|
await triggerApi.delete(row.id)
|
||||||
await reload()
|
await reload()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
window.alert('Delete failed: ' + (e as Error).message)
|
window.alert(t('triggers.deleteFailed', { msg: (e as Error).message }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -321,6 +371,15 @@ watch(workspaceId, reload)
|
|||||||
font-family: 'JetBrains Mono', Consolas, monospace;
|
font-family: 'JetBrains Mono', Consolas, monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
.pattern-hint {
|
||||||
|
display: block;
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 400;
|
||||||
|
font-family: 'JetBrains Mono', Consolas, monospace;
|
||||||
|
opacity: 0.7;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
.form-card footer {
|
.form-card footer {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -4,22 +4,19 @@
|
|||||||
<div class="mc-page-inner workflows-page">
|
<div class="mc-page-inner workflows-page">
|
||||||
<div class="mc-page-header">
|
<div class="mc-page-header">
|
||||||
<div>
|
<div>
|
||||||
<div class="mc-page-kicker">Workflows</div>
|
<div class="mc-page-kicker">{{ t('workflows.kicker') }}</div>
|
||||||
<h1 class="mc-page-title">Workflow Editor</h1>
|
<h1 class="mc-page-title">{{ t('workflows.title') }}</h1>
|
||||||
<p class="mc-page-desc">
|
<p class="mc-page-desc">{{ t('workflows.desc') }}</p>
|
||||||
Author multi-step workflows. Save the draft as you go, compile to surface diagnostics,
|
|
||||||
then publish a revision when it is ready to run.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-primary" @click="openCreate">+ New Workflow</button>
|
<button class="btn-primary" @click="openCreate">{{ t('workflows.newWorkflow') }}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="workflows-grid">
|
<div class="workflows-grid">
|
||||||
<!-- left: list -->
|
<!-- left: list -->
|
||||||
<aside class="workflows-list mc-surface-card">
|
<aside class="workflows-list mc-surface-card">
|
||||||
<div class="list-header">
|
<div class="list-header">
|
||||||
<span>Defined ({{ workflows.length }})</span>
|
<span>{{ t('workflows.defined', { count: workflows.length }) }}</span>
|
||||||
<button class="btn-ghost" @click="reload">Refresh</button>
|
<button class="btn-ghost" @click="reload">{{ t('workflows.refresh') }}</button>
|
||||||
</div>
|
</div>
|
||||||
<ul class="list-body">
|
<ul class="list-body">
|
||||||
<li
|
<li
|
||||||
@ -30,37 +27,53 @@
|
|||||||
@click="select(wf.id)"
|
@click="select(wf.id)"
|
||||||
>
|
>
|
||||||
<div class="list-row-name">
|
<div class="list-row-name">
|
||||||
{{ wf.name || '(unnamed)' }}
|
{{ wf.name || t('workflows.unnamed') }}
|
||||||
<span v-if="wf.latestRevisionId" class="badge published">v{{ wf.latestRevisionId }}</span>
|
<span v-if="wf.latestRevisionId" class="badge published">{{ t('workflows.publishedBadge', { rev: wf.latestRevisionId }) }}</span>
|
||||||
<span v-else class="badge draft">draft</span>
|
<span v-else class="badge draft">{{ t('workflows.draftBadge') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="list-row-desc">{{ wf.description || '-' }}</div>
|
<div class="list-row-desc">{{ wf.description || '-' }}</div>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="!workflows.length" class="list-empty">No workflows yet — create one to get started.</li>
|
<li v-if="!workflows.length" class="list-empty">{{ t('workflows.listEmpty') }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<!-- middle: editor -->
|
<!-- middle: editor -->
|
||||||
<section class="workflows-editor mc-surface-card" v-if="selected">
|
<section class="workflows-editor mc-surface-card" v-if="selected">
|
||||||
<header class="editor-header">
|
<header class="editor-header">
|
||||||
<input v-model="selected.name" class="editor-name" placeholder="Workflow name" />
|
<input v-model="selected.name" class="editor-name" :placeholder="t('workflows.namePlaceholder')" />
|
||||||
<input v-model="selected.description" class="editor-desc" placeholder="Optional description" />
|
<input v-model="selected.description" class="editor-desc" :placeholder="t('workflows.descPlaceholder')" />
|
||||||
<div class="editor-actions">
|
<div class="editor-actions">
|
||||||
<button class="btn-ghost" :disabled="busy" @click="saveMeta">Save Meta</button>
|
<button class="btn-ghost" :disabled="busy" @click="saveMeta">{{ t('workflows.actions.saveMeta') }}</button>
|
||||||
<button class="btn-ghost" :disabled="busy" @click="saveDraft">Save Draft</button>
|
<button class="btn-ghost" :disabled="busy" @click="saveDraft">{{ t('workflows.actions.saveDraft') }}</button>
|
||||||
<button class="btn-ghost" :disabled="busy" @click="compile">Compile</button>
|
<button class="btn-ghost" :disabled="busy" @click="compile">{{ t('workflows.actions.compile') }}</button>
|
||||||
<button class="btn-primary" :disabled="busy" @click="publish">Publish</button>
|
<button class="btn-primary" :disabled="busy" @click="publish">{{ t('workflows.actions.publish') }}</button>
|
||||||
<button class="btn-danger" :disabled="busy" @click="remove">Delete</button>
|
<button class="btn-danger" :disabled="busy" @click="remove">{{ t('workflows.actions.delete') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
<div class="editor-toolbar">
|
||||||
|
<label class="template-picker">
|
||||||
|
<span>{{ t('workflows.templates.label') }}</span>
|
||||||
|
<select v-model="templateChoice" @change="insertTemplate">
|
||||||
|
<option value="">{{ t('workflows.templates.placeholder') }}</option>
|
||||||
|
<option value="sequential">{{ t('workflows.templates.sequential') }}</option>
|
||||||
|
<option value="fan_out">{{ t('workflows.templates.fan_out') }}</option>
|
||||||
|
<option value="collect">{{ t('workflows.templates.collect') }}</option>
|
||||||
|
<option value="conditional">{{ t('workflows.templates.conditional') }}</option>
|
||||||
|
<option value="await_approval">{{ t('workflows.templates.await_approval') }}</option>
|
||||||
|
<option value="dispatch_channel">{{ t('workflows.templates.dispatch_channel') }}</option>
|
||||||
|
<option value="write_memory">{{ t('workflows.templates.write_memory') }}</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<span class="json-hint" :class="jsonHintKind">{{ jsonHint }}</span>
|
||||||
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
v-model="draftJson"
|
v-model="draftJson"
|
||||||
class="editor-body"
|
class="editor-body"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
placeholder='{"steps":[{"name":"step-a","agentName":"...","mode":{"type":"sequential"},"promptTemplate":"..."}]}'
|
:placeholder="t('workflows.bodyPlaceholder')"
|
||||||
/>
|
/>
|
||||||
<div v-if="compileErrors.length" class="errors-panel">
|
<div v-if="compileErrors.length" class="errors-panel">
|
||||||
<div class="errors-title">{{ compileErrors.length }} compile error(s)</div>
|
<div class="errors-title">{{ t('workflows.compileErrorsTitle', { count: compileErrors.length }) }}</div>
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="(err, idx) in compileErrors" :key="idx">
|
<li v-for="(err, idx) in compileErrors" :key="idx">
|
||||||
<code>{{ err.code }}</code>
|
<code>{{ err.code }}</code>
|
||||||
@ -75,14 +88,14 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="workflows-empty mc-surface-card" v-else>
|
<section class="workflows-empty mc-surface-card" v-else>
|
||||||
<p>Select a workflow on the left, or click "New Workflow" to start a fresh draft.</p>
|
<p>{{ t('workflows.selectHint') }}</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- right: runs -->
|
<!-- right: runs -->
|
||||||
<aside class="workflows-runs mc-surface-card" v-if="selected">
|
<aside class="workflows-runs mc-surface-card" v-if="selected">
|
||||||
<header class="runs-header">
|
<header class="runs-header">
|
||||||
<span>Recent runs ({{ runs.length }})</span>
|
<span>{{ t('workflows.runs.header', { count: runs.length }) }}</span>
|
||||||
<button class="btn-ghost" @click="reloadRuns">Refresh</button>
|
<button class="btn-ghost" @click="reloadRuns">{{ t('workflows.refresh') }}</button>
|
||||||
</header>
|
</header>
|
||||||
<ul class="runs-list">
|
<ul class="runs-list">
|
||||||
<li v-for="run in runs" :key="run.id" class="run-row" @click="loadRun(run.id)">
|
<li v-for="run in runs" :key="run.id" class="run-row" @click="loadRun(run.id)">
|
||||||
@ -91,19 +104,19 @@
|
|||||||
<span class="run-time">{{ formatTime(run.startedAt) }}</span>
|
<span class="run-time">{{ formatTime(run.startedAt) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="run-row-meta">
|
<div class="run-row-meta">
|
||||||
<span>run #{{ run.id }}</span>
|
<span>{{ t('workflows.runs.runHash', { id: run.id }) }}</span>
|
||||||
<span v-if="run.triggeredBy">· {{ run.triggeredBy }}</span>
|
<span v-if="run.triggeredBy">· {{ run.triggeredBy }}</span>
|
||||||
<span v-if="run.errorMessage" class="run-err">· {{ run.errorMessage }}</span>
|
<span v-if="run.errorMessage" class="run-err">· {{ run.errorMessage }}</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="!runs.length" class="runs-empty">No runs yet.</li>
|
<li v-if="!runs.length" class="runs-empty">{{ t('workflows.runs.empty') }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
<section v-if="runDetail" class="run-detail">
|
<section v-if="runDetail" class="run-detail">
|
||||||
<div class="run-detail-title">Run #{{ runDetail.run.id }} — {{ runDetail.run.state }}</div>
|
<div class="run-detail-title">{{ t('workflows.runs.detailTitle', { id: runDetail.run.id, state: runDetail.run.state }) }}</div>
|
||||||
<ol class="run-steps">
|
<ol class="run-steps">
|
||||||
<li v-for="step in runDetail.steps" :key="step.id">
|
<li v-for="step in runDetail.steps" :key="step.id">
|
||||||
<span class="step-state" :class="'state-' + step.state">{{ step.state }}</span>
|
<span class="step-state" :class="'state-' + step.state">{{ step.state }}</span>
|
||||||
<span class="step-name">{{ step.stepName || '(unnamed)' }}</span>
|
<span class="step-name">{{ step.stepName || t('workflows.unnamed') }}</span>
|
||||||
<span v-if="step.durationMs != null" class="step-duration">{{ step.durationMs }} ms</span>
|
<span v-if="step.durationMs != null" class="step-duration">{{ step.durationMs }} ms</span>
|
||||||
<span v-if="step.errorMessage" class="step-err">{{ step.errorMessage }}</span>
|
<span v-if="step.errorMessage" class="step-err">{{ step.errorMessage }}</span>
|
||||||
</li>
|
</li>
|
||||||
@ -118,6 +131,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref, watch } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import {
|
import {
|
||||||
workflowApi,
|
workflowApi,
|
||||||
type WorkflowSummary,
|
type WorkflowSummary,
|
||||||
@ -128,6 +142,7 @@ import {
|
|||||||
} from '@/api'
|
} from '@/api'
|
||||||
import { useWorkspaceStore } from '@/stores/useWorkspaceStore'
|
import { useWorkspaceStore } from '@/stores/useWorkspaceStore'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
const workspaceStore = useWorkspaceStore()
|
const workspaceStore = useWorkspaceStore()
|
||||||
const workspaceId = computed(() => workspaceStore.currentWorkspaceId)
|
const workspaceId = computed(() => workspaceStore.currentWorkspaceId)
|
||||||
|
|
||||||
@ -143,6 +158,97 @@ const busy = ref(false)
|
|||||||
const runs = ref<WorkflowRun[]>([])
|
const runs = ref<WorkflowRun[]>([])
|
||||||
const runDetail = ref<{ run: WorkflowRun; steps: WorkflowRunStep[] } | null>(null)
|
const runDetail = ref<{ run: WorkflowRun; steps: WorkflowRunStep[] } | null>(null)
|
||||||
|
|
||||||
|
const templateChoice = ref('')
|
||||||
|
|
||||||
|
// Live JSON-syntax check on the textarea so the operator sees parse errors
|
||||||
|
// immediately, instead of waiting for compile to round-trip.
|
||||||
|
const jsonHint = computed(() => {
|
||||||
|
if (!draftJson.value.trim()) return ''
|
||||||
|
try {
|
||||||
|
JSON.parse(draftJson.value)
|
||||||
|
return t('workflows.jsonOk')
|
||||||
|
} catch (e) {
|
||||||
|
return t('workflows.jsonInvalid', { msg: (e as Error).message })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const jsonHintKind = computed(() => (jsonHint.value === t('workflows.jsonOk') ? 'ok' : 'err'))
|
||||||
|
|
||||||
|
const STEP_TEMPLATES: Record<string, object> = {
|
||||||
|
sequential: {
|
||||||
|
name: 'step-sequential',
|
||||||
|
agentName: 'agent-name',
|
||||||
|
mode: { type: 'sequential' },
|
||||||
|
promptTemplate: 'Process: {{ inputs.payload }}',
|
||||||
|
},
|
||||||
|
fan_out: {
|
||||||
|
name: 'step-fan',
|
||||||
|
agentName: 'agent-name',
|
||||||
|
mode: { type: 'fan_out' },
|
||||||
|
promptTemplate: 'Branch task',
|
||||||
|
},
|
||||||
|
collect: {
|
||||||
|
name: 'step-collect',
|
||||||
|
agentName: 'agent-name',
|
||||||
|
mode: { type: 'collect' },
|
||||||
|
promptTemplate: 'Combine: {{ inputs.payload }}',
|
||||||
|
},
|
||||||
|
conditional: {
|
||||||
|
name: 'step-conditional',
|
||||||
|
agentName: 'agent-name',
|
||||||
|
mode: { type: 'conditional', expression: '{{ inputs.payload != null }}' },
|
||||||
|
promptTemplate: 'Run only when condition holds',
|
||||||
|
},
|
||||||
|
await_approval: {
|
||||||
|
name: 'step-approval',
|
||||||
|
mode: {
|
||||||
|
type: 'await_approval',
|
||||||
|
approvalKind: 'manual',
|
||||||
|
approvalMessage: 'Please review and approve',
|
||||||
|
timeoutSecs: 3600,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dispatch_channel: {
|
||||||
|
name: 'step-dispatch',
|
||||||
|
mode: {
|
||||||
|
type: 'dispatch_channel',
|
||||||
|
channels: ['feishu'],
|
||||||
|
targets: { feishu: 'group-id-here' },
|
||||||
|
content: 'Notification: {{ inputs.payload }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
write_memory: {
|
||||||
|
name: 'step-memory',
|
||||||
|
mode: {
|
||||||
|
type: 'write_memory',
|
||||||
|
employeeId: 'employee-id',
|
||||||
|
file: 'workspace.md',
|
||||||
|
mergeStrategy: 'append',
|
||||||
|
content: '{{ inputs.payload }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertTemplate() {
|
||||||
|
const choice = templateChoice.value
|
||||||
|
if (!choice) return
|
||||||
|
const stepBlock = STEP_TEMPLATES[choice]
|
||||||
|
if (!stepBlock) return
|
||||||
|
// Try to inject into an existing draft's `steps` array; fall back to a
|
||||||
|
// fresh skeleton if the current text isn't valid JSON or doesn't have
|
||||||
|
// the expected shape.
|
||||||
|
let next: string
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(draftJson.value || '{}') as { steps?: unknown[] }
|
||||||
|
if (!Array.isArray(parsed.steps)) parsed.steps = []
|
||||||
|
parsed.steps.push(stepBlock)
|
||||||
|
next = JSON.stringify(parsed, null, 2)
|
||||||
|
} catch {
|
||||||
|
next = JSON.stringify({ steps: [stepBlock] }, null, 2)
|
||||||
|
}
|
||||||
|
draftJson.value = next
|
||||||
|
templateChoice.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
async function reload() {
|
async function reload() {
|
||||||
if (!workspaceId.value) return
|
if (!workspaceId.value) return
|
||||||
try {
|
try {
|
||||||
@ -188,7 +294,7 @@ async function loadRun(runId: number) {
|
|||||||
|
|
||||||
async function openCreate() {
|
async function openCreate() {
|
||||||
if (!workspaceId.value) return
|
if (!workspaceId.value) return
|
||||||
const name = window.prompt('New workflow name:', 'untitled-workflow')
|
const name = window.prompt(t('workflows.prompts.newName'), t('workflows.prompts.defaultName'))
|
||||||
if (!name) return
|
if (!name) return
|
||||||
busy.value = true
|
busy.value = true
|
||||||
try {
|
try {
|
||||||
@ -201,7 +307,7 @@ async function openCreate() {
|
|||||||
await reload()
|
await reload()
|
||||||
if (created?.id) await select(created.id)
|
if (created?.id) await select(created.id)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setStatus('Create failed: ' + (e as Error).message, 'err')
|
setStatus(t('workflows.status.createFailed', { msg: (e as Error).message }), 'err')
|
||||||
} finally {
|
} finally {
|
||||||
busy.value = false
|
busy.value = false
|
||||||
}
|
}
|
||||||
@ -216,10 +322,10 @@ async function saveMeta() {
|
|||||||
description: selected.value.description,
|
description: selected.value.description,
|
||||||
enabled: selected.value.enabled,
|
enabled: selected.value.enabled,
|
||||||
})
|
})
|
||||||
setStatus('Metadata saved.', 'ok')
|
setStatus(t('workflows.status.metaSaved'), 'ok')
|
||||||
await reload()
|
await reload()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setStatus('Save failed: ' + (e as Error).message, 'err')
|
setStatus(t('workflows.status.saveFailed', { msg: (e as Error).message }), 'err')
|
||||||
} finally {
|
} finally {
|
||||||
busy.value = false
|
busy.value = false
|
||||||
}
|
}
|
||||||
@ -230,9 +336,9 @@ async function saveDraft() {
|
|||||||
busy.value = true
|
busy.value = true
|
||||||
try {
|
try {
|
||||||
await workflowApi.saveDraft(selected.value.id, draftJson.value)
|
await workflowApi.saveDraft(selected.value.id, draftJson.value)
|
||||||
setStatus('Draft saved.', 'ok')
|
setStatus(t('workflows.status.draftSaved'), 'ok')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setStatus('Save draft failed: ' + (e as Error).message, 'err')
|
setStatus(t('workflows.status.saveDraftFailed', { msg: (e as Error).message }), 'err')
|
||||||
} finally {
|
} finally {
|
||||||
busy.value = false
|
busy.value = false
|
||||||
}
|
}
|
||||||
@ -245,7 +351,7 @@ async function compile() {
|
|||||||
try {
|
try {
|
||||||
await workflowApi.saveDraft(selected.value.id, draftJson.value)
|
await workflowApi.saveDraft(selected.value.id, draftJson.value)
|
||||||
await workflowApi.compile(selected.value.id)
|
await workflowApi.compile(selected.value.id)
|
||||||
setStatus('Compile OK.', 'ok')
|
setStatus(t('workflows.status.compileOk'), 'ok')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleCompileError(e)
|
handleCompileError(e)
|
||||||
} finally {
|
} finally {
|
||||||
@ -259,9 +365,9 @@ async function publish() {
|
|||||||
compileErrors.value = []
|
compileErrors.value = []
|
||||||
try {
|
try {
|
||||||
await workflowApi.saveDraft(selected.value.id, draftJson.value)
|
await workflowApi.saveDraft(selected.value.id, draftJson.value)
|
||||||
const note = window.prompt('Optional publish note:', '') ?? undefined
|
const note = window.prompt(t('workflows.prompts.publishNote'), '') ?? undefined
|
||||||
await workflowApi.publish(selected.value.id, note)
|
await workflowApi.publish(selected.value.id, note)
|
||||||
setStatus('Published.', 'ok')
|
setStatus(t('workflows.status.published'), 'ok')
|
||||||
await reload()
|
await reload()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleCompileError(e)
|
handleCompileError(e)
|
||||||
@ -272,7 +378,7 @@ async function publish() {
|
|||||||
|
|
||||||
async function remove() {
|
async function remove() {
|
||||||
if (!selected.value) return
|
if (!selected.value) return
|
||||||
if (!window.confirm(`Delete workflow "${selected.value.name}"? This is reversible via the audit log.`)) return
|
if (!window.confirm(t('workflows.prompts.deleteConfirm', { name: selected.value.name ?? '' }))) return
|
||||||
busy.value = true
|
busy.value = true
|
||||||
try {
|
try {
|
||||||
await workflowApi.delete(selected.value.id)
|
await workflowApi.delete(selected.value.id)
|
||||||
@ -280,9 +386,9 @@ async function remove() {
|
|||||||
selectedId.value = null
|
selectedId.value = null
|
||||||
draftJson.value = ''
|
draftJson.value = ''
|
||||||
await reload()
|
await reload()
|
||||||
setStatus('Deleted.', 'ok')
|
setStatus(t('workflows.status.deleted'), 'ok')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setStatus('Delete failed: ' + (e as Error).message, 'err')
|
setStatus(t('workflows.status.deleteFailed', { msg: (e as Error).message }), 'err')
|
||||||
} finally {
|
} finally {
|
||||||
busy.value = false
|
busy.value = false
|
||||||
}
|
}
|
||||||
@ -296,9 +402,9 @@ function handleCompileError(e: unknown) {
|
|||||||
const failure = err.response?.data?.data
|
const failure = err.response?.data?.data
|
||||||
if (failure?.errors?.length) {
|
if (failure?.errors?.length) {
|
||||||
compileErrors.value = failure.errors
|
compileErrors.value = failure.errors
|
||||||
setStatus(`${failure.errorCount} compile error(s) — see panel below.`, 'err')
|
setStatus(t('workflows.status.compileFailed', { count: failure.errorCount ?? failure.errors.length }), 'err')
|
||||||
} else {
|
} else {
|
||||||
setStatus(err.message || 'Compile / publish failed.', 'err')
|
setStatus(err.message || t('workflows.status.compileFallback'), 'err')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -430,6 +536,36 @@ watch(workspaceId, reload)
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
.editor-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.template-picker {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.template-picker select {
|
||||||
|
padding: 4px 6px;
|
||||||
|
border: 1px solid var(--mc-border, rgba(0, 0, 0, 0.12));
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.json-hint {
|
||||||
|
font-family: 'JetBrains Mono', Consolas, monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
opacity: 0.85;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.json-hint.ok { color: #1e8449; }
|
||||||
|
.json-hint.err { color: #c0392b; }
|
||||||
.editor-body {
|
.editor-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-family: 'JetBrains Mono', Consolas, monospace;
|
font-family: 'JetBrains Mono', Consolas, monospace;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user