@softtynet/zplus-worker 1.0.0 → 1.0.4
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
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* 🏛️ ZPlus Headless Worker Engine
|
|
4
|
-
* Quản lý vòng đời chạy ngầm 24/7 (WSS listeners, HTTP Ingress, Distributed Lock, Delta Sync, AI automation, Heartbeats)
|
|
5
|
-
*/
|
|
6
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.WorkerEngine = void 0;
|
|
8
|
-
const core_1 = require("@zplus/core");
|
|
9
|
-
const ChannelRunnerRegistry_1 = require("../channels/ChannelRunnerRegistry");
|
|
10
|
-
const DistributedLockManager_1 = require("../lock/DistributedLockManager");
|
|
11
|
-
const CloudPendingMessageQueue_1 = require("../sync/CloudPendingMessageQueue");
|
|
12
|
-
const HeadlessAutomationEngine_1 = require("../automation/HeadlessAutomationEngine");
|
|
13
|
-
const PushNotificationDispatcher_1 = require("../notifications/PushNotificationDispatcher");
|
|
14
|
-
const WorkerHttpServer_1 = require("../server/WorkerHttpServer");
|
|
15
|
-
class WorkerEngine {
|
|
16
|
-
config;
|
|
17
|
-
logger;
|
|
18
|
-
channelRunnerRegistry;
|
|
19
|
-
lockManager;
|
|
20
|
-
deltaQueue;
|
|
21
|
-
automationEngine;
|
|
22
|
-
pushDispatcher;
|
|
23
|
-
httpServer;
|
|
24
|
-
status = "idle";
|
|
25
|
-
startTime = 0;
|
|
26
|
-
activeAccounts = new Map();
|
|
27
|
-
messagesProcessed = 0;
|
|
28
|
-
heartbeatTimer;
|
|
29
|
-
constructor(config) {
|
|
30
|
-
this.config = config;
|
|
31
|
-
this.logger = (0, core_1.createLogger)(`WorkerEngine:${config.workerId}`);
|
|
32
|
-
this.channelRunnerRegistry = new ChannelRunnerRegistry_1.ChannelRunnerRegistry(this.logger);
|
|
33
|
-
this.lockManager = new DistributedLockManager_1.DistributedLockManager(60000, this.logger);
|
|
34
|
-
this.deltaQueue = new CloudPendingMessageQueue_1.CloudPendingMessageQueue(10000, this.logger);
|
|
35
|
-
this.automationEngine = new HeadlessAutomationEngine_1.HeadlessAutomationEngine(this.logger);
|
|
36
|
-
this.pushDispatcher = new PushNotificationDispatcher_1.PushNotificationDispatcher(this.logger);
|
|
37
|
-
this.httpServer = new WorkerHttpServer_1.WorkerHttpServer(config.port, this, this.lockManager, this.deltaQueue, this.logger);
|
|
38
|
-
// Khi Master Preemption xảy ra, buộc Worker ngắt WSS lập tức
|
|
39
|
-
this.lockManager.onPreempt(async (accountId) => {
|
|
40
|
-
this.logger.info(`Preemption callback: stopping WSS for account ${accountId}...`);
|
|
41
|
-
const allRunners = this.channelRunnerRegistry.getAll();
|
|
42
|
-
for (const r of allRunners) {
|
|
43
|
-
if (r.accountId === accountId) {
|
|
44
|
-
await r.stop();
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
async start() {
|
|
50
|
-
if (this.status === "running") {
|
|
51
|
-
this.logger.warn("Worker is already running.");
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
this.logger.info("Starting ZPlus 24/7 Headless Worker Engine...");
|
|
55
|
-
this.status = "initializing";
|
|
56
|
-
this.startTime = Date.now();
|
|
57
|
-
// 1. Khởi động HTTP Ingress Server
|
|
58
|
-
try {
|
|
59
|
-
await this.httpServer.start();
|
|
60
|
-
}
|
|
61
|
-
catch (err) {
|
|
62
|
-
this.logger.error("Failed to start Worker HTTP Ingress Server:", err);
|
|
63
|
-
}
|
|
64
|
-
// 2. Khởi tạo vòng lặp heartbeat định kỳ
|
|
65
|
-
this.heartbeatTimer = setInterval(() => {
|
|
66
|
-
this.sendHeartbeat();
|
|
67
|
-
}, this.config.heartbeatIntervalMs);
|
|
68
|
-
this.status = "running";
|
|
69
|
-
this.logger.info(`✅ ZPlus Worker Engine started successfully [ID: ${this.config.workerId}, Port: ${this.config.port}]`);
|
|
70
|
-
}
|
|
71
|
-
async stop() {
|
|
72
|
-
this.logger.info("Stopping ZPlus Worker Engine...");
|
|
73
|
-
if (this.heartbeatTimer) {
|
|
74
|
-
clearInterval(this.heartbeatTimer);
|
|
75
|
-
this.heartbeatTimer = undefined;
|
|
76
|
-
}
|
|
77
|
-
// Dừng HTTP Ingress
|
|
78
|
-
await this.httpServer.stop();
|
|
79
|
-
// Dừng tất cả runners
|
|
80
|
-
await this.channelRunnerRegistry.stopAll();
|
|
81
|
-
this.status = "stopped";
|
|
82
|
-
this.logger.info("ZPlus Worker Engine stopped.");
|
|
83
|
-
}
|
|
84
|
-
registerRunner(runner) {
|
|
85
|
-
runner.onMessage?.((msg) => {
|
|
86
|
-
this.handleIncomingMessage(msg, runner);
|
|
87
|
-
});
|
|
88
|
-
this.channelRunnerRegistry.register(runner);
|
|
89
|
-
}
|
|
90
|
-
registerAccount(account) {
|
|
91
|
-
this.activeAccounts.set(account.id, account);
|
|
92
|
-
this.logger.info(`Registered active account: ${account.accountName} [${account.platform}]`);
|
|
93
|
-
}
|
|
94
|
-
unregisterAccount(accountId) {
|
|
95
|
-
this.activeAccounts.delete(accountId);
|
|
96
|
-
this.logger.info(`Unregistered account: ${accountId}`);
|
|
97
|
-
}
|
|
98
|
-
recordMessageProcessed() {
|
|
99
|
-
this.messagesProcessed++;
|
|
100
|
-
}
|
|
101
|
-
async handleIncomingMessage(message, runner) {
|
|
102
|
-
this.recordMessageProcessed();
|
|
103
|
-
// 1. Lưu vào hàng đợi Delta Sync để Desktop sáng mai kéo về
|
|
104
|
-
this.deltaQueue.enqueue(message);
|
|
105
|
-
// 2. Đánh giá AI Auto-reply ca đêm
|
|
106
|
-
try {
|
|
107
|
-
const autoReplyText = await this.automationEngine.evaluateIncomingMessage(message);
|
|
108
|
-
if (autoReplyText && runner) {
|
|
109
|
-
this.logger.info(`[NightAutoReply] Sending automated reply to thread ${message.threadId}: "${autoReplyText}"`);
|
|
110
|
-
const replyMsg = {
|
|
111
|
-
id: `reply_${Date.now()}`,
|
|
112
|
-
platform: message.platform,
|
|
113
|
-
conversationId: message.conversationId,
|
|
114
|
-
threadId: message.threadId,
|
|
115
|
-
accountId: message.accountId,
|
|
116
|
-
senderId: message.accountId,
|
|
117
|
-
senderName: "ZPlus Night Assistant",
|
|
118
|
-
senderType: "agent",
|
|
119
|
-
messageType: "text",
|
|
120
|
-
content: autoReplyText,
|
|
121
|
-
timestamp: Date.now(),
|
|
122
|
-
isSelf: true,
|
|
123
|
-
status: "sent",
|
|
124
|
-
};
|
|
125
|
-
await runner.sendMessage(replyMsg);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
catch (err) {
|
|
129
|
-
this.logger.warn("Auto-reply evaluation notice:", err);
|
|
130
|
-
}
|
|
131
|
-
// 3. Bắn Push Notification về ZPlus Mobile
|
|
132
|
-
try {
|
|
133
|
-
await this.pushDispatcher.dispatchMessageAlert("fcm_token_mobile_owner", message);
|
|
134
|
-
}
|
|
135
|
-
catch (err) {
|
|
136
|
-
this.logger.debug("Push notification dispatch notice:", err);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
getMetrics() {
|
|
140
|
-
const uptime = this.startTime > 0 ? Math.floor((Date.now() - this.startTime) / 1000) : 0;
|
|
141
|
-
const memUsage = process.memoryUsage().rss / (1024 * 1024);
|
|
142
|
-
return {
|
|
143
|
-
workerId: this.config.workerId,
|
|
144
|
-
status: this.status,
|
|
145
|
-
uptimeSeconds: uptime,
|
|
146
|
-
activeAccountsCount: this.activeAccounts.size,
|
|
147
|
-
totalMessagesProcessed: this.messagesProcessed,
|
|
148
|
-
memoryUsageMb: Math.round(memUsage * 100) / 100,
|
|
149
|
-
lastHeartbeat: new Date().toISOString(),
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
sendHeartbeat() {
|
|
153
|
-
const metrics = this.getMetrics();
|
|
154
|
-
this.logger.info(`💓 Heartbeat: status=${metrics.status}, accounts=${metrics.activeAccountsCount}, msgs=${metrics.totalMessagesProcessed}, pendingDelta=${this.deltaQueue.getCount()}, uptime=${metrics.uptimeSeconds}s`);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
exports.WorkerEngine = WorkerEngine;
|
package/dist/index.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export * from "./engine/WorkerEngine";
|
|
2
|
-
export * from "./config/WorkerConfig";
|
|
3
|
-
export * from "./channels";
|
|
4
|
-
export * from "./lock/DistributedLockManager";
|
|
5
|
-
export * from "./sync/CloudSyncWorker";
|
|
6
|
-
export * from "./sync/CloudPendingMessageQueue";
|
|
7
|
-
export * from "./server/WorkerHttpServer";
|
|
8
|
-
export * from "./automation/HeadlessAutomationEngine";
|
|
9
|
-
export * from "./notifications/PushNotificationDispatcher";
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 🔐 Distributed Mutex Lock Manager
|
|
3
|
-
* Quản lý khóa loại trừ tương hỗ, chống xung đột 2 kết nối WSS song song (Desktop & Worker)
|
|
4
|
-
* Hỗ trợ Master Preemption: Desktop luôn có quyền tối thượng thu hồi lock trong < 1.000ms.
|
|
5
|
-
*/
|
|
6
|
-
import { ILogger } from "@zplus/core";
|
|
7
|
-
export interface LockRecord {
|
|
8
|
-
accountId: string;
|
|
9
|
-
ownerId: string;
|
|
10
|
-
acquiredAt: number;
|
|
11
|
-
expiresAt: number;
|
|
12
|
-
}
|
|
13
|
-
export type LockPreemptCallback = (accountId: string, newOwnerId: string) => Promise<void> | void;
|
|
14
|
-
export declare class DistributedLockManager {
|
|
15
|
-
private readonly defaultTtlMs;
|
|
16
|
-
private readonly logger;
|
|
17
|
-
private readonly locks;
|
|
18
|
-
private readonly preemptCallbacks;
|
|
19
|
-
constructor(defaultTtlMs?: number, logger?: ILogger);
|
|
20
|
-
onPreempt(cb: LockPreemptCallback): void;
|
|
21
|
-
/**
|
|
22
|
-
* Xin giữ lock cho tài khoản
|
|
23
|
-
*/
|
|
24
|
-
acquireLock(accountId: string, ownerId: string, ttlMs?: number): boolean;
|
|
25
|
-
/**
|
|
26
|
-
* Desktop chiếm lại quyền tối thượng (Master Preemption)
|
|
27
|
-
* Buộc Worker nhả socket WSS trong vòng tối đa 1.000ms
|
|
28
|
-
*/
|
|
29
|
-
preemptLock(accountId: string, preemptorId: string): Promise<boolean>;
|
|
30
|
-
/**
|
|
31
|
-
* Giải phóng khóa
|
|
32
|
-
*/
|
|
33
|
-
releaseLock(accountId: string, ownerId?: string): boolean;
|
|
34
|
-
/**
|
|
35
|
-
* Giải phóng toàn bộ khóa thuộc sở hữu của ownerId
|
|
36
|
-
*/
|
|
37
|
-
releaseAllForOwner(ownerId: string): number;
|
|
38
|
-
isLocked(accountId: string): boolean;
|
|
39
|
-
getLockOwner(accountId: string): string | null;
|
|
40
|
-
}
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* 🔐 Distributed Mutex Lock Manager
|
|
4
|
-
* Quản lý khóa loại trừ tương hỗ, chống xung đột 2 kết nối WSS song song (Desktop & Worker)
|
|
5
|
-
* Hỗ trợ Master Preemption: Desktop luôn có quyền tối thượng thu hồi lock trong < 1.000ms.
|
|
6
|
-
*/
|
|
7
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
-
exports.DistributedLockManager = void 0;
|
|
9
|
-
const core_1 = require("@zplus/core");
|
|
10
|
-
class DistributedLockManager {
|
|
11
|
-
defaultTtlMs;
|
|
12
|
-
logger;
|
|
13
|
-
locks = new Map();
|
|
14
|
-
preemptCallbacks = [];
|
|
15
|
-
constructor(defaultTtlMs = 60000, logger) {
|
|
16
|
-
this.defaultTtlMs = defaultTtlMs;
|
|
17
|
-
this.logger = logger || (0, core_1.createLogger)("DistributedLockManager");
|
|
18
|
-
}
|
|
19
|
-
onPreempt(cb) {
|
|
20
|
-
this.preemptCallbacks.push(cb);
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Xin giữ lock cho tài khoản
|
|
24
|
-
*/
|
|
25
|
-
acquireLock(accountId, ownerId, ttlMs = this.defaultTtlMs) {
|
|
26
|
-
const now = Date.now();
|
|
27
|
-
const existing = this.locks.get(accountId);
|
|
28
|
-
if (existing && existing.expiresAt > now && existing.ownerId !== ownerId) {
|
|
29
|
-
this.logger.warn(`Lock denied for account ${accountId}: currently held by ${existing.ownerId} (expires in ${existing.expiresAt - now}ms)`);
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
this.locks.set(accountId, {
|
|
33
|
-
accountId,
|
|
34
|
-
ownerId,
|
|
35
|
-
acquiredAt: now,
|
|
36
|
-
expiresAt: now + ttlMs,
|
|
37
|
-
});
|
|
38
|
-
this.logger.info(`✅ Lock granted: account=${accountId}, owner=${ownerId}, ttl=${ttlMs}ms`);
|
|
39
|
-
return true;
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Desktop chiếm lại quyền tối thượng (Master Preemption)
|
|
43
|
-
* Buộc Worker nhả socket WSS trong vòng tối đa 1.000ms
|
|
44
|
-
*/
|
|
45
|
-
async preemptLock(accountId, preemptorId) {
|
|
46
|
-
const existing = this.locks.get(accountId);
|
|
47
|
-
this.logger.info(`⚡ Preemption triggered for account ${accountId} by master ${preemptorId} (previous owner: ${existing?.ownerId || "none"})`);
|
|
48
|
-
// Kích hoạt tất cả callback giải phóng socket WSS khẩn cấp
|
|
49
|
-
const callbackPromises = this.preemptCallbacks.map((cb) => {
|
|
50
|
-
try {
|
|
51
|
-
return Promise.resolve(cb(accountId, preemptorId));
|
|
52
|
-
}
|
|
53
|
-
catch (err) {
|
|
54
|
-
this.logger.error(`Error in preempt callback for ${accountId}:`, err);
|
|
55
|
-
return Promise.resolve();
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
await Promise.race([
|
|
59
|
-
Promise.all(callbackPromises),
|
|
60
|
-
new Promise((resolve) => setTimeout(resolve, 1000)),
|
|
61
|
-
]);
|
|
62
|
-
const now = Date.now();
|
|
63
|
-
this.locks.set(accountId, {
|
|
64
|
-
accountId,
|
|
65
|
-
ownerId: preemptorId,
|
|
66
|
-
acquiredAt: now,
|
|
67
|
-
expiresAt: now + this.defaultTtlMs,
|
|
68
|
-
});
|
|
69
|
-
this.logger.info(`✅ Account ${accountId} preempted successfully by ${preemptorId}.`);
|
|
70
|
-
return true;
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Giải phóng khóa
|
|
74
|
-
*/
|
|
75
|
-
releaseLock(accountId, ownerId) {
|
|
76
|
-
const existing = this.locks.get(accountId);
|
|
77
|
-
if (!existing)
|
|
78
|
-
return true;
|
|
79
|
-
if (ownerId && existing.ownerId !== ownerId) {
|
|
80
|
-
this.logger.warn(`Attempted unauthorized lock release for ${accountId} by ${ownerId} (real owner: ${existing.ownerId})`);
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
this.locks.delete(accountId);
|
|
84
|
-
this.logger.info(`🔓 Lock released for account ${accountId}.`);
|
|
85
|
-
return true;
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Giải phóng toàn bộ khóa thuộc sở hữu của ownerId
|
|
89
|
-
*/
|
|
90
|
-
releaseAllForOwner(ownerId) {
|
|
91
|
-
let released = 0;
|
|
92
|
-
for (const [accountId, lock] of this.locks.entries()) {
|
|
93
|
-
if (lock.ownerId === ownerId) {
|
|
94
|
-
this.locks.delete(accountId);
|
|
95
|
-
released++;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return released;
|
|
99
|
-
}
|
|
100
|
-
isLocked(accountId) {
|
|
101
|
-
const existing = this.locks.get(accountId);
|
|
102
|
-
if (!existing)
|
|
103
|
-
return false;
|
|
104
|
-
return existing.expiresAt > Date.now();
|
|
105
|
-
}
|
|
106
|
-
getLockOwner(accountId) {
|
|
107
|
-
const existing = this.locks.get(accountId);
|
|
108
|
-
if (!existing || existing.expiresAt <= Date.now())
|
|
109
|
-
return null;
|
|
110
|
-
return existing.ownerId;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
exports.DistributedLockManager = DistributedLockManager;
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { ILogger } from "@zplus/core";
|
|
2
|
-
import { UnifiedMessage } from "@zplus/contracts";
|
|
3
|
-
export interface PushNotificationPayload {
|
|
4
|
-
recipientToken: string;
|
|
5
|
-
title: string;
|
|
6
|
-
body: string;
|
|
7
|
-
data?: Record<string, any>;
|
|
8
|
-
}
|
|
9
|
-
export declare class PushNotificationDispatcher {
|
|
10
|
-
private readonly logger;
|
|
11
|
-
constructor(logger?: ILogger);
|
|
12
|
-
dispatchMessageAlert(token: string, message: UnifiedMessage): Promise<boolean>;
|
|
13
|
-
sendNotification(payload: PushNotificationPayload): Promise<boolean>;
|
|
14
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PushNotificationDispatcher = void 0;
|
|
4
|
-
const core_1 = require("@zplus/core");
|
|
5
|
-
class PushNotificationDispatcher {
|
|
6
|
-
logger;
|
|
7
|
-
constructor(logger) {
|
|
8
|
-
this.logger = logger || (0, core_1.createLogger)("PushNotificationDispatcher");
|
|
9
|
-
}
|
|
10
|
-
async dispatchMessageAlert(token, message) {
|
|
11
|
-
const title = `${message.senderName} (${message.platform.toUpperCase()})`;
|
|
12
|
-
const body = message.content || "New multimedia message received";
|
|
13
|
-
return this.sendNotification({
|
|
14
|
-
recipientToken: token,
|
|
15
|
-
title,
|
|
16
|
-
body,
|
|
17
|
-
data: {
|
|
18
|
-
messageId: message.id,
|
|
19
|
-
threadId: message.threadId,
|
|
20
|
-
platform: message.platform,
|
|
21
|
-
accountId: message.accountId,
|
|
22
|
-
},
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
async sendNotification(payload) {
|
|
26
|
-
this.logger.debug(`Dispatching push notification to device token: ${payload.recipientToken.slice(0, 8)}...`);
|
|
27
|
-
// Integrates with Firebase Cloud Messaging / APNs gateway
|
|
28
|
-
return true;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
exports.PushNotificationDispatcher = PushNotificationDispatcher;
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 🌐 ZPlus Worker HTTP Ingress Server
|
|
3
|
-
* Cung cấp các endpoint REST cho Desktop Client giao tiếp với 24/7 Cloud Worker:
|
|
4
|
-
* - GET /health: Kiểm tra tình trạng nhịp tim & tài nguyên
|
|
5
|
-
* - POST /api/v1/sentinel/handover: Bàn giao session từ Desktop sang Worker khi tắt máy
|
|
6
|
-
* - POST /api/v1/sentinel/reclaim: Thu hồi quyền phiên khi Desktop mở lại máy tính
|
|
7
|
-
* - GET /api/v1/sync/delta: Trả về tin nhắn phát sinh trong đêm (Delta Sync)
|
|
8
|
-
*/
|
|
9
|
-
import { ILogger } from "@zplus/core";
|
|
10
|
-
import { WorkerEngine } from "../engine/WorkerEngine";
|
|
11
|
-
import { DistributedLockManager } from "../lock/DistributedLockManager";
|
|
12
|
-
import { CloudPendingMessageQueue } from "../sync/CloudPendingMessageQueue";
|
|
13
|
-
export declare class WorkerHttpServer {
|
|
14
|
-
private readonly port;
|
|
15
|
-
private readonly engine;
|
|
16
|
-
private readonly lockManager;
|
|
17
|
-
private readonly deltaQueue;
|
|
18
|
-
private readonly logger;
|
|
19
|
-
private server;
|
|
20
|
-
constructor(port: number, engine: WorkerEngine, lockManager: DistributedLockManager, deltaQueue: CloudPendingMessageQueue, logger?: ILogger);
|
|
21
|
-
start(): Promise<void>;
|
|
22
|
-
stop(): Promise<void>;
|
|
23
|
-
private handleRequest;
|
|
24
|
-
private readJsonBody;
|
|
25
|
-
}
|
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* 🌐 ZPlus Worker HTTP Ingress Server
|
|
4
|
-
* Cung cấp các endpoint REST cho Desktop Client giao tiếp với 24/7 Cloud Worker:
|
|
5
|
-
* - GET /health: Kiểm tra tình trạng nhịp tim & tài nguyên
|
|
6
|
-
* - POST /api/v1/sentinel/handover: Bàn giao session từ Desktop sang Worker khi tắt máy
|
|
7
|
-
* - POST /api/v1/sentinel/reclaim: Thu hồi quyền phiên khi Desktop mở lại máy tính
|
|
8
|
-
* - GET /api/v1/sync/delta: Trả về tin nhắn phát sinh trong đêm (Delta Sync)
|
|
9
|
-
*/
|
|
10
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
11
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
12
|
-
};
|
|
13
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
-
exports.WorkerHttpServer = void 0;
|
|
15
|
-
const http_1 = __importDefault(require("http"));
|
|
16
|
-
const url_1 = require("url");
|
|
17
|
-
const core_1 = require("@zplus/core");
|
|
18
|
-
const ZaloChannelRunner_1 = require("../channels/runners/ZaloChannelRunner");
|
|
19
|
-
const FacebookChannelRunner_1 = require("../channels/runners/FacebookChannelRunner");
|
|
20
|
-
const TelegramChannelRunner_1 = require("../channels/runners/TelegramChannelRunner");
|
|
21
|
-
const WhatsAppChannelRunner_1 = require("../channels/runners/WhatsAppChannelRunner");
|
|
22
|
-
class WorkerHttpServer {
|
|
23
|
-
port;
|
|
24
|
-
engine;
|
|
25
|
-
lockManager;
|
|
26
|
-
deltaQueue;
|
|
27
|
-
logger;
|
|
28
|
-
server = null;
|
|
29
|
-
constructor(port, engine, lockManager, deltaQueue, logger) {
|
|
30
|
-
this.port = port;
|
|
31
|
-
this.engine = engine;
|
|
32
|
-
this.lockManager = lockManager;
|
|
33
|
-
this.deltaQueue = deltaQueue;
|
|
34
|
-
this.logger = logger || (0, core_1.createLogger)("WorkerHttpServer");
|
|
35
|
-
}
|
|
36
|
-
async start() {
|
|
37
|
-
if (this.server)
|
|
38
|
-
return;
|
|
39
|
-
return new Promise((resolve, reject) => {
|
|
40
|
-
this.server = http_1.default.createServer((req, res) => {
|
|
41
|
-
this.handleRequest(req, res).catch((err) => {
|
|
42
|
-
this.logger.error("Unhandled HTTP error:", err);
|
|
43
|
-
if (!res.headersSent) {
|
|
44
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
45
|
-
res.end(JSON.stringify({ error: "Internal Server Error" }));
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
this.server.listen(this.port, () => {
|
|
50
|
-
this.logger.info(`🚀 Worker HTTP Server listening on port ${this.port}`);
|
|
51
|
-
resolve();
|
|
52
|
-
});
|
|
53
|
-
this.server.on("error", (err) => {
|
|
54
|
-
this.logger.error(`Worker HTTP Server error on port ${this.port}:`, err);
|
|
55
|
-
reject(err);
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
async stop() {
|
|
60
|
-
if (!this.server)
|
|
61
|
-
return;
|
|
62
|
-
return new Promise((resolve) => {
|
|
63
|
-
this.server?.close(() => {
|
|
64
|
-
this.logger.info("Worker HTTP Server closed.");
|
|
65
|
-
this.server = null;
|
|
66
|
-
resolve();
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
async handleRequest(req, res) {
|
|
71
|
-
const parsedUrl = new url_1.URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
|
|
72
|
-
const pathname = parsedUrl.pathname;
|
|
73
|
-
const method = (req.method || "GET").toUpperCase();
|
|
74
|
-
// CORS headers
|
|
75
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
76
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
77
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Softty-User-Id");
|
|
78
|
-
if (method === "OPTIONS") {
|
|
79
|
-
res.writeHead(204);
|
|
80
|
-
res.end();
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
// 1. Healthcheck
|
|
84
|
-
if (method === "GET" && pathname === "/health") {
|
|
85
|
-
const metrics = this.engine.getMetrics();
|
|
86
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
87
|
-
res.end(JSON.stringify({
|
|
88
|
-
status: "healthy",
|
|
89
|
-
timestamp: Date.now(),
|
|
90
|
-
pendingDeltaMessages: this.deltaQueue.getCount(),
|
|
91
|
-
metrics,
|
|
92
|
-
}));
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
// 2. Sentinel Handover (Desktop -> Worker)
|
|
96
|
-
if (method === "POST" && pathname === "/api/v1/sentinel/handover") {
|
|
97
|
-
const body = await this.readJsonBody(req);
|
|
98
|
-
const { deviceId, sessions } = body || {};
|
|
99
|
-
if (!Array.isArray(sessions)) {
|
|
100
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
101
|
-
res.end(JSON.stringify({ error: "Invalid sessions array payload" }));
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
this.logger.info(`[Handover] Received handover request from device ${deviceId || "unknown"} with ${sessions.length} sessions.`);
|
|
105
|
-
let activatedCount = 0;
|
|
106
|
-
for (const item of sessions) {
|
|
107
|
-
if (!item.id || !item.nightWorkerEnabled)
|
|
108
|
-
continue;
|
|
109
|
-
// Xin Distributed Lock cho Worker
|
|
110
|
-
const lockGranted = this.lockManager.acquireLock(item.id, `worker_${item.id}`);
|
|
111
|
-
if (!lockGranted) {
|
|
112
|
-
this.logger.warn(`Skipping account ${item.id} because lock was not acquired.`);
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
// Tạo Runner phù hợp
|
|
116
|
-
let runner;
|
|
117
|
-
const platform = item.platform || "zalo";
|
|
118
|
-
if (platform === "zalo") {
|
|
119
|
-
runner = new ZaloChannelRunner_1.ZaloChannelRunner(item.id, item.sessionData);
|
|
120
|
-
}
|
|
121
|
-
else if (platform === "facebook") {
|
|
122
|
-
runner = new FacebookChannelRunner_1.FacebookChannelRunner(item.id, item.sessionData);
|
|
123
|
-
}
|
|
124
|
-
else if (platform === "telegram") {
|
|
125
|
-
runner = new TelegramChannelRunner_1.TelegramChannelRunner(item.id, item.sessionData);
|
|
126
|
-
}
|
|
127
|
-
else if (platform === "whatsapp") {
|
|
128
|
-
runner = new WhatsAppChannelRunner_1.WhatsAppChannelRunner(item.id, item.sessionData);
|
|
129
|
-
}
|
|
130
|
-
if (runner) {
|
|
131
|
-
this.engine.channelRunnerRegistry.register(runner);
|
|
132
|
-
await runner.start();
|
|
133
|
-
this.engine.registerAccount({
|
|
134
|
-
id: item.id,
|
|
135
|
-
platform,
|
|
136
|
-
accountName: item.accountName || item.id,
|
|
137
|
-
status: "active",
|
|
138
|
-
nightWorkerEnabled: true,
|
|
139
|
-
});
|
|
140
|
-
activatedCount++;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
144
|
-
res.end(JSON.stringify({
|
|
145
|
-
success: true,
|
|
146
|
-
message: "Sessions handed over successfully",
|
|
147
|
-
activatedCount,
|
|
148
|
-
timestamp: Date.now(),
|
|
149
|
-
}));
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
// 3. Sentinel Reclaim (Desktop wakes up, reclaims lock & forces worker to close WSS)
|
|
153
|
-
if (method === "POST" && pathname === "/api/v1/sentinel/reclaim") {
|
|
154
|
-
const body = await this.readJsonBody(req);
|
|
155
|
-
const { deviceId } = body || {};
|
|
156
|
-
this.logger.info(`[Reclaim] Master device ${deviceId || "desktop"} reclaiming all active sessions. Stopping cloud runners...`);
|
|
157
|
-
// Ngắt tất cả runners trên Cloud để nhường quyền kết nối cho Desktop
|
|
158
|
-
const count = this.engine.channelRunnerRegistry.getCount();
|
|
159
|
-
await this.engine.channelRunnerRegistry.stopAll();
|
|
160
|
-
// Giải phóng khóa
|
|
161
|
-
this.lockManager.releaseAllForOwner(`worker`);
|
|
162
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
163
|
-
res.end(JSON.stringify({
|
|
164
|
-
success: true,
|
|
165
|
-
reclaimedCount: count,
|
|
166
|
-
timestamp: Date.now(),
|
|
167
|
-
}));
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
// 4. Delta Sync (Desktop kéo tin nhắn phát sinh trong đêm)
|
|
171
|
-
if (method === "GET" && pathname === "/api/v1/sync/delta") {
|
|
172
|
-
const accountId = parsedUrl.searchParams.get("accountId") || undefined;
|
|
173
|
-
const since = parseInt(parsedUrl.searchParams.get("since") || "0", 10);
|
|
174
|
-
const deltas = this.deltaQueue.getDeltas(accountId, since);
|
|
175
|
-
this.logger.info(`[DeltaSync] Serving ${deltas.length} night messages (since: ${since}, account: ${accountId || "all"})`);
|
|
176
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
177
|
-
res.end(JSON.stringify({
|
|
178
|
-
success: true,
|
|
179
|
-
messages: deltas,
|
|
180
|
-
count: deltas.length,
|
|
181
|
-
syncedTimestamp: Date.now(),
|
|
182
|
-
}));
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
186
|
-
res.end(JSON.stringify({ error: "Endpoint not found" }));
|
|
187
|
-
}
|
|
188
|
-
readJsonBody(req) {
|
|
189
|
-
return new Promise((resolve) => {
|
|
190
|
-
let bodyStr = "";
|
|
191
|
-
req.on("data", (chunk) => {
|
|
192
|
-
bodyStr += chunk;
|
|
193
|
-
});
|
|
194
|
-
req.on("end", () => {
|
|
195
|
-
try {
|
|
196
|
-
resolve(JSON.parse(bodyStr));
|
|
197
|
-
}
|
|
198
|
-
catch {
|
|
199
|
-
resolve({});
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
exports.WorkerHttpServer = WorkerHttpServer;
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 📥 Cloud Pending Message Queue
|
|
3
|
-
* Hàng đợi lưu tạm thời các tin nhắn khách hàng gửi đến trong lúc tắt máy tính (ca đêm)
|
|
4
|
-
* để cung cấp cho API GET /api/v1/sync/delta khi Desktop khởi động lại.
|
|
5
|
-
*/
|
|
6
|
-
import { UnifiedMessage } from "@zplus/contracts";
|
|
7
|
-
import { ILogger } from "@zplus/core";
|
|
8
|
-
export declare class CloudPendingMessageQueue {
|
|
9
|
-
private readonly logger;
|
|
10
|
-
private readonly queue;
|
|
11
|
-
private readonly maxCapacity;
|
|
12
|
-
constructor(maxCapacity?: number, logger?: ILogger);
|
|
13
|
-
/**
|
|
14
|
-
* Đưa tin nhắn mới nhận được trong lúc gác đền vào hàng đợi
|
|
15
|
-
*/
|
|
16
|
-
enqueue(message: UnifiedMessage): void;
|
|
17
|
-
/**
|
|
18
|
-
* Lấy danh sách tin nhắn phát sinh kể từ mốc thời gian sinceTimestamp
|
|
19
|
-
*/
|
|
20
|
-
getDeltas(accountId?: string, sinceTimestamp?: number): UnifiedMessage[];
|
|
21
|
-
/**
|
|
22
|
-
* Đánh dấu đã đồng bộ và dọn dẹp các tin nhắn cũ hơn upToTimestamp
|
|
23
|
-
*/
|
|
24
|
-
purgeSynced(upToTimestamp: number, accountId?: string): number;
|
|
25
|
-
getCount(): number;
|
|
26
|
-
clear(): void;
|
|
27
|
-
}
|