@web-auto/webauto 0.1.17 → 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 +229 -14
- package/apps/desktop-console/dist/renderer/index.js +237 -8
- package/apps/desktop-console/entry/ui-cli.mjs +290 -21
- 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));
|
|
@@ -18,6 +18,8 @@ const readEnvPositiveInt = (name, fallback) => {
|
|
|
18
18
|
const DEFAULT_HTTP_TIMEOUT_MS = readEnvPositiveInt('WEBAUTO_UI_CLI_HTTP_TIMEOUT_MS', 25_000);
|
|
19
19
|
const DEFAULT_HTTP_RETRIES = readEnvPositiveInt('WEBAUTO_UI_CLI_HTTP_RETRIES', 1);
|
|
20
20
|
const DEFAULT_START_HEALTH_TIMEOUT_MS = readEnvPositiveInt('WEBAUTO_UI_CLI_START_HEALTH_TIMEOUT_MS', 8_000);
|
|
21
|
+
const DEFAULT_STATUS_TIMEOUT_MS = readEnvPositiveInt('WEBAUTO_UI_CLI_STATUS_TIMEOUT_MS', 45_000);
|
|
22
|
+
const DEFAULT_ACTION_HTTP_TIMEOUT_MS = readEnvPositiveInt('WEBAUTO_UI_CLI_ACTION_HTTP_TIMEOUT_MS', 40_000);
|
|
21
23
|
|
|
22
24
|
function normalizePathForPlatform(raw, platform = process.platform) {
|
|
23
25
|
const input = String(raw || '').trim();
|
|
@@ -57,7 +59,7 @@ const CONTROL_FILE = path.join(resolveWebautoRoot(), 'run', 'ui-cli.json');
|
|
|
57
59
|
|
|
58
60
|
const args = minimist(process.argv.slice(2), {
|
|
59
61
|
boolean: ['help', 'json', 'auto-start', 'build', 'install', 'continue-on-error', 'exact', 'keep-open', 'detailed'],
|
|
60
|
-
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'],
|
|
61
63
|
alias: { h: 'help' },
|
|
62
64
|
default: { 'auto-start': false, json: false, 'keep-open': false },
|
|
63
65
|
});
|
|
@@ -82,6 +84,7 @@ Usage:
|
|
|
82
84
|
webauto ui cli full-cover [--build] [--install] [--output <report.json>] [--keep-open]
|
|
83
85
|
webauto ui cli run --file <steps.json> [--continue-on-error]
|
|
84
86
|
webauto ui cli stop
|
|
87
|
+
webauto ui cli restart [--reason <text>] [--timeout <ms>]
|
|
85
88
|
|
|
86
89
|
Options:
|
|
87
90
|
--host <host> UI CLI bridge host (default 127.0.0.1)
|
|
@@ -107,18 +110,41 @@ function parseIntSafe(input, fallback) {
|
|
|
107
110
|
return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
|
|
108
111
|
}
|
|
109
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
|
+
|
|
110
122
|
function readControlFile() {
|
|
111
123
|
try {
|
|
112
124
|
if (!existsSync(CONTROL_FILE)) return null;
|
|
113
125
|
const raw = JSON.parse(readFileSync(CONTROL_FILE, 'utf8'));
|
|
114
126
|
const host = String(raw?.host || '').trim() || DEFAULT_HOST;
|
|
115
127
|
const port = parseIntSafe(raw?.port, DEFAULT_PORT);
|
|
116
|
-
|
|
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
|
+
};
|
|
117
135
|
} catch {
|
|
118
136
|
return null;
|
|
119
137
|
}
|
|
120
138
|
}
|
|
121
139
|
|
|
140
|
+
function removeControlFileIfPresent() {
|
|
141
|
+
try {
|
|
142
|
+
rmSync(CONTROL_FILE, { force: true });
|
|
143
|
+
} catch {
|
|
144
|
+
// ignore
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
122
148
|
function resolveEndpoint() {
|
|
123
149
|
const fromFile = readControlFile();
|
|
124
150
|
const host = String(args.host || fromFile?.host || DEFAULT_HOST).trim();
|
|
@@ -165,6 +191,52 @@ function sleep(ms) {
|
|
|
165
191
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
166
192
|
}
|
|
167
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
|
+
|
|
168
240
|
async function waitForHealth(endpoint, timeoutMs = 30_000) {
|
|
169
241
|
const started = Date.now();
|
|
170
242
|
while (Date.now() - started <= timeoutMs) {
|
|
@@ -179,9 +251,60 @@ async function waitForHealth(endpoint, timeoutMs = 30_000) {
|
|
|
179
251
|
return null;
|
|
180
252
|
}
|
|
181
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
|
+
|
|
182
291
|
async function startConsoleIfNeeded(endpoint) {
|
|
183
292
|
const health = await waitForHealth(endpoint, 3000);
|
|
184
|
-
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
|
+
}
|
|
185
308
|
|
|
186
309
|
const uiConsoleScript = path.join(APP_ROOT, 'entry', 'ui-console.mjs');
|
|
187
310
|
const runUiConsole = async (extraArgs = []) => {
|
|
@@ -205,6 +328,13 @@ async function startConsoleIfNeeded(endpoint) {
|
|
|
205
328
|
|
|
206
329
|
const ready = await waitForHealth(endpoint, 45_000);
|
|
207
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
|
+
}
|
|
208
338
|
return ready;
|
|
209
339
|
}
|
|
210
340
|
|
|
@@ -221,15 +351,20 @@ function printOutput(payload) {
|
|
|
221
351
|
}
|
|
222
352
|
|
|
223
353
|
async function sendAction(endpoint, payload) {
|
|
224
|
-
const
|
|
354
|
+
const actionBudgetMs = payload?.action === 'wait'
|
|
225
355
|
? parseIntSafe(payload?.timeoutMs, 15_000) + 5_000
|
|
226
|
-
:
|
|
227
|
-
const timeoutMs = Math.max(DEFAULT_HTTP_TIMEOUT_MS,
|
|
356
|
+
: DEFAULT_ACTION_HTTP_TIMEOUT_MS;
|
|
357
|
+
const timeoutMs = Math.max(DEFAULT_HTTP_TIMEOUT_MS, actionBudgetMs);
|
|
228
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
|
+
};
|
|
229
364
|
return requestJson(endpoint, '/action', {
|
|
230
365
|
method: 'POST',
|
|
231
366
|
headers: { 'Content-Type': 'application/json' },
|
|
232
|
-
body: JSON.stringify(
|
|
367
|
+
body: JSON.stringify(bodyPayload),
|
|
233
368
|
timeoutMs,
|
|
234
369
|
retries,
|
|
235
370
|
});
|
|
@@ -541,14 +676,15 @@ async function runFullCover(endpoint) {
|
|
|
541
676
|
await click('#scheduler-dryrun');
|
|
542
677
|
await input('#scheduler-like-keywords', '真牛逼,购买链接');
|
|
543
678
|
await click('#scheduler-save-btn');
|
|
544
|
-
await
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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
|
+
});
|
|
552
688
|
await runProbe('scheduler', '#scheduler-list button', { text: '编辑' });
|
|
553
689
|
await runProbe('scheduler', '#scheduler-list button', { text: '执行' });
|
|
554
690
|
await runProbe('scheduler', '#scheduler-list button', { text: '导出' });
|
|
@@ -679,9 +815,10 @@ async function main() {
|
|
|
679
815
|
}
|
|
680
816
|
|
|
681
817
|
if (cmd === 'status' || cmd === 'snapshot') {
|
|
682
|
-
const pathName = cmd === 'snapshot' ? '/snapshot' : '/
|
|
818
|
+
const pathName = cmd === 'snapshot' ? '/snapshot' : '/health';
|
|
819
|
+
const statusTimeoutMs = parseIntSafe(args.timeout, DEFAULT_STATUS_TIMEOUT_MS);
|
|
683
820
|
const ret = await requestJson(endpoint, pathName, {
|
|
684
|
-
timeoutMs:
|
|
821
|
+
timeoutMs: statusTimeoutMs,
|
|
685
822
|
retries: DEFAULT_HTTP_RETRIES,
|
|
686
823
|
});
|
|
687
824
|
if (!ret.ok) throw new Error(ret.json?.error || `http_${ret.status}`);
|
|
@@ -690,9 +827,141 @@ async function main() {
|
|
|
690
827
|
}
|
|
691
828
|
|
|
692
829
|
if (cmd === 'stop') {
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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
|
+
});
|
|
696
965
|
return;
|
|
697
966
|
}
|
|
698
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
|
|