channel-worker 1.6.19 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/api-client.js +13 -0
- package/lib/command-poller.js +149 -4
- package/lib/nst-manager.js +2 -8
- package/package.json +1 -1
package/lib/api-client.js
CHANGED
|
@@ -73,6 +73,19 @@ class ApiClient {
|
|
|
73
73
|
return this.request('PUT', `/workers/commands/${commandId}`, data);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
// Scene dispatch
|
|
77
|
+
async getRenderers() {
|
|
78
|
+
return this.request('GET', '/workers/renderers');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async getSceneQueueCount() {
|
|
82
|
+
return this.request('GET', '/workers/scene-queue-count');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async sceneDispatch(nstProfileId) {
|
|
86
|
+
return this.request('POST', '/workers/scene-dispatch', { nst_profile_id: nstProfileId });
|
|
87
|
+
}
|
|
88
|
+
|
|
76
89
|
// Extension download
|
|
77
90
|
async getExtensionVersion() {
|
|
78
91
|
const data = await this.request('GET', '/extension-download/version');
|
package/lib/command-poller.js
CHANGED
|
@@ -25,13 +25,21 @@ class CommandPoller {
|
|
|
25
25
|
console.log('[commands] Polling for commands (every 3s)');
|
|
26
26
|
this.poll();
|
|
27
27
|
this.timer = setInterval(() => this.poll(), this.pollIntervalMs);
|
|
28
|
+
|
|
29
|
+
// Scene dispatcher — processes queued scene commands sequentially
|
|
30
|
+
console.log('[scene-dispatch] Started (every 5s)');
|
|
31
|
+
this._dispatchTimer = setInterval(() => this._dispatchScenes(), 5000);
|
|
32
|
+
this._dispatching = false;
|
|
33
|
+
|
|
34
|
+
// Profile timeout — close idle profiles after 3 minutes with no work
|
|
35
|
+
this._profileTimeoutTimer = setInterval(() => this._checkProfileTimeouts(), 60000);
|
|
36
|
+
this._profileLastActivity = {}; // { nst_profile_id: timestamp }
|
|
28
37
|
}
|
|
29
38
|
|
|
30
39
|
stop() {
|
|
31
|
-
if (this.timer) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
40
|
+
if (this.timer) { clearInterval(this.timer); this.timer = null; }
|
|
41
|
+
if (this._dispatchTimer) { clearInterval(this._dispatchTimer); this._dispatchTimer = null; }
|
|
42
|
+
if (this._profileTimeoutTimer) { clearInterval(this._profileTimeoutTimer); this._profileTimeoutTimer = null; }
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
async poll() {
|
|
@@ -1107,6 +1115,143 @@ class CommandPoller {
|
|
|
1107
1115
|
});
|
|
1108
1116
|
}
|
|
1109
1117
|
}
|
|
1118
|
+
// ─── Scene Dispatcher ────────────────────────────────────────────────────────
|
|
1119
|
+
// Polls queued scene commands, checks Nstbrowser for running profiles,
|
|
1120
|
+
// assigns command to a profile (launching if under parallel limit).
|
|
1121
|
+
// Processes ONE command per cycle — sequential, no race conditions.
|
|
1122
|
+
|
|
1123
|
+
async _dispatchScenes() {
|
|
1124
|
+
if (this._dispatching) return;
|
|
1125
|
+
this._dispatching = true;
|
|
1126
|
+
try {
|
|
1127
|
+
// 1. Check if there are queued commands
|
|
1128
|
+
const queueCount = await this.api.getSceneQueueCount();
|
|
1129
|
+
if (!queueCount) { this._dispatching = false; return; }
|
|
1130
|
+
|
|
1131
|
+
// 2. Init Nstbrowser if needed
|
|
1132
|
+
if (!this.nst) {
|
|
1133
|
+
const apiKey = await this.api.getSetting('nst_api_key');
|
|
1134
|
+
if (apiKey) this.nst = new NstManager(apiKey);
|
|
1135
|
+
else { this._dispatching = false; return; }
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// 3. Get parallel limit + renderers
|
|
1139
|
+
const parallelLimit = parseInt(await this.api.getSetting('veo3_parallel_limit')) || 1;
|
|
1140
|
+
const renderers = await this.api.getRenderers();
|
|
1141
|
+
if (!renderers?.length) { this._dispatching = false; return; }
|
|
1142
|
+
|
|
1143
|
+
// 4. Get running browsers from Nstbrowser (source of truth)
|
|
1144
|
+
const running = await this.nst.getRunningBrowsers();
|
|
1145
|
+
const runningIds = new Set(running.map(b => b.profileId));
|
|
1146
|
+
|
|
1147
|
+
// 5. Match renderers to running profiles
|
|
1148
|
+
const runningRenderers = renderers.filter(r => {
|
|
1149
|
+
const profileId = this.nst.isUUID(r.nst_profile_id)
|
|
1150
|
+
? r.nst_profile_id
|
|
1151
|
+
: this._resolvedProfiles?.[r.nst_profile_id];
|
|
1152
|
+
return profileId && runningIds.has(profileId);
|
|
1153
|
+
});
|
|
1154
|
+
const offlineRenderers = renderers.filter(r => !runningRenderers.includes(r));
|
|
1155
|
+
|
|
1156
|
+
// 6. Pick target profile
|
|
1157
|
+
let target = null;
|
|
1158
|
+
|
|
1159
|
+
if (runningRenderers.length > 0) {
|
|
1160
|
+
// Assign to a running renderer (round-robin by name for simplicity)
|
|
1161
|
+
target = runningRenderers[0]; // TODO: could pick least loaded
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
if (!target && runningRenderers.length < parallelLimit && offlineRenderers.length > 0) {
|
|
1165
|
+
// Under limit — launch a new profile
|
|
1166
|
+
target = offlineRenderers[0];
|
|
1167
|
+
console.log(`[scene-dispatch] Launching ${target.name} (running: ${runningRenderers.length}/${parallelLimit})`);
|
|
1168
|
+
try {
|
|
1169
|
+
await this.nst.ensureProfile(target.nst_profile_id, { os: target.os || 'windows', proxy: target.proxy || null });
|
|
1170
|
+
|
|
1171
|
+
const path = require('path');
|
|
1172
|
+
const os_mod = require('os');
|
|
1173
|
+
const defaultCCExtPath = path.join(os_mod.homedir(), 'content-creator-ext');
|
|
1174
|
+
const baseExtPath = this.config.content_creator_ext_path || defaultCCExtPath;
|
|
1175
|
+
await this._ensureContentCreatorExt(baseExtPath);
|
|
1176
|
+
|
|
1177
|
+
// Create unique ext dir per profile
|
|
1178
|
+
let extensionPath = baseExtPath;
|
|
1179
|
+
const fs = require('fs');
|
|
1180
|
+
const uniqueExtPath = baseExtPath + '-' + target.nst_profile_id;
|
|
1181
|
+
try {
|
|
1182
|
+
if (fs.existsSync(uniqueExtPath)) fs.rmSync(uniqueExtPath, { recursive: true });
|
|
1183
|
+
fs.mkdirSync(uniqueExtPath, { recursive: true });
|
|
1184
|
+
fs.cpSync(baseExtPath, uniqueExtPath, { recursive: true });
|
|
1185
|
+
fs.writeFileSync(path.join(uniqueExtPath, 'config.json'), JSON.stringify({
|
|
1186
|
+
channelManagerApi: this.api.baseUrl,
|
|
1187
|
+
profileId: target.nst_profile_id,
|
|
1188
|
+
workerToken: this.config.worker_token || '',
|
|
1189
|
+
workerType: 'veo3',
|
|
1190
|
+
}));
|
|
1191
|
+
extensionPath = uniqueExtPath;
|
|
1192
|
+
} catch (e) {
|
|
1193
|
+
console.warn(`[scene-dispatch] Ext dir failed: ${e.message}, using base`);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
await this.nst.launchProfile(target.nst_profile_id, { proxy: target.proxy || null, extensionPath });
|
|
1197
|
+
console.log(`[scene-dispatch] ${target.name} launched`);
|
|
1198
|
+
} catch (err) {
|
|
1199
|
+
console.error(`[scene-dispatch] Failed to launch ${target.name}: ${err.message}`);
|
|
1200
|
+
target = runningRenderers[0] || null; // fallback to running
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
if (!target) {
|
|
1205
|
+
// At limit, no running renderers available — wait for next cycle
|
|
1206
|
+
this._dispatching = false;
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// 7. Claim command and assign to profile
|
|
1211
|
+
const cmd = await this.api.sceneDispatch(target.nst_profile_id);
|
|
1212
|
+
if (cmd) {
|
|
1213
|
+
this._profileLastActivity[target.nst_profile_id] = Date.now();
|
|
1214
|
+
console.log(`[scene-dispatch] Assigned ${cmd.type} → ${target.name} (queue: ${queueCount - 1})`);
|
|
1215
|
+
}
|
|
1216
|
+
} catch (err) {
|
|
1217
|
+
console.error(`[scene-dispatch] Error: ${err.message}`);
|
|
1218
|
+
}
|
|
1219
|
+
this._dispatching = false;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// ─── Profile Timeout ───────────────────────────────────────────────────────
|
|
1223
|
+
// Close profiles that have been idle (no commands assigned) for too long.
|
|
1224
|
+
|
|
1225
|
+
async _checkProfileTimeouts() {
|
|
1226
|
+
try {
|
|
1227
|
+
if (!this.nst) return;
|
|
1228
|
+
|
|
1229
|
+
const IDLE_TIMEOUT = 3 * 60 * 1000; // 3 minutes
|
|
1230
|
+
const now = Date.now();
|
|
1231
|
+
const running = await this.nst.getRunningBrowsers();
|
|
1232
|
+
if (running.length === 0) return;
|
|
1233
|
+
|
|
1234
|
+
// Check if there are any queued commands — if so, don't close anything
|
|
1235
|
+
const queueCount = await this.api.getSceneQueueCount();
|
|
1236
|
+
if (queueCount > 0) return;
|
|
1237
|
+
|
|
1238
|
+
for (const browser of running) {
|
|
1239
|
+
const profileId = browser.profileId;
|
|
1240
|
+
const lastActivity = this._profileLastActivity[profileId] || 0;
|
|
1241
|
+
if (lastActivity && (now - lastActivity) > IDLE_TIMEOUT) {
|
|
1242
|
+
console.log(`[profile-timeout] Closing idle profile ${browser.name || profileId} (idle ${Math.round((now - lastActivity) / 1000)}s)`);
|
|
1243
|
+
try {
|
|
1244
|
+
await this.nst.stopProfile(profileId);
|
|
1245
|
+
delete this._profileLastActivity[profileId];
|
|
1246
|
+
} catch (e) {
|
|
1247
|
+
console.warn(`[profile-timeout] Failed to close ${profileId}: ${e.message}`);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
} catch (err) {
|
|
1252
|
+
console.error(`[profile-timeout] Error: ${err.message}`);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1110
1255
|
}
|
|
1111
1256
|
|
|
1112
1257
|
module.exports = { CommandPoller };
|
package/lib/nst-manager.js
CHANGED
|
@@ -153,14 +153,8 @@ class NstManager {
|
|
|
153
153
|
console.log(`[nst] Running browsers: ${running.map(b => b.name || b.profileId).join(', ') || 'none'}`);
|
|
154
154
|
const isRunning = running.some(b => b.profileId === profileId);
|
|
155
155
|
if (isRunning) {
|
|
156
|
-
console.log(`[nst] Profile ${profileId} already running —
|
|
157
|
-
|
|
158
|
-
await this.stopProfile(profileId);
|
|
159
|
-
console.log(`[nst] Stop command sent, waiting 3s...`);
|
|
160
|
-
await new Promise(r => setTimeout(r, 3000));
|
|
161
|
-
} catch (e) {
|
|
162
|
-
console.warn(`[nst] Stop failed: ${e.message} — continuing anyway`);
|
|
163
|
-
}
|
|
156
|
+
console.log(`[nst] Profile ${profileId} already running — skipping launch`);
|
|
157
|
+
return { profileId, alreadyRunning: true };
|
|
164
158
|
}
|
|
165
159
|
|
|
166
160
|
// Update profile language to en-US (Custom) before launch
|