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

Thêm channel mới

  • 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

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:

app/composables/use-helpdesk-types.ts:271-299
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>
}
app/channels/email/descriptor.ts (rút gọn)
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):

app/channels/whatsapp/descriptor.ts (rút gọn)
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.

app/plugins/channels.ts (đầy đủ)
import { emailDescriptor } from "../channels/email/descriptor"
import { whatsappDescriptor } from "../channels/whatsapp/descriptor"
export default defineNuxtPlugin(() => {
const { registerChannel } = useHelpdeskChannels()
registerChannel(emailDescriptor)
registerChannel(whatsappDescriptor)
})
  1. Tạo app/channels/<id>/descriptor.ts implement ChannelDescriptor.
  2. 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).
  3. Import descriptor + gọi registerChannel(...) trong app/plugins/channels.ts.
  4. Set đúng identityField ("email" hoặc "phone" — quyết định field nào trên c_contacts dùng làm định danh requester cho channel này) và khai capabilities trung thực (composer/renderer đọc capabilities để bật/tắt UI).

Nếu cần inbound thật cho channel mới, viết một “source” riêng theo khuôn imap-source.ts:

backend/extensions/helpdesk/src/endpoints/services/inbound/imap-source.ts (đầu file)
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:

  1. Fetch/receive raw message từ provider (poll như IMAP, hoặc webhook như Twilio/WhatsApp Cloud API).
  2. Decrypt credential đã lưu trên hd_connections — dùng crypto.ts (decrypt()), không tự chế mã hoá khác.
  3. Normalize identity người gửi (tương đương normalizeFrom trong normalized.ts) để khớp/tạo c_contacts qua contact-choke-point.
  4. Sanitize nội dung nếu là HTML/rich content (tương đương sanitize.ts).
  5. Đẩy vào pipeline dùng chung kiểu processInboundEmail — resolve inbox, loop-guard, sender filter, dedup, threading, tạo/append hd_messages, trigger automation. Viết pipeline riêng cho channel mới hoặc tái cấu trúc pipeline.ts để tổng quát hoá tuỳ độ giống nhau giữa 2 channel.
  6. 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).