Automation engine
Vị trí mã nguồn
Phần tiêu đề “Vị trí mã nguồn”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
Chữ ký gọi vào: runAutomationForEvent
Phần tiêu đề “Chữ ký gọi vào: runAutomationForEvent”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.
Load context: hội thoại + inbox + label
Phần tiêu đề “Load context: hội thoại + inbox + label”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).
Evaluate condition: pure switch, AND
Phần tiêu đề “Evaluate condition: pure switch, AND”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.
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 |
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á:
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;}Chạy action: pure switch, không dynamic code
Phần tiêu đề “Chạy action: pure switch, không dynamic code”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 |
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.
Chống vòng lặp: runGuarded
Phần tiêu đề “Chống vòng lặp: runGuarded”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.
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); }}automationInFlight là mộ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.
Đọc tiếp
Phần tiêu đề “Đọc tiếp”- SLA & Snooze — trigger
sla_warninggọi quarunGuardedtừ cron - Outbound (SMTP) —
deliverOutgoingđược actionreply/send_templatetái dùng - Hooks (filter/action) — nơi các trigger
conversation_created/message_received/status_changedđược fire