@web-auto/webauto 0.1.7 → 0.1.9
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 +802 -90
- 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 +784 -332
- package/apps/desktop-console/entry/ui-cli.mjs +23 -8
- package/apps/desktop-console/entry/ui-console.mjs +8 -3
- package/apps/webauto/entry/account.mjs +69 -8
- package/apps/webauto/entry/lib/account-detect.mjs +106 -25
- package/apps/webauto/entry/lib/account-store.mjs +121 -22
- package/apps/webauto/entry/lib/schedule-store.mjs +0 -12
- 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 +220 -51
- package/apps/webauto/entry/xhs-unified.mjs +33 -6
- package/bin/webauto.mjs +80 -4
- 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 +6 -3
- 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,10 +1,10 @@
|
|
|
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
10
|
import { existsSync, promises as fs } from "node:fs";
|
|
@@ -273,7 +273,9 @@ import path2 from "path";
|
|
|
273
273
|
import { existsSync as existsSync2 } 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");
|
|
279
281
|
function sleep(ms) {
|
|
@@ -323,6 +325,9 @@ async function areCoreServicesHealthy() {
|
|
|
323
325
|
const health = await Promise.all(CORE_HEALTH_URLS.map((url) => checkHttpHealth(url)));
|
|
324
326
|
return health.every(Boolean);
|
|
325
327
|
}
|
|
328
|
+
async function isUnifiedApiHealthy() {
|
|
329
|
+
return checkHttpHealth(UNIFIED_API_HEALTH_URL);
|
|
330
|
+
}
|
|
326
331
|
async function runNodeScript(scriptPath, timeoutMs) {
|
|
327
332
|
return new Promise((resolve) => {
|
|
328
333
|
const nodeBin = resolveNodeBin();
|
|
@@ -405,14 +410,18 @@ async function startCoreDaemon() {
|
|
|
405
410
|
4e4
|
|
406
411
|
);
|
|
407
412
|
if (!startedBrowser) {
|
|
408
|
-
console.
|
|
409
|
-
return false;
|
|
413
|
+
console.warn("[CoreDaemonManager] Failed to start camo browser backend, continue in degraded mode");
|
|
410
414
|
}
|
|
411
415
|
for (let i = 0; i < 20; i += 1) {
|
|
412
|
-
|
|
416
|
+
const [allHealthy, unifiedHealthy] = await Promise.all([
|
|
417
|
+
areCoreServicesHealthy(),
|
|
418
|
+
isUnifiedApiHealthy()
|
|
419
|
+
]);
|
|
420
|
+
if (allHealthy) return true;
|
|
421
|
+
if (unifiedHealthy) return true;
|
|
413
422
|
await sleep(300);
|
|
414
423
|
}
|
|
415
|
-
console.error("[CoreDaemonManager]
|
|
424
|
+
console.error("[CoreDaemonManager] Unified API still unhealthy after start");
|
|
416
425
|
return false;
|
|
417
426
|
}
|
|
418
427
|
async function stopCoreDaemon() {
|
|
@@ -817,6 +826,13 @@ function resolvePathFromOutput(stdout) {
|
|
|
817
826
|
}
|
|
818
827
|
return "";
|
|
819
828
|
}
|
|
829
|
+
function resolveCamoufoxExecutable(installRoot) {
|
|
830
|
+
return process.platform === "win32" ? path4.join(installRoot, "camoufox.exe") : path4.join(installRoot, "camoufox");
|
|
831
|
+
}
|
|
832
|
+
function isValidCamoufoxInstallRoot(installRoot) {
|
|
833
|
+
if (!installRoot || !existsSync3(installRoot)) return false;
|
|
834
|
+
return existsSync3(resolveCamoufoxExecutable(installRoot));
|
|
835
|
+
}
|
|
820
836
|
async function checkCamoCli() {
|
|
821
837
|
const camoCandidates = process.platform === "win32" ? ["camo.cmd", "camo.exe", "camo.bat", "camo.ps1"] : ["camo"];
|
|
822
838
|
for (const candidate of camoCandidates) {
|
|
@@ -858,10 +874,12 @@ async function checkServices() {
|
|
|
858
874
|
}
|
|
859
875
|
async function checkFirefox() {
|
|
860
876
|
const candidates = process.platform === "win32" ? [
|
|
877
|
+
{ command: "camoufox", args: ["path"] },
|
|
861
878
|
{ command: "python", args: ["-m", "camoufox", "path"] },
|
|
862
879
|
{ command: "py", args: ["-3", "-m", "camoufox", "path"] },
|
|
863
880
|
{ command: resolveNpxBin2(), args: ["--yes", "--package=camoufox", "camoufox", "path"] }
|
|
864
881
|
] : [
|
|
882
|
+
{ command: "camoufox", args: ["path"] },
|
|
865
883
|
{ command: "python3", args: ["-m", "camoufox", "path"] },
|
|
866
884
|
{ command: resolveNpxBin2(), args: ["--yes", "--package=camoufox", "camoufox", "path"] }
|
|
867
885
|
];
|
|
@@ -874,7 +892,10 @@ async function checkFirefox() {
|
|
|
874
892
|
});
|
|
875
893
|
if (ret.status !== 0) continue;
|
|
876
894
|
const resolvedPath = resolvePathFromOutput(String(ret.stdout || ""));
|
|
877
|
-
|
|
895
|
+
if (resolvedPath && isValidCamoufoxInstallRoot(resolvedPath)) {
|
|
896
|
+
return { installed: true, path: resolvedPath };
|
|
897
|
+
}
|
|
898
|
+
if (!resolvedPath) return { installed: true };
|
|
878
899
|
} catch {
|
|
879
900
|
}
|
|
880
901
|
}
|
|
@@ -894,8 +915,16 @@ async function checkEnvironment() {
|
|
|
894
915
|
checkFirefox(),
|
|
895
916
|
checkGeoIP()
|
|
896
917
|
]);
|
|
897
|
-
const
|
|
898
|
-
|
|
918
|
+
const browserReady = Boolean(firefox.installed || services.camoRuntime);
|
|
919
|
+
const missing = {
|
|
920
|
+
core: !services.unifiedApi,
|
|
921
|
+
runtimeService: !services.camoRuntime,
|
|
922
|
+
camo: !camo.installed,
|
|
923
|
+
runtime: !browserReady,
|
|
924
|
+
geoip: !geoip.installed
|
|
925
|
+
};
|
|
926
|
+
const allReady = camo.installed && services.unifiedApi && browserReady;
|
|
927
|
+
return { camo, services, firefox, geoip, browserReady, missing, allReady };
|
|
899
928
|
}
|
|
900
929
|
|
|
901
930
|
// src/main/ui-cli-bridge.mts
|
|
@@ -985,6 +1014,20 @@ function buildSnapshotScript() {
|
|
|
985
1014
|
if ('value' in el) return String(el.value ?? '');
|
|
986
1015
|
return String(el.textContent || '').trim();
|
|
987
1016
|
};
|
|
1017
|
+
const firstText = (selectors) => {
|
|
1018
|
+
for (const sel of selectors) {
|
|
1019
|
+
const v = text(sel);
|
|
1020
|
+
if (v) return v;
|
|
1021
|
+
}
|
|
1022
|
+
return '';
|
|
1023
|
+
};
|
|
1024
|
+
const firstValue = (selectors) => {
|
|
1025
|
+
for (const sel of selectors) {
|
|
1026
|
+
const v = value(sel);
|
|
1027
|
+
if (v) return v;
|
|
1028
|
+
}
|
|
1029
|
+
return '';
|
|
1030
|
+
};
|
|
988
1031
|
const activeTab = document.querySelector('.tab.active');
|
|
989
1032
|
const errors = Array.from(document.querySelectorAll('#recent-errors-list li'))
|
|
990
1033
|
.map((el) => String(el.textContent || '').trim())
|
|
@@ -1000,10 +1043,10 @@ function buildSnapshotScript() {
|
|
|
1000
1043
|
currentPhase: text('#current-phase'),
|
|
1001
1044
|
currentAction: text('#current-action'),
|
|
1002
1045
|
progressPercent: text('#progress-percent'),
|
|
1003
|
-
keyword:
|
|
1004
|
-
target:
|
|
1005
|
-
account:
|
|
1006
|
-
env:
|
|
1046
|
+
keyword: firstValue(['#task-keyword', '#keyword-input', '#task-keyword-input']),
|
|
1047
|
+
target: firstValue(['#task-target', '#target-input', '#task-target-input']),
|
|
1048
|
+
account: firstValue(['#task-account', '#task-profile', '#account-select']),
|
|
1049
|
+
env: firstValue(['#task-env', '#env-select']),
|
|
1007
1050
|
recentErrors: errors,
|
|
1008
1051
|
ts: new Date().toISOString(),
|
|
1009
1052
|
};
|
|
@@ -1451,28 +1494,337 @@ var UiCliBridge = class {
|
|
|
1451
1494
|
}
|
|
1452
1495
|
};
|
|
1453
1496
|
|
|
1497
|
+
// src/main/task-gateway.mts
|
|
1498
|
+
import path6 from "node:path";
|
|
1499
|
+
var KEYWORD_REQUIRED_TYPES = /* @__PURE__ */ new Set(["xhs-unified", "weibo-search", "1688-search"]);
|
|
1500
|
+
var RUN_AT_TYPES = /* @__PURE__ */ new Set(["once", "daily", "weekly"]);
|
|
1501
|
+
function asText(value) {
|
|
1502
|
+
return String(value ?? "").trim();
|
|
1503
|
+
}
|
|
1504
|
+
function asPositiveInt(value, fallback) {
|
|
1505
|
+
const n = Number(value);
|
|
1506
|
+
if (!Number.isFinite(n) || n <= 0) return fallback;
|
|
1507
|
+
return Math.max(1, Math.floor(n));
|
|
1508
|
+
}
|
|
1509
|
+
function asBool(value, fallback) {
|
|
1510
|
+
if (typeof value === "boolean") return value;
|
|
1511
|
+
const text = asText(value).toLowerCase();
|
|
1512
|
+
if (!text) return fallback;
|
|
1513
|
+
if (["1", "true", "yes", "y", "on"].includes(text)) return true;
|
|
1514
|
+
if (["0", "false", "no", "n", "off"].includes(text)) return false;
|
|
1515
|
+
return fallback;
|
|
1516
|
+
}
|
|
1517
|
+
function asOptionalRunAt(value) {
|
|
1518
|
+
const runAt = asText(value);
|
|
1519
|
+
return runAt ? runAt : null;
|
|
1520
|
+
}
|
|
1521
|
+
function normalizeScheduleType(value) {
|
|
1522
|
+
const raw = asText(value);
|
|
1523
|
+
if (raw === "once" || raw === "daily" || raw === "weekly") return raw;
|
|
1524
|
+
return "interval";
|
|
1525
|
+
}
|
|
1526
|
+
function deriveTaskName(commandType, argv) {
|
|
1527
|
+
const keyword = asText(argv.keyword);
|
|
1528
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
1529
|
+
return keyword ? `${commandType}-${keyword}` : `${commandType}-${stamp}`;
|
|
1530
|
+
}
|
|
1531
|
+
function getPlatformFromCommandType(commandType) {
|
|
1532
|
+
const value = asText(commandType).toLowerCase();
|
|
1533
|
+
if (value.startsWith("weibo")) return "weibo";
|
|
1534
|
+
if (value.startsWith("1688")) return "1688";
|
|
1535
|
+
return "xiaohongshu";
|
|
1536
|
+
}
|
|
1537
|
+
function hasExplicitProfileArg(argv) {
|
|
1538
|
+
return Boolean(asText(argv?.profile) || asText(argv?.profiles) || asText(argv?.profilepool));
|
|
1539
|
+
}
|
|
1540
|
+
async function pickDefaultProfileForPlatform(options, platform) {
|
|
1541
|
+
const accountScript = path6.join(options.repoRoot, "apps", "webauto", "entry", "account.mjs");
|
|
1542
|
+
const ret = await options.runJson({
|
|
1543
|
+
title: `account list --platform ${platform}`,
|
|
1544
|
+
cwd: options.repoRoot,
|
|
1545
|
+
args: [accountScript, "list", "--platform", platform, "--json"],
|
|
1546
|
+
timeoutMs: 2e4
|
|
1547
|
+
});
|
|
1548
|
+
if (!ret?.ok) return "";
|
|
1549
|
+
const rows = Array.isArray(ret?.json?.profiles) ? ret.json.profiles : [];
|
|
1550
|
+
const validRows = rows.filter((row) => row?.valid === true && asText(row?.accountId)).sort((a, b) => {
|
|
1551
|
+
const ta = Date.parse(asText(a?.updatedAt) || "") || 0;
|
|
1552
|
+
const tb = Date.parse(asText(b?.updatedAt) || "") || 0;
|
|
1553
|
+
if (tb !== ta) return tb - ta;
|
|
1554
|
+
return asText(a?.profileId).localeCompare(asText(b?.profileId));
|
|
1555
|
+
});
|
|
1556
|
+
return asText(validRows[0]?.profileId) || "";
|
|
1557
|
+
}
|
|
1558
|
+
async function ensureProfileArg(options, commandType, argv) {
|
|
1559
|
+
if (hasExplicitProfileArg(argv)) return argv;
|
|
1560
|
+
const platform = getPlatformFromCommandType(commandType);
|
|
1561
|
+
const profileId = await pickDefaultProfileForPlatform(options, platform);
|
|
1562
|
+
if (!profileId) {
|
|
1563
|
+
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`);
|
|
1564
|
+
}
|
|
1565
|
+
return {
|
|
1566
|
+
...argv,
|
|
1567
|
+
profile: profileId
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
function normalizeWeiboTaskType(commandType) {
|
|
1571
|
+
if (commandType === "weibo-search") return "search";
|
|
1572
|
+
if (commandType === "weibo-monitor") return "monitor";
|
|
1573
|
+
return "timeline";
|
|
1574
|
+
}
|
|
1575
|
+
function createUiTriggerId() {
|
|
1576
|
+
return `ui-${Date.now()}-${Math.random().toString(16).slice(2, 10)}`;
|
|
1577
|
+
}
|
|
1578
|
+
function normalizeSavePayload(payload) {
|
|
1579
|
+
const commandType = asText(payload?.commandType) || "xhs-unified";
|
|
1580
|
+
const argv = payload?.argv && typeof payload.argv === "object" ? { ...payload.argv } : {};
|
|
1581
|
+
const scheduleType = normalizeScheduleType(payload?.scheduleType);
|
|
1582
|
+
const runAt = asOptionalRunAt(payload?.runAt);
|
|
1583
|
+
const intervalMinutes = asPositiveInt(payload?.intervalMinutes, 30);
|
|
1584
|
+
const maxRunsRaw = Number(payload?.maxRuns);
|
|
1585
|
+
const maxRuns = Number.isFinite(maxRunsRaw) && maxRunsRaw > 0 ? Math.max(1, Math.floor(maxRunsRaw)) : null;
|
|
1586
|
+
const name = asText(payload?.name) || deriveTaskName(commandType, argv);
|
|
1587
|
+
const enabled = asBool(payload?.enabled, true);
|
|
1588
|
+
const id = asText(payload?.id);
|
|
1589
|
+
if (KEYWORD_REQUIRED_TYPES.has(commandType) && !asText(argv.keyword)) {
|
|
1590
|
+
throw new Error("\u5173\u952E\u8BCD\u4E0D\u80FD\u4E3A\u7A7A");
|
|
1591
|
+
}
|
|
1592
|
+
if (commandType.startsWith("weibo-")) {
|
|
1593
|
+
argv["task-type"] = asText(argv["task-type"]) || normalizeWeiboTaskType(commandType);
|
|
1594
|
+
if (commandType === "weibo-monitor" && !asText(argv["user-id"])) {
|
|
1595
|
+
throw new Error("\u5FAE\u535A monitor \u4EFB\u52A1\u9700\u8981 user-id");
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
if (RUN_AT_TYPES.has(scheduleType) && !runAt) {
|
|
1599
|
+
throw new Error(`${scheduleType} \u4EFB\u52A1\u9700\u8981\u951A\u70B9\u65F6\u95F4`);
|
|
1600
|
+
}
|
|
1601
|
+
return {
|
|
1602
|
+
id,
|
|
1603
|
+
name,
|
|
1604
|
+
enabled,
|
|
1605
|
+
commandType,
|
|
1606
|
+
scheduleType,
|
|
1607
|
+
intervalMinutes,
|
|
1608
|
+
runAt,
|
|
1609
|
+
maxRuns,
|
|
1610
|
+
argv
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
async function runScheduleJson(options, args, timeoutMs) {
|
|
1614
|
+
const script = path6.join(options.repoRoot, "apps", "webauto", "entry", "schedule.mjs");
|
|
1615
|
+
const ret = await options.runJson({
|
|
1616
|
+
title: `schedule ${args.join(" ")}`.trim(),
|
|
1617
|
+
cwd: options.repoRoot,
|
|
1618
|
+
args: [script, ...args, "--json"],
|
|
1619
|
+
timeoutMs: typeof timeoutMs === "number" && timeoutMs > 0 ? timeoutMs : void 0
|
|
1620
|
+
});
|
|
1621
|
+
if (!ret?.ok) {
|
|
1622
|
+
const reason = asText(ret?.error) || asText(ret?.stderr) || asText(ret?.stdout) || "schedule command failed";
|
|
1623
|
+
return { ok: false, error: reason };
|
|
1624
|
+
}
|
|
1625
|
+
return { ok: true, json: ret?.json || {} };
|
|
1626
|
+
}
|
|
1627
|
+
async function scheduleInvoke(options, input) {
|
|
1628
|
+
try {
|
|
1629
|
+
const action = asText(input?.action);
|
|
1630
|
+
const timeoutMs = input?.timeoutMs;
|
|
1631
|
+
if (action === "list") return runScheduleJson(options, ["list"], timeoutMs);
|
|
1632
|
+
if (action === "get") {
|
|
1633
|
+
const taskId = asText(input?.taskId);
|
|
1634
|
+
if (!taskId) return { ok: false, error: "missing taskId" };
|
|
1635
|
+
return runScheduleJson(options, ["get", taskId], timeoutMs);
|
|
1636
|
+
}
|
|
1637
|
+
if (action === "save") {
|
|
1638
|
+
const payload = normalizeSavePayload(input?.payload);
|
|
1639
|
+
payload.argv = await ensureProfileArg(options, payload.commandType, payload.argv);
|
|
1640
|
+
const args = payload.id ? ["update", payload.id] : ["add"];
|
|
1641
|
+
args.push("--name", payload.name);
|
|
1642
|
+
args.push("--enabled", String(payload.enabled));
|
|
1643
|
+
args.push("--command-type", payload.commandType);
|
|
1644
|
+
args.push("--schedule-type", payload.scheduleType);
|
|
1645
|
+
if (RUN_AT_TYPES.has(payload.scheduleType)) {
|
|
1646
|
+
args.push("--run-at", String(payload.runAt || ""));
|
|
1647
|
+
} else {
|
|
1648
|
+
args.push("--interval-minutes", String(payload.intervalMinutes));
|
|
1649
|
+
}
|
|
1650
|
+
args.push("--max-runs", payload.maxRuns === null ? "0" : String(payload.maxRuns));
|
|
1651
|
+
args.push("--argv-json", JSON.stringify(payload.argv));
|
|
1652
|
+
return runScheduleJson(options, args, timeoutMs);
|
|
1653
|
+
}
|
|
1654
|
+
if (action === "run") {
|
|
1655
|
+
const taskId = asText(input?.taskId);
|
|
1656
|
+
if (!taskId) return { ok: false, error: "missing taskId" };
|
|
1657
|
+
return runScheduleJson(options, ["run", taskId], timeoutMs);
|
|
1658
|
+
}
|
|
1659
|
+
if (action === "delete") {
|
|
1660
|
+
const taskId = asText(input?.taskId);
|
|
1661
|
+
if (!taskId) return { ok: false, error: "missing taskId" };
|
|
1662
|
+
return runScheduleJson(options, ["delete", taskId], timeoutMs);
|
|
1663
|
+
}
|
|
1664
|
+
if (action === "export") {
|
|
1665
|
+
const taskId = asText(input?.taskId);
|
|
1666
|
+
return taskId ? runScheduleJson(options, ["export", taskId], timeoutMs) : runScheduleJson(options, ["export"], timeoutMs);
|
|
1667
|
+
}
|
|
1668
|
+
if (action === "import") {
|
|
1669
|
+
const payloadJson = asText(input?.payloadJson);
|
|
1670
|
+
if (!payloadJson) return { ok: false, error: "missing payloadJson" };
|
|
1671
|
+
const mode = asText(input?.mode) === "replace" ? "replace" : "merge";
|
|
1672
|
+
return runScheduleJson(options, ["import", "--payload-json", payloadJson, "--mode", mode], timeoutMs);
|
|
1673
|
+
}
|
|
1674
|
+
if (action === "run-due") {
|
|
1675
|
+
const limit = asPositiveInt(input?.limit, 20);
|
|
1676
|
+
return runScheduleJson(options, ["run-due", "--limit", String(limit)], timeoutMs);
|
|
1677
|
+
}
|
|
1678
|
+
if (action === "daemon-start") {
|
|
1679
|
+
const interval = asPositiveInt(input?.intervalSec, 30);
|
|
1680
|
+
const limit = asPositiveInt(input?.limit, 20);
|
|
1681
|
+
const script = path6.join(options.repoRoot, "apps", "webauto", "entry", "schedule.mjs");
|
|
1682
|
+
const ret = await options.spawnCommand({
|
|
1683
|
+
title: `schedule daemon ${interval}s`,
|
|
1684
|
+
cwd: options.repoRoot,
|
|
1685
|
+
args: [script, "daemon", "--interval-sec", String(Math.max(5, interval)), "--limit", String(limit), "--json"],
|
|
1686
|
+
groupKey: "scheduler"
|
|
1687
|
+
});
|
|
1688
|
+
return { ok: true, runId: asText(ret?.runId) };
|
|
1689
|
+
}
|
|
1690
|
+
return { ok: false, error: `unsupported action: ${action || "<empty>"}` };
|
|
1691
|
+
} catch (err) {
|
|
1692
|
+
return { ok: false, error: err?.message || String(err) };
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
async function runEphemeralTask(options, input) {
|
|
1696
|
+
try {
|
|
1697
|
+
const commandType = asText(input?.commandType) || "xhs-unified";
|
|
1698
|
+
let argv = input?.argv && typeof input.argv === "object" ? { ...input.argv } : {};
|
|
1699
|
+
argv = await ensureProfileArg(options, commandType, argv);
|
|
1700
|
+
const profile = asText(argv.profile);
|
|
1701
|
+
const keyword = asText(argv.keyword);
|
|
1702
|
+
const target = asPositiveInt(argv["max-notes"] ?? argv.target, 50);
|
|
1703
|
+
const env = asText(argv.env) || "debug";
|
|
1704
|
+
if (!profile) return { ok: false, error: "\u8BF7\u8F93\u5165 Profile ID" };
|
|
1705
|
+
if (KEYWORD_REQUIRED_TYPES.has(commandType) && !keyword) {
|
|
1706
|
+
return { ok: false, error: "\u8BF7\u8F93\u5165\u5173\u952E\u8BCD" };
|
|
1707
|
+
}
|
|
1708
|
+
if (commandType === "xhs-unified") {
|
|
1709
|
+
const uiTriggerId = createUiTriggerId();
|
|
1710
|
+
const script = path6.join(options.repoRoot, "apps", "webauto", "entry", "xhs-unified.mjs");
|
|
1711
|
+
const ret = await options.spawnCommand({
|
|
1712
|
+
title: `xhs unified: ${keyword}`,
|
|
1713
|
+
cwd: options.repoRoot,
|
|
1714
|
+
groupKey: "xhs-unified",
|
|
1715
|
+
args: [
|
|
1716
|
+
script,
|
|
1717
|
+
"--profile",
|
|
1718
|
+
profile,
|
|
1719
|
+
"--keyword",
|
|
1720
|
+
keyword,
|
|
1721
|
+
"--target",
|
|
1722
|
+
String(target),
|
|
1723
|
+
"--max-notes",
|
|
1724
|
+
String(target),
|
|
1725
|
+
"--env",
|
|
1726
|
+
env,
|
|
1727
|
+
"--do-comments",
|
|
1728
|
+
String(asBool(argv["do-comments"], true)),
|
|
1729
|
+
"--fetch-body",
|
|
1730
|
+
String(asBool(argv["fetch-body"], true)),
|
|
1731
|
+
"--do-likes",
|
|
1732
|
+
String(asBool(argv["do-likes"], false)),
|
|
1733
|
+
"--like-keywords",
|
|
1734
|
+
asText(argv["like-keywords"]),
|
|
1735
|
+
"--ui-trigger-id",
|
|
1736
|
+
uiTriggerId
|
|
1737
|
+
]
|
|
1738
|
+
});
|
|
1739
|
+
return { ok: true, runId: asText(ret?.runId), commandType, profile, uiTriggerId };
|
|
1740
|
+
}
|
|
1741
|
+
if (commandType === "weibo-search") {
|
|
1742
|
+
const script = path6.join(options.repoRoot, "apps", "webauto", "entry", "weibo-unified.mjs");
|
|
1743
|
+
const ret = await options.spawnCommand({
|
|
1744
|
+
title: `weibo: ${keyword}`,
|
|
1745
|
+
cwd: options.repoRoot,
|
|
1746
|
+
groupKey: "weibo-search",
|
|
1747
|
+
args: [
|
|
1748
|
+
script,
|
|
1749
|
+
"search",
|
|
1750
|
+
"--profile",
|
|
1751
|
+
profile,
|
|
1752
|
+
"--keyword",
|
|
1753
|
+
keyword,
|
|
1754
|
+
"--target",
|
|
1755
|
+
String(target),
|
|
1756
|
+
"--env",
|
|
1757
|
+
env
|
|
1758
|
+
]
|
|
1759
|
+
});
|
|
1760
|
+
return { ok: true, runId: asText(ret?.runId), commandType, profile };
|
|
1761
|
+
}
|
|
1762
|
+
return { ok: false, error: `\u5F53\u524D\u4EFB\u52A1\u7C7B\u578B\u6682\u4E0D\u652F\u6301\u4EC5\u6267\u884C(\u4E0D\u4FDD\u5B58): ${commandType}` };
|
|
1763
|
+
} catch (err) {
|
|
1764
|
+
return { ok: false, error: err?.message || String(err) };
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1454
1768
|
// src/main/index.mts
|
|
1455
1769
|
var { app, BrowserWindow: BrowserWindow2, ipcMain: ipcMain2, shell, clipboard } = electron;
|
|
1456
|
-
var
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1770
|
+
var spawnedBrowserProcesses = /* @__PURE__ */ new Set();
|
|
1771
|
+
function trackBrowserProcess(pid) {
|
|
1772
|
+
if (pid > 0) {
|
|
1773
|
+
spawnedBrowserProcesses.add(pid);
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
function cleanupAllBrowserProcesses(reason = "ui_close") {
|
|
1777
|
+
console.log(`[process-cleanup] Cleaning up ${spawnedBrowserProcesses.size} browser process(s) (${reason})`);
|
|
1778
|
+
for (const pid of spawnedBrowserProcesses) {
|
|
1779
|
+
try {
|
|
1780
|
+
if (process.platform === "win32") {
|
|
1781
|
+
spawn2("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
|
|
1782
|
+
} else {
|
|
1783
|
+
process.kill(pid, "SIGTERM");
|
|
1784
|
+
}
|
|
1785
|
+
} catch (err) {
|
|
1786
|
+
console.warn(`[process-cleanup] Failed to kill PID ${pid}:`, err);
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
spawnedBrowserProcesses.clear();
|
|
1790
|
+
console.log(`[process-cleanup] Cleanup complete`);
|
|
1791
|
+
}
|
|
1792
|
+
var __dirname = path7.dirname(fileURLToPath2(import.meta.url));
|
|
1793
|
+
var APP_ROOT = path7.resolve(__dirname, "../..");
|
|
1794
|
+
var REPO_ROOT2 = path7.resolve(APP_ROOT, "../..");
|
|
1795
|
+
function readJsonVersion(filePath) {
|
|
1796
|
+
try {
|
|
1797
|
+
const json = JSON.parse(readFileSync(filePath, "utf8"));
|
|
1798
|
+
return String(json?.version || "").trim();
|
|
1799
|
+
} catch {
|
|
1800
|
+
return "";
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
function resolveVersionInfo() {
|
|
1804
|
+
const webauto = readJsonVersion(path7.join(REPO_ROOT2, "package.json")) || "0.0.0";
|
|
1805
|
+
const desktop = readJsonVersion(path7.join(APP_ROOT, "package.json")) || webauto;
|
|
1806
|
+
const windowTitle = `WebAuto Desktop v${webauto}`;
|
|
1807
|
+
const badge = desktop === webauto ? `v${webauto}` : `webauto v${webauto} \xB7 console v${desktop}`;
|
|
1808
|
+
return { webauto, desktop, windowTitle, badge };
|
|
1809
|
+
}
|
|
1810
|
+
var VERSION_INFO = resolveVersionInfo();
|
|
1811
|
+
var DESKTOP_HEARTBEAT_FILE = path7.join(
|
|
1460
1812
|
os5.homedir(),
|
|
1461
1813
|
".webauto",
|
|
1462
1814
|
"run",
|
|
1463
1815
|
"desktop-console-heartbeat.json"
|
|
1464
1816
|
);
|
|
1465
1817
|
var profileStore = createProfileStore({ repoRoot: REPO_ROOT2 });
|
|
1466
|
-
var XHS_SCRIPTS_ROOT =
|
|
1818
|
+
var XHS_SCRIPTS_ROOT = path7.join(REPO_ROOT2, "scripts", "xiaohongshu");
|
|
1467
1819
|
var XHS_FULL_COLLECT_RE = /collect-content\.mjs$/;
|
|
1468
1820
|
function configureElectronPaths() {
|
|
1469
1821
|
try {
|
|
1470
1822
|
const downloadRoot = resolveDefaultDownloadRoot();
|
|
1471
|
-
const normalized =
|
|
1472
|
-
const baseDir =
|
|
1473
|
-
const userDataRoot =
|
|
1474
|
-
const cacheRoot =
|
|
1475
|
-
const gpuCacheRoot =
|
|
1823
|
+
const normalized = path7.normalize(downloadRoot);
|
|
1824
|
+
const baseDir = path7.basename(normalized).toLowerCase() === "download" ? path7.dirname(normalized) : normalized;
|
|
1825
|
+
const userDataRoot = path7.join(baseDir, "desktop-console");
|
|
1826
|
+
const cacheRoot = path7.join(userDataRoot, "cache");
|
|
1827
|
+
const gpuCacheRoot = path7.join(cacheRoot, "gpu");
|
|
1476
1828
|
try {
|
|
1477
1829
|
mkdirSync(cacheRoot, { recursive: true });
|
|
1478
1830
|
} catch {
|
|
@@ -1485,6 +1837,17 @@ function configureElectronPaths() {
|
|
|
1485
1837
|
app.setPath("cache", cacheRoot);
|
|
1486
1838
|
app.commandLine.appendSwitch("disk-cache-dir", cacheRoot);
|
|
1487
1839
|
app.commandLine.appendSwitch("gpu-cache-dir", gpuCacheRoot);
|
|
1840
|
+
const disableGpuByDefault = process.platform === "win32" && String(process.env.WEBAUTO_ELECTRON_DISABLE_GPU || "1").trim() !== "0";
|
|
1841
|
+
if (disableGpuByDefault) {
|
|
1842
|
+
try {
|
|
1843
|
+
app.disableHardwareAcceleration();
|
|
1844
|
+
} catch {
|
|
1845
|
+
}
|
|
1846
|
+
app.commandLine.appendSwitch("disable-gpu");
|
|
1847
|
+
app.commandLine.appendSwitch("disable-gpu-compositing");
|
|
1848
|
+
app.commandLine.appendSwitch("disable-direct-composition");
|
|
1849
|
+
app.commandLine.appendSwitch("use-angle", "swiftshader");
|
|
1850
|
+
}
|
|
1488
1851
|
} catch (err) {
|
|
1489
1852
|
console.warn("[desktop-console] failed to configure cache paths", err);
|
|
1490
1853
|
}
|
|
@@ -1514,8 +1877,35 @@ var GroupQueue = class {
|
|
|
1514
1877
|
};
|
|
1515
1878
|
var groupQueues = /* @__PURE__ */ new Map();
|
|
1516
1879
|
var runs = /* @__PURE__ */ new Map();
|
|
1880
|
+
var runLifecycle = /* @__PURE__ */ new Map();
|
|
1517
1881
|
var trackedRunPids = /* @__PURE__ */ new Set();
|
|
1518
1882
|
var appExitCleanupPromise = null;
|
|
1883
|
+
function setRunLifecycle(runId, patch) {
|
|
1884
|
+
const rid = String(runId || "").trim();
|
|
1885
|
+
if (!rid) return;
|
|
1886
|
+
const current = runLifecycle.get(rid) || {
|
|
1887
|
+
runId: rid,
|
|
1888
|
+
state: "queued",
|
|
1889
|
+
title: "",
|
|
1890
|
+
queuedAt: now()
|
|
1891
|
+
};
|
|
1892
|
+
const next = {
|
|
1893
|
+
...current,
|
|
1894
|
+
...patch,
|
|
1895
|
+
runId: rid
|
|
1896
|
+
};
|
|
1897
|
+
runLifecycle.set(rid, next);
|
|
1898
|
+
if (runLifecycle.size > 400) {
|
|
1899
|
+
const rows = Array.from(runLifecycle.values()).sort((a, b) => (a.queuedAt || 0) - (b.queuedAt || 0));
|
|
1900
|
+
const toDrop = rows.slice(0, Math.max(0, rows.length - 200));
|
|
1901
|
+
for (const row of toDrop) runLifecycle.delete(row.runId);
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
function getRunLifecycle(runId) {
|
|
1905
|
+
const rid = String(runId || "").trim();
|
|
1906
|
+
if (!rid) return null;
|
|
1907
|
+
return runLifecycle.get(rid) || null;
|
|
1908
|
+
}
|
|
1519
1909
|
var UI_HEARTBEAT_TIMEOUT_MS = resolveUiHeartbeatTimeoutMs(process.env);
|
|
1520
1910
|
var lastUiHeartbeatAt = Date.now();
|
|
1521
1911
|
var heartbeatWatchdog = null;
|
|
@@ -1523,6 +1913,16 @@ var heartbeatTimeoutHandled = false;
|
|
|
1523
1913
|
var coreServicesStopRequested = false;
|
|
1524
1914
|
var coreServiceHeartbeatTimer = null;
|
|
1525
1915
|
var coreServiceHeartbeatStopped = false;
|
|
1916
|
+
var RUN_LOG_DIR = path7.join(os5.homedir(), ".webauto", "logs");
|
|
1917
|
+
function appendRunLog(runId, line) {
|
|
1918
|
+
const rid = String(runId || "").trim();
|
|
1919
|
+
const text = String(line || "").replace(/\r?\n/g, " ").trim();
|
|
1920
|
+
if (!rid || !text) return;
|
|
1921
|
+
const logPath = path7.join(RUN_LOG_DIR, `ui-run-${rid}.log`);
|
|
1922
|
+
void fs4.mkdir(RUN_LOG_DIR, { recursive: true }).then(() => fs4.appendFile(logPath, `${text}
|
|
1923
|
+
`, "utf8")).catch(() => {
|
|
1924
|
+
});
|
|
1925
|
+
}
|
|
1526
1926
|
async function writeCoreServiceHeartbeat(status) {
|
|
1527
1927
|
const filePath = String(process.env.WEBAUTO_HEARTBEAT_FILE || DESKTOP_HEARTBEAT_FILE).trim() || DESKTOP_HEARTBEAT_FILE;
|
|
1528
1928
|
const payload = {
|
|
@@ -1532,7 +1932,7 @@ async function writeCoreServiceHeartbeat(status) {
|
|
|
1532
1932
|
source: "desktop-console"
|
|
1533
1933
|
};
|
|
1534
1934
|
try {
|
|
1535
|
-
await fs4.mkdir(
|
|
1935
|
+
await fs4.mkdir(path7.dirname(filePath), { recursive: true });
|
|
1536
1936
|
await fs4.writeFile(filePath, JSON.stringify(payload), "utf8");
|
|
1537
1937
|
} catch {
|
|
1538
1938
|
}
|
|
@@ -1682,7 +2082,7 @@ async function cleanupTrackedRunPidsBestEffort(reason) {
|
|
|
1682
2082
|
}
|
|
1683
2083
|
}
|
|
1684
2084
|
async function cleanupCamoSessionsBestEffort(reason, includeLocks) {
|
|
1685
|
-
const camoCli =
|
|
2085
|
+
const camoCli = path7.join(REPO_ROOT2, "bin", "camoufox-cli.mjs");
|
|
1686
2086
|
const invoke = async (args, timeoutMs = 6e4) => runJson({
|
|
1687
2087
|
title: `camo ${args.join(" ")}`,
|
|
1688
2088
|
cwd: REPO_ROOT2,
|
|
@@ -1703,6 +2103,7 @@ async function cleanupRuntimeEnvironment(reason, options = {}) {
|
|
|
1703
2103
|
killAllRuns(reason);
|
|
1704
2104
|
await cleanupTrackedRunPidsBestEffort(reason);
|
|
1705
2105
|
await cleanupCamoSessionsBestEffort(reason, options.includeLockCleanup !== false);
|
|
2106
|
+
cleanupAllBrowserProcesses(reason);
|
|
1706
2107
|
if (options.stopUiBridge) {
|
|
1707
2108
|
await uiCliBridge.stop().catch(() => null);
|
|
1708
2109
|
}
|
|
@@ -1775,12 +2176,13 @@ function getQueue(groupKey) {
|
|
|
1775
2176
|
function generateRunId() {
|
|
1776
2177
|
return `run_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`;
|
|
1777
2178
|
}
|
|
1778
|
-
function createLineEmitter(runId, type) {
|
|
2179
|
+
function createLineEmitter(runId, type, onLine) {
|
|
1779
2180
|
let pending = "";
|
|
1780
2181
|
const emit = (line) => {
|
|
1781
2182
|
const normalized = String(line || "").replace(/\r$/, "");
|
|
1782
2183
|
if (!normalized) return;
|
|
1783
2184
|
sendEvent({ type, runId, line: normalized, ts: now() });
|
|
2185
|
+
if (typeof onLine === "function") onLine(normalized);
|
|
1784
2186
|
};
|
|
1785
2187
|
return {
|
|
1786
2188
|
push(chunk) {
|
|
@@ -1804,19 +2206,44 @@ function resolveNodeBin2() {
|
|
|
1804
2206
|
const explicit = String(process.env.WEBAUTO_NODE_BIN || "").trim();
|
|
1805
2207
|
if (explicit) return explicit;
|
|
1806
2208
|
const npmNode = String(process.env.npm_node_execpath || "").trim();
|
|
1807
|
-
if (npmNode)
|
|
2209
|
+
if (npmNode) {
|
|
2210
|
+
const base = path7.basename(npmNode).toLowerCase();
|
|
2211
|
+
const isNode = base === "node" || base === "node.exe";
|
|
2212
|
+
if (isNode) return npmNode;
|
|
2213
|
+
}
|
|
1808
2214
|
return process.platform === "win32" ? "node.exe" : "node";
|
|
1809
2215
|
}
|
|
1810
2216
|
function resolveCwd(input) {
|
|
1811
2217
|
const raw = String(input || "").trim();
|
|
1812
2218
|
if (!raw) return REPO_ROOT2;
|
|
1813
|
-
return
|
|
2219
|
+
return path7.isAbsolute(raw) ? raw : path7.resolve(REPO_ROOT2, raw);
|
|
2220
|
+
}
|
|
2221
|
+
function isPidAlive(pid) {
|
|
2222
|
+
const target = Number(pid || 0);
|
|
2223
|
+
if (!Number.isFinite(target) || target <= 0) return false;
|
|
2224
|
+
if (process.platform === "win32") {
|
|
2225
|
+
const ret = spawnSync2("tasklist", ["/FI", `PID eq ${target}`], {
|
|
2226
|
+
windowsHide: true,
|
|
2227
|
+
encoding: "utf8",
|
|
2228
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
2229
|
+
});
|
|
2230
|
+
const out = String(ret.stdout || "");
|
|
2231
|
+
if (!out) return false;
|
|
2232
|
+
const lines = out.split(/\r?\n/g).map((line) => line.trim()).filter(Boolean);
|
|
2233
|
+
return lines.some((line) => line.includes(` ${target}`));
|
|
2234
|
+
}
|
|
2235
|
+
try {
|
|
2236
|
+
process.kill(target, 0);
|
|
2237
|
+
return true;
|
|
2238
|
+
} catch {
|
|
2239
|
+
return false;
|
|
2240
|
+
}
|
|
1814
2241
|
}
|
|
1815
2242
|
var cachedStateMod = null;
|
|
1816
2243
|
async function getStateModule() {
|
|
1817
2244
|
if (cachedStateMod) return cachedStateMod;
|
|
1818
2245
|
try {
|
|
1819
|
-
const p =
|
|
2246
|
+
const p = path7.join(REPO_ROOT2, "dist", "modules", "state", "src", "xiaohongshu-collect-state.js");
|
|
1820
2247
|
cachedStateMod = await import(pathToFileURL3(p).href);
|
|
1821
2248
|
return cachedStateMod;
|
|
1822
2249
|
} catch {
|
|
@@ -1830,6 +2257,12 @@ async function spawnCommand(spec) {
|
|
|
1830
2257
|
const q = getQueue(groupKey);
|
|
1831
2258
|
const cwd = resolveCwd(spec.cwd);
|
|
1832
2259
|
const args = Array.isArray(spec.args) ? spec.args : [];
|
|
2260
|
+
appendRunLog(runId, `[queued] title=${String(spec.title || "").trim() || "-"} cwd=${cwd}`);
|
|
2261
|
+
setRunLifecycle(runId, {
|
|
2262
|
+
state: "queued",
|
|
2263
|
+
title: String(spec.title || ""),
|
|
2264
|
+
queuedAt: now()
|
|
2265
|
+
});
|
|
1833
2266
|
const isXhsRunCommand = args.some((item) => /xhs-(orchestrate|unified)\.mjs$/i.test(String(item || "").replace(/\\/g, "/")));
|
|
1834
2267
|
const extractProfilesFromArgs = (argv) => {
|
|
1835
2268
|
const out = [];
|
|
@@ -1862,49 +2295,133 @@ async function spawnCommand(spec) {
|
|
|
1862
2295
|
let finished = false;
|
|
1863
2296
|
let exitCode = null;
|
|
1864
2297
|
let exitSignal = null;
|
|
2298
|
+
let orphanCheckTimer = null;
|
|
1865
2299
|
const finalize = (code, signal) => {
|
|
1866
2300
|
if (finished) return;
|
|
1867
2301
|
finished = true;
|
|
2302
|
+
if (orphanCheckTimer) {
|
|
2303
|
+
clearInterval(orphanCheckTimer);
|
|
2304
|
+
orphanCheckTimer = null;
|
|
2305
|
+
}
|
|
2306
|
+
setRunLifecycle(runId, {
|
|
2307
|
+
state: "exited",
|
|
2308
|
+
exitedAt: now(),
|
|
2309
|
+
exitCode: code,
|
|
2310
|
+
signal
|
|
2311
|
+
});
|
|
1868
2312
|
sendEvent({ type: "exit", runId, exitCode: code, signal, ts: now() });
|
|
2313
|
+
appendRunLog(runId, `[exit] code=${code ?? "null"} signal=${signal ?? "null"}`);
|
|
1869
2314
|
runs.delete(runId);
|
|
1870
2315
|
resolve();
|
|
1871
2316
|
};
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
2317
|
+
let child;
|
|
2318
|
+
try {
|
|
2319
|
+
const nodeBin = resolveNodeBin2();
|
|
2320
|
+
appendRunLog(runId, `[cmd] ${nodeBin}`);
|
|
2321
|
+
child = spawn2(nodeBin, args, {
|
|
2322
|
+
cwd,
|
|
2323
|
+
env: {
|
|
2324
|
+
...process.env,
|
|
2325
|
+
...spec.env || {}
|
|
2326
|
+
},
|
|
2327
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2328
|
+
windowsHide: true
|
|
2329
|
+
});
|
|
2330
|
+
} catch (err) {
|
|
2331
|
+
const message = err?.message || String(err);
|
|
2332
|
+
setRunLifecycle(runId, {
|
|
2333
|
+
state: "exited",
|
|
2334
|
+
exitedAt: now(),
|
|
2335
|
+
exitCode: null,
|
|
2336
|
+
signal: "spawn_exception",
|
|
2337
|
+
lastError: message
|
|
2338
|
+
});
|
|
2339
|
+
sendEvent({ type: "stderr", runId, line: `[spawn-throw] ${message}`, ts: now() });
|
|
2340
|
+
appendRunLog(runId, `[spawn-throw] ${message}`);
|
|
2341
|
+
finalize(null, "spawn_exception");
|
|
2342
|
+
return;
|
|
2343
|
+
}
|
|
2344
|
+
const stdoutLines = createLineEmitter(runId, "stdout", (line) => appendRunLog(runId, `[stdout] ${line}`));
|
|
2345
|
+
const stderrLines = createLineEmitter(runId, "stderr", (line) => appendRunLog(runId, `[stderr] ${line}`));
|
|
2346
|
+
try {
|
|
2347
|
+
child.stdout?.on("data", (chunk) => {
|
|
2348
|
+
stdoutLines.push(chunk);
|
|
2349
|
+
});
|
|
2350
|
+
child.stderr?.on("data", (chunk) => {
|
|
2351
|
+
const text = chunk.toString("utf8");
|
|
2352
|
+
const lines = text.split(/\r?\n/g).map((line) => line.trim()).filter(Boolean);
|
|
2353
|
+
if (lines.length > 0) {
|
|
2354
|
+
setRunLifecycle(runId, { lastError: lines[lines.length - 1] });
|
|
2355
|
+
}
|
|
2356
|
+
stderrLines.push(chunk);
|
|
2357
|
+
});
|
|
2358
|
+
child.on("error", (err) => {
|
|
2359
|
+
const message = err?.message || String(err);
|
|
2360
|
+
setRunLifecycle(runId, { lastError: message });
|
|
2361
|
+
sendEvent({ type: "stderr", runId, line: `[spawn-error] ${message}`, ts: now() });
|
|
2362
|
+
appendRunLog(runId, `[spawn-error] ${message}`);
|
|
2363
|
+
finalize(null, "error");
|
|
2364
|
+
});
|
|
2365
|
+
child.on("exit", (code, signal) => {
|
|
2366
|
+
exitCode = code;
|
|
2367
|
+
exitSignal = signal;
|
|
2368
|
+
const timer = setTimeout(() => {
|
|
2369
|
+
if (finished) return;
|
|
2370
|
+
untrackRunPid(child);
|
|
2371
|
+
stdoutLines.flush();
|
|
2372
|
+
stderrLines.flush();
|
|
2373
|
+
finalize(exitCode ?? null, exitSignal ?? null);
|
|
2374
|
+
}, 200);
|
|
2375
|
+
if (timer && typeof timer.unref === "function") {
|
|
2376
|
+
timer.unref();
|
|
2377
|
+
}
|
|
2378
|
+
});
|
|
2379
|
+
child.on("close", (code, signal) => {
|
|
2380
|
+
untrackRunPid(child);
|
|
2381
|
+
stdoutLines.flush();
|
|
2382
|
+
stderrLines.flush();
|
|
2383
|
+
finalize(exitCode ?? code ?? null, exitSignal ?? signal ?? null);
|
|
2384
|
+
});
|
|
2385
|
+
} catch (err) {
|
|
2386
|
+
const message = err?.message || String(err);
|
|
2387
|
+
setRunLifecycle(runId, { lastError: message });
|
|
2388
|
+
sendEvent({ type: "stderr", runId, line: `[spawn-setup-error] ${message}`, ts: now() });
|
|
2389
|
+
appendRunLog(runId, `[spawn-setup-error] ${message}`);
|
|
2390
|
+
finalize(null, "setup_error");
|
|
2391
|
+
return;
|
|
2392
|
+
}
|
|
1883
2393
|
trackRunPid(child);
|
|
2394
|
+
if (child.pid) {
|
|
2395
|
+
trackBrowserProcess(child.pid);
|
|
2396
|
+
}
|
|
1884
2397
|
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);
|
|
2398
|
+
setRunLifecycle(runId, {
|
|
2399
|
+
state: "running",
|
|
2400
|
+
startedAt: now(),
|
|
2401
|
+
pid: child.pid || -1,
|
|
2402
|
+
title: String(spec.title || "")
|
|
1907
2403
|
});
|
|
2404
|
+
sendEvent({ type: "started", runId, title: spec.title, pid: child.pid ?? -1, ts: now() });
|
|
2405
|
+
appendRunLog(runId, `[started] pid=${child.pid ?? -1} title=${String(spec.title || "").trim() || "-"}`);
|
|
2406
|
+
if (args.length > 0) {
|
|
2407
|
+
appendRunLog(runId, `[args] ${args.join(" ")}`);
|
|
2408
|
+
}
|
|
2409
|
+
const pid = Number(child.pid || 0);
|
|
2410
|
+
if (pid > 0) {
|
|
2411
|
+
orphanCheckTimer = setInterval(() => {
|
|
2412
|
+
if (finished) return;
|
|
2413
|
+
if (!isPidAlive(pid)) {
|
|
2414
|
+
untrackRunPid(child);
|
|
2415
|
+
stdoutLines.flush();
|
|
2416
|
+
stderrLines.flush();
|
|
2417
|
+
appendRunLog(runId, "[watchdog] child pid disappeared before close/exit event");
|
|
2418
|
+
finalize(exitCode, exitSignal || "pid_gone");
|
|
2419
|
+
}
|
|
2420
|
+
}, 1e3);
|
|
2421
|
+
if (orphanCheckTimer && typeof orphanCheckTimer.unref === "function") {
|
|
2422
|
+
orphanCheckTimer.unref();
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
1908
2425
|
})
|
|
1909
2426
|
);
|
|
1910
2427
|
return { runId };
|
|
@@ -1944,9 +2461,106 @@ async function runJson(spec) {
|
|
|
1944
2461
|
return { ok: true, code, stdout: out, stderr: err };
|
|
1945
2462
|
}
|
|
1946
2463
|
}
|
|
2464
|
+
function sleep2(ms) {
|
|
2465
|
+
return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
|
|
2466
|
+
}
|
|
2467
|
+
function toEpochMs(value) {
|
|
2468
|
+
const raw = String(value ?? "").trim();
|
|
2469
|
+
if (!raw) return 0;
|
|
2470
|
+
const asNum = Number(raw);
|
|
2471
|
+
if (Number.isFinite(asNum) && asNum > 0) return asNum;
|
|
2472
|
+
const asDate = Date.parse(raw);
|
|
2473
|
+
return Number.isFinite(asDate) ? asDate : 0;
|
|
2474
|
+
}
|
|
2475
|
+
function resolveUnifiedApiBaseUrl() {
|
|
2476
|
+
return String(
|
|
2477
|
+
process.env.WEBAUTO_UNIFIED_API || process.env.WEBAUTO_UNIFIED_URL || "http://127.0.0.1:7701"
|
|
2478
|
+
).trim().replace(/\/+$/, "");
|
|
2479
|
+
}
|
|
2480
|
+
async function listUnifiedTasks() {
|
|
2481
|
+
const baseUrl = resolveUnifiedApiBaseUrl();
|
|
2482
|
+
const res = await fetch(`${baseUrl}/api/v1/tasks`, { signal: AbortSignal.timeout(3e3) });
|
|
2483
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
2484
|
+
const payload = await res.json().catch(() => ({}));
|
|
2485
|
+
return Array.isArray(payload?.data) ? payload.data : [];
|
|
2486
|
+
}
|
|
2487
|
+
function pickUnifiedRunId(tasks, options) {
|
|
2488
|
+
const profileId = String(options.profileId || "").trim();
|
|
2489
|
+
const keyword = String(options.keyword || "").trim();
|
|
2490
|
+
const uiTriggerId = String(options.uiTriggerId || "").trim();
|
|
2491
|
+
const minTs = Number(options.minTs || 0);
|
|
2492
|
+
const baselineRunIds = options.baselineRunIds instanceof Set ? options.baselineRunIds : /* @__PURE__ */ new Set();
|
|
2493
|
+
const rows = tasks.filter((task) => {
|
|
2494
|
+
const runId = String(task?.runId || task?.id || "").trim();
|
|
2495
|
+
if (!runId) return false;
|
|
2496
|
+
if (baselineRunIds.has(runId)) return false;
|
|
2497
|
+
const phase = String(task?.phase || task?.lastPhase || "").trim().toLowerCase();
|
|
2498
|
+
if (phase !== "unified") return false;
|
|
2499
|
+
const taskProfile = String(task?.profileId || "").trim();
|
|
2500
|
+
const taskKeyword = String(task?.keyword || "").trim();
|
|
2501
|
+
const taskTrigger = String(
|
|
2502
|
+
task?.uiTriggerId || task?.triggerId || task?.meta?.uiTriggerId || task?.context?.uiTriggerId || ""
|
|
2503
|
+
).trim();
|
|
2504
|
+
if (uiTriggerId && taskTrigger !== uiTriggerId) return false;
|
|
2505
|
+
if (profileId && taskProfile && taskProfile !== profileId) return false;
|
|
2506
|
+
if (keyword && taskKeyword && taskKeyword !== keyword) return false;
|
|
2507
|
+
const createdTs = toEpochMs(task?.createdAt);
|
|
2508
|
+
const startedTs = toEpochMs(task?.startedAt);
|
|
2509
|
+
const ts = createdTs || startedTs;
|
|
2510
|
+
return ts >= minTs;
|
|
2511
|
+
}).sort((a, b) => {
|
|
2512
|
+
const ta = toEpochMs(a?.createdAt) || toEpochMs(a?.startedAt);
|
|
2513
|
+
const tb = toEpochMs(b?.createdAt) || toEpochMs(b?.startedAt);
|
|
2514
|
+
return tb - ta;
|
|
2515
|
+
});
|
|
2516
|
+
const picked = rows[0] || null;
|
|
2517
|
+
return String(picked?.runId || picked?.id || "").trim();
|
|
2518
|
+
}
|
|
2519
|
+
async function waitForUnifiedRunRegistration(input) {
|
|
2520
|
+
const desktopRunId = String(input.desktopRunId || "").trim();
|
|
2521
|
+
const profileId = String(input.profileId || "").trim();
|
|
2522
|
+
const keyword = String(input.keyword || "").trim();
|
|
2523
|
+
const uiTriggerId = String(input.uiTriggerId || "").trim();
|
|
2524
|
+
const timeoutMs = Math.max(2e3, Number(input.timeoutMs || 2e4) || 2e4);
|
|
2525
|
+
const startedAt = Date.now();
|
|
2526
|
+
const minTs = startedAt - 3e4;
|
|
2527
|
+
const baselineRunIds = input.baselineRunIds instanceof Set ? input.baselineRunIds : /* @__PURE__ */ new Set();
|
|
2528
|
+
let lastFetchError = "";
|
|
2529
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
2530
|
+
try {
|
|
2531
|
+
const tasks = await listUnifiedTasks();
|
|
2532
|
+
const unifiedRunId = pickUnifiedRunId(tasks, {
|
|
2533
|
+
profileId,
|
|
2534
|
+
keyword,
|
|
2535
|
+
uiTriggerId,
|
|
2536
|
+
minTs,
|
|
2537
|
+
baselineRunIds
|
|
2538
|
+
});
|
|
2539
|
+
if (unifiedRunId) return { ok: true, runId: unifiedRunId };
|
|
2540
|
+
lastFetchError = "";
|
|
2541
|
+
} catch (err) {
|
|
2542
|
+
lastFetchError = err?.message || String(err);
|
|
2543
|
+
}
|
|
2544
|
+
const lifecycle2 = getRunLifecycle(desktopRunId);
|
|
2545
|
+
if (lifecycle2?.state === "exited") {
|
|
2546
|
+
const detail = lifecycle2.lastError || `exit=${lifecycle2.exitCode ?? "null"}`;
|
|
2547
|
+
return { ok: false, error: `\u4EFB\u52A1\u8FDB\u7A0B\u63D0\u524D\u9000\u51FA\uFF0C\u672A\u6CE8\u518C unified runId (${detail})` };
|
|
2548
|
+
}
|
|
2549
|
+
await sleep2(500);
|
|
2550
|
+
}
|
|
2551
|
+
const lifecycle = getRunLifecycle(desktopRunId);
|
|
2552
|
+
if (lifecycle?.state === "queued") {
|
|
2553
|
+
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" };
|
|
2554
|
+
}
|
|
2555
|
+
if (lifecycle?.state === "running") {
|
|
2556
|
+
return { ok: false, error: "\u4EFB\u52A1\u8FDB\u7A0B\u5DF2\u542F\u52A8\uFF0C\u4F46\u5728\u8D85\u65F6\u5185\u672A\u6CE8\u518C unified runId" };
|
|
2557
|
+
}
|
|
2558
|
+
const suffix = lastFetchError ? `: ${lastFetchError}` : "";
|
|
2559
|
+
return { ok: false, error: `\u672A\u68C0\u6D4B\u5230 unified runId${suffix}` };
|
|
2560
|
+
}
|
|
1947
2561
|
async function scanResults(input) {
|
|
1948
2562
|
const downloadRoot = String(input.downloadRoot || resolveDefaultDownloadRoot());
|
|
1949
|
-
const root =
|
|
2563
|
+
const root = path7.join(downloadRoot, "xiaohongshu");
|
|
1950
2564
|
const result = { ok: true, root, entries: [] };
|
|
1951
2565
|
try {
|
|
1952
2566
|
const stateMod = await getStateModule();
|
|
@@ -1954,12 +2568,12 @@ async function scanResults(input) {
|
|
|
1954
2568
|
for (const envEnt of envDirs) {
|
|
1955
2569
|
if (!envEnt.isDirectory()) continue;
|
|
1956
2570
|
const env = envEnt.name;
|
|
1957
|
-
const envPath =
|
|
2571
|
+
const envPath = path7.join(root, env);
|
|
1958
2572
|
const keywordDirs = await fs4.readdir(envPath, { withFileTypes: true });
|
|
1959
2573
|
for (const kwEnt of keywordDirs) {
|
|
1960
2574
|
if (!kwEnt.isDirectory()) continue;
|
|
1961
2575
|
const keyword = kwEnt.name;
|
|
1962
|
-
const kwPath =
|
|
2576
|
+
const kwPath = path7.join(envPath, keyword);
|
|
1963
2577
|
const stat = await fs4.stat(kwPath).catch(() => null);
|
|
1964
2578
|
let stateSummary = null;
|
|
1965
2579
|
if (stateMod?.loadXhsCollectState) {
|
|
@@ -1994,7 +2608,7 @@ async function listXhsFullCollectScripts() {
|
|
|
1994
2608
|
return {
|
|
1995
2609
|
id: `xhs:${name}`,
|
|
1996
2610
|
label: `Full Collect (${name})`,
|
|
1997
|
-
path:
|
|
2611
|
+
path: path7.join(XHS_SCRIPTS_ROOT, name)
|
|
1998
2612
|
};
|
|
1999
2613
|
});
|
|
2000
2614
|
return { ok: true, scripts };
|
|
@@ -2064,11 +2678,11 @@ async function listDir(input) {
|
|
|
2064
2678
|
const items = await fs4.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
2065
2679
|
for (const ent of items) {
|
|
2066
2680
|
if (entries.length >= maxEntries) break;
|
|
2067
|
-
const full =
|
|
2681
|
+
const full = path7.join(dir, ent.name);
|
|
2068
2682
|
const st = await fs4.stat(full).catch(() => null);
|
|
2069
2683
|
entries.push({
|
|
2070
2684
|
path: full,
|
|
2071
|
-
rel:
|
|
2685
|
+
rel: path7.relative(root, full),
|
|
2072
2686
|
name: ent.name,
|
|
2073
2687
|
isDir: ent.isDirectory(),
|
|
2074
2688
|
size: st?.size || 0,
|
|
@@ -2085,13 +2699,13 @@ async function listDir(input) {
|
|
|
2085
2699
|
}
|
|
2086
2700
|
function createWindow() {
|
|
2087
2701
|
win = new BrowserWindow2({
|
|
2088
|
-
title:
|
|
2702
|
+
title: VERSION_INFO.windowTitle,
|
|
2089
2703
|
width: 1280,
|
|
2090
2704
|
height: 900,
|
|
2091
2705
|
minWidth: 920,
|
|
2092
2706
|
minHeight: 800,
|
|
2093
2707
|
webPreferences: {
|
|
2094
|
-
preload:
|
|
2708
|
+
preload: path7.join(APP_ROOT, "dist", "main", "preload.mjs"),
|
|
2095
2709
|
contextIsolation: true,
|
|
2096
2710
|
nodeIntegration: false,
|
|
2097
2711
|
sandbox: false,
|
|
@@ -2099,7 +2713,7 @@ function createWindow() {
|
|
|
2099
2713
|
backgroundThrottling: false
|
|
2100
2714
|
}
|
|
2101
2715
|
});
|
|
2102
|
-
const htmlPath =
|
|
2716
|
+
const htmlPath = path7.join(APP_ROOT, "dist", "renderer", "index.html");
|
|
2103
2717
|
void win.loadFile(htmlPath);
|
|
2104
2718
|
ensureStateBridge();
|
|
2105
2719
|
}
|
|
@@ -2131,6 +2745,7 @@ ipcMain2.on("preload:test", () => {
|
|
|
2131
2745
|
setTimeout(() => app.quit(), 200);
|
|
2132
2746
|
});
|
|
2133
2747
|
ipcMain2.handle("settings:get", async () => readDesktopConsoleSettings({ appRoot: APP_ROOT, repoRoot: REPO_ROOT2 }));
|
|
2748
|
+
ipcMain2.handle("app:getVersion", async () => VERSION_INFO);
|
|
2134
2749
|
ipcMain2.handle("settings:set", async (_evt, next) => {
|
|
2135
2750
|
const updated = await writeDesktopConsoleSettings({ appRoot: APP_ROOT, repoRoot: REPO_ROOT2 }, next || {});
|
|
2136
2751
|
const w = getWin();
|
|
@@ -2238,6 +2853,73 @@ ipcMain2.handle("cmd:runJson", async (_evt, spec) => {
|
|
|
2238
2853
|
const args = Array.isArray(spec?.args) ? spec.args : [];
|
|
2239
2854
|
return runJson({ ...spec, cwd, args });
|
|
2240
2855
|
});
|
|
2856
|
+
ipcMain2.handle("schedule:invoke", async (_evt, input) => scheduleInvoke(
|
|
2857
|
+
{
|
|
2858
|
+
repoRoot: REPO_ROOT2,
|
|
2859
|
+
runJson: (spec) => runJson(spec),
|
|
2860
|
+
spawnCommand: (spec) => spawnCommand(spec)
|
|
2861
|
+
},
|
|
2862
|
+
input || { action: "list" }
|
|
2863
|
+
));
|
|
2864
|
+
ipcMain2.handle("task:runEphemeral", async (_evt, input) => {
|
|
2865
|
+
const payload = input || {};
|
|
2866
|
+
let baselineRunIds = /* @__PURE__ */ new Set();
|
|
2867
|
+
try {
|
|
2868
|
+
const baselineTasks = await listUnifiedTasks();
|
|
2869
|
+
baselineRunIds = new Set(
|
|
2870
|
+
baselineTasks.map((task) => String(task?.runId || task?.id || "").trim()).filter(Boolean)
|
|
2871
|
+
);
|
|
2872
|
+
} catch {
|
|
2873
|
+
baselineRunIds = /* @__PURE__ */ new Set();
|
|
2874
|
+
}
|
|
2875
|
+
const result = await runEphemeralTask(
|
|
2876
|
+
{
|
|
2877
|
+
repoRoot: REPO_ROOT2,
|
|
2878
|
+
runJson: (spec) => runJson(spec),
|
|
2879
|
+
spawnCommand: (spec) => spawnCommand(spec)
|
|
2880
|
+
},
|
|
2881
|
+
payload
|
|
2882
|
+
);
|
|
2883
|
+
if (!result?.ok) return result;
|
|
2884
|
+
const commandType = String(result?.commandType || payload?.commandType || "").trim().toLowerCase();
|
|
2885
|
+
const desktopRunId = String(result?.runId || "").trim();
|
|
2886
|
+
if (commandType !== "xhs-unified" || !desktopRunId) {
|
|
2887
|
+
return result;
|
|
2888
|
+
}
|
|
2889
|
+
const profileId = String(result?.profile || payload?.argv?.profile || "").trim();
|
|
2890
|
+
const keyword = String(payload?.argv?.keyword || payload?.argv?.k || "").trim();
|
|
2891
|
+
const uiTriggerId = String(result?.uiTriggerId || payload?.argv?.["ui-trigger-id"] || "").trim();
|
|
2892
|
+
appendRunLog(
|
|
2893
|
+
desktopRunId,
|
|
2894
|
+
`[wait-register] profile=${profileId || "-"} keyword=${keyword || "-"} uiTriggerId=${uiTriggerId || "-"}`
|
|
2895
|
+
);
|
|
2896
|
+
const waited = await waitForUnifiedRunRegistration({
|
|
2897
|
+
desktopRunId,
|
|
2898
|
+
profileId,
|
|
2899
|
+
keyword,
|
|
2900
|
+
uiTriggerId,
|
|
2901
|
+
baselineRunIds,
|
|
2902
|
+
timeoutMs: 2e4
|
|
2903
|
+
});
|
|
2904
|
+
if (!waited.ok) {
|
|
2905
|
+
appendRunLog(desktopRunId, `[wait-register-failed] ${String(waited.error || "unknown_error")}`);
|
|
2906
|
+
return {
|
|
2907
|
+
ok: false,
|
|
2908
|
+
error: waited.error,
|
|
2909
|
+
runId: desktopRunId,
|
|
2910
|
+
commandType: result?.commandType || "xhs-unified",
|
|
2911
|
+
profile: profileId,
|
|
2912
|
+
uiTriggerId
|
|
2913
|
+
};
|
|
2914
|
+
}
|
|
2915
|
+
appendRunLog(desktopRunId, `[wait-register-ok] unifiedRunId=${waited.runId || "-"} uiTriggerId=${uiTriggerId || "-"}`);
|
|
2916
|
+
return {
|
|
2917
|
+
...result,
|
|
2918
|
+
runId: desktopRunId,
|
|
2919
|
+
unifiedRunId: waited.runId,
|
|
2920
|
+
uiTriggerId
|
|
2921
|
+
};
|
|
2922
|
+
});
|
|
2241
2923
|
ipcMain2.handle("results:scan", async (_evt, spec) => scanResults(spec || {}));
|
|
2242
2924
|
ipcMain2.handle("fs:listDir", async (_evt, spec) => listDir(spec));
|
|
2243
2925
|
ipcMain2.handle(
|
|
@@ -2277,12 +2959,56 @@ ipcMain2.handle("env:repairCore", async () => {
|
|
|
2277
2959
|
const services = await checkServices().catch(() => ({ unifiedApi: false, camoRuntime: false }));
|
|
2278
2960
|
return { ok, services };
|
|
2279
2961
|
});
|
|
2962
|
+
ipcMain2.handle("env:cleanup", async () => {
|
|
2963
|
+
markUiHeartbeat("env_cleanup");
|
|
2964
|
+
console.log("[env:cleanup] Starting environment cleanup...");
|
|
2965
|
+
await cleanupRuntimeEnvironment("env_cleanup_requested", {
|
|
2966
|
+
stopUiBridge: false,
|
|
2967
|
+
stopHeartbeat: false,
|
|
2968
|
+
stopCoreServices: false,
|
|
2969
|
+
stopStateBridge: false,
|
|
2970
|
+
includeLockCleanup: true
|
|
2971
|
+
});
|
|
2972
|
+
let locksCleared = 0;
|
|
2973
|
+
try {
|
|
2974
|
+
const profiles = await profileStore.listProfiles();
|
|
2975
|
+
for (const p of profiles) {
|
|
2976
|
+
const lockFile = path7.join(p.path, ".lock");
|
|
2977
|
+
try {
|
|
2978
|
+
await fs4.unlink(lockFile);
|
|
2979
|
+
locksCleared++;
|
|
2980
|
+
console.log(`[env:cleanup] Cleared lock for profile ${p.profileId}`);
|
|
2981
|
+
} catch (err) {
|
|
2982
|
+
if (err?.code !== "ENOENT") {
|
|
2983
|
+
console.warn(`[env:cleanup] Failed to clear lock for ${p.profileId}:`, err.message);
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
} catch (err) {
|
|
2988
|
+
console.warn("[env:cleanup] Failed to list profiles:", err);
|
|
2989
|
+
}
|
|
2990
|
+
await stopCoreDaemon().catch(() => null);
|
|
2991
|
+
const restarted = await startCoreDaemon().catch(() => false);
|
|
2992
|
+
const services = await checkServices().catch(() => ({ unifiedApi: false, camoRuntime: false }));
|
|
2993
|
+
const firefox = await checkFirefox().catch(() => ({ installed: false }));
|
|
2994
|
+
const camo = await checkCamoCli().catch(() => ({ installed: false }));
|
|
2995
|
+
console.log("[env:cleanup] Cleanup complete:", { locksCleared, restarted, services, firefox, camo });
|
|
2996
|
+
return {
|
|
2997
|
+
ok: true,
|
|
2998
|
+
locksCleared,
|
|
2999
|
+
coreRestarted: restarted,
|
|
3000
|
+
services,
|
|
3001
|
+
firefox,
|
|
3002
|
+
camo
|
|
3003
|
+
};
|
|
3004
|
+
});
|
|
2280
3005
|
ipcMain2.handle("env:repairDeps", async (_evt, input) => {
|
|
2281
3006
|
const wantCore = Boolean(input?.core);
|
|
2282
3007
|
const wantBrowser = Boolean(input?.browser);
|
|
2283
3008
|
const wantGeoip = Boolean(input?.geoip);
|
|
2284
3009
|
const wantReinstall = Boolean(input?.reinstall);
|
|
2285
3010
|
const wantUninstall = Boolean(input?.uninstall);
|
|
3011
|
+
const wantEnsureBackend = Boolean(input?.ensureBackend);
|
|
2286
3012
|
const result = { ok: true, core: null, install: null, env: null };
|
|
2287
3013
|
if (wantCore) {
|
|
2288
3014
|
const coreOk = await startCoreDaemon().catch(() => false);
|
|
@@ -2293,13 +3019,13 @@ ipcMain2.handle("env:repairDeps", async (_evt, input) => {
|
|
|
2293
3019
|
if (!coreOk) result.ok = false;
|
|
2294
3020
|
}
|
|
2295
3021
|
if (wantBrowser || wantGeoip) {
|
|
2296
|
-
const args = [
|
|
3022
|
+
const args = [path7.join("apps", "webauto", "entry", "xhs-install.mjs")];
|
|
2297
3023
|
if (wantReinstall) args.push("--reinstall");
|
|
2298
3024
|
else if (wantUninstall) args.push("--uninstall");
|
|
2299
3025
|
else args.push("--install");
|
|
2300
3026
|
if (wantBrowser) args.push("--download-browser");
|
|
2301
3027
|
if (wantGeoip) args.push("--download-geoip");
|
|
2302
|
-
if (!wantUninstall) args.push("--ensure-backend");
|
|
3028
|
+
if (!wantUninstall && wantEnsureBackend) args.push("--ensure-backend");
|
|
2303
3029
|
const installRes = await runJson({
|
|
2304
3030
|
title: "env repair deps",
|
|
2305
3031
|
cwd: REPO_ROOT2,
|
|
@@ -2312,20 +3038,6 @@ ipcMain2.handle("env:repairDeps", async (_evt, input) => {
|
|
|
2312
3038
|
result.env = await checkEnvironment().catch(() => null);
|
|
2313
3039
|
return result;
|
|
2314
3040
|
});
|
|
2315
|
-
ipcMain2.handle("env:cleanup", async () => {
|
|
2316
|
-
markUiHeartbeat("env_cleanup");
|
|
2317
|
-
await cleanupRuntimeEnvironment("env_cleanup", {
|
|
2318
|
-
stopUiBridge: false,
|
|
2319
|
-
stopHeartbeat: false,
|
|
2320
|
-
stopCoreServices: false,
|
|
2321
|
-
stopStateBridge: false,
|
|
2322
|
-
includeLockCleanup: true
|
|
2323
|
-
});
|
|
2324
|
-
return {
|
|
2325
|
-
ok: true,
|
|
2326
|
-
services: await checkServices().catch(() => ({ unifiedApi: false, camoRuntime: false }))
|
|
2327
|
-
};
|
|
2328
|
-
});
|
|
2329
3041
|
ipcMain2.handle("config:saveLast", async (_evt, config) => {
|
|
2330
3042
|
await saveCrawlConfig({ appRoot: APP_ROOT, repoRoot: REPO_ROOT2 }, config);
|
|
2331
3043
|
return { ok: true };
|
|
@@ -2416,7 +3128,7 @@ ipcMain2.handle("runtime:kill", async (_evt, input) => {
|
|
|
2416
3128
|
ipcMain2.handle("runtime:restartPhase1", async (_evt, input) => {
|
|
2417
3129
|
const profileId = String(input?.profileId || "").trim();
|
|
2418
3130
|
if (!profileId) return { ok: false, error: "missing profileId" };
|
|
2419
|
-
const args = [
|
|
3131
|
+
const args = [path7.join(REPO_ROOT2, "scripts", "xiaohongshu", "phase1-boot.mjs"), "--profile", profileId, "--headless", "false"];
|
|
2420
3132
|
return spawnCommand({ title: `Phase1 restart ${profileId}`, cwd: REPO_ROOT2, args, groupKey: "phase1" });
|
|
2421
3133
|
});
|
|
2422
3134
|
ipcMain2.handle("runtime:setBrowserTitle", async (_evt, input) => {
|