Bỏ qua để đến nội dung

Automation engine

  • Thư mụcbackend/extensions/helpdesk/src/endpoints/services/automation/
    • engine.ts runAutomationForEvent, runGuarded, loadConversation
    • conditions.ts evaluateCondition/evaluateConditions — pure switch
    • actions.ts runAction — pure switch, không dynamic code
engine.ts
export async function runAutomationForEvent(
ctx: AppContext,
accountability: any,
trigger: AutomationTrigger,
conversationId: number,
event: EventContext = {},
): Promise<void> {
const conversation = await loadConversation(ctx, conversationId);
if (!conversation) return;
const rules = await ctx.database('hd_automation_rules')
.where({ trigger, is_active: true });
if (!rules.length) return;
// ...
}

AutomationTrigger gồm 4 giá trị: conversation_created · message_received · status_changed · sla_warning.

engine.ts — loadConversation
async function loadConversation(ctx: AppContext, conversationId: number) {
const conv = await ctx.database('hd_conversations').where('id', conversationId).first();
if (!conv) return null;
const labelRows = await ctx.database('hd_conversation_labels')
.where('hd_conversations_id', conversationId)
.pluck('hd_labels_id');
conv.label_ids = labelRows;
return conv;
}

Đọc raw Knex (không qua ItemsService) — không bao giờ tự fire event nên an toàn để đọc trong lúc đang xử lý một event khác. Inbox được load thêm nếu hội thoại có inbox_id (cần cho condition channel_type/business_hours).

conditions.ts là bản port verbatim từ client app/utils/automation-conditions.ts để đảm bảo parity client/server — chỉ khác chỗ server coi id là raw số từ DB nên coerce String() hai chiều trước khi so sánh.

conditions.ts
export function evaluateConditions(conditions: AutomationCondition[], ctx: ConditionContext): boolean {
if (!conditions.length) return true; // không có condition = luôn khớp
return conditions.every((cond) => evaluateCondition(cond, ctx)); // AND
}
Field Operator hỗ trợ Nguồn dữ liệu
inbox_id eq neq in not_in conversation.inbox_id
priority eq neq in not_in conversation.priority
status eq neq in not_in conversation.status
content contains eq neq regex message.content (chỉ có ở trigger message_received)
label_ids in not_in eq neq conversation.label_ids (hydrated)
assignee_id eq neq in not_in conversation.assignee_id
business_hours is_open is_closed inbox.working_hours_idđơn giản hoá, chỉ check field có tồn tại, không tính lịch thật
channel_type eq neq in not_in inbox.channel_type
conditions.ts — content.regex
case 'content': {
const text = message?.content ?? '';
switch (op) {
case 'regex': {
try { return new RegExp(String(value), 'i').test(text); }
catch { return false; }
}
// contains / eq / neq
}
}

business_hours hiện là stub đơn giản hoá:

conditions.ts
function isBusinessHoursOpen(ctx: ConditionContext): boolean {
if (!ctx.inbox?.working_hours_id) return false;
if (!ctx.currentTime) return false;
// Simplified: we just use the current time field; real check done in auto-reply composable
return true;
}

actions.ts — 6 loại action, mỗi loại map tới một write cụ thể (ItemsService hoặc raw Knex cho junction):

Action Params Write
assign agent_id hd_conversations.assignee_id qua ItemsService
label label_id, op: add|remove hd_conversation_labels (raw Knex, dedupe trước khi insert)
set_priority priority (phải nằm trong VALID_PRIORITIES) hd_conversations.priority
set_status status (phải nằm trong VALID_STATUSES) hd_conversations.status
send_template template_id, vars tạo hd_messages outgoing, content_type: template, thay {{var}}
reply body hoặc canned_id tạo hd_messages outgoing, content_type: text
engine.ts — vòng lặp rule
for (const rule of rules) {
const conditions = parseJson(rule.conditions) ?? [];
const actions: AutomationAction[] = parseJson(rule.actions) ?? [];
if (!evaluateConditions(conditions, condCtx)) continue;
for (const action of actions) {
try {
await runAction(ctx, accountability, conversation, action);
} catch (err: any) {
ctx.log.error({ err: err?.message, rule: rule.id, action: action?.type }, 'helpdesk automation action failed');
}
}
await ctx.database('hd_automation_rules').where('id', rule.id).increment('run_count', 1);
}

Mỗi action trong 1 rule chạy trong try/catch riêng — một action lỗi (ví dụ template_id không tồn tại) không chặn các action còn lại của cùng rule, và cũng không chặn rule tiếp theo.

run_count tăng bằng .increment('run_count', 1) — raw Knex atomic increment, không phải đọc-rồi-ghi (tránh lost-update khi nhiều event fire gần nhau), và cố ý không đi qua ItemsService để không fire event hd_automation_rules.update giả (đây là counter denormalized, không phải logical edit).

reply/send_template gọi deliverOutgoing() ngay sau khi tạo message outgoing (xem Outbound) — vì action không có bước link-attachment như flow tạo message thủ công của agent, không cần đợi.

Action assign/label/set_priority/set_status update chính hội thoại đang trigger automation qua ItemsService — việc này tự fire lại event hd_conversations.items.update, có thể trigger status_changed automation lần nữa, và nếu rule đó lại set status → vòng lặp vô hạn.

engine.ts — runGuarded
const automationInFlight = new Set<number>();
export async function runGuarded(
ctx: AppContext,
trigger: AutomationTrigger,
conversationId: number,
event: EventContext = {},
): Promise<void> {
if (!conversationId || automationInFlight.has(conversationId)) return;
automationInFlight.add(conversationId);
try {
await runAutomationForEvent(ctx, SYSTEM_ACCOUNTABILITY, trigger, conversationId, event);
} finally {
automationInFlight.delete(conversationId);
}
}

automationInFlightmột Set module-level dùng chung cho mọi entry point (hooks hd_messages/hd_conversations VÀ cron SLA sla_warning) — một run đang guard hội thoại #42 từ hook cũng chặn cron SLA re-enter cùng hội thoại #42, không phải 2 guard độc lập.

  • SLA & Snooze — trigger sla_warning gọi qua runGuarded từ cron
  • Outbound (SMTP)deliverOutgoing được action reply/send_template tái dùng
  • Hooks (filter/action) — nơi các trigger conversation_created/message_received/status_changed được fire