diff --git a/docs/commands.zh-CN.md b/docs/commands.zh-CN.md index 611cbd8..6d50e35 100644 --- a/docs/commands.zh-CN.md +++ b/docs/commands.zh-CN.md @@ -140,6 +140,7 @@ mailly workflows publish [--in-flight-policy continue] mailly workflows pause|resume|archive mailly workflows runs [--status ] mailly workflows run +mailly workflows cancel --reason mailly templates list mailly templates create --key --name --file template.json @@ -541,6 +542,8 @@ mailly workflows runs trial-onboarding --json mailly workflows run trial-onboarding --json ``` +需要停掉某一条在途 Run(`queued`/`running`/`waiting`)时,用 `mailly workflows cancel --reason `。Run 会变成 `terminated`,`exit_reason` 为 `cancelled`,不再执行后续 delay、条件或发送;已经发出的邮件不会撤回,同一 Workflow 的其他 Run 不受影响。对已结束的 Run 重复取消不会改动任何记录。取消原因和时间记录在 `mailly workflows run ` 的日志里。 + Variables mapping 支持 `contact`、`trigger_event`、`latest_event`(必须指定 `event`)和 `workflow_config`。Contact 的 `locale` 会按 exact → language → Template default 回退。`--file -` 可从 stdin 读取 JSON。重复上报相同 `event_id` 和相同不可变内容会返回成功且不会重复触发;同 ID 但内容不同会返回 `409`。 Contact 以 `external_id` 为业务身份:新事件和 `contacts upsert` 可以更新邮箱及 custom fields,send 始终读取当前邮箱或已冻结 Campaign snapshot 中的邮箱;同一 Workspace 内两个 Contact 不能占用同一邮箱。`contacts delete` 是保留历史归因的软删除,并自动加入 Workspace suppression;恢复前必须先显式解除 suppression,再以 `status: "active"` upsert,不能靠换 Contact ID 绕过退订。 diff --git a/src/client.ts b/src/client.ts index fe3c59c..5acc91b 100644 --- a/src/client.ts +++ b/src/client.ts @@ -649,6 +649,14 @@ export class MaillyClient { ); } + cancelWorkflowRun(workflowKey: string, runId: string, input: { reason: string }) { + return this.request( + 'POST', + `/workflows/${encodeURIComponent(workflowKey)}/runs/${encodeURIComponent(runId)}/cancel`, + { body: input }, + ); + } + campaignAction(campaignKey: string, action: 'pause' | 'resume' | 'cancel' | 'complete') { return this.request('POST', `/campaigns/${encodeURIComponent(campaignKey)}/${action}`); } diff --git a/src/commands/workflows.ts b/src/commands/workflows.ts index fff70c9..e1171fb 100644 --- a/src/commands/workflows.ts +++ b/src/commands/workflows.ts @@ -142,5 +142,31 @@ export function registerWorkflows(program: Command) { send.statusReason ?? '-', ])); } + if (res.logs?.length) { + info(''); + printTable(['Time', 'Step', 'Action', 'Outcome', 'Reason'], res.logs.map((log: any) => [ + log.createdAt, + log.stepIndex == null ? '-' : String(log.stepIndex), + log.action, + log.outcome, + log.details?.reason ?? '-', + ])); + } + }); + + workflows + .command('cancel') + .description('Cancel one queued, running or waiting run; emails already sent are not recalled') + .argument('') + .argument('') + .requiredOption('--reason ', 'Why the run is cancelled, kept in the run log') + .action(async (workflowKey, runId, opts, command) => { + const res = await (await clientFor(command)).cancelWorkflowRun(workflowKey, runId, { reason: opts.reason }); + if (getOutputFormat() === 'json') return printJSON(res); + if (res.cancelled) success(`Run ${runId} cancelled`); + else info(`Run ${runId} had already ended; nothing changed`); + info(` Status: ${res.run.status}`); + if (res.run.exitReason) info(` Exit reason: ${res.run.exitReason}`); + if (res.platformFailures?.length) info(` Platform operation failures: ${res.platformFailures.length}`); }); }