@zenify-omni/chat-js 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +28 -0
  3. package/api/campaign.js +19 -0
  4. package/api/chat.js +518 -0
  5. package/api/chatroom.js +254 -0
  6. package/api/contact.js +45 -0
  7. package/api/crm-organization.js +101 -0
  8. package/api/customer.js +143 -0
  9. package/api/facebook-labels.js +11 -0
  10. package/api/field.js +11 -0
  11. package/api/file.js +17 -0
  12. package/api/filter.js +50 -0
  13. package/api/group.js +11 -0
  14. package/api/iam.js +23 -0
  15. package/api/index.js +54 -0
  16. package/api/orgTree.js +45 -0
  17. package/api/post.js +111 -0
  18. package/api/queue.js +15 -0
  19. package/api/report.js +422 -0
  20. package/api/send.js +21 -0
  21. package/api/session-event.js +30 -0
  22. package/api/session.js +249 -0
  23. package/api/settings/assign-task.js +10 -0
  24. package/api/settings/channel.js +136 -0
  25. package/api/settings/chat-layout.js +37 -0
  26. package/api/settings/chat-timeout.js +10 -0
  27. package/api/settings/common.js +32 -0
  28. package/api/settings/config.js +19 -0
  29. package/api/settings/index.js +24 -0
  30. package/api/settings/ivr.js +25 -0
  31. package/api/settings/queue-v3.js +56 -0
  32. package/api/settings/queue.js +48 -0
  33. package/api/settings/routing.js +32 -0
  34. package/api/settings/service.js +31 -0
  35. package/api/settings.js +567 -0
  36. package/api/shipping.js +19 -0
  37. package/api/shopee.js +21 -0
  38. package/api/smart-qc/index.js +4 -0
  39. package/api/smart-qc/keyword.js +10 -0
  40. package/api/tag.js +11 -0
  41. package/api/user.js +177 -0
  42. package/client.js +391 -0
  43. package/config/env.js +76 -0
  44. package/constants/advance-filter.js +358 -0
  45. package/constants/auto-assign-tag.js +13 -0
  46. package/constants/chat-message.js +423 -0
  47. package/constants/chat-room-tag.js +7 -0
  48. package/constants/chat-source.js +68 -0
  49. package/constants/chatbot.js +58 -0
  50. package/constants/common.js +4 -0
  51. package/constants/crm-organization.js +75 -0
  52. package/constants/distribute.js +8 -0
  53. package/constants/distribution-log.js +14 -0
  54. package/constants/events.js +27 -0
  55. package/constants/file-upload.js +86 -0
  56. package/constants/lead.js +111 -0
  57. package/constants/mask-data-config.js +16 -0
  58. package/constants/month.js +78 -0
  59. package/constants/quick-filter.js +49 -0
  60. package/constants/regex.js +34 -0
  61. package/constants/setting.js +62 -0
  62. package/constants/shipping.js +12 -0
  63. package/constants/social.js +21 -0
  64. package/constants/sortable-fields.js +146 -0
  65. package/constants/tenant_config.js +154 -0
  66. package/constants/use-query-key.js +470 -0
  67. package/constants/user.js +293 -0
  68. package/exceptions/axios-error.js +24 -0
  69. package/exceptions/https-error.js +28 -0
  70. package/exceptions/validator-error.js +20 -0
  71. package/lib/avatar-color.js +31 -0
  72. package/lib/i18n/index.js +85 -0
  73. package/lib/i18n/language-store.js +74 -0
  74. package/lib/i18n/locales/en/campaign.json +3 -0
  75. package/lib/i18n/locales/en/chat.json +656 -0
  76. package/lib/i18n/locales/en/common.json +1846 -0
  77. package/lib/i18n/locales/en/customer.json +20 -0
  78. package/lib/i18n/locales/en/lead.json +5 -0
  79. package/lib/i18n/locales/en/lumi.json +4 -0
  80. package/lib/i18n/locales/en/notification.json +107 -0
  81. package/lib/i18n/locales/en/org.json +4 -0
  82. package/lib/i18n/locales/en/posts.json +8 -0
  83. package/lib/i18n/locales/en/settings.json +39 -0
  84. package/lib/i18n/locales/vi/campaign.json +3 -0
  85. package/lib/i18n/locales/vi/chat.json +660 -0
  86. package/lib/i18n/locales/vi/common.json +1846 -0
  87. package/lib/i18n/locales/vi/customer.json +20 -0
  88. package/lib/i18n/locales/vi/lead.json +5 -0
  89. package/lib/i18n/locales/vi/lumi.json +4 -0
  90. package/lib/i18n/locales/vi/notification.json +107 -0
  91. package/lib/i18n/locales/vi/org.json +4 -0
  92. package/lib/i18n/locales/vi/posts.json +8 -0
  93. package/lib/i18n/locales/vi/settings.json +39 -0
  94. package/lib/i18n/server-label.js +33 -0
  95. package/lib/phone.js +70 -0
  96. package/lib/user-avatar.js +21 -0
  97. package/package.json +45 -0
  98. package/socket/index.js +30 -0
  99. package/transport-registry.js +77 -0
  100. package/utils/access-detail.js +119 -0
  101. package/utils/auth.js +205 -0
  102. package/utils/chunk-reload-guard.js +4 -0
  103. package/utils/concurrency-limiter.js +30 -0
  104. package/utils/crm-organization.js +0 -0
  105. package/utils/currency.js +14 -0
  106. package/utils/customers.js +42 -0
  107. package/utils/data.js +178 -0
  108. package/utils/date.js +144 -0
  109. package/utils/debounced-invalidate.js +15 -0
  110. package/utils/emoticon.js +134 -0
  111. package/utils/export.js +7 -0
  112. package/utils/field-layout.js +33 -0
  113. package/utils/filter-adapters.js +9 -0
  114. package/utils/filter.js +18 -0
  115. package/utils/formatters.js +6 -0
  116. package/utils/image-compression.js +169 -0
  117. package/utils/leads.js +13 -0
  118. package/utils/microlink.js +20 -0
  119. package/utils/org-tree.js +108 -0
  120. package/utils/request.js +461 -0
  121. package/utils/session-end.js +19 -0
  122. package/utils/string.js +154 -0
  123. package/utils/table-display-config.js +80 -0
  124. package/utils/validation.js +49 -0
package/api/user.js ADDED
@@ -0,0 +1,177 @@
1
+ import { requestApi, requestHubApi } from "../utils/request.js";
2
+ import { UserRole } from "../constants/user.js";
3
+ const SEARCH_USER_PAGE_SIZE = 50;
4
+ const findUser = (query) => requestApi({
5
+ method: "GET",
6
+ endpoint: `/v2/find-user`,
7
+ params: query
8
+ });
9
+ const updateUser = (userId, data) => {
10
+ return requestApi({
11
+ method: "PUT",
12
+ endpoint: `/v2/update-user/${userId}`,
13
+ data
14
+ });
15
+ };
16
+ const allUser = (query) => requestApi({
17
+ method: "get",
18
+ endpoint: "/v2/all-user",
19
+ params: query
20
+ });
21
+ const searchCcUser = async (params) => {
22
+ const { data } = await requestApi({
23
+ method: "get",
24
+ endpoint: "/v2/all-user",
25
+ params: {
26
+ ...params,
27
+ cc: true
28
+ }
29
+ });
30
+ return data ?? {};
31
+ };
32
+ const searchCcUserV3 = async (params) => {
33
+ const { data } = await requestApi({
34
+ method: "get",
35
+ endpoint: "/v3/all-user",
36
+ params: {
37
+ ...params,
38
+ cc: true
39
+ }
40
+ });
41
+ return data ?? {};
42
+ };
43
+ const searchUser = async ({ searchText, roles, ...args }) => {
44
+ const params = {
45
+ keyword: searchText.trim(),
46
+ roles: JSON.stringify(
47
+ Array.isArray(roles) ? roles : [
48
+ UserRole.ADMINISTRATOR,
49
+ UserRole.AGENT,
50
+ UserRole.SUPERVISOR,
51
+ UserRole.TEAM_LEADER
52
+ ]
53
+ ),
54
+ limit: SEARCH_USER_PAGE_SIZE,
55
+ ...args
56
+ };
57
+ const { data } = await requestApi({
58
+ method: "get",
59
+ endpoint: "/v2/all-user",
60
+ params
61
+ });
62
+ return data?.list ?? [];
63
+ };
64
+ const getUserTemplate = (userId) => requestApi({
65
+ method: "get",
66
+ endpoint: `/v2/templates/user/${userId}`
67
+ });
68
+ const getUserGroups = () => requestApi({
69
+ method: "get",
70
+ endpoint: `v2/all-group?target=user`
71
+ });
72
+ const resetPassword = ({ userId, data }) => {
73
+ return requestApi({
74
+ method: "post",
75
+ endpoint: `/v2/reset-password/${userId}`,
76
+ data
77
+ });
78
+ };
79
+ const getTenantConfig = ({ signal } = {}) => {
80
+ return requestApi({
81
+ method: "get",
82
+ endpoint: `/v2/my-account`,
83
+ signal
84
+ });
85
+ };
86
+ const getUserInfo = (userId) => {
87
+ return requestApi({
88
+ method: "get",
89
+ endpoint: `/v2/users/${userId}`
90
+ });
91
+ };
92
+ function activeUser({ id, data }) {
93
+ return requestApi({
94
+ method: "put",
95
+ endpoint: `/v2/active-user/${id}`,
96
+ data
97
+ });
98
+ }
99
+ function activeManyUser({ data }) {
100
+ return requestApi({
101
+ method: "post",
102
+ endpoint: `/v2/active-users`,
103
+ data
104
+ });
105
+ }
106
+ function disableUser({ id, data }) {
107
+ return requestApi({
108
+ method: "put",
109
+ endpoint: `/v2/disable-user/${id}`,
110
+ data
111
+ });
112
+ }
113
+ function disableManyUser({ data }) {
114
+ return requestApi({
115
+ method: "post",
116
+ endpoint: `/v2/disable-users`,
117
+ data
118
+ });
119
+ }
120
+ function getListAgents({ params }) {
121
+ return requestApi({
122
+ method: "get",
123
+ endpoint: "/v2/list-agents",
124
+ params
125
+ });
126
+ }
127
+ const getUsersV3 = ({ params }) => {
128
+ return requestHubApi({
129
+ method: "get",
130
+ endpoint: "/api/v3/users",
131
+ params
132
+ });
133
+ };
134
+ const getAvailableAgents = () => {
135
+ return requestHubApi({
136
+ method: "get",
137
+ endpoint: "/api/v3/users/available"
138
+ });
139
+ };
140
+ const getUserV3 = (id) => requestHubApi({
141
+ method: "get",
142
+ endpoint: `/api/v3/users/${id}`
143
+ }).then(({ data }) => data);
144
+ const updateUserV3 = (id, data) => requestHubApi({
145
+ method: "put",
146
+ endpoint: `/api/v3/users/${id}`,
147
+ data
148
+ }).then(({ data: data2 }) => data2);
149
+ const updateMyLanguage = (language) => requestApi({
150
+ method: "PATCH",
151
+ endpoint: "/v2/me/language",
152
+ data: { language }
153
+ });
154
+ export {
155
+ SEARCH_USER_PAGE_SIZE,
156
+ activeManyUser,
157
+ activeUser,
158
+ allUser,
159
+ disableManyUser,
160
+ disableUser,
161
+ findUser,
162
+ getAvailableAgents,
163
+ getListAgents,
164
+ getTenantConfig,
165
+ getUserGroups,
166
+ getUserInfo,
167
+ getUserTemplate,
168
+ getUserV3,
169
+ getUsersV3,
170
+ resetPassword,
171
+ searchCcUser,
172
+ searchCcUserV3,
173
+ searchUser,
174
+ updateMyLanguage,
175
+ updateUser,
176
+ updateUserV3
177
+ };
package/client.js ADDED
@@ -0,0 +1,391 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+ import { LOGOUT_EVENT } from "./constants/events.js";
5
+ import * as api from "./api/index.js";
6
+ import {
7
+ getUser,
8
+ setUser,
9
+ login as saveSession,
10
+ logout as clearSession,
11
+ decodeTokenPayload
12
+ } from "./utils/auth.js";
13
+ import { configureChatEnv, env } from "./config/env.js";
14
+ import { setTenantTz } from "./utils/date.js";
15
+ import {
16
+ createSocketFacade,
17
+ connectChatTransports,
18
+ disconnectChatTransports
19
+ } from "./transport-registry.js";
20
+ const SOCKET_IDENTITY_KEYS = ["_id", "name", "tenant_id", "agent_id", "role"];
21
+ const REQUIRED_ENV = {
22
+ API_EVS_URL: "m\u1ECDi request t\u1EDBi contact-center-be s\u1EBD \u0111i sai base"
23
+ };
24
+ const ChatClientStatus = Object.freeze({
25
+ IDLE: "idle",
26
+ AUTHENTICATING: "authenticating",
27
+ BOOTSTRAPPING: "bootstrapping",
28
+ CONNECTING: "connecting",
29
+ READY: "ready",
30
+ ERROR: "error",
31
+ DISPOSED: "disposed"
32
+ });
33
+ class ChatClientConflictError extends Error {
34
+ constructor() {
35
+ super(...arguments);
36
+ __publicField(this, "name", "ChatClientConflictError");
37
+ }
38
+ }
39
+ const throwIfAborted = (signal) => {
40
+ if (signal?.aborted) {
41
+ const e = new Error("Kh\u1EDFi t\u1EA1o b\u1ECB hu\u1EF7");
42
+ e.name = "AbortError";
43
+ throw e;
44
+ }
45
+ };
46
+ const fingerprint = (cfg) => JSON.stringify({
47
+ domain: cfg.domain ?? null,
48
+ username: cfg.username ?? null,
49
+ email: cfg.email ?? null,
50
+ hasPassword: !!cfg.password,
51
+ // Chỉ ghi CÓ/KHÔNG, không bao giờ đưa api key vào vân tay: chuỗi này đi vào
52
+ // thông báo lỗi và log.
53
+ hasApiKey: !!cfg.apiKey,
54
+ hasToken: !!cfg.token,
55
+ env: cfg.env ?? null,
56
+ locale: cfg.locale ?? null
57
+ });
58
+ let current = null;
59
+ const getChatClient = () => current;
60
+ const disposeChatClient = () => current?.dispose();
61
+ function createChatClient(config = {}) {
62
+ const fp = fingerprint(config);
63
+ if (current && current.status !== ChatClientStatus.DISPOSED) {
64
+ if (current.fingerprint === fp) return current;
65
+ throw new ChatClientConflictError(
66
+ "\u0110\xE3 c\xF3 m\u1ED9t ChatClient v\u1EDBi c\u1EA5u h\xECnh kh\xE1c \u0111ang s\u1ED1ng. M\u1ED9t trang = m\u1ED9t phi\xEAn. G\u1ECDi disposeChatClient() tr\u01B0\u1EDBc, ho\u1EB7c d\xF9ng l\u1EA1i client hi\u1EC7n c\xF3 qua getChatClient()."
67
+ );
68
+ }
69
+ const devBefore = env.DEV;
70
+ configureChatEnv(config.env ?? {});
71
+ if (config.env && "DEV" in config.env && config.env.DEV !== devBefore) {
72
+ console.warn(
73
+ "[chat-js] `DEV` truy\u1EC1n qua createChatClient() l\xE0 QU\xC1 MU\u1ED8N \u2014 zustand devtools \u0111\u1ECDc n\xF3 l\xFAc t\u1EA1o store, t\u1EE9c l\xFAc import g\xF3i. \u0110\u1EB7t globalThis.__CHAT_ENV__ = { DEV: true } tr\u01B0\u1EDBc m\u1ECDi import (banner c\u1EE7a bundler)."
74
+ );
75
+ }
76
+ for (const [key, consequence] of Object.entries(REQUIRED_ENV)) {
77
+ if (!env[key]) {
78
+ throw new Error(`[chat-js] thi\u1EBFu env \`${key}\` \u2014 ${consequence}.`);
79
+ }
80
+ }
81
+ const listeners = /* @__PURE__ */ new Map();
82
+ const disposers = /* @__PURE__ */ new Set();
83
+ let status = ChatClientStatus.IDLE;
84
+ let startPromise = null;
85
+ let context = {};
86
+ const emit = (event, detail) => {
87
+ for (const fn of listeners.get(event) ?? []) {
88
+ try {
89
+ fn(detail);
90
+ } catch (e) {
91
+ console.error(`[chat-js] handler "${event}" n\xE9m l\u1ED7i:`, e);
92
+ }
93
+ }
94
+ };
95
+ const setStatus = (next, detail) => {
96
+ if (status === next) return;
97
+ status = next;
98
+ config.onStatus?.(next, detail);
99
+ emit("status", { status: next, detail });
100
+ };
101
+ const assertAlive = () => {
102
+ if (status === ChatClientStatus.DISPOSED) {
103
+ throw new Error("[chat-js] client \u0111\xE3 dispose()");
104
+ }
105
+ };
106
+ async function signIn({ domain, username, password } = {}) {
107
+ assertAlive();
108
+ const creds = {
109
+ domain: domain ?? config.domain,
110
+ // BE nhận trường `email`; `username` chỉ là tên thân thiện ở API của ta.
111
+ email: username ?? config.username,
112
+ password: password ?? config.password
113
+ };
114
+ if (!creds.domain || !creds.email || !creds.password) {
115
+ throw new Error("[chat-js] signIn: thi\u1EBFu domain / username / password");
116
+ }
117
+ const res = await api.session.login(creds);
118
+ const payload = res?.data;
119
+ if (!payload?.token) throw new Error("[chat-js] \u0111\u0103ng nh\u1EADp th\u1EA5t b\u1EA1i");
120
+ saveSession({
121
+ ...payload.info,
122
+ token: payload.token,
123
+ refresh_token: payload.refresh_token
124
+ });
125
+ return getUser();
126
+ }
127
+ async function signInWithApiKey({ email, apiKey, ttlInSecond } = {}) {
128
+ assertAlive();
129
+ const creds = {
130
+ email: email ?? config.email,
131
+ apiKey: apiKey ?? config.apiKey,
132
+ ttlInSecond: ttlInSecond ?? config.ttlInSecond
133
+ };
134
+ if (!creds.email || !creds.apiKey) {
135
+ throw new Error("[chat-js] signInWithApiKey: thi\u1EBFu email / apiKey");
136
+ }
137
+ const res = await api.session.signInWithApiKey(creds);
138
+ const payload = res?.data?.data ?? res?.data;
139
+ if (!payload?.token) throw new Error("[chat-js] \u0111\u0103ng nh\u1EADp b\u1EB1ng api key th\u1EA5t b\u1EA1i");
140
+ saveSession({ ...payload.info, token: payload.token });
141
+ scheduleRenewal();
142
+ return getUser();
143
+ }
144
+ const RENEW_MARGIN_MS = 5 * 60 * 1e3;
145
+ const MAX_TIMEOUT_MS = 24 * 60 * 60 * 1e3;
146
+ let renewTimer = null;
147
+ function stopRenewal() {
148
+ if (renewTimer) clearTimeout(renewTimer);
149
+ renewTimer = null;
150
+ }
151
+ function scheduleRenewal() {
152
+ stopRenewal();
153
+ if (!config.apiKey || !config.email) return;
154
+ const exp = decodeTokenPayload(getUser()?.token)?.exp;
155
+ if (typeof exp !== "number") return;
156
+ const wait = Math.max(0, exp * 1e3 - RENEW_MARGIN_MS - Date.now());
157
+ if (wait > MAX_TIMEOUT_MS) {
158
+ renewTimer = setTimeout(scheduleRenewal, MAX_TIMEOUT_MS);
159
+ return;
160
+ }
161
+ renewTimer = setTimeout(async () => {
162
+ try {
163
+ await signInWithApiKey();
164
+ } catch (error) {
165
+ console.warn("[chat-js] gia h\u1EA1n token b\u1EB1ng api key th\u1EA5t b\u1EA1i", error);
166
+ emit("error", error);
167
+ }
168
+ }, wait);
169
+ }
170
+ disposers.add(stopRenewal);
171
+ async function resumeSession(token = config.token, { domain } = {}) {
172
+ assertAlive();
173
+ if (!token) throw new Error("[chat-js] resumeSession: thi\u1EBFu token");
174
+ const claims = decodeTokenPayload(token) ?? {};
175
+ saveSession({
176
+ token,
177
+ domain: domain ?? config.domain,
178
+ _id: claims.user_id != null ? String(claims.user_id) : claims.sub != null ? String(claims.sub) : void 0,
179
+ tenant_id: claims.tenant_id != null ? String(claims.tenant_id) : void 0
180
+ });
181
+ return getUser();
182
+ }
183
+ function pickActiveWorkspace(workspaces = [], permissions = {}, isSuperAdmin = false) {
184
+ if (!workspaces.length) return null;
185
+ const canUse = (w) => isSuperAdmin || permissions[w._id]?.length > 0;
186
+ const target = workspaces.find((w) => !w.is_lumi_workspace && !w.is_qa_workspace && canUse(w)) || workspaces.find(canUse) || workspaces[0];
187
+ return target?._id ?? null;
188
+ }
189
+ async function bootstrap({ signal } = {}) {
190
+ assertAlive();
191
+ const { data } = await api.user.getTenantConfig({ signal });
192
+ throwIfAborted(signal);
193
+ setTenantTz(data?.timezone);
194
+ const account = data?.user ?? {};
195
+ const permissions = account?.iam_role?.permissions ?? {};
196
+ const isSuperAdmin = !!account?.admin;
197
+ const workspaces = data?.workspaces ?? [];
198
+ hydrateSocketIdentity(data);
199
+ context = {
200
+ tenantConfig: data,
201
+ timezone: data?.timezone || null,
202
+ account,
203
+ permissions,
204
+ isSuperAdmin,
205
+ workspaces,
206
+ activeWorkspaceId: pickActiveWorkspace(workspaces, permissions, isSuperAdmin)
207
+ };
208
+ return context;
209
+ }
210
+ function hydrateSocketIdentity(data) {
211
+ const stored = getUser();
212
+ if (!stored) return;
213
+ const account = data?.user ?? {};
214
+ const filled = { ...stored };
215
+ const candidates = {
216
+ _id: account._id,
217
+ name: account.name,
218
+ role: account.role,
219
+ agent_id: account.agent_id,
220
+ tenant_id: data?._id,
221
+ email: account.email
222
+ };
223
+ let changed = false;
224
+ for (const [key, value] of Object.entries(candidates)) {
225
+ if (filled[key] == null && value != null) {
226
+ filled[key] = value;
227
+ changed = true;
228
+ }
229
+ }
230
+ if (changed) setUser(filled);
231
+ }
232
+ function assertSocketIdentity() {
233
+ const user = getUser();
234
+ const missing = SOCKET_IDENTITY_KEYS.filter((k) => user?.[k] == null);
235
+ if (missing.length) {
236
+ console.error(
237
+ `[chat-js] h\u1ED3 s\u01A1 phi\xEAn thi\u1EBFu ${missing.join(", ")} \u2014 socket s\u1EBD k\u1EBFt n\u1ED1i \u0111\u01B0\u1EE3c nh\u01B0ng server KH\xD4NG \u0111\u1ECBnh danh n\u1ED5i, n\xEAn b\u1EA1n s\u1EBD kh\xF4ng nh\u1EADn \u0111\u01B0\u1EE3c tin nh\u1EAFn n\xE0o v\xE0 c\u0169ng kh\xF4ng th\u1EA5y l\u1ED7i n\xE0o kh\xE1c. Ki\u1EC3m tra payload \u0111\u0103ng nh\u1EADp.`
238
+ );
239
+ }
240
+ return missing;
241
+ }
242
+ function connect() {
243
+ assertAlive();
244
+ if (!env.CHAT_URL) {
245
+ throw new Error(
246
+ "[chat-js] thi\u1EBFu env `CHAT_URL` \u2014 socket s\u1EBD b\xE1o connected nh\u01B0ng kh\xF4ng nh\u1EADn \u0111\u01B0\u1EE3c tin nh\u1EAFn n\xE0o. Kho\xE1 n\xE0y do chat-ui-react-js khai; ki\u1EC3m tra g\xF3i UI \u0111\xE3 \u0111\u01B0\u1EE3c n\u1EA1p v\xE0 env \u0111\xE3 truy\u1EC1n \u0111\u1EE7."
247
+ );
248
+ }
249
+ assertSocketIdentity();
250
+ connectChatTransports();
251
+ }
252
+ function disconnect() {
253
+ disconnectChatTransports();
254
+ }
255
+ function start({ signal } = {}) {
256
+ assertAlive();
257
+ if (startPromise) return startPromise;
258
+ startPromise = (async () => {
259
+ try {
260
+ if (!getUser()?.token) {
261
+ setStatus(ChatClientStatus.AUTHENTICATING);
262
+ if (config.token) await resumeSession(config.token);
263
+ else if (config.apiKey) await signInWithApiKey();
264
+ else await signIn();
265
+ } else if (config.apiKey) {
266
+ scheduleRenewal();
267
+ }
268
+ throwIfAborted(signal);
269
+ setStatus(ChatClientStatus.BOOTSTRAPPING);
270
+ const ctx = await bootstrap({ signal });
271
+ throwIfAborted(signal);
272
+ await config.onBootstrap?.(ctx);
273
+ throwIfAborted(signal);
274
+ if (config.autoConnect !== false) {
275
+ setStatus(ChatClientStatus.CONNECTING);
276
+ connect();
277
+ }
278
+ setStatus(ChatClientStatus.READY);
279
+ return { user: getUser(), context: ctx };
280
+ } catch (error) {
281
+ startPromise = null;
282
+ if (error?.name !== "AbortError") {
283
+ setStatus(ChatClientStatus.ERROR, error);
284
+ emit("error", error);
285
+ }
286
+ throw error;
287
+ }
288
+ })();
289
+ return startPromise;
290
+ }
291
+ function signOut() {
292
+ stopRenewal();
293
+ disconnect();
294
+ clearSession();
295
+ startPromise = null;
296
+ context = {};
297
+ setStatus(ChatClientStatus.IDLE);
298
+ emit("session-end");
299
+ }
300
+ const onExternalLogout = () => {
301
+ if (status === ChatClientStatus.DISPOSED) return;
302
+ stopRenewal();
303
+ disconnect();
304
+ startPromise = null;
305
+ context = {};
306
+ setStatus(ChatClientStatus.IDLE);
307
+ emit("session-end");
308
+ };
309
+ if (typeof document !== "undefined") {
310
+ document.addEventListener(LOGOUT_EVENT, onExternalLogout);
311
+ disposers.add(
312
+ () => document.removeEventListener(LOGOUT_EVENT, onExternalLogout)
313
+ );
314
+ }
315
+ function dispose() {
316
+ if (status === ChatClientStatus.DISPOSED) return;
317
+ disconnect();
318
+ for (const fn of disposers) {
319
+ try {
320
+ fn();
321
+ } catch (e) {
322
+ console.error("[chat-js] l\u1ED7i khi dispose:", e);
323
+ }
324
+ }
325
+ disposers.clear();
326
+ listeners.clear();
327
+ startPromise = null;
328
+ context = {};
329
+ status = ChatClientStatus.DISPOSED;
330
+ if (current === client) current = null;
331
+ }
332
+ const client = {
333
+ fingerprint: fp,
334
+ config: Object.freeze({
335
+ domain: config.domain,
336
+ username: config.username,
337
+ password: config.password ? "***" : void 0,
338
+ token: config.token ? "***" : void 0,
339
+ locale: config.locale,
340
+ autoConnect: config.autoConnect !== false
341
+ }),
342
+ get status() {
343
+ return status;
344
+ },
345
+ get user() {
346
+ return getUser();
347
+ },
348
+ get context() {
349
+ return context;
350
+ },
351
+ get env() {
352
+ return env;
353
+ },
354
+ api,
355
+ socket: createSocketFacade(),
356
+ auth: { getUser, signIn, signInWithApiKey, resumeSession, signOut },
357
+ signIn,
358
+ signInWithApiKey,
359
+ resumeSession,
360
+ bootstrap,
361
+ start,
362
+ connect,
363
+ disconnect,
364
+ signOut,
365
+ dispose,
366
+ /** Chỉ để test và chẩn đoán. */
367
+ _assertSocketIdentity: assertSocketIdentity,
368
+ on(event, handler) {
369
+ if (!listeners.has(event)) listeners.set(event, /* @__PURE__ */ new Set());
370
+ listeners.get(event).add(handler);
371
+ return () => client.off(event, handler);
372
+ },
373
+ off(event, handler) {
374
+ listeners.get(event)?.delete(handler);
375
+ },
376
+ /** Đăng ký một hàm dọn dẹp chạy lúc `dispose()`. */
377
+ onDispose(fn) {
378
+ disposers.add(fn);
379
+ return () => disposers.delete(fn);
380
+ }
381
+ };
382
+ current = client;
383
+ return client;
384
+ }
385
+ export {
386
+ ChatClientConflictError,
387
+ ChatClientStatus,
388
+ createChatClient,
389
+ disposeChatClient,
390
+ getChatClient
391
+ };
package/config/env.js ADDED
@@ -0,0 +1,76 @@
1
+ const DEFAULTS = {
2
+ // --- URL dịch vụ ---
3
+ API_EVS_URL: "https://api.zenify.vn",
4
+ EMAIL_URL: "https://api.zenify.vn/emailgateway",
5
+ CDN_URL: "https://cdn.zenify.vn",
6
+ ZALO_LOGIN_URL: "https://zalop.zenify.vn",
7
+ HUB_URL: "https://apihub.zenify.vn",
8
+ MSG_HUB_URL: "https://msghub.zenify.vn",
9
+ AUTH_URL: "",
10
+ // rỗng → rơi về HUB_URL (xem utils/request.js)
11
+ OMAP_URL: "https://api.zenify.vn/omap",
12
+ OTT_GATEWAY_URL: "https://chat.zenify.vn",
13
+ // --- Cờ theo tenant ---
14
+ // Nhận cả chuỗi phân tách bằng dấu phẩy (dán thẳng từ .env cũ) lẫn mảng.
15
+ // Khoảng trắng thừa và phần tử rỗng bị bỏ.
16
+ HELP_DESK_PLAN_TENANT_IDS: ["694816f69eba11635c959f07", "69cfd4b2058ab7189b354578"],
17
+ // Chế độ dev: thay cho `import.meta.env.DEV`. Chỉ tắt vài thứ gây nhiễu khi
18
+ // phát triển (log web-vitals, chunk-reload guard) — không ảnh hưởng nghiệp vụ.
19
+ DEV: false
20
+ };
21
+ const env = { ...DEFAULTS, ...readGlobalSeed() };
22
+ function readGlobalSeed() {
23
+ const seed = globalThis.__CHAT_ENV__;
24
+ return seed && typeof seed === "object" ? seed : {};
25
+ }
26
+ const setCache = /* @__PURE__ */ new Map();
27
+ function registerEnvDefaults(defaults = {}) {
28
+ const seed = readGlobalSeed();
29
+ for (const [k, v] of Object.entries(defaults)) {
30
+ DEFAULTS[k] = v;
31
+ if (!(k in env)) env[k] = k in seed ? seed[k] : v;
32
+ }
33
+ setCache.clear();
34
+ return env;
35
+ }
36
+ function configureChatEnv(values = {}) {
37
+ const unknown = Object.keys(values).filter((k) => !(k in DEFAULTS));
38
+ if (unknown.length) {
39
+ console.warn(`[chat-js] configureChatEnv: kho\xE1 l\u1EA1 \u2014 ${unknown.join(", ")}`);
40
+ }
41
+ Object.assign(env, values);
42
+ setCache.clear();
43
+ return env;
44
+ }
45
+ function resetChatEnv() {
46
+ Object.assign(env, DEFAULTS, readGlobalSeed());
47
+ setCache.clear();
48
+ return env;
49
+ }
50
+ function envKeys() {
51
+ return Object.keys(DEFAULTS);
52
+ }
53
+ const toList = (raw) => {
54
+ const parts = Array.isArray(raw) ? raw : String(raw ?? "").split(",");
55
+ return parts.map((s) => String(s).trim()).filter(Boolean);
56
+ };
57
+ function envList(key) {
58
+ return toList(env[key]);
59
+ }
60
+ function envSet(key) {
61
+ let cached = setCache.get(key);
62
+ if (!cached) {
63
+ cached = new Set(toList(env[key]));
64
+ setCache.set(key, cached);
65
+ }
66
+ return cached;
67
+ }
68
+ export {
69
+ configureChatEnv,
70
+ env,
71
+ envKeys,
72
+ envList,
73
+ envSet,
74
+ registerEnvDefaults,
75
+ resetChatEnv
76
+ };