@web-auto/camo 0.1.24 → 0.1.25
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/package.json +1 -1
- package/src/cli.mjs +9 -0
- package/src/commands/browser.mjs +9 -7
- package/src/container/change-notifier.mjs +90 -39
- package/src/container/runtime-core/operations/index.mjs +108 -48
- package/src/container/runtime-core/operations/tab-pool.mjs +301 -99
- package/src/container/runtime-core/operations/tab-pool.mjs.bak +762 -0
- package/src/container/runtime-core/operations/tab-pool.mjs.syntax-error +762 -0
- package/src/container/runtime-core/operations/viewport.mjs +46 -0
- package/src/container/runtime-core/subscription.mjs +72 -7
- package/src/container/runtime-core/validation.mjs +61 -4
- package/src/core/utils.mjs +4 -0
- package/src/services/browser-service/index.js +27 -10
- package/src/services/browser-service/index.js.bak +671 -0
- package/src/services/browser-service/internal/BrowserSession.js +34 -2
- package/src/services/browser-service/internal/browser-session/page-management.js +120 -58
- package/src/services/browser-service/internal/browser-session/page-management.test.js +43 -0
- package/src/services/controller/controller.js +1 -1
- package/src/services/controller/transport.js +8 -1
- package/src/utils/args.mjs +1 -0
- package/src/utils/browser-service.mjs +13 -1
- package/src/utils/command-log.mjs +64 -0
- package/src/utils/help.mjs +3 -3
|
@@ -41,6 +41,7 @@ export async function executeViewportOperation({ profileId, action, params = {}
|
|
|
41
41
|
const width = hasTargetViewport ? Math.max(320, rawWidth) : null;
|
|
42
42
|
const height = hasTargetViewport ? Math.max(240, rawHeight) : null;
|
|
43
43
|
const followWindow = params.followWindow !== false;
|
|
44
|
+
const fitDisplayWindow = params.fitDisplayWindow !== false;
|
|
44
45
|
const settleMs = Math.max(0, Number(params.settleMs ?? 180) || 180);
|
|
45
46
|
const attempts = Math.max(1, Number(params.attempts ?? 3) || 3);
|
|
46
47
|
const tolerance = Math.max(0, Number(params.tolerancePx ?? 3) || 3);
|
|
@@ -56,6 +57,49 @@ export async function executeViewportOperation({ profileId, action, params = {}
|
|
|
56
57
|
|
|
57
58
|
let measured = await probeWindow();
|
|
58
59
|
if (followWindow && !hasTargetViewport) {
|
|
60
|
+
let resizedWindow = false;
|
|
61
|
+
let windowTarget = null;
|
|
62
|
+
if (fitDisplayWindow) {
|
|
63
|
+
try {
|
|
64
|
+
const displayPayload = await callWithTimeout('system:display', {}, apiTimeoutMs);
|
|
65
|
+
const display = displayPayload?.metrics || displayPayload || {};
|
|
66
|
+
const reserveFromEnv = Number(process.env.CAMO_DEFAULT_WINDOW_VERTICAL_RESERVE ?? 0);
|
|
67
|
+
const reservePx = Number.isFinite(reserveFromEnv) ? Math.max(0, Math.min(240, Math.floor(reserveFromEnv))) : 0;
|
|
68
|
+
const workWidth = Number(display.workWidth || 0);
|
|
69
|
+
const workHeight = Number(display.workHeight || 0);
|
|
70
|
+
const displayWidth = Number(display.width || 0);
|
|
71
|
+
const displayHeight = Number(display.height || 0);
|
|
72
|
+
const baseW = Math.floor(workWidth > 0 ? workWidth : displayWidth);
|
|
73
|
+
const baseH = Math.floor(workHeight > 0 ? workHeight : displayHeight);
|
|
74
|
+
if (baseW > 0 && baseH > 0) {
|
|
75
|
+
windowTarget = {
|
|
76
|
+
width: Math.max(960, baseW),
|
|
77
|
+
height: Math.max(700, baseH - reservePx),
|
|
78
|
+
};
|
|
79
|
+
const currentOuterWidth = Number(measured.outerWidth || 0);
|
|
80
|
+
const currentOuterHeight = Number(measured.outerHeight || 0);
|
|
81
|
+
const shouldResize = (
|
|
82
|
+
!Number.isFinite(currentOuterWidth)
|
|
83
|
+
|| !Number.isFinite(currentOuterHeight)
|
|
84
|
+
|| currentOuterWidth < Math.floor(windowTarget.width * 0.92)
|
|
85
|
+
|| currentOuterHeight < Math.floor(windowTarget.height * 0.92)
|
|
86
|
+
);
|
|
87
|
+
if (shouldResize) {
|
|
88
|
+
await callWithTimeout('window:resize', {
|
|
89
|
+
profileId,
|
|
90
|
+
width: windowTarget.width,
|
|
91
|
+
height: windowTarget.height,
|
|
92
|
+
}, apiTimeoutMs);
|
|
93
|
+
if (settleMs > 0) await new Promise((resolve) => setTimeout(resolve, settleMs));
|
|
94
|
+
measured = await probeWindow();
|
|
95
|
+
resizedWindow = true;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// display probing is best-effort and must not block follow-window sync
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
59
103
|
const innerWidth = Math.max(320, Number(measured.innerWidth || 0) || 1280);
|
|
60
104
|
const innerHeight = Math.max(240, Number(measured.innerHeight || 0) || 720);
|
|
61
105
|
const outerWidth = Math.max(320, Number(measured.outerWidth || 0) || innerWidth);
|
|
@@ -77,6 +121,8 @@ export async function executeViewportOperation({ profileId, action, params = {}
|
|
|
77
121
|
followWindow: true,
|
|
78
122
|
viewport: { width: followWidth, height: followHeight },
|
|
79
123
|
frame: { width: frameW, height: frameH },
|
|
124
|
+
resizedWindow,
|
|
125
|
+
windowTarget,
|
|
80
126
|
measured: synced,
|
|
81
127
|
},
|
|
82
128
|
};
|
|
@@ -2,6 +2,30 @@ import { getDomSnapshotByProfile } from '../../utils/browser-service.mjs';
|
|
|
2
2
|
import { ChangeNotifier } from '../change-notifier.mjs';
|
|
3
3
|
import { ensureActiveSession, getCurrentUrl, normalizeArray } from './utils.mjs';
|
|
4
4
|
|
|
5
|
+
function normalizeElementKeys(elements) {
|
|
6
|
+
return (Array.isArray(elements) ? elements : [])
|
|
7
|
+
.map((node) => String(node?.path || '').trim())
|
|
8
|
+
.filter(Boolean)
|
|
9
|
+
.sort();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function joinElementKeys(keys) {
|
|
13
|
+
return Array.isArray(keys) && keys.length > 0 ? keys.join('|') : 'none';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function buildEventKey(subscriptionId, type, presenceVersion, keys) {
|
|
17
|
+
return `${subscriptionId}:${type}:p${Math.max(0, Number(presenceVersion) || 0)}:k${joinElementKeys(keys)}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isTransientSubscriptionError(error) {
|
|
21
|
+
const message = String(error?.message || error || '').trim().toLowerCase();
|
|
22
|
+
if (!message) return false;
|
|
23
|
+
return message.includes('execution context was destroyed')
|
|
24
|
+
|| message.includes('most likely because of a navigation')
|
|
25
|
+
|| message.includes('cannot find context with specified id')
|
|
26
|
+
|| message.includes('target closed');
|
|
27
|
+
}
|
|
28
|
+
|
|
5
29
|
function resolveFilterMode(input) {
|
|
6
30
|
const text = String(input || process.env.CAMO_FILTER_MODE || 'strict').trim().toLowerCase();
|
|
7
31
|
if (!text) return 'strict';
|
|
@@ -11,9 +35,9 @@ function resolveFilterMode(input) {
|
|
|
11
35
|
|
|
12
36
|
function urlMatchesFilter(url, item) {
|
|
13
37
|
const href = String(url || '').trim();
|
|
14
|
-
const includes = normalizeArray(item?.pageUrlIncludes).map((token) => String(token || '').trim()).filter(Boolean);
|
|
15
|
-
const excludes = normalizeArray(item?.pageUrlExcludes).map((token) => String(token || '').trim()).filter(Boolean);
|
|
16
|
-
if (includes.length > 0 && !includes.
|
|
38
|
+
const includes = normalizeArray(item?.pageUrlIncludes || item?.urlIncludes).map((token) => String(token || '').trim()).filter(Boolean);
|
|
39
|
+
const excludes = normalizeArray(item?.pageUrlExcludes || item?.urlExcludes).map((token) => String(token || '').trim()).filter(Boolean);
|
|
40
|
+
if (includes.length > 0 && !includes.some((token) => href.includes(token))) return false;
|
|
17
41
|
if (excludes.length > 0 && excludes.some((token) => href.includes(token))) return false;
|
|
18
42
|
return true;
|
|
19
43
|
}
|
|
@@ -51,7 +75,13 @@ export async function watchSubscriptions({
|
|
|
51
75
|
})
|
|
52
76
|
.filter(Boolean);
|
|
53
77
|
|
|
54
|
-
const state = new Map(items.map((item) => [item.id, {
|
|
78
|
+
const state = new Map(items.map((item) => [item.id, {
|
|
79
|
+
exists: false,
|
|
80
|
+
stateSig: '',
|
|
81
|
+
appearCount: 0,
|
|
82
|
+
presenceVersion: 0,
|
|
83
|
+
elementKeys: [],
|
|
84
|
+
}]));
|
|
55
85
|
const intervalMs = Math.max(100, Number(throttle) || 500);
|
|
56
86
|
let stopped = false;
|
|
57
87
|
|
|
@@ -67,21 +97,36 @@ export async function watchSubscriptions({
|
|
|
67
97
|
if (stopped) return;
|
|
68
98
|
try {
|
|
69
99
|
const snapshot = await getDomSnapshotByProfile(resolvedProfile);
|
|
70
|
-
const currentUrl = await getCurrentUrl(resolvedProfile).catch(() => '');
|
|
100
|
+
const currentUrl = String(snapshot?.__url || '') || await getCurrentUrl(resolvedProfile).catch(() => '');
|
|
71
101
|
const ts = new Date().toISOString();
|
|
72
102
|
for (const item of items) {
|
|
73
|
-
const prev = state.get(item.id) || {
|
|
103
|
+
const prev = state.get(item.id) || {
|
|
104
|
+
exists: false,
|
|
105
|
+
stateSig: '',
|
|
106
|
+
appearCount: 0,
|
|
107
|
+
presenceVersion: 0,
|
|
108
|
+
elementKeys: [],
|
|
109
|
+
};
|
|
74
110
|
const urlMatched = urlMatchesFilter(currentUrl, item);
|
|
75
111
|
const elements = urlMatched
|
|
76
112
|
? notifier.findElements(snapshot, { css: item.selector, visible: item.visible })
|
|
77
113
|
: [];
|
|
78
114
|
const exists = elements.length > 0;
|
|
79
|
-
const
|
|
115
|
+
const elementKeys = normalizeElementKeys(elements);
|
|
116
|
+
const prevElementKeys = Array.isArray(prev.elementKeys) ? prev.elementKeys : [];
|
|
117
|
+
const prevElementKeySet = new Set(prevElementKeys);
|
|
118
|
+
const elementKeySet = new Set(elementKeys);
|
|
119
|
+
const appearedKeys = elementKeys.filter((key) => !prevElementKeySet.has(key));
|
|
120
|
+
const disappearedKeys = prevElementKeys.filter((key) => !elementKeySet.has(key));
|
|
121
|
+
const stateSig = elementKeys.join(',');
|
|
80
122
|
const changed = stateSig !== prev.stateSig;
|
|
123
|
+
const presenceVersion = prev.presenceVersion + (exists && !prev.exists ? 1 : 0);
|
|
81
124
|
const next = {
|
|
82
125
|
exists,
|
|
83
126
|
stateSig,
|
|
84
127
|
appearCount: prev.appearCount + (exists && !prev.exists ? 1 : 0),
|
|
128
|
+
presenceVersion,
|
|
129
|
+
elementKeys,
|
|
85
130
|
};
|
|
86
131
|
state.set(item.id, next);
|
|
87
132
|
|
|
@@ -96,6 +141,10 @@ export async function watchSubscriptions({
|
|
|
96
141
|
elements,
|
|
97
142
|
pageUrl: currentUrl,
|
|
98
143
|
filterMode: effectiveFilterMode,
|
|
144
|
+
elementKeys,
|
|
145
|
+
presenceVersion,
|
|
146
|
+
stateKey: stateSig,
|
|
147
|
+
eventKey: buildEventKey(item.id, 'appear', presenceVersion, appearedKeys.length > 0 ? appearedKeys : elementKeys),
|
|
99
148
|
timestamp: ts,
|
|
100
149
|
});
|
|
101
150
|
}
|
|
@@ -109,6 +158,11 @@ export async function watchSubscriptions({
|
|
|
109
158
|
elements: [],
|
|
110
159
|
pageUrl: currentUrl,
|
|
111
160
|
filterMode: effectiveFilterMode,
|
|
161
|
+
elementKeys: [],
|
|
162
|
+
departedElementKeys: disappearedKeys,
|
|
163
|
+
presenceVersion: prev.presenceVersion,
|
|
164
|
+
stateKey: '',
|
|
165
|
+
eventKey: buildEventKey(item.id, 'disappear', prev.presenceVersion, disappearedKeys),
|
|
112
166
|
timestamp: ts,
|
|
113
167
|
});
|
|
114
168
|
}
|
|
@@ -122,6 +176,10 @@ export async function watchSubscriptions({
|
|
|
122
176
|
elements,
|
|
123
177
|
pageUrl: currentUrl,
|
|
124
178
|
filterMode: effectiveFilterMode,
|
|
179
|
+
elementKeys,
|
|
180
|
+
presenceVersion,
|
|
181
|
+
stateKey: stateSig,
|
|
182
|
+
eventKey: buildEventKey(item.id, 'exist', presenceVersion, elementKeys),
|
|
125
183
|
timestamp: ts,
|
|
126
184
|
});
|
|
127
185
|
}
|
|
@@ -135,12 +193,19 @@ export async function watchSubscriptions({
|
|
|
135
193
|
elements,
|
|
136
194
|
pageUrl: currentUrl,
|
|
137
195
|
filterMode: effectiveFilterMode,
|
|
196
|
+
elementKeys,
|
|
197
|
+
appearedElementKeys: appearedKeys,
|
|
198
|
+
departedElementKeys: disappearedKeys,
|
|
199
|
+
presenceVersion,
|
|
200
|
+
stateKey: stateSig,
|
|
201
|
+
eventKey: buildEventKey(item.id, 'change', presenceVersion, elementKeys),
|
|
138
202
|
timestamp: ts,
|
|
139
203
|
});
|
|
140
204
|
}
|
|
141
205
|
}
|
|
142
206
|
await emit({ type: 'tick', profileId: resolvedProfile, timestamp: ts });
|
|
143
207
|
} catch (err) {
|
|
208
|
+
if (isTransientSubscriptionError(err)) return;
|
|
144
209
|
onError(err);
|
|
145
210
|
}
|
|
146
211
|
};
|
|
@@ -26,13 +26,21 @@ async function validatePage(profileId, spec = {}, platform = 'generic') {
|
|
|
26
26
|
errors.push(`url host mismatch, expected one of: ${hostIncludes.join(',')}`);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
// 锚点驱动的 checkpoint 检查
|
|
29
30
|
const checkpoints = normalizeArray(spec.checkpointIn || []);
|
|
30
31
|
let checkpoint = null;
|
|
31
32
|
if (checkpoints.length > 0) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
// 使用锚点轮询,而非 evaluate
|
|
34
|
+
const checkpointSelectors = getCheckpointSelectors(checkpoints, platform);
|
|
35
|
+
const anchorResult = await validateAnchors(profileId, checkpointSelectors, {
|
|
36
|
+
timeoutMs: 15000, // 15秒最大等待
|
|
37
|
+
intervalMs: 500,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (anchorResult.found) {
|
|
41
|
+
checkpoint = checkpoints[0]; // 锚点存在,使用第一个 checkpoint
|
|
42
|
+
} else {
|
|
43
|
+
errors.push(`anchor not found within ${anchorResult.elapsed}ms, expected one of: ${checkpoints.join(',')}`);
|
|
36
44
|
}
|
|
37
45
|
}
|
|
38
46
|
|
|
@@ -42,8 +50,29 @@ async function validatePage(profileId, spec = {}, platform = 'generic') {
|
|
|
42
50
|
checkpoint,
|
|
43
51
|
errors,
|
|
44
52
|
};
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// 添加 getCheckpointSelectors 辅助函数
|
|
56
|
+
function getCheckpointSelectors(checkpoints, platform = 'generic') {
|
|
57
|
+
const XHS_CHECKPOINTS = {
|
|
58
|
+
search_ready: ['#search-input', 'input.search-input', '.search-result-list'],
|
|
59
|
+
home_ready: ['.note-item', '.note-item a', 'a[href*="/explore/"]', '[class*="note-item"]'],
|
|
60
|
+
detail_ready: ['.note-scroller', '.note-content', '.interaction-container'],
|
|
61
|
+
comments_ready: ['.comments-container', '.comment-item'],
|
|
62
|
+
login_guard: ['.login-container', '.login-dialog', '#login-container'],
|
|
63
|
+
risk_control: ['.qrcode-box', '.captcha-container', '[class*="captcha"]'],
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const selectors = [];
|
|
67
|
+
for (const cp of checkpoints) {
|
|
68
|
+
if (platform === 'xiaohongshu' && XHS_CHECKPOINTS[cp]) {
|
|
69
|
+
selectors.push(...XHS_CHECKPOINTS[cp]);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return selectors.length > 0 ? selectors : checkpoints;
|
|
45
73
|
}
|
|
46
74
|
|
|
75
|
+
|
|
47
76
|
async function validateContainer(profileId, spec = {}) {
|
|
48
77
|
const snapshot = await getDomSnapshotByProfile(profileId);
|
|
49
78
|
const selector = maybeSelector({
|
|
@@ -125,3 +154,31 @@ export async function validateOperation({
|
|
|
125
154
|
return asErrorPayload('VALIDATION_FAILED', err?.message || String(err), { phase, context });
|
|
126
155
|
}
|
|
127
156
|
}
|
|
157
|
+
|
|
158
|
+
// 锚点驱动的验证:轮询容器 selector,而非 evaluate
|
|
159
|
+
async function validateAnchors(profileId, selectors = [], options = {}) {
|
|
160
|
+
const maxMs = Math.max(1000, Number(options.timeoutMs || 30000));
|
|
161
|
+
const intervalMs = Math.max(200, Number(options.intervalMs || 500));
|
|
162
|
+
const startTime = Date.now();
|
|
163
|
+
|
|
164
|
+
while (Date.now() - startTime < maxMs) {
|
|
165
|
+
try {
|
|
166
|
+
const snapshot = await getDomSnapshotByProfile(profileId, { maxDepth: 5, maxChildren: 50 });
|
|
167
|
+
for (const selector of selectors) {
|
|
168
|
+
const matched = buildSelectorCheck(snapshot, selector);
|
|
169
|
+
if (matched.length > 0) {
|
|
170
|
+
return { ok: true, found: true, selector, count: matched.length, elapsed: Date.now() - startTime };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
} catch (err) {
|
|
174
|
+
// 忽略 snapshot 错误,继续轮询
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 等待下次检查
|
|
178
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return { ok: false, found: false, elapsed: maxMs };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export { validateAnchors };
|
package/src/core/utils.mjs
CHANGED
|
@@ -48,6 +48,10 @@ export async function withTimeout(promise, ms, message = 'Timeout') {
|
|
|
48
48
|
export function ensureUrlScheme(url) {
|
|
49
49
|
if (!url) return url;
|
|
50
50
|
if (url.startsWith('http://') || url.startsWith('https://')) return url;
|
|
51
|
+
// Skip special browser URLs that don't need scheme
|
|
52
|
+
if (url.startsWith('about:') || url.startsWith('chrome:') || url.startsWith('file:')) {
|
|
53
|
+
return url;
|
|
54
|
+
}
|
|
51
55
|
if (url.startsWith('localhost') || url.match(/^\\d+\\.\\d+/)) {
|
|
52
56
|
return `http://${url}`;
|
|
53
57
|
}
|
|
@@ -7,6 +7,7 @@ import { BrowserWsServer } from './internal/ws-server.js';
|
|
|
7
7
|
import { logDebug } from './internal/logging.js';
|
|
8
8
|
import { installServiceProcessLogger } from './internal/service-process-logger.js';
|
|
9
9
|
import { startHeartbeatWriter } from './internal/heartbeat.js';
|
|
10
|
+
import { appendCommandLog } from '../../utils/command-log.mjs';
|
|
10
11
|
const clients = new Set();
|
|
11
12
|
const autoLoops = new Map();
|
|
12
13
|
function readNumber(value) {
|
|
@@ -215,6 +216,18 @@ export async function startBrowserService(opts = {}) {
|
|
|
215
216
|
const t0 = Date.now();
|
|
216
217
|
const action = String(payload?.action || '');
|
|
217
218
|
const profileId = String(payload?.args?.profileId || payload?.args?.profile || payload?.args?.sessionId || '');
|
|
219
|
+
appendCommandLog({
|
|
220
|
+
action,
|
|
221
|
+
profileId,
|
|
222
|
+
payload: payload?.args ?? {},
|
|
223
|
+
meta: {
|
|
224
|
+
source: 'browser-service',
|
|
225
|
+
cwd: process.cwd(),
|
|
226
|
+
pid: process.pid,
|
|
227
|
+
ppid: process.ppid,
|
|
228
|
+
sender: payload?.meta?.sender && typeof payload.meta.sender === 'object' ? payload.meta.sender : {},
|
|
229
|
+
},
|
|
230
|
+
});
|
|
218
231
|
logEvent('browser.command.start', { action, profileId });
|
|
219
232
|
const result = await handleCommand(payload, sessionManager, wsServer, { onSessionStart: markSessionStarted });
|
|
220
233
|
logEvent('browser.command.done', { action, profileId, ok: result.ok, ms: Date.now() - t0 });
|
|
@@ -292,16 +305,19 @@ async function handleCommand(payload, manager, wsServer, options = {}) {
|
|
|
292
305
|
switch (action) {
|
|
293
306
|
case 'start': {
|
|
294
307
|
const startViewport = resolveStartViewport(args);
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
308
|
+
const opts = {
|
|
309
|
+
profileId: args.profileId || 'default',
|
|
310
|
+
sessionName: args.profileId || 'default',
|
|
311
|
+
headless: !!args.headless,
|
|
312
|
+
initialUrl: args.url,
|
|
313
|
+
engine: args.engine || 'camoufox',
|
|
314
|
+
fingerprintPlatform: args.fingerprintPlatform || null,
|
|
315
|
+
...(startViewport ? { viewport: startViewport } : {}),
|
|
316
|
+
...(args.ownerPid ? { ownerPid: args.ownerPid } : {}),
|
|
317
|
+
};
|
|
318
|
+
if (Number.isFinite(args.maxTabs) && args.maxTabs >= 1) {
|
|
319
|
+
opts.maxTabs = Math.floor(args.maxTabs);
|
|
320
|
+
}
|
|
305
321
|
const res = await manager.createSession(opts);
|
|
306
322
|
const session = manager.getSession(opts.profileId);
|
|
307
323
|
if (!session) {
|
|
@@ -479,6 +495,7 @@ async function handleCommand(payload, manager, wsServer, options = {}) {
|
|
|
479
495
|
const activeIndex = pages.find((p) => p.active)?.index ?? 0;
|
|
480
496
|
return { ok: true, body: { ok: true, pages, activeIndex } };
|
|
481
497
|
}
|
|
498
|
+
case 'newTab':
|
|
482
499
|
case 'page:new':
|
|
483
500
|
case 'newPage': {
|
|
484
501
|
const profileId = args.profileId || 'default';
|