
CX
29/9/2026
Tóm tắt
blog.share_blog
@cogover/sdk: Notification và emailTrang này thuộc API reference của @cogover/sdk.
notifications lưu một notification vào danh sách thông báo (biểu tượng chuông) của
từng người nhận và gửi nó trong web app, qua web push, mobile push và một bản sao
email, theo các kênh thông báo (notification channel) của workspace. Khác với
push message, notification vẫn nằm trong danh sách sau khi đã được
gửi, nên phù hợp với việc một người cần xử lý sau, ví dụ yêu cầu phê duyệt. Việc gửi
diễn ra bất đồng bộ và là best-effort: lời gọi hoàn tất khi Cogover đã tiếp nhận
notification, không phải khi nó tới thiết bị hay hộp thư.
type NotificationContentType = "text" | "html";
type NotificationDisplay = "SIMPLE" | "FULL";
type NotificationSender = "workspace" | "actor";
type NotificationLinkTarget = "SAME_TAB" | "NEW_TAB";
interface NotificationLink {
readonly url: string;
readonly openIn?: NotificationLinkTarget;
}
interface NotificationMessage {
readonly to: readonly string[];
readonly exclude?: readonly string[];
readonly title: string;
readonly content: string;
readonly contentType?: NotificationContentType;
readonly subtitle?: string;
readonly display?: NotificationDisplay;
readonly link?: NotificationLink;
readonly sender?: NotificationSender;
readonly channel?: string;
readonly email?: boolean;
readonly idempotencyKey?: string;
}
interface NotificationResult {
readonly requestId: string;
readonly notified: number;
readonly skippedPersonnelIds: readonly string[];
readonly duplicate: boolean;
}
interface NotificationsApi {
send(message: NotificationMessage): Promise<NotificationResult>;
}const result = await notifications.send({
to: approverIds,
exclude: ["actor"],
title: "Leave request waiting for your approval",
content: "A leave request of 3 days was submitted and needs your decision.",
link: { url: `/app/leave_request/${recordId}` },
channel: approvalChannelId,
idempotencyKey: `leave-approval:${recordId}`,
});
// result.notified: số tài khoản được gửi; result.skippedPersonnelIds: personnel chưa có tài khoảnnotifications.send(message): Promise<NotificationResult>
Người nhận. to liệt kê từ 1 đến 200 personnel ID; ID trùng được loại bỏ.
Notification được gửi tới tài khoản người dùng của từng personnel. Personnel chưa có
tài khoản người dùng được bỏ qua và trả về trong skippedPersonnelIds, còn
notified là số tài khoản được gửi sau khi đã loại trừ. exclude liệt kê personnel ID
không bao giờ nhận notification; giá trị đặc biệt "actor" là người dùng có hành động
khởi đầu execution hiện tại (người gọi HTTP route, người lưu record làm trigger chạy,
người có lời gọi đã enqueue lần chạy job). Execution không có người dùng thì không có
actor, nên "actor" không loại ai. Personnel ID trong to không tồn tại ném
NotFoundError với resource là "personnel" trước khi gửi bất cứ gì. Khi mọi người
nhận đều bị loại trừ hoặc bỏ qua, không có gì được gửi và notified là 0.
Nội dung và cách hiển thị.
title (bắt buộc, tối đa 200 ký tự) là tiêu đề của notification và là subject của
bản sao email.
content (bắt buộc, tối đa 16.384 ký tự) là nội dung. Với contentType: "text"
(mặc định), đây là văn bản thuần. Với "html", nội dung được rút gọn về định dạng
cơ bản trước khi lưu — đoạn văn, tiêu đề, danh sách, bảng, liên kết và nhấn mạnh;
script, style và hình ảnh bị loại bỏ — và vẫn phải còn chữ hiển thị được, nếu không
sẽ ném ValidationError.
subtitle (tối đa 200 ký tự) chỉ hiển thị ở chế độ "FULL". display mặc định là
"FULL" khi có subtitle và "SIMPLE" khi không có; subtitle đi kèm
display: "SIMPLE" ném ValidationError.
link là trang được mở khi người nhận bấm vào notification. url là một path của
web app Cogover bắt đầu bằng /, hoặc một URL tuyệt đối http hay https, tối đa
2.048 ký tự. openIn là "SAME_TAB" hoặc "NEW_TAB"; mặc định "SAME_TAB" với
path và "NEW_TAB" với URL tuyệt đối.
sender là "workspace" (mặc định) hoặc "actor", hiển thị tên và ảnh đại diện
của người dùng có hành động khởi đầu execution. "actor" trong execution không có
người dùng ném ValidationError.
Kênh thông báo và bản sao email. channel là record ID của một notification
channel của workspace. Kênh quyết định hình thức gửi nào được bật — trong web app,
web push, mobile push và email — và mỗi người dùng có thể ghi đè lựa chọn đó trong
cài đặt thông báo của riêng mình. Không có channel thì mọi hình thức gửi đều được
bật. Kênh không tồn tại ném NotFoundError với resource là "notificationChannel";
workspace chưa thiết lập notification ném NotFoundError với resource là
"object" và resourceId là "notification_channel".
email: false bỏ bản sao email kể cả khi kênh bật email. Bản sao dùng title làm
subject và content làm nội dung, được gửi từ một địa chỉ thông báo của Cogover chứ
không phải từ hộp thư của workspace, và được tính vào hạn mức email thông báo hằng
ngày theo gói của workspace. Để gửi email từ hộp thư của workspace, dùng
email.send.
Idempotency. idempotencyKey bắt đầu bằng chữ cái hoặc chữ số và chứa tối đa 128
chữ cái, chữ số, ., _, : hoặc -. Lời gọi có key mà project đã dùng cho một
notification trong 7 ngày gần nhất sẽ không gửi gì và trả về kết quả của lời gọi đầu
tiên với duplicate: true. Lời gọi thực hiện trong lúc lời gọi đầu tiên cùng key vẫn
đang gửi ném CogoverApiError với code: "DUPLICATE_IN_PROGRESS"; hãy thử lại sau
giây lát. Lời gọi thất bại không giữ key, nên có thể thử lại. Key thuộc về project và
tách biệt với key của email.send. Trong trigger after-change hoặc job, vốn có thể
chạy nhiều hơn một lần, hãy tạo key từ trigger.changeId và record.id, hoặc từ
job.id hay một key nghiệp vụ. Cogover không so sánh nội dung: lời gọi dùng lại key với
người nhận hoặc nội dung khác vẫn trả về kết quả đầu tiên và không gửi gì.
notifications có trong script, route, trigger after-change, background job, lời
gọi inbound webhook và Development Session có quyền ghi. Trigger before-change là
chỉ đọc và nhận PermissionDeniedError có details.reason là
"TRIGGER_READ_ONLY"; preview chỉ đọc và Development Session chỉ đọc cũng từ chối.
RateLimitError và không gửi gì.
ValidationError trước khi gửi bất cứ gì. Mỗi lời gọi tính một capability call.
CogoverApiError với code
NOTIFICATIONS_DISABLED.
idempotencyKey.
email gửi email từ hộp thư của workspace: một hộp thư dùng chung của workspace, hoặc
hộp thư cá nhân mặc định của người dùng có hành động khởi đầu execution. Identity
policy đã duyệt của Project quyết định project được dùng những hộp thư nào. Khi có
record, email còn được ghi nhận là một hoạt động email trên timeline của record đó,
giống email gửi từ trang record. Việc gửi diễn ra bất đồng bộ: lời gọi hoàn tất khi
Cogover đã tiếp nhận email để gửi, và lỗi gửi xảy ra sau đó không được báo lại cho
script.
type EmailSender = { readonly mailbox: string } | "actor";
type EmailRecipient =
| string
| { readonly email: string; readonly name?: string }
| { readonly personnelId: string };
interface EmailRecordLink<ObjectSlug extends string = string> {
readonly object: ObjectSlug;
readonly recordId: string;
}
interface EmailAttachment<ObjectSlug extends string = string> {
readonly object: ObjectSlug;
readonly recordId: string;
readonly field: string;
}
type EmailMessage<ObjectSlug extends string = string> = {
readonly from: EmailSender;
readonly to?: readonly EmailRecipient[];
readonly cc?: readonly EmailRecipient[];
readonly bcc?: readonly EmailRecipient[];
readonly subject: string;
readonly record?: EmailRecordLink<ObjectSlug>;
readonly recordEmailFields?: readonly string[];
readonly attachments?: readonly EmailAttachment<ObjectSlug>[];
readonly appendSignature?: boolean;
readonly idempotencyKey?: string;
} & (
| { readonly html: string; readonly text?: never }
| { readonly text: string; readonly html?: never }
);
type EmailDelivery = "activity" | "direct";
interface EmailSendResult {
readonly requestId: string;
readonly delivery: EmailDelivery;
readonly recipients: number;
readonly duplicate: boolean;
}
type EmailSenderKind = "workspace" | "actor";
interface EmailSenderInfo {
readonly id: string;
readonly kind: EmailSenderKind;
readonly email: string;
readonly displayName: string;
}
interface EmailApi<Schema extends object = EffectiveWorkspaceObjects> {
send(message: EmailMessage<StringKeyOf<Schema>>): Promise<EmailSendResult>;
senders(): Promise<readonly EmailSenderInfo[]>;
}const result = await email.send({
from: { mailbox: "EMWXXXXXXXXXXXX" },
bcc: ["sales-archive@example.com"],
subject: `Quote ${quote.fields.code}`,
html: "<p>Dear customer,</p><p>Please find our quote attached.</p>",
record: { object: "quote", recordId: quote.id },
recordEmailFields: ["contact_email"],
attachments: [{ object: "quote", recordId: quote.id, field: "quote_pdf" }],
idempotencyKey: `quote-email:${quote.id}`,
});
// result.delivery === "activity": email xuất hiện trên timeline của báo giá.email.send(message): Promise<EmailSendResult>
Người gửi. from: { mailbox: id } gửi từ một hộp thư dùng chung của workspace;
from: "actor" gửi từ hộp thư cá nhân mặc định của người dùng có hành động khởi đầu
execution. Người gửi phải được phép trong mục email của identity policy đã duyệt
của Project: workspaceMailboxIds liệt kê các hộp thư dùng chung và
allowActorMailbox cho phép "actor". Giống phần còn lại của policy, mục này chỉ áp
dụng cho các caller được chọn trong callerPersonnelIds, và cho execution không có
người dùng chỉ khi policy đặt allowInternalSystem: true. Project có policy không có
mục email thì không gửi được email. Người gửi không được phép ném
PermissionDeniedError có details.reason là "EMAIL_SENDER_NOT_GRANTED"; bước kiểm
tra này diễn ra trước mọi bước tra cứu khác. Hộp thư không tồn tại hoặc đã bị vô hiệu
hoá, và actor không có hộp thư cá nhân đang hoạt động, ném NotFoundError với
resource là "mailbox". "actor" trong execution không có người dùng ném
ValidationError.
Người nhận.
to, cc và bcc nhận một chuỗi địa chỉ, { email, name? } hoặc
{ personnelId }. Personnel được đổi thành email công việc của họ, hoặc email của
tài khoản người dùng khi không có email công việc; personnel không có cả hai ném
ValidationError, và personnel không tồn tại ném NotFoundError với resource là
"personnel".
name là một dòng, tối đa 200 ký tự.
to phải có ít nhất một người nhận, trừ khi có recordEmailFields.
Nội dung. subject là bắt buộc: một dòng, tối đa 255 ký tự. Cung cấp đúng một
trong html và text; nội dung tối đa 192 KB (196.608 byte) theo UTF-8, đo trên dạng
đã mã hoá JSON: mỗi dấu ngoặc kép, dấu gạch chéo ngược hoặc ký tự điều khiển như xuống
dòng được tính theo chuỗi escape của nó (\" là 2 byte, \n là 2 byte, \u0001 là 6
byte). Nội dung dài hơn ném ValidationError trước khi gọi.
Record. record: { object, recordId } ghi nhận email là một hoạt động email trên
timeline của record đó: Cogover theo dõi lượt mở và lượt bấm liên kết, gom các thư
trả lời vào cùng luồng, và result.delivery là "activity". Record phải đọc được
bằng danh tính mặc định của execution: record không tồn tại ném NotFoundError với
resource là "record", còn record mà danh tính đó không có quyền đọc ném
PermissionDeniedError. Không có record thì email chỉ được gửi từ hộp thư và
delivery là "direct". Các tuỳ chọn sau cần record; thiếu record chúng ném
ValidationError:
recordEmailFields liệt kê tối đa 20 field email của record đó, địa chỉ trong các
field này được thêm vào to. Field không phải field email ném ValidationError.
attachments đính kèm mọi file lưu trong file field field của từng record được
liệt kê: tối đa 10 file và tổng cộng 20 MB. Các record phải đọc được bằng danh tính
mặc định; field không phải file field ném ValidationError.
appendSignature: true thêm chữ ký email mặc định của người gửi. Tuỳ chọn này cần
hộp thư cá nhân, tức from: "actor"; nếu không sẽ ném ValidationError.
Kết quả. requestId định danh lần gửi. recipients đếm các địa chỉ trong to,
cc và bcc sau khi loại trùng; địa chỉ của recordEmailFields được xác định sau
nên không được đếm. duplicate là true khi một lời gọi trước đó cùng
idempotencyKey đã gửi email.
Idempotency. idempotencyKey theo cùng quy tắc với
notification: lời gọi lặp lại cùng key trong 7 ngày trả về kết quả
đầu tiên với duplicate: true thay vì gửi lại, và lời gọi lặp lại trong lúc lời gọi
đầu tiên vẫn đang gửi ném CogoverApiError với code: "DUPLICATE_IN_PROGRESS". Key
của email tách biệt với key của notification. Giống notification, nội dung không được
so sánh: dùng lại key cho một email khác vẫn trả về kết quả đầu tiên và không gửi gì.
email.senders(): Promise<readonly EmailSenderInfo[]>
Trả về các hộp thư đang hoạt động mà execution này được dùng để gửi: một mục
kind: "workspace" cho mỗi hộp thư dùng chung đang hoạt động được policy áp dụng liệt
kê trong workspaceMailboxIds, dùng dưới dạng { mailbox: id }, và, khi policy đặt
allowActorMailbox và actor có hộp thư cá nhân đang hoạt động, tối đa một mục
kind: "actor" mô tả hộp thư mà from: "actor" sử dụng. Danh sách rỗng khi policy
không cấp người gửi nào cho execution này. Dùng method này để hiển thị các người gửi
khả dụng hoặc chọn người gửi theo địa chỉ email thay vì hard-code ID. Method không
gửi gì, nên cũng dùng được trong preview chỉ đọc và Development Session chỉ đọc.
email có trong script, route, trigger after-change, background job, lời gọi
inbound webhook và Development Session. Trigger before-change nhận
PermissionDeniedError có details.reason là "TRIGGER_READ_ONLY" với cả hai
method; preview chỉ đọc và Development Session chỉ đọc từ chối email.send.
RateLimitError và không gửi gì. Một lời gọi là một email, bất kể có bao nhiêu
người nhận.
ValidationError trước khi gửi bất cứ gì. Mỗi lời gọi tính một capability call.
CogoverApiError với code
EMAIL_DISABLED.
idempotencyKey.

blog.blogs_related
Bắt đầu đổi mới phương thức vận hành và tự chủ hệ thống quản trị công việc của bạn
CÔNG TY CỔ PHẦN STRINGEE
Văn phòng