mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(ui/triggers): structured per-pattern form for all 6 pattern types
This commit is contained in:
parent
231724d6a9
commit
6bcf6b5ee4
@ -93,13 +93,13 @@
|
||||
<label class="panel-field">
|
||||
<span class="field-label">{{ t('workflows.canvas.modeLabel') }}</span>
|
||||
<select class="mc-input" :value="modeType" @change="onModeChange">
|
||||
<option value="sequential">sequential</option>
|
||||
<option value="fan_out">fan_out</option>
|
||||
<option value="collect">collect</option>
|
||||
<option value="conditional">conditional</option>
|
||||
<option value="await_approval">await_approval</option>
|
||||
<option value="dispatch_channel">dispatch_channel</option>
|
||||
<option value="write_memory">write_memory</option>
|
||||
<option value="sequential">{{ modeOptionLabel('sequential') }}</option>
|
||||
<option value="fan_out">{{ modeOptionLabel('fan_out') }}</option>
|
||||
<option value="collect">{{ modeOptionLabel('collect') }}</option>
|
||||
<option value="conditional">{{ modeOptionLabel('conditional') }}</option>
|
||||
<option value="await_approval">{{ modeOptionLabel('await_approval') }}</option>
|
||||
<option value="dispatch_channel">{{ modeOptionLabel('dispatch_channel') }}</option>
|
||||
<option value="write_memory">{{ modeOptionLabel('write_memory') }}</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
@ -265,11 +265,15 @@ const { t } = useI18n()
|
||||
const modeType = computed(() => (props.step?.mode?.type ?? 'sequential') as string)
|
||||
|
||||
const localizedModeLabel = computed(() => {
|
||||
const key = `workflows.canvas.modeLabels.${modeType.value}`
|
||||
const localized = t(key, '')
|
||||
return localized && localized !== key ? localized : modeType.value
|
||||
return modeOptionLabel(modeType.value)
|
||||
})
|
||||
|
||||
function modeOptionLabel(type: string): string {
|
||||
const key = `workflows.canvas.modeLabels.${type}`
|
||||
const localized = t(key, '')
|
||||
return localized && localized !== key ? localized : type
|
||||
}
|
||||
|
||||
const modeNeedsAgent = computed(() => {
|
||||
// fan_out / collect / await_approval / dispatch_channel / write_memory
|
||||
// don't take an agent — the runtime calls a service adapter instead.
|
||||
|
||||
386
mateclaw-ui/src/components/workflow/TriggerPatternForm.vue
Normal file
386
mateclaw-ui/src/components/workflow/TriggerPatternForm.vue
Normal file
@ -0,0 +1,386 @@
|
||||
<template>
|
||||
<div class="pattern-form">
|
||||
<!-- cron — schedule expression + timezone -->
|
||||
<template v-if="patternType === 'cron'">
|
||||
<div class="pf-field">
|
||||
<span class="pf-label">{{ t('triggers.pattern.cronExpression') }}</span>
|
||||
<input
|
||||
v-model.trim="form.cron"
|
||||
class="pf-input mono"
|
||||
:placeholder="t('triggers.pattern.cronExpressionPlaceholder')"
|
||||
spellcheck="false"
|
||||
@input="emitFromForm"
|
||||
/>
|
||||
<small v-if="errors.cron" class="pf-err">{{ errors.cron }}</small>
|
||||
<small v-else class="pf-hint">{{ t('triggers.pattern.cronExpressionHint') }}</small>
|
||||
</div>
|
||||
<div class="pf-field">
|
||||
<span class="pf-label">{{ t('triggers.pattern.cronTimezone') }}</span>
|
||||
<input
|
||||
v-model.trim="form.timezone"
|
||||
class="pf-input"
|
||||
:placeholder="t('triggers.pattern.cronTimezonePlaceholder')"
|
||||
spellcheck="false"
|
||||
@input="emitFromForm"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- channel_message — adapter dropdown + optional senderEquals -->
|
||||
<template v-else-if="patternType === 'channel_message'">
|
||||
<div class="pf-field">
|
||||
<span class="pf-label">{{ t('triggers.pattern.channelType') }}</span>
|
||||
<select v-model="form.channelType" class="pf-input" @change="emitFromForm">
|
||||
<option value="">{{ t('triggers.pattern.channelTypeAny') }}</option>
|
||||
<option value="feishu">feishu</option>
|
||||
<option value="dingtalk">dingtalk</option>
|
||||
<option value="wecom">wecom</option>
|
||||
<option value="telegram">telegram</option>
|
||||
<option value="discord">discord</option>
|
||||
<option value="slack">slack</option>
|
||||
<option value="qq">qq</option>
|
||||
<option value="web">web</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="pf-field">
|
||||
<span class="pf-label">{{ t('triggers.pattern.senderEquals') }}</span>
|
||||
<input
|
||||
v-model.trim="form.senderEquals"
|
||||
class="pf-input"
|
||||
:placeholder="t('triggers.pattern.senderEqualsPlaceholder')"
|
||||
spellcheck="false"
|
||||
@input="emitFromForm"
|
||||
/>
|
||||
<small class="pf-hint">{{ t('triggers.pattern.senderEqualsHint') }}</small>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- content_match — substring (required) -->
|
||||
<template v-else-if="patternType === 'content_match'">
|
||||
<div class="pf-field">
|
||||
<span class="pf-label">{{ t('triggers.pattern.substring') }} *</span>
|
||||
<input
|
||||
v-model.trim="form.substring"
|
||||
class="pf-input"
|
||||
:placeholder="t('triggers.pattern.substringPlaceholder')"
|
||||
spellcheck="false"
|
||||
@input="emitFromForm"
|
||||
/>
|
||||
<small v-if="errors.substring" class="pf-err">{{ errors.substring }}</small>
|
||||
<small v-else class="pf-hint">{{ t('triggers.pattern.substringHint') }}</small>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- agent_lifecycle — agentId + phase -->
|
||||
<template v-else-if="patternType === 'agent_lifecycle'">
|
||||
<div class="pf-field">
|
||||
<span class="pf-label">{{ t('triggers.pattern.agentId') }}</span>
|
||||
<input
|
||||
v-model.number="form.agentId"
|
||||
type="number"
|
||||
min="0"
|
||||
class="pf-input"
|
||||
:placeholder="t('triggers.pattern.agentIdPlaceholder')"
|
||||
@input="emitFromForm"
|
||||
/>
|
||||
<small class="pf-hint">{{ t('triggers.pattern.agentIdHint') }}</small>
|
||||
</div>
|
||||
<div class="pf-field">
|
||||
<span class="pf-label">{{ t('triggers.pattern.phase') }}</span>
|
||||
<select v-model="form.phase" class="pf-input" @change="emitFromForm">
|
||||
<option value="">{{ t('triggers.pattern.phaseAny') }}</option>
|
||||
<option value="spawned">spawned</option>
|
||||
<option value="enabled">enabled</option>
|
||||
<option value="disabled">disabled</option>
|
||||
<option value="terminated">terminated</option>
|
||||
<option value="crashed">crashed</option>
|
||||
</select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- workflow_completion — sourceWorkflowId + stateFilter -->
|
||||
<template v-else-if="patternType === 'workflow_completion'">
|
||||
<div class="pf-field">
|
||||
<span class="pf-label">{{ t('triggers.pattern.sourceWorkflowId') }}</span>
|
||||
<select v-model.number="form.sourceWorkflowId" class="pf-input" @change="emitFromForm">
|
||||
<option :value="undefined">{{ t('triggers.pattern.sourceWorkflowAny') }}</option>
|
||||
<option v-for="wf in availableWorkflows" :key="wf.id" :value="wf.id">
|
||||
#{{ wf.id }} — {{ wf.name || '(unnamed)' }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="pf-field">
|
||||
<span class="pf-label">{{ t('triggers.pattern.stateFilter') }}</span>
|
||||
<select v-model="form.stateFilter" class="pf-input" @change="emitFromForm">
|
||||
<option value="">{{ t('triggers.pattern.stateFilterAny') }}</option>
|
||||
<option value="completed">completed</option>
|
||||
<option value="succeeded">succeeded</option>
|
||||
<option value="failed">failed</option>
|
||||
</select>
|
||||
<small class="pf-hint">{{ t('triggers.pattern.stateFilterHint') }}</small>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- webhook — secret hint, no fields -->
|
||||
<template v-else-if="patternType === 'webhook'">
|
||||
<div class="pf-field">
|
||||
<span class="pf-label">{{ t('triggers.pattern.webhookHeader') }}</span>
|
||||
<p class="pf-prose">{{ t('triggers.pattern.webhookBody') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- raw JSON escape hatch — collapsible details so it doesn't get
|
||||
in the way for the typical operator who lets the form generate
|
||||
everything. -->
|
||||
<details class="pf-raw" :open="expanded">
|
||||
<summary @click.prevent="expanded = !expanded">
|
||||
{{ t('triggers.pattern.rawHeader') }}
|
||||
</summary>
|
||||
<textarea
|
||||
:value="modelValue"
|
||||
class="pf-input mono"
|
||||
rows="4"
|
||||
spellcheck="false"
|
||||
@input="onRawInput"
|
||||
/>
|
||||
<small v-if="rawError" class="pf-err">{{ rawError }}</small>
|
||||
</details>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, watch, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { WorkflowSummary } from '@/api'
|
||||
|
||||
interface Props {
|
||||
/** Pattern type (cron / channel_message / …) drives which form is shown. */
|
||||
patternType: string
|
||||
/** Current pattern_json string — also the only thing emitted back. */
|
||||
modelValue: string
|
||||
/** Optional list of published workflows for the workflow_completion form. */
|
||||
availableWorkflows?: WorkflowSummary[]
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
availableWorkflows: () => [],
|
||||
})
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', json: string): void
|
||||
(e: 'validation', err: Record<string, string>): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface FormState {
|
||||
cron?: string
|
||||
timezone?: string
|
||||
channelType?: string
|
||||
senderEquals?: string
|
||||
substring?: string
|
||||
agentId?: number | null
|
||||
phase?: string
|
||||
sourceWorkflowId?: number
|
||||
stateFilter?: string
|
||||
}
|
||||
|
||||
const form = reactive<FormState>({})
|
||||
const errors = reactive<Record<string, string>>({})
|
||||
const rawError = ref<string | null>(null)
|
||||
const expanded = ref(false)
|
||||
|
||||
// Parse the incoming JSON into the form state. Anything we can't parse
|
||||
// is treated as an empty draft — the operator can either fix the raw
|
||||
// JSON (escape hatch) or fill the form anew, which will re-serialize
|
||||
// over the broken text.
|
||||
function loadFromJson(json: string) {
|
||||
rawError.value = null
|
||||
if (!json || !json.trim()) {
|
||||
Object.keys(form).forEach((k) => delete (form as Record<string, unknown>)[k])
|
||||
return
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(json) as Record<string, unknown>
|
||||
Object.keys(form).forEach((k) => delete (form as Record<string, unknown>)[k])
|
||||
if (typeof parsed.cron === 'string') form.cron = parsed.cron
|
||||
if (typeof parsed.timezone === 'string') form.timezone = parsed.timezone
|
||||
if (typeof parsed.channelType === 'string') form.channelType = parsed.channelType
|
||||
if (typeof parsed.senderEquals === 'string') form.senderEquals = parsed.senderEquals
|
||||
if (typeof parsed.substring === 'string') form.substring = parsed.substring
|
||||
if (typeof parsed.agentId === 'number') form.agentId = parsed.agentId
|
||||
if (typeof parsed.phase === 'string') form.phase = parsed.phase
|
||||
if (typeof parsed.sourceWorkflowId === 'number') form.sourceWorkflowId = parsed.sourceWorkflowId
|
||||
if (typeof parsed.stateFilter === 'string') form.stateFilter = parsed.stateFilter
|
||||
} catch (e) {
|
||||
rawError.value = (e as Error).message
|
||||
}
|
||||
}
|
||||
|
||||
// Build the JSON string from the form, dropping empty / undefined fields
|
||||
// so the matcher's per-field "narrow if present" logic stays clean.
|
||||
function buildJsonFromForm(): string {
|
||||
const out: Record<string, unknown> = {}
|
||||
switch (props.patternType) {
|
||||
case 'cron':
|
||||
if (form.cron) out.cron = form.cron
|
||||
if (form.timezone) out.timezone = form.timezone
|
||||
break
|
||||
case 'channel_message':
|
||||
if (form.channelType) out.channelType = form.channelType
|
||||
if (form.senderEquals) out.senderEquals = form.senderEquals
|
||||
break
|
||||
case 'content_match':
|
||||
if (form.substring) out.substring = form.substring
|
||||
break
|
||||
case 'agent_lifecycle':
|
||||
if (typeof form.agentId === 'number' && !Number.isNaN(form.agentId)) {
|
||||
out.agentId = form.agentId
|
||||
}
|
||||
if (form.phase) out.phase = form.phase
|
||||
break
|
||||
case 'workflow_completion':
|
||||
if (typeof form.sourceWorkflowId === 'number') {
|
||||
out.sourceWorkflowId = form.sourceWorkflowId
|
||||
}
|
||||
if (form.stateFilter) out.stateFilter = form.stateFilter
|
||||
break
|
||||
case 'webhook':
|
||||
// Webhook is opaque pass-through; no fields.
|
||||
break
|
||||
}
|
||||
return JSON.stringify(out)
|
||||
}
|
||||
|
||||
// Local validation — required-field checks that mirror the matcher's
|
||||
// fail-closed rules so the operator hears about a mistake before the
|
||||
// trigger silently misses every event.
|
||||
function validate(): Record<string, string> {
|
||||
const errs: Record<string, string> = {}
|
||||
if (props.patternType === 'cron') {
|
||||
if (!form.cron || !form.cron.trim()) {
|
||||
errs.cron = t('triggers.pattern.errCronRequired')
|
||||
} else {
|
||||
// Quartz-style 6-field cron is what the scheduler accepts; allow
|
||||
// 5- or 6-field for ergonomics, just check field count.
|
||||
const parts = form.cron.trim().split(/\s+/)
|
||||
if (parts.length < 5 || parts.length > 7) {
|
||||
errs.cron = t('triggers.pattern.errCronShape')
|
||||
}
|
||||
}
|
||||
}
|
||||
if (props.patternType === 'content_match') {
|
||||
if (!form.substring || !form.substring.trim()) {
|
||||
errs.substring = t('triggers.pattern.errSubstringRequired')
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
function emitFromForm() {
|
||||
Object.keys(errors).forEach((k) => delete errors[k])
|
||||
Object.assign(errors, validate())
|
||||
emit('update:modelValue', buildJsonFromForm())
|
||||
emit('validation', { ...errors })
|
||||
}
|
||||
|
||||
function onRawInput(e: Event) {
|
||||
const next = (e.target as HTMLTextAreaElement).value
|
||||
rawError.value = null
|
||||
if (next.trim()) {
|
||||
try { JSON.parse(next) } catch (err) { rawError.value = (err as Error).message }
|
||||
}
|
||||
emit('update:modelValue', next)
|
||||
// Refresh form state to reflect whatever the operator typed.
|
||||
loadFromJson(next)
|
||||
}
|
||||
|
||||
// Re-derive form state when the parent flips patternType or the JSON
|
||||
// changes from outside (e.g. after save or when editing a different
|
||||
// trigger). We don't deep-watch form back into the JSON on these — that's
|
||||
// what emitFromForm covers.
|
||||
watch(
|
||||
() => [props.patternType, props.modelValue] as const,
|
||||
() => loadFromJson(props.modelValue),
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// Surface the no-op-when-empty error map at mount so the parent can
|
||||
// disable the save button right away if a required field is missing.
|
||||
watch(() => props.patternType, () => {
|
||||
Object.keys(errors).forEach((k) => delete errors[k])
|
||||
Object.assign(errors, validate())
|
||||
emit('validation', { ...errors })
|
||||
}, { immediate: true })
|
||||
|
||||
// Expose computed only so the template can read availableWorkflows
|
||||
// without verbosity.
|
||||
const availableWorkflows = computed(() => props.availableWorkflows ?? [])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pattern-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.pf-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.pf-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--mc-text-secondary, #555);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.pf-input {
|
||||
padding: 6px 8px;
|
||||
border: 1px solid var(--mc-border, rgba(0, 0, 0, 0.12));
|
||||
border-radius: 5px;
|
||||
background: var(--mc-bg, transparent);
|
||||
color: var(--mc-text-primary, inherit);
|
||||
font: inherit;
|
||||
font-size: 12.5px;
|
||||
outline: none;
|
||||
transition: border-color 0.12s ease;
|
||||
width: 100%;
|
||||
}
|
||||
.pf-input:focus {
|
||||
border-color: var(--mc-primary, #4084ff);
|
||||
}
|
||||
.pf-input.mono {
|
||||
font-family: 'JetBrains Mono', Consolas, monospace;
|
||||
font-size: 11.5px;
|
||||
}
|
||||
.pf-hint {
|
||||
font-size: 11px;
|
||||
opacity: 0.65;
|
||||
font-family: 'JetBrains Mono', Consolas, monospace;
|
||||
}
|
||||
.pf-err {
|
||||
font-size: 11px;
|
||||
color: var(--mc-danger, #c0392b);
|
||||
}
|
||||
.pf-prose {
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
color: var(--mc-text-secondary, #555);
|
||||
}
|
||||
.pf-raw {
|
||||
margin-top: 6px;
|
||||
border-top: 1px dashed var(--mc-border-light, rgba(0, 0, 0, 0.08));
|
||||
padding-top: 8px;
|
||||
}
|
||||
.pf-raw summary {
|
||||
font-size: 11px;
|
||||
color: var(--mc-text-tertiary, #888);
|
||||
cursor: pointer;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.pf-raw textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
</style>
|
||||
@ -2081,6 +2081,37 @@ export default {
|
||||
agent_lifecycle: 'Optional agentId and phase (spawned / terminated / crashed).',
|
||||
workflow_completion: 'Optional sourceWorkflowId and stateFilter (completed / failed / any).',
|
||||
},
|
||||
pattern: {
|
||||
cronExpression: 'Cron expression',
|
||||
cronExpressionPlaceholder: '0 0 * * * *',
|
||||
cronExpressionHint: '5 fields (min hour day month weekday) or 6 fields (with seconds). Each change bumps pattern_version.',
|
||||
cronTimezone: 'Timezone',
|
||||
cronTimezonePlaceholder: 'UTC',
|
||||
channelType: 'Channel type',
|
||||
channelTypeAny: 'Any channel',
|
||||
senderEquals: 'Sender equals',
|
||||
senderEqualsPlaceholder: 'Optional sender ID',
|
||||
senderEqualsHint: 'Empty matches any sender; filled narrows to that one sender.',
|
||||
substring: 'Substring',
|
||||
substringPlaceholder: 'order',
|
||||
substringHint: 'Case-insensitive. Empty strings refuse to fire (no broadcast trap).',
|
||||
agentId: 'Agent ID',
|
||||
agentIdPlaceholder: '0 means any',
|
||||
agentIdHint: 'Empty or 0 matches any agent.',
|
||||
phase: 'Lifecycle phase',
|
||||
phaseAny: 'Any phase',
|
||||
sourceWorkflowId: 'Source workflow',
|
||||
sourceWorkflowAny: 'Any upstream',
|
||||
stateFilter: 'Completion state',
|
||||
stateFilterAny: 'Any state',
|
||||
stateFilterHint: 'completed is an alias for succeeded; failed only matches failed terminal runs.',
|
||||
webhookHeader: 'Webhook pass-through',
|
||||
webhookBody: 'No filtering on the UI side — make sure the HTTP entry validates the webhook secret; every accepted request reaches the trigger.',
|
||||
rawHeader: 'Raw pattern_json (advanced)',
|
||||
errCronRequired: 'Cron expression cannot be empty',
|
||||
errCronShape: 'Cron must be 5–7 fields',
|
||||
errSubstringRequired: 'Substring cannot be empty or the trigger never fires',
|
||||
},
|
||||
unnamed: '(unnamed)',
|
||||
rateUnit: '{count}/min',
|
||||
enabled: 'enabled',
|
||||
|
||||
@ -1943,10 +1943,10 @@ export default {
|
||||
templates: {
|
||||
label: '插入模板',
|
||||
placeholder: '选择步骤模板',
|
||||
sequential: 'sequential(顺序步骤)',
|
||||
fan_out: 'fan_out(并行扇出)',
|
||||
sequential: '顺序步骤',
|
||||
fan_out: '并行扇出',
|
||||
collect: 'collect(汇聚结果)',
|
||||
conditional: 'conditional(条件分支)',
|
||||
conditional: '条件分支',
|
||||
await_approval: 'await_approval(等待审批)',
|
||||
dispatch_channel: 'dispatch_channel(消息分发)',
|
||||
write_memory: 'write_memory(写入记忆)',
|
||||
@ -2093,6 +2093,37 @@ export default {
|
||||
agent_lifecycle: '可选 agentId 和 phase(spawned / terminated / crashed)。',
|
||||
workflow_completion: '可选 sourceWorkflowId 和 stateFilter(completed / failed / any)。',
|
||||
},
|
||||
pattern: {
|
||||
cronExpression: 'Cron 表达式',
|
||||
cronExpressionPlaceholder: '0 0 * * * *',
|
||||
cronExpressionHint: '5 字段(分 时 日 月 周)或 6 字段(含秒)。修改后 pattern_version 自增。',
|
||||
cronTimezone: '时区',
|
||||
cronTimezonePlaceholder: 'Asia/Shanghai',
|
||||
channelType: '渠道类型',
|
||||
channelTypeAny: '任意渠道',
|
||||
senderEquals: '发送者匹配',
|
||||
senderEqualsPlaceholder: '可选,发送者 ID',
|
||||
senderEqualsHint: '留空表示任意发送者;填写后只匹配该发送者的消息。',
|
||||
substring: '子串匹配',
|
||||
substringPlaceholder: '订单 / order',
|
||||
substringHint: '不区分大小写。空字符串将拒绝触发(避免无意义广播)。',
|
||||
agentId: '智能体 ID',
|
||||
agentIdPlaceholder: '0 表示任意',
|
||||
agentIdHint: '留空或填 0 表示任意智能体。',
|
||||
phase: '生命周期阶段',
|
||||
phaseAny: '任意阶段',
|
||||
sourceWorkflowId: '上游工作流',
|
||||
sourceWorkflowAny: '任意上游',
|
||||
stateFilter: '完成状态',
|
||||
stateFilterAny: '任意状态',
|
||||
stateFilterHint: 'completed 等价于 succeeded;failed 仅匹配失败结束。',
|
||||
webhookHeader: 'Webhook 透传匹配',
|
||||
webhookBody: '此模式不在前端做过滤——请确保 HTTP 入口校验 webhook 密钥;通过校验的请求会全部进入触发器。',
|
||||
rawHeader: '原始 pattern_json(高级)',
|
||||
errCronRequired: 'Cron 表达式不能为空',
|
||||
errCronShape: 'Cron 表达式格式不正确(5 ~ 7 字段)',
|
||||
errSubstringRequired: '子串不能为空,否则触发器不会激活',
|
||||
},
|
||||
unnamed: '(未命名)',
|
||||
rateUnit: '{count}/分钟',
|
||||
enabled: '已启用',
|
||||
|
||||
@ -74,11 +74,15 @@
|
||||
<option value="webhook">webhook</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="span-2">{{ t('triggers.fields.patternJson') }}
|
||||
<textarea v-model="formState.patternJson" rows="3"
|
||||
:placeholder="t('triggers.fields.patternJsonPlaceholder')" />
|
||||
<small class="pattern-hint">{{ patternHint }}</small>
|
||||
</label>
|
||||
<div class="span-2">
|
||||
<span class="form-grid-label">{{ t('triggers.fields.patternJson') }}</span>
|
||||
<TriggerPatternForm
|
||||
v-model="formState.patternJson"
|
||||
:pattern-type="formState.patternType"
|
||||
:available-workflows="availableWorkflows"
|
||||
@validation="onPatternValidation"
|
||||
/>
|
||||
</div>
|
||||
<label>{{ t('triggers.fields.targetType') }}
|
||||
<!-- v0 only ships the workflow dispatcher; agent target is
|
||||
reserved for v1 and rejected by the API to avoid the
|
||||
@ -121,7 +125,12 @@
|
||||
</div>
|
||||
<footer>
|
||||
<button class="btn-ghost" @click="closeForm">{{ t('triggers.actions.cancel') }}</button>
|
||||
<button class="btn-primary" :disabled="busy" @click="save">{{ t('triggers.actions.save') }}</button>
|
||||
<button
|
||||
class="btn-primary"
|
||||
:disabled="busy || patternHasErrors"
|
||||
:title="patternHasErrors ? Object.values(patternErrors).join('; ') : ''"
|
||||
@click="save"
|
||||
>{{ t('triggers.actions.save') }}</button>
|
||||
</footer>
|
||||
</section>
|
||||
</div>
|
||||
@ -136,6 +145,7 @@ import { ElMessage } from 'element-plus'
|
||||
import { mcConfirm } from '@/components/common/useConfirm'
|
||||
import { triggerApi, type TriggerSummary, workflowApi, type WorkflowSummary } from '@/api'
|
||||
import { useWorkspaceStore } from '@/stores/useWorkspaceStore'
|
||||
import TriggerPatternForm from '@/components/workflow/TriggerPatternForm.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
@ -147,10 +157,11 @@ const availableWorkflows = computed(() =>
|
||||
workflows.value.filter((w) => w.latestRevisionId)
|
||||
)
|
||||
|
||||
const patternHint = computed(() => {
|
||||
const key = `triggers.patternHints.${formState.value.patternType}`
|
||||
return t(key, '')
|
||||
})
|
||||
const patternErrors = ref<Record<string, string>>({})
|
||||
function onPatternValidation(errs: Record<string, string>) {
|
||||
patternErrors.value = errs
|
||||
}
|
||||
const patternHasErrors = computed(() => Object.keys(patternErrors.value).length > 0)
|
||||
|
||||
const formOpen = ref(false)
|
||||
const editing = ref<TriggerSummary | null>(null)
|
||||
@ -389,6 +400,13 @@ watch(workspaceId, reload)
|
||||
opacity: 0.7;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.form-grid-label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
color: var(--mc-text-secondary, inherit);
|
||||
}
|
||||
.form-card footer {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user