@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.
Files changed (65) hide show
  1. package/README.md +122 -53
  2. package/apps/desktop-console/dist/main/index.mjs +227 -12
  3. package/apps/desktop-console/dist/renderer/index.js +237 -8
  4. package/apps/desktop-console/entry/ui-cli.mjs +282 -16
  5. package/apps/desktop-console/entry/ui-console.mjs +46 -15
  6. package/apps/webauto/entry/account.mjs +126 -27
  7. package/apps/webauto/entry/lib/account-detect.mjs +399 -9
  8. package/apps/webauto/entry/lib/account-store.mjs +201 -109
  9. package/apps/webauto/entry/lib/iflow-reply.mjs +194 -0
  10. package/apps/webauto/entry/lib/profile-policy.mjs +48 -0
  11. package/apps/webauto/entry/lib/profilepool.mjs +12 -0
  12. package/apps/webauto/entry/lib/schedule-store.mjs +29 -2
  13. package/apps/webauto/entry/lib/session-init.mjs +227 -0
  14. package/apps/webauto/entry/lib/upgrade-check.mjs +269 -0
  15. package/apps/webauto/entry/lib/xhs-unified-blocks.mjs +160 -0
  16. package/apps/webauto/entry/lib/xhs-unified-output-blocks.mjs +83 -0
  17. package/apps/webauto/entry/lib/xhs-unified-plan-blocks.mjs +55 -0
  18. package/apps/webauto/entry/lib/xhs-unified-profile-blocks.mjs +542 -0
  19. package/apps/webauto/entry/lib/xhs-unified-runtime-blocks.mjs +436 -0
  20. package/apps/webauto/entry/profilepool.mjs +56 -9
  21. package/apps/webauto/entry/smart-reply-cli.mjs +267 -0
  22. package/apps/webauto/entry/weibo-unified.mjs +84 -11
  23. package/apps/webauto/entry/xhs-orchestrate.mjs +43 -1
  24. package/apps/webauto/entry/xhs-unified.mjs +92 -997
  25. package/bin/webauto.mjs +22 -4
  26. package/dist/modules/camo-backend/src/index.js +33 -0
  27. package/dist/modules/camo-backend/src/internal/BrowserSession.js +232 -49
  28. package/dist/modules/camo-backend/src/internal/engine-manager.js +14 -13
  29. package/dist/modules/camo-backend/src/internal/ws-server.js +16 -19
  30. package/dist/modules/camo-runtime/src/utils/browser-service.mjs +38 -6
  31. package/dist/modules/workflow/blocks/EnsureSession.js +0 -8
  32. package/dist/modules/workflow/blocks/WeiboCollectFromLinksBlock.js +78 -6
  33. package/dist/modules/workflow/blocks/WeiboCollectSearchLinksBlock.js +266 -192
  34. package/dist/modules/workflow/definitions/weibo-search-workflow-v1.js +2 -0
  35. package/dist/modules/workflow/src/runner.js +2 -0
  36. package/dist/modules/xiaohongshu/app/src/blocks/ReplyInteractBlock.js +150 -37
  37. package/dist/modules/xiaohongshu/app/src/blocks/SmartReplyBlock.js +491 -0
  38. package/modules/camo-backend/src/index.ts +31 -0
  39. package/modules/camo-backend/src/internal/BrowserSession.ts +224 -53
  40. package/modules/camo-backend/src/internal/engine-manager.ts +14 -15
  41. package/modules/camo-backend/src/internal/ws-server.ts +17 -17
  42. package/modules/camo-runtime/src/autoscript/action-providers/xhs/common.mjs +12 -2
  43. package/modules/camo-runtime/src/autoscript/action-providers/xhs/persistence.mjs +57 -0
  44. package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +2475 -243
  45. package/modules/camo-runtime/src/autoscript/runtime.mjs +35 -30
  46. package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +80 -443
  47. package/modules/camo-runtime/src/container/runtime-core/checkpoint.mjs +39 -6
  48. package/modules/camo-runtime/src/container/runtime-core/operations/index.mjs +206 -39
  49. package/modules/camo-runtime/src/container/runtime-core/operations/tab-pool.mjs +0 -79
  50. package/modules/camo-runtime/src/container/runtime-core/operations/viewport.mjs +46 -0
  51. package/modules/camo-runtime/src/utils/browser-service.mjs +41 -6
  52. package/modules/camo-runtime/src/utils/js-policy.mjs +28 -0
  53. package/modules/workflow/blocks/EnsureSession.ts +0 -4
  54. package/modules/workflow/blocks/WeiboCollectFromLinksBlock.ts +81 -6
  55. package/modules/workflow/blocks/WeiboCollectSearchLinksBlock.ts +316 -0
  56. package/modules/workflow/definitions/weibo-search-workflow-v1.ts +2 -0
  57. package/modules/workflow/src/runner.ts +2 -0
  58. package/modules/xiaohongshu/app/src/blocks/ReplyInteractBlock.ts +198 -53
  59. package/modules/xiaohongshu/app/src/blocks/SmartReplyBlock.ts +706 -0
  60. package/package.json +2 -2
  61. package/modules/camo-runtime/src/autoscript/action-providers/xhs/comments.mjs +0 -498
  62. package/modules/camo-runtime/src/autoscript/action-providers/xhs/detail.mjs +0 -181
  63. package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +0 -691
  64. package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +0 -388
  65. 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
- return { host, port };
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) return 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(payload),
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 wait('#scheduler-list');
547
- // Wait for async schedule refresh to render the new task.
548
- for (let i = 0; i < 6; i += 1) {
549
- const raw = await probeRaw('#scheduler-list', { text: taskName });
550
- if (Number(raw?.textMatchedCount || 0) > 0) break;
551
- await sleep(500);
552
- }
553
- await runProbe('scheduler', '#scheduler-list', { text: taskName });
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
- const ret = await sendAction(endpoint, { action: 'close_window' });
697
- if (!ret.ok) throw new Error(ret.json?.error || `http_${ret.status}`);
698
- printOutput(ret.json);
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 quotePsSingle(value) {
98
- return String(value || '').replace(/'/g, "''");
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 = process.platform === 'win32'
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 filePath = electronBin;
188
- const argList = [DIST_MAIN];
189
- const psArgList = argList.map((item) => `'${quotePsSingle(item)}'`).join(',');
190
- const psScript = `$p = Start-Process -FilePath '${quotePsSingle(filePath)}' -ArgumentList @(${psArgList}) -WorkingDirectory '${quotePsSingle(APP_ROOT)}' -PassThru; Write-Output $p.Id`;
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('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', psScript], {
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 === 0) {
205
- const pid = String(stdout || '').trim().split(/\s+/).pop();
206
- resolve(pid || 'unknown');
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