@web-auto/webauto 0.1.18 → 0.1.19
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 +122 -53
- package/apps/desktop-console/dist/main/index.mjs +227 -12
- package/apps/desktop-console/dist/renderer/index.js +237 -8
- package/apps/desktop-console/entry/ui-cli.mjs +282 -16
- package/apps/desktop-console/entry/ui-console.mjs +46 -15
- package/apps/webauto/entry/account.mjs +126 -27
- package/apps/webauto/entry/lib/account-detect.mjs +399 -9
- package/apps/webauto/entry/lib/account-store.mjs +201 -109
- package/apps/webauto/entry/lib/iflow-reply.mjs +194 -0
- package/apps/webauto/entry/lib/profile-policy.mjs +48 -0
- package/apps/webauto/entry/lib/profilepool.mjs +12 -0
- package/apps/webauto/entry/lib/schedule-store.mjs +29 -2
- package/apps/webauto/entry/lib/session-init.mjs +227 -0
- package/apps/webauto/entry/lib/upgrade-check.mjs +269 -0
- package/apps/webauto/entry/lib/xhs-unified-blocks.mjs +160 -0
- package/apps/webauto/entry/lib/xhs-unified-output-blocks.mjs +83 -0
- package/apps/webauto/entry/lib/xhs-unified-plan-blocks.mjs +55 -0
- package/apps/webauto/entry/lib/xhs-unified-profile-blocks.mjs +542 -0
- package/apps/webauto/entry/lib/xhs-unified-runtime-blocks.mjs +436 -0
- package/apps/webauto/entry/profilepool.mjs +56 -9
- package/apps/webauto/entry/smart-reply-cli.mjs +267 -0
- package/apps/webauto/entry/weibo-unified.mjs +84 -11
- package/apps/webauto/entry/xhs-orchestrate.mjs +43 -1
- package/apps/webauto/entry/xhs-unified.mjs +92 -997
- package/bin/webauto.mjs +22 -4
- package/dist/modules/camo-backend/src/index.js +33 -0
- package/dist/modules/camo-backend/src/internal/BrowserSession.js +232 -49
- package/dist/modules/camo-backend/src/internal/engine-manager.js +14 -13
- package/dist/modules/camo-backend/src/internal/ws-server.js +16 -19
- package/dist/modules/camo-runtime/src/utils/browser-service.mjs +38 -6
- package/dist/modules/workflow/blocks/EnsureSession.js +0 -8
- package/dist/modules/workflow/blocks/WeiboCollectFromLinksBlock.js +78 -6
- package/dist/modules/workflow/blocks/WeiboCollectSearchLinksBlock.js +266 -192
- package/dist/modules/workflow/definitions/weibo-search-workflow-v1.js +2 -0
- package/dist/modules/workflow/src/runner.js +2 -0
- package/dist/modules/xiaohongshu/app/src/blocks/ReplyInteractBlock.js +150 -37
- package/dist/modules/xiaohongshu/app/src/blocks/SmartReplyBlock.js +491 -0
- package/modules/camo-backend/src/index.ts +31 -0
- package/modules/camo-backend/src/internal/BrowserSession.ts +224 -53
- package/modules/camo-backend/src/internal/engine-manager.ts +14 -15
- package/modules/camo-backend/src/internal/ws-server.ts +17 -17
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/common.mjs +12 -2
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/persistence.mjs +57 -0
- package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +2475 -243
- package/modules/camo-runtime/src/autoscript/runtime.mjs +35 -30
- package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +80 -443
- package/modules/camo-runtime/src/container/runtime-core/checkpoint.mjs +39 -6
- package/modules/camo-runtime/src/container/runtime-core/operations/index.mjs +206 -39
- package/modules/camo-runtime/src/container/runtime-core/operations/tab-pool.mjs +0 -79
- package/modules/camo-runtime/src/container/runtime-core/operations/viewport.mjs +46 -0
- package/modules/camo-runtime/src/utils/browser-service.mjs +41 -6
- package/modules/camo-runtime/src/utils/js-policy.mjs +28 -0
- package/modules/workflow/blocks/EnsureSession.ts +0 -4
- package/modules/workflow/blocks/WeiboCollectFromLinksBlock.ts +81 -6
- package/modules/workflow/blocks/WeiboCollectSearchLinksBlock.ts +316 -0
- package/modules/workflow/definitions/weibo-search-workflow-v1.ts +2 -0
- package/modules/workflow/src/runner.ts +2 -0
- package/modules/xiaohongshu/app/src/blocks/ReplyInteractBlock.ts +198 -53
- package/modules/xiaohongshu/app/src/blocks/SmartReplyBlock.ts +706 -0
- package/package.json +2 -2
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/comments.mjs +0 -498
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/detail.mjs +0 -181
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +0 -691
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +0 -388
- package/modules/camo-runtime/src/container/runtime-core/operations/selector-scripts.mjs +0 -135
|
@@ -3,7 +3,7 @@ import minimist from 'minimist';
|
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
|
|
9
9
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -59,7 +59,7 @@ const CONTROL_FILE = path.join(resolveWebautoRoot(), 'run', 'ui-cli.json');
|
|
|
59
59
|
|
|
60
60
|
const args = minimist(process.argv.slice(2), {
|
|
61
61
|
boolean: ['help', 'json', 'auto-start', 'build', 'install', 'continue-on-error', 'exact', 'keep-open', 'detailed'],
|
|
62
|
-
string: ['host', 'port', 'selector', 'value', 'text', 'key', 'tab', 'label', 'state', 'file', 'output', 'timeout', 'interval', 'nth'],
|
|
62
|
+
string: ['host', 'port', 'selector', 'value', 'text', 'key', 'tab', 'label', 'state', 'file', 'output', 'timeout', 'interval', 'nth', 'reason'],
|
|
63
63
|
alias: { h: 'help' },
|
|
64
64
|
default: { 'auto-start': false, json: false, 'keep-open': false },
|
|
65
65
|
});
|
|
@@ -84,6 +84,7 @@ Usage:
|
|
|
84
84
|
webauto ui cli full-cover [--build] [--install] [--output <report.json>] [--keep-open]
|
|
85
85
|
webauto ui cli run --file <steps.json> [--continue-on-error]
|
|
86
86
|
webauto ui cli stop
|
|
87
|
+
webauto ui cli restart [--reason <text>] [--timeout <ms>]
|
|
87
88
|
|
|
88
89
|
Options:
|
|
89
90
|
--host <host> UI CLI bridge host (default 127.0.0.1)
|
|
@@ -109,18 +110,41 @@ function parseIntSafe(input, fallback) {
|
|
|
109
110
|
return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
|
|
110
111
|
}
|
|
111
112
|
|
|
113
|
+
function buildUiCliClientMeta(cmd = '') {
|
|
114
|
+
return {
|
|
115
|
+
client: 'webauto-ui-cli',
|
|
116
|
+
cmd: String(cmd || '').trim() || null,
|
|
117
|
+
pid: process.pid,
|
|
118
|
+
ppid: process.ppid,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
112
122
|
function readControlFile() {
|
|
113
123
|
try {
|
|
114
124
|
if (!existsSync(CONTROL_FILE)) return null;
|
|
115
125
|
const raw = JSON.parse(readFileSync(CONTROL_FILE, 'utf8'));
|
|
116
126
|
const host = String(raw?.host || '').trim() || DEFAULT_HOST;
|
|
117
127
|
const port = parseIntSafe(raw?.port, DEFAULT_PORT);
|
|
118
|
-
|
|
128
|
+
const pid = parseIntSafe(raw?.pid, 0);
|
|
129
|
+
return {
|
|
130
|
+
host,
|
|
131
|
+
port,
|
|
132
|
+
pid: pid > 0 ? pid : null,
|
|
133
|
+
startedAt: String(raw?.startedAt || '').trim() || null,
|
|
134
|
+
};
|
|
119
135
|
} catch {
|
|
120
136
|
return null;
|
|
121
137
|
}
|
|
122
138
|
}
|
|
123
139
|
|
|
140
|
+
function removeControlFileIfPresent() {
|
|
141
|
+
try {
|
|
142
|
+
rmSync(CONTROL_FILE, { force: true });
|
|
143
|
+
} catch {
|
|
144
|
+
// ignore
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
124
148
|
function resolveEndpoint() {
|
|
125
149
|
const fromFile = readControlFile();
|
|
126
150
|
const host = String(args.host || fromFile?.host || DEFAULT_HOST).trim();
|
|
@@ -167,6 +191,52 @@ function sleep(ms) {
|
|
|
167
191
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
168
192
|
}
|
|
169
193
|
|
|
194
|
+
function isProcessAlive(pid) {
|
|
195
|
+
const targetPid = parseIntSafe(pid, 0);
|
|
196
|
+
if (targetPid <= 0) return false;
|
|
197
|
+
try {
|
|
198
|
+
process.kill(targetPid, 0);
|
|
199
|
+
return true;
|
|
200
|
+
} catch (err) {
|
|
201
|
+
if (err?.code === 'ESRCH') return false;
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function terminatePid(pid) {
|
|
207
|
+
const targetPid = parseIntSafe(pid, 0);
|
|
208
|
+
if (targetPid <= 0) return { ok: false, error: 'invalid_pid' };
|
|
209
|
+
if (process.platform === 'win32') {
|
|
210
|
+
return new Promise((resolve) => {
|
|
211
|
+
const child = spawn('taskkill', ['/PID', String(targetPid), '/T', '/F'], {
|
|
212
|
+
windowsHide: true,
|
|
213
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
214
|
+
});
|
|
215
|
+
let stderr = '';
|
|
216
|
+
child.stderr.on('data', (chunk) => { stderr += String(chunk || ''); });
|
|
217
|
+
child.on('error', (err) => resolve({ ok: false, error: err?.message || String(err) }));
|
|
218
|
+
child.on('close', (code) => {
|
|
219
|
+
if (code === 0) return resolve({ ok: true, pid: targetPid });
|
|
220
|
+
if (!isProcessAlive(targetPid)) {
|
|
221
|
+
return resolve({ ok: true, pid: targetPid, alreadyStopped: true });
|
|
222
|
+
}
|
|
223
|
+
return resolve({
|
|
224
|
+
ok: false,
|
|
225
|
+
pid: targetPid,
|
|
226
|
+
error: stderr.trim() || `taskkill_exit_${code}`,
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
process.kill(targetPid, 'SIGTERM');
|
|
233
|
+
return { ok: true, pid: targetPid };
|
|
234
|
+
} catch (err) {
|
|
235
|
+
if (err?.code === 'ESRCH') return { ok: true, pid: targetPid, alreadyStopped: true };
|
|
236
|
+
return { ok: false, pid: targetPid, error: err?.message || String(err) };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
170
240
|
async function waitForHealth(endpoint, timeoutMs = 30_000) {
|
|
171
241
|
const started = Date.now();
|
|
172
242
|
while (Date.now() - started <= timeoutMs) {
|
|
@@ -181,9 +251,60 @@ async function waitForHealth(endpoint, timeoutMs = 30_000) {
|
|
|
181
251
|
return null;
|
|
182
252
|
}
|
|
183
253
|
|
|
254
|
+
async function waitForHealthDown(endpoint, timeoutMs = 15_000) {
|
|
255
|
+
const started = Date.now();
|
|
256
|
+
while (Date.now() - started <= timeoutMs) {
|
|
257
|
+
try {
|
|
258
|
+
const ret = await requestJson(endpoint, '/health', { timeoutMs: 1500, retries: 0 });
|
|
259
|
+
if (!ret.ok || !ret.json?.ok) return true;
|
|
260
|
+
} catch {
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
await sleep(300);
|
|
264
|
+
}
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function probeActionChannel(endpoint, timeoutMs = 6000) {
|
|
269
|
+
try {
|
|
270
|
+
const ret = await requestJson(endpoint, '/action', {
|
|
271
|
+
method: 'POST',
|
|
272
|
+
headers: { 'Content-Type': 'application/json' },
|
|
273
|
+
body: JSON.stringify({ action: 'probe', selector: 'body', _client: buildUiCliClientMeta('probe') }),
|
|
274
|
+
timeoutMs,
|
|
275
|
+
retries: 0,
|
|
276
|
+
});
|
|
277
|
+
return ret.ok && Boolean(ret.json?.ok);
|
|
278
|
+
} catch {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function resolveKnownPid(statusRet = null) {
|
|
284
|
+
const fromHealth = parseIntSafe(statusRet?.json?.pid, 0);
|
|
285
|
+
if (fromHealth > 0) return fromHealth;
|
|
286
|
+
const fromFile = parseIntSafe(readControlFile()?.pid, 0);
|
|
287
|
+
if (fromFile > 0) return fromFile;
|
|
288
|
+
return 0;
|
|
289
|
+
}
|
|
290
|
+
|
|
184
291
|
async function startConsoleIfNeeded(endpoint) {
|
|
185
292
|
const health = await waitForHealth(endpoint, 3000);
|
|
186
|
-
if (health)
|
|
293
|
+
if (health) {
|
|
294
|
+
const channelReady = await probeActionChannel(endpoint);
|
|
295
|
+
if (channelReady) return health;
|
|
296
|
+
const pid = parseIntSafe(health?.pid, 0) || parseIntSafe(readControlFile()?.pid, 0);
|
|
297
|
+
if (pid > 0) await terminatePid(pid);
|
|
298
|
+
removeControlFileIfPresent();
|
|
299
|
+
await sleep(500);
|
|
300
|
+
} else {
|
|
301
|
+
const stalePid = parseIntSafe(readControlFile()?.pid, 0);
|
|
302
|
+
if (stalePid > 0) {
|
|
303
|
+
await terminatePid(stalePid);
|
|
304
|
+
removeControlFileIfPresent();
|
|
305
|
+
await sleep(500);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
187
308
|
|
|
188
309
|
const uiConsoleScript = path.join(APP_ROOT, 'entry', 'ui-console.mjs');
|
|
189
310
|
const runUiConsole = async (extraArgs = []) => {
|
|
@@ -207,6 +328,13 @@ async function startConsoleIfNeeded(endpoint) {
|
|
|
207
328
|
|
|
208
329
|
const ready = await waitForHealth(endpoint, 45_000);
|
|
209
330
|
if (!ready) throw new Error('ui cli bridge is not ready after start');
|
|
331
|
+
const readyChannel = await probeActionChannel(endpoint);
|
|
332
|
+
if (!readyChannel) {
|
|
333
|
+
const pid = parseIntSafe(ready?.pid, 0);
|
|
334
|
+
if (pid > 0) await terminatePid(pid);
|
|
335
|
+
removeControlFileIfPresent();
|
|
336
|
+
throw new Error('ui cli bridge action channel is not ready after start');
|
|
337
|
+
}
|
|
210
338
|
return ready;
|
|
211
339
|
}
|
|
212
340
|
|
|
@@ -228,10 +356,15 @@ async function sendAction(endpoint, payload) {
|
|
|
228
356
|
: DEFAULT_ACTION_HTTP_TIMEOUT_MS;
|
|
229
357
|
const timeoutMs = Math.max(DEFAULT_HTTP_TIMEOUT_MS, actionBudgetMs);
|
|
230
358
|
const retries = payload?.action === 'wait' ? 0 : DEFAULT_HTTP_RETRIES;
|
|
359
|
+
const baseCmd = String(args._[0] || '').trim();
|
|
360
|
+
const bodyPayload = {
|
|
361
|
+
...(payload || {}),
|
|
362
|
+
_client: buildUiCliClientMeta(baseCmd || payload?.action || ''),
|
|
363
|
+
};
|
|
231
364
|
return requestJson(endpoint, '/action', {
|
|
232
365
|
method: 'POST',
|
|
233
366
|
headers: { 'Content-Type': 'application/json' },
|
|
234
|
-
body: JSON.stringify(
|
|
367
|
+
body: JSON.stringify(bodyPayload),
|
|
235
368
|
timeoutMs,
|
|
236
369
|
retries,
|
|
237
370
|
});
|
|
@@ -543,14 +676,15 @@ async function runFullCover(endpoint) {
|
|
|
543
676
|
await click('#scheduler-dryrun');
|
|
544
677
|
await input('#scheduler-like-keywords', '真牛逼,购买链接');
|
|
545
678
|
await click('#scheduler-save-btn');
|
|
546
|
-
await
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
679
|
+
await waitForElement('#scheduler-list', 40, 250);
|
|
680
|
+
await runProbe('scheduler', '#scheduler-list');
|
|
681
|
+
// Record whether the newly saved task name is visible, but don't fail hard here.
|
|
682
|
+
// The scheduler list can transiently refresh and reorder under active datasets.
|
|
683
|
+
const taskNameProbe = await probeRaw('#scheduler-list', { text: taskName });
|
|
684
|
+
pushStep('scheduler:task_name_visible', Number(taskNameProbe?.textMatchedCount || 0) > 0, {
|
|
685
|
+
payload: { selector: '#scheduler-list', text: taskName },
|
|
686
|
+
result: taskNameProbe,
|
|
687
|
+
});
|
|
554
688
|
await runProbe('scheduler', '#scheduler-list button', { text: '编辑' });
|
|
555
689
|
await runProbe('scheduler', '#scheduler-list button', { text: '执行' });
|
|
556
690
|
await runProbe('scheduler', '#scheduler-list button', { text: '导出' });
|
|
@@ -693,9 +827,141 @@ async function main() {
|
|
|
693
827
|
}
|
|
694
828
|
|
|
695
829
|
if (cmd === 'stop') {
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
830
|
+
let statusRet = null;
|
|
831
|
+
try {
|
|
832
|
+
statusRet = await requestJson(endpoint, '/health', {
|
|
833
|
+
timeoutMs: Math.min(8000, parseIntSafe(args.timeout, DEFAULT_HTTP_TIMEOUT_MS)),
|
|
834
|
+
retries: 0,
|
|
835
|
+
});
|
|
836
|
+
} catch {
|
|
837
|
+
statusRet = null;
|
|
838
|
+
}
|
|
839
|
+
const knownPid = resolveKnownPid(statusRet);
|
|
840
|
+
const ret = await requestJson(endpoint, '/action', {
|
|
841
|
+
method: 'POST',
|
|
842
|
+
headers: { 'Content-Type': 'application/json' },
|
|
843
|
+
body: JSON.stringify({ action: 'close_window', _client: buildUiCliClientMeta('stop') }),
|
|
844
|
+
timeoutMs: Math.min(8000, parseIntSafe(args.timeout, DEFAULT_ACTION_HTTP_TIMEOUT_MS)),
|
|
845
|
+
retries: 0,
|
|
846
|
+
}).catch((error) => ({
|
|
847
|
+
ok: false,
|
|
848
|
+
status: 0,
|
|
849
|
+
json: { ok: false, error: error?.message || String(error) },
|
|
850
|
+
}));
|
|
851
|
+
if (ret.ok && ret.json?.ok) {
|
|
852
|
+
const down = await waitForHealthDown(endpoint, 12_000);
|
|
853
|
+
if (down) {
|
|
854
|
+
removeControlFileIfPresent();
|
|
855
|
+
printOutput(ret.json);
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
if (knownPid > 0) {
|
|
859
|
+
const killed = await terminatePid(knownPid);
|
|
860
|
+
if (!killed.ok) {
|
|
861
|
+
throw new Error(`close_window pending, force-stop failed: ${killed.error || 'unknown_error'}`);
|
|
862
|
+
}
|
|
863
|
+
removeControlFileIfPresent();
|
|
864
|
+
printOutput({
|
|
865
|
+
ok: true,
|
|
866
|
+
forced: true,
|
|
867
|
+
pid: knownPid,
|
|
868
|
+
reason: 'close_window_timeout',
|
|
869
|
+
});
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
printOutput(ret.json);
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
const actionError = String(ret?.json?.error || `http_${ret?.status || 'request'}`).trim() || 'unknown_error';
|
|
877
|
+
if (knownPid > 0) {
|
|
878
|
+
const killed = await terminatePid(knownPid);
|
|
879
|
+
if (!killed.ok) {
|
|
880
|
+
throw new Error(`force-stop failed: ${killed.error || 'unknown_error'}`);
|
|
881
|
+
}
|
|
882
|
+
removeControlFileIfPresent();
|
|
883
|
+
printOutput({
|
|
884
|
+
ok: true,
|
|
885
|
+
forced: true,
|
|
886
|
+
pid: knownPid,
|
|
887
|
+
reason: actionError || 'request_failed',
|
|
888
|
+
});
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
throw new Error(actionError);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
if (cmd === 'restart') {
|
|
895
|
+
let statusRet = null;
|
|
896
|
+
try {
|
|
897
|
+
statusRet = await requestJson(endpoint, '/health', {
|
|
898
|
+
timeoutMs: Math.min(8000, parseIntSafe(args.timeout, DEFAULT_HTTP_TIMEOUT_MS)),
|
|
899
|
+
retries: 0,
|
|
900
|
+
});
|
|
901
|
+
} catch {
|
|
902
|
+
statusRet = null;
|
|
903
|
+
}
|
|
904
|
+
const knownPid = resolveKnownPid(statusRet);
|
|
905
|
+
const restartReason = String(args.reason || '').trim() || 'ui_cli';
|
|
906
|
+
const ret = await requestJson(endpoint, '/action', {
|
|
907
|
+
method: 'POST',
|
|
908
|
+
headers: { 'Content-Type': 'application/json' },
|
|
909
|
+
body: JSON.stringify({ action: 'restart', reason: restartReason, _client: buildUiCliClientMeta('restart') }),
|
|
910
|
+
timeoutMs: Math.min(8000, parseIntSafe(args.timeout, DEFAULT_ACTION_HTTP_TIMEOUT_MS)),
|
|
911
|
+
retries: 0,
|
|
912
|
+
}).catch((error) => ({
|
|
913
|
+
ok: false,
|
|
914
|
+
status: 0,
|
|
915
|
+
json: { ok: false, error: error?.message || String(error) },
|
|
916
|
+
}));
|
|
917
|
+
if (!ret.ok || !ret.json?.ok) {
|
|
918
|
+
const actionError = String(ret?.json?.error || `http_${ret?.status || 'request'}`).trim() || 'unknown_error';
|
|
919
|
+
if (knownPid > 0) {
|
|
920
|
+
await terminatePid(knownPid);
|
|
921
|
+
removeControlFileIfPresent();
|
|
922
|
+
}
|
|
923
|
+
const recovered = await startConsoleIfNeeded(endpoint);
|
|
924
|
+
printOutput({
|
|
925
|
+
ok: true,
|
|
926
|
+
restarting: true,
|
|
927
|
+
reason: restartReason,
|
|
928
|
+
recoveredByForceRestart: true,
|
|
929
|
+
previousPid: knownPid > 0 ? knownPid : null,
|
|
930
|
+
status: recovered,
|
|
931
|
+
});
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const transitionBudgetMs = Math.min(15_000, Math.max(2_000, parseIntSafe(args.timeout, 60_000)));
|
|
936
|
+
const transitionStart = Date.now();
|
|
937
|
+
while (Date.now() - transitionStart <= transitionBudgetMs) {
|
|
938
|
+
try {
|
|
939
|
+
const probe = await requestJson(endpoint, '/health', { timeoutMs: 1500, retries: 0 });
|
|
940
|
+
const probePid = Number(probe?.json?.pid || 0);
|
|
941
|
+
if (!probe.ok || !probe.json?.ok) break;
|
|
942
|
+
if (knownPid > 0 && probePid > 0 && probePid !== knownPid) break;
|
|
943
|
+
} catch {
|
|
944
|
+
break;
|
|
945
|
+
}
|
|
946
|
+
await sleep(250);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
const restartWaitMs = parseIntSafe(args.timeout, 90_000);
|
|
950
|
+
let status = await waitForHealth(endpoint, restartWaitMs);
|
|
951
|
+
if (!status || (knownPid > 0 && Number(status?.pid || 0) === knownPid)) {
|
|
952
|
+
if (knownPid > 0) {
|
|
953
|
+
await terminatePid(knownPid);
|
|
954
|
+
removeControlFileIfPresent();
|
|
955
|
+
}
|
|
956
|
+
status = await startConsoleIfNeeded(endpoint);
|
|
957
|
+
}
|
|
958
|
+
printOutput({
|
|
959
|
+
ok: true,
|
|
960
|
+
restarting: true,
|
|
961
|
+
reason: restartReason,
|
|
962
|
+
previousPid: knownPid > 0 ? knownPid : null,
|
|
963
|
+
status,
|
|
964
|
+
});
|
|
699
965
|
return;
|
|
700
966
|
}
|
|
701
967
|
|
|
@@ -94,8 +94,17 @@ function checkBuildStatus() {
|
|
|
94
94
|
return existsSync(DIST_MAIN);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
function
|
|
98
|
-
|
|
97
|
+
function resolveElectronBin() {
|
|
98
|
+
const distDir = path.join(APP_ROOT, 'node_modules', 'electron', 'dist');
|
|
99
|
+
const candidates = process.platform === 'win32'
|
|
100
|
+
? [path.join(distDir, 'electron.exe')]
|
|
101
|
+
: (process.platform === 'darwin'
|
|
102
|
+
? [
|
|
103
|
+
path.join(distDir, 'electron'),
|
|
104
|
+
path.join(distDir, 'Electron.app', 'Contents', 'MacOS', 'Electron'),
|
|
105
|
+
]
|
|
106
|
+
: [path.join(distDir, 'electron')]);
|
|
107
|
+
return candidates.find((item) => existsSync(item)) || candidates[0];
|
|
99
108
|
}
|
|
100
109
|
|
|
101
110
|
function sleep(ms) {
|
|
@@ -119,6 +128,26 @@ async function waitForCoreServicesHealthy(timeoutMs = 90000) {
|
|
|
119
128
|
return false;
|
|
120
129
|
}
|
|
121
130
|
|
|
131
|
+
async function readWindowsSessionId(pid) {
|
|
132
|
+
const targetPid = Number(pid || 0);
|
|
133
|
+
if (!Number.isFinite(targetPid) || targetPid <= 0 || process.platform !== 'win32') return null;
|
|
134
|
+
return new Promise((resolve) => {
|
|
135
|
+
const child = spawn('wmic', ['process', 'where', `processid=${targetPid}`, 'get', 'SessionId', '/value'], {
|
|
136
|
+
windowsHide: true,
|
|
137
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
138
|
+
});
|
|
139
|
+
let stdout = '';
|
|
140
|
+
child.stdout.on('data', (chunk) => { stdout += String(chunk || ''); });
|
|
141
|
+
child.on('error', () => resolve(null));
|
|
142
|
+
child.on('close', () => {
|
|
143
|
+
const match = stdout.match(/SessionId\s*=\s*(\d+)/i);
|
|
144
|
+
if (!match) return resolve(null);
|
|
145
|
+
const sessionId = Number(match[1]);
|
|
146
|
+
resolve(Number.isFinite(sessionId) ? sessionId : null);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
122
151
|
function terminateProcessTree(pid) {
|
|
123
152
|
const target = Number(pid || 0);
|
|
124
153
|
if (!Number.isFinite(target) || target <= 0) return;
|
|
@@ -174,9 +203,7 @@ async function startConsole(noDaemon = false) {
|
|
|
174
203
|
if (noDaemon) env.WEBAUTO_NO_DAEMON = '1';
|
|
175
204
|
const detached = !noDaemon;
|
|
176
205
|
const stdio = detached ? 'ignore' : 'inherit';
|
|
177
|
-
const electronBin =
|
|
178
|
-
? path.join(APP_ROOT, 'node_modules', 'electron', 'dist', 'electron.exe')
|
|
179
|
-
: path.join(APP_ROOT, 'node_modules', 'electron', 'dist', 'electron');
|
|
206
|
+
const electronBin = resolveElectronBin();
|
|
180
207
|
if (!existsSync(electronBin)) {
|
|
181
208
|
throw new Error(`electron binary not found: ${electronBin}`);
|
|
182
209
|
}
|
|
@@ -184,12 +211,12 @@ async function startConsole(noDaemon = false) {
|
|
|
184
211
|
const spawnArgs = [DIST_MAIN];
|
|
185
212
|
|
|
186
213
|
if (process.platform === 'win32' && detached) {
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
214
|
+
const escaped = (input) => String(input || '').replace(/"/g, '""');
|
|
215
|
+
const commandLine = [`"${escaped(electronBin)}"`]
|
|
216
|
+
.concat(spawnArgs.map((arg) => `"${escaped(arg)}"`))
|
|
217
|
+
.join(' ');
|
|
191
218
|
const pid = await new Promise((resolve, reject) => {
|
|
192
|
-
const child = spawn('
|
|
219
|
+
const child = spawn('wmic', ['process', 'call', 'create', commandLine], {
|
|
193
220
|
cwd: APP_ROOT,
|
|
194
221
|
env,
|
|
195
222
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -201,12 +228,12 @@ async function startConsole(noDaemon = false) {
|
|
|
201
228
|
child.stderr.on('data', (chunk) => { stderr += String(chunk || ''); });
|
|
202
229
|
child.on('error', reject);
|
|
203
230
|
child.on('close', (code) => {
|
|
204
|
-
if (code
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
} else {
|
|
208
|
-
reject(new Error(`Start-Process failed (${code}): ${stderr.trim() || stdout.trim() || 'unknown error'}`));
|
|
231
|
+
if (code !== 0) {
|
|
232
|
+
reject(new Error(`wmic create failed (${code}): ${stderr.trim() || stdout.trim() || 'unknown error'}`));
|
|
233
|
+
return;
|
|
209
234
|
}
|
|
235
|
+
const match = stdout.match(/ProcessId\s*=\s*(\d+)/i);
|
|
236
|
+
resolve(match?.[1] || 'unknown');
|
|
210
237
|
});
|
|
211
238
|
});
|
|
212
239
|
const healthy = await waitForCoreServicesHealthy();
|
|
@@ -214,7 +241,11 @@ async function startConsole(noDaemon = false) {
|
|
|
214
241
|
terminateProcessTree(pid);
|
|
215
242
|
throw new Error('desktop console started but core services did not become healthy');
|
|
216
243
|
}
|
|
244
|
+
const sessionId = await readWindowsSessionId(pid);
|
|
217
245
|
console.log(`[ui-console] Started (PID: ${pid || 'unknown'})`);
|
|
246
|
+
if (sessionId === 0) {
|
|
247
|
+
console.warn('[ui-console] started in Session 0 (service/non-interactive). UI bridge is available, but desktop window will not be visible.');
|
|
248
|
+
}
|
|
218
249
|
return;
|
|
219
250
|
}
|
|
220
251
|
|