Thêm channel mới
Vị trí mã nguồn
Phần tiêu đề “Vị trí mã nguồn”Thư mụcadmin/packages/helpdesk/app/
- composables/use-helpdesk-types.ts interface ChannelDescriptor / ChannelCapabilities
Thư mụcchannels/
Thư mụcemail/ descriptor.ts + inbox-config-form.vue + message-renderer.vue + composer-extras.vue
- …
Thư mụcwhatsapp/ + templates-tab.vue (inboxEditTabs)
- …
- plugins/channels.ts registerChannel(…) cho từng descriptor
Thư mụcbackend/extensions/helpdesk/src/endpoints/services/inbound/
- imap-source.ts poll cron — CHỈ CÓ cho email/IMAP
- pipeline.ts processInboundEmail dùng chung mọi provider email
- normalized.ts · sanitize.ts · crypto.ts · security-alert.ts
ChannelDescriptor — interface
Phần tiêu đề “ChannelDescriptor — interface”Khai báo trong use-helpdesk-types.ts, không phải file descriptor.ts riêng như tên gọi hay nhầm:
export interface ChannelCapabilities { attachments: boolean privateNotes: boolean richText: boolean cc_bcc: boolean requiresTemplate?: (conversation: Conversation) => boolean maxMessageLength?: number}
export interface ChannelInboxEditTab { id: string label: string component: Component}
export interface ChannelDescriptor { id: string label: string icon: string identityField: "email" | "phone" inboxConfigForm: Component inboxConfigDefaults: () => Record<string, any> validateInboxConfig: (config: Record<string, any>) => string[] messageRenderer?: Component composerExtras?: Component inboxEditTabs?: ChannelInboxEditTab[] capabilities: ChannelCapabilities generateMockMessage?: (contact: Contact) => Partial<Message>}Ví dụ 1: email descriptor
Phần tiêu đề “Ví dụ 1: email descriptor”import { defineAsyncComponent } from "vue"import type { ChannelDescriptor } from "../../composables/use-helpdesk-types"
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
export const emailDescriptor: ChannelDescriptor = { id: "email", label: "Email", icon: "i-ph-envelope-light", identityField: "email",
inboxConfigForm: defineAsyncComponent(() => import("./inbox-config-form.vue")), messageRenderer: defineAsyncComponent(() => import("./message-renderer.vue")), composerExtras: defineAsyncComponent(() => import("./composer-extras.vue")),
inboxConfigDefaults() { return { provider: "forward", email: "", reply_to: "", signature: "" } },
validateInboxConfig(config) { const errors: string[] = [] if (!config.email) errors.push("Email address is required") else if (!EMAIL_REGEX.test(config.email)) errors.push("Email address format is invalid") if (config.provider === "forward" && !config.forward_to) errors.push("Forward-to address is required when provider is Email Forward") if (config.provider === "imap_smtp") { if (!config.imap_host) errors.push("IMAP host is required") if (!config.smtp_host) errors.push("SMTP host is required") } return errors },
capabilities: { attachments: true, privateNotes: true, richText: true, cc_bcc: true },}Ví dụ 2: whatsapp — khác biệt so với email
Phần tiêu đề “Ví dụ 2: whatsapp — khác biệt so với email”Có thêm inboxEditTabs (tab Templates riêng) và capability requiresTemplate (24h window → bắt buộc HSM template):
export const whatsappDescriptor: ChannelDescriptor = { id: "whatsapp", label: "WhatsApp Business", icon: "i-ph-whatsapp-logo-light", identityField: "phone",
inboxConfigForm: defineAsyncComponent(() => import("./inbox-config-form.vue")), messageRenderer: defineAsyncComponent(() => import("./message-renderer.vue")), composerExtras: defineAsyncComponent(() => import("./composer-extras.vue")),
inboxEditTabs: [ { id: "templates", label: "Templates", component: defineAsyncComponent(() => import("./templates-tab.vue")), }, ],
inboxConfigDefaults() { return { provider: "twilio", account_sid: "", auth_token: "", sender_phone: "", business_display_name: "" } },
capabilities: { attachments: true, privateNotes: true, richText: false, cc_bcc: false, requiresTemplate: (conversation) => { return Date.now() > ((conversation.channel_meta?.window_expires_at as number | undefined) ?? 0) }, maxMessageLength: 4096, },}requiresTemplate là hàm nhận conversation, trả true khi cửa sổ 24h đã hết hạn — composer dùng nó để bắt buộc chọn template thay vì soạn tự do.
Bước đăng ký
Phần tiêu đề “Bước đăng ký”import { emailDescriptor } from "../channels/email/descriptor"import { whatsappDescriptor } from "../channels/whatsapp/descriptor"
export default defineNuxtPlugin(() => { const { registerChannel } = useHelpdeskChannels() registerChannel(emailDescriptor) registerChannel(whatsappDescriptor)})- Tạo
app/channels/<id>/descriptor.tsimplementChannelDescriptor. - Tạo 3 component async:
inbox-config-form.vue(form cấu hình inbox theo channel),message-renderer.vue(render bubble tin nhắn),composer-extras.vue(phần composer đặc thù — template picker, CC/BCC, v.v). - Import descriptor + gọi
registerChannel(...)trongapp/plugins/channels.ts. - Set đúng
identityField("email"hoặc"phone"— quyết định field nào trênc_contactsdùng làm định danh requester cho channel này) và khaicapabilitiestrung thực (composer/renderer đọccapabilitiesđể bật/tắt UI).
Nửa còn lại: backend inbound
Phần tiêu đề “Nửa còn lại: backend inbound”Nếu cần inbound thật cho channel mới, viết một “source” riêng theo khuôn imap-source.ts:
import { ImapFlow } from 'imapflow';import { simpleParser, type ParsedMail } from 'mailparser';import type { AppContext } from '../../context.js';import { processInboundEmail, type HdConnection } from './pipeline.js';import { normalizeFrom, type NormalizedInboundEmail } from './normalized.js';import { decrypt } from './crypto.js';
// ── IMAP-pull source (Phase 1) ──// A per-minute cron (context.schedule, multi-instance safe) polls each active// IMAP connection, fetches mail newer than the stored UID watermark, parses via// mailparser, and runs the shared processInboundEmail pipeline.Một source mới (cron poll hoặc webhook) cần làm đủ các việc sau trước khi có thể tái dùng hạ tầng chung:
- Fetch/receive raw message từ provider (poll như IMAP, hoặc webhook như Twilio/WhatsApp Cloud API).
- Decrypt credential đã lưu trên
hd_connections— dùngcrypto.ts(decrypt()), không tự chế mã hoá khác. - Normalize identity người gửi (tương đương
normalizeFromtrongnormalized.ts) để khớp/tạoc_contactsqua contact-choke-point. - Sanitize nội dung nếu là HTML/rich content (tương đương
sanitize.ts). - Đẩy vào pipeline dùng chung kiểu
processInboundEmail— resolve inbox, loop-guard, sender filter, dedup, threading, tạo/appendhd_messages, trigger automation. Viết pipeline riêng cho channel mới hoặc tái cấu trúcpipeline.tsđể tổng quát hoá tuỳ độ giống nhau giữa 2 channel. - Bắn cảnh báo bảo mật nếu channel có nguy cơ nội dung độc hại (tương đương
security-alert.ts).
Đọc tiếp
Phần tiêu đề “Đọc tiếp”- Inbound: IMAP → ticket — pipeline 8 bước hiện có, dùng làm khuôn cho channel mới
- Outbound (SMTP) — chiều gửi đi cho email, cần tương đương cho channel mới
- Thêm sub-collection hd_* mới — nếu channel cần bảng phụ (v.d. template HSM)