@softtynet/zplus-worker 1.0.0 → 1.0.3
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.
- package/README.md +27 -55
- package/dist/cli.js +1 -1408
- package/package.json +3 -3
- package/dist/automation/HeadlessAutomationEngine.d.ts +0 -12
- package/dist/automation/HeadlessAutomationEngine.js +0 -34
- package/dist/channels/ChannelRunnerRegistry.d.ts +0 -14
- package/dist/channels/ChannelRunnerRegistry.js +0 -65
- package/dist/channels/IChannelRunner.d.ts +0 -10
- package/dist/channels/IChannelRunner.js +0 -2
- package/dist/channels/index.d.ts +0 -6
- package/dist/channels/index.js +0 -22
- package/dist/channels/runners/FacebookChannelRunner.d.ts +0 -19
- package/dist/channels/runners/FacebookChannelRunner.js +0 -47
- package/dist/channels/runners/TelegramChannelRunner.d.ts +0 -19
- package/dist/channels/runners/TelegramChannelRunner.js +0 -47
- package/dist/channels/runners/WhatsAppChannelRunner.d.ts +0 -19
- package/dist/channels/runners/WhatsAppChannelRunner.js +0 -47
- package/dist/channels/runners/ZaloChannelRunner.d.ts +0 -22
- package/dist/channels/runners/ZaloChannelRunner.js +0 -50
- package/dist/cli.d.ts +0 -6
- package/dist/config/WorkerConfig.d.ts +0 -16
- package/dist/config/WorkerConfig.js +0 -21
- package/dist/engine/WorkerEngine.d.ts +0 -38
- package/dist/engine/WorkerEngine.js +0 -157
- package/dist/index.d.ts +0 -9
- package/dist/lock/DistributedLockManager.d.ts +0 -40
- package/dist/lock/DistributedLockManager.js +0 -113
- package/dist/notifications/PushNotificationDispatcher.d.ts +0 -14
- package/dist/notifications/PushNotificationDispatcher.js +0 -31
- package/dist/server/WorkerHttpServer.d.ts +0 -25
- package/dist/server/WorkerHttpServer.js +0 -205
- package/dist/sync/CloudPendingMessageQueue.d.ts +0 -27
- package/dist/sync/CloudPendingMessageQueue.js +0 -69
- package/dist/sync/CloudSyncWorker.d.ts +0 -19
- package/dist/sync/CloudSyncWorker.js +0 -53
package/dist/cli.js
CHANGED
|
@@ -1,1409 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
"use strict";
|
|
3
|
-
var __create = Object.create;
|
|
4
|
-
var __defProp = Object.defineProperty;
|
|
5
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
-
var __commonJS = (cb, mod) => function __require() {
|
|
10
|
-
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
11
|
-
};
|
|
12
|
-
var __copyProps = (to, from, except, desc) => {
|
|
13
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
-
for (let key of __getOwnPropNames(from))
|
|
15
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
-
}
|
|
18
|
-
return to;
|
|
19
|
-
};
|
|
20
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
-
mod
|
|
27
|
-
));
|
|
28
|
-
|
|
29
|
-
// ../../packages/core/dist/logger/index.js
|
|
30
|
-
var require_logger = __commonJS({
|
|
31
|
-
"../../packages/core/dist/logger/index.js"(exports2) {
|
|
32
|
-
"use strict";
|
|
33
|
-
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
34
|
-
exports2.ConsoleLogger = void 0;
|
|
35
|
-
exports2.createLogger = createLogger13;
|
|
36
|
-
var ConsoleLogger = class {
|
|
37
|
-
context;
|
|
38
|
-
constructor(context = "CORE") {
|
|
39
|
-
this.context = context;
|
|
40
|
-
}
|
|
41
|
-
format(level, message) {
|
|
42
|
-
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
43
|
-
return `[${ts}] [${level}] [${this.context}] ${message}`;
|
|
44
|
-
}
|
|
45
|
-
info(message, ...meta) {
|
|
46
|
-
console.log(this.format("INFO", message), ...meta);
|
|
47
|
-
}
|
|
48
|
-
debug(message, ...meta) {
|
|
49
|
-
console.debug(this.format("DEBUG", message), ...meta);
|
|
50
|
-
}
|
|
51
|
-
warn(message, ...meta) {
|
|
52
|
-
console.warn(this.format("WARN", message), ...meta);
|
|
53
|
-
}
|
|
54
|
-
error(message, ...meta) {
|
|
55
|
-
console.error(this.format("ERROR", message), ...meta);
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
exports2.ConsoleLogger = ConsoleLogger;
|
|
59
|
-
function createLogger13(context) {
|
|
60
|
-
return new ConsoleLogger(context);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
// ../../packages/core/dist/crypto/index.js
|
|
66
|
-
var require_crypto = __commonJS({
|
|
67
|
-
"../../packages/core/dist/crypto/index.js"(exports2) {
|
|
68
|
-
"use strict";
|
|
69
|
-
var __createBinding = exports2 && exports2.__createBinding || (Object.create ? (function(o, m, k, k2) {
|
|
70
|
-
if (k2 === void 0) k2 = k;
|
|
71
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
72
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
73
|
-
desc = { enumerable: true, get: function() {
|
|
74
|
-
return m[k];
|
|
75
|
-
} };
|
|
76
|
-
}
|
|
77
|
-
Object.defineProperty(o, k2, desc);
|
|
78
|
-
}) : (function(o, m, k, k2) {
|
|
79
|
-
if (k2 === void 0) k2 = k;
|
|
80
|
-
o[k2] = m[k];
|
|
81
|
-
}));
|
|
82
|
-
var __setModuleDefault = exports2 && exports2.__setModuleDefault || (Object.create ? (function(o, v) {
|
|
83
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
84
|
-
}) : function(o, v) {
|
|
85
|
-
o["default"] = v;
|
|
86
|
-
});
|
|
87
|
-
var __importStar = exports2 && exports2.__importStar || /* @__PURE__ */ (function() {
|
|
88
|
-
var ownKeys = function(o) {
|
|
89
|
-
ownKeys = Object.getOwnPropertyNames || function(o2) {
|
|
90
|
-
var ar = [];
|
|
91
|
-
for (var k in o2) if (Object.prototype.hasOwnProperty.call(o2, k)) ar[ar.length] = k;
|
|
92
|
-
return ar;
|
|
93
|
-
};
|
|
94
|
-
return ownKeys(o);
|
|
95
|
-
};
|
|
96
|
-
return function(mod) {
|
|
97
|
-
if (mod && mod.__esModule) return mod;
|
|
98
|
-
var result = {};
|
|
99
|
-
if (mod != null) {
|
|
100
|
-
for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
101
|
-
}
|
|
102
|
-
__setModuleDefault(result, mod);
|
|
103
|
-
return result;
|
|
104
|
-
};
|
|
105
|
-
})();
|
|
106
|
-
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
107
|
-
exports2.CryptoUtils = void 0;
|
|
108
|
-
var crypto = __importStar(require("crypto"));
|
|
109
|
-
var CryptoUtils = class {
|
|
110
|
-
/**
|
|
111
|
-
* Sinh MD5 hash tất định (32 ký tự hex)
|
|
112
|
-
*/
|
|
113
|
-
static md5(input) {
|
|
114
|
-
return crypto.createHash("md5").update(input).digest("hex");
|
|
115
|
-
}
|
|
116
|
-
/**
|
|
117
|
-
* Sinh SHA256 hash (64 ký tự hex)
|
|
118
|
-
*/
|
|
119
|
-
static sha256(input) {
|
|
120
|
-
return crypto.createHash("sha256").update(input).digest("hex");
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Tạo Document ID an toàn và bất biến cho Appwrite (Tối đa 36 ký tự, hợp lệ regex Appwrite)
|
|
124
|
-
* Không dùng substring cắt cụt gây trùng lặp bản ghi.
|
|
125
|
-
*/
|
|
126
|
-
static buildSafeDocumentId(tableName, recordId) {
|
|
127
|
-
if (!recordId) {
|
|
128
|
-
return `doc_${crypto.randomBytes(12).toString("hex")}`;
|
|
129
|
-
}
|
|
130
|
-
const cleanId = String(recordId).trim();
|
|
131
|
-
const validAppwriteIdRegex = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,35}$/;
|
|
132
|
-
if (validAppwriteIdRegex.test(cleanId)) {
|
|
133
|
-
return cleanId;
|
|
134
|
-
}
|
|
135
|
-
const hash = this.md5(`${tableName}:${cleanId}`);
|
|
136
|
-
return `z_${hash}`;
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Sinh chuỗi token ngẫu nhiên an toàn
|
|
140
|
-
*/
|
|
141
|
-
static randomToken(bytes = 16) {
|
|
142
|
-
return crypto.randomBytes(bytes).toString("hex");
|
|
143
|
-
}
|
|
144
|
-
};
|
|
145
|
-
exports2.CryptoUtils = CryptoUtils;
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
// ../../packages/core/dist/events/index.js
|
|
150
|
-
var require_events = __commonJS({
|
|
151
|
-
"../../packages/core/dist/events/index.js"(exports2) {
|
|
152
|
-
"use strict";
|
|
153
|
-
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
154
|
-
exports2.TypedEventBus = void 0;
|
|
155
|
-
var events_1 = require("events");
|
|
156
|
-
var TypedEventBus = class {
|
|
157
|
-
emitter = new events_1.EventEmitter();
|
|
158
|
-
constructor(maxListeners = 50) {
|
|
159
|
-
this.emitter.setMaxListeners(maxListeners);
|
|
160
|
-
}
|
|
161
|
-
emit(event, payload) {
|
|
162
|
-
return this.emitter.emit(event, payload);
|
|
163
|
-
}
|
|
164
|
-
on(event, listener) {
|
|
165
|
-
this.emitter.on(event, listener);
|
|
166
|
-
return this;
|
|
167
|
-
}
|
|
168
|
-
once(event, listener) {
|
|
169
|
-
this.emitter.once(event, listener);
|
|
170
|
-
return this;
|
|
171
|
-
}
|
|
172
|
-
off(event, listener) {
|
|
173
|
-
this.emitter.off(event, listener);
|
|
174
|
-
return this;
|
|
175
|
-
}
|
|
176
|
-
removeAllListeners(event) {
|
|
177
|
-
this.emitter.removeAllListeners(event);
|
|
178
|
-
return this;
|
|
179
|
-
}
|
|
180
|
-
};
|
|
181
|
-
exports2.TypedEventBus = TypedEventBus;
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
// ../../packages/core/dist/utils/index.js
|
|
186
|
-
var require_utils = __commonJS({
|
|
187
|
-
"../../packages/core/dist/utils/index.js"(exports2) {
|
|
188
|
-
"use strict";
|
|
189
|
-
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
190
|
-
exports2.JsonUtils = exports2.AsyncUtils = void 0;
|
|
191
|
-
var AsyncUtils = class {
|
|
192
|
-
static sleep(ms) {
|
|
193
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
194
|
-
}
|
|
195
|
-
/**
|
|
196
|
-
* Chạy lại tác vụ với Exponential Backoff
|
|
197
|
-
*/
|
|
198
|
-
static async retryWithBackoff(fn, options = {}) {
|
|
199
|
-
const { maxRetries = 3, initialDelayMs = 500, maxDelayMs = 1e4, backoffFactor = 2, shouldRetry = () => true } = options;
|
|
200
|
-
let delay = initialDelayMs;
|
|
201
|
-
let lastError;
|
|
202
|
-
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
203
|
-
try {
|
|
204
|
-
return await fn();
|
|
205
|
-
} catch (err) {
|
|
206
|
-
lastError = err;
|
|
207
|
-
if (attempt >= maxRetries || !shouldRetry(err)) {
|
|
208
|
-
throw err;
|
|
209
|
-
}
|
|
210
|
-
await this.sleep(delay);
|
|
211
|
-
delay = Math.min(delay * backoffFactor, maxDelayMs);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
throw lastError;
|
|
215
|
-
}
|
|
216
|
-
};
|
|
217
|
-
exports2.AsyncUtils = AsyncUtils;
|
|
218
|
-
var JsonUtils = class {
|
|
219
|
-
static safeParse(jsonStr, fallback) {
|
|
220
|
-
if (!jsonStr || typeof jsonStr !== "string") {
|
|
221
|
-
return fallback;
|
|
222
|
-
}
|
|
223
|
-
try {
|
|
224
|
-
return JSON.parse(jsonStr);
|
|
225
|
-
} catch {
|
|
226
|
-
return fallback;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
static safeStringify(val, fallback = "{}") {
|
|
230
|
-
if (val === void 0 || val === null) {
|
|
231
|
-
return fallback;
|
|
232
|
-
}
|
|
233
|
-
try {
|
|
234
|
-
return JSON.stringify(val);
|
|
235
|
-
} catch {
|
|
236
|
-
return fallback;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
};
|
|
240
|
-
exports2.JsonUtils = JsonUtils;
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
// ../../packages/core/dist/errors/index.js
|
|
245
|
-
var require_errors = __commonJS({
|
|
246
|
-
"../../packages/core/dist/errors/index.js"(exports2) {
|
|
247
|
-
"use strict";
|
|
248
|
-
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
249
|
-
exports2.DatabaseError = exports2.NetworkError = exports2.RateLimitError = exports2.ConflictError = exports2.NotFoundError = exports2.ForbiddenError = exports2.AuthenticationError = exports2.ValidationError = exports2.ZPlusError = void 0;
|
|
250
|
-
var ZPlusError = class extends Error {
|
|
251
|
-
code;
|
|
252
|
-
statusCode;
|
|
253
|
-
details;
|
|
254
|
-
constructor(message, code = "INTERNAL_ERROR", statusCode = 500, details) {
|
|
255
|
-
super(message);
|
|
256
|
-
this.name = this.constructor.name;
|
|
257
|
-
this.code = code;
|
|
258
|
-
this.statusCode = statusCode;
|
|
259
|
-
this.details = details;
|
|
260
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
261
|
-
}
|
|
262
|
-
};
|
|
263
|
-
exports2.ZPlusError = ZPlusError;
|
|
264
|
-
var ValidationError = class extends ZPlusError {
|
|
265
|
-
constructor(message, details) {
|
|
266
|
-
super(message, "VALIDATION_ERROR", 400, details);
|
|
267
|
-
}
|
|
268
|
-
};
|
|
269
|
-
exports2.ValidationError = ValidationError;
|
|
270
|
-
var AuthenticationError = class extends ZPlusError {
|
|
271
|
-
constructor(message = "Authentication required or token expired", details) {
|
|
272
|
-
super(message, "UNAUTHORIZED", 401, details);
|
|
273
|
-
}
|
|
274
|
-
};
|
|
275
|
-
exports2.AuthenticationError = AuthenticationError;
|
|
276
|
-
var ForbiddenError = class extends ZPlusError {
|
|
277
|
-
constructor(message = "Access forbidden for current user or device", details) {
|
|
278
|
-
super(message, "FORBIDDEN", 403, details);
|
|
279
|
-
}
|
|
280
|
-
};
|
|
281
|
-
exports2.ForbiddenError = ForbiddenError;
|
|
282
|
-
var NotFoundError = class extends ZPlusError {
|
|
283
|
-
constructor(resource, id) {
|
|
284
|
-
const msg = id ? `${resource} with identifier '${id}' not found` : `${resource} not found`;
|
|
285
|
-
super(msg, "NOT_FOUND", 404, { resource, id });
|
|
286
|
-
}
|
|
287
|
-
};
|
|
288
|
-
exports2.NotFoundError = NotFoundError;
|
|
289
|
-
var ConflictError = class extends ZPlusError {
|
|
290
|
-
constructor(message, details) {
|
|
291
|
-
super(message, "CONFLICT", 409, details);
|
|
292
|
-
}
|
|
293
|
-
};
|
|
294
|
-
exports2.ConflictError = ConflictError;
|
|
295
|
-
var RateLimitError = class extends ZPlusError {
|
|
296
|
-
retryAfterSeconds;
|
|
297
|
-
constructor(message = "Rate limit exceeded. Please cool down.", retryAfterSeconds) {
|
|
298
|
-
super(message, "RATE_LIMIT_EXCEEDED", 429, { retryAfterSeconds });
|
|
299
|
-
this.retryAfterSeconds = retryAfterSeconds;
|
|
300
|
-
}
|
|
301
|
-
};
|
|
302
|
-
exports2.RateLimitError = RateLimitError;
|
|
303
|
-
var NetworkError = class extends ZPlusError {
|
|
304
|
-
constructor(message, details) {
|
|
305
|
-
super(message, "NETWORK_ERROR", 503, details);
|
|
306
|
-
}
|
|
307
|
-
};
|
|
308
|
-
exports2.NetworkError = NetworkError;
|
|
309
|
-
var DatabaseError = class extends ZPlusError {
|
|
310
|
-
constructor(message, details) {
|
|
311
|
-
super(message, "DATABASE_ERROR", 500, details);
|
|
312
|
-
}
|
|
313
|
-
};
|
|
314
|
-
exports2.DatabaseError = DatabaseError;
|
|
315
|
-
}
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
// ../../packages/core/dist/sanitizer/index.js
|
|
319
|
-
var require_sanitizer = __commonJS({
|
|
320
|
-
"../../packages/core/dist/sanitizer/index.js"(exports2) {
|
|
321
|
-
"use strict";
|
|
322
|
-
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
323
|
-
exports2.DataSanitizer = exports2.PhoneNormalizer = void 0;
|
|
324
|
-
var PhoneNormalizer = class {
|
|
325
|
-
/**
|
|
326
|
-
* Normalizes any Vietnamese phone number into standard '0xxxxxxxxx' (10 digits)
|
|
327
|
-
*/
|
|
328
|
-
static normalizeToLocal(input) {
|
|
329
|
-
if (!input)
|
|
330
|
-
return "";
|
|
331
|
-
let cleaned = input.replace(/[\s\-().+]/g, "");
|
|
332
|
-
if (cleaned.startsWith("84") && cleaned.length >= 11) {
|
|
333
|
-
cleaned = "0" + cleaned.slice(2);
|
|
334
|
-
} else if (cleaned.startsWith("0084")) {
|
|
335
|
-
cleaned = "0" + cleaned.slice(4);
|
|
336
|
-
} else if (!cleaned.startsWith("0") && cleaned.length === 9) {
|
|
337
|
-
cleaned = "0" + cleaned;
|
|
338
|
-
}
|
|
339
|
-
return cleaned;
|
|
340
|
-
}
|
|
341
|
-
/**
|
|
342
|
-
* Normalizes to international standard e.g. '+84901234567'
|
|
343
|
-
*/
|
|
344
|
-
static normalizeToInternational(input) {
|
|
345
|
-
const local = this.normalizeToLocal(input);
|
|
346
|
-
if (!local || local.length !== 10 || !local.startsWith("0")) {
|
|
347
|
-
return input;
|
|
348
|
-
}
|
|
349
|
-
return "+84" + local.slice(1);
|
|
350
|
-
}
|
|
351
|
-
/**
|
|
352
|
-
* Validates if string matches Vietnamese 10-digit mobile number format
|
|
353
|
-
*/
|
|
354
|
-
static isValidVietnameseMobile(input) {
|
|
355
|
-
const local = this.normalizeToLocal(input);
|
|
356
|
-
return /^(03|05|07|08|09)[0-9]{8}$/.test(local);
|
|
357
|
-
}
|
|
358
|
-
};
|
|
359
|
-
exports2.PhoneNormalizer = PhoneNormalizer;
|
|
360
|
-
var DataSanitizer = class {
|
|
361
|
-
/**
|
|
362
|
-
* Strips harmful HTML tags from text
|
|
363
|
-
*/
|
|
364
|
-
static stripHtml(input) {
|
|
365
|
-
if (!input)
|
|
366
|
-
return "";
|
|
367
|
-
return input.replace(/<[^>]*>?/gm, "").trim();
|
|
368
|
-
}
|
|
369
|
-
/**
|
|
370
|
-
* Masks sensitive PII tokens or phone numbers for logging
|
|
371
|
-
* Example: 0912345678 -> 091****678
|
|
372
|
-
*/
|
|
373
|
-
static maskPhoneNumber(phone) {
|
|
374
|
-
if (!phone || phone.length < 7)
|
|
375
|
-
return phone;
|
|
376
|
-
const start = phone.slice(0, 3);
|
|
377
|
-
const end = phone.slice(-3);
|
|
378
|
-
return `${start}****${end}`;
|
|
379
|
-
}
|
|
380
|
-
/**
|
|
381
|
-
* Masks secret tokens or passwords for logging
|
|
382
|
-
*/
|
|
383
|
-
static maskToken(token) {
|
|
384
|
-
if (!token)
|
|
385
|
-
return "";
|
|
386
|
-
if (token.length <= 8)
|
|
387
|
-
return "********";
|
|
388
|
-
return token.slice(0, 4) + "..." + token.slice(-4);
|
|
389
|
-
}
|
|
390
|
-
};
|
|
391
|
-
exports2.DataSanitizer = DataSanitizer;
|
|
392
|
-
}
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
// ../../packages/core/dist/id/index.js
|
|
396
|
-
var require_id = __commonJS({
|
|
397
|
-
"../../packages/core/dist/id/index.js"(exports2) {
|
|
398
|
-
"use strict";
|
|
399
|
-
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
400
|
-
exports2.IdGenerator = void 0;
|
|
401
|
-
var crypto_1 = require("crypto");
|
|
402
|
-
var ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
403
|
-
var ENCODING_LEN = ENCODING.length;
|
|
404
|
-
var IdGenerator = class {
|
|
405
|
-
/**
|
|
406
|
-
* Generates a 26-character time-sortable ULID (Universally Unique Lexicographically Sortable Identifier)
|
|
407
|
-
*/
|
|
408
|
-
static generateUlid(timestamp = Date.now()) {
|
|
409
|
-
let timeStr = "";
|
|
410
|
-
let time = timestamp;
|
|
411
|
-
for (let i = 9; i >= 0; i--) {
|
|
412
|
-
const mod = time % ENCODING_LEN;
|
|
413
|
-
timeStr = ENCODING.charAt(mod) + timeStr;
|
|
414
|
-
time = Math.floor(time / ENCODING_LEN);
|
|
415
|
-
}
|
|
416
|
-
const random = (0, crypto_1.randomBytes)(10);
|
|
417
|
-
let randStr = "";
|
|
418
|
-
for (let i = 0; i < 16; i++) {
|
|
419
|
-
const randByte = random[i % 10] ^ i * 17;
|
|
420
|
-
randStr += ENCODING.charAt(randByte % ENCODING_LEN);
|
|
421
|
-
}
|
|
422
|
-
return timeStr + randStr;
|
|
423
|
-
}
|
|
424
|
-
/**
|
|
425
|
-
* Generates a standard RFC4122 v4 UUID
|
|
426
|
-
*/
|
|
427
|
-
static generateUuidV4() {
|
|
428
|
-
const bytes = (0, crypto_1.randomBytes)(16);
|
|
429
|
-
bytes[6] = bytes[6] & 15 | 64;
|
|
430
|
-
bytes[8] = bytes[8] & 63 | 128;
|
|
431
|
-
const hex = bytes.toString("hex");
|
|
432
|
-
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
433
|
-
}
|
|
434
|
-
/**
|
|
435
|
-
* Generates a compact alphanumeric ID for human-friendly references (e.g. order numbers, short codes)
|
|
436
|
-
*/
|
|
437
|
-
static generateShortId(prefix = "", length = 8) {
|
|
438
|
-
const bytes = (0, crypto_1.randomBytes)(length);
|
|
439
|
-
const chars = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
|
|
440
|
-
let result = "";
|
|
441
|
-
for (let i = 0; i < length; i++) {
|
|
442
|
-
result += chars.charAt(bytes[i] % chars.length);
|
|
443
|
-
}
|
|
444
|
-
return prefix ? `${prefix}_${result}` : result;
|
|
445
|
-
}
|
|
446
|
-
};
|
|
447
|
-
exports2.IdGenerator = IdGenerator;
|
|
448
|
-
}
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
// ../../packages/core/dist/constants/index.js
|
|
452
|
-
var require_constants = __commonJS({
|
|
453
|
-
"../../packages/core/dist/constants/index.js"(exports2) {
|
|
454
|
-
"use strict";
|
|
455
|
-
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
456
|
-
exports2.CLOUD_ENDPOINTS = exports2.LIMITS = exports2.DEFAULT_PAGINATION = exports2.PLATFORMS = void 0;
|
|
457
|
-
exports2.PLATFORMS = {
|
|
458
|
-
ZALO: "zalo",
|
|
459
|
-
FACEBOOK: "facebook",
|
|
460
|
-
TELEGRAM: "telegram",
|
|
461
|
-
WHATSAPP: "whatsapp"
|
|
462
|
-
};
|
|
463
|
-
exports2.DEFAULT_PAGINATION = {
|
|
464
|
-
PAGE_SIZE: 50,
|
|
465
|
-
MAX_PAGE_SIZE: 200
|
|
466
|
-
};
|
|
467
|
-
exports2.LIMITS = {
|
|
468
|
-
MAX_ATTACHMENT_BYTES: 100 * 1024 * 1024,
|
|
469
|
-
// 100MB
|
|
470
|
-
MAX_IMAGE_BYTES: 25 * 1024 * 1024,
|
|
471
|
-
// 25MB
|
|
472
|
-
MAX_BATCH_SYNC_ITEMS: 500,
|
|
473
|
-
HEARTBEAT_INTERVAL_MS: 3e4,
|
|
474
|
-
// 30s
|
|
475
|
-
WSS_RECONNECT_MAX_DELAY_MS: 6e4
|
|
476
|
-
// 60s
|
|
477
|
-
};
|
|
478
|
-
exports2.CLOUD_ENDPOINTS = {
|
|
479
|
-
DEFAULT_APPWRITE_ENDPOINT: "https://cloud.appwrite.io/v1",
|
|
480
|
-
DEFAULT_PROJECT_ID: "zplus-cloud"
|
|
481
|
-
};
|
|
482
|
-
}
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
// ../../packages/core/dist/index.js
|
|
486
|
-
var require_dist = __commonJS({
|
|
487
|
-
"../../packages/core/dist/index.js"(exports2) {
|
|
488
|
-
"use strict";
|
|
489
|
-
var __createBinding = exports2 && exports2.__createBinding || (Object.create ? (function(o, m, k, k2) {
|
|
490
|
-
if (k2 === void 0) k2 = k;
|
|
491
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
492
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
493
|
-
desc = { enumerable: true, get: function() {
|
|
494
|
-
return m[k];
|
|
495
|
-
} };
|
|
496
|
-
}
|
|
497
|
-
Object.defineProperty(o, k2, desc);
|
|
498
|
-
}) : (function(o, m, k, k2) {
|
|
499
|
-
if (k2 === void 0) k2 = k;
|
|
500
|
-
o[k2] = m[k];
|
|
501
|
-
}));
|
|
502
|
-
var __exportStar = exports2 && exports2.__exportStar || function(m, exports3) {
|
|
503
|
-
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports3, p)) __createBinding(exports3, m, p);
|
|
504
|
-
};
|
|
505
|
-
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
506
|
-
__exportStar(require_logger(), exports2);
|
|
507
|
-
__exportStar(require_crypto(), exports2);
|
|
508
|
-
__exportStar(require_events(), exports2);
|
|
509
|
-
__exportStar(require_utils(), exports2);
|
|
510
|
-
__exportStar(require_errors(), exports2);
|
|
511
|
-
__exportStar(require_sanitizer(), exports2);
|
|
512
|
-
__exportStar(require_id(), exports2);
|
|
513
|
-
__exportStar(require_constants(), exports2);
|
|
514
|
-
}
|
|
515
|
-
});
|
|
516
|
-
|
|
517
|
-
// src/config/WorkerConfig.ts
|
|
518
|
-
var WorkerConfig = class {
|
|
519
|
-
static loadFromEnv() {
|
|
520
|
-
return {
|
|
521
|
-
workerId: process.env.WORKER_ID || `worker_${process.pid}`,
|
|
522
|
-
port: Number(process.env.PORT || 8080),
|
|
523
|
-
appwriteEndpoint: process.env.APPWRITE_ENDPOINT,
|
|
524
|
-
appwriteProjectId: process.env.APPWRITE_PROJECT_ID,
|
|
525
|
-
appwriteApiKey: process.env.APPWRITE_API_KEY,
|
|
526
|
-
databaseId: process.env.APPWRITE_DATABASE_ID || "zplus_app",
|
|
527
|
-
heartbeatIntervalMs: Number(process.env.HEARTBEAT_INTERVAL_MS || 3e4),
|
|
528
|
-
logLevel: process.env.LOG_LEVEL || "info"
|
|
529
|
-
};
|
|
530
|
-
}
|
|
531
|
-
};
|
|
532
|
-
|
|
533
|
-
// src/engine/WorkerEngine.ts
|
|
534
|
-
var import_core11 = __toESM(require_dist());
|
|
535
|
-
|
|
536
|
-
// src/channels/ChannelRunnerRegistry.ts
|
|
537
|
-
var import_core = __toESM(require_dist());
|
|
538
|
-
var ChannelRunnerRegistry = class {
|
|
539
|
-
runners = /* @__PURE__ */ new Map();
|
|
540
|
-
logger;
|
|
541
|
-
constructor(logger2) {
|
|
542
|
-
this.logger = logger2 || (0, import_core.createLogger)("ChannelRunnerRegistry");
|
|
543
|
-
}
|
|
544
|
-
register(runner) {
|
|
545
|
-
const key = `${runner.platform}:${runner.accountId}`;
|
|
546
|
-
if (this.runners.has(key)) {
|
|
547
|
-
this.logger.warn(`Overwriting existing runner for ${key}`);
|
|
548
|
-
}
|
|
549
|
-
this.runners.set(key, runner);
|
|
550
|
-
this.logger.info(`Registered runner for ${key}`);
|
|
551
|
-
}
|
|
552
|
-
unregister(platform, accountId) {
|
|
553
|
-
const key = `${platform}:${accountId}`;
|
|
554
|
-
const runner = this.runners.get(key);
|
|
555
|
-
if (runner) {
|
|
556
|
-
runner.stop().catch((err) => {
|
|
557
|
-
this.logger.error(
|
|
558
|
-
`Error stopping runner ${key} during unregister`,
|
|
559
|
-
err
|
|
560
|
-
);
|
|
561
|
-
});
|
|
562
|
-
this.runners.delete(key);
|
|
563
|
-
this.logger.info(`Unregistered runner for ${key}`);
|
|
564
|
-
return true;
|
|
565
|
-
}
|
|
566
|
-
return false;
|
|
567
|
-
}
|
|
568
|
-
get(platform, accountId) {
|
|
569
|
-
return this.runners.get(`${platform}:${accountId}`);
|
|
570
|
-
}
|
|
571
|
-
getAll() {
|
|
572
|
-
return Array.from(this.runners.values());
|
|
573
|
-
}
|
|
574
|
-
getCount() {
|
|
575
|
-
return this.runners.size;
|
|
576
|
-
}
|
|
577
|
-
async startAll() {
|
|
578
|
-
this.logger.info(
|
|
579
|
-
`Starting all ${this.runners.size} registered channel runners...`
|
|
580
|
-
);
|
|
581
|
-
for (const [key, runner] of this.runners.entries()) {
|
|
582
|
-
try {
|
|
583
|
-
await runner.start();
|
|
584
|
-
} catch (err) {
|
|
585
|
-
this.logger.error(`Failed to start runner ${key}:`, err);
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
async stopAll() {
|
|
590
|
-
this.logger.info(`Stopping all ${this.runners.size} channel runners...`);
|
|
591
|
-
for (const [key, runner] of this.runners.entries()) {
|
|
592
|
-
try {
|
|
593
|
-
await runner.stop();
|
|
594
|
-
} catch (err) {
|
|
595
|
-
this.logger.error(`Failed to stop runner ${key}:`, err);
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
this.runners.clear();
|
|
599
|
-
}
|
|
600
|
-
};
|
|
601
|
-
|
|
602
|
-
// src/lock/DistributedLockManager.ts
|
|
603
|
-
var import_core2 = __toESM(require_dist());
|
|
604
|
-
var DistributedLockManager = class {
|
|
605
|
-
constructor(defaultTtlMs = 6e4, logger2) {
|
|
606
|
-
this.defaultTtlMs = defaultTtlMs;
|
|
607
|
-
this.logger = logger2 || (0, import_core2.createLogger)("DistributedLockManager");
|
|
608
|
-
}
|
|
609
|
-
logger;
|
|
610
|
-
locks = /* @__PURE__ */ new Map();
|
|
611
|
-
preemptCallbacks = [];
|
|
612
|
-
onPreempt(cb) {
|
|
613
|
-
this.preemptCallbacks.push(cb);
|
|
614
|
-
}
|
|
615
|
-
/**
|
|
616
|
-
* Xin giữ lock cho tài khoản
|
|
617
|
-
*/
|
|
618
|
-
acquireLock(accountId, ownerId, ttlMs = this.defaultTtlMs) {
|
|
619
|
-
const now = Date.now();
|
|
620
|
-
const existing = this.locks.get(accountId);
|
|
621
|
-
if (existing && existing.expiresAt > now && existing.ownerId !== ownerId) {
|
|
622
|
-
this.logger.warn(
|
|
623
|
-
`Lock denied for account ${accountId}: currently held by ${existing.ownerId} (expires in ${existing.expiresAt - now}ms)`
|
|
624
|
-
);
|
|
625
|
-
return false;
|
|
626
|
-
}
|
|
627
|
-
this.locks.set(accountId, {
|
|
628
|
-
accountId,
|
|
629
|
-
ownerId,
|
|
630
|
-
acquiredAt: now,
|
|
631
|
-
expiresAt: now + ttlMs
|
|
632
|
-
});
|
|
633
|
-
this.logger.info(
|
|
634
|
-
`\u2705 Lock granted: account=${accountId}, owner=${ownerId}, ttl=${ttlMs}ms`
|
|
635
|
-
);
|
|
636
|
-
return true;
|
|
637
|
-
}
|
|
638
|
-
/**
|
|
639
|
-
* Desktop chiếm lại quyền tối thượng (Master Preemption)
|
|
640
|
-
* Buộc Worker nhả socket WSS trong vòng tối đa 1.000ms
|
|
641
|
-
*/
|
|
642
|
-
async preemptLock(accountId, preemptorId) {
|
|
643
|
-
const existing = this.locks.get(accountId);
|
|
644
|
-
this.logger.info(
|
|
645
|
-
`\u26A1 Preemption triggered for account ${accountId} by master ${preemptorId} (previous owner: ${existing?.ownerId || "none"})`
|
|
646
|
-
);
|
|
647
|
-
const callbackPromises = this.preemptCallbacks.map((cb) => {
|
|
648
|
-
try {
|
|
649
|
-
return Promise.resolve(cb(accountId, preemptorId));
|
|
650
|
-
} catch (err) {
|
|
651
|
-
this.logger.error(`Error in preempt callback for ${accountId}:`, err);
|
|
652
|
-
return Promise.resolve();
|
|
653
|
-
}
|
|
654
|
-
});
|
|
655
|
-
await Promise.race([
|
|
656
|
-
Promise.all(callbackPromises),
|
|
657
|
-
new Promise((resolve) => setTimeout(resolve, 1e3))
|
|
658
|
-
]);
|
|
659
|
-
const now = Date.now();
|
|
660
|
-
this.locks.set(accountId, {
|
|
661
|
-
accountId,
|
|
662
|
-
ownerId: preemptorId,
|
|
663
|
-
acquiredAt: now,
|
|
664
|
-
expiresAt: now + this.defaultTtlMs
|
|
665
|
-
});
|
|
666
|
-
this.logger.info(
|
|
667
|
-
`\u2705 Account ${accountId} preempted successfully by ${preemptorId}.`
|
|
668
|
-
);
|
|
669
|
-
return true;
|
|
670
|
-
}
|
|
671
|
-
/**
|
|
672
|
-
* Giải phóng khóa
|
|
673
|
-
*/
|
|
674
|
-
releaseLock(accountId, ownerId) {
|
|
675
|
-
const existing = this.locks.get(accountId);
|
|
676
|
-
if (!existing) return true;
|
|
677
|
-
if (ownerId && existing.ownerId !== ownerId) {
|
|
678
|
-
this.logger.warn(
|
|
679
|
-
`Attempted unauthorized lock release for ${accountId} by ${ownerId} (real owner: ${existing.ownerId})`
|
|
680
|
-
);
|
|
681
|
-
return false;
|
|
682
|
-
}
|
|
683
|
-
this.locks.delete(accountId);
|
|
684
|
-
this.logger.info(`\u{1F513} Lock released for account ${accountId}.`);
|
|
685
|
-
return true;
|
|
686
|
-
}
|
|
687
|
-
/**
|
|
688
|
-
* Giải phóng toàn bộ khóa thuộc sở hữu của ownerId
|
|
689
|
-
*/
|
|
690
|
-
releaseAllForOwner(ownerId) {
|
|
691
|
-
let released = 0;
|
|
692
|
-
for (const [accountId, lock] of this.locks.entries()) {
|
|
693
|
-
if (lock.ownerId === ownerId) {
|
|
694
|
-
this.locks.delete(accountId);
|
|
695
|
-
released++;
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
return released;
|
|
699
|
-
}
|
|
700
|
-
isLocked(accountId) {
|
|
701
|
-
const existing = this.locks.get(accountId);
|
|
702
|
-
if (!existing) return false;
|
|
703
|
-
return existing.expiresAt > Date.now();
|
|
704
|
-
}
|
|
705
|
-
getLockOwner(accountId) {
|
|
706
|
-
const existing = this.locks.get(accountId);
|
|
707
|
-
if (!existing || existing.expiresAt <= Date.now()) return null;
|
|
708
|
-
return existing.ownerId;
|
|
709
|
-
}
|
|
710
|
-
};
|
|
711
|
-
|
|
712
|
-
// src/sync/CloudPendingMessageQueue.ts
|
|
713
|
-
var import_core3 = __toESM(require_dist());
|
|
714
|
-
var CloudPendingMessageQueue = class {
|
|
715
|
-
logger;
|
|
716
|
-
queue = [];
|
|
717
|
-
maxCapacity;
|
|
718
|
-
constructor(maxCapacity = 1e4, logger2) {
|
|
719
|
-
this.maxCapacity = maxCapacity;
|
|
720
|
-
this.logger = logger2 || (0, import_core3.createLogger)("CloudPendingMessageQueue");
|
|
721
|
-
}
|
|
722
|
-
/**
|
|
723
|
-
* Đưa tin nhắn mới nhận được trong lúc gác đền vào hàng đợi
|
|
724
|
-
*/
|
|
725
|
-
enqueue(message) {
|
|
726
|
-
if (this.queue.length >= this.maxCapacity) {
|
|
727
|
-
const removed = this.queue.shift();
|
|
728
|
-
this.logger.warn(
|
|
729
|
-
`Queue capacity reached (${this.maxCapacity}). Dropped oldest message: ${removed?.id}`
|
|
730
|
-
);
|
|
731
|
-
}
|
|
732
|
-
this.queue.push(message);
|
|
733
|
-
this.logger.debug(
|
|
734
|
-
`Enqueued pending message ${message.id} for account ${message.accountId} (Total pending: ${this.queue.length})`
|
|
735
|
-
);
|
|
736
|
-
}
|
|
737
|
-
/**
|
|
738
|
-
* Lấy danh sách tin nhắn phát sinh kể từ mốc thời gian sinceTimestamp
|
|
739
|
-
*/
|
|
740
|
-
getDeltas(accountId, sinceTimestamp = 0) {
|
|
741
|
-
return this.queue.filter((msg) => {
|
|
742
|
-
const matchAccount = !accountId || msg.accountId === accountId;
|
|
743
|
-
const matchTime = msg.timestamp >= sinceTimestamp;
|
|
744
|
-
return matchAccount && matchTime;
|
|
745
|
-
});
|
|
746
|
-
}
|
|
747
|
-
/**
|
|
748
|
-
* Đánh dấu đã đồng bộ và dọn dẹp các tin nhắn cũ hơn upToTimestamp
|
|
749
|
-
*/
|
|
750
|
-
purgeSynced(upToTimestamp, accountId) {
|
|
751
|
-
const initialLen = this.queue.length;
|
|
752
|
-
const removeIndices = [];
|
|
753
|
-
for (let i = 0; i < this.queue.length; i++) {
|
|
754
|
-
const msg = this.queue[i];
|
|
755
|
-
if (msg.timestamp <= upToTimestamp && (!accountId || msg.accountId === accountId)) {
|
|
756
|
-
removeIndices.push(i);
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
for (let i = removeIndices.length - 1; i >= 0; i--) {
|
|
760
|
-
this.queue.splice(removeIndices[i], 1);
|
|
761
|
-
}
|
|
762
|
-
const removedCount = initialLen - this.queue.length;
|
|
763
|
-
if (removedCount > 0) {
|
|
764
|
-
this.logger.info(
|
|
765
|
-
`Purged ${removedCount} synced delta messages from queue.`
|
|
766
|
-
);
|
|
767
|
-
}
|
|
768
|
-
return removedCount;
|
|
769
|
-
}
|
|
770
|
-
getCount() {
|
|
771
|
-
return this.queue.length;
|
|
772
|
-
}
|
|
773
|
-
clear() {
|
|
774
|
-
this.queue.length = 0;
|
|
775
|
-
}
|
|
776
|
-
};
|
|
777
|
-
|
|
778
|
-
// src/automation/HeadlessAutomationEngine.ts
|
|
779
|
-
var import_core4 = __toESM(require_dist());
|
|
780
|
-
var HeadlessAutomationEngine = class {
|
|
781
|
-
logger;
|
|
782
|
-
campaigns = /* @__PURE__ */ new Map();
|
|
783
|
-
rules = /* @__PURE__ */ new Map();
|
|
784
|
-
constructor(logger2) {
|
|
785
|
-
this.logger = logger2 || (0, import_core4.createLogger)("HeadlessAutomationEngine");
|
|
786
|
-
}
|
|
787
|
-
registerCampaign(campaign) {
|
|
788
|
-
this.campaigns.set(campaign.id, campaign);
|
|
789
|
-
this.logger.info(`Loaded campaign ${campaign.id} (${campaign.name})`);
|
|
790
|
-
}
|
|
791
|
-
registerRule(rule) {
|
|
792
|
-
this.rules.set(rule.id, rule);
|
|
793
|
-
this.logger.info(`Loaded automation rule ${rule.id} (${rule.name})`);
|
|
794
|
-
}
|
|
795
|
-
async evaluateIncomingMessage(message) {
|
|
796
|
-
if (message.senderType === "customer") {
|
|
797
|
-
this.logger.debug(
|
|
798
|
-
`Evaluating automation triggers for incoming customer message ${message.id}`
|
|
799
|
-
);
|
|
800
|
-
const defaultNightReply = "C\u1EA3m \u01A1n b\u1EA1n \u0111\xE3 nh\u1EAFn tin. Hi\u1EC7n t\u1EA1i \u0111ang trong khung gi\u1EDD tr\u1EF1c \u0111\xEAm, tin nh\u1EAFn c\u1EE7a b\u1EA1n \u0111\xE3 \u0111\u01B0\u1EE3c ghi nh\u1EADn v\xE0 nh\xE2n vi\xEAn s\u1EBD h\u1ED7 tr\u1EE3 ngay khi m\u1EDF ca l\xE0m vi\u1EC7c!";
|
|
801
|
-
return defaultNightReply;
|
|
802
|
-
}
|
|
803
|
-
return null;
|
|
804
|
-
}
|
|
805
|
-
clear() {
|
|
806
|
-
this.campaigns.clear();
|
|
807
|
-
this.rules.clear();
|
|
808
|
-
}
|
|
809
|
-
};
|
|
810
|
-
|
|
811
|
-
// src/notifications/PushNotificationDispatcher.ts
|
|
812
|
-
var import_core5 = __toESM(require_dist());
|
|
813
|
-
var PushNotificationDispatcher = class {
|
|
814
|
-
logger;
|
|
815
|
-
constructor(logger2) {
|
|
816
|
-
this.logger = logger2 || (0, import_core5.createLogger)("PushNotificationDispatcher");
|
|
817
|
-
}
|
|
818
|
-
async dispatchMessageAlert(token, message) {
|
|
819
|
-
const title = `${message.senderName} (${message.platform.toUpperCase()})`;
|
|
820
|
-
const body = message.content || "New multimedia message received";
|
|
821
|
-
return this.sendNotification({
|
|
822
|
-
recipientToken: token,
|
|
823
|
-
title,
|
|
824
|
-
body,
|
|
825
|
-
data: {
|
|
826
|
-
messageId: message.id,
|
|
827
|
-
threadId: message.threadId,
|
|
828
|
-
platform: message.platform,
|
|
829
|
-
accountId: message.accountId
|
|
830
|
-
}
|
|
831
|
-
});
|
|
832
|
-
}
|
|
833
|
-
async sendNotification(payload) {
|
|
834
|
-
this.logger.debug(
|
|
835
|
-
`Dispatching push notification to device token: ${payload.recipientToken.slice(0, 8)}...`
|
|
836
|
-
);
|
|
837
|
-
return true;
|
|
838
|
-
}
|
|
839
|
-
};
|
|
840
|
-
|
|
841
|
-
// src/server/WorkerHttpServer.ts
|
|
842
|
-
var import_http = __toESM(require("http"));
|
|
843
|
-
var import_url = require("url");
|
|
844
|
-
var import_core10 = __toESM(require_dist());
|
|
845
|
-
|
|
846
|
-
// src/channels/runners/ZaloChannelRunner.ts
|
|
847
|
-
var import_core6 = __toESM(require_dist());
|
|
848
|
-
var ZaloChannelRunner = class {
|
|
849
|
-
constructor(accountId, sessionData, proxyData, logger2) {
|
|
850
|
-
this.accountId = accountId;
|
|
851
|
-
this.sessionData = sessionData;
|
|
852
|
-
this.proxyData = proxyData;
|
|
853
|
-
this.logger = logger2 || (0, import_core6.createLogger)(`ZaloRunner:${accountId}`);
|
|
854
|
-
}
|
|
855
|
-
platform = "zalo";
|
|
856
|
-
status = "offline";
|
|
857
|
-
logger;
|
|
858
|
-
messageHandler;
|
|
859
|
-
onMessage(handler) {
|
|
860
|
-
this.messageHandler = handler;
|
|
861
|
-
}
|
|
862
|
-
async start() {
|
|
863
|
-
const proxyInfo = this.proxyData ? ` via resident proxy ${this.proxyData.host || this.proxyData.ip || "configured"}` : " (direct)";
|
|
864
|
-
this.logger.info(
|
|
865
|
-
`Starting 24/7 headless Zalo runner for account ${this.accountId}${proxyInfo}...`
|
|
866
|
-
);
|
|
867
|
-
this.status = "active";
|
|
868
|
-
}
|
|
869
|
-
async stop() {
|
|
870
|
-
this.logger.info(
|
|
871
|
-
`Stopping headless Zalo runner for account ${this.accountId}...`
|
|
872
|
-
);
|
|
873
|
-
this.status = "offline";
|
|
874
|
-
}
|
|
875
|
-
getStatus() {
|
|
876
|
-
return this.status;
|
|
877
|
-
}
|
|
878
|
-
async sendMessage(message) {
|
|
879
|
-
this.logger.info(
|
|
880
|
-
`Headless Zalo runner sending message ${message.id} to thread ${message.threadId}`
|
|
881
|
-
);
|
|
882
|
-
return true;
|
|
883
|
-
}
|
|
884
|
-
/**
|
|
885
|
-
* Hook nhận tin nhắn từ socket WSS (zca-js/zocial) và bắn qua pipeline
|
|
886
|
-
*/
|
|
887
|
-
receiveIncomingMessage(message) {
|
|
888
|
-
if (this.status !== "active") return;
|
|
889
|
-
this.logger.debug(
|
|
890
|
-
`Zalo runner received message ${message.id} from sender ${message.senderId}`
|
|
891
|
-
);
|
|
892
|
-
this.messageHandler?.(message);
|
|
893
|
-
}
|
|
894
|
-
};
|
|
895
|
-
|
|
896
|
-
// src/channels/runners/FacebookChannelRunner.ts
|
|
897
|
-
var import_core7 = __toESM(require_dist());
|
|
898
|
-
var FacebookChannelRunner = class {
|
|
899
|
-
constructor(accountId, sessionData, proxyData, logger2) {
|
|
900
|
-
this.accountId = accountId;
|
|
901
|
-
this.sessionData = sessionData;
|
|
902
|
-
this.proxyData = proxyData;
|
|
903
|
-
this.logger = logger2 || (0, import_core7.createLogger)(`FacebookRunner:${accountId}`);
|
|
904
|
-
}
|
|
905
|
-
platform = "facebook";
|
|
906
|
-
status = "offline";
|
|
907
|
-
logger;
|
|
908
|
-
messageHandler;
|
|
909
|
-
onMessage(handler) {
|
|
910
|
-
this.messageHandler = handler;
|
|
911
|
-
}
|
|
912
|
-
async start() {
|
|
913
|
-
const proxyInfo = this.proxyData ? ` via resident proxy ${this.proxyData.host || this.proxyData.ip || "configured"}` : " (direct)";
|
|
914
|
-
this.logger.info(
|
|
915
|
-
`Starting 24/7 headless Facebook runner for account ${this.accountId}${proxyInfo}...`
|
|
916
|
-
);
|
|
917
|
-
this.status = "active";
|
|
918
|
-
}
|
|
919
|
-
async stop() {
|
|
920
|
-
this.logger.info(
|
|
921
|
-
`Stopping headless Facebook runner for account ${this.accountId}...`
|
|
922
|
-
);
|
|
923
|
-
this.status = "offline";
|
|
924
|
-
}
|
|
925
|
-
getStatus() {
|
|
926
|
-
return this.status;
|
|
927
|
-
}
|
|
928
|
-
async sendMessage(message) {
|
|
929
|
-
this.logger.info(
|
|
930
|
-
`Headless Facebook runner sending message ${message.id} to thread ${message.threadId}`
|
|
931
|
-
);
|
|
932
|
-
return true;
|
|
933
|
-
}
|
|
934
|
-
receiveIncomingMessage(message) {
|
|
935
|
-
if (this.status !== "active") return;
|
|
936
|
-
this.logger.debug(
|
|
937
|
-
`Facebook runner received message ${message.id} from sender ${message.senderId}`
|
|
938
|
-
);
|
|
939
|
-
this.messageHandler?.(message);
|
|
940
|
-
}
|
|
941
|
-
};
|
|
942
|
-
|
|
943
|
-
// src/channels/runners/TelegramChannelRunner.ts
|
|
944
|
-
var import_core8 = __toESM(require_dist());
|
|
945
|
-
var TelegramChannelRunner = class {
|
|
946
|
-
constructor(accountId, sessionData, proxyData, logger2) {
|
|
947
|
-
this.accountId = accountId;
|
|
948
|
-
this.sessionData = sessionData;
|
|
949
|
-
this.proxyData = proxyData;
|
|
950
|
-
this.logger = logger2 || (0, import_core8.createLogger)(`TelegramRunner:${accountId}`);
|
|
951
|
-
}
|
|
952
|
-
platform = "telegram";
|
|
953
|
-
status = "offline";
|
|
954
|
-
logger;
|
|
955
|
-
messageHandler;
|
|
956
|
-
onMessage(handler) {
|
|
957
|
-
this.messageHandler = handler;
|
|
958
|
-
}
|
|
959
|
-
async start() {
|
|
960
|
-
const proxyInfo = this.proxyData ? ` via resident proxy ${this.proxyData.host || this.proxyData.ip || "configured"}` : " (direct)";
|
|
961
|
-
this.logger.info(
|
|
962
|
-
`Starting 24/7 headless Telegram runner for account ${this.accountId}${proxyInfo}...`
|
|
963
|
-
);
|
|
964
|
-
this.status = "active";
|
|
965
|
-
}
|
|
966
|
-
async stop() {
|
|
967
|
-
this.logger.info(
|
|
968
|
-
`Stopping headless Telegram runner for account ${this.accountId}...`
|
|
969
|
-
);
|
|
970
|
-
this.status = "offline";
|
|
971
|
-
}
|
|
972
|
-
getStatus() {
|
|
973
|
-
return this.status;
|
|
974
|
-
}
|
|
975
|
-
async sendMessage(message) {
|
|
976
|
-
this.logger.info(
|
|
977
|
-
`Headless Telegram runner sending message ${message.id} to thread ${message.threadId}`
|
|
978
|
-
);
|
|
979
|
-
return true;
|
|
980
|
-
}
|
|
981
|
-
receiveIncomingMessage(message) {
|
|
982
|
-
if (this.status !== "active") return;
|
|
983
|
-
this.logger.debug(
|
|
984
|
-
`Telegram runner received message ${message.id} from sender ${message.senderId}`
|
|
985
|
-
);
|
|
986
|
-
this.messageHandler?.(message);
|
|
987
|
-
}
|
|
988
|
-
};
|
|
989
|
-
|
|
990
|
-
// src/channels/runners/WhatsAppChannelRunner.ts
|
|
991
|
-
var import_core9 = __toESM(require_dist());
|
|
992
|
-
var WhatsAppChannelRunner = class {
|
|
993
|
-
constructor(accountId, sessionData, proxyData, logger2) {
|
|
994
|
-
this.accountId = accountId;
|
|
995
|
-
this.sessionData = sessionData;
|
|
996
|
-
this.proxyData = proxyData;
|
|
997
|
-
this.logger = logger2 || (0, import_core9.createLogger)(`WhatsAppRunner:${accountId}`);
|
|
998
|
-
}
|
|
999
|
-
platform = "whatsapp";
|
|
1000
|
-
status = "offline";
|
|
1001
|
-
logger;
|
|
1002
|
-
messageHandler;
|
|
1003
|
-
onMessage(handler) {
|
|
1004
|
-
this.messageHandler = handler;
|
|
1005
|
-
}
|
|
1006
|
-
async start() {
|
|
1007
|
-
const proxyInfo = this.proxyData ? ` via resident proxy ${this.proxyData.host || this.proxyData.ip || "configured"}` : " (direct)";
|
|
1008
|
-
this.logger.info(
|
|
1009
|
-
`Starting 24/7 headless WhatsApp runner for account ${this.accountId}${proxyInfo}...`
|
|
1010
|
-
);
|
|
1011
|
-
this.status = "active";
|
|
1012
|
-
}
|
|
1013
|
-
async stop() {
|
|
1014
|
-
this.logger.info(
|
|
1015
|
-
`Stopping headless WhatsApp runner for account ${this.accountId}...`
|
|
1016
|
-
);
|
|
1017
|
-
this.status = "offline";
|
|
1018
|
-
}
|
|
1019
|
-
getStatus() {
|
|
1020
|
-
return this.status;
|
|
1021
|
-
}
|
|
1022
|
-
async sendMessage(message) {
|
|
1023
|
-
this.logger.info(
|
|
1024
|
-
`Headless WhatsApp runner sending message ${message.id} to thread ${message.threadId}`
|
|
1025
|
-
);
|
|
1026
|
-
return true;
|
|
1027
|
-
}
|
|
1028
|
-
receiveIncomingMessage(message) {
|
|
1029
|
-
if (this.status !== "active") return;
|
|
1030
|
-
this.logger.debug(
|
|
1031
|
-
`WhatsApp runner received message ${message.id} from sender ${message.senderId}`
|
|
1032
|
-
);
|
|
1033
|
-
this.messageHandler?.(message);
|
|
1034
|
-
}
|
|
1035
|
-
};
|
|
1036
|
-
|
|
1037
|
-
// src/server/WorkerHttpServer.ts
|
|
1038
|
-
var WorkerHttpServer = class {
|
|
1039
|
-
constructor(port, engine, lockManager, deltaQueue, logger2) {
|
|
1040
|
-
this.port = port;
|
|
1041
|
-
this.engine = engine;
|
|
1042
|
-
this.lockManager = lockManager;
|
|
1043
|
-
this.deltaQueue = deltaQueue;
|
|
1044
|
-
this.logger = logger2 || (0, import_core10.createLogger)("WorkerHttpServer");
|
|
1045
|
-
}
|
|
1046
|
-
logger;
|
|
1047
|
-
server = null;
|
|
1048
|
-
async start() {
|
|
1049
|
-
if (this.server) return;
|
|
1050
|
-
return new Promise((resolve, reject) => {
|
|
1051
|
-
this.server = import_http.default.createServer((req, res) => {
|
|
1052
|
-
this.handleRequest(req, res).catch((err) => {
|
|
1053
|
-
this.logger.error("Unhandled HTTP error:", err);
|
|
1054
|
-
if (!res.headersSent) {
|
|
1055
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1056
|
-
res.end(JSON.stringify({ error: "Internal Server Error" }));
|
|
1057
|
-
}
|
|
1058
|
-
});
|
|
1059
|
-
});
|
|
1060
|
-
this.server.listen(this.port, () => {
|
|
1061
|
-
this.logger.info(
|
|
1062
|
-
`\u{1F680} Worker HTTP Server listening on port ${this.port}`
|
|
1063
|
-
);
|
|
1064
|
-
resolve();
|
|
1065
|
-
});
|
|
1066
|
-
this.server.on("error", (err) => {
|
|
1067
|
-
this.logger.error(
|
|
1068
|
-
`Worker HTTP Server error on port ${this.port}:`,
|
|
1069
|
-
err
|
|
1070
|
-
);
|
|
1071
|
-
reject(err);
|
|
1072
|
-
});
|
|
1073
|
-
});
|
|
1074
|
-
}
|
|
1075
|
-
async stop() {
|
|
1076
|
-
if (!this.server) return;
|
|
1077
|
-
return new Promise((resolve) => {
|
|
1078
|
-
this.server?.close(() => {
|
|
1079
|
-
this.logger.info("Worker HTTP Server closed.");
|
|
1080
|
-
this.server = null;
|
|
1081
|
-
resolve();
|
|
1082
|
-
});
|
|
1083
|
-
});
|
|
1084
|
-
}
|
|
1085
|
-
async handleRequest(req, res) {
|
|
1086
|
-
const parsedUrl = new import_url.URL(
|
|
1087
|
-
req.url || "/",
|
|
1088
|
-
`http://${req.headers.host || "localhost"}`
|
|
1089
|
-
);
|
|
1090
|
-
const pathname = parsedUrl.pathname;
|
|
1091
|
-
const method = (req.method || "GET").toUpperCase();
|
|
1092
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1093
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
1094
|
-
res.setHeader(
|
|
1095
|
-
"Access-Control-Allow-Headers",
|
|
1096
|
-
"Content-Type, X-Softty-User-Id"
|
|
1097
|
-
);
|
|
1098
|
-
if (method === "OPTIONS") {
|
|
1099
|
-
res.writeHead(204);
|
|
1100
|
-
res.end();
|
|
1101
|
-
return;
|
|
1102
|
-
}
|
|
1103
|
-
if (method === "GET" && pathname === "/health") {
|
|
1104
|
-
const metrics = this.engine.getMetrics();
|
|
1105
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1106
|
-
res.end(
|
|
1107
|
-
JSON.stringify({
|
|
1108
|
-
status: "healthy",
|
|
1109
|
-
timestamp: Date.now(),
|
|
1110
|
-
pendingDeltaMessages: this.deltaQueue.getCount(),
|
|
1111
|
-
metrics
|
|
1112
|
-
})
|
|
1113
|
-
);
|
|
1114
|
-
return;
|
|
1115
|
-
}
|
|
1116
|
-
if (method === "POST" && pathname === "/api/v1/sentinel/handover") {
|
|
1117
|
-
const body = await this.readJsonBody(req);
|
|
1118
|
-
const { deviceId, sessions } = body || {};
|
|
1119
|
-
if (!Array.isArray(sessions)) {
|
|
1120
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1121
|
-
res.end(JSON.stringify({ error: "Invalid sessions array payload" }));
|
|
1122
|
-
return;
|
|
1123
|
-
}
|
|
1124
|
-
this.logger.info(
|
|
1125
|
-
`[Handover] Received handover request from device ${deviceId || "unknown"} with ${sessions.length} sessions.`
|
|
1126
|
-
);
|
|
1127
|
-
let activatedCount = 0;
|
|
1128
|
-
for (const item of sessions) {
|
|
1129
|
-
if (!item.id || !item.nightWorkerEnabled) continue;
|
|
1130
|
-
const lockGranted = this.lockManager.acquireLock(
|
|
1131
|
-
item.id,
|
|
1132
|
-
`worker_${item.id}`
|
|
1133
|
-
);
|
|
1134
|
-
if (!lockGranted) {
|
|
1135
|
-
this.logger.warn(
|
|
1136
|
-
`Skipping account ${item.id} because lock was not acquired.`
|
|
1137
|
-
);
|
|
1138
|
-
continue;
|
|
1139
|
-
}
|
|
1140
|
-
let runner;
|
|
1141
|
-
const platform = item.platform || "zalo";
|
|
1142
|
-
if (platform === "zalo") {
|
|
1143
|
-
runner = new ZaloChannelRunner(item.id, item.sessionData);
|
|
1144
|
-
} else if (platform === "facebook") {
|
|
1145
|
-
runner = new FacebookChannelRunner(item.id, item.sessionData);
|
|
1146
|
-
} else if (platform === "telegram") {
|
|
1147
|
-
runner = new TelegramChannelRunner(item.id, item.sessionData);
|
|
1148
|
-
} else if (platform === "whatsapp") {
|
|
1149
|
-
runner = new WhatsAppChannelRunner(item.id, item.sessionData);
|
|
1150
|
-
}
|
|
1151
|
-
if (runner) {
|
|
1152
|
-
this.engine.channelRunnerRegistry.register(runner);
|
|
1153
|
-
await runner.start();
|
|
1154
|
-
this.engine.registerAccount({
|
|
1155
|
-
id: item.id,
|
|
1156
|
-
platform,
|
|
1157
|
-
accountName: item.accountName || item.id,
|
|
1158
|
-
status: "active",
|
|
1159
|
-
nightWorkerEnabled: true
|
|
1160
|
-
});
|
|
1161
|
-
activatedCount++;
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1165
|
-
res.end(
|
|
1166
|
-
JSON.stringify({
|
|
1167
|
-
success: true,
|
|
1168
|
-
message: "Sessions handed over successfully",
|
|
1169
|
-
activatedCount,
|
|
1170
|
-
timestamp: Date.now()
|
|
1171
|
-
})
|
|
1172
|
-
);
|
|
1173
|
-
return;
|
|
1174
|
-
}
|
|
1175
|
-
if (method === "POST" && pathname === "/api/v1/sentinel/reclaim") {
|
|
1176
|
-
const body = await this.readJsonBody(req);
|
|
1177
|
-
const { deviceId } = body || {};
|
|
1178
|
-
this.logger.info(
|
|
1179
|
-
`[Reclaim] Master device ${deviceId || "desktop"} reclaiming all active sessions. Stopping cloud runners...`
|
|
1180
|
-
);
|
|
1181
|
-
const count = this.engine.channelRunnerRegistry.getCount();
|
|
1182
|
-
await this.engine.channelRunnerRegistry.stopAll();
|
|
1183
|
-
this.lockManager.releaseAllForOwner(`worker`);
|
|
1184
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1185
|
-
res.end(
|
|
1186
|
-
JSON.stringify({
|
|
1187
|
-
success: true,
|
|
1188
|
-
reclaimedCount: count,
|
|
1189
|
-
timestamp: Date.now()
|
|
1190
|
-
})
|
|
1191
|
-
);
|
|
1192
|
-
return;
|
|
1193
|
-
}
|
|
1194
|
-
if (method === "GET" && pathname === "/api/v1/sync/delta") {
|
|
1195
|
-
const accountId = parsedUrl.searchParams.get("accountId") || void 0;
|
|
1196
|
-
const since = parseInt(parsedUrl.searchParams.get("since") || "0", 10);
|
|
1197
|
-
const deltas = this.deltaQueue.getDeltas(accountId, since);
|
|
1198
|
-
this.logger.info(
|
|
1199
|
-
`[DeltaSync] Serving ${deltas.length} night messages (since: ${since}, account: ${accountId || "all"})`
|
|
1200
|
-
);
|
|
1201
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1202
|
-
res.end(
|
|
1203
|
-
JSON.stringify({
|
|
1204
|
-
success: true,
|
|
1205
|
-
messages: deltas,
|
|
1206
|
-
count: deltas.length,
|
|
1207
|
-
syncedTimestamp: Date.now()
|
|
1208
|
-
})
|
|
1209
|
-
);
|
|
1210
|
-
return;
|
|
1211
|
-
}
|
|
1212
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1213
|
-
res.end(JSON.stringify({ error: "Endpoint not found" }));
|
|
1214
|
-
}
|
|
1215
|
-
readJsonBody(req) {
|
|
1216
|
-
return new Promise((resolve) => {
|
|
1217
|
-
let bodyStr = "";
|
|
1218
|
-
req.on("data", (chunk) => {
|
|
1219
|
-
bodyStr += chunk;
|
|
1220
|
-
});
|
|
1221
|
-
req.on("end", () => {
|
|
1222
|
-
try {
|
|
1223
|
-
resolve(JSON.parse(bodyStr));
|
|
1224
|
-
} catch {
|
|
1225
|
-
resolve({});
|
|
1226
|
-
}
|
|
1227
|
-
});
|
|
1228
|
-
});
|
|
1229
|
-
}
|
|
1230
|
-
};
|
|
1231
|
-
|
|
1232
|
-
// src/engine/WorkerEngine.ts
|
|
1233
|
-
var WorkerEngine = class {
|
|
1234
|
-
constructor(config) {
|
|
1235
|
-
this.config = config;
|
|
1236
|
-
this.logger = (0, import_core11.createLogger)(`WorkerEngine:${config.workerId}`);
|
|
1237
|
-
this.channelRunnerRegistry = new ChannelRunnerRegistry(this.logger);
|
|
1238
|
-
this.lockManager = new DistributedLockManager(6e4, this.logger);
|
|
1239
|
-
this.deltaQueue = new CloudPendingMessageQueue(1e4, this.logger);
|
|
1240
|
-
this.automationEngine = new HeadlessAutomationEngine(this.logger);
|
|
1241
|
-
this.pushDispatcher = new PushNotificationDispatcher(this.logger);
|
|
1242
|
-
this.httpServer = new WorkerHttpServer(
|
|
1243
|
-
config.port,
|
|
1244
|
-
this,
|
|
1245
|
-
this.lockManager,
|
|
1246
|
-
this.deltaQueue,
|
|
1247
|
-
this.logger
|
|
1248
|
-
);
|
|
1249
|
-
this.lockManager.onPreempt(async (accountId) => {
|
|
1250
|
-
this.logger.info(
|
|
1251
|
-
`Preemption callback: stopping WSS for account ${accountId}...`
|
|
1252
|
-
);
|
|
1253
|
-
const allRunners = this.channelRunnerRegistry.getAll();
|
|
1254
|
-
for (const r of allRunners) {
|
|
1255
|
-
if (r.accountId === accountId) {
|
|
1256
|
-
await r.stop();
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
});
|
|
1260
|
-
}
|
|
1261
|
-
logger;
|
|
1262
|
-
channelRunnerRegistry;
|
|
1263
|
-
lockManager;
|
|
1264
|
-
deltaQueue;
|
|
1265
|
-
automationEngine;
|
|
1266
|
-
pushDispatcher;
|
|
1267
|
-
httpServer;
|
|
1268
|
-
status = "idle";
|
|
1269
|
-
startTime = 0;
|
|
1270
|
-
activeAccounts = /* @__PURE__ */ new Map();
|
|
1271
|
-
messagesProcessed = 0;
|
|
1272
|
-
heartbeatTimer;
|
|
1273
|
-
async start() {
|
|
1274
|
-
if (this.status === "running") {
|
|
1275
|
-
this.logger.warn("Worker is already running.");
|
|
1276
|
-
return;
|
|
1277
|
-
}
|
|
1278
|
-
this.logger.info("Starting ZPlus 24/7 Headless Worker Engine...");
|
|
1279
|
-
this.status = "initializing";
|
|
1280
|
-
this.startTime = Date.now();
|
|
1281
|
-
try {
|
|
1282
|
-
await this.httpServer.start();
|
|
1283
|
-
} catch (err) {
|
|
1284
|
-
this.logger.error("Failed to start Worker HTTP Ingress Server:", err);
|
|
1285
|
-
}
|
|
1286
|
-
this.heartbeatTimer = setInterval(() => {
|
|
1287
|
-
this.sendHeartbeat();
|
|
1288
|
-
}, this.config.heartbeatIntervalMs);
|
|
1289
|
-
this.status = "running";
|
|
1290
|
-
this.logger.info(
|
|
1291
|
-
`\u2705 ZPlus Worker Engine started successfully [ID: ${this.config.workerId}, Port: ${this.config.port}]`
|
|
1292
|
-
);
|
|
1293
|
-
}
|
|
1294
|
-
async stop() {
|
|
1295
|
-
this.logger.info("Stopping ZPlus Worker Engine...");
|
|
1296
|
-
if (this.heartbeatTimer) {
|
|
1297
|
-
clearInterval(this.heartbeatTimer);
|
|
1298
|
-
this.heartbeatTimer = void 0;
|
|
1299
|
-
}
|
|
1300
|
-
await this.httpServer.stop();
|
|
1301
|
-
await this.channelRunnerRegistry.stopAll();
|
|
1302
|
-
this.status = "stopped";
|
|
1303
|
-
this.logger.info("ZPlus Worker Engine stopped.");
|
|
1304
|
-
}
|
|
1305
|
-
registerRunner(runner) {
|
|
1306
|
-
runner.onMessage?.((msg) => {
|
|
1307
|
-
this.handleIncomingMessage(msg, runner);
|
|
1308
|
-
});
|
|
1309
|
-
this.channelRunnerRegistry.register(runner);
|
|
1310
|
-
}
|
|
1311
|
-
registerAccount(account) {
|
|
1312
|
-
this.activeAccounts.set(account.id, account);
|
|
1313
|
-
this.logger.info(
|
|
1314
|
-
`Registered active account: ${account.accountName} [${account.platform}]`
|
|
1315
|
-
);
|
|
1316
|
-
}
|
|
1317
|
-
unregisterAccount(accountId) {
|
|
1318
|
-
this.activeAccounts.delete(accountId);
|
|
1319
|
-
this.logger.info(`Unregistered account: ${accountId}`);
|
|
1320
|
-
}
|
|
1321
|
-
recordMessageProcessed() {
|
|
1322
|
-
this.messagesProcessed++;
|
|
1323
|
-
}
|
|
1324
|
-
async handleIncomingMessage(message, runner) {
|
|
1325
|
-
this.recordMessageProcessed();
|
|
1326
|
-
this.deltaQueue.enqueue(message);
|
|
1327
|
-
try {
|
|
1328
|
-
const autoReplyText = await this.automationEngine.evaluateIncomingMessage(message);
|
|
1329
|
-
if (autoReplyText && runner) {
|
|
1330
|
-
this.logger.info(
|
|
1331
|
-
`[NightAutoReply] Sending automated reply to thread ${message.threadId}: "${autoReplyText}"`
|
|
1332
|
-
);
|
|
1333
|
-
const replyMsg = {
|
|
1334
|
-
id: `reply_${Date.now()}`,
|
|
1335
|
-
platform: message.platform,
|
|
1336
|
-
conversationId: message.conversationId,
|
|
1337
|
-
threadId: message.threadId,
|
|
1338
|
-
accountId: message.accountId,
|
|
1339
|
-
senderId: message.accountId,
|
|
1340
|
-
senderName: "ZPlus Night Assistant",
|
|
1341
|
-
senderType: "agent",
|
|
1342
|
-
messageType: "text",
|
|
1343
|
-
content: autoReplyText,
|
|
1344
|
-
timestamp: Date.now(),
|
|
1345
|
-
isSelf: true,
|
|
1346
|
-
status: "sent"
|
|
1347
|
-
};
|
|
1348
|
-
await runner.sendMessage(replyMsg);
|
|
1349
|
-
}
|
|
1350
|
-
} catch (err) {
|
|
1351
|
-
this.logger.warn("Auto-reply evaluation notice:", err);
|
|
1352
|
-
}
|
|
1353
|
-
try {
|
|
1354
|
-
await this.pushDispatcher.dispatchMessageAlert(
|
|
1355
|
-
"fcm_token_mobile_owner",
|
|
1356
|
-
message
|
|
1357
|
-
);
|
|
1358
|
-
} catch (err) {
|
|
1359
|
-
this.logger.debug("Push notification dispatch notice:", err);
|
|
1360
|
-
}
|
|
1361
|
-
}
|
|
1362
|
-
getMetrics() {
|
|
1363
|
-
const uptime = this.startTime > 0 ? Math.floor((Date.now() - this.startTime) / 1e3) : 0;
|
|
1364
|
-
const memUsage = process.memoryUsage().rss / (1024 * 1024);
|
|
1365
|
-
return {
|
|
1366
|
-
workerId: this.config.workerId,
|
|
1367
|
-
status: this.status,
|
|
1368
|
-
uptimeSeconds: uptime,
|
|
1369
|
-
activeAccountsCount: this.activeAccounts.size,
|
|
1370
|
-
totalMessagesProcessed: this.messagesProcessed,
|
|
1371
|
-
memoryUsageMb: Math.round(memUsage * 100) / 100,
|
|
1372
|
-
lastHeartbeat: (/* @__PURE__ */ new Date()).toISOString()
|
|
1373
|
-
};
|
|
1374
|
-
}
|
|
1375
|
-
sendHeartbeat() {
|
|
1376
|
-
const metrics = this.getMetrics();
|
|
1377
|
-
this.logger.info(
|
|
1378
|
-
`\u{1F493} Heartbeat: status=${metrics.status}, accounts=${metrics.activeAccountsCount}, msgs=${metrics.totalMessagesProcessed}, pendingDelta=${this.deltaQueue.getCount()}, uptime=${metrics.uptimeSeconds}s`
|
|
1379
|
-
);
|
|
1380
|
-
}
|
|
1381
|
-
};
|
|
1382
|
-
|
|
1383
|
-
// src/cli.ts
|
|
1384
|
-
var import_core12 = __toESM(require_dist());
|
|
1385
|
-
var logger = (0, import_core12.createLogger)("CLI");
|
|
1386
|
-
async function main() {
|
|
1387
|
-
logger.info("==================================================");
|
|
1388
|
-
logger.info("\u{1F680} ZPlus Headless 24/7 Automation Worker CLI");
|
|
1389
|
-
logger.info("==================================================");
|
|
1390
|
-
const config = WorkerConfig.loadFromEnv();
|
|
1391
|
-
const engine = new WorkerEngine(config);
|
|
1392
|
-
process.on("SIGINT", async () => {
|
|
1393
|
-
logger.info("Received SIGINT, shutting down gracefully...");
|
|
1394
|
-
await engine.stop();
|
|
1395
|
-
process.exit(0);
|
|
1396
|
-
});
|
|
1397
|
-
process.on("SIGTERM", async () => {
|
|
1398
|
-
logger.info("Received SIGTERM, shutting down gracefully...");
|
|
1399
|
-
await engine.stop();
|
|
1400
|
-
process.exit(0);
|
|
1401
|
-
});
|
|
1402
|
-
await engine.start();
|
|
1403
|
-
}
|
|
1404
|
-
if (process.argv[1] && process.argv[1].endsWith("cli.js")) {
|
|
1405
|
-
main().catch((err) => {
|
|
1406
|
-
logger.error("Worker fatal error:", err);
|
|
1407
|
-
process.exit(1);
|
|
1408
|
-
});
|
|
1409
|
-
}
|
|
2
|
+
"use strict";var Le=Object.create;var ae=Object.defineProperty;var _e=Object.getOwnPropertyDescriptor;var De=Object.getOwnPropertyNames;var ke=Object.getPrototypeOf,Ce=Object.prototype.hasOwnProperty;var y=(s,e)=>()=>(e||s((e={exports:{}}).exports,e),e.exports);var Oe=(s,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of De(e))!Ce.call(s,i)&&i!==t&&ae(s,i,{get:()=>e[i],enumerable:!(r=_e(e,i))||r.enumerable});return s};var d=(s,e,t)=>(t=s!=null?Le(ke(s)):{},Oe(e||!s||!s.__esModule?ae(t,"default",{value:s,enumerable:!0}):t,s));var ce=y($=>{"use strict";Object.defineProperty($,"__esModule",{value:!0});$.ConsoleLogger=void 0;$.createLogger=xe;var S=class{context;constructor(e="CORE"){this.context=e}format(e,t){return`[${new Date().toISOString()}] [${e}] [${this.context}] ${t}`}info(e,...t){console.log(this.format("INFO",e),...t)}debug(e,...t){console.debug(this.format("DEBUG",e),...t)}warn(e,...t){console.warn(this.format("WARN",e),...t)}error(e,...t){console.error(this.format("ERROR",e),...t)}};$.ConsoleLogger=S;function xe(s){return new S(s)}});var le=y(f=>{"use strict";var Ne=f&&f.__createBinding||(Object.create?(function(s,e,t,r){r===void 0&&(r=t);var i=Object.getOwnPropertyDescriptor(e,t);(!i||("get"in i?!e.__esModule:i.writable||i.configurable))&&(i={enumerable:!0,get:function(){return e[t]}}),Object.defineProperty(s,r,i)}):(function(s,e,t,r){r===void 0&&(r=t),s[r]=e[t]})),Ue=f&&f.__setModuleDefault||(Object.create?(function(s,e){Object.defineProperty(s,"default",{enumerable:!0,value:e})}):function(s,e){s.default=e}),He=f&&f.__importStar||(function(){var s=function(e){return s=Object.getOwnPropertyNames||function(t){var r=[];for(var i in t)Object.prototype.hasOwnProperty.call(t,i)&&(r[r.length]=i);return r},s(e)};return function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r=s(e),i=0;i<r.length;i++)r[i]!=="default"&&Ne(t,e,r[i]);return Ue(t,e),t}})();Object.defineProperty(f,"__esModule",{value:!0});f.CryptoUtils=void 0;var E=He(require("crypto")),F=class{static md5(e){return E.createHash("md5").update(e).digest("hex")}static sha256(e){return E.createHash("sha256").update(e).digest("hex")}static buildSafeDocumentId(e,t){if(!t)return`doc_${E.randomBytes(12).toString("hex")}`;let r=String(t).trim();return/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,35}$/.test(r)?r:`z_${this.md5(`${e}:${r}`)}`}static randomToken(e=16){return E.randomBytes(e).toString("hex")}};f.CryptoUtils=F});var ue=y(L=>{"use strict";Object.defineProperty(L,"__esModule",{value:!0});L.TypedEventBus=void 0;var We=require("events"),B=class{emitter=new We.EventEmitter;constructor(e=50){this.emitter.setMaxListeners(e)}emit(e,t){return this.emitter.emit(e,t)}on(e,t){return this.emitter.on(e,t),this}once(e,t){return this.emitter.once(e,t),this}off(e,t){return this.emitter.off(e,t),this}removeAllListeners(e){return this.emitter.removeAllListeners(e),this}};L.TypedEventBus=B});var ge=y(I=>{"use strict";Object.defineProperty(I,"__esModule",{value:!0});I.JsonUtils=I.AsyncUtils=void 0;var G=class{static sleep(e){return new Promise(t=>setTimeout(t,e))}static async retryWithBackoff(e,t={}){let{maxRetries:r=3,initialDelayMs:i=500,maxDelayMs:n=1e4,backoffFactor:o=2,shouldRetry:l=()=>!0}=t,g=i,w;for(let a=1;a<=r;a++)try{return await e()}catch(T){if(w=T,a>=r||!l(T))throw T;await this.sleep(g),g=Math.min(g*o,n)}throw w}};I.AsyncUtils=G;var z=class{static safeParse(e,t){if(!e||typeof e!="string")return t;try{return JSON.parse(e)}catch{return t}}static safeStringify(e,t="{}"){if(e==null)return t;try{return JSON.stringify(e)}catch{return t}}};I.JsonUtils=z});var de=y(c=>{"use strict";Object.defineProperty(c,"__esModule",{value:!0});c.DatabaseError=c.NetworkError=c.RateLimitError=c.ConflictError=c.NotFoundError=c.ForbiddenError=c.AuthenticationError=c.ValidationError=c.ZPlusError=void 0;var h=class extends Error{code;statusCode;details;constructor(e,t="INTERNAL_ERROR",r=500,i){super(e),this.name=this.constructor.name,this.code=t,this.statusCode=r,this.details=i,Object.setPrototypeOf(this,new.target.prototype)}};c.ZPlusError=h;var Z=class extends h{constructor(e,t){super(e,"VALIDATION_ERROR",400,t)}};c.ValidationError=Z;var J=class extends h{constructor(e="Authentication required or token expired",t){super(e,"UNAUTHORIZED",401,t)}};c.AuthenticationError=J;var Q=class extends h{constructor(e="Access forbidden for current user or device",t){super(e,"FORBIDDEN",403,t)}};c.ForbiddenError=Q;var V=class extends h{constructor(e,t){let r=t?`${e} with identifier '${t}' not found`:`${e} not found`;super(r,"NOT_FOUND",404,{resource:e,id:t})}};c.NotFoundError=V;var K=class extends h{constructor(e,t){super(e,"CONFLICT",409,t)}};c.ConflictError=K;var X=class extends h{retryAfterSeconds;constructor(e="Rate limit exceeded. Please cool down.",t){super(e,"RATE_LIMIT_EXCEEDED",429,{retryAfterSeconds:t}),this.retryAfterSeconds=t}};c.RateLimitError=X;var Y=class extends h{constructor(e,t){super(e,"NETWORK_ERROR",503,t)}};c.NetworkError=Y;var ee=class extends h{constructor(e,t){super(e,"DATABASE_ERROR",500,t)}};c.DatabaseError=ee});var pe=y(P=>{"use strict";Object.defineProperty(P,"__esModule",{value:!0});P.DataSanitizer=P.PhoneNormalizer=void 0;var te=class{static normalizeToLocal(e){if(!e)return"";let t=e.replace(/[\s\-().+]/g,"");return t.startsWith("84")&&t.length>=11?t="0"+t.slice(2):t.startsWith("0084")?t="0"+t.slice(4):!t.startsWith("0")&&t.length===9&&(t="0"+t),t}static normalizeToInternational(e){let t=this.normalizeToLocal(e);return!t||t.length!==10||!t.startsWith("0")?e:"+84"+t.slice(1)}static isValidVietnameseMobile(e){let t=this.normalizeToLocal(e);return/^(03|05|07|08|09)[0-9]{8}$/.test(t)}};P.PhoneNormalizer=te;var re=class{static stripHtml(e){return e?e.replace(/<[^>]*>?/gm,"").trim():""}static maskPhoneNumber(e){if(!e||e.length<7)return e;let t=e.slice(0,3),r=e.slice(-3);return`${t}****${r}`}static maskToken(e){return e?e.length<=8?"********":e.slice(0,4)+"..."+e.slice(-4):""}};P.DataSanitizer=re});var he=y(_=>{"use strict";Object.defineProperty(_,"__esModule",{value:!0});_.IdGenerator=void 0;var ie=require("crypto"),ne="0123456789ABCDEFGHJKMNPQRSTVWXYZ",se=ne.length,oe=class{static generateUlid(e=Date.now()){let t="",r=e;for(let o=9;o>=0;o--){let l=r%se;t=ne.charAt(l)+t,r=Math.floor(r/se)}let i=(0,ie.randomBytes)(10),n="";for(let o=0;o<16;o++){let l=i[o%10]^o*17;n+=ne.charAt(l%se)}return t+n}static generateUuidV4(){let e=(0,ie.randomBytes)(16);e[6]=e[6]&15|64,e[8]=e[8]&63|128;let t=e.toString("hex");return`${t.slice(0,8)}-${t.slice(8,12)}-${t.slice(12,16)}-${t.slice(16,20)}-${t.slice(20,32)}`}static generateShortId(e="",t=8){let r=(0,ie.randomBytes)(t),i="23456789ABCDEFGHJKLMNPQRSTUVWXYZ",n="";for(let o=0;o<t;o++)n+=i.charAt(r[o]%i.length);return e?`${e}_${n}`:n}};_.IdGenerator=oe});var fe=y(m=>{"use strict";Object.defineProperty(m,"__esModule",{value:!0});m.CLOUD_ENDPOINTS=m.LIMITS=m.DEFAULT_PAGINATION=m.PLATFORMS=void 0;m.PLATFORMS={ZALO:"zalo",FACEBOOK:"facebook",TELEGRAM:"telegram",WHATSAPP:"whatsapp"};m.DEFAULT_PAGINATION={PAGE_SIZE:50,MAX_PAGE_SIZE:200};m.LIMITS={MAX_ATTACHMENT_BYTES:100*1024*1024,MAX_IMAGE_BYTES:25*1024*1024,MAX_BATCH_SYNC_ITEMS:500,HEARTBEAT_INTERVAL_MS:3e4,WSS_RECONNECT_MAX_DELAY_MS:6e4};m.CLOUD_ENDPOINTS={DEFAULT_APPWRITE_ENDPOINT:"https://cloud.appwrite.io/v1",DEFAULT_PROJECT_ID:"zplus-cloud"}});var p=y(u=>{"use strict";var qe=u&&u.__createBinding||(Object.create?(function(s,e,t,r){r===void 0&&(r=t);var i=Object.getOwnPropertyDescriptor(e,t);(!i||("get"in i?!e.__esModule:i.writable||i.configurable))&&(i={enumerable:!0,get:function(){return e[t]}}),Object.defineProperty(s,r,i)}):(function(s,e,t,r){r===void 0&&(r=t),s[r]=e[t]})),v=u&&u.__exportStar||function(s,e){for(var t in s)t!=="default"&&!Object.prototype.hasOwnProperty.call(e,t)&&qe(e,s,t)};Object.defineProperty(u,"__esModule",{value:!0});v(ce(),u);v(le(),u);v(ue(),u);v(ge(),u);v(de(),u);v(pe(),u);v(he(),u);v(fe(),u)});var R=class{static loadFromEnv(){return{workerId:process.env.WORKER_ID||`worker_${process.pid}`,port:Number(process.env.PORT||8080),appwriteEndpoint:process.env.APPWRITE_ENDPOINT,appwriteProjectId:process.env.APPWRITE_PROJECT_ID,appwriteApiKey:process.env.APPWRITE_API_KEY,databaseId:process.env.APPWRITE_DATABASE_ID||"zplus_app",heartbeatIntervalMs:Number(process.env.HEARTBEAT_INTERVAL_MS||3e4),logLevel:process.env.LOG_LEVEL||"info"}}};var Se=d(p());var me=d(p()),D=class{runners=new Map;logger;constructor(e){this.logger=e||(0,me.createLogger)("ChannelRunnerRegistry")}register(e){let t=`${e.platform}:${e.accountId}`;this.runners.has(t)&&this.logger.warn(`Overwriting existing runner for ${t}`),this.runners.set(t,e),this.logger.info(`Registered runner for ${t}`)}unregister(e,t){let r=`${e}:${t}`,i=this.runners.get(r);return i?(i.stop().catch(n=>{this.logger.error(`Error stopping runner ${r} during unregister`,n)}),this.runners.delete(r),this.logger.info(`Unregistered runner for ${r}`),!0):!1}get(e,t){return this.runners.get(`${e}:${t}`)}getAll(){return Array.from(this.runners.values())}getCount(){return this.runners.size}async startAll(){this.logger.info(`Starting all ${this.runners.size} registered channel runners...`);for(let[e,t]of this.runners.entries())try{await t.start()}catch(r){this.logger.error(`Failed to start runner ${e}:`,r)}}async stopAll(){this.logger.info(`Stopping all ${this.runners.size} channel runners...`);for(let[e,t]of this.runners.entries())try{await t.stop()}catch(r){this.logger.error(`Failed to stop runner ${e}:`,r)}this.runners.clear()}};var ye=d(p()),k=class{constructor(e=6e4,t){this.defaultTtlMs=e;this.logger=t||(0,ye.createLogger)("DistributedLockManager")}logger;locks=new Map;preemptCallbacks=[];onPreempt(e){this.preemptCallbacks.push(e)}acquireLock(e,t,r=this.defaultTtlMs){let i=Date.now(),n=this.locks.get(e);return n&&n.expiresAt>i&&n.ownerId!==t?(this.logger.warn(`Lock denied for account ${e}: currently held by ${n.ownerId} (expires in ${n.expiresAt-i}ms)`),!1):(this.locks.set(e,{accountId:e,ownerId:t,acquiredAt:i,expiresAt:i+r}),this.logger.info(`\u2705 Lock granted: account=${e}, owner=${t}, ttl=${r}ms`),!0)}async preemptLock(e,t){let r=this.locks.get(e);this.logger.info(`\u26A1 Preemption triggered for account ${e} by master ${t} (previous owner: ${r?.ownerId||"none"})`);let i=this.preemptCallbacks.map(o=>{try{return Promise.resolve(o(e,t))}catch(l){return this.logger.error(`Error in preempt callback for ${e}:`,l),Promise.resolve()}});await Promise.race([Promise.all(i),new Promise(o=>setTimeout(o,1e3))]);let n=Date.now();return this.locks.set(e,{accountId:e,ownerId:t,acquiredAt:n,expiresAt:n+this.defaultTtlMs}),this.logger.info(`\u2705 Account ${e} preempted successfully by ${t}.`),!0}releaseLock(e,t){let r=this.locks.get(e);return r?t&&r.ownerId!==t?(this.logger.warn(`Attempted unauthorized lock release for ${e} by ${t} (real owner: ${r.ownerId})`),!1):(this.locks.delete(e),this.logger.info(`\u{1F513} Lock released for account ${e}.`),!0):!0}releaseAllForOwner(e){let t=0;for(let[r,i]of this.locks.entries())i.ownerId===e&&(this.locks.delete(r),t++);return t}isLocked(e){let t=this.locks.get(e);return t?t.expiresAt>Date.now():!1}getLockOwner(e){let t=this.locks.get(e);return!t||t.expiresAt<=Date.now()?null:t.ownerId}};var ve=d(p()),C=class{logger;queue=[];maxCapacity;constructor(e=1e4,t){this.maxCapacity=e,this.logger=t||(0,ve.createLogger)("CloudPendingMessageQueue")}enqueue(e){if(this.queue.length>=this.maxCapacity){let t=this.queue.shift();this.logger.warn(`Queue capacity reached (${this.maxCapacity}). Dropped oldest message: ${t?.id}`)}this.queue.push(e),this.logger.debug(`Enqueued pending message ${e.id} for account ${e.accountId} (Total pending: ${this.queue.length})`)}getDeltas(e,t=0){return this.queue.filter(r=>{let i=!e||r.accountId===e,n=r.timestamp>=t;return i&&n})}purgeSynced(e,t){let r=this.queue.length,i=[];for(let o=0;o<this.queue.length;o++){let l=this.queue[o];l.timestamp<=e&&(!t||l.accountId===t)&&i.push(o)}for(let o=i.length-1;o>=0;o--)this.queue.splice(i[o],1);let n=r-this.queue.length;return n>0&&this.logger.info(`Purged ${n} synced delta messages from queue.`),n}getCount(){return this.queue.length}clear(){this.queue.length=0}};var be=d(p()),O=class{logger;campaigns=new Map;rules=new Map;constructor(e){this.logger=e||(0,be.createLogger)("HeadlessAutomationEngine")}registerCampaign(e){this.campaigns.set(e.id,e),this.logger.info(`Loaded campaign ${e.id} (${e.name})`)}registerRule(e){this.rules.set(e.id,e),this.logger.info(`Loaded automation rule ${e.id} (${e.name})`)}async evaluateIncomingMessage(e){return e.senderType==="customer"?(this.logger.debug(`Evaluating automation triggers for incoming customer message ${e.id}`),"C\u1EA3m \u01A1n b\u1EA1n \u0111\xE3 nh\u1EAFn tin. Hi\u1EC7n t\u1EA1i \u0111ang trong khung gi\u1EDD tr\u1EF1c \u0111\xEAm, tin nh\u1EAFn c\u1EE7a b\u1EA1n \u0111\xE3 \u0111\u01B0\u1EE3c ghi nh\u1EADn v\xE0 nh\xE2n vi\xEAn s\u1EBD h\u1ED7 tr\u1EE3 ngay khi m\u1EDF ca l\xE0m vi\u1EC7c!"):null}clear(){this.campaigns.clear(),this.rules.clear()}};var Ie=d(p()),x=class{logger;constructor(e){this.logger=e||(0,Ie.createLogger)("PushNotificationDispatcher")}async dispatchMessageAlert(e,t){let r=`${t.senderName} (${t.platform.toUpperCase()})`,i=t.content||"New multimedia message received";return this.sendNotification({recipientToken:e,title:r,body:i,data:{messageId:t.id,threadId:t.threadId,platform:t.platform,accountId:t.accountId}})}async sendNotification(e){return this.logger.debug(`Dispatching push notification to device token: ${e.recipientToken.slice(0,8)}...`),!0}};var $e=d(require("http")),Te=require("url"),Re=d(p());var Pe=d(p()),N=class{constructor(e,t,r,i){this.accountId=e;this.sessionData=t;this.proxyData=r;this.logger=i||(0,Pe.createLogger)(`ZaloRunner:${e}`)}platform="zalo";status="offline";logger;messageHandler;onMessage(e){this.messageHandler=e}async start(){let e=this.proxyData?` via resident proxy ${this.proxyData.host||this.proxyData.ip||"configured"}`:" (direct)";this.logger.info(`Starting 24/7 headless Zalo runner for account ${this.accountId}${e}...`),this.status="active"}async stop(){this.logger.info(`Stopping headless Zalo runner for account ${this.accountId}...`),this.status="offline"}getStatus(){return this.status}async sendMessage(e){return this.logger.info(`Headless Zalo runner sending message ${e.id} to thread ${e.threadId}`),!0}receiveIncomingMessage(e){this.status==="active"&&(this.logger.debug(`Zalo runner received message ${e.id} from sender ${e.senderId}`),this.messageHandler?.(e))}};var Ae=d(p()),U=class{constructor(e,t,r,i){this.accountId=e;this.sessionData=t;this.proxyData=r;this.logger=i||(0,Ae.createLogger)(`FacebookRunner:${e}`)}platform="facebook";status="offline";logger;messageHandler;onMessage(e){this.messageHandler=e}async start(){let e=this.proxyData?` via resident proxy ${this.proxyData.host||this.proxyData.ip||"configured"}`:" (direct)";this.logger.info(`Starting 24/7 headless Facebook runner for account ${this.accountId}${e}...`),this.status="active"}async stop(){this.logger.info(`Stopping headless Facebook runner for account ${this.accountId}...`),this.status="offline"}getStatus(){return this.status}async sendMessage(e){return this.logger.info(`Headless Facebook runner sending message ${e.id} to thread ${e.threadId}`),!0}receiveIncomingMessage(e){this.status==="active"&&(this.logger.debug(`Facebook runner received message ${e.id} from sender ${e.senderId}`),this.messageHandler?.(e))}};var we=d(p()),H=class{constructor(e,t,r,i){this.accountId=e;this.sessionData=t;this.proxyData=r;this.logger=i||(0,we.createLogger)(`TelegramRunner:${e}`)}platform="telegram";status="offline";logger;messageHandler;onMessage(e){this.messageHandler=e}async start(){let e=this.proxyData?` via resident proxy ${this.proxyData.host||this.proxyData.ip||"configured"}`:" (direct)";this.logger.info(`Starting 24/7 headless Telegram runner for account ${this.accountId}${e}...`),this.status="active"}async stop(){this.logger.info(`Stopping headless Telegram runner for account ${this.accountId}...`),this.status="offline"}getStatus(){return this.status}async sendMessage(e){return this.logger.info(`Headless Telegram runner sending message ${e.id} to thread ${e.threadId}`),!0}receiveIncomingMessage(e){this.status==="active"&&(this.logger.debug(`Telegram runner received message ${e.id} from sender ${e.senderId}`),this.messageHandler?.(e))}};var Me=d(p()),W=class{constructor(e,t,r,i){this.accountId=e;this.sessionData=t;this.proxyData=r;this.logger=i||(0,Me.createLogger)(`WhatsAppRunner:${e}`)}platform="whatsapp";status="offline";logger;messageHandler;onMessage(e){this.messageHandler=e}async start(){let e=this.proxyData?` via resident proxy ${this.proxyData.host||this.proxyData.ip||"configured"}`:" (direct)";this.logger.info(`Starting 24/7 headless WhatsApp runner for account ${this.accountId}${e}...`),this.status="active"}async stop(){this.logger.info(`Stopping headless WhatsApp runner for account ${this.accountId}...`),this.status="offline"}getStatus(){return this.status}async sendMessage(e){return this.logger.info(`Headless WhatsApp runner sending message ${e.id} to thread ${e.threadId}`),!0}receiveIncomingMessage(e){this.status==="active"&&(this.logger.debug(`WhatsApp runner received message ${e.id} from sender ${e.senderId}`),this.messageHandler?.(e))}};var q=class{constructor(e,t,r,i,n){this.port=e;this.engine=t;this.lockManager=r;this.deltaQueue=i;this.logger=n||(0,Re.createLogger)("WorkerHttpServer")}logger;server=null;async start(){if(!this.server)return new Promise((e,t)=>{this.server=$e.default.createServer((r,i)=>{this.handleRequest(r,i).catch(n=>{this.logger.error("Unhandled HTTP error:",n),i.headersSent||(i.writeHead(500,{"Content-Type":"application/json"}),i.end(JSON.stringify({error:"Internal Server Error"})))})}),this.server.listen(this.port,()=>{this.logger.info(`\u{1F680} Worker HTTP Server listening on port ${this.port}`),e()}),this.server.on("error",r=>{this.logger.error(`Worker HTTP Server error on port ${this.port}:`,r),t(r)})})}async stop(){if(this.server)return new Promise(e=>{this.server?.close(()=>{this.logger.info("Worker HTTP Server closed."),this.server=null,e()})})}async handleRequest(e,t){let r=new Te.URL(e.url||"/",`http://${e.headers.host||"localhost"}`),i=r.pathname,n=(e.method||"GET").toUpperCase();if(t.setHeader("Access-Control-Allow-Origin","*"),t.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),t.setHeader("Access-Control-Allow-Headers","Content-Type, X-Softty-User-Id"),n==="OPTIONS"){t.writeHead(204),t.end();return}if(n==="GET"&&i==="/health"){let o=this.engine.getMetrics();t.writeHead(200,{"Content-Type":"application/json"}),t.end(JSON.stringify({status:"healthy",timestamp:Date.now(),pendingDeltaMessages:this.deltaQueue.getCount(),metrics:o}));return}if(n==="POST"&&i==="/api/v1/sentinel/handover"){let o=await this.readJsonBody(e),{deviceId:l,sessions:g}=o||{};if(!Array.isArray(g)){t.writeHead(400,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:"Invalid sessions array payload"}));return}this.logger.info(`[Handover] Received handover request from device ${l||"unknown"} with ${g.length} sessions.`);let w=0;for(let a of g){if(!a.id||!a.nightWorkerEnabled)continue;if(!this.lockManager.acquireLock(a.id,`worker_${a.id}`)){this.logger.warn(`Skipping account ${a.id} because lock was not acquired.`);continue}let b,M=a.platform||"zalo";M==="zalo"?b=new N(a.id,a.sessionData):M==="facebook"?b=new U(a.id,a.sessionData):M==="telegram"?b=new H(a.id,a.sessionData):M==="whatsapp"&&(b=new W(a.id,a.sessionData)),b&&(this.engine.channelRunnerRegistry.register(b),await b.start(),this.engine.registerAccount({id:a.id,platform:M,accountName:a.accountName||a.id,status:"active",nightWorkerEnabled:!0}),w++)}t.writeHead(200,{"Content-Type":"application/json"}),t.end(JSON.stringify({success:!0,message:"Sessions handed over successfully",activatedCount:w,timestamp:Date.now()}));return}if(n==="POST"&&i==="/api/v1/sentinel/reclaim"){let o=await this.readJsonBody(e),{deviceId:l}=o||{};this.logger.info(`[Reclaim] Master device ${l||"desktop"} reclaiming all active sessions. Stopping cloud runners...`);let g=this.engine.channelRunnerRegistry.getCount();await this.engine.channelRunnerRegistry.stopAll(),this.lockManager.releaseAllForOwner("worker"),t.writeHead(200,{"Content-Type":"application/json"}),t.end(JSON.stringify({success:!0,reclaimedCount:g,timestamp:Date.now()}));return}if(n==="GET"&&i==="/api/v1/sync/delta"){let o=r.searchParams.get("accountId")||void 0,l=parseInt(r.searchParams.get("since")||"0",10),g=this.deltaQueue.getDeltas(o,l);this.logger.info(`[DeltaSync] Serving ${g.length} night messages (since: ${l}, account: ${o||"all"})`),t.writeHead(200,{"Content-Type":"application/json"}),t.end(JSON.stringify({success:!0,messages:g,count:g.length,syncedTimestamp:Date.now()}));return}t.writeHead(404,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:"Endpoint not found"}))}readJsonBody(e){return new Promise(t=>{let r="";e.on("data",i=>{r+=i}),e.on("end",()=>{try{t(JSON.parse(r))}catch{t({})}})})}};var j=class{constructor(e){this.config=e;this.logger=(0,Se.createLogger)(`WorkerEngine:${e.workerId}`),this.channelRunnerRegistry=new D(this.logger),this.lockManager=new k(6e4,this.logger),this.deltaQueue=new C(1e4,this.logger),this.automationEngine=new O(this.logger),this.pushDispatcher=new x(this.logger),this.httpServer=new q(e.port,this,this.lockManager,this.deltaQueue,this.logger),this.lockManager.onPreempt(async t=>{this.logger.info(`Preemption callback: stopping WSS for account ${t}...`);let r=this.channelRunnerRegistry.getAll();for(let i of r)i.accountId===t&&await i.stop()})}logger;channelRunnerRegistry;lockManager;deltaQueue;automationEngine;pushDispatcher;httpServer;status="idle";startTime=0;activeAccounts=new Map;messagesProcessed=0;heartbeatTimer;async start(){if(this.status==="running"){this.logger.warn("Worker is already running.");return}this.logger.info("Starting ZPlus 24/7 Headless Worker Engine..."),this.status="initializing",this.startTime=Date.now();try{await this.httpServer.start()}catch(e){this.logger.error("Failed to start Worker HTTP Ingress Server:",e)}this.heartbeatTimer=setInterval(()=>{this.sendHeartbeat()},this.config.heartbeatIntervalMs),this.status="running",this.logger.info(`\u2705 ZPlus Worker Engine started successfully [ID: ${this.config.workerId}, Port: ${this.config.port}]`)}async stop(){this.logger.info("Stopping ZPlus Worker Engine..."),this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=void 0),await this.httpServer.stop(),await this.channelRunnerRegistry.stopAll(),this.status="stopped",this.logger.info("ZPlus Worker Engine stopped.")}registerRunner(e){e.onMessage?.(t=>{this.handleIncomingMessage(t,e)}),this.channelRunnerRegistry.register(e)}registerAccount(e){this.activeAccounts.set(e.id,e),this.logger.info(`Registered active account: ${e.accountName} [${e.platform}]`)}unregisterAccount(e){this.activeAccounts.delete(e),this.logger.info(`Unregistered account: ${e}`)}recordMessageProcessed(){this.messagesProcessed++}async handleIncomingMessage(e,t){this.recordMessageProcessed(),this.deltaQueue.enqueue(e);try{let r=await this.automationEngine.evaluateIncomingMessage(e);if(r&&t){this.logger.info(`[NightAutoReply] Sending automated reply to thread ${e.threadId}: "${r}"`);let i={id:`reply_${Date.now()}`,platform:e.platform,conversationId:e.conversationId,threadId:e.threadId,accountId:e.accountId,senderId:e.accountId,senderName:"ZPlus Night Assistant",senderType:"agent",messageType:"text",content:r,timestamp:Date.now(),isSelf:!0,status:"sent"};await t.sendMessage(i)}}catch(r){this.logger.warn("Auto-reply evaluation notice:",r)}try{await this.pushDispatcher.dispatchMessageAlert("fcm_token_mobile_owner",e)}catch(r){this.logger.debug("Push notification dispatch notice:",r)}}getMetrics(){let e=this.startTime>0?Math.floor((Date.now()-this.startTime)/1e3):0,t=process.memoryUsage().rss/(1024*1024);return{workerId:this.config.workerId,status:this.status,uptimeSeconds:e,activeAccountsCount:this.activeAccounts.size,totalMessagesProcessed:this.messagesProcessed,memoryUsageMb:Math.round(t*100)/100,lastHeartbeat:new Date().toISOString()}}sendHeartbeat(){let e=this.getMetrics();this.logger.info(`\u{1F493} Heartbeat: status=${e.status}, accounts=${e.activeAccountsCount}, msgs=${e.totalMessagesProcessed}, pendingDelta=${this.deltaQueue.getCount()}, uptime=${e.uptimeSeconds}s`)}};var Ee=d(p()),A=(0,Ee.createLogger)("CLI");async function je(){A.info("=================================================="),A.info("\u{1F680} ZPlus Headless 24/7 Automation Worker CLI"),A.info("==================================================");let s=R.loadFromEnv(),e=new j(s);process.on("SIGINT",async()=>{A.info("Received SIGINT, shutting down gracefully..."),await e.stop(),process.exit(0)}),process.on("SIGTERM",async()=>{A.info("Received SIGTERM, shutting down gracefully..."),await e.stop(),process.exit(0)}),await e.start()}process.argv[1]&&process.argv[1].endsWith("cli.js")&&je().catch(s=>{A.error("Worker fatal error:",s),process.exit(1)});
|