@web-auto/webauto 0.1.8 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/desktop-console/dist/main/index.mjs +909 -105
- package/apps/desktop-console/dist/main/preload.mjs +3 -0
- package/apps/desktop-console/dist/renderer/index.html +9 -1
- package/apps/desktop-console/dist/renderer/index.js +796 -331
- package/apps/desktop-console/entry/ui-cli.mjs +59 -9
- package/apps/desktop-console/entry/ui-console.mjs +8 -3
- package/apps/webauto/entry/account.mjs +70 -9
- package/apps/webauto/entry/lib/account-detect.mjs +106 -25
- package/apps/webauto/entry/lib/account-store.mjs +122 -35
- package/apps/webauto/entry/lib/profilepool.mjs +45 -13
- package/apps/webauto/entry/lib/schedule-store.mjs +1 -25
- package/apps/webauto/entry/profilepool.mjs +45 -3
- package/apps/webauto/entry/schedule.mjs +44 -2
- package/apps/webauto/entry/weibo-unified.mjs +2 -2
- package/apps/webauto/entry/xhs-install.mjs +248 -52
- package/apps/webauto/entry/xhs-unified.mjs +33 -6
- package/bin/webauto.mjs +137 -5
- package/dist/modules/camo-runtime/src/utils/browser-service.mjs +4 -0
- package/dist/services/unified-api/server.js +5 -0
- package/dist/services/unified-api/task-state.js +2 -0
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +142 -14
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +16 -1
- package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +104 -0
- package/modules/camo-runtime/src/autoscript/runtime.mjs +14 -4
- package/modules/camo-runtime/src/autoscript/schema.mjs +9 -0
- package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +9 -2
- package/modules/camo-runtime/src/container/runtime-core/checkpoint.mjs +107 -1
- package/modules/camo-runtime/src/container/runtime-core/subscription.mjs +24 -2
- package/modules/camo-runtime/src/utils/browser-service.mjs +4 -0
- package/package.json +7 -3
- package/runtime/infra/utils/README.md +13 -0
- package/runtime/infra/utils/scripts/README.md +0 -0
- package/runtime/infra/utils/scripts/development/eval-in-session.mjs +40 -0
- package/runtime/infra/utils/scripts/development/highlight-search-containers.mjs +35 -0
- package/runtime/infra/utils/scripts/service/kill-port.mjs +24 -0
- package/runtime/infra/utils/scripts/service/start-api.mjs +103 -0
- package/runtime/infra/utils/scripts/service/start-browser-service.mjs +173 -0
- package/runtime/infra/utils/scripts/service/stop-api.mjs +30 -0
- package/runtime/infra/utils/scripts/service/stop-browser-service.mjs +104 -0
- package/runtime/infra/utils/scripts/test-services.mjs +94 -0
- package/scripts/bump-version.mjs +120 -0
- package/services/unified-api/server.ts +4 -0
- package/services/unified-api/task-state.ts +5 -0
|
@@ -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
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
{
|
|
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: '
|
|
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 (
|
|
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 {
|
|
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
|
|
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.
|
|
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); });
|