channel-worker 2.5.150 → 2.5.151
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/lib/api-client.js +51 -5
- package/lib/command-poller.js +55 -22
- package/package.json +1 -1
package/lib/api-client.js
CHANGED
|
@@ -1,7 +1,21 @@
|
|
|
1
|
+
// Final-result reports (done / failed / pending) ride out an API restart:
|
|
2
|
+
// one lost PUT left the command 'running' until the stale cleanup marked it
|
|
3
|
+
// "Timed out — có thể bài ĐÃ lên" and the scheduler stopped retrying the
|
|
4
|
+
// idea. ~2.5 minutes covers a pm2 restart with room to spare.
|
|
5
|
+
const REPORT_RETRY_DELAYS_MS = [2000, 4000, 8000, 15000, 30000, 30000, 60000];
|
|
6
|
+
|
|
7
|
+
// The API never answered: connection refused/reset, a gateway 502/503/504, or
|
|
8
|
+
// a non-JSON page while it restarts. An answer from the API itself — success:
|
|
9
|
+
// false, even a 500 from a handler — is final: repeating it changes nothing.
|
|
10
|
+
class ApiUnreachableError extends Error {}
|
|
11
|
+
const GATEWAY_STATUSES = new Set([502, 503, 504]);
|
|
12
|
+
|
|
1
13
|
class ApiClient {
|
|
2
14
|
constructor(baseUrl, workerToken) {
|
|
3
15
|
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
4
16
|
this.workerToken = workerToken || '';
|
|
17
|
+
this.reportRetryDelaysMs = REPORT_RETRY_DELAYS_MS;
|
|
18
|
+
this.sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
5
19
|
}
|
|
6
20
|
|
|
7
21
|
async request(method, path, body = null) {
|
|
@@ -15,8 +29,21 @@ class ApiClient {
|
|
|
15
29
|
};
|
|
16
30
|
if (body) options.body = JSON.stringify(body);
|
|
17
31
|
|
|
18
|
-
|
|
19
|
-
|
|
32
|
+
let res;
|
|
33
|
+
try {
|
|
34
|
+
res = await fetch(url, options);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
throw new ApiUnreachableError(`API unreachable: ${err.message}`);
|
|
37
|
+
}
|
|
38
|
+
let json;
|
|
39
|
+
try {
|
|
40
|
+
json = await res.json();
|
|
41
|
+
} catch {
|
|
42
|
+
throw new ApiUnreachableError(`API error ${res.status} (no JSON body)`);
|
|
43
|
+
}
|
|
44
|
+
if (GATEWAY_STATUSES.has(res.status) && !json?.success) {
|
|
45
|
+
throw new ApiUnreachableError(json?.message || `API error ${res.status}`);
|
|
46
|
+
}
|
|
20
47
|
|
|
21
48
|
if (!json.success) {
|
|
22
49
|
throw new Error(json.message || `API error ${res.status}`);
|
|
@@ -24,6 +51,21 @@ class ApiClient {
|
|
|
24
51
|
return json.data;
|
|
25
52
|
}
|
|
26
53
|
|
|
54
|
+
// Same request, retried while the API is unreachable. Only for writes that
|
|
55
|
+
// are safe to repeat — every report here sets a final state.
|
|
56
|
+
async reportRequest(method, path, body) {
|
|
57
|
+
for (let attempt = 0; ; attempt++) {
|
|
58
|
+
try {
|
|
59
|
+
return await this.request(method, path, body);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
const delay = this.reportRetryDelaysMs[attempt];
|
|
62
|
+
if (!(err instanceof ApiUnreachableError) || delay === undefined) throw err;
|
|
63
|
+
console.warn(`[api] ${method} ${path} failed (${err.message}) — retry ${attempt + 1}/${this.reportRetryDelaysMs.length} in ${Math.round(delay / 1000)}s`);
|
|
64
|
+
await this.sleep(delay);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
27
69
|
// Worker
|
|
28
70
|
async register(workerData) {
|
|
29
71
|
return this.request('POST', '/workers/register', workerData);
|
|
@@ -79,8 +121,12 @@ class ApiClient {
|
|
|
79
121
|
return this.request('GET', `/workers/commands?worker_id=${workerId}&types=${encodeURIComponent(workerTypes)}`);
|
|
80
122
|
}
|
|
81
123
|
|
|
124
|
+
// A 'running' touch is a liveness beat: NOT retried — a beat landing late
|
|
125
|
+
// would overwrite the final 'done' that followed it.
|
|
82
126
|
async updateCommand(commandId, data) {
|
|
83
|
-
|
|
127
|
+
const path = `/workers/commands/${commandId}`;
|
|
128
|
+
if (data?.status === 'running') return this.request('PUT', path, data);
|
|
129
|
+
return this.reportRequest('PUT', path, data);
|
|
84
130
|
}
|
|
85
131
|
|
|
86
132
|
// Report a command result via the extension-style endpoint, which also
|
|
@@ -88,7 +134,7 @@ class ApiClient {
|
|
|
88
134
|
// idea UI reflects pw-pipeline outcomes the same way it does for extension
|
|
89
135
|
// uploads). Use this for upload_*_pw results.
|
|
90
136
|
async postCommandResult(commandId, { status, result, error } = {}) {
|
|
91
|
-
return this.
|
|
137
|
+
return this.reportRequest('POST', `/extension/commands/${commandId}/result`, { status, result: result || {}, error: error || null });
|
|
92
138
|
}
|
|
93
139
|
|
|
94
140
|
// Shopee scraper callbacks — worker ships parsed products, API upserts into
|
|
@@ -222,4 +268,4 @@ class ApiClient {
|
|
|
222
268
|
}
|
|
223
269
|
}
|
|
224
270
|
|
|
225
|
-
module.exports = { ApiClient };
|
|
271
|
+
module.exports = { ApiClient, ApiUnreachableError, REPORT_RETRY_DELAYS_MS };
|
package/lib/command-poller.js
CHANGED
|
@@ -2,6 +2,7 @@ const { NstManager } = require('./nst-manager');
|
|
|
2
2
|
const { refreshPinnedAgentAddress } = require('./nst-agent-locator');
|
|
3
3
|
const { checkAndUpdateExtension } = require('./extension-updater');
|
|
4
4
|
const { StatsSyncer } = require('./stats-syncer');
|
|
5
|
+
const { ApiUnreachableError } = require('./api-client');
|
|
5
6
|
|
|
6
7
|
// Quãng RẢNH trước khi đóng profile của luồng đăng bài. Phải đủ dài để lượt
|
|
7
8
|
// upload nền tảng kế tiếp (facebook → youtube → tiktok của cùng một idea) kịp
|
|
@@ -284,11 +285,23 @@ class CommandPoller {
|
|
|
284
285
|
)), ms);
|
|
285
286
|
});
|
|
286
287
|
try {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
288
|
+
let patch;
|
|
289
|
+
try {
|
|
290
|
+
patch = await Promise.race([job, deadline]);
|
|
291
|
+
} catch (err) {
|
|
292
|
+
console.error(`${failLog}: ${err.message}`);
|
|
293
|
+
patch = { status: 'failed', error: String(err.message || err).slice(0, 500) };
|
|
294
|
+
}
|
|
295
|
+
// Outside the work's try: a report that cannot reach the API must not
|
|
296
|
+
// turn a finished job into 'failed'.
|
|
297
|
+
try {
|
|
298
|
+
await this.api.updateCommand(command._id, patch);
|
|
299
|
+
} catch (err) {
|
|
300
|
+
console.error(`${failLog}: ${patch?.status} NOT reported: ${err.message}`);
|
|
301
|
+
if (!(err instanceof ApiUnreachableError) && patch?.status !== 'failed') {
|
|
302
|
+
await this.api.updateCommand(command._id, { status: 'failed', error: String(err.message || err).slice(0, 500) }).catch(() => {});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
292
305
|
} finally {
|
|
293
306
|
clearTimeout(timer);
|
|
294
307
|
abandoned = true;
|
|
@@ -747,23 +760,43 @@ class CommandPoller {
|
|
|
747
760
|
}, RUN_BEAT_MS);
|
|
748
761
|
|
|
749
762
|
try {
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
763
|
+
let outcome;
|
|
764
|
+
try {
|
|
765
|
+
const result = await runPlaywrightScript({
|
|
766
|
+
nst: this.nst,
|
|
767
|
+
profileId,
|
|
768
|
+
scriptName,
|
|
769
|
+
payload,
|
|
770
|
+
log,
|
|
771
|
+
keepOpen: keepOpen.enabled,
|
|
772
|
+
info: runInfo,
|
|
773
|
+
});
|
|
774
|
+
outcome = { status: 'done', result: result || {} };
|
|
775
|
+
console.log(`[commands/pw] ${command.type} done`);
|
|
776
|
+
} catch (err) {
|
|
777
|
+
console.error(`[commands/pw] ${command.type} failed: ${err.message}`);
|
|
778
|
+
outcome = { status: 'failed', error: String(err.message || err).slice(0, 500) };
|
|
779
|
+
}
|
|
780
|
+
// Stop the 'running' beat first: a beat landing after the result would
|
|
781
|
+
// put the command back to 'running'.
|
|
782
|
+
clearInterval(beat);
|
|
783
|
+
// Reported once, outside the script's try: a report that cannot reach
|
|
784
|
+
// the API used to fall into the catch above and report the posted video
|
|
785
|
+
// as 'failed'. postCommandResult (not updateCommand) so the existing
|
|
786
|
+
// extension-side logic mirrors status into ContentJob +
|
|
787
|
+
// ContentIdea.publish_results + broadcasts the WS update — pw and
|
|
788
|
+
// extension paths share idea-state UX.
|
|
789
|
+
try {
|
|
790
|
+
await this.api.postCommandResult(command._id, outcome);
|
|
791
|
+
} catch (err) {
|
|
792
|
+
console.error(`[commands/pw] ${command.type} ${outcome.status} NOT reported: ${err.message}`);
|
|
793
|
+
// The API answered and refused it: settle the command as failed
|
|
794
|
+
// rather than leave it 'running'. Unreachable after every retry:
|
|
795
|
+
// nothing more can be sent.
|
|
796
|
+
if (!(err instanceof ApiUnreachableError) && outcome.status !== 'failed') {
|
|
797
|
+
await this.api.postCommandResult(command._id, { status: 'failed', error: `API từ chối kết quả: ${String(err.message).slice(0, 300)}` }).catch(() => {});
|
|
798
|
+
}
|
|
799
|
+
}
|
|
767
800
|
} finally {
|
|
768
801
|
clearInterval(beat);
|
|
769
802
|
// Always release the per-profile mutex — even on throw — or sibling
|