@web-auto/webauto 0.1.18 → 0.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +122 -53
  2. package/apps/desktop-console/dist/main/index.mjs +227 -12
  3. package/apps/desktop-console/dist/renderer/index.js +237 -8
  4. package/apps/desktop-console/entry/ui-cli.mjs +282 -16
  5. package/apps/desktop-console/entry/ui-console.mjs +46 -15
  6. package/apps/webauto/entry/account.mjs +126 -27
  7. package/apps/webauto/entry/lib/account-detect.mjs +399 -9
  8. package/apps/webauto/entry/lib/account-store.mjs +201 -109
  9. package/apps/webauto/entry/lib/iflow-reply.mjs +194 -0
  10. package/apps/webauto/entry/lib/profile-policy.mjs +48 -0
  11. package/apps/webauto/entry/lib/profilepool.mjs +12 -0
  12. package/apps/webauto/entry/lib/schedule-store.mjs +29 -2
  13. package/apps/webauto/entry/lib/session-init.mjs +227 -0
  14. package/apps/webauto/entry/lib/upgrade-check.mjs +269 -0
  15. package/apps/webauto/entry/lib/xhs-unified-blocks.mjs +160 -0
  16. package/apps/webauto/entry/lib/xhs-unified-output-blocks.mjs +83 -0
  17. package/apps/webauto/entry/lib/xhs-unified-plan-blocks.mjs +55 -0
  18. package/apps/webauto/entry/lib/xhs-unified-profile-blocks.mjs +542 -0
  19. package/apps/webauto/entry/lib/xhs-unified-runtime-blocks.mjs +436 -0
  20. package/apps/webauto/entry/profilepool.mjs +56 -9
  21. package/apps/webauto/entry/smart-reply-cli.mjs +267 -0
  22. package/apps/webauto/entry/weibo-unified.mjs +84 -11
  23. package/apps/webauto/entry/xhs-orchestrate.mjs +43 -1
  24. package/apps/webauto/entry/xhs-unified.mjs +92 -997
  25. package/bin/webauto.mjs +22 -4
  26. package/dist/modules/camo-backend/src/index.js +33 -0
  27. package/dist/modules/camo-backend/src/internal/BrowserSession.js +232 -49
  28. package/dist/modules/camo-backend/src/internal/engine-manager.js +14 -13
  29. package/dist/modules/camo-backend/src/internal/ws-server.js +16 -19
  30. package/dist/modules/camo-runtime/src/utils/browser-service.mjs +38 -6
  31. package/dist/modules/workflow/blocks/EnsureSession.js +0 -8
  32. package/dist/modules/workflow/blocks/WeiboCollectFromLinksBlock.js +78 -6
  33. package/dist/modules/workflow/blocks/WeiboCollectSearchLinksBlock.js +266 -192
  34. package/dist/modules/workflow/definitions/weibo-search-workflow-v1.js +2 -0
  35. package/dist/modules/workflow/src/runner.js +2 -0
  36. package/dist/modules/xiaohongshu/app/src/blocks/ReplyInteractBlock.js +150 -37
  37. package/dist/modules/xiaohongshu/app/src/blocks/SmartReplyBlock.js +491 -0
  38. package/modules/camo-backend/src/index.ts +31 -0
  39. package/modules/camo-backend/src/internal/BrowserSession.ts +224 -53
  40. package/modules/camo-backend/src/internal/engine-manager.ts +14 -15
  41. package/modules/camo-backend/src/internal/ws-server.ts +17 -17
  42. package/modules/camo-runtime/src/autoscript/action-providers/xhs/common.mjs +12 -2
  43. package/modules/camo-runtime/src/autoscript/action-providers/xhs/persistence.mjs +57 -0
  44. package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +2475 -243
  45. package/modules/camo-runtime/src/autoscript/runtime.mjs +35 -30
  46. package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +80 -443
  47. package/modules/camo-runtime/src/container/runtime-core/checkpoint.mjs +39 -6
  48. package/modules/camo-runtime/src/container/runtime-core/operations/index.mjs +206 -39
  49. package/modules/camo-runtime/src/container/runtime-core/operations/tab-pool.mjs +0 -79
  50. package/modules/camo-runtime/src/container/runtime-core/operations/viewport.mjs +46 -0
  51. package/modules/camo-runtime/src/utils/browser-service.mjs +41 -6
  52. package/modules/camo-runtime/src/utils/js-policy.mjs +28 -0
  53. package/modules/workflow/blocks/EnsureSession.ts +0 -4
  54. package/modules/workflow/blocks/WeiboCollectFromLinksBlock.ts +81 -6
  55. package/modules/workflow/blocks/WeiboCollectSearchLinksBlock.ts +316 -0
  56. package/modules/workflow/definitions/weibo-search-workflow-v1.ts +2 -0
  57. package/modules/workflow/src/runner.ts +2 -0
  58. package/modules/xiaohongshu/app/src/blocks/ReplyInteractBlock.ts +198 -53
  59. package/modules/xiaohongshu/app/src/blocks/SmartReplyBlock.ts +706 -0
  60. package/package.json +2 -2
  61. package/modules/camo-runtime/src/autoscript/action-providers/xhs/comments.mjs +0 -498
  62. package/modules/camo-runtime/src/autoscript/action-providers/xhs/detail.mjs +0 -181
  63. package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +0 -691
  64. package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +0 -388
  65. package/modules/camo-runtime/src/container/runtime-core/operations/selector-scripts.mjs +0 -135
package/README.md CHANGED
@@ -1,137 +1,206 @@
1
1
  # @web-auto/webauto
2
2
 
3
- Windows 优先的 WebAuto CLI + Desktop UI 使用说明。
3
+ Windows 优先的 WebAuto CLI + Desktop UI 使用说明(面向直接安装使用)。
4
4
 
5
- ## 1. 安装(Windows)
5
+ ## 1. 安装
6
6
 
7
7
  要求:
8
8
  - Node.js 18+(建议 20+)
9
9
  - npm 可用
10
10
 
11
- 全局安装:
12
-
13
11
  ```bat
14
12
  npm install -g @web-auto/webauto
13
+ webauto --help
15
14
  ```
16
15
 
17
- 验证:
16
+ ## 2. 首次启动(推荐路径)
17
+
18
+ 直接启动 UI:
18
19
 
19
20
  ```bat
20
- webauto --help
21
+ webauto ui console
22
+ ```
23
+
24
+ 也可以用 UI CLI 自动拉起(适合脚本/远程):
25
+
26
+ ```bat
27
+ webauto ui cli start --json
28
+ webauto ui cli status --json
21
29
  ```
22
30
 
23
- ## 2. 直接启动 UI
31
+ 说明:
32
+ - `webauto` 默认会自动设置运行根目录,不需要手工设置 `WEBAUTO_REPO_ROOT`。
33
+ - 首次运行会按需准备运行依赖(Electron/服务进程)。
34
+
35
+ ## 3. Windows 默认目录规则
36
+
37
+ 未设置环境变量时:
38
+ - 如果存在 `D:` 盘:默认使用 `D:\webauto`
39
+ - 否则:默认使用 `%USERPROFILE%\.webauto`
24
40
 
25
- 最直接方式:
41
+ 可选覆盖(仅在你需要自定义目录时):
42
+ - `WEBAUTO_HOME`(推荐)
43
+ - `WEBAUTO_ROOT` / `WEBAUTO_PORTABLE_ROOT`(兼容旧变量)
44
+
45
+ PowerShell 示例:
46
+
47
+ ```powershell
48
+ $env:WEBAUTO_HOME = 'E:\my-webauto'
49
+ webauto ui console
50
+ ```
51
+
52
+ CMD 示例:
26
53
 
27
54
  ```bat
55
+ set WEBAUTO_HOME=E:\my-webauto
28
56
  webauto ui console
29
57
  ```
30
58
 
31
- 或用 UI CLI 自动拉起(适合脚本化):
59
+ ## 4. UI 常用流程(推荐人机流程)
60
+
61
+ 1. 启动 UI:`webauto ui console`
62
+ 2. 在任务页填写关键词、目标数、账号等参数
63
+ 3. 点“保存并执行”或“执行”
64
+ 4. 用 UI CLI 查询当前状态(可选)
32
65
 
33
66
  ```bat
34
- webauto ui cli start --json
35
67
  webauto ui cli status --json
68
+ webauto xhs status --json
36
69
  ```
37
70
 
38
71
  说明:
39
- - 首次启动会自动准备 Desktop runtime 依赖(Electron),不需要手工安装。
40
- - 启动成功后,UI CLI bridge 默认端口是 `7716`。
72
+ - `ui cli status`:轻量健康/状态查询(适合轮询)
73
+ - `ui cli snapshot`:完整 UI 快照(字段更全,开销更大)
41
74
 
42
- ## 3. UI CLI 常用命令(模拟真实 UI 操作)
75
+ ## 5. UI CLI 操作示例(模拟真实 UI 操作)
43
76
 
44
77
  ```bat
45
- webauto ui cli --help
78
+ :: 启动并确认 UI
79
+ webauto ui cli start --json
80
+ webauto ui cli status --json
46
81
 
47
- :: 切换到任务页
82
+ :: 切到任务 tab
48
83
  webauto ui cli tab --tab tasks --json
49
84
 
50
- :: 输入任务参数
85
+ :: 输入参数
51
86
  webauto ui cli input --selector "#task-keyword" --value "deepseek" --json
52
87
  webauto ui cli input --selector "#task-target" --value "20" --json
53
88
 
54
- :: 点击执行
55
- webauto ui cli click --selector "#task-run-ephemeral-btn" --json
89
+ :: 触发执行(按你的页面按钮 selector)
90
+ webauto ui cli click --selector "#task-run-btn" --json
91
+
92
+ :: 等待 runId 出现
93
+ webauto ui cli wait --selector "#run-id-text" --state exists --timeout 20000 --json
56
94
 
57
- :: 读取当前 UI 快照
95
+ :: 取轻量状态 / 完整快照
58
96
  webauto ui cli status --json
59
97
  webauto ui cli snapshot --json
98
+ ```
60
99
 
61
- :: 元素探测
62
- webauto ui cli probe --selector "#task-likes" --json
100
+ ## 6. 账号与任务命令(CLI)
101
+
102
+ ```bat
103
+ :: 账号
104
+ webauto account list
105
+ webauto account login xhs-0001 --url https://www.xiaohongshu.com
106
+ webauto account sync-alias xhs-0001
107
+
108
+ :: 调度任务
109
+ webauto schedule list
110
+ webauto schedule run <taskId>
63
111
  ```
64
112
 
65
- 任务状态查看(后端状态):
113
+ ## 7. XHS 运行前初始化
114
+
115
+ 建议先检查/准备依赖:
66
116
 
67
117
  ```bat
68
- webauto xhs status --json
69
- webauto xhs status --run-id <runId> --json
118
+ webauto xhs install --check --json
119
+ webauto xhs install --download-browser --json
120
+ webauto xhs install --download-geoip --json
121
+ webauto xhs install --ensure-backend --json
70
122
  ```
71
123
 
72
- ## 4. Windows 路径与设置
124
+ ## 8. XHS 任务执行与状态
73
125
 
74
- 默认数据目录(未设置环境变量时):
75
- - 有 `D:` 盘:`D:\webauto`
76
- - 无 `D:` 盘:`%USERPROFILE%\.webauto`
126
+ 完整采集(搜索 + 评论 + 点赞):
77
127
 
78
- 可选环境变量:
79
- - `WEBAUTO_HOME`:显式指定数据根目录(推荐)
80
- - `WEBAUTO_ROOT` / `WEBAUTO_PORTABLE_ROOT`:兼容旧变量(会归一化到 `.webauto`)
128
+ ```bat
129
+ webauto xhs unified --profile xiaohongshu-batch-1 --keyword "deepseek" --max-notes 200 --do-comments true --persist-comments true --do-likes true --like-keywords "太强了,真不错" --match-mode any --match-min-hits 1 --max-likes 10 --env debug --tab-count 4
130
+ ```
81
131
 
82
- PowerShell 设置示例:
132
+ 仅查状态:
83
133
 
84
- ```powershell
85
- $env:WEBAUTO_HOME = "D:\webauto"
86
- webauto ui console
134
+ ```bat
135
+ webauto xhs status --json
136
+ webauto xhs status --run-id <runId> --json
87
137
  ```
88
138
 
89
- CMD 设置示例:
139
+ ## 9. 流控 Gate(按平台隔离)
140
+
141
+ 默认会使用平台 gate 参数控制节奏(含随机区间),你可以在线修改并立即生效。
90
142
 
91
143
  ```bat
92
- set WEBAUTO_HOME=D:\webauto
93
- webauto ui console
144
+ webauto xhs gate get --platform xiaohongshu --json
145
+ webauto xhs gate set --platform xiaohongshu --patch-json "{\"noteInterval\":{\"minMs\":2600,\"maxMs\":5200}}" --json
146
+ webauto xhs gate reset --platform xiaohongshu --json
94
147
  ```
95
148
 
96
- 不设置也可以直接用,系统会按默认规则落盘。
149
+ ## 10. 输出与日志
150
+
151
+ 常见目录(以默认目录为例):
152
+ - 数据根:`D:\webauto` 或 `%USERPROFILE%\.webauto`
153
+ - 采集输出:`<WEBAUTO_HOME>\download\xiaohongshu\<env>\<keyword>\`
154
+ - 运行日志:`<WEBAUTO_HOME>\logs\`
97
155
 
98
- ## 5. 首次安装常见问题
156
+ 典型文件:
157
+ - `comments.jsonl`
158
+ - `like-evidence\<noteId>\summary-*.json`
159
+ - `run.log` / `run-events.jsonl`
99
160
 
100
- ### 5.1 `Lock file can not be created` / 启动失败
161
+ ## 11. 常见问题排查
101
162
 
102
- 说明:通常是残留的 Electron/Node 进程占用。
163
+ ### 11.1 UI 启动报错 `Lock file can not be created`
164
+
165
+ 通常是残留进程占用:
103
166
 
104
167
  ```bat
105
168
  taskkill /F /IM electron.exe /T
106
169
  taskkill /F /IM node.exe /T
107
- webauto ui console
170
+ webauto ui console --no-daemon
108
171
  ```
109
172
 
110
- ### 5.2 `ui cli fetch failed`
173
+ ### 11.2 `ui cli fetch failed`
111
174
 
112
- 先确认 UI 已启动并就绪:
175
+ 先确认 UI 已启动,再查状态:
113
176
 
114
177
  ```bat
115
178
  webauto ui cli start --json
116
179
  webauto ui cli status --json
117
180
  ```
118
181
 
119
- 若仍失败,前台启动看日志:
182
+ 如果仍失败,前台模式查看实时日志:
120
183
 
121
184
  ```bat
122
185
  webauto ui console --no-daemon
123
186
  ```
124
187
 
125
- ## 6. 资源检查/安装
188
+ ### 11.3 旧账号/Profile 看不到
189
+
190
+ 先确认当前数据根目录是否与历史目录一致。
191
+ - 若历史数据在其它目录,可临时设置 `WEBAUTO_HOME` 指向旧目录再启动。
192
+ - 或把旧目录下的 `profiles/`、`cookies/` 等迁移到当前数据根后再启动。
193
+
194
+ ## 12. 升级与版本确认
126
195
 
127
196
  ```bat
128
- webauto xhs install --check --json
129
- webauto xhs install --download-browser --json
130
- webauto xhs install --download-geoip --json
197
+ npm install -g @web-auto/webauto@latest
198
+ webauto version
199
+ webauto version --json
131
200
  ```
132
201
 
133
- ## 7. 开发者(仓库模式)
202
+ ## 13. 开发者文档
134
203
 
135
- 在仓库中开发请看:
204
+ 如果你在仓库模式下开发,请看:
136
205
  - `apps/desktop-console/README.md`
137
-
206
+ - `AGENTS.md`
@@ -922,6 +922,7 @@ var DEFAULT_PORT = 7716;
922
922
  var DEFAULT_SNAPSHOT_TIMEOUT_MS = 35e3;
923
923
  var DEFAULT_ACTION_TIMEOUT_MS = 3e4;
924
924
  var DEFAULT_WAIT_PROBE_TIMEOUT_MS = 3e3;
925
+ var UI_CLI_ACTION_LOG_FILE = path5.join(resolveWebautoRoot2(), "logs", "ui-cli-actions.jsonl");
925
926
  function normalizePathForPlatform(raw, platform = process.platform) {
926
927
  const input = String(raw || "").trim();
927
928
  const isWinPath = platform === "win32" || /^[A-Za-z]:[\\/]/.test(input);
@@ -997,6 +998,45 @@ function toActionError(input, error, extra = {}) {
997
998
  };
998
999
  return payload;
999
1000
  }
1001
+ function clipText(value, maxLen = 220) {
1002
+ const text = String(value ?? "");
1003
+ if (text.length <= maxLen) return text;
1004
+ return `${text.slice(0, Math.max(0, maxLen - 3))}...`;
1005
+ }
1006
+ function summarizeAction(input) {
1007
+ const rawClient = input?._client;
1008
+ const client = rawClient && typeof rawClient === "object" ? {
1009
+ client: clipText(rawClient.client ?? "", 80) || null,
1010
+ cmd: clipText(rawClient.cmd ?? "", 80) || null,
1011
+ pid: Number.isFinite(Number(rawClient.pid)) ? Math.floor(Number(rawClient.pid)) : null,
1012
+ ppid: Number.isFinite(Number(rawClient.ppid)) ? Math.floor(Number(rawClient.ppid)) : null
1013
+ } : null;
1014
+ return {
1015
+ action: String(input?.action || "").trim() || null,
1016
+ selector: String(input?.selector || "").trim() || null,
1017
+ tabId: String(input?.tabId || "").trim() || null,
1018
+ tabLabel: String(input?.tabLabel || "").trim() || null,
1019
+ state: String(input?.state || "").trim() || null,
1020
+ key: String(input?.key || "").trim() || null,
1021
+ text: clipText(input?.text ?? "", 160) || null,
1022
+ value: clipText(input?.value ?? "", 160) || null,
1023
+ timeoutMs: readInt(input?.timeoutMs, 0) || null,
1024
+ intervalMs: readInt(input?.intervalMs, 0) || null,
1025
+ nth: Number.isFinite(Number(input?.nth)) ? Math.floor(Number(input?.nth)) : null,
1026
+ exact: input?.exact === true ? true : null,
1027
+ detailed: input?.detailed === true ? true : null,
1028
+ client
1029
+ };
1030
+ }
1031
+ async function appendActionLog(entry) {
1032
+ const payload = { ts: (/* @__PURE__ */ new Date()).toISOString(), ...entry };
1033
+ try {
1034
+ await fs3.mkdir(path5.dirname(UI_CLI_ACTION_LOG_FILE), { recursive: true });
1035
+ await fs3.appendFile(UI_CLI_ACTION_LOG_FILE, `${JSON.stringify(payload)}
1036
+ `, "utf8");
1037
+ } catch {
1038
+ }
1039
+ }
1000
1040
  async function withTimeout(promise, timeoutMs, label) {
1001
1041
  const ms = readInt(timeoutMs, 0);
1002
1042
  if (ms <= 0) return promise;
@@ -1382,7 +1422,26 @@ var UiCliBridge = class {
1382
1422
  }
1383
1423
  if (method === "POST" && url.pathname === "/action") {
1384
1424
  const body = await parseBody(req);
1425
+ const actionId = `act-${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`;
1426
+ const startedAt = Date.now();
1427
+ await appendActionLog({
1428
+ event: "action.request",
1429
+ actionId,
1430
+ method,
1431
+ path: url.pathname,
1432
+ remoteAddress: req.socket?.remoteAddress || null,
1433
+ remotePort: Number.isFinite(Number(req.socket?.remotePort)) ? Number(req.socket?.remotePort) : null,
1434
+ userAgent: clipText(req.headers?.["user-agent"] || "", 160) || null,
1435
+ payload: summarizeAction(body || {})
1436
+ });
1385
1437
  const result = await this.handleAction(body || {});
1438
+ await appendActionLog({
1439
+ event: "action.response",
1440
+ actionId,
1441
+ elapsedMs: Date.now() - startedAt,
1442
+ ok: result?.ok === true,
1443
+ error: result?.ok === true ? null : String(result?.error || "action_failed")
1444
+ });
1386
1445
  return sendJson(res, result.ok ? 200 : 400, result);
1387
1446
  }
1388
1447
  return sendJson(res, 404, { ok: false, error: "not_found" });
@@ -1434,6 +1493,9 @@ var UiCliBridge = class {
1434
1493
  async handleAction(input) {
1435
1494
  const action = String(input?.action || "").trim();
1436
1495
  if (!action) return toActionError(input, "missing_action");
1496
+ if (action === "restart") {
1497
+ return this.handleRestart(input);
1498
+ }
1437
1499
  if (action === "wait") {
1438
1500
  return this.waitForSelector(input);
1439
1501
  }
@@ -1451,6 +1513,22 @@ var UiCliBridge = class {
1451
1513
  return toActionError(input, err?.message || String(err), { details: err?.stack || null });
1452
1514
  }
1453
1515
  }
1516
+ async handleRestart(input) {
1517
+ const onRestart = this.options.onRestart;
1518
+ if (typeof onRestart !== "function") {
1519
+ return toActionError(input, "restart_not_supported");
1520
+ }
1521
+ const reason = String(input?.reason || input?.value || "").trim() || "ui_cli";
1522
+ try {
1523
+ const out = await Promise.resolve(onRestart({ reason, source: "ui_cli_bridge" }));
1524
+ if (out && typeof out === "object") {
1525
+ return { ok: true, restarting: true, reason, ...out };
1526
+ }
1527
+ return { ok: true, restarting: true, reason };
1528
+ } catch (err) {
1529
+ return toActionError(input, err?.message || String(err), { details: err?.stack || null });
1530
+ }
1531
+ }
1454
1532
  async waitForSelector(input) {
1455
1533
  const selector = String(input.selector || "").trim();
1456
1534
  if (!selector) return toActionError(input, "missing_selector");
@@ -1696,6 +1774,25 @@ async function scheduleInvoke(options, input) {
1696
1774
  if (action === "run") {
1697
1775
  const taskId = asText(input?.taskId);
1698
1776
  if (!taskId) return { ok: false, error: "missing taskId" };
1777
+ if (input?.background === true) {
1778
+ const script = path6.join(options.repoRoot, "apps", "webauto", "entry", "schedule.mjs");
1779
+ const ret = await options.spawnCommand({
1780
+ title: `schedule run ${taskId}`,
1781
+ cwd: options.repoRoot,
1782
+ args: [script, "run", taskId, "--json"],
1783
+ groupKey: `schedule-run:${taskId}`
1784
+ });
1785
+ const runId = asText(ret?.runId);
1786
+ return {
1787
+ ok: true,
1788
+ json: {
1789
+ ok: true,
1790
+ background: true,
1791
+ runId: runId || null,
1792
+ runResult: runId ? { runId } : {}
1793
+ }
1794
+ };
1795
+ }
1699
1796
  return runScheduleJson(options, ["run", taskId], timeoutMs);
1700
1797
  }
1701
1798
  if (action === "delete") {
@@ -1770,6 +1867,8 @@ async function runEphemeralTask(options, input) {
1770
1867
  String(asBool(argv["do-comments"], true)),
1771
1868
  "--fetch-body",
1772
1869
  String(asBool(argv["fetch-body"], true)),
1870
+ "--service-reset",
1871
+ String(asBool(argv["service-reset"], false)),
1773
1872
  "--do-likes",
1774
1873
  String(asBool(argv["do-likes"], false)),
1775
1874
  "--like-keywords",
@@ -1815,8 +1914,15 @@ function trackBrowserProcess(pid) {
1815
1914
  spawnedBrowserProcesses.add(pid);
1816
1915
  }
1817
1916
  }
1917
+ function safeConsole(method, ...args) {
1918
+ try {
1919
+ const fn = console[method];
1920
+ if (typeof fn === "function") fn(...args);
1921
+ } catch {
1922
+ }
1923
+ }
1818
1924
  function cleanupAllBrowserProcesses(reason = "ui_close") {
1819
- console.log(`[process-cleanup] Cleaning up ${spawnedBrowserProcesses.size} browser process(s) (${reason})`);
1925
+ safeConsole("log", `[process-cleanup] Cleaning up ${spawnedBrowserProcesses.size} browser process(s) (${reason})`);
1820
1926
  for (const pid of spawnedBrowserProcesses) {
1821
1927
  try {
1822
1928
  if (process.platform === "win32") {
@@ -1825,11 +1931,11 @@ function cleanupAllBrowserProcesses(reason = "ui_close") {
1825
1931
  process.kill(pid, "SIGTERM");
1826
1932
  }
1827
1933
  } catch (err) {
1828
- console.warn(`[process-cleanup] Failed to kill PID ${pid}:`, err);
1934
+ safeConsole("warn", `[process-cleanup] Failed to kill PID ${pid}:`, err);
1829
1935
  }
1830
1936
  }
1831
1937
  spawnedBrowserProcesses.clear();
1832
- console.log(`[process-cleanup] Cleanup complete`);
1938
+ safeConsole("log", "[process-cleanup] Cleanup complete");
1833
1939
  }
1834
1940
  var __dirname = path7.dirname(fileURLToPath2(import.meta.url));
1835
1941
  var APP_ROOT = path7.resolve(__dirname, "../..");
@@ -1856,6 +1962,12 @@ var DESKTOP_HEARTBEAT_FILE = path7.join(
1856
1962
  "run",
1857
1963
  "desktop-console-heartbeat.json"
1858
1964
  );
1965
+ var DESKTOP_LIFECYCLE_LOG_FILE = path7.join(
1966
+ os5.homedir(),
1967
+ ".webauto",
1968
+ "logs",
1969
+ "desktop-lifecycle.jsonl"
1970
+ );
1859
1971
  var profileStore = createProfileStore({ repoRoot: REPO_ROOT2 });
1860
1972
  var XHS_SCRIPTS_ROOT = path7.join(REPO_ROOT2, "scripts", "xiaohongshu");
1861
1973
  var XHS_FULL_COLLECT_RE = /collect-content\.mjs$/;
@@ -1897,6 +2009,20 @@ function configureElectronPaths() {
1897
2009
  function now() {
1898
2010
  return Date.now();
1899
2011
  }
2012
+ async function appendDesktopLifecycle(event, extra = {}) {
2013
+ const payload = {
2014
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2015
+ event: String(event || "unknown").trim() || "unknown",
2016
+ pid: process.pid,
2017
+ ...extra
2018
+ };
2019
+ try {
2020
+ await fs4.mkdir(path7.dirname(DESKTOP_LIFECYCLE_LOG_FILE), { recursive: true });
2021
+ await fs4.appendFile(DESKTOP_LIFECYCLE_LOG_FILE, `${JSON.stringify(payload)}
2022
+ `, "utf8");
2023
+ } catch {
2024
+ }
2025
+ }
1900
2026
  var GroupQueue = class {
1901
2027
  running = false;
1902
2028
  queue = [];
@@ -1955,6 +2081,7 @@ var heartbeatTimeoutHandled = false;
1955
2081
  var coreServicesStopRequested = false;
1956
2082
  var coreServiceHeartbeatTimer = null;
1957
2083
  var coreServiceHeartbeatStopped = false;
2084
+ var restartRequested = false;
1958
2085
  var RUN_LOG_DIR = path7.join(os5.homedir(), ".webauto", "logs");
1959
2086
  function appendRunLog(runId, line) {
1960
2087
  const rid = String(runId || "").trim();
@@ -2012,16 +2139,55 @@ function ensureStateBridge() {
2012
2139
  }
2013
2140
  }
2014
2141
  var win = null;
2015
- var uiCliBridge = new UiCliBridge({ getWindow: getWin });
2142
+ var uiCliBridge = new UiCliBridge({
2143
+ getWindow: getWin,
2144
+ onRestart: ({ reason }) => requestAppRestart(reason)
2145
+ });
2016
2146
  configureElectronPaths();
2017
2147
  var singleInstanceLock = app.requestSingleInstanceLock();
2018
2148
  if (!singleInstanceLock) {
2149
+ void appendDesktopLifecycle("single_instance_lock_failed");
2019
2150
  app.quit();
2020
2151
  }
2021
2152
  function getWin() {
2022
2153
  if (!win || win.isDestroyed()) return null;
2023
2154
  return win;
2024
2155
  }
2156
+ function requestAppRestart(reason = "ui_cli") {
2157
+ const normalizedReason = String(reason || "").trim() || "ui_cli";
2158
+ if (restartRequested) {
2159
+ return { accepted: false, reason: normalizedReason };
2160
+ }
2161
+ restartRequested = true;
2162
+ void appendDesktopLifecycle("restart_requested", { reason: normalizedReason });
2163
+ console.warn(`[desktop-console] restart requested: ${normalizedReason}`);
2164
+ const w = getWin();
2165
+ if (w) {
2166
+ try {
2167
+ w.webContents.send("app:restart-requested", {
2168
+ reason: normalizedReason,
2169
+ ts: (/* @__PURE__ */ new Date()).toISOString()
2170
+ });
2171
+ } catch {
2172
+ }
2173
+ }
2174
+ setTimeout(() => {
2175
+ void Promise.race([
2176
+ ensureAppExitCleanup(`restart:${normalizedReason}`, { stopStateBridge: true }),
2177
+ sleep2(1500)
2178
+ ]).catch(() => null).finally(() => {
2179
+ try {
2180
+ app.relaunch();
2181
+ } catch (err) {
2182
+ restartRequested = false;
2183
+ console.error("[desktop-console] relaunch failed", err);
2184
+ return;
2185
+ }
2186
+ app.exit(0);
2187
+ });
2188
+ }, 25);
2189
+ return { accepted: true, reason: normalizedReason };
2190
+ }
2025
2191
  if (singleInstanceLock) {
2026
2192
  app.on("second-instance", () => {
2027
2193
  const w = getWin();
@@ -2469,7 +2635,8 @@ async function spawnCommand(spec) {
2469
2635
  return { runId };
2470
2636
  }
2471
2637
  async function runJson(spec) {
2472
- const timeoutMs = typeof spec.timeoutMs === "number" ? spec.timeoutMs : 2e4;
2638
+ const timeoutRaw = Number(spec.timeoutMs);
2639
+ const timeoutMs = Number.isFinite(timeoutRaw) ? Math.floor(timeoutRaw) : 2e4;
2473
2640
  const cwd = resolveCwd(spec.cwd);
2474
2641
  const child = spawn2(resolveNodeBin2(), spec.args, {
2475
2642
  cwd,
@@ -2481,16 +2648,19 @@ async function runJson(spec) {
2481
2648
  const stderr = [];
2482
2649
  child.stdout?.on("data", (c) => stdout.push(c));
2483
2650
  child.stderr?.on("data", (c) => stderr.push(c));
2484
- const timer = setTimeout(() => {
2485
- try {
2486
- child.kill("SIGTERM");
2487
- } catch {
2488
- }
2489
- }, timeoutMs);
2651
+ let timer = null;
2652
+ if (timeoutMs > 0) {
2653
+ timer = setTimeout(() => {
2654
+ try {
2655
+ child.kill("SIGTERM");
2656
+ } catch {
2657
+ }
2658
+ }, timeoutMs);
2659
+ }
2490
2660
  const { code } = await new Promise((resolve) => {
2491
2661
  child.on("exit", (c) => resolve({ code: c }));
2492
2662
  });
2493
- clearTimeout(timer);
2663
+ if (timer) clearTimeout(timer);
2494
2664
  const out = Buffer.concat(stdout).toString("utf8").trim();
2495
2665
  const err = Buffer.concat(stderr).toString("utf8").trim();
2496
2666
  if (code !== 0) {
@@ -2756,20 +2926,58 @@ function createWindow() {
2756
2926
  }
2757
2927
  });
2758
2928
  const htmlPath = path7.join(APP_ROOT, "dist", "renderer", "index.html");
2929
+ void appendDesktopLifecycle("window_created", {
2930
+ width: win.getBounds().width,
2931
+ height: win.getBounds().height,
2932
+ title: VERSION_INFO.windowTitle
2933
+ });
2934
+ win.on("close", () => {
2935
+ void appendDesktopLifecycle("window_close");
2936
+ });
2937
+ win.on("closed", () => {
2938
+ void appendDesktopLifecycle("window_closed");
2939
+ });
2940
+ win.on("unresponsive", () => {
2941
+ void appendDesktopLifecycle("window_unresponsive");
2942
+ });
2943
+ win.webContents.on("render-process-gone", (_event, details) => {
2944
+ void appendDesktopLifecycle("render_process_gone", {
2945
+ reason: String(details?.reason || "").trim() || null,
2946
+ exitCode: Number.isFinite(Number(details?.exitCode)) ? Number(details?.exitCode) : null
2947
+ });
2948
+ });
2949
+ win.webContents.on("did-fail-load", (_event, code, desc, validatedURL, isMainFrame) => {
2950
+ void appendDesktopLifecycle("did_fail_load", {
2951
+ code,
2952
+ desc: String(desc || ""),
2953
+ url: String(validatedURL || ""),
2954
+ isMainFrame: Boolean(isMainFrame)
2955
+ });
2956
+ });
2759
2957
  void win.loadFile(htmlPath);
2760
2958
  ensureStateBridge();
2761
2959
  }
2762
2960
  app.on("window-all-closed", () => {
2961
+ void appendDesktopLifecycle("window_all_closed");
2763
2962
  void ensureAppExitCleanup("window_closed");
2764
2963
  app.quit();
2765
2964
  });
2766
2965
  app.on("before-quit", () => {
2966
+ void appendDesktopLifecycle("before_quit");
2767
2967
  void ensureAppExitCleanup("before_quit");
2768
2968
  });
2769
2969
  app.on("will-quit", () => {
2970
+ void appendDesktopLifecycle("will_quit");
2770
2971
  void ensureAppExitCleanup("will_quit", { stopStateBridge: true });
2771
2972
  });
2973
+ app.on("quit", (_evt, exitCode) => {
2974
+ void appendDesktopLifecycle("quit", { exitCode });
2975
+ });
2976
+ process.on("exit", (code) => {
2977
+ void appendDesktopLifecycle("process_exit", { code });
2978
+ });
2772
2979
  app.whenReady().then(async () => {
2980
+ void appendDesktopLifecycle("app_ready");
2773
2981
  startCoreServiceHeartbeat();
2774
2982
  const started = await startCoreDaemon().catch((err) => {
2775
2983
  console.error("[desktop-console] core services startup failed", err);
@@ -2786,12 +2994,19 @@ app.whenReady().then(async () => {
2786
2994
  createWindow();
2787
2995
  try {
2788
2996
  await uiCliBridge.start();
2997
+ void appendDesktopLifecycle("ui_cli_bridge_started");
2789
2998
  } catch (err) {
2999
+ void appendDesktopLifecycle("ui_cli_bridge_start_failed", {
3000
+ error: err?.message || String(err)
3001
+ });
2790
3002
  console.error("[desktop-console] ui-cli bridge start failed", err);
2791
3003
  await ensureAppExitCleanup("ui_cli_bridge_start_failed", { stopStateBridge: true }).catch(() => null);
2792
3004
  app.exit(1);
2793
3005
  }
2794
3006
  }).catch(async (err) => {
3007
+ void appendDesktopLifecycle("app_startup_exception", {
3008
+ error: err?.message || String(err)
3009
+ });
2795
3010
  console.error("[desktop-console] fatal startup error", err);
2796
3011
  await ensureAppExitCleanup("startup_exception", { stopStateBridge: true }).catch(() => null);
2797
3012
  app.exit(1);