@web-auto/webauto 0.1.8 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/apps/desktop-console/dist/main/index.mjs +909 -105
  2. package/apps/desktop-console/dist/main/preload.mjs +3 -0
  3. package/apps/desktop-console/dist/renderer/index.html +9 -1
  4. package/apps/desktop-console/dist/renderer/index.js +796 -331
  5. package/apps/desktop-console/entry/ui-cli.mjs +59 -9
  6. package/apps/desktop-console/entry/ui-console.mjs +8 -3
  7. package/apps/webauto/entry/account.mjs +70 -9
  8. package/apps/webauto/entry/lib/account-detect.mjs +106 -25
  9. package/apps/webauto/entry/lib/account-store.mjs +122 -35
  10. package/apps/webauto/entry/lib/profilepool.mjs +45 -13
  11. package/apps/webauto/entry/lib/schedule-store.mjs +1 -25
  12. package/apps/webauto/entry/profilepool.mjs +45 -3
  13. package/apps/webauto/entry/schedule.mjs +44 -2
  14. package/apps/webauto/entry/weibo-unified.mjs +2 -2
  15. package/apps/webauto/entry/xhs-install.mjs +248 -52
  16. package/apps/webauto/entry/xhs-unified.mjs +33 -6
  17. package/bin/webauto.mjs +137 -5
  18. package/dist/modules/camo-runtime/src/utils/browser-service.mjs +4 -0
  19. package/dist/services/unified-api/server.js +5 -0
  20. package/dist/services/unified-api/task-state.js +2 -0
  21. package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +142 -14
  22. package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +16 -1
  23. package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +104 -0
  24. package/modules/camo-runtime/src/autoscript/runtime.mjs +14 -4
  25. package/modules/camo-runtime/src/autoscript/schema.mjs +9 -0
  26. package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +9 -2
  27. package/modules/camo-runtime/src/container/runtime-core/checkpoint.mjs +107 -1
  28. package/modules/camo-runtime/src/container/runtime-core/subscription.mjs +24 -2
  29. package/modules/camo-runtime/src/utils/browser-service.mjs +4 -0
  30. package/package.json +7 -3
  31. package/runtime/infra/utils/README.md +13 -0
  32. package/runtime/infra/utils/scripts/README.md +0 -0
  33. package/runtime/infra/utils/scripts/development/eval-in-session.mjs +40 -0
  34. package/runtime/infra/utils/scripts/development/highlight-search-containers.mjs +35 -0
  35. package/runtime/infra/utils/scripts/service/kill-port.mjs +24 -0
  36. package/runtime/infra/utils/scripts/service/start-api.mjs +103 -0
  37. package/runtime/infra/utils/scripts/service/start-browser-service.mjs +173 -0
  38. package/runtime/infra/utils/scripts/service/stop-api.mjs +30 -0
  39. package/runtime/infra/utils/scripts/service/stop-browser-service.mjs +104 -0
  40. package/runtime/infra/utils/scripts/test-services.mjs +94 -0
  41. package/scripts/bump-version.mjs +120 -0
  42. package/services/unified-api/server.ts +4 -0
  43. package/services/unified-api/task-state.ts +5 -0
@@ -340,6 +340,7 @@ export class AutoscriptRunner {
340
340
  'sync_window_viewport',
341
341
  'verify_subscriptions',
342
342
  'xhs_submit_search',
343
+ 'xhs_assert_logged_in',
343
344
  'xhs_open_detail',
344
345
  'xhs_detail_harvest',
345
346
  'xhs_expand_replies',
@@ -359,10 +360,19 @@ export class AutoscriptRunner {
359
360
 
360
361
  resolveTimeoutMs(operation) {
361
362
  const pacing = this.resolvePacing(operation);
362
- const disableTimeout = Boolean(
363
- operation?.disableTimeout ?? this.script?.defaults?.disableTimeout,
364
- );
365
- if (disableTimeout) return 0;
363
+ const operationDisableTimeout = operation?.disableTimeout;
364
+ if (operationDisableTimeout === true) return 0;
365
+
366
+ const rawOperationTimeout = operation?.timeoutMs ?? operation?.pacing?.timeoutMs;
367
+ const hasOperationTimeout = Number.isFinite(Number(rawOperationTimeout))
368
+ && Number(rawOperationTimeout) > 0;
369
+ const defaultDisableTimeout = Boolean(this.script?.defaults?.disableTimeout);
370
+
371
+ // Keep default "no-timeout" mode, but allow operation-level timeout to opt in.
372
+ if (defaultDisableTimeout && operationDisableTimeout !== false && !hasOperationTimeout) {
373
+ return 0;
374
+ }
375
+
366
376
  if (pacing.timeoutMs === 0) return 0;
367
377
  if (Number.isFinite(pacing.timeoutMs) && pacing.timeoutMs > 0) return pacing.timeoutMs;
368
378
  return this.getDefaultTimeoutMs(operation);
@@ -126,10 +126,19 @@ function normalizeSubscription(item, index, defaults) {
126
126
  const id = toTrimmedString(item.id) || `subscription_${index + 1}`;
127
127
  const selector = toTrimmedString(item.selector);
128
128
  if (!selector) return null;
129
+ const pageUrlIncludes = toArray(item.pageUrlIncludes || item.urlIncludes)
130
+ .map((value) => toTrimmedString(value))
131
+ .filter(Boolean);
132
+ const pageUrlExcludes = toArray(item.pageUrlExcludes || item.urlExcludes)
133
+ .map((value) => toTrimmedString(value))
134
+ .filter(Boolean);
129
135
  const events = toArray(item.events).map((name) => toTrimmedString(name)).filter(Boolean);
130
136
  return {
131
137
  id,
132
138
  selector,
139
+ visible: item.visible === false ? false : true,
140
+ pageUrlIncludes,
141
+ pageUrlExcludes,
133
142
  events: events.length > 0 ? events : ['appear', 'exist', 'disappear', 'change'],
134
143
  dependsOn: toArray(item.dependsOn).map((x) => toTrimmedString(x)).filter(Boolean),
135
144
  retry: normalizeRetry(item.retry, defaults.retry),
@@ -575,7 +575,14 @@ export function buildXhsUnifiedAutoscript(rawOptions = {}) {
575
575
  { id: 'home_search_input', selector: '#search-input, input.search-input', events: ['appear', 'exist', 'disappear'] },
576
576
  { id: 'home_search_button', selector: '.input-button, .input-button .search-icon', events: ['exist'] },
577
577
  { id: 'search_result_item', selector: '.note-item', events: ['appear', 'exist', 'change'] },
578
- { id: 'detail_modal', selector: '.note-detail-mask, .note-detail-page, .note-detail-dialog, .note-detail-mask .detail-container, .note-detail-mask .media-container, .note-detail-mask .note-scroller, .note-detail-mask .note-content, .note-detail-mask .interaction-container, .note-detail-mask .comments-container', events: ['appear', 'exist', 'disappear'] },
578
+ {
579
+ id: 'detail_modal',
580
+ selector: 'body',
581
+ visible: false,
582
+ pageUrlIncludes: ['/explore/'],
583
+ pageUrlExcludes: ['/search_result'],
584
+ events: ['appear', 'exist', 'disappear'],
585
+ },
579
586
  { id: 'detail_comment_item', selector: '.comment-item, [class*="comment-item"]', events: ['appear', 'exist', 'change'] },
580
587
  { id: 'detail_show_more', selector: '.note-detail-mask .show-more, .note-detail-mask .reply-expand, .note-detail-mask [class*="expand"], .note-detail-page .show-more, .note-detail-page .reply-expand, .note-detail-page [class*="expand"]', events: ['appear', 'exist'] },
581
588
  { id: 'detail_discover_button', selector: 'a[href*="/explore?channel_id=homefeed_recommend"]', events: ['appear', 'exist'] },
@@ -884,7 +891,7 @@ export function buildXhsUnifiedAutoscript(rawOptions = {}) {
884
891
  },
885
892
  {
886
893
  id: 'abort_on_login_guard',
887
- action: 'raise_error',
894
+ action: 'xhs_assert_logged_in',
888
895
  params: { code: 'LOGIN_GUARD_DETECTED' },
889
896
  trigger: 'login_guard.appear',
890
897
  once: false,
@@ -40,6 +40,105 @@ export const XHS_CHECKPOINTS = {
40
40
  ],
41
41
  };
42
42
 
43
+ function extractEvaluateResult(payload) {
44
+ if (!payload || typeof payload !== 'object') return {};
45
+ if (payload.result && typeof payload.result === 'object') return payload.result;
46
+ if (payload.data && typeof payload.data === 'object') return payload.data;
47
+ return payload;
48
+ }
49
+
50
+ async function detectXhsLoginSignal(profileId) {
51
+ const script = `(() => {
52
+ const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
53
+ const selectors = ${JSON.stringify(XHS_CHECKPOINTS.login_guard)};
54
+ const isVisible = (node) => {
55
+ if (!(node instanceof HTMLElement)) return false;
56
+ const style = window.getComputedStyle(node);
57
+ if (!style) return false;
58
+ if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') === 0) return false;
59
+ const rect = node.getBoundingClientRect();
60
+ return rect.width > 0 && rect.height > 0;
61
+ };
62
+ const nodes = selectors.flatMap((selector) => Array.from(document.querySelectorAll(selector)));
63
+ const visibleNodes = nodes.filter((node) => isVisible(node));
64
+ const guardText = visibleNodes
65
+ .slice(0, 10)
66
+ .map((node) => normalize(node.textContent || ''))
67
+ .filter(Boolean)
68
+ .join(' ');
69
+ const hasLoginText = /登录|扫码|验证码|手机号|请先登录|注册|sign\\s*in/i.test(guardText);
70
+ const loginUrl = /\\/login|signin|passport|account\\/login/i.test(String(location.href || ''));
71
+
72
+ let accountId = '';
73
+ try {
74
+ const initialState = (typeof window !== 'undefined' && window.__INITIAL_STATE__) || null;
75
+ const rawUserInfo = initialState && initialState.user && initialState.user.userInfo
76
+ ? (
77
+ (initialState.user.userInfo._rawValue && typeof initialState.user.userInfo._rawValue === 'object' && initialState.user.userInfo._rawValue)
78
+ || (initialState.user.userInfo._value && typeof initialState.user.userInfo._value === 'object' && initialState.user.userInfo._value)
79
+ || (typeof initialState.user.userInfo === 'object' ? initialState.user.userInfo : null)
80
+ )
81
+ : null;
82
+ accountId = normalize(rawUserInfo?.user_id || rawUserInfo?.userId || '');
83
+ } catch {}
84
+
85
+ if (!accountId) {
86
+ const selfAnchor = Array.from(document.querySelectorAll('a[href*="/user/profile/"]'))
87
+ .find((node) => {
88
+ const text = normalize(node.textContent || '');
89
+ const title = normalize(node.getAttribute('title') || '');
90
+ const aria = normalize(node.getAttribute('aria-label') || '');
91
+ return ['我', '我的', '个人主页', '我的主页'].includes(text)
92
+ || ['我', '我的', '个人主页', '我的主页'].includes(title)
93
+ || ['我', '我的', '个人主页', '我的主页'].includes(aria);
94
+ });
95
+ if (selfAnchor) {
96
+ const href = normalize(selfAnchor.getAttribute('href') || '');
97
+ const matched = href.match(/\\/user\\/profile\\/([^/?#]+)/);
98
+ if (matched && matched[1]) accountId = normalize(matched[1]);
99
+ }
100
+ }
101
+
102
+ const hasAccountSignal = Boolean(accountId);
103
+ const hasLoginGuard = (visibleNodes.length > 0 && hasLoginText) || loginUrl;
104
+ return {
105
+ hasLoginGuard,
106
+ hasLoginText,
107
+ hasAccountSignal,
108
+ accountId: accountId || null,
109
+ visibleGuardCount: visibleNodes.length,
110
+ guardTextPreview: guardText.slice(0, 240),
111
+ loginUrl,
112
+ };
113
+ })()`;
114
+
115
+ try {
116
+ const ret = await callAPI('evaluate', { profileId, script });
117
+ const data = extractEvaluateResult(ret);
118
+ return {
119
+ hasLoginGuard: data?.hasLoginGuard === true,
120
+ hasLoginText: data?.hasLoginText === true,
121
+ hasAccountSignal: data?.hasAccountSignal === true,
122
+ accountId: String(data?.accountId || '').trim() || null,
123
+ visibleGuardCount: Math.max(0, Number(data?.visibleGuardCount || 0) || 0),
124
+ guardTextPreview: String(data?.guardTextPreview || '').trim(),
125
+ loginUrl: data?.loginUrl === true,
126
+ ok: true,
127
+ };
128
+ } catch {
129
+ return {
130
+ hasLoginGuard: false,
131
+ hasLoginText: false,
132
+ hasAccountSignal: false,
133
+ accountId: null,
134
+ visibleGuardCount: 0,
135
+ guardTextPreview: '',
136
+ loginUrl: false,
137
+ ok: false,
138
+ };
139
+ }
140
+ }
141
+
43
142
  export async function detectCheckpoint({ profileId, platform = 'xiaohongshu' }) {
44
143
  if (platform !== 'xiaohongshu') {
45
144
  return asErrorPayload('UNSUPPORTED_PLATFORM', `Unsupported platform: ${platform}`);
@@ -72,10 +171,16 @@ export async function detectCheckpoint({ profileId, platform = 'xiaohongshu' })
72
171
  addCount('login_guard', XHS_CHECKPOINTS.login_guard);
73
172
  addCount('risk_control', XHS_CHECKPOINTS.risk_control);
74
173
 
174
+ const loginSignal = await detectXhsLoginSignal(resolvedProfile);
175
+ const loginGuardBySelector = signals.some((item) => item.startsWith('login_guard:'));
176
+ const loginGuardConfirmed = loginSignal.ok
177
+ ? loginSignal.hasLoginGuard
178
+ : loginGuardBySelector;
179
+
75
180
  let checkpoint = 'unknown';
76
181
  if (!url || !url.includes('xiaohongshu.com')) checkpoint = 'offsite';
77
182
  else if (isCheckpointRiskUrl(url)) checkpoint = 'risk_control';
78
- else if (signals.some((item) => item.startsWith('login_guard:'))) checkpoint = 'login_guard';
183
+ else if (loginGuardConfirmed) checkpoint = 'login_guard';
79
184
  else if (signals.some((item) => item.startsWith('comments_ready:'))) checkpoint = 'comments_ready';
80
185
  else if (signals.some((item) => item.startsWith('detail_ready:'))) checkpoint = 'detail_ready';
81
186
  else if (signals.some((item) => item.startsWith('search_ready:'))) checkpoint = 'search_ready';
@@ -92,6 +197,7 @@ export async function detectCheckpoint({ profileId, platform = 'xiaohongshu' })
92
197
  url,
93
198
  signals,
94
199
  selectorHits: counter,
200
+ loginSignal,
95
201
  },
96
202
  };
97
203
  } catch (err) {
@@ -18,8 +18,22 @@ export async function watchSubscriptions({
18
18
  const id = String(item.id || `sub_${index + 1}`);
19
19
  const selector = String(item.selector || '').trim();
20
20
  if (!selector) return null;
21
+ const visible = item.visible !== false;
22
+ const pageUrlIncludes = normalizeArray(item.pageUrlIncludes || item.urlIncludes)
23
+ .map((value) => String(value || '').trim())
24
+ .filter(Boolean);
25
+ const pageUrlExcludes = normalizeArray(item.pageUrlExcludes || item.urlExcludes)
26
+ .map((value) => String(value || '').trim())
27
+ .filter(Boolean);
21
28
  const events = normalizeArray(item.events).map((name) => String(name).trim()).filter(Boolean);
22
- return { id, selector, events: events.length > 0 ? new Set(events) : null };
29
+ return {
30
+ id,
31
+ selector,
32
+ visible,
33
+ pageUrlIncludes,
34
+ pageUrlExcludes,
35
+ events: events.length > 0 ? new Set(events) : null,
36
+ };
23
37
  })
24
38
  .filter(Boolean);
25
39
 
@@ -40,9 +54,17 @@ export async function watchSubscriptions({
40
54
  try {
41
55
  const snapshot = await getDomSnapshotByProfile(resolvedProfile);
42
56
  const ts = new Date().toISOString();
57
+ const currentUrl = String(snapshot?.__url || '');
43
58
  for (const item of items) {
44
59
  const prev = state.get(item.id) || { exists: false, stateSig: '', appearCount: 0 };
45
- const elements = notifier.findElements(snapshot, { css: item.selector });
60
+ const includeOk = item.pageUrlIncludes.length === 0
61
+ || item.pageUrlIncludes.some((token) => currentUrl.includes(token));
62
+ const excludeHit = item.pageUrlExcludes.length > 0
63
+ && item.pageUrlExcludes.some((token) => currentUrl.includes(token));
64
+ const pageUrlMatched = includeOk && !excludeHit;
65
+ const elements = pageUrlMatched
66
+ ? notifier.findElements(snapshot, { css: item.selector, visible: item.visible })
67
+ : [];
46
68
  const exists = elements.length > 0;
47
69
  const stateSig = elements.map((node) => node.path).sort().join(',');
48
70
  const changed = stateSig !== prev.stateSig;
@@ -215,6 +215,7 @@ function buildDomSnapshotScript(maxDepth, maxChildren) {
215
215
  const root = collect(document.body || document.documentElement, 0, 'root');
216
216
  return {
217
217
  dom_tree: root,
218
+ current_url: String(window.location.href || ''),
218
219
  viewport: {
219
220
  width: viewportWidth,
220
221
  height: viewportHeight,
@@ -238,6 +239,9 @@ export async function getDomSnapshotByProfile(profileId, options = {}) {
238
239
  height: Number(payload.viewport.height) || 0,
239
240
  };
240
241
  }
242
+ if (tree && payload.current_url) {
243
+ tree.__url = String(payload.current_url);
244
+ }
241
245
  return tree;
242
246
  }
243
247
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@web-auto/webauto",
3
- "version": "0.1.8",
3
+ "version": "0.1.11",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "webauto": "bin/webauto.mjs"
@@ -9,6 +9,7 @@
9
9
  "bin/",
10
10
  "dist/",
11
11
  "modules/",
12
+ "runtime/",
12
13
  "scripts/",
13
14
  "services/",
14
15
  "apps/desktop-console/dist/",
@@ -59,9 +60,9 @@
59
60
  "check:modules": "echo \"Module checks skipped\"",
60
61
  "check:ts": "tsc --noEmit -p tsconfig.services.json",
61
62
  "test:modules:unit": "npx tsx --test $(find modules -name \"*.test.ts\" -o -name \"*.test.mts\" 2>/dev/null | tr \"\\n\" \" \")",
62
- "test:desktop-console:unit": "tsx --test apps/desktop-console/src/main/profile-store.test.mts apps/desktop-console/src/main/index-streaming.test.mts apps/desktop-console/src/main/ui-cli-bridge.test.mts apps/desktop-console/src/main/heartbeat-watchdog.test.mts apps/desktop-console/src/main/core-daemon-manager.test.mts apps/desktop-console/src/main/desktop-settings.test.mts apps/desktop-console/src/main/env-check.test.mts",
63
+ "test:desktop-console:unit": "tsx --test apps/desktop-console/src/main/profile-store.test.mts apps/desktop-console/src/main/index-streaming.test.mts apps/desktop-console/src/main/ui-cli-bridge.test.mts apps/desktop-console/src/main/task-gateway.test.mts apps/desktop-console/src/main/heartbeat-watchdog.test.mts apps/desktop-console/src/main/core-daemon-manager.test.mts apps/desktop-console/src/main/desktop-settings.test.mts apps/desktop-console/src/main/env-check.test.mts",
63
64
  "test:desktop-console:renderer": "npm --prefix apps/desktop-console run test:renderer",
64
- "test:desktop-console:coverage": "c8 --reporter=text --reporter=lcov --all --src apps/desktop-console/src/main --extension .mts --extension .mjs --extension .ts --exclude \"**/*.test.*\" --check-coverage --lines 75 --functions 75 --branches 55 --statements 75 --include \"apps/desktop-console/src/main/profile-store.mts\" --include \"apps/desktop-console/src/main/ui-cli-bridge.mts\" --include \"apps/desktop-console/src/main/heartbeat-watchdog.mts\" --include \"apps/desktop-console/src/main/core-daemon-manager.mts\" --include \"apps/desktop-console/src/main/desktop-settings.mts\" --include \"apps/desktop-console/src/main/env-check.mts\" tsx --test apps/desktop-console/src/main/profile-store.test.mts apps/desktop-console/src/main/index-streaming.test.mts apps/desktop-console/src/main/ui-cli-bridge.test.mts apps/desktop-console/src/main/heartbeat-watchdog.test.mts apps/desktop-console/src/main/core-daemon-manager.test.mts apps/desktop-console/src/main/desktop-settings.test.mts apps/desktop-console/src/main/env-check.test.mts",
65
+ "test:desktop-console:coverage": "c8 --reporter=text --reporter=lcov --all --src apps/desktop-console/src/main --extension .mts --extension .mjs --extension .ts --exclude \"**/*.test.*\" --check-coverage --lines 75 --functions 75 --branches 55 --statements 75 --include \"apps/desktop-console/src/main/profile-store.mts\" --include \"apps/desktop-console/src/main/ui-cli-bridge.mts\" --include \"apps/desktop-console/src/main/task-gateway.mts\" --include \"apps/desktop-console/src/main/heartbeat-watchdog.mts\" --include \"apps/desktop-console/src/main/core-daemon-manager.mts\" --include \"apps/desktop-console/src/main/desktop-settings.mts\" --include \"apps/desktop-console/src/main/env-check.mts\" tsx --test apps/desktop-console/src/main/profile-store.test.mts apps/desktop-console/src/main/index-streaming.test.mts apps/desktop-console/src/main/ui-cli-bridge.test.mts apps/desktop-console/src/main/task-gateway.test.mts apps/desktop-console/src/main/heartbeat-watchdog.test.mts apps/desktop-console/src/main/core-daemon-manager.test.mts apps/desktop-console/src/main/desktop-settings.test.mts apps/desktop-console/src/main/env-check.test.mts",
65
66
  "test:webauto:schedule:unit": "node --test tests/unit/webauto/schedule-store.test.mjs tests/unit/webauto/schedule-cli.test.mjs",
66
67
  "test:webauto:ui-cli:unit": "node --test tests/unit/webauto/ui-cli-command.test.mjs",
67
68
  "test:webauto:install:unit": "node --test tests/unit/webauto/xhs-install.test.mjs",
@@ -69,6 +70,9 @@
69
70
  "test:ci": "npm test && npm --prefix apps/desktop-console run test:renderer",
70
71
  "coverage:ci": "node scripts/test/run-coverage.mjs",
71
72
  "build:release": "node bin/webauto.mjs build:release",
73
+ "version:bump": "node scripts/bump-version.mjs patch",
74
+ "version:bump:minor": "node scripts/bump-version.mjs minor",
75
+ "version:bump:major": "node scripts/bump-version.mjs major",
72
76
  "test": "npm run check:modules && npm run check:ts && npm run test:modules:unit && npm run test:webauto:schedule:unit && npm run test:webauto:ui-cli:unit && npm run test:webauto:install:unit && npm run test:desktop-console:unit && npm run test:services:unit",
73
77
  "cli:session-manager": "tsx modules/session-manager/src/cli.ts",
74
78
  "cli:logging": "tsx modules/logging/src/cli.ts",
@@ -0,0 +1,13 @@
1
+ # Runtime Utilities
2
+
3
+ 运行时工具统一收敛到 `runtime/infra/utils/scripts/`。
4
+
5
+ 当前保留目录:
6
+ - `scripts/development/`:开发期调试脚本(会话内执行、高亮等)
7
+ - `scripts/service/`:服务启停与端口清理脚本
8
+ - `scripts/` 根脚本:跨场景工具(例如 `test-services.mjs`)
9
+
10
+ 已移除:
11
+ - `local-dev/`
12
+ - `scripts/local-dev/`
13
+ - 其它历史遗留脚本目录
File without changes
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ // Post a local JS file (or inline) to Workflow API /browser/eval for the given session
3
+ import { readFileSync, existsSync } from 'node:fs';
4
+
5
+ async function main() {
6
+ const args = process.argv.slice(2);
7
+ if (args.length < 2) {
8
+ console.log('Usage: node scripts/dev/eval-in-session.mjs <sessionId> <file.js|--code="JS..."> [--host=http://127.0.0.1:7701]');
9
+ process.exit(1);
10
+ }
11
+ const sessionId = args[0];
12
+ let codeArg = args[1];
13
+ let host = 'http://127.0.0.1:7701';
14
+ for (const a of args.slice(2)) {
15
+ if (a.startsWith('--host=')) host = a.slice('--host='.length);
16
+ }
17
+ let script = '';
18
+ if (codeArg.startsWith('--code=')) {
19
+ script = codeArg.slice('--code='.length);
20
+ } else {
21
+ if (!existsSync(codeArg)) {
22
+ console.error('File not found:', codeArg);
23
+ process.exit(2);
24
+ }
25
+ script = readFileSync(codeArg, 'utf8');
26
+ }
27
+ const res = await fetch(host + '/browser/eval', {
28
+ method: 'POST',
29
+ headers: { 'Content-Type': 'application/json' },
30
+ body: JSON.stringify({ sessionId, script })
31
+ }).then(r => r.json()).catch(e => ({ success: false, error: String(e) }));
32
+ if (!res || !res.success) {
33
+ console.error('Eval failed:', res?.error || 'unknown');
34
+ process.exit(1);
35
+ }
36
+ console.log('Eval OK:', JSON.stringify(res.value));
37
+ }
38
+
39
+ main().catch(e => { console.error(e); process.exit(1); });
40
+
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ // Highlight search containers for 3 seconds: root -> listContainer -> first item
3
+ const host = process.env.WORKFLOW_HOST || 'http://127.0.0.1:7701';
4
+
5
+ function arg(k, d){ const a = process.argv.find(x=>x.startsWith(`--${k}=`)); return a ? a.split('=')[1] : d; }
6
+ const sleep = (ms)=>new Promise(r=>setTimeout(r,ms));
7
+
8
+ async function j(u,opt){ const r = await fetch(host+u, { headers:{'Content-Type':'application/json'}, ...(opt||{}) }); return await r.json(); }
9
+ async function lastSession(){ const s = await j('/v1/sessions'); const arr=s.sessions||[]; return arr[arr.length-1]||null; }
10
+
11
+ async function highlight({ sessionId, selector, label }){
12
+ return await j('/v1/containers/highlight', { method:'POST', body: JSON.stringify({ sessionId, containerSelector: selector, label, durationMs: 3000 }) });
13
+ }
14
+
15
+ async function validate({ sessionId, selector }){ return await j('/v1/containers/validate', { method:'POST', body: JSON.stringify({ sessionId, containerSelector: selector }) }); }
16
+
17
+ async function main(){
18
+ const sessionId = arg('sessionId', null) || await lastSession();
19
+ if (!sessionId) { console.error('No active session.'); process.exit(1); }
20
+ // container definitions (must match container registry)
21
+ const rootSel = '.search-ui2024, .search-i18nUi, body';
22
+ const listSel = '.space-common-offerlist, .offer-list, #offer-list';
23
+ const itemSel = '.offer-item, .sm-offer, [class*=offer]';
24
+
25
+ const out = { sessionId, checks: [] };
26
+ for (const [label, sel] of [['ROOT', rootSel], ['LIST', listSel], ['ITEM', itemSel]]){
27
+ const v = await validate({ sessionId, selector: sel });
28
+ out.checks.push({ label, selector: sel, found: !!v.found });
29
+ if (v.found) await highlight({ sessionId, selector: sel, label });
30
+ await sleep(400);
31
+ }
32
+ console.log(JSON.stringify({ ok:true, ...out }, null, 2));
33
+ }
34
+
35
+ main().catch(e=>{ console.error(e); process.exit(1); });
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ // 杀掉占用指定端口的进程(跨平台尽力)
3
+ import { execSync } from 'node:child_process';
4
+
5
+ const port = Number(process.argv[2] || 0);
6
+ if (!port) { console.error('Usage: node runtime/infra/utils/scripts/service/kill-port.mjs <port>'); process.exit(1); }
7
+
8
+ try {
9
+ if (process.platform === 'win32') {
10
+ const out = execSync(`netstat -ano | findstr :${port}`, { encoding: 'utf8' });
11
+ const pids = new Set();
12
+ out.split(/\r?\n/).forEach(line=>{ const m=line.trim().match(/\s(\d+)\s*$/); if (m) pids.add(Number(m[1])); });
13
+ for (const pid of pids) { try { execSync(`taskkill /F /PID ${pid}`); console.log(`killed pid ${pid}`); } catch {} }
14
+ if (pids.size===0) console.log('no process found');
15
+ } else {
16
+ const out = execSync(`lsof -ti :${port}`, { encoding: 'utf8' });
17
+ const pids = out.split(/\s+/).map(s=>Number(s.trim())).filter(Boolean);
18
+ for (const pid of pids) { try { process.kill(pid, 'SIGKILL'); console.log(`killed pid ${pid}`); } catch {} }
19
+ if (pids.length===0) console.log('no process found');
20
+ }
21
+ } catch (e) {
22
+ console.log('nothing to kill or command failed');
23
+ }
24
+
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+ import { spawn, spawnSync } from 'node:child_process';
3
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { setTimeout as wait } from 'node:timers/promises';
7
+
8
+ const API_PID_FILE = process.env.WEBAUTO_API_PID_FILE
9
+ ? path.resolve(process.env.WEBAUTO_API_PID_FILE)
10
+ : path.join(os.tmpdir(), 'webauto-api.pid');
11
+ const DIST_SERVER = path.resolve(process.cwd(), 'dist', 'apps', 'webauto', 'server.js');
12
+
13
+ function resolveOnPath(candidates) {
14
+ const pathEnv = process.env.PATH || process.env.Path || '';
15
+ const dirs = String(pathEnv).split(path.delimiter).map((x) => x.trim()).filter(Boolean);
16
+ for (const dir of dirs) {
17
+ for (const name of candidates) {
18
+ const full = path.join(dir, name);
19
+ if (existsSync(full)) return full;
20
+ }
21
+ }
22
+ return null;
23
+ }
24
+
25
+ function resolveNodeBin() {
26
+ const explicit = String(process.env.WEBAUTO_NODE_BIN || '').trim();
27
+ if (explicit) return explicit;
28
+ const npmNode = String(process.env.npm_node_execpath || '').trim();
29
+ if (npmNode) return npmNode;
30
+ const fromPath = resolveOnPath(process.platform === 'win32' ? ['node.exe', 'node.cmd', 'node'] : ['node']);
31
+ if (fromPath) return fromPath;
32
+ return process.execPath;
33
+ }
34
+
35
+ function resolveNpmBin() {
36
+ if (process.platform !== 'win32') return 'npm';
37
+ const fromPath = resolveOnPath(['npm.cmd', 'npm.exe', 'npm.bat', 'npm.ps1']);
38
+ return fromPath || 'npm.cmd';
39
+ }
40
+
41
+ function quoteCmdArg(value) {
42
+ if (!value) return '""';
43
+ if (!/[\s"]/u.test(value)) return value;
44
+ return `"${String(value).replace(/"/g, '""')}"`;
45
+ }
46
+
47
+ function run(command, args) {
48
+ const lower = String(command || '').toLowerCase();
49
+ if (process.platform === 'win32' && (lower.endsWith('.cmd') || lower.endsWith('.bat'))) {
50
+ const cmdLine = [quoteCmdArg(command), ...args.map(quoteCmdArg)].join(' ');
51
+ return spawnSync('cmd.exe', ['/d', '/s', '/c', cmdLine], { stdio: 'inherit', windowsHide: true });
52
+ }
53
+ if (process.platform === 'win32' && lower.endsWith('.ps1')) {
54
+ return spawnSync(
55
+ 'powershell.exe',
56
+ ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', command, ...args],
57
+ { stdio: 'inherit', windowsHide: true },
58
+ );
59
+ }
60
+ return spawnSync(command, args, { stdio: 'inherit', windowsHide: true });
61
+ }
62
+
63
+ async function health(url='http://127.0.0.1:7701/health'){
64
+ try{
65
+ const res = await fetch(url);
66
+ if (!res.ok) return false;
67
+ const j = await res.json().catch(() => ({}));
68
+ return Boolean(j?.ok ?? true);
69
+ }catch{
70
+ return false;
71
+ }
72
+ }
73
+
74
+ async function main(){
75
+ if (!existsSync(DIST_SERVER)) {
76
+ const npmBin = resolveNpmBin();
77
+ const buildRet = run(npmBin, ['run', '-s', 'build:services']);
78
+ if (buildRet.status !== 0) {
79
+ console.error('build:services failed');
80
+ process.exit(1);
81
+ }
82
+ }
83
+ // start
84
+ const child = spawn(resolveNodeBin(), [DIST_SERVER], {
85
+ detached: true,
86
+ stdio: 'ignore',
87
+ windowsHide: true,
88
+ env: {
89
+ ...process.env,
90
+ WEBAUTO_RUNTIME_MODE: 'unified',
91
+ },
92
+ });
93
+ child.unref();
94
+ try {
95
+ mkdirSync(path.dirname(API_PID_FILE), { recursive: true });
96
+ } catch {}
97
+ writeFileSync(API_PID_FILE, String(child.pid));
98
+ // wait for health
99
+ for(let i=0;i<20;i++){ if (await health()) { console.log('API started. PID', child.pid); return; } await wait(500); }
100
+ console.error('API did not become healthy in time'); process.exit(1);
101
+ }
102
+
103
+ main().catch(e=>{ console.error(e); process.exit(1); });