@web-auto/webauto 0.1.8 → 0.1.11
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/apps/desktop-console/dist/main/index.mjs +909 -105
- package/apps/desktop-console/dist/main/preload.mjs +3 -0
- package/apps/desktop-console/dist/renderer/index.html +9 -1
- package/apps/desktop-console/dist/renderer/index.js +796 -331
- package/apps/desktop-console/entry/ui-cli.mjs +59 -9
- package/apps/desktop-console/entry/ui-console.mjs +8 -3
- package/apps/webauto/entry/account.mjs +70 -9
- package/apps/webauto/entry/lib/account-detect.mjs +106 -25
- package/apps/webauto/entry/lib/account-store.mjs +122 -35
- package/apps/webauto/entry/lib/profilepool.mjs +45 -13
- package/apps/webauto/entry/lib/schedule-store.mjs +1 -25
- package/apps/webauto/entry/profilepool.mjs +45 -3
- package/apps/webauto/entry/schedule.mjs +44 -2
- package/apps/webauto/entry/weibo-unified.mjs +2 -2
- package/apps/webauto/entry/xhs-install.mjs +248 -52
- package/apps/webauto/entry/xhs-unified.mjs +33 -6
- package/bin/webauto.mjs +137 -5
- package/dist/modules/camo-runtime/src/utils/browser-service.mjs +4 -0
- package/dist/services/unified-api/server.js +5 -0
- package/dist/services/unified-api/task-state.js +2 -0
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +142 -14
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +16 -1
- package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +104 -0
- package/modules/camo-runtime/src/autoscript/runtime.mjs +14 -4
- package/modules/camo-runtime/src/autoscript/schema.mjs +9 -0
- package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +9 -2
- package/modules/camo-runtime/src/container/runtime-core/checkpoint.mjs +107 -1
- package/modules/camo-runtime/src/container/runtime-core/subscription.mjs +24 -2
- package/modules/camo-runtime/src/utils/browser-service.mjs +4 -0
- package/package.json +7 -3
- package/runtime/infra/utils/README.md +13 -0
- package/runtime/infra/utils/scripts/README.md +0 -0
- package/runtime/infra/utils/scripts/development/eval-in-session.mjs +40 -0
- package/runtime/infra/utils/scripts/development/highlight-search-containers.mjs +35 -0
- package/runtime/infra/utils/scripts/service/kill-port.mjs +24 -0
- package/runtime/infra/utils/scripts/service/start-api.mjs +103 -0
- package/runtime/infra/utils/scripts/service/start-browser-service.mjs +173 -0
- package/runtime/infra/utils/scripts/service/stop-api.mjs +30 -0
- package/runtime/infra/utils/scripts/service/stop-browser-service.mjs +104 -0
- package/runtime/infra/utils/scripts/test-services.mjs +94 -0
- package/scripts/bump-version.mjs +120 -0
- package/services/unified-api/server.ts +4 -0
- package/services/unified-api/task-state.ts +5 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// src/main/index.mts
|
|
2
2
|
import electron from "electron";
|
|
3
|
-
import { spawn as spawn2 } from "node:child_process";
|
|
3
|
+
import { spawn as spawn2, spawnSync as spawnSync2 } from "node:child_process";
|
|
4
4
|
import os5 from "node:os";
|
|
5
|
-
import
|
|
5
|
+
import path7 from "node:path";
|
|
6
6
|
import { fileURLToPath as fileURLToPath2, pathToFileURL as pathToFileURL3 } from "node:url";
|
|
7
|
-
import { mkdirSync, promises as fs4 } from "node:fs";
|
|
7
|
+
import { mkdirSync, readFileSync, promises as fs4 } from "node:fs";
|
|
8
8
|
|
|
9
9
|
// src/main/desktop-settings.mts
|
|
10
|
-
import { existsSync, promises as fs } from "node:fs";
|
|
10
|
+
import { existsSync as existsSync2, promises as fs } from "node:fs";
|
|
11
11
|
import os from "node:os";
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
import { pathToFileURL } from "node:url";
|
|
@@ -21,7 +21,7 @@ function resolveLegacySettingsPath() {
|
|
|
21
21
|
}
|
|
22
22
|
function hasWindowsDriveD() {
|
|
23
23
|
try {
|
|
24
|
-
return
|
|
24
|
+
return existsSync2("D:\\");
|
|
25
25
|
} catch {
|
|
26
26
|
return false;
|
|
27
27
|
}
|
|
@@ -270,12 +270,16 @@ async function importConfigFromFile(filePath) {
|
|
|
270
270
|
// src/main/core-daemon-manager.mts
|
|
271
271
|
import { spawn } from "child_process";
|
|
272
272
|
import path2 from "path";
|
|
273
|
-
import { existsSync as
|
|
273
|
+
import { existsSync as existsSync3 } from "fs";
|
|
274
274
|
import { fileURLToPath } from "url";
|
|
275
275
|
var REPO_ROOT = path2.resolve(path2.dirname(fileURLToPath(import.meta.url)), "../../../..");
|
|
276
|
-
var
|
|
276
|
+
var UNIFIED_API_HEALTH_URL = "http://127.0.0.1:7701/health";
|
|
277
|
+
var CAMO_RUNTIME_HEALTH_URL = "http://127.0.0.1:7704/health";
|
|
278
|
+
var CORE_HEALTH_URLS = [UNIFIED_API_HEALTH_URL, CAMO_RUNTIME_HEALTH_URL];
|
|
277
279
|
var START_API_SCRIPT = path2.join(REPO_ROOT, "runtime", "infra", "utils", "scripts", "service", "start-api.mjs");
|
|
278
280
|
var STOP_API_SCRIPT = path2.join(REPO_ROOT, "runtime", "infra", "utils", "scripts", "service", "stop-api.mjs");
|
|
281
|
+
var DIST_UNIFIED_ENTRY = path2.join(REPO_ROOT, "dist", "apps", "webauto", "server.js");
|
|
282
|
+
var fallbackUnifiedApiPid = null;
|
|
279
283
|
function sleep(ms) {
|
|
280
284
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
281
285
|
}
|
|
@@ -301,7 +305,7 @@ function resolveOnPath(candidates) {
|
|
|
301
305
|
for (const dir of dirs) {
|
|
302
306
|
for (const name of candidates) {
|
|
303
307
|
const full = path2.join(dir, name);
|
|
304
|
-
if (
|
|
308
|
+
if (existsSync3(full)) return full;
|
|
305
309
|
}
|
|
306
310
|
}
|
|
307
311
|
return null;
|
|
@@ -323,6 +327,9 @@ async function areCoreServicesHealthy() {
|
|
|
323
327
|
const health = await Promise.all(CORE_HEALTH_URLS.map((url) => checkHttpHealth(url)));
|
|
324
328
|
return health.every(Boolean);
|
|
325
329
|
}
|
|
330
|
+
async function isUnifiedApiHealthy() {
|
|
331
|
+
return checkHttpHealth(UNIFIED_API_HEALTH_URL);
|
|
332
|
+
}
|
|
326
333
|
async function runNodeScript(scriptPath, timeoutMs) {
|
|
327
334
|
return new Promise((resolve) => {
|
|
328
335
|
const nodeBin = resolveNodeBin();
|
|
@@ -353,6 +360,45 @@ async function runNodeScript(scriptPath, timeoutMs) {
|
|
|
353
360
|
});
|
|
354
361
|
});
|
|
355
362
|
}
|
|
363
|
+
async function runLongLivedNodeScript(scriptPath) {
|
|
364
|
+
return new Promise((resolve) => {
|
|
365
|
+
const nodeBin = resolveNodeBin();
|
|
366
|
+
const child = spawn(nodeBin, [scriptPath], {
|
|
367
|
+
cwd: REPO_ROOT,
|
|
368
|
+
stdio: "ignore",
|
|
369
|
+
windowsHide: true,
|
|
370
|
+
detached: false,
|
|
371
|
+
env: {
|
|
372
|
+
...process.env,
|
|
373
|
+
WEBAUTO_RUNTIME_MODE: "unified"
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
let settled = false;
|
|
377
|
+
const timer = setTimeout(() => {
|
|
378
|
+
if (settled) return;
|
|
379
|
+
settled = true;
|
|
380
|
+
fallbackUnifiedApiPid = typeof child.pid === "number" ? child.pid : null;
|
|
381
|
+
resolve(true);
|
|
382
|
+
}, 200);
|
|
383
|
+
child.once("error", () => {
|
|
384
|
+
if (settled) return;
|
|
385
|
+
settled = true;
|
|
386
|
+
clearTimeout(timer);
|
|
387
|
+
resolve(false);
|
|
388
|
+
});
|
|
389
|
+
child.once("exit", () => {
|
|
390
|
+
if (!settled) {
|
|
391
|
+
settled = true;
|
|
392
|
+
clearTimeout(timer);
|
|
393
|
+
resolve(false);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (fallbackUnifiedApiPid && child.pid === fallbackUnifiedApiPid) {
|
|
397
|
+
fallbackUnifiedApiPid = null;
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
}
|
|
356
402
|
async function runCommand(command, args, timeoutMs) {
|
|
357
403
|
return new Promise((resolve) => {
|
|
358
404
|
const lower = String(command || "").toLowerCase();
|
|
@@ -394,7 +440,7 @@ async function runCommand(command, args, timeoutMs) {
|
|
|
394
440
|
}
|
|
395
441
|
async function startCoreDaemon() {
|
|
396
442
|
if (await areCoreServicesHealthy()) return true;
|
|
397
|
-
const startedApi = await runNodeScript(START_API_SCRIPT, 4e4);
|
|
443
|
+
const startedApi = existsSync3(START_API_SCRIPT) ? await runNodeScript(START_API_SCRIPT, 4e4) : existsSync3(DIST_UNIFIED_ENTRY) ? await runLongLivedNodeScript(DIST_UNIFIED_ENTRY) : false;
|
|
398
444
|
if (!startedApi) {
|
|
399
445
|
console.error("[CoreDaemonManager] Failed to start unified API service");
|
|
400
446
|
return false;
|
|
@@ -405,17 +451,29 @@ async function startCoreDaemon() {
|
|
|
405
451
|
4e4
|
|
406
452
|
);
|
|
407
453
|
if (!startedBrowser) {
|
|
408
|
-
console.
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
454
|
+
console.warn("[CoreDaemonManager] Failed to start camo browser backend, continue in degraded mode");
|
|
455
|
+
}
|
|
456
|
+
for (let i = 0; i < 60; i += 1) {
|
|
457
|
+
const [allHealthy, unifiedHealthy] = await Promise.all([
|
|
458
|
+
areCoreServicesHealthy(),
|
|
459
|
+
isUnifiedApiHealthy()
|
|
460
|
+
]);
|
|
461
|
+
if (allHealthy) return true;
|
|
462
|
+
if (unifiedHealthy) return true;
|
|
463
|
+
await sleep(500);
|
|
464
|
+
}
|
|
465
|
+
console.error("[CoreDaemonManager] Unified API still unhealthy after start");
|
|
416
466
|
return false;
|
|
417
467
|
}
|
|
418
468
|
async function stopCoreDaemon() {
|
|
469
|
+
if (fallbackUnifiedApiPid) {
|
|
470
|
+
try {
|
|
471
|
+
process.kill(fallbackUnifiedApiPid, "SIGTERM");
|
|
472
|
+
} catch {
|
|
473
|
+
}
|
|
474
|
+
fallbackUnifiedApiPid = null;
|
|
475
|
+
}
|
|
476
|
+
if (!existsSync3(STOP_API_SCRIPT)) return true;
|
|
419
477
|
const stoppedApi = await runNodeScript(STOP_API_SCRIPT, 2e4);
|
|
420
478
|
if (!stoppedApi) {
|
|
421
479
|
console.error("[CoreDaemonManager] Failed to stop core services");
|
|
@@ -730,12 +788,30 @@ var stateBridge = new StateBridge();
|
|
|
730
788
|
|
|
731
789
|
// src/main/env-check.mts
|
|
732
790
|
import { spawnSync } from "node:child_process";
|
|
733
|
-
import { existsSync as
|
|
791
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
734
792
|
import path4 from "node:path";
|
|
735
793
|
import os3 from "node:os";
|
|
736
794
|
function resolveWebautoRoot() {
|
|
737
|
-
const
|
|
738
|
-
|
|
795
|
+
const normalizePathForPlatform2 = (raw, platform = process.platform) => {
|
|
796
|
+
const input = String(raw || "").trim();
|
|
797
|
+
const isWinPath = platform === "win32" || /^[A-Za-z]:[\\/]/.test(input);
|
|
798
|
+
const pathApi = isWinPath ? path4.win32 : path4;
|
|
799
|
+
return isWinPath ? pathApi.normalize(input) : path4.resolve(input);
|
|
800
|
+
};
|
|
801
|
+
const normalizeLegacyWebautoRoot2 = (raw, platform = process.platform) => {
|
|
802
|
+
const pathApi = platform === "win32" ? path4.win32 : path4;
|
|
803
|
+
const resolved = normalizePathForPlatform2(raw, platform);
|
|
804
|
+
const base = pathApi.basename(resolved).toLowerCase();
|
|
805
|
+
return base === ".webauto" || base === "webauto" ? resolved : pathApi.join(resolved, ".webauto");
|
|
806
|
+
};
|
|
807
|
+
const explicitHome = String(process.env.WEBAUTO_HOME || "").trim();
|
|
808
|
+
if (explicitHome) return normalizePathForPlatform2(explicitHome);
|
|
809
|
+
const legacyRoot = String(process.env.WEBAUTO_ROOT || process.env.WEBAUTO_PORTABLE_ROOT || "").trim();
|
|
810
|
+
if (legacyRoot) return normalizeLegacyWebautoRoot2(legacyRoot);
|
|
811
|
+
if (process.platform === "win32") {
|
|
812
|
+
return existsSync4("D:\\") ? "D:\\webauto" : path4.join(os3.homedir(), ".webauto");
|
|
813
|
+
}
|
|
814
|
+
return path4.join(os3.homedir(), ".webauto");
|
|
739
815
|
}
|
|
740
816
|
function resolveNpxBin2() {
|
|
741
817
|
if (process.platform !== "win32") return "npx";
|
|
@@ -748,7 +824,7 @@ function resolveOnPath2(candidates) {
|
|
|
748
824
|
for (const dir of dirs) {
|
|
749
825
|
for (const name of candidates) {
|
|
750
826
|
const full = path4.join(dir, name);
|
|
751
|
-
if (
|
|
827
|
+
if (existsSync4(full)) return full;
|
|
752
828
|
}
|
|
753
829
|
}
|
|
754
830
|
return null;
|
|
@@ -817,6 +893,13 @@ function resolvePathFromOutput(stdout) {
|
|
|
817
893
|
}
|
|
818
894
|
return "";
|
|
819
895
|
}
|
|
896
|
+
function resolveCamoufoxExecutable(installRoot) {
|
|
897
|
+
return process.platform === "win32" ? path4.join(installRoot, "camoufox.exe") : path4.join(installRoot, "camoufox");
|
|
898
|
+
}
|
|
899
|
+
function isValidCamoufoxInstallRoot(installRoot) {
|
|
900
|
+
if (!installRoot || !existsSync4(installRoot)) return false;
|
|
901
|
+
return existsSync4(resolveCamoufoxExecutable(installRoot));
|
|
902
|
+
}
|
|
820
903
|
async function checkCamoCli() {
|
|
821
904
|
const camoCandidates = process.platform === "win32" ? ["camo.cmd", "camo.exe", "camo.bat", "camo.ps1"] : ["camo"];
|
|
822
905
|
for (const candidate of camoCandidates) {
|
|
@@ -832,7 +915,7 @@ async function checkCamoCli() {
|
|
|
832
915
|
for (const localRoot of localRoots) {
|
|
833
916
|
for (const suffix of camoCandidates) {
|
|
834
917
|
const candidate = path4.resolve(localRoot, suffix);
|
|
835
|
-
if (!
|
|
918
|
+
if (!existsSync4(candidate)) continue;
|
|
836
919
|
const ret = runVersionCheck(candidate, ["help"], candidate);
|
|
837
920
|
if (ret.installed) return ret;
|
|
838
921
|
}
|
|
@@ -858,10 +941,12 @@ async function checkServices() {
|
|
|
858
941
|
}
|
|
859
942
|
async function checkFirefox() {
|
|
860
943
|
const candidates = process.platform === "win32" ? [
|
|
944
|
+
{ command: "camoufox", args: ["path"] },
|
|
861
945
|
{ command: "python", args: ["-m", "camoufox", "path"] },
|
|
862
946
|
{ command: "py", args: ["-3", "-m", "camoufox", "path"] },
|
|
863
947
|
{ command: resolveNpxBin2(), args: ["--yes", "--package=camoufox", "camoufox", "path"] }
|
|
864
948
|
] : [
|
|
949
|
+
{ command: "camoufox", args: ["path"] },
|
|
865
950
|
{ command: "python3", args: ["-m", "camoufox", "path"] },
|
|
866
951
|
{ command: resolveNpxBin2(), args: ["--yes", "--package=camoufox", "camoufox", "path"] }
|
|
867
952
|
];
|
|
@@ -874,7 +959,10 @@ async function checkFirefox() {
|
|
|
874
959
|
});
|
|
875
960
|
if (ret.status !== 0) continue;
|
|
876
961
|
const resolvedPath = resolvePathFromOutput(String(ret.stdout || ""));
|
|
877
|
-
|
|
962
|
+
if (resolvedPath && isValidCamoufoxInstallRoot(resolvedPath)) {
|
|
963
|
+
return { installed: true, path: resolvedPath };
|
|
964
|
+
}
|
|
965
|
+
if (!resolvedPath) return { installed: true };
|
|
878
966
|
} catch {
|
|
879
967
|
}
|
|
880
968
|
}
|
|
@@ -882,7 +970,7 @@ async function checkFirefox() {
|
|
|
882
970
|
}
|
|
883
971
|
async function checkGeoIP() {
|
|
884
972
|
const geoIpPath = path4.join(resolveWebautoRoot(), "geoip", "GeoLite2-City.mmdb");
|
|
885
|
-
if (
|
|
973
|
+
if (existsSync4(geoIpPath)) {
|
|
886
974
|
return { installed: true, path: geoIpPath };
|
|
887
975
|
}
|
|
888
976
|
return { installed: false };
|
|
@@ -894,8 +982,16 @@ async function checkEnvironment() {
|
|
|
894
982
|
checkFirefox(),
|
|
895
983
|
checkGeoIP()
|
|
896
984
|
]);
|
|
897
|
-
const
|
|
898
|
-
|
|
985
|
+
const browserReady = Boolean(firefox.installed || services.camoRuntime);
|
|
986
|
+
const missing = {
|
|
987
|
+
core: !services.unifiedApi,
|
|
988
|
+
runtimeService: !services.camoRuntime,
|
|
989
|
+
camo: !camo.installed,
|
|
990
|
+
runtime: !browserReady,
|
|
991
|
+
geoip: !geoip.installed
|
|
992
|
+
};
|
|
993
|
+
const allReady = camo.installed && services.unifiedApi && browserReady;
|
|
994
|
+
return { camo, services, firefox, geoip, browserReady, missing, allReady };
|
|
899
995
|
}
|
|
900
996
|
|
|
901
997
|
// src/main/ui-cli-bridge.mts
|
|
@@ -905,7 +1001,33 @@ import path5 from "node:path";
|
|
|
905
1001
|
import { promises as fs3 } from "node:fs";
|
|
906
1002
|
var DEFAULT_HOST = "127.0.0.1";
|
|
907
1003
|
var DEFAULT_PORT = 7716;
|
|
908
|
-
|
|
1004
|
+
function normalizePathForPlatform(raw, platform = process.platform) {
|
|
1005
|
+
const input = String(raw || "").trim();
|
|
1006
|
+
const isWinPath = platform === "win32" || /^[A-Za-z]:[\\/]/.test(input);
|
|
1007
|
+
const pathApi = isWinPath ? path5.win32 : path5;
|
|
1008
|
+
return isWinPath ? pathApi.normalize(input) : path5.resolve(input);
|
|
1009
|
+
}
|
|
1010
|
+
function normalizeLegacyWebautoRoot(raw, platform = process.platform) {
|
|
1011
|
+
const pathApi = platform === "win32" ? path5.win32 : path5;
|
|
1012
|
+
const resolved = normalizePathForPlatform(raw, platform);
|
|
1013
|
+
const base = pathApi.basename(resolved).toLowerCase();
|
|
1014
|
+
return base === ".webauto" || base === "webauto" ? resolved : pathApi.join(resolved, ".webauto");
|
|
1015
|
+
}
|
|
1016
|
+
function resolveWebautoRoot2() {
|
|
1017
|
+
const explicitHome = String(process.env.WEBAUTO_HOME || "").trim();
|
|
1018
|
+
if (explicitHome) return normalizePathForPlatform(explicitHome);
|
|
1019
|
+
const legacyRoot = String(process.env.WEBAUTO_ROOT || process.env.WEBAUTO_PORTABLE_ROOT || "").trim();
|
|
1020
|
+
if (legacyRoot) return normalizeLegacyWebautoRoot(legacyRoot);
|
|
1021
|
+
if (process.platform === "win32") {
|
|
1022
|
+
try {
|
|
1023
|
+
if (existsSync("D:\\")) return "D:\\webauto";
|
|
1024
|
+
} catch {
|
|
1025
|
+
}
|
|
1026
|
+
return path5.join(os4.homedir(), ".webauto");
|
|
1027
|
+
}
|
|
1028
|
+
return path5.join(os4.homedir(), ".webauto");
|
|
1029
|
+
}
|
|
1030
|
+
var CONTROL_FILE = path5.join(resolveWebautoRoot2(), "run", "ui-cli.json");
|
|
909
1031
|
function readInt(input, fallback) {
|
|
910
1032
|
const n = Number(input);
|
|
911
1033
|
return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
|
|
@@ -985,6 +1107,20 @@ function buildSnapshotScript() {
|
|
|
985
1107
|
if ('value' in el) return String(el.value ?? '');
|
|
986
1108
|
return String(el.textContent || '').trim();
|
|
987
1109
|
};
|
|
1110
|
+
const firstText = (selectors) => {
|
|
1111
|
+
for (const sel of selectors) {
|
|
1112
|
+
const v = text(sel);
|
|
1113
|
+
if (v) return v;
|
|
1114
|
+
}
|
|
1115
|
+
return '';
|
|
1116
|
+
};
|
|
1117
|
+
const firstValue = (selectors) => {
|
|
1118
|
+
for (const sel of selectors) {
|
|
1119
|
+
const v = value(sel);
|
|
1120
|
+
if (v) return v;
|
|
1121
|
+
}
|
|
1122
|
+
return '';
|
|
1123
|
+
};
|
|
988
1124
|
const activeTab = document.querySelector('.tab.active');
|
|
989
1125
|
const errors = Array.from(document.querySelectorAll('#recent-errors-list li'))
|
|
990
1126
|
.map((el) => String(el.textContent || '').trim())
|
|
@@ -1000,10 +1136,10 @@ function buildSnapshotScript() {
|
|
|
1000
1136
|
currentPhase: text('#current-phase'),
|
|
1001
1137
|
currentAction: text('#current-action'),
|
|
1002
1138
|
progressPercent: text('#progress-percent'),
|
|
1003
|
-
keyword:
|
|
1004
|
-
target:
|
|
1005
|
-
account:
|
|
1006
|
-
env:
|
|
1139
|
+
keyword: firstValue(['#task-keyword', '#keyword-input', '#task-keyword-input']),
|
|
1140
|
+
target: firstValue(['#task-target', '#target-input', '#task-target-input']),
|
|
1141
|
+
account: firstValue(['#task-account', '#task-profile', '#account-select']),
|
|
1142
|
+
env: firstValue(['#task-env', '#env-select']),
|
|
1007
1143
|
recentErrors: errors,
|
|
1008
1144
|
ts: new Date().toISOString(),
|
|
1009
1145
|
};
|
|
@@ -1451,28 +1587,337 @@ var UiCliBridge = class {
|
|
|
1451
1587
|
}
|
|
1452
1588
|
};
|
|
1453
1589
|
|
|
1590
|
+
// src/main/task-gateway.mts
|
|
1591
|
+
import path6 from "node:path";
|
|
1592
|
+
var KEYWORD_REQUIRED_TYPES = /* @__PURE__ */ new Set(["xhs-unified", "weibo-search", "1688-search"]);
|
|
1593
|
+
var RUN_AT_TYPES = /* @__PURE__ */ new Set(["once", "daily", "weekly"]);
|
|
1594
|
+
function asText(value) {
|
|
1595
|
+
return String(value ?? "").trim();
|
|
1596
|
+
}
|
|
1597
|
+
function asPositiveInt(value, fallback) {
|
|
1598
|
+
const n = Number(value);
|
|
1599
|
+
if (!Number.isFinite(n) || n <= 0) return fallback;
|
|
1600
|
+
return Math.max(1, Math.floor(n));
|
|
1601
|
+
}
|
|
1602
|
+
function asBool(value, fallback) {
|
|
1603
|
+
if (typeof value === "boolean") return value;
|
|
1604
|
+
const text = asText(value).toLowerCase();
|
|
1605
|
+
if (!text) return fallback;
|
|
1606
|
+
if (["1", "true", "yes", "y", "on"].includes(text)) return true;
|
|
1607
|
+
if (["0", "false", "no", "n", "off"].includes(text)) return false;
|
|
1608
|
+
return fallback;
|
|
1609
|
+
}
|
|
1610
|
+
function asOptionalRunAt(value) {
|
|
1611
|
+
const runAt = asText(value);
|
|
1612
|
+
return runAt ? runAt : null;
|
|
1613
|
+
}
|
|
1614
|
+
function normalizeScheduleType(value) {
|
|
1615
|
+
const raw = asText(value);
|
|
1616
|
+
if (raw === "once" || raw === "daily" || raw === "weekly") return raw;
|
|
1617
|
+
return "interval";
|
|
1618
|
+
}
|
|
1619
|
+
function deriveTaskName(commandType, argv) {
|
|
1620
|
+
const keyword = asText(argv.keyword);
|
|
1621
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
1622
|
+
return keyword ? `${commandType}-${keyword}` : `${commandType}-${stamp}`;
|
|
1623
|
+
}
|
|
1624
|
+
function getPlatformFromCommandType(commandType) {
|
|
1625
|
+
const value = asText(commandType).toLowerCase();
|
|
1626
|
+
if (value.startsWith("weibo")) return "weibo";
|
|
1627
|
+
if (value.startsWith("1688")) return "1688";
|
|
1628
|
+
return "xiaohongshu";
|
|
1629
|
+
}
|
|
1630
|
+
function hasExplicitProfileArg(argv) {
|
|
1631
|
+
return Boolean(asText(argv?.profile) || asText(argv?.profiles) || asText(argv?.profilepool));
|
|
1632
|
+
}
|
|
1633
|
+
async function pickDefaultProfileForPlatform(options, platform) {
|
|
1634
|
+
const accountScript = path6.join(options.repoRoot, "apps", "webauto", "entry", "account.mjs");
|
|
1635
|
+
const ret = await options.runJson({
|
|
1636
|
+
title: `account list --platform ${platform}`,
|
|
1637
|
+
cwd: options.repoRoot,
|
|
1638
|
+
args: [accountScript, "list", "--platform", platform, "--json"],
|
|
1639
|
+
timeoutMs: 2e4
|
|
1640
|
+
});
|
|
1641
|
+
if (!ret?.ok) return "";
|
|
1642
|
+
const rows = Array.isArray(ret?.json?.profiles) ? ret.json.profiles : [];
|
|
1643
|
+
const validRows = rows.filter((row) => row?.valid === true && asText(row?.accountId)).sort((a, b) => {
|
|
1644
|
+
const ta = Date.parse(asText(a?.updatedAt) || "") || 0;
|
|
1645
|
+
const tb = Date.parse(asText(b?.updatedAt) || "") || 0;
|
|
1646
|
+
if (tb !== ta) return tb - ta;
|
|
1647
|
+
return asText(a?.profileId).localeCompare(asText(b?.profileId));
|
|
1648
|
+
});
|
|
1649
|
+
return asText(validRows[0]?.profileId) || "";
|
|
1650
|
+
}
|
|
1651
|
+
async function ensureProfileArg(options, commandType, argv) {
|
|
1652
|
+
if (hasExplicitProfileArg(argv)) return argv;
|
|
1653
|
+
const platform = getPlatformFromCommandType(commandType);
|
|
1654
|
+
const profileId = await pickDefaultProfileForPlatform(options, platform);
|
|
1655
|
+
if (!profileId) {
|
|
1656
|
+
throw new Error(`\u672A\u6307\u5B9A Profile\uFF0C\u4E14\u672A\u627E\u5230\u5E73\u53F0(${platform})\u6709\u6548\u8D26\u53F7\u3002\u8BF7\u5148\u5728\u8D26\u53F7\u9875\u767B\u5F55\u5E76\u6821\u9A8C\u540E\u91CD\u8BD5\u3002`);
|
|
1657
|
+
}
|
|
1658
|
+
return {
|
|
1659
|
+
...argv,
|
|
1660
|
+
profile: profileId
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
function normalizeWeiboTaskType(commandType) {
|
|
1664
|
+
if (commandType === "weibo-search") return "search";
|
|
1665
|
+
if (commandType === "weibo-monitor") return "monitor";
|
|
1666
|
+
return "timeline";
|
|
1667
|
+
}
|
|
1668
|
+
function createUiTriggerId() {
|
|
1669
|
+
return `ui-${Date.now()}-${Math.random().toString(16).slice(2, 10)}`;
|
|
1670
|
+
}
|
|
1671
|
+
function normalizeSavePayload(payload) {
|
|
1672
|
+
const commandType = asText(payload?.commandType) || "xhs-unified";
|
|
1673
|
+
const argv = payload?.argv && typeof payload.argv === "object" ? { ...payload.argv } : {};
|
|
1674
|
+
const scheduleType = normalizeScheduleType(payload?.scheduleType);
|
|
1675
|
+
const runAt = asOptionalRunAt(payload?.runAt);
|
|
1676
|
+
const intervalMinutes = asPositiveInt(payload?.intervalMinutes, 30);
|
|
1677
|
+
const maxRunsRaw = Number(payload?.maxRuns);
|
|
1678
|
+
const maxRuns = Number.isFinite(maxRunsRaw) && maxRunsRaw > 0 ? Math.max(1, Math.floor(maxRunsRaw)) : null;
|
|
1679
|
+
const name = asText(payload?.name) || deriveTaskName(commandType, argv);
|
|
1680
|
+
const enabled = asBool(payload?.enabled, true);
|
|
1681
|
+
const id = asText(payload?.id);
|
|
1682
|
+
if (KEYWORD_REQUIRED_TYPES.has(commandType) && !asText(argv.keyword)) {
|
|
1683
|
+
throw new Error("\u5173\u952E\u8BCD\u4E0D\u80FD\u4E3A\u7A7A");
|
|
1684
|
+
}
|
|
1685
|
+
if (commandType.startsWith("weibo-")) {
|
|
1686
|
+
argv["task-type"] = asText(argv["task-type"]) || normalizeWeiboTaskType(commandType);
|
|
1687
|
+
if (commandType === "weibo-monitor" && !asText(argv["user-id"])) {
|
|
1688
|
+
throw new Error("\u5FAE\u535A monitor \u4EFB\u52A1\u9700\u8981 user-id");
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
if (RUN_AT_TYPES.has(scheduleType) && !runAt) {
|
|
1692
|
+
throw new Error(`${scheduleType} \u4EFB\u52A1\u9700\u8981\u951A\u70B9\u65F6\u95F4`);
|
|
1693
|
+
}
|
|
1694
|
+
return {
|
|
1695
|
+
id,
|
|
1696
|
+
name,
|
|
1697
|
+
enabled,
|
|
1698
|
+
commandType,
|
|
1699
|
+
scheduleType,
|
|
1700
|
+
intervalMinutes,
|
|
1701
|
+
runAt,
|
|
1702
|
+
maxRuns,
|
|
1703
|
+
argv
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
async function runScheduleJson(options, args, timeoutMs) {
|
|
1707
|
+
const script = path6.join(options.repoRoot, "apps", "webauto", "entry", "schedule.mjs");
|
|
1708
|
+
const ret = await options.runJson({
|
|
1709
|
+
title: `schedule ${args.join(" ")}`.trim(),
|
|
1710
|
+
cwd: options.repoRoot,
|
|
1711
|
+
args: [script, ...args, "--json"],
|
|
1712
|
+
timeoutMs: typeof timeoutMs === "number" && timeoutMs > 0 ? timeoutMs : void 0
|
|
1713
|
+
});
|
|
1714
|
+
if (!ret?.ok) {
|
|
1715
|
+
const reason = asText(ret?.error) || asText(ret?.stderr) || asText(ret?.stdout) || "schedule command failed";
|
|
1716
|
+
return { ok: false, error: reason };
|
|
1717
|
+
}
|
|
1718
|
+
return { ok: true, json: ret?.json || {} };
|
|
1719
|
+
}
|
|
1720
|
+
async function scheduleInvoke(options, input) {
|
|
1721
|
+
try {
|
|
1722
|
+
const action = asText(input?.action);
|
|
1723
|
+
const timeoutMs = input?.timeoutMs;
|
|
1724
|
+
if (action === "list") return runScheduleJson(options, ["list"], timeoutMs);
|
|
1725
|
+
if (action === "get") {
|
|
1726
|
+
const taskId = asText(input?.taskId);
|
|
1727
|
+
if (!taskId) return { ok: false, error: "missing taskId" };
|
|
1728
|
+
return runScheduleJson(options, ["get", taskId], timeoutMs);
|
|
1729
|
+
}
|
|
1730
|
+
if (action === "save") {
|
|
1731
|
+
const payload = normalizeSavePayload(input?.payload);
|
|
1732
|
+
payload.argv = await ensureProfileArg(options, payload.commandType, payload.argv);
|
|
1733
|
+
const args = payload.id ? ["update", payload.id] : ["add"];
|
|
1734
|
+
args.push("--name", payload.name);
|
|
1735
|
+
args.push("--enabled", String(payload.enabled));
|
|
1736
|
+
args.push("--command-type", payload.commandType);
|
|
1737
|
+
args.push("--schedule-type", payload.scheduleType);
|
|
1738
|
+
if (RUN_AT_TYPES.has(payload.scheduleType)) {
|
|
1739
|
+
args.push("--run-at", String(payload.runAt || ""));
|
|
1740
|
+
} else {
|
|
1741
|
+
args.push("--interval-minutes", String(payload.intervalMinutes));
|
|
1742
|
+
}
|
|
1743
|
+
args.push("--max-runs", payload.maxRuns === null ? "0" : String(payload.maxRuns));
|
|
1744
|
+
args.push("--argv-json", JSON.stringify(payload.argv));
|
|
1745
|
+
return runScheduleJson(options, args, timeoutMs);
|
|
1746
|
+
}
|
|
1747
|
+
if (action === "run") {
|
|
1748
|
+
const taskId = asText(input?.taskId);
|
|
1749
|
+
if (!taskId) return { ok: false, error: "missing taskId" };
|
|
1750
|
+
return runScheduleJson(options, ["run", taskId], timeoutMs);
|
|
1751
|
+
}
|
|
1752
|
+
if (action === "delete") {
|
|
1753
|
+
const taskId = asText(input?.taskId);
|
|
1754
|
+
if (!taskId) return { ok: false, error: "missing taskId" };
|
|
1755
|
+
return runScheduleJson(options, ["delete", taskId], timeoutMs);
|
|
1756
|
+
}
|
|
1757
|
+
if (action === "export") {
|
|
1758
|
+
const taskId = asText(input?.taskId);
|
|
1759
|
+
return taskId ? runScheduleJson(options, ["export", taskId], timeoutMs) : runScheduleJson(options, ["export"], timeoutMs);
|
|
1760
|
+
}
|
|
1761
|
+
if (action === "import") {
|
|
1762
|
+
const payloadJson = asText(input?.payloadJson);
|
|
1763
|
+
if (!payloadJson) return { ok: false, error: "missing payloadJson" };
|
|
1764
|
+
const mode = asText(input?.mode) === "replace" ? "replace" : "merge";
|
|
1765
|
+
return runScheduleJson(options, ["import", "--payload-json", payloadJson, "--mode", mode], timeoutMs);
|
|
1766
|
+
}
|
|
1767
|
+
if (action === "run-due") {
|
|
1768
|
+
const limit = asPositiveInt(input?.limit, 20);
|
|
1769
|
+
return runScheduleJson(options, ["run-due", "--limit", String(limit)], timeoutMs);
|
|
1770
|
+
}
|
|
1771
|
+
if (action === "daemon-start") {
|
|
1772
|
+
const interval = asPositiveInt(input?.intervalSec, 30);
|
|
1773
|
+
const limit = asPositiveInt(input?.limit, 20);
|
|
1774
|
+
const script = path6.join(options.repoRoot, "apps", "webauto", "entry", "schedule.mjs");
|
|
1775
|
+
const ret = await options.spawnCommand({
|
|
1776
|
+
title: `schedule daemon ${interval}s`,
|
|
1777
|
+
cwd: options.repoRoot,
|
|
1778
|
+
args: [script, "daemon", "--interval-sec", String(Math.max(5, interval)), "--limit", String(limit), "--json"],
|
|
1779
|
+
groupKey: "scheduler"
|
|
1780
|
+
});
|
|
1781
|
+
return { ok: true, runId: asText(ret?.runId) };
|
|
1782
|
+
}
|
|
1783
|
+
return { ok: false, error: `unsupported action: ${action || "<empty>"}` };
|
|
1784
|
+
} catch (err) {
|
|
1785
|
+
return { ok: false, error: err?.message || String(err) };
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
async function runEphemeralTask(options, input) {
|
|
1789
|
+
try {
|
|
1790
|
+
const commandType = asText(input?.commandType) || "xhs-unified";
|
|
1791
|
+
let argv = input?.argv && typeof input.argv === "object" ? { ...input.argv } : {};
|
|
1792
|
+
argv = await ensureProfileArg(options, commandType, argv);
|
|
1793
|
+
const profile = asText(argv.profile);
|
|
1794
|
+
const keyword = asText(argv.keyword);
|
|
1795
|
+
const target = asPositiveInt(argv["max-notes"] ?? argv.target, 50);
|
|
1796
|
+
const env = asText(argv.env) || "debug";
|
|
1797
|
+
if (!profile) return { ok: false, error: "\u8BF7\u8F93\u5165 Profile ID" };
|
|
1798
|
+
if (KEYWORD_REQUIRED_TYPES.has(commandType) && !keyword) {
|
|
1799
|
+
return { ok: false, error: "\u8BF7\u8F93\u5165\u5173\u952E\u8BCD" };
|
|
1800
|
+
}
|
|
1801
|
+
if (commandType === "xhs-unified") {
|
|
1802
|
+
const uiTriggerId = createUiTriggerId();
|
|
1803
|
+
const script = path6.join(options.repoRoot, "apps", "webauto", "entry", "xhs-unified.mjs");
|
|
1804
|
+
const ret = await options.spawnCommand({
|
|
1805
|
+
title: `xhs unified: ${keyword}`,
|
|
1806
|
+
cwd: options.repoRoot,
|
|
1807
|
+
groupKey: "xhs-unified",
|
|
1808
|
+
args: [
|
|
1809
|
+
script,
|
|
1810
|
+
"--profile",
|
|
1811
|
+
profile,
|
|
1812
|
+
"--keyword",
|
|
1813
|
+
keyword,
|
|
1814
|
+
"--target",
|
|
1815
|
+
String(target),
|
|
1816
|
+
"--max-notes",
|
|
1817
|
+
String(target),
|
|
1818
|
+
"--env",
|
|
1819
|
+
env,
|
|
1820
|
+
"--do-comments",
|
|
1821
|
+
String(asBool(argv["do-comments"], true)),
|
|
1822
|
+
"--fetch-body",
|
|
1823
|
+
String(asBool(argv["fetch-body"], true)),
|
|
1824
|
+
"--do-likes",
|
|
1825
|
+
String(asBool(argv["do-likes"], false)),
|
|
1826
|
+
"--like-keywords",
|
|
1827
|
+
asText(argv["like-keywords"]),
|
|
1828
|
+
"--ui-trigger-id",
|
|
1829
|
+
uiTriggerId
|
|
1830
|
+
]
|
|
1831
|
+
});
|
|
1832
|
+
return { ok: true, runId: asText(ret?.runId), commandType, profile, uiTriggerId };
|
|
1833
|
+
}
|
|
1834
|
+
if (commandType === "weibo-search") {
|
|
1835
|
+
const script = path6.join(options.repoRoot, "apps", "webauto", "entry", "weibo-unified.mjs");
|
|
1836
|
+
const ret = await options.spawnCommand({
|
|
1837
|
+
title: `weibo: ${keyword}`,
|
|
1838
|
+
cwd: options.repoRoot,
|
|
1839
|
+
groupKey: "weibo-search",
|
|
1840
|
+
args: [
|
|
1841
|
+
script,
|
|
1842
|
+
"search",
|
|
1843
|
+
"--profile",
|
|
1844
|
+
profile,
|
|
1845
|
+
"--keyword",
|
|
1846
|
+
keyword,
|
|
1847
|
+
"--target",
|
|
1848
|
+
String(target),
|
|
1849
|
+
"--env",
|
|
1850
|
+
env
|
|
1851
|
+
]
|
|
1852
|
+
});
|
|
1853
|
+
return { ok: true, runId: asText(ret?.runId), commandType, profile };
|
|
1854
|
+
}
|
|
1855
|
+
return { ok: false, error: `\u5F53\u524D\u4EFB\u52A1\u7C7B\u578B\u6682\u4E0D\u652F\u6301\u4EC5\u6267\u884C(\u4E0D\u4FDD\u5B58): ${commandType}` };
|
|
1856
|
+
} catch (err) {
|
|
1857
|
+
return { ok: false, error: err?.message || String(err) };
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1454
1861
|
// src/main/index.mts
|
|
1455
1862
|
var { app, BrowserWindow: BrowserWindow2, ipcMain: ipcMain2, shell, clipboard } = electron;
|
|
1456
|
-
var
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1863
|
+
var spawnedBrowserProcesses = /* @__PURE__ */ new Set();
|
|
1864
|
+
function trackBrowserProcess(pid) {
|
|
1865
|
+
if (pid > 0) {
|
|
1866
|
+
spawnedBrowserProcesses.add(pid);
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
function cleanupAllBrowserProcesses(reason = "ui_close") {
|
|
1870
|
+
console.log(`[process-cleanup] Cleaning up ${spawnedBrowserProcesses.size} browser process(s) (${reason})`);
|
|
1871
|
+
for (const pid of spawnedBrowserProcesses) {
|
|
1872
|
+
try {
|
|
1873
|
+
if (process.platform === "win32") {
|
|
1874
|
+
spawn2("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
|
|
1875
|
+
} else {
|
|
1876
|
+
process.kill(pid, "SIGTERM");
|
|
1877
|
+
}
|
|
1878
|
+
} catch (err) {
|
|
1879
|
+
console.warn(`[process-cleanup] Failed to kill PID ${pid}:`, err);
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
spawnedBrowserProcesses.clear();
|
|
1883
|
+
console.log(`[process-cleanup] Cleanup complete`);
|
|
1884
|
+
}
|
|
1885
|
+
var __dirname = path7.dirname(fileURLToPath2(import.meta.url));
|
|
1886
|
+
var APP_ROOT = path7.resolve(__dirname, "../..");
|
|
1887
|
+
var REPO_ROOT2 = path7.resolve(APP_ROOT, "../..");
|
|
1888
|
+
function readJsonVersion(filePath) {
|
|
1889
|
+
try {
|
|
1890
|
+
const json = JSON.parse(readFileSync(filePath, "utf8"));
|
|
1891
|
+
return String(json?.version || "").trim();
|
|
1892
|
+
} catch {
|
|
1893
|
+
return "";
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
function resolveVersionInfo() {
|
|
1897
|
+
const webauto = readJsonVersion(path7.join(REPO_ROOT2, "package.json")) || "0.0.0";
|
|
1898
|
+
const desktop = readJsonVersion(path7.join(APP_ROOT, "package.json")) || webauto;
|
|
1899
|
+
const windowTitle = `WebAuto Desktop v${webauto}`;
|
|
1900
|
+
const badge = desktop === webauto ? `v${webauto}` : `webauto v${webauto} \xB7 console v${desktop}`;
|
|
1901
|
+
return { webauto, desktop, windowTitle, badge };
|
|
1902
|
+
}
|
|
1903
|
+
var VERSION_INFO = resolveVersionInfo();
|
|
1904
|
+
var DESKTOP_HEARTBEAT_FILE = path7.join(
|
|
1460
1905
|
os5.homedir(),
|
|
1461
1906
|
".webauto",
|
|
1462
1907
|
"run",
|
|
1463
1908
|
"desktop-console-heartbeat.json"
|
|
1464
1909
|
);
|
|
1465
1910
|
var profileStore = createProfileStore({ repoRoot: REPO_ROOT2 });
|
|
1466
|
-
var XHS_SCRIPTS_ROOT =
|
|
1911
|
+
var XHS_SCRIPTS_ROOT = path7.join(REPO_ROOT2, "scripts", "xiaohongshu");
|
|
1467
1912
|
var XHS_FULL_COLLECT_RE = /collect-content\.mjs$/;
|
|
1468
1913
|
function configureElectronPaths() {
|
|
1469
1914
|
try {
|
|
1470
1915
|
const downloadRoot = resolveDefaultDownloadRoot();
|
|
1471
|
-
const normalized =
|
|
1472
|
-
const baseDir =
|
|
1473
|
-
const userDataRoot =
|
|
1474
|
-
const cacheRoot =
|
|
1475
|
-
const gpuCacheRoot =
|
|
1916
|
+
const normalized = path7.normalize(downloadRoot);
|
|
1917
|
+
const baseDir = path7.basename(normalized).toLowerCase() === "download" ? path7.dirname(normalized) : normalized;
|
|
1918
|
+
const userDataRoot = path7.join(baseDir, "desktop-console");
|
|
1919
|
+
const cacheRoot = path7.join(userDataRoot, "cache");
|
|
1920
|
+
const gpuCacheRoot = path7.join(cacheRoot, "gpu");
|
|
1476
1921
|
try {
|
|
1477
1922
|
mkdirSync(cacheRoot, { recursive: true });
|
|
1478
1923
|
} catch {
|
|
@@ -1485,6 +1930,17 @@ function configureElectronPaths() {
|
|
|
1485
1930
|
app.setPath("cache", cacheRoot);
|
|
1486
1931
|
app.commandLine.appendSwitch("disk-cache-dir", cacheRoot);
|
|
1487
1932
|
app.commandLine.appendSwitch("gpu-cache-dir", gpuCacheRoot);
|
|
1933
|
+
const disableGpuByDefault = process.platform === "win32" && String(process.env.WEBAUTO_ELECTRON_DISABLE_GPU || "1").trim() !== "0";
|
|
1934
|
+
if (disableGpuByDefault) {
|
|
1935
|
+
try {
|
|
1936
|
+
app.disableHardwareAcceleration();
|
|
1937
|
+
} catch {
|
|
1938
|
+
}
|
|
1939
|
+
app.commandLine.appendSwitch("disable-gpu");
|
|
1940
|
+
app.commandLine.appendSwitch("disable-gpu-compositing");
|
|
1941
|
+
app.commandLine.appendSwitch("disable-direct-composition");
|
|
1942
|
+
app.commandLine.appendSwitch("use-angle", "swiftshader");
|
|
1943
|
+
}
|
|
1488
1944
|
} catch (err) {
|
|
1489
1945
|
console.warn("[desktop-console] failed to configure cache paths", err);
|
|
1490
1946
|
}
|
|
@@ -1514,8 +1970,35 @@ var GroupQueue = class {
|
|
|
1514
1970
|
};
|
|
1515
1971
|
var groupQueues = /* @__PURE__ */ new Map();
|
|
1516
1972
|
var runs = /* @__PURE__ */ new Map();
|
|
1973
|
+
var runLifecycle = /* @__PURE__ */ new Map();
|
|
1517
1974
|
var trackedRunPids = /* @__PURE__ */ new Set();
|
|
1518
1975
|
var appExitCleanupPromise = null;
|
|
1976
|
+
function setRunLifecycle(runId, patch) {
|
|
1977
|
+
const rid = String(runId || "").trim();
|
|
1978
|
+
if (!rid) return;
|
|
1979
|
+
const current = runLifecycle.get(rid) || {
|
|
1980
|
+
runId: rid,
|
|
1981
|
+
state: "queued",
|
|
1982
|
+
title: "",
|
|
1983
|
+
queuedAt: now()
|
|
1984
|
+
};
|
|
1985
|
+
const next = {
|
|
1986
|
+
...current,
|
|
1987
|
+
...patch,
|
|
1988
|
+
runId: rid
|
|
1989
|
+
};
|
|
1990
|
+
runLifecycle.set(rid, next);
|
|
1991
|
+
if (runLifecycle.size > 400) {
|
|
1992
|
+
const rows = Array.from(runLifecycle.values()).sort((a, b) => (a.queuedAt || 0) - (b.queuedAt || 0));
|
|
1993
|
+
const toDrop = rows.slice(0, Math.max(0, rows.length - 200));
|
|
1994
|
+
for (const row of toDrop) runLifecycle.delete(row.runId);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
function getRunLifecycle(runId) {
|
|
1998
|
+
const rid = String(runId || "").trim();
|
|
1999
|
+
if (!rid) return null;
|
|
2000
|
+
return runLifecycle.get(rid) || null;
|
|
2001
|
+
}
|
|
1519
2002
|
var UI_HEARTBEAT_TIMEOUT_MS = resolveUiHeartbeatTimeoutMs(process.env);
|
|
1520
2003
|
var lastUiHeartbeatAt = Date.now();
|
|
1521
2004
|
var heartbeatWatchdog = null;
|
|
@@ -1523,6 +2006,16 @@ var heartbeatTimeoutHandled = false;
|
|
|
1523
2006
|
var coreServicesStopRequested = false;
|
|
1524
2007
|
var coreServiceHeartbeatTimer = null;
|
|
1525
2008
|
var coreServiceHeartbeatStopped = false;
|
|
2009
|
+
var RUN_LOG_DIR = path7.join(os5.homedir(), ".webauto", "logs");
|
|
2010
|
+
function appendRunLog(runId, line) {
|
|
2011
|
+
const rid = String(runId || "").trim();
|
|
2012
|
+
const text = String(line || "").replace(/\r?\n/g, " ").trim();
|
|
2013
|
+
if (!rid || !text) return;
|
|
2014
|
+
const logPath = path7.join(RUN_LOG_DIR, `ui-run-${rid}.log`);
|
|
2015
|
+
void fs4.mkdir(RUN_LOG_DIR, { recursive: true }).then(() => fs4.appendFile(logPath, `${text}
|
|
2016
|
+
`, "utf8")).catch(() => {
|
|
2017
|
+
});
|
|
2018
|
+
}
|
|
1526
2019
|
async function writeCoreServiceHeartbeat(status) {
|
|
1527
2020
|
const filePath = String(process.env.WEBAUTO_HEARTBEAT_FILE || DESKTOP_HEARTBEAT_FILE).trim() || DESKTOP_HEARTBEAT_FILE;
|
|
1528
2021
|
const payload = {
|
|
@@ -1532,7 +2025,7 @@ async function writeCoreServiceHeartbeat(status) {
|
|
|
1532
2025
|
source: "desktop-console"
|
|
1533
2026
|
};
|
|
1534
2027
|
try {
|
|
1535
|
-
await fs4.mkdir(
|
|
2028
|
+
await fs4.mkdir(path7.dirname(filePath), { recursive: true });
|
|
1536
2029
|
await fs4.writeFile(filePath, JSON.stringify(payload), "utf8");
|
|
1537
2030
|
} catch {
|
|
1538
2031
|
}
|
|
@@ -1682,7 +2175,7 @@ async function cleanupTrackedRunPidsBestEffort(reason) {
|
|
|
1682
2175
|
}
|
|
1683
2176
|
}
|
|
1684
2177
|
async function cleanupCamoSessionsBestEffort(reason, includeLocks) {
|
|
1685
|
-
const camoCli =
|
|
2178
|
+
const camoCli = path7.join(REPO_ROOT2, "bin", "camoufox-cli.mjs");
|
|
1686
2179
|
const invoke = async (args, timeoutMs = 6e4) => runJson({
|
|
1687
2180
|
title: `camo ${args.join(" ")}`,
|
|
1688
2181
|
cwd: REPO_ROOT2,
|
|
@@ -1703,6 +2196,7 @@ async function cleanupRuntimeEnvironment(reason, options = {}) {
|
|
|
1703
2196
|
killAllRuns(reason);
|
|
1704
2197
|
await cleanupTrackedRunPidsBestEffort(reason);
|
|
1705
2198
|
await cleanupCamoSessionsBestEffort(reason, options.includeLockCleanup !== false);
|
|
2199
|
+
cleanupAllBrowserProcesses(reason);
|
|
1706
2200
|
if (options.stopUiBridge) {
|
|
1707
2201
|
await uiCliBridge.stop().catch(() => null);
|
|
1708
2202
|
}
|
|
@@ -1775,12 +2269,13 @@ function getQueue(groupKey) {
|
|
|
1775
2269
|
function generateRunId() {
|
|
1776
2270
|
return `run_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`;
|
|
1777
2271
|
}
|
|
1778
|
-
function createLineEmitter(runId, type) {
|
|
2272
|
+
function createLineEmitter(runId, type, onLine) {
|
|
1779
2273
|
let pending = "";
|
|
1780
2274
|
const emit = (line) => {
|
|
1781
2275
|
const normalized = String(line || "").replace(/\r$/, "");
|
|
1782
2276
|
if (!normalized) return;
|
|
1783
2277
|
sendEvent({ type, runId, line: normalized, ts: now() });
|
|
2278
|
+
if (typeof onLine === "function") onLine(normalized);
|
|
1784
2279
|
};
|
|
1785
2280
|
return {
|
|
1786
2281
|
push(chunk) {
|
|
@@ -1804,19 +2299,44 @@ function resolveNodeBin2() {
|
|
|
1804
2299
|
const explicit = String(process.env.WEBAUTO_NODE_BIN || "").trim();
|
|
1805
2300
|
if (explicit) return explicit;
|
|
1806
2301
|
const npmNode = String(process.env.npm_node_execpath || "").trim();
|
|
1807
|
-
if (npmNode)
|
|
2302
|
+
if (npmNode) {
|
|
2303
|
+
const base = path7.basename(npmNode).toLowerCase();
|
|
2304
|
+
const isNode = base === "node" || base === "node.exe";
|
|
2305
|
+
if (isNode) return npmNode;
|
|
2306
|
+
}
|
|
1808
2307
|
return process.platform === "win32" ? "node.exe" : "node";
|
|
1809
2308
|
}
|
|
1810
2309
|
function resolveCwd(input) {
|
|
1811
2310
|
const raw = String(input || "").trim();
|
|
1812
2311
|
if (!raw) return REPO_ROOT2;
|
|
1813
|
-
return
|
|
2312
|
+
return path7.isAbsolute(raw) ? raw : path7.resolve(REPO_ROOT2, raw);
|
|
2313
|
+
}
|
|
2314
|
+
function isPidAlive(pid) {
|
|
2315
|
+
const target = Number(pid || 0);
|
|
2316
|
+
if (!Number.isFinite(target) || target <= 0) return false;
|
|
2317
|
+
if (process.platform === "win32") {
|
|
2318
|
+
const ret = spawnSync2("tasklist", ["/FI", `PID eq ${target}`], {
|
|
2319
|
+
windowsHide: true,
|
|
2320
|
+
encoding: "utf8",
|
|
2321
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
2322
|
+
});
|
|
2323
|
+
const out = String(ret.stdout || "");
|
|
2324
|
+
if (!out) return false;
|
|
2325
|
+
const lines = out.split(/\r?\n/g).map((line) => line.trim()).filter(Boolean);
|
|
2326
|
+
return lines.some((line) => line.includes(` ${target}`));
|
|
2327
|
+
}
|
|
2328
|
+
try {
|
|
2329
|
+
process.kill(target, 0);
|
|
2330
|
+
return true;
|
|
2331
|
+
} catch {
|
|
2332
|
+
return false;
|
|
2333
|
+
}
|
|
1814
2334
|
}
|
|
1815
2335
|
var cachedStateMod = null;
|
|
1816
2336
|
async function getStateModule() {
|
|
1817
2337
|
if (cachedStateMod) return cachedStateMod;
|
|
1818
2338
|
try {
|
|
1819
|
-
const p =
|
|
2339
|
+
const p = path7.join(REPO_ROOT2, "dist", "modules", "state", "src", "xiaohongshu-collect-state.js");
|
|
1820
2340
|
cachedStateMod = await import(pathToFileURL3(p).href);
|
|
1821
2341
|
return cachedStateMod;
|
|
1822
2342
|
} catch {
|
|
@@ -1830,6 +2350,12 @@ async function spawnCommand(spec) {
|
|
|
1830
2350
|
const q = getQueue(groupKey);
|
|
1831
2351
|
const cwd = resolveCwd(spec.cwd);
|
|
1832
2352
|
const args = Array.isArray(spec.args) ? spec.args : [];
|
|
2353
|
+
appendRunLog(runId, `[queued] title=${String(spec.title || "").trim() || "-"} cwd=${cwd}`);
|
|
2354
|
+
setRunLifecycle(runId, {
|
|
2355
|
+
state: "queued",
|
|
2356
|
+
title: String(spec.title || ""),
|
|
2357
|
+
queuedAt: now()
|
|
2358
|
+
});
|
|
1833
2359
|
const isXhsRunCommand = args.some((item) => /xhs-(orchestrate|unified)\.mjs$/i.test(String(item || "").replace(/\\/g, "/")));
|
|
1834
2360
|
const extractProfilesFromArgs = (argv) => {
|
|
1835
2361
|
const out = [];
|
|
@@ -1862,49 +2388,133 @@ async function spawnCommand(spec) {
|
|
|
1862
2388
|
let finished = false;
|
|
1863
2389
|
let exitCode = null;
|
|
1864
2390
|
let exitSignal = null;
|
|
2391
|
+
let orphanCheckTimer = null;
|
|
1865
2392
|
const finalize = (code, signal) => {
|
|
1866
2393
|
if (finished) return;
|
|
1867
2394
|
finished = true;
|
|
2395
|
+
if (orphanCheckTimer) {
|
|
2396
|
+
clearInterval(orphanCheckTimer);
|
|
2397
|
+
orphanCheckTimer = null;
|
|
2398
|
+
}
|
|
2399
|
+
setRunLifecycle(runId, {
|
|
2400
|
+
state: "exited",
|
|
2401
|
+
exitedAt: now(),
|
|
2402
|
+
exitCode: code,
|
|
2403
|
+
signal
|
|
2404
|
+
});
|
|
1868
2405
|
sendEvent({ type: "exit", runId, exitCode: code, signal, ts: now() });
|
|
2406
|
+
appendRunLog(runId, `[exit] code=${code ?? "null"} signal=${signal ?? "null"}`);
|
|
1869
2407
|
runs.delete(runId);
|
|
1870
2408
|
resolve();
|
|
1871
2409
|
};
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
2410
|
+
let child;
|
|
2411
|
+
try {
|
|
2412
|
+
const nodeBin = resolveNodeBin2();
|
|
2413
|
+
appendRunLog(runId, `[cmd] ${nodeBin}`);
|
|
2414
|
+
child = spawn2(nodeBin, args, {
|
|
2415
|
+
cwd,
|
|
2416
|
+
env: {
|
|
2417
|
+
...process.env,
|
|
2418
|
+
...spec.env || {}
|
|
2419
|
+
},
|
|
2420
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2421
|
+
windowsHide: true
|
|
2422
|
+
});
|
|
2423
|
+
} catch (err) {
|
|
2424
|
+
const message = err?.message || String(err);
|
|
2425
|
+
setRunLifecycle(runId, {
|
|
2426
|
+
state: "exited",
|
|
2427
|
+
exitedAt: now(),
|
|
2428
|
+
exitCode: null,
|
|
2429
|
+
signal: "spawn_exception",
|
|
2430
|
+
lastError: message
|
|
2431
|
+
});
|
|
2432
|
+
sendEvent({ type: "stderr", runId, line: `[spawn-throw] ${message}`, ts: now() });
|
|
2433
|
+
appendRunLog(runId, `[spawn-throw] ${message}`);
|
|
2434
|
+
finalize(null, "spawn_exception");
|
|
2435
|
+
return;
|
|
2436
|
+
}
|
|
2437
|
+
const stdoutLines = createLineEmitter(runId, "stdout", (line) => appendRunLog(runId, `[stdout] ${line}`));
|
|
2438
|
+
const stderrLines = createLineEmitter(runId, "stderr", (line) => appendRunLog(runId, `[stderr] ${line}`));
|
|
2439
|
+
try {
|
|
2440
|
+
child.stdout?.on("data", (chunk) => {
|
|
2441
|
+
stdoutLines.push(chunk);
|
|
2442
|
+
});
|
|
2443
|
+
child.stderr?.on("data", (chunk) => {
|
|
2444
|
+
const text = chunk.toString("utf8");
|
|
2445
|
+
const lines = text.split(/\r?\n/g).map((line) => line.trim()).filter(Boolean);
|
|
2446
|
+
if (lines.length > 0) {
|
|
2447
|
+
setRunLifecycle(runId, { lastError: lines[lines.length - 1] });
|
|
2448
|
+
}
|
|
2449
|
+
stderrLines.push(chunk);
|
|
2450
|
+
});
|
|
2451
|
+
child.on("error", (err) => {
|
|
2452
|
+
const message = err?.message || String(err);
|
|
2453
|
+
setRunLifecycle(runId, { lastError: message });
|
|
2454
|
+
sendEvent({ type: "stderr", runId, line: `[spawn-error] ${message}`, ts: now() });
|
|
2455
|
+
appendRunLog(runId, `[spawn-error] ${message}`);
|
|
2456
|
+
finalize(null, "error");
|
|
2457
|
+
});
|
|
2458
|
+
child.on("exit", (code, signal) => {
|
|
2459
|
+
exitCode = code;
|
|
2460
|
+
exitSignal = signal;
|
|
2461
|
+
const timer = setTimeout(() => {
|
|
2462
|
+
if (finished) return;
|
|
2463
|
+
untrackRunPid(child);
|
|
2464
|
+
stdoutLines.flush();
|
|
2465
|
+
stderrLines.flush();
|
|
2466
|
+
finalize(exitCode ?? null, exitSignal ?? null);
|
|
2467
|
+
}, 200);
|
|
2468
|
+
if (timer && typeof timer.unref === "function") {
|
|
2469
|
+
timer.unref();
|
|
2470
|
+
}
|
|
2471
|
+
});
|
|
2472
|
+
child.on("close", (code, signal) => {
|
|
2473
|
+
untrackRunPid(child);
|
|
2474
|
+
stdoutLines.flush();
|
|
2475
|
+
stderrLines.flush();
|
|
2476
|
+
finalize(exitCode ?? code ?? null, exitSignal ?? signal ?? null);
|
|
2477
|
+
});
|
|
2478
|
+
} catch (err) {
|
|
2479
|
+
const message = err?.message || String(err);
|
|
2480
|
+
setRunLifecycle(runId, { lastError: message });
|
|
2481
|
+
sendEvent({ type: "stderr", runId, line: `[spawn-setup-error] ${message}`, ts: now() });
|
|
2482
|
+
appendRunLog(runId, `[spawn-setup-error] ${message}`);
|
|
2483
|
+
finalize(null, "setup_error");
|
|
2484
|
+
return;
|
|
2485
|
+
}
|
|
1883
2486
|
trackRunPid(child);
|
|
2487
|
+
if (child.pid) {
|
|
2488
|
+
trackBrowserProcess(child.pid);
|
|
2489
|
+
}
|
|
1884
2490
|
runs.set(runId, { child, title: spec.title, startedAt: now(), profiles: requestedProfiles });
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
});
|
|
1891
|
-
child.stderr?.on("data", (chunk) => {
|
|
1892
|
-
stderrLines.push(chunk);
|
|
1893
|
-
});
|
|
1894
|
-
child.on("error", (err) => {
|
|
1895
|
-
sendEvent({ type: "stderr", runId, line: `[spawn-error] ${err?.message || String(err)}`, ts: now() });
|
|
1896
|
-
finalize(null, "error");
|
|
1897
|
-
});
|
|
1898
|
-
child.on("exit", (code, signal) => {
|
|
1899
|
-
exitCode = code;
|
|
1900
|
-
exitSignal = signal;
|
|
1901
|
-
});
|
|
1902
|
-
child.on("close", (code, signal) => {
|
|
1903
|
-
untrackRunPid(child);
|
|
1904
|
-
stdoutLines.flush();
|
|
1905
|
-
stderrLines.flush();
|
|
1906
|
-
finalize(exitCode ?? code ?? null, exitSignal ?? signal ?? null);
|
|
2491
|
+
setRunLifecycle(runId, {
|
|
2492
|
+
state: "running",
|
|
2493
|
+
startedAt: now(),
|
|
2494
|
+
pid: child.pid || -1,
|
|
2495
|
+
title: String(spec.title || "")
|
|
1907
2496
|
});
|
|
2497
|
+
sendEvent({ type: "started", runId, title: spec.title, pid: child.pid ?? -1, ts: now() });
|
|
2498
|
+
appendRunLog(runId, `[started] pid=${child.pid ?? -1} title=${String(spec.title || "").trim() || "-"}`);
|
|
2499
|
+
if (args.length > 0) {
|
|
2500
|
+
appendRunLog(runId, `[args] ${args.join(" ")}`);
|
|
2501
|
+
}
|
|
2502
|
+
const pid = Number(child.pid || 0);
|
|
2503
|
+
if (pid > 0) {
|
|
2504
|
+
orphanCheckTimer = setInterval(() => {
|
|
2505
|
+
if (finished) return;
|
|
2506
|
+
if (!isPidAlive(pid)) {
|
|
2507
|
+
untrackRunPid(child);
|
|
2508
|
+
stdoutLines.flush();
|
|
2509
|
+
stderrLines.flush();
|
|
2510
|
+
appendRunLog(runId, "[watchdog] child pid disappeared before close/exit event");
|
|
2511
|
+
finalize(exitCode, exitSignal || "pid_gone");
|
|
2512
|
+
}
|
|
2513
|
+
}, 1e3);
|
|
2514
|
+
if (orphanCheckTimer && typeof orphanCheckTimer.unref === "function") {
|
|
2515
|
+
orphanCheckTimer.unref();
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
1908
2518
|
})
|
|
1909
2519
|
);
|
|
1910
2520
|
return { runId };
|
|
@@ -1944,9 +2554,106 @@ async function runJson(spec) {
|
|
|
1944
2554
|
return { ok: true, code, stdout: out, stderr: err };
|
|
1945
2555
|
}
|
|
1946
2556
|
}
|
|
2557
|
+
function sleep2(ms) {
|
|
2558
|
+
return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
|
|
2559
|
+
}
|
|
2560
|
+
function toEpochMs(value) {
|
|
2561
|
+
const raw = String(value ?? "").trim();
|
|
2562
|
+
if (!raw) return 0;
|
|
2563
|
+
const asNum = Number(raw);
|
|
2564
|
+
if (Number.isFinite(asNum) && asNum > 0) return asNum;
|
|
2565
|
+
const asDate = Date.parse(raw);
|
|
2566
|
+
return Number.isFinite(asDate) ? asDate : 0;
|
|
2567
|
+
}
|
|
2568
|
+
function resolveUnifiedApiBaseUrl() {
|
|
2569
|
+
return String(
|
|
2570
|
+
process.env.WEBAUTO_UNIFIED_API || process.env.WEBAUTO_UNIFIED_URL || "http://127.0.0.1:7701"
|
|
2571
|
+
).trim().replace(/\/+$/, "");
|
|
2572
|
+
}
|
|
2573
|
+
async function listUnifiedTasks() {
|
|
2574
|
+
const baseUrl = resolveUnifiedApiBaseUrl();
|
|
2575
|
+
const res = await fetch(`${baseUrl}/api/v1/tasks`, { signal: AbortSignal.timeout(3e3) });
|
|
2576
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
2577
|
+
const payload = await res.json().catch(() => ({}));
|
|
2578
|
+
return Array.isArray(payload?.data) ? payload.data : [];
|
|
2579
|
+
}
|
|
2580
|
+
function pickUnifiedRunId(tasks, options) {
|
|
2581
|
+
const profileId = String(options.profileId || "").trim();
|
|
2582
|
+
const keyword = String(options.keyword || "").trim();
|
|
2583
|
+
const uiTriggerId = String(options.uiTriggerId || "").trim();
|
|
2584
|
+
const minTs = Number(options.minTs || 0);
|
|
2585
|
+
const baselineRunIds = options.baselineRunIds instanceof Set ? options.baselineRunIds : /* @__PURE__ */ new Set();
|
|
2586
|
+
const rows = tasks.filter((task) => {
|
|
2587
|
+
const runId = String(task?.runId || task?.id || "").trim();
|
|
2588
|
+
if (!runId) return false;
|
|
2589
|
+
if (baselineRunIds.has(runId)) return false;
|
|
2590
|
+
const phase = String(task?.phase || task?.lastPhase || "").trim().toLowerCase();
|
|
2591
|
+
if (phase !== "unified") return false;
|
|
2592
|
+
const taskProfile = String(task?.profileId || "").trim();
|
|
2593
|
+
const taskKeyword = String(task?.keyword || "").trim();
|
|
2594
|
+
const taskTrigger = String(
|
|
2595
|
+
task?.uiTriggerId || task?.triggerId || task?.meta?.uiTriggerId || task?.context?.uiTriggerId || ""
|
|
2596
|
+
).trim();
|
|
2597
|
+
if (uiTriggerId && taskTrigger !== uiTriggerId) return false;
|
|
2598
|
+
if (profileId && taskProfile && taskProfile !== profileId) return false;
|
|
2599
|
+
if (keyword && taskKeyword && taskKeyword !== keyword) return false;
|
|
2600
|
+
const createdTs = toEpochMs(task?.createdAt);
|
|
2601
|
+
const startedTs = toEpochMs(task?.startedAt);
|
|
2602
|
+
const ts = createdTs || startedTs;
|
|
2603
|
+
return ts >= minTs;
|
|
2604
|
+
}).sort((a, b) => {
|
|
2605
|
+
const ta = toEpochMs(a?.createdAt) || toEpochMs(a?.startedAt);
|
|
2606
|
+
const tb = toEpochMs(b?.createdAt) || toEpochMs(b?.startedAt);
|
|
2607
|
+
return tb - ta;
|
|
2608
|
+
});
|
|
2609
|
+
const picked = rows[0] || null;
|
|
2610
|
+
return String(picked?.runId || picked?.id || "").trim();
|
|
2611
|
+
}
|
|
2612
|
+
async function waitForUnifiedRunRegistration(input) {
|
|
2613
|
+
const desktopRunId = String(input.desktopRunId || "").trim();
|
|
2614
|
+
const profileId = String(input.profileId || "").trim();
|
|
2615
|
+
const keyword = String(input.keyword || "").trim();
|
|
2616
|
+
const uiTriggerId = String(input.uiTriggerId || "").trim();
|
|
2617
|
+
const timeoutMs = Math.max(2e3, Number(input.timeoutMs || 2e4) || 2e4);
|
|
2618
|
+
const startedAt = Date.now();
|
|
2619
|
+
const minTs = startedAt - 3e4;
|
|
2620
|
+
const baselineRunIds = input.baselineRunIds instanceof Set ? input.baselineRunIds : /* @__PURE__ */ new Set();
|
|
2621
|
+
let lastFetchError = "";
|
|
2622
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
2623
|
+
try {
|
|
2624
|
+
const tasks = await listUnifiedTasks();
|
|
2625
|
+
const unifiedRunId = pickUnifiedRunId(tasks, {
|
|
2626
|
+
profileId,
|
|
2627
|
+
keyword,
|
|
2628
|
+
uiTriggerId,
|
|
2629
|
+
minTs,
|
|
2630
|
+
baselineRunIds
|
|
2631
|
+
});
|
|
2632
|
+
if (unifiedRunId) return { ok: true, runId: unifiedRunId };
|
|
2633
|
+
lastFetchError = "";
|
|
2634
|
+
} catch (err) {
|
|
2635
|
+
lastFetchError = err?.message || String(err);
|
|
2636
|
+
}
|
|
2637
|
+
const lifecycle2 = getRunLifecycle(desktopRunId);
|
|
2638
|
+
if (lifecycle2?.state === "exited") {
|
|
2639
|
+
const detail = lifecycle2.lastError || `exit=${lifecycle2.exitCode ?? "null"}`;
|
|
2640
|
+
return { ok: false, error: `\u4EFB\u52A1\u8FDB\u7A0B\u63D0\u524D\u9000\u51FA\uFF0C\u672A\u6CE8\u518C unified runId (${detail})` };
|
|
2641
|
+
}
|
|
2642
|
+
await sleep2(500);
|
|
2643
|
+
}
|
|
2644
|
+
const lifecycle = getRunLifecycle(desktopRunId);
|
|
2645
|
+
if (lifecycle?.state === "queued") {
|
|
2646
|
+
return { ok: false, error: "\u4EFB\u52A1\u4ECD\u5728\u6392\u961F\uFF0C\u672A\u8FDB\u5165\u8FD0\u884C\u6001\uFF08\u8BF7\u68C0\u67E5\u662F\u5426\u6709\u540C\u7C7B\u4EFB\u52A1\u5360\u7528\uFF09" };
|
|
2647
|
+
}
|
|
2648
|
+
if (lifecycle?.state === "running") {
|
|
2649
|
+
return { ok: false, error: "\u4EFB\u52A1\u8FDB\u7A0B\u5DF2\u542F\u52A8\uFF0C\u4F46\u5728\u8D85\u65F6\u5185\u672A\u6CE8\u518C unified runId" };
|
|
2650
|
+
}
|
|
2651
|
+
const suffix = lastFetchError ? `: ${lastFetchError}` : "";
|
|
2652
|
+
return { ok: false, error: `\u672A\u68C0\u6D4B\u5230 unified runId${suffix}` };
|
|
2653
|
+
}
|
|
1947
2654
|
async function scanResults(input) {
|
|
1948
2655
|
const downloadRoot = String(input.downloadRoot || resolveDefaultDownloadRoot());
|
|
1949
|
-
const root =
|
|
2656
|
+
const root = path7.join(downloadRoot, "xiaohongshu");
|
|
1950
2657
|
const result = { ok: true, root, entries: [] };
|
|
1951
2658
|
try {
|
|
1952
2659
|
const stateMod = await getStateModule();
|
|
@@ -1954,12 +2661,12 @@ async function scanResults(input) {
|
|
|
1954
2661
|
for (const envEnt of envDirs) {
|
|
1955
2662
|
if (!envEnt.isDirectory()) continue;
|
|
1956
2663
|
const env = envEnt.name;
|
|
1957
|
-
const envPath =
|
|
2664
|
+
const envPath = path7.join(root, env);
|
|
1958
2665
|
const keywordDirs = await fs4.readdir(envPath, { withFileTypes: true });
|
|
1959
2666
|
for (const kwEnt of keywordDirs) {
|
|
1960
2667
|
if (!kwEnt.isDirectory()) continue;
|
|
1961
2668
|
const keyword = kwEnt.name;
|
|
1962
|
-
const kwPath =
|
|
2669
|
+
const kwPath = path7.join(envPath, keyword);
|
|
1963
2670
|
const stat = await fs4.stat(kwPath).catch(() => null);
|
|
1964
2671
|
let stateSummary = null;
|
|
1965
2672
|
if (stateMod?.loadXhsCollectState) {
|
|
@@ -1994,7 +2701,7 @@ async function listXhsFullCollectScripts() {
|
|
|
1994
2701
|
return {
|
|
1995
2702
|
id: `xhs:${name}`,
|
|
1996
2703
|
label: `Full Collect (${name})`,
|
|
1997
|
-
path:
|
|
2704
|
+
path: path7.join(XHS_SCRIPTS_ROOT, name)
|
|
1998
2705
|
};
|
|
1999
2706
|
});
|
|
2000
2707
|
return { ok: true, scripts };
|
|
@@ -2064,11 +2771,11 @@ async function listDir(input) {
|
|
|
2064
2771
|
const items = await fs4.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
2065
2772
|
for (const ent of items) {
|
|
2066
2773
|
if (entries.length >= maxEntries) break;
|
|
2067
|
-
const full =
|
|
2774
|
+
const full = path7.join(dir, ent.name);
|
|
2068
2775
|
const st = await fs4.stat(full).catch(() => null);
|
|
2069
2776
|
entries.push({
|
|
2070
2777
|
path: full,
|
|
2071
|
-
rel:
|
|
2778
|
+
rel: path7.relative(root, full),
|
|
2072
2779
|
name: ent.name,
|
|
2073
2780
|
isDir: ent.isDirectory(),
|
|
2074
2781
|
size: st?.size || 0,
|
|
@@ -2085,13 +2792,13 @@ async function listDir(input) {
|
|
|
2085
2792
|
}
|
|
2086
2793
|
function createWindow() {
|
|
2087
2794
|
win = new BrowserWindow2({
|
|
2088
|
-
title:
|
|
2795
|
+
title: VERSION_INFO.windowTitle,
|
|
2089
2796
|
width: 1280,
|
|
2090
2797
|
height: 900,
|
|
2091
2798
|
minWidth: 920,
|
|
2092
2799
|
minHeight: 800,
|
|
2093
2800
|
webPreferences: {
|
|
2094
|
-
preload:
|
|
2801
|
+
preload: path7.join(APP_ROOT, "dist", "main", "preload.mjs"),
|
|
2095
2802
|
contextIsolation: true,
|
|
2096
2803
|
nodeIntegration: false,
|
|
2097
2804
|
sandbox: false,
|
|
@@ -2099,7 +2806,7 @@ function createWindow() {
|
|
|
2099
2806
|
backgroundThrottling: false
|
|
2100
2807
|
}
|
|
2101
2808
|
});
|
|
2102
|
-
const htmlPath =
|
|
2809
|
+
const htmlPath = path7.join(APP_ROOT, "dist", "renderer", "index.html");
|
|
2103
2810
|
void win.loadFile(htmlPath);
|
|
2104
2811
|
ensureStateBridge();
|
|
2105
2812
|
}
|
|
@@ -2131,6 +2838,7 @@ ipcMain2.on("preload:test", () => {
|
|
|
2131
2838
|
setTimeout(() => app.quit(), 200);
|
|
2132
2839
|
});
|
|
2133
2840
|
ipcMain2.handle("settings:get", async () => readDesktopConsoleSettings({ appRoot: APP_ROOT, repoRoot: REPO_ROOT2 }));
|
|
2841
|
+
ipcMain2.handle("app:getVersion", async () => VERSION_INFO);
|
|
2134
2842
|
ipcMain2.handle("settings:set", async (_evt, next) => {
|
|
2135
2843
|
const updated = await writeDesktopConsoleSettings({ appRoot: APP_ROOT, repoRoot: REPO_ROOT2 }, next || {});
|
|
2136
2844
|
const w = getWin();
|
|
@@ -2238,6 +2946,73 @@ ipcMain2.handle("cmd:runJson", async (_evt, spec) => {
|
|
|
2238
2946
|
const args = Array.isArray(spec?.args) ? spec.args : [];
|
|
2239
2947
|
return runJson({ ...spec, cwd, args });
|
|
2240
2948
|
});
|
|
2949
|
+
ipcMain2.handle("schedule:invoke", async (_evt, input) => scheduleInvoke(
|
|
2950
|
+
{
|
|
2951
|
+
repoRoot: REPO_ROOT2,
|
|
2952
|
+
runJson: (spec) => runJson(spec),
|
|
2953
|
+
spawnCommand: (spec) => spawnCommand(spec)
|
|
2954
|
+
},
|
|
2955
|
+
input || { action: "list" }
|
|
2956
|
+
));
|
|
2957
|
+
ipcMain2.handle("task:runEphemeral", async (_evt, input) => {
|
|
2958
|
+
const payload = input || {};
|
|
2959
|
+
let baselineRunIds = /* @__PURE__ */ new Set();
|
|
2960
|
+
try {
|
|
2961
|
+
const baselineTasks = await listUnifiedTasks();
|
|
2962
|
+
baselineRunIds = new Set(
|
|
2963
|
+
baselineTasks.map((task) => String(task?.runId || task?.id || "").trim()).filter(Boolean)
|
|
2964
|
+
);
|
|
2965
|
+
} catch {
|
|
2966
|
+
baselineRunIds = /* @__PURE__ */ new Set();
|
|
2967
|
+
}
|
|
2968
|
+
const result = await runEphemeralTask(
|
|
2969
|
+
{
|
|
2970
|
+
repoRoot: REPO_ROOT2,
|
|
2971
|
+
runJson: (spec) => runJson(spec),
|
|
2972
|
+
spawnCommand: (spec) => spawnCommand(spec)
|
|
2973
|
+
},
|
|
2974
|
+
payload
|
|
2975
|
+
);
|
|
2976
|
+
if (!result?.ok) return result;
|
|
2977
|
+
const commandType = String(result?.commandType || payload?.commandType || "").trim().toLowerCase();
|
|
2978
|
+
const desktopRunId = String(result?.runId || "").trim();
|
|
2979
|
+
if (commandType !== "xhs-unified" || !desktopRunId) {
|
|
2980
|
+
return result;
|
|
2981
|
+
}
|
|
2982
|
+
const profileId = String(result?.profile || payload?.argv?.profile || "").trim();
|
|
2983
|
+
const keyword = String(payload?.argv?.keyword || payload?.argv?.k || "").trim();
|
|
2984
|
+
const uiTriggerId = String(result?.uiTriggerId || payload?.argv?.["ui-trigger-id"] || "").trim();
|
|
2985
|
+
appendRunLog(
|
|
2986
|
+
desktopRunId,
|
|
2987
|
+
`[wait-register] profile=${profileId || "-"} keyword=${keyword || "-"} uiTriggerId=${uiTriggerId || "-"}`
|
|
2988
|
+
);
|
|
2989
|
+
const waited = await waitForUnifiedRunRegistration({
|
|
2990
|
+
desktopRunId,
|
|
2991
|
+
profileId,
|
|
2992
|
+
keyword,
|
|
2993
|
+
uiTriggerId,
|
|
2994
|
+
baselineRunIds,
|
|
2995
|
+
timeoutMs: 2e4
|
|
2996
|
+
});
|
|
2997
|
+
if (!waited.ok) {
|
|
2998
|
+
appendRunLog(desktopRunId, `[wait-register-failed] ${String(waited.error || "unknown_error")}`);
|
|
2999
|
+
return {
|
|
3000
|
+
ok: false,
|
|
3001
|
+
error: waited.error,
|
|
3002
|
+
runId: desktopRunId,
|
|
3003
|
+
commandType: result?.commandType || "xhs-unified",
|
|
3004
|
+
profile: profileId,
|
|
3005
|
+
uiTriggerId
|
|
3006
|
+
};
|
|
3007
|
+
}
|
|
3008
|
+
appendRunLog(desktopRunId, `[wait-register-ok] unifiedRunId=${waited.runId || "-"} uiTriggerId=${uiTriggerId || "-"}`);
|
|
3009
|
+
return {
|
|
3010
|
+
...result,
|
|
3011
|
+
runId: desktopRunId,
|
|
3012
|
+
unifiedRunId: waited.runId,
|
|
3013
|
+
uiTriggerId
|
|
3014
|
+
};
|
|
3015
|
+
});
|
|
2241
3016
|
ipcMain2.handle("results:scan", async (_evt, spec) => scanResults(spec || {}));
|
|
2242
3017
|
ipcMain2.handle("fs:listDir", async (_evt, spec) => listDir(spec));
|
|
2243
3018
|
ipcMain2.handle(
|
|
@@ -2277,6 +3052,49 @@ ipcMain2.handle("env:repairCore", async () => {
|
|
|
2277
3052
|
const services = await checkServices().catch(() => ({ unifiedApi: false, camoRuntime: false }));
|
|
2278
3053
|
return { ok, services };
|
|
2279
3054
|
});
|
|
3055
|
+
ipcMain2.handle("env:cleanup", async () => {
|
|
3056
|
+
markUiHeartbeat("env_cleanup");
|
|
3057
|
+
console.log("[env:cleanup] Starting environment cleanup...");
|
|
3058
|
+
await cleanupRuntimeEnvironment("env_cleanup_requested", {
|
|
3059
|
+
stopUiBridge: false,
|
|
3060
|
+
stopHeartbeat: false,
|
|
3061
|
+
stopCoreServices: false,
|
|
3062
|
+
stopStateBridge: false,
|
|
3063
|
+
includeLockCleanup: true
|
|
3064
|
+
});
|
|
3065
|
+
let locksCleared = 0;
|
|
3066
|
+
try {
|
|
3067
|
+
const profiles = await profileStore.listProfiles();
|
|
3068
|
+
for (const p of profiles) {
|
|
3069
|
+
const lockFile = path7.join(p.path, ".lock");
|
|
3070
|
+
try {
|
|
3071
|
+
await fs4.unlink(lockFile);
|
|
3072
|
+
locksCleared++;
|
|
3073
|
+
console.log(`[env:cleanup] Cleared lock for profile ${p.profileId}`);
|
|
3074
|
+
} catch (err) {
|
|
3075
|
+
if (err?.code !== "ENOENT") {
|
|
3076
|
+
console.warn(`[env:cleanup] Failed to clear lock for ${p.profileId}:`, err.message);
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
} catch (err) {
|
|
3081
|
+
console.warn("[env:cleanup] Failed to list profiles:", err);
|
|
3082
|
+
}
|
|
3083
|
+
await stopCoreDaemon().catch(() => null);
|
|
3084
|
+
const restarted = await startCoreDaemon().catch(() => false);
|
|
3085
|
+
const services = await checkServices().catch(() => ({ unifiedApi: false, camoRuntime: false }));
|
|
3086
|
+
const firefox = await checkFirefox().catch(() => ({ installed: false }));
|
|
3087
|
+
const camo = await checkCamoCli().catch(() => ({ installed: false }));
|
|
3088
|
+
console.log("[env:cleanup] Cleanup complete:", { locksCleared, restarted, services, firefox, camo });
|
|
3089
|
+
return {
|
|
3090
|
+
ok: true,
|
|
3091
|
+
locksCleared,
|
|
3092
|
+
coreRestarted: restarted,
|
|
3093
|
+
services,
|
|
3094
|
+
firefox,
|
|
3095
|
+
camo
|
|
3096
|
+
};
|
|
3097
|
+
});
|
|
2280
3098
|
ipcMain2.handle("env:repairDeps", async (_evt, input) => {
|
|
2281
3099
|
const wantCore = Boolean(input?.core);
|
|
2282
3100
|
const wantBrowser = Boolean(input?.browser);
|
|
@@ -2294,7 +3112,7 @@ ipcMain2.handle("env:repairDeps", async (_evt, input) => {
|
|
|
2294
3112
|
if (!coreOk) result.ok = false;
|
|
2295
3113
|
}
|
|
2296
3114
|
if (wantBrowser || wantGeoip) {
|
|
2297
|
-
const args = [
|
|
3115
|
+
const args = [path7.join("apps", "webauto", "entry", "xhs-install.mjs")];
|
|
2298
3116
|
if (wantReinstall) args.push("--reinstall");
|
|
2299
3117
|
else if (wantUninstall) args.push("--uninstall");
|
|
2300
3118
|
else args.push("--install");
|
|
@@ -2313,20 +3131,6 @@ ipcMain2.handle("env:repairDeps", async (_evt, input) => {
|
|
|
2313
3131
|
result.env = await checkEnvironment().catch(() => null);
|
|
2314
3132
|
return result;
|
|
2315
3133
|
});
|
|
2316
|
-
ipcMain2.handle("env:cleanup", async () => {
|
|
2317
|
-
markUiHeartbeat("env_cleanup");
|
|
2318
|
-
await cleanupRuntimeEnvironment("env_cleanup", {
|
|
2319
|
-
stopUiBridge: false,
|
|
2320
|
-
stopHeartbeat: false,
|
|
2321
|
-
stopCoreServices: false,
|
|
2322
|
-
stopStateBridge: false,
|
|
2323
|
-
includeLockCleanup: true
|
|
2324
|
-
});
|
|
2325
|
-
return {
|
|
2326
|
-
ok: true,
|
|
2327
|
-
services: await checkServices().catch(() => ({ unifiedApi: false, camoRuntime: false }))
|
|
2328
|
-
};
|
|
2329
|
-
});
|
|
2330
3134
|
ipcMain2.handle("config:saveLast", async (_evt, config) => {
|
|
2331
3135
|
await saveCrawlConfig({ appRoot: APP_ROOT, repoRoot: REPO_ROOT2 }, config);
|
|
2332
3136
|
return { ok: true };
|
|
@@ -2417,7 +3221,7 @@ ipcMain2.handle("runtime:kill", async (_evt, input) => {
|
|
|
2417
3221
|
ipcMain2.handle("runtime:restartPhase1", async (_evt, input) => {
|
|
2418
3222
|
const profileId = String(input?.profileId || "").trim();
|
|
2419
3223
|
if (!profileId) return { ok: false, error: "missing profileId" };
|
|
2420
|
-
const args = [
|
|
3224
|
+
const args = [path7.join(REPO_ROOT2, "scripts", "xiaohongshu", "phase1-boot.mjs"), "--profile", profileId, "--headless", "false"];
|
|
2421
3225
|
return spawnCommand({ title: `Phase1 restart ${profileId}`, cwd: REPO_ROOT2, args, groupKey: "phase1" });
|
|
2422
3226
|
});
|
|
2423
3227
|
ipcMain2.handle("runtime:setBrowserTitle", async (_evt, input) => {
|