Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/commands.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ mailly workflows publish <key> [--in-flight-policy continue]
mailly workflows pause|resume|archive <key>
mailly workflows runs <key> [--status <status>]
mailly workflows run <key> <run-id>
mailly workflows cancel <key> <run-id> --reason <text>

mailly templates list
mailly templates create --key <key> --name <name> --file template.json
Expand Down Expand Up @@ -541,6 +542,8 @@ mailly workflows runs trial-onboarding --json
mailly workflows run trial-onboarding <run-id> --json
```

需要停掉某一条在途 Run(`queued`/`running`/`waiting`)时,用 `mailly workflows cancel <key> <run-id> --reason <text>`。Run 会变成 `terminated`,`exit_reason` 为 `cancelled`,不再执行后续 delay、条件或发送;已经发出的邮件不会撤回,同一 Workflow 的其他 Run 不受影响。对已结束的 Run 重复取消不会改动任何记录。取消原因和时间记录在 `mailly workflows run <key> <run-id>` 的日志里。

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 绕过退订。
Expand Down
8 changes: 8 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,14 @@ export class MaillyClient {
);
}

cancelWorkflowRun(workflowKey: string, runId: string, input: { reason: string }) {
return this.request<any>(
'POST',
`/workflows/${encodeURIComponent(workflowKey)}/runs/${encodeURIComponent(runId)}/cancel`,
{ body: input },
);
}

campaignAction(campaignKey: string, action: 'pause' | 'resume' | 'cancel' | 'complete') {
return this.request<any>('POST', `/campaigns/${encodeURIComponent(campaignKey)}/${action}`);
}
Expand Down
26 changes: 26 additions & 0 deletions src/commands/workflows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('<workflow-key>')
.argument('<run-id>')
.requiredOption('--reason <text>', '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}`);
});
}
Loading