@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
@@ -130,6 +130,25 @@ function parseIsoTs(value) {
130
130
  return Number.isFinite(ts) ? ts : null;
131
131
  }
132
132
 
133
+ function parsePid(value) {
134
+ const n = Number(value);
135
+ if (!Number.isFinite(n) || n <= 0) return null;
136
+ return Math.floor(n);
137
+ }
138
+
139
+ function isPidAlive(pid) {
140
+ if (!Number.isFinite(pid) || pid <= 0) return true;
141
+ try {
142
+ process.kill(pid, 0);
143
+ return true;
144
+ } catch (error) {
145
+ const code = String(error?.code || '').toUpperCase();
146
+ if (code === 'ESRCH') return false;
147
+ // EPERM/UNKNOWN should be treated as alive to avoid false reclamation.
148
+ return true;
149
+ }
150
+ }
151
+
133
152
  function buildLeasePayload({ ownerId, runToken = null, leaseMs, meta = {}, nowMs = Date.now() }) {
134
153
  const ttl = Math.max(1_000, Math.floor(Number(leaseMs) || DEFAULT_TASK_LEASE_MS));
135
154
  const iso = new Date(nowMs).toISOString();
@@ -148,6 +167,14 @@ function isLeaseExpired(payload, nowMs = Date.now()) {
148
167
  return !Number.isFinite(expiryTs) || expiryTs <= nowMs;
149
168
  }
150
169
 
170
+ function isLeaseStale(payload, nowMs = Date.now()) {
171
+ if (!payload || typeof payload !== 'object') return true;
172
+ if (isLeaseExpired(payload, nowMs)) return true;
173
+ const pid = parsePid(payload?.pid);
174
+ if (!pid) return false;
175
+ return !isPidAlive(pid);
176
+ }
177
+
151
178
  function safeUnlink(filePath) {
152
179
  try {
153
180
  fs.unlinkSync(filePath);
@@ -209,7 +236,7 @@ function acquireLease(filePath, { ownerId, runToken = null, leaseMs, meta = {},
209
236
  return { ok: true, acquired: false, renewed: true, lease: renewed };
210
237
  }
211
238
 
212
- if (!isLeaseExpired(existing, nowMs)) {
239
+ if (!isLeaseStale(existing, nowMs)) {
213
240
  return { ok: false, reason: 'busy', lease: existing };
214
241
  }
215
242
 
@@ -250,7 +277,7 @@ function listActiveLeases(rootDir, nowMs = Date.now()) {
250
277
  if (!entry.isFile() || !entry.name.endsWith('.json')) continue;
251
278
  const fullPath = path.join(rootDir, entry.name);
252
279
  const payload = readLease(fullPath);
253
- if (!payload || isLeaseExpired(payload, nowMs)) {
280
+ if (!payload || isLeaseStale(payload, nowMs)) {
254
281
  safeUnlink(fullPath);
255
282
  continue;
256
283
  }
@@ -0,0 +1,227 @@
1
+ import { runCamo } from './camo-cli.mjs';
2
+
3
+ const START_WINDOW_MIN_WIDTH = 960;
4
+ const START_WINDOW_MIN_HEIGHT = 700;
5
+ const START_WINDOW_MAX_RESERVE = 240;
6
+
7
+ function sleep(ms) {
8
+ return new Promise((resolve) => setTimeout(resolve, ms));
9
+ }
10
+
11
+ function toNumber(value, fallback = 0) {
12
+ const num = Number(value);
13
+ return Number.isFinite(num) ? num : fallback;
14
+ }
15
+
16
+ function clamp(value, min, max) {
17
+ return Math.min(Math.max(value, min), max);
18
+ }
19
+
20
+ function extractResult(payload) {
21
+ if (payload && typeof payload === 'object') {
22
+ if (Object.prototype.hasOwnProperty.call(payload, 'result')) return payload.result;
23
+ if (Object.prototype.hasOwnProperty.call(payload, 'data')) return payload.data;
24
+ }
25
+ return payload || {};
26
+ }
27
+
28
+ async function callBrowserApi(action, args = {}) {
29
+ const { callAPI } = await import('../../../../modules/camo-runtime/src/utils/browser-service.mjs');
30
+ return callAPI(action, args);
31
+ }
32
+
33
+ async function resolveStartWindow(profileOptions = {}) {
34
+ try {
35
+ const displayPayload = await callBrowserApi('system:display', {});
36
+ return computeStartWindowSize(extractResult(displayPayload), profileOptions);
37
+ } catch {
38
+ return computeStartWindowSize(null, profileOptions);
39
+ }
40
+ }
41
+
42
+ export function computeStartWindowSize(metrics, options = {}) {
43
+ const display = metrics?.metrics || metrics || {};
44
+ const reserveFromEnv = toNumber(process.env.WEBAUTO_WINDOW_VERTICAL_RESERVE, 0);
45
+ const reserve = clamp(
46
+ toNumber(options.reservePx, reserveFromEnv),
47
+ 0,
48
+ START_WINDOW_MAX_RESERVE,
49
+ );
50
+ const workWidth = toNumber(display.workWidth, 0);
51
+ const workHeight = toNumber(display.workHeight, 0);
52
+ const width = toNumber(display.width, 0);
53
+ const height = toNumber(display.height, 0);
54
+ const baseW = Math.floor(workWidth > 0 ? workWidth : width);
55
+ const baseH = Math.floor(workHeight > 0 ? workHeight : height);
56
+ if (baseW <= 0 || baseH <= 0) {
57
+ return {
58
+ width: 1920,
59
+ height: 1000,
60
+ reservePx: reserve,
61
+ source: 'fallback',
62
+ };
63
+ }
64
+ return {
65
+ width: Math.max(START_WINDOW_MIN_WIDTH, baseW),
66
+ height: Math.max(START_WINDOW_MIN_HEIGHT, baseH - reserve),
67
+ reservePx: reserve,
68
+ source: workWidth > 0 || workHeight > 0 ? 'workArea' : 'screen',
69
+ };
70
+ }
71
+
72
+ export function computeTargetViewportFromWindowMetrics(measured) {
73
+ const innerWidth = Math.max(320, toNumber(measured?.innerWidth, 0));
74
+ const innerHeight = Math.max(240, toNumber(measured?.innerHeight, 0));
75
+ const outerWidth = Math.max(320, toNumber(measured?.outerWidth, innerWidth));
76
+ const outerHeight = Math.max(240, toNumber(measured?.outerHeight, innerHeight));
77
+ const rawDeltaW = Math.max(0, outerWidth - innerWidth);
78
+ const rawDeltaH = Math.max(0, outerHeight - innerHeight);
79
+ const frameW = rawDeltaW > 400 ? 16 : Math.min(rawDeltaW, 120);
80
+ const frameH = rawDeltaH > 400 ? 88 : Math.min(rawDeltaH, 180);
81
+ return {
82
+ width: Math.max(320, outerWidth - frameW),
83
+ height: Math.max(240, outerHeight - frameH),
84
+ frameW,
85
+ frameH,
86
+ };
87
+ }
88
+
89
+ async function probeWindowMetrics(profileId) {
90
+ const measured = await callBrowserApi('evaluate', {
91
+ profileId,
92
+ script: '({ innerWidth: window.innerWidth, innerHeight: window.innerHeight, outerWidth: window.outerWidth, outerHeight: window.outerHeight })',
93
+ });
94
+ return extractResult(measured);
95
+ }
96
+
97
+ export async function applyNearFullWindow(profileId, options = {}) {
98
+ const id = String(profileId || '').trim();
99
+ if (!id) {
100
+ return { ok: false, error: 'missing_profile_id' };
101
+ }
102
+ try {
103
+ const startWindow = await resolveStartWindow(options);
104
+ await callBrowserApi('window:resize', {
105
+ profileId: id,
106
+ width: startWindow.width,
107
+ height: startWindow.height,
108
+ });
109
+
110
+ let targetViewport = {
111
+ width: startWindow.width,
112
+ height: startWindow.height,
113
+ frameW: 0,
114
+ frameH: 0,
115
+ };
116
+ const attempts = Math.max(1, Math.min(4, Math.floor(toNumber(options.attempts, 2))));
117
+ for (let i = 0; i < attempts; i += 1) {
118
+ const measured = await probeWindowMetrics(id).catch(() => ({}));
119
+ targetViewport = computeTargetViewportFromWindowMetrics(measured);
120
+ await callBrowserApi('page:setViewport', {
121
+ profileId: id,
122
+ width: targetViewport.width,
123
+ height: targetViewport.height,
124
+ });
125
+ if (i + 1 < attempts) {
126
+ await sleep(Math.max(80, toNumber(options.settleMs, 180)));
127
+ }
128
+ }
129
+ return {
130
+ ok: true,
131
+ profileId: id,
132
+ startWindow,
133
+ targetViewport,
134
+ };
135
+ } catch (error) {
136
+ return {
137
+ ok: false,
138
+ profileId: id,
139
+ error: error?.message || String(error),
140
+ };
141
+ }
142
+ }
143
+
144
+ export async function ensureSessionInitialized(profileId, options = {}) {
145
+ const id = String(profileId || '').trim();
146
+ if (!id) {
147
+ return { ok: false, error: 'missing_profile_id' };
148
+ }
149
+ const headless = options.headless === true;
150
+ const url = String(options.url || '').trim();
151
+ const rootDir = String(options.rootDir || process.cwd()).trim();
152
+ const timeoutMs = Math.max(1000, Math.floor(toNumber(options.timeoutMs, 60000)));
153
+ const restartSession = options.restartSession !== false;
154
+ let stopRet = null;
155
+ if (restartSession) {
156
+ stopRet = runCamo(['stop', id], {
157
+ rootDir,
158
+ timeoutMs: Math.max(3000, Math.min(timeoutMs, 15000)),
159
+ });
160
+ }
161
+ const startWindow = headless
162
+ ? {
163
+ width: 1920,
164
+ height: 1000,
165
+ reservePx: 0,
166
+ source: 'headless-default',
167
+ }
168
+ : await resolveStartWindow({ reservePx: options.reservePx });
169
+ const startArgs = [
170
+ 'start',
171
+ id,
172
+ ];
173
+ if (headless) {
174
+ startArgs.push('--headless');
175
+ } else {
176
+ startArgs.push(
177
+ '--width',
178
+ String(startWindow.width),
179
+ '--height',
180
+ String(startWindow.height),
181
+ );
182
+ }
183
+ if (url) startArgs.push('--url', url);
184
+ const startRet = runCamo(startArgs, {
185
+ rootDir,
186
+ timeoutMs,
187
+ });
188
+ if (!startRet?.ok) {
189
+ return {
190
+ ok: false,
191
+ profileId: id,
192
+ error: startRet?.stderr || startRet?.stdout || 'camo start failed',
193
+ stop: stopRet,
194
+ start: startRet,
195
+ startWindow,
196
+ };
197
+ }
198
+ let gotoRet = null;
199
+ if (url) {
200
+ gotoRet = runCamo(['goto', id, url], {
201
+ rootDir,
202
+ timeoutMs,
203
+ });
204
+ }
205
+ const windowInit = headless
206
+ ? {
207
+ ok: true,
208
+ profileId: id,
209
+ skipped: true,
210
+ reason: 'headless_mode',
211
+ }
212
+ : await applyNearFullWindow(id, {
213
+ settleMs: options.settleMs,
214
+ attempts: options.attempts,
215
+ reservePx: options.reservePx,
216
+ });
217
+ return {
218
+ ok: true,
219
+ profileId: id,
220
+ headless,
221
+ stop: stopRet,
222
+ startWindow,
223
+ start: startRet,
224
+ goto: gotoRet,
225
+ windowInit,
226
+ };
227
+ }
@@ -0,0 +1,269 @@
1
+ /**
2
+ * upgrade-check.mjs
3
+ *
4
+ * 检查 npm 包是否有新版本,提示用户升级
5
+ */
6
+
7
+ import { spawn } from 'child_process';
8
+ import https from 'https';
9
+ import { createInterface } from 'readline';
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+ import { fileURLToPath } from 'url';
13
+
14
+ const NPM_REGISTRY = 'https://registry.npmjs.org';
15
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
+ const REPO_ROOT = path.resolve(__dirname, '../../../..');
17
+
18
+ /**
19
+ * 从 package.json 获取当前版本
20
+ * @param {string} [pkgPath] - 自定义 package.json 路径
21
+ * @returns {string|null}
22
+ */
23
+ function getLocalVersion(pkgPath) {
24
+ try {
25
+ // 优先使用传入路径
26
+ if (pkgPath && fs.existsSync(pkgPath)) {
27
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
28
+ return pkg.version || null;
29
+ }
30
+
31
+ // 尝试从已知位置查找
32
+ const candidates = [
33
+ path.join(REPO_ROOT, 'package.json'), // 开发模式
34
+ path.join(__dirname, '..', '..', '..', 'package.json'), // 相对位置
35
+ path.join(__dirname, 'package.json'), // 模块自身
36
+ ];
37
+
38
+ for (const candidate of candidates) {
39
+ if (fs.existsSync(candidate)) {
40
+ const pkg = JSON.parse(fs.readFileSync(candidate, 'utf8'));
41
+ if (pkg.name === '@web-auto/webauto' && pkg.version) {
42
+ return pkg.version;
43
+ }
44
+ }
45
+ }
46
+
47
+ return null;
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * 从 npm registry 获取最新版本
55
+ * @param {string} packageName
56
+ * @returns {Promise<string|null>}
57
+ */
58
+ function getLatestVersion(packageName) {
59
+ return new Promise((resolve) => {
60
+ const url = `${NPM_REGISTRY}/${packageName}/latest`;
61
+
62
+ https.get(url, { timeout: 10000 }, (res) => {
63
+ let data = '';
64
+
65
+ res.on('data', (chunk) => {
66
+ data += chunk;
67
+ });
68
+
69
+ res.on('end', () => {
70
+ try {
71
+ const json = JSON.parse(data);
72
+ resolve(json.version || null);
73
+ } catch {
74
+ resolve(null);
75
+ }
76
+ });
77
+ }).on('error', () => {
78
+ resolve(null);
79
+ }).on('timeout', () => {
80
+ resolve(null);
81
+ });
82
+ });
83
+ }
84
+
85
+ /**
86
+ * 比较版本号
87
+ * @param {string} v1
88
+ * @param {string} v2
89
+ * @returns {number} -1 if v1 < v2, 0 if equal, 1 if v1 > v2
90
+ */
91
+ function compareVersions(v1, v2) {
92
+ const parts1 = v1.split('.').map(Number);
93
+ const parts2 = v2.split('.').map(Number);
94
+
95
+ for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
96
+ const p1 = parts1[i] || 0;
97
+ const p2 = parts2[i] || 0;
98
+
99
+ if (p1 < p2) return -1;
100
+ if (p1 > p2) return 1;
101
+ }
102
+
103
+ return 0;
104
+ }
105
+
106
+ /**
107
+ * 询问用户是否升级
108
+ * @param {string} current
109
+ * @param {string} latest
110
+ * @returns {Promise<boolean>}
111
+ */
112
+ async function askUserConfirmation(current, latest) {
113
+ const rl = createInterface({
114
+ input: process.stdin,
115
+ output: process.stdout,
116
+ });
117
+
118
+ return new Promise((resolve) => {
119
+ rl.question(
120
+ `\n📦 发现新版本!\n 当前版本: ${current}\n 最新版本: ${latest}\n\n是否升级?(y/n): `,
121
+ (answer) => {
122
+ rl.close();
123
+ const normalized = answer.trim().toLowerCase();
124
+ resolve(normalized === 'y' || normalized === 'yes');
125
+ },
126
+ );
127
+ });
128
+ }
129
+
130
+ /**
131
+ * 执行 npm update
132
+ * @param {string} packageName
133
+ * @returns {Promise<{ok: boolean, error?: string}>}
134
+ */
135
+ async function runNpmUpdate(packageName) {
136
+ return new Promise((resolve) => {
137
+ console.log(`\n⏳ 正在更新 ${packageName}...`);
138
+
139
+ const child = spawn('npm', ['install', '-g', packageName], {
140
+ stdio: 'inherit',
141
+ });
142
+
143
+ child.on('error', (err) => {
144
+ resolve({ ok: false, error: err.message });
145
+ });
146
+
147
+ child.on('close', (code) => {
148
+ if (code === 0) {
149
+ console.log(`\n✅ ${packageName} 已更新到最新版本`);
150
+ resolve({ ok: true });
151
+ } else {
152
+ resolve({ ok: false, error: `npm install exited with code ${code}` });
153
+ }
154
+ });
155
+ });
156
+ }
157
+
158
+ /**
159
+ * 检查并提示升级
160
+ * @param {string} packageName - 包名
161
+ * @param {{autoUpdate?: boolean, silent?: boolean, pkgPath?: string}} options
162
+ * @returns {Promise<{ok: boolean, versionInfo?: any, error?: string, userDeclined?: boolean, updated?: boolean}>}
163
+ */
164
+ export async function checkAndPromptUpgrade(packageName, options = {}) {
165
+ const { autoUpdate = false, silent = false, pkgPath } = options;
166
+
167
+ // 获取当前版本
168
+ const current = getLocalVersion(pkgPath);
169
+ if (!current) {
170
+ return {
171
+ ok: false,
172
+ error: `无法获取 ${packageName} 的当前版本`,
173
+ };
174
+ }
175
+
176
+ // 获取最新版本
177
+ const latest = await getLatestVersion(packageName);
178
+ if (!latest) {
179
+ return {
180
+ ok: false,
181
+ error: `无法从 npm registry 获取最新版本`,
182
+ versionInfo: { current, latest: current, needsUpdate: false },
183
+ };
184
+ }
185
+
186
+ const needsUpdate = compareVersions(current, latest) < 0;
187
+
188
+ const versionInfo = { current, latest, needsUpdate };
189
+
190
+ // 无需更新
191
+ if (!needsUpdate) {
192
+ if (!silent) {
193
+ console.log(`✓ ${packageName} 已是最新版本 (${current})`);
194
+ }
195
+ return { ok: true, versionInfo };
196
+ }
197
+
198
+ // 需要更新
199
+ if (!silent) {
200
+ console.log(`\n┌${'─'.repeat(50)}┐`);
201
+ console.log(`│ 📦 ${packageName} 有新版本可用`.padEnd(50) + '│');
202
+ console.log(`│ 当前: ${current}`.padEnd(50) + '│');
203
+ console.log(`│ 最新: ${latest}`.padEnd(50) + '│');
204
+ console.log(`└${'─'.repeat(50)}┘`);
205
+ }
206
+
207
+ // 自动更新模式
208
+ if (autoUpdate) {
209
+ const result = await runNpmUpdate(packageName);
210
+ return {
211
+ ok: result.ok,
212
+ versionInfo,
213
+ updated: result.ok,
214
+ error: result.error,
215
+ };
216
+ }
217
+
218
+ // 交互式确认
219
+ if (process.stdin.isTTY) {
220
+ const confirmed = await askUserConfirmation(current, latest);
221
+
222
+ if (confirmed) {
223
+ const result = await runNpmUpdate(packageName);
224
+ return {
225
+ ok: result.ok,
226
+ versionInfo,
227
+ updated: result.ok,
228
+ error: result.error,
229
+ };
230
+ } else {
231
+ return {
232
+ ok: true,
233
+ versionInfo,
234
+ userDeclined: true,
235
+ };
236
+ }
237
+ }
238
+
239
+ // 非 TTY 环境,仅提示
240
+ return {
241
+ ok: true,
242
+ versionInfo,
243
+ userDeclined: true,
244
+ };
245
+ }
246
+
247
+ /**
248
+ * CLI 入口
249
+ */
250
+ export async function main() {
251
+ const packageName = '@web-auto/webauto';
252
+ const result = await checkAndPromptUpgrade(packageName);
253
+
254
+ if (result.error) {
255
+ console.error(`❌ 检查更新失败: ${result.error}`);
256
+ process.exit(1);
257
+ }
258
+
259
+ if (result.userDeclined) {
260
+ console.log('\n💡 稍后可以运行 `npm install -g @web-auto/webauto` 手动更新');
261
+ }
262
+
263
+ process.exit(0);
264
+ }
265
+
266
+ // 直接运行时执行
267
+ if (process.argv[1]?.includes('upgrade-check.mjs')) {
268
+ main().catch(console.error);
269
+ }
@@ -0,0 +1,160 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import { spawnSync } from 'node:child_process';
4
+
5
+ export function nowIso() {
6
+ return new Date().toISOString();
7
+ }
8
+
9
+ export function sleepMs(ms) {
10
+ const waitMs = Math.max(0, Number(ms) || 0);
11
+ if (waitMs <= 0) return Promise.resolve();
12
+ return new Promise((resolve) => setTimeout(resolve, waitMs));
13
+ }
14
+
15
+ export function formatRunLabel() {
16
+ return new Date().toISOString().replace(/[:.]/g, '-');
17
+ }
18
+
19
+ export function parseBool(value, fallback = false) {
20
+ if (value === undefined || value === null || value === '') return fallback;
21
+ if (typeof value === 'boolean') return value;
22
+ const text = String(value).trim().toLowerCase();
23
+ if (['1', 'true', 'yes', 'on'].includes(text)) return true;
24
+ if (['0', 'false', 'no', 'off'].includes(text)) return false;
25
+ return fallback;
26
+ }
27
+
28
+ export function parseIntFlag(value, fallback, min = 1) {
29
+ if (value === undefined || value === null || value === '') return fallback;
30
+ const num = Number(value);
31
+ if (!Number.isFinite(num)) return fallback;
32
+ return Math.max(min, Math.floor(num));
33
+ }
34
+
35
+ export function parseNonNegativeInt(value, fallback = 0) {
36
+ if (value === undefined || value === null || value === '') return fallback;
37
+ const num = Number(value);
38
+ if (!Number.isFinite(num)) return fallback;
39
+ return Math.max(0, Math.floor(num));
40
+ }
41
+
42
+ export function pickRandomInt(min, max) {
43
+ const floorMin = Math.max(0, Math.floor(Number(min) || 0));
44
+ const floorMax = Math.max(floorMin, Math.floor(Number(max) || 0));
45
+ if (floorMax <= floorMin) return floorMin;
46
+ return floorMin + Math.floor(Math.random() * (floorMax - floorMin + 1));
47
+ }
48
+
49
+ export function sanitizeForPath(name, fallback = 'unknown') {
50
+ const text = String(name || '').trim();
51
+ if (!text) return fallback;
52
+ const cleaned = text.replace(/[\\/:"*?<>|]+/g, '_').trim();
53
+ return cleaned || fallback;
54
+ }
55
+
56
+ function parseLastJson(stdout = '') {
57
+ const text = String(stdout || '').trim();
58
+ if (!text) return null;
59
+ try {
60
+ return JSON.parse(text);
61
+ } catch {
62
+ // fall through
63
+ }
64
+ const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
65
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
66
+ const line = lines[i];
67
+ if (!line.startsWith('{') && !line.startsWith('[')) continue;
68
+ try {
69
+ return JSON.parse(line);
70
+ } catch {
71
+ // ignore
72
+ }
73
+ }
74
+ return null;
75
+ }
76
+
77
+ function runWebautoCli(args, options = {}) {
78
+ const rootDir = String(options.rootDir || process.cwd()).trim() || process.cwd();
79
+ const timeoutMs = Math.max(1000, Number(options.timeoutMs) || 120000);
80
+ const scriptPath = path.join(rootDir, 'bin', 'webauto.mjs');
81
+ const ret = spawnSync(process.execPath, [scriptPath, ...args], {
82
+ cwd: rootDir,
83
+ env: { ...process.env, ...(options.env || {}) },
84
+ encoding: 'utf8',
85
+ timeout: timeoutMs,
86
+ windowsHide: true,
87
+ });
88
+ const stdout = String(ret.stdout || '').trim();
89
+ const stderr = String(ret.stderr || '').trim();
90
+ return {
91
+ ok: ret.status === 0,
92
+ code: ret.status,
93
+ stdout,
94
+ stderr,
95
+ json: parseLastJson(stdout),
96
+ };
97
+ }
98
+
99
+ export async function resetTaskServices(argv, options = {}) {
100
+ const enabled = parseBool(argv['service-reset'], true);
101
+ const rootDir = String(options.rootDir || process.cwd()).trim() || process.cwd();
102
+ const debugActionLogPath = String(options.debugActionLogPath || '').trim();
103
+ if (!enabled) {
104
+ return {
105
+ ok: true,
106
+ skipped: true,
107
+ reason: 'service_reset_disabled',
108
+ actionLogPath: null,
109
+ };
110
+ }
111
+ const envName = String(argv.env || 'prod').trim() || 'prod';
112
+ const debugMode = envName === 'debug';
113
+ const actionLogPath = debugMode
114
+ ? (debugActionLogPath || path.join(os.homedir(), '.webauto', 'logs', `input-actions-${Date.now()}.jsonl`))
115
+ : null;
116
+ const serviceEnv = debugMode
117
+ ? {
118
+ WEBAUTO_DEBUG_ACTION_JSONL: '1',
119
+ WEBAUTO_DEBUG_ACTION_LOG_PATH: actionLogPath,
120
+ }
121
+ : {};
122
+
123
+ const stopRet = runWebautoCli(['ui', 'cli', 'stop', '--json'], {
124
+ rootDir,
125
+ timeoutMs: 120000,
126
+ env: serviceEnv,
127
+ });
128
+ const startRet = runWebautoCli(['ui', 'cli', 'start', '--json'], {
129
+ rootDir,
130
+ timeoutMs: 240000,
131
+ env: serviceEnv,
132
+ });
133
+ if (!startRet.ok) {
134
+ throw new Error(`ui cli start failed: ${startRet.stderr || startRet.stdout || 'unknown error'}`);
135
+ }
136
+
137
+ let statusRet = null;
138
+ let ready = false;
139
+ for (let attempt = 1; attempt <= 8; attempt += 1) {
140
+ statusRet = runWebautoCli(['ui', 'cli', 'status', '--json'], {
141
+ rootDir,
142
+ timeoutMs: 60000,
143
+ env: serviceEnv,
144
+ });
145
+ ready = Boolean(statusRet?.ok && statusRet?.json?.ready === true);
146
+ if (ready) break;
147
+ await sleepMs(600);
148
+ }
149
+ if (!ready) {
150
+ throw new Error(`ui cli status not ready after restart: ${statusRet?.stderr || statusRet?.stdout || 'unknown error'}`);
151
+ }
152
+ return {
153
+ ok: true,
154
+ skipped: false,
155
+ actionLogPath,
156
+ stop: stopRet,
157
+ start: startRet,
158
+ status: statusRet,
159
+ };
160
+ }