@web-auto/camo 0.1.14 → 0.1.16

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.
@@ -9,11 +9,10 @@ import {
9
9
  } from '../utils/config.mjs';
10
10
  import { callAPI, ensureCamoufox, ensureBrowserService, getSessionByProfile, checkBrowserService } from '../utils/browser-service.mjs';
11
11
  import { resolveProfileId, ensureUrlScheme, looksLikeUrlToken, getPositionals } from '../utils/args.mjs';
12
+ import { ensureJsExecutionEnabled } from '../utils/js-policy.mjs';
12
13
  import { acquireLock, releaseLock, cleanupStaleLocks } from '../lifecycle/lock.mjs';
13
14
  import {
14
- buildSelectorClickScript,
15
15
  buildScrollTargetScript,
16
- buildSelectorTypeScript,
17
16
  } from '../container/runtime-core/operations/selector-scripts.mjs';
18
17
  import {
19
18
  registerSession,
@@ -31,11 +30,15 @@ import { startSessionWatchdog, stopAllSessionWatchdogs, stopSessionWatchdog } fr
31
30
  const START_WINDOW_MIN_WIDTH = 960;
32
31
  const START_WINDOW_MIN_HEIGHT = 700;
33
32
  const START_WINDOW_MAX_RESERVE = 240;
34
- const START_WINDOW_DEFAULT_RESERVE = 72;
33
+ const START_WINDOW_DEFAULT_RESERVE = 0;
35
34
  const DEFAULT_HEADLESS_IDLE_TIMEOUT_MS = 30 * 60 * 1000;
36
35
  const DEVTOOLS_SHORTCUTS = process.platform === 'darwin'
37
36
  ? ['Meta+Alt+I', 'F12']
38
37
  : ['F12', 'Control+Shift+I'];
38
+ const INPUT_ACTION_TIMEOUT_MS = Math.max(
39
+ 1000,
40
+ parseNumber(process.env.CAMO_INPUT_ACTION_TIMEOUT_MS, 30000),
41
+ );
39
42
 
40
43
  function sleep(ms) {
41
44
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -77,6 +80,167 @@ function parseDurationMs(raw, fallbackMs) {
77
80
  return Math.floor(value * factor);
78
81
  }
79
82
 
83
+ function assertExistingProfile(profileId, profileSet = null) {
84
+ const id = String(profileId || '').trim();
85
+ if (!id) throw new Error('profileId is required');
86
+ const known = profileSet || new Set(listProfiles());
87
+ if (!known.has(id)) {
88
+ throw new Error(`profile not found: ${id}. create it first with "camo profile create ${id}"`);
89
+ }
90
+ return id;
91
+ }
92
+
93
+ async function resolveVisibleTargetPoint(profileId, selector, options = {}) {
94
+ const selectorLiteral = JSON.stringify(String(selector || '').trim());
95
+ const highlight = options.highlight === true;
96
+ const payload = await callAPI('evaluate', {
97
+ profileId,
98
+ script: `(() => {
99
+ const selector = ${selectorLiteral};
100
+ const highlight = ${highlight ? 'true' : 'false'};
101
+ const nodes = Array.from(document.querySelectorAll(selector));
102
+ const isVisible = (node) => {
103
+ if (!(node instanceof Element)) return false;
104
+ const rect = node.getBoundingClientRect?.();
105
+ if (!rect || rect.width <= 0 || rect.height <= 0) return false;
106
+ try {
107
+ const style = window.getComputedStyle(node);
108
+ if (!style) return false;
109
+ if (style.display === 'none') return false;
110
+ if (style.visibility === 'hidden' || style.visibility === 'collapse') return false;
111
+ const opacity = Number.parseFloat(String(style.opacity || '1'));
112
+ if (Number.isFinite(opacity) && opacity <= 0.01) return false;
113
+ } catch {
114
+ return false;
115
+ }
116
+ return true;
117
+ };
118
+ const hitVisible = (node) => {
119
+ if (!(node instanceof Element)) return false;
120
+ const rect = node.getBoundingClientRect?.();
121
+ if (!rect) return false;
122
+ const x = Math.max(0, Math.min((window.innerWidth || 1) - 1, Math.round(rect.left + rect.width / 2)));
123
+ const y = Math.max(0, Math.min((window.innerHeight || 1) - 1, Math.round(rect.top + rect.height / 2)));
124
+ const top = document.elementFromPoint(x, y);
125
+ if (!top) return false;
126
+ return top === node || node.contains(top) || top.contains(node);
127
+ };
128
+ const target = nodes.find((item) => isVisible(item) && hitVisible(item))
129
+ || nodes.find((item) => isVisible(item))
130
+ || nodes[0]
131
+ || null;
132
+ if (!target) {
133
+ return { ok: false, error: 'selector_not_found', selector };
134
+ }
135
+ const rect = target.getBoundingClientRect?.() || { left: 0, top: 0, width: 1, height: 1 };
136
+ const center = {
137
+ x: Math.max(1, Math.min((window.innerWidth || 1) - 1, Math.round(rect.left + Math.max(1, rect.width / 2)))),
138
+ y: Math.max(1, Math.min((window.innerHeight || 1) - 1, Math.round(rect.top + Math.max(1, rect.height / 2)))),
139
+ };
140
+ if (highlight) {
141
+ try {
142
+ const id = 'webauto-action-highlight-overlay';
143
+ const old = document.getElementById(id);
144
+ if (old) old.remove();
145
+ const overlay = document.createElement('div');
146
+ overlay.id = id;
147
+ overlay.style.position = 'fixed';
148
+ overlay.style.left = rect.left + 'px';
149
+ overlay.style.top = rect.top + 'px';
150
+ overlay.style.width = rect.width + 'px';
151
+ overlay.style.height = rect.height + 'px';
152
+ overlay.style.border = '2px solid #00A8FF';
153
+ overlay.style.borderRadius = '8px';
154
+ overlay.style.background = 'rgba(0,168,255,0.12)';
155
+ overlay.style.pointerEvents = 'none';
156
+ overlay.style.zIndex = '2147483647';
157
+ overlay.style.transition = 'opacity 120ms ease';
158
+ overlay.style.opacity = '1';
159
+ document.documentElement.appendChild(overlay);
160
+ setTimeout(() => {
161
+ overlay.style.opacity = '0';
162
+ setTimeout(() => overlay.remove(), 180);
163
+ }, 260);
164
+ } catch {}
165
+ }
166
+ return {
167
+ ok: true,
168
+ selector,
169
+ center,
170
+ rect: {
171
+ left: rect.left,
172
+ top: rect.top,
173
+ width: rect.width,
174
+ height: rect.height,
175
+ },
176
+ viewport: {
177
+ width: Number(window.innerWidth || 0),
178
+ height: Number(window.innerHeight || 0),
179
+ },
180
+ };
181
+ })()`,
182
+ }, { timeoutMs: INPUT_ACTION_TIMEOUT_MS });
183
+ const result = payload?.result || payload?.data?.result || payload?.data || payload || null;
184
+ if (!result || result.ok !== true || !result.center) {
185
+ throw new Error(`Element not found: ${selector}`);
186
+ }
187
+ return result;
188
+ }
189
+
190
+ function isTargetFullyInViewport(target, margin = 6) {
191
+ const rect = target?.rect && typeof target.rect === 'object' ? target.rect : null;
192
+ const viewport = target?.viewport && typeof target.viewport === 'object' ? target.viewport : null;
193
+ if (!rect || !viewport) return true;
194
+ const vw = Number(viewport.width || 0);
195
+ const vh = Number(viewport.height || 0);
196
+ if (!Number.isFinite(vw) || !Number.isFinite(vh) || vw <= 0 || vh <= 0) return true;
197
+ const left = Number(rect.left || 0);
198
+ const top = Number(rect.top || 0);
199
+ const width = Math.max(0, Number(rect.width || 0));
200
+ const height = Math.max(0, Number(rect.height || 0));
201
+ const right = left + width;
202
+ const bottom = top + height;
203
+ const m = Math.max(0, Number(margin) || 0);
204
+ return left >= m && top >= m && right <= (vw - m) && bottom <= (vh - m);
205
+ }
206
+
207
+ async function ensureClickTargetInViewport(profileId, selector, initialTarget, options = {}) {
208
+ let target = initialTarget;
209
+ const maxSteps = Math.max(0, Number(options.maxAutoScrollSteps ?? 8) || 8);
210
+ const settleMs = Math.max(0, Number(options.autoScrollSettleMs ?? 140) || 140);
211
+ let autoScrolled = 0;
212
+
213
+ while (autoScrolled < maxSteps && !isTargetFullyInViewport(target)) {
214
+ const rect = target?.rect && typeof target.rect === 'object' ? target.rect : {};
215
+ const viewport = target?.viewport && typeof target.viewport === 'object' ? target.viewport : {};
216
+ const vw = Math.max(1, Number(viewport.width || 1));
217
+ const vh = Math.max(1, Number(viewport.height || 1));
218
+ const rawCenterY = Number(rect.top || 0) + Math.max(1, Number(rect.height || 0)) / 2;
219
+ const desiredCenterY = clamp(Math.round(vh * 0.45), 80, Math.max(80, vh - 80));
220
+ let deltaY = Math.round(rawCenterY - desiredCenterY);
221
+ deltaY = clamp(deltaY, -900, 900);
222
+ if (Math.abs(deltaY) < 100) {
223
+ deltaY = deltaY >= 0 ? 120 : -120;
224
+ }
225
+ const anchorX = clamp(Math.round(vw / 2), 1, Math.max(1, vw - 1));
226
+ const anchorY = clamp(Math.round(vh / 2), 1, Math.max(1, vh - 1));
227
+
228
+ await callAPI('mouse:move', { profileId, x: anchorX, y: anchorY, steps: 1 }, { timeoutMs: INPUT_ACTION_TIMEOUT_MS });
229
+ await callAPI('mouse:wheel', { profileId, deltaX: 0, deltaY }, { timeoutMs: INPUT_ACTION_TIMEOUT_MS });
230
+ autoScrolled += 1;
231
+ if (settleMs > 0) {
232
+ await sleep(settleMs);
233
+ }
234
+ target = await resolveVisibleTargetPoint(profileId, selector, { highlight: false });
235
+ }
236
+
237
+ return {
238
+ target,
239
+ autoScrolled,
240
+ targetFullyVisible: isTargetFullyInViewport(target),
241
+ };
242
+ }
243
+
80
244
  function validateAlias(alias) {
81
245
  const text = String(alias || '').trim();
82
246
  if (!text) return null;
@@ -378,6 +542,7 @@ export async function handleStartCommand(args) {
378
542
  throw new Error('No default profile set. Run: camo profile default <profileId>');
379
543
  }
380
544
  }
545
+ assertExistingProfile(profileId, profileSet);
381
546
  if (alias && isSessionAliasTaken(alias, profileId)) {
382
547
  throw new Error(`Alias is already in use: ${alias}`);
383
548
  }
@@ -497,17 +662,28 @@ export async function handleStartCommand(args) {
497
662
  source: 'explicit',
498
663
  };
499
664
  } else {
665
+ const display = await callAPI('system:display', {}).catch(() => null);
666
+ const displayTarget = computeStartWindowSize(display);
500
667
  const rememberedWindow = getProfileWindowSize(profileId);
501
668
  if (rememberedWindow) {
502
- windowTarget = {
669
+ const rememberedTarget = {
503
670
  width: rememberedWindow.width,
504
671
  height: rememberedWindow.height,
505
672
  source: 'profile',
506
673
  updatedAt: rememberedWindow.updatedAt,
507
674
  };
675
+ const canTrustDisplayTarget = displayTarget?.source && displayTarget.source !== 'fallback';
676
+ const refreshFromDisplay = canTrustDisplayTarget
677
+ && (
678
+ rememberedTarget.height < Math.floor(displayTarget.height * 0.92)
679
+ || rememberedTarget.width < Math.floor(displayTarget.width * 0.92)
680
+ );
681
+ windowTarget = refreshFromDisplay ? {
682
+ ...displayTarget,
683
+ source: 'display',
684
+ } : rememberedTarget;
508
685
  } else {
509
- const display = await callAPI('system:display', {}).catch(() => null);
510
- windowTarget = computeStartWindowSize(display);
686
+ windowTarget = displayTarget;
511
687
  }
512
688
  }
513
689
 
@@ -704,6 +880,7 @@ export async function handleStatusCommand(args) {
704
880
  export async function handleGotoCommand(args) {
705
881
  await ensureBrowserService();
706
882
  const positionals = getPositionals(args);
883
+ const profileSet = new Set(listProfiles());
707
884
 
708
885
  let profileId;
709
886
  let url;
@@ -718,6 +895,13 @@ export async function handleGotoCommand(args) {
718
895
 
719
896
  if (!profileId) throw new Error('Usage: camo goto [profileId] <url> (or set default profile first)');
720
897
  if (!url) throw new Error('Usage: camo goto [profileId] <url>');
898
+ assertExistingProfile(profileId, profileSet);
899
+ const active = await getSessionByProfile(profileId);
900
+ if (!active) {
901
+ throw new Error(
902
+ `No active session for profile: ${profileId}. Start via "camo start ${profileId}" (or UI CLI startup) before goto.`,
903
+ );
904
+ }
721
905
 
722
906
  const result = await callAPI('goto', { profileId, url: ensureUrlScheme(url) });
723
907
  updateSession(profileId, { url: ensureUrlScheme(url), lastSeen: Date.now() });
@@ -768,7 +952,8 @@ export async function handleScrollCommand(args) {
768
952
  const isFlag = (arg) => arg?.startsWith('--');
769
953
  const selectorIdx = args.indexOf('--selector');
770
954
  const selector = selectorIdx >= 0 ? String(args[selectorIdx + 1] || '').trim() : null;
771
- const highlight = resolveHighlightEnabled(args);
955
+ const highlightRequested = resolveHighlightEnabled(args);
956
+ const highlight = highlightRequested;
772
957
 
773
958
  let profileId = null;
774
959
  for (let i = 1; i < args.length; i++) {
@@ -794,16 +979,15 @@ export async function handleScrollCommand(args) {
794
979
  const target = await callAPI('evaluate', {
795
980
  profileId,
796
981
  script: buildScrollTargetScript({ selector, highlight }),
797
- });
982
+ }, { timeoutMs: INPUT_ACTION_TIMEOUT_MS });
798
983
  const centerX = Number(target?.result?.center?.x);
799
984
  const centerY = Number(target?.result?.center?.y);
800
985
  if (Number.isFinite(centerX) && Number.isFinite(centerY)) {
801
- await callAPI('mouse:move', { profileId, x: centerX, y: centerY, steps: 2 });
986
+ await callAPI('mouse:move', { profileId, x: centerX, y: centerY, steps: 2 }, { timeoutMs: INPUT_ACTION_TIMEOUT_MS });
802
987
  }
803
-
804
988
  const deltaX = direction === 'left' ? -amount : direction === 'right' ? amount : 0;
805
989
  const deltaY = direction === 'up' ? -amount : direction === 'down' ? amount : 0;
806
- const result = await callAPI('mouse:wheel', { profileId, deltaX, deltaY });
990
+ const result = await callAPI('mouse:wheel', { profileId, deltaX, deltaY }, { timeoutMs: INPUT_ACTION_TIMEOUT_MS });
807
991
  console.log(JSON.stringify({
808
992
  ...result,
809
993
  scrollTarget: target?.result || null,
@@ -829,11 +1013,33 @@ export async function handleClickCommand(args) {
829
1013
  if (!profileId) throw new Error('Usage: camo click [profileId] <selector> [--highlight|--no-highlight]');
830
1014
  if (!selector) throw new Error('Usage: camo click [profileId] <selector> [--highlight|--no-highlight]');
831
1015
 
832
- const result = await callAPI('evaluate', {
833
- profileId,
834
- script: buildSelectorClickScript({ selector, highlight }),
1016
+ let target = await resolveVisibleTargetPoint(profileId, selector, { highlight });
1017
+ const ensured = await ensureClickTargetInViewport(profileId, selector, target, {
1018
+ maxAutoScrollSteps: 3,
835
1019
  });
836
- console.log(JSON.stringify(result, null, 2));
1020
+ if (!ensured.targetFullyVisible) {
1021
+ throw new Error(`Click target not fully visible after ${ensured.autoScrolled} auto-scroll attempts: ${selector}`);
1022
+ }
1023
+ target = ensured.target;
1024
+ if (highlight) {
1025
+ target = await resolveVisibleTargetPoint(profileId, selector, { highlight: true });
1026
+ }
1027
+ await callAPI('mouse:move', { profileId, x: target.center.x, y: target.center.y, steps: 2 }, { timeoutMs: INPUT_ACTION_TIMEOUT_MS });
1028
+ const result = await callAPI('mouse:click', {
1029
+ profileId,
1030
+ x: target.center.x,
1031
+ y: target.center.y,
1032
+ button: 'left',
1033
+ clicks: 1,
1034
+ }, { timeoutMs: INPUT_ACTION_TIMEOUT_MS });
1035
+ console.log(JSON.stringify({
1036
+ ...result,
1037
+ selector,
1038
+ highlight,
1039
+ autoScrolled: ensured.autoScrolled,
1040
+ targetFullyVisible: ensured.targetFullyVisible,
1041
+ target,
1042
+ }, null, 2));
837
1043
  }
838
1044
 
839
1045
  export async function handleTypeCommand(args) {
@@ -857,15 +1063,36 @@ export async function handleTypeCommand(args) {
857
1063
  if (!profileId) throw new Error('Usage: camo type [profileId] <selector> <text> [--highlight|--no-highlight]');
858
1064
  if (!selector || text === undefined) throw new Error('Usage: camo type [profileId] <selector> <text> [--highlight|--no-highlight]');
859
1065
 
860
- const result = await callAPI('evaluate', {
1066
+ const target = await resolveVisibleTargetPoint(profileId, selector, { highlight });
1067
+ await callAPI('mouse:move', { profileId, x: target.center.x, y: target.center.y, steps: 2 }, { timeoutMs: INPUT_ACTION_TIMEOUT_MS });
1068
+ await callAPI('mouse:click', {
861
1069
  profileId,
862
- script: buildSelectorTypeScript({ selector, highlight, text }),
863
- });
864
- console.log(JSON.stringify(result, null, 2));
1070
+ x: target.center.x,
1071
+ y: target.center.y,
1072
+ button: 'left',
1073
+ clicks: 1,
1074
+ }, { timeoutMs: INPUT_ACTION_TIMEOUT_MS });
1075
+ await callAPI('keyboard:press', {
1076
+ profileId,
1077
+ key: process.platform === 'darwin' ? 'Meta+A' : 'Control+A',
1078
+ }, { timeoutMs: INPUT_ACTION_TIMEOUT_MS });
1079
+ await callAPI('keyboard:press', { profileId, key: 'Backspace' }, { timeoutMs: INPUT_ACTION_TIMEOUT_MS });
1080
+ const result = await callAPI('keyboard:type', {
1081
+ profileId,
1082
+ text: String(text),
1083
+ }, { timeoutMs: INPUT_ACTION_TIMEOUT_MS });
1084
+ console.log(JSON.stringify({
1085
+ ...result,
1086
+ selector,
1087
+ typed: String(text).length,
1088
+ highlight,
1089
+ target,
1090
+ }, null, 2));
865
1091
  }
866
1092
 
867
1093
  export async function handleHighlightCommand(args) {
868
1094
  await ensureBrowserService();
1095
+ ensureJsExecutionEnabled('highlight command');
869
1096
  const positionals = getPositionals(args);
870
1097
  let profileId;
871
1098
  let selector;
@@ -898,6 +1125,7 @@ export async function handleHighlightCommand(args) {
898
1125
 
899
1126
  export async function handleClearHighlightCommand(args) {
900
1127
  await ensureBrowserService();
1128
+ ensureJsExecutionEnabled('clear-highlight command');
901
1129
  const profileId = resolveProfileId(args, 1, getDefaultProfile);
902
1130
  if (!profileId) throw new Error('Usage: camo clear-highlight [profileId]');
903
1131
 
@@ -2,6 +2,12 @@ import { getDefaultProfile } from '../utils/config.mjs';
2
2
  import { callAPI, ensureBrowserService } from '../utils/browser-service.mjs';
3
3
  import { getPositionals } from '../utils/args.mjs';
4
4
 
5
+ const INPUT_ACTION_TIMEOUT_MS = (() => {
6
+ const parsed = Number(process.env.CAMO_INPUT_ACTION_TIMEOUT_MS);
7
+ if (!Number.isFinite(parsed) || parsed <= 0) return 30000;
8
+ return Math.max(1000, Math.floor(parsed));
9
+ })();
10
+
5
11
  export async function handleMouseCommand(args) {
6
12
  await ensureBrowserService();
7
13
 
@@ -17,7 +23,7 @@ export async function handleMouseCommand(args) {
17
23
  const x = parseInt(args[xIdx + 1]);
18
24
  const y = parseInt(args[yIdx + 1]);
19
25
  const steps = stepsIdx >= 0 ? parseInt(args[stepsIdx + 1]) : undefined;
20
- const result = await callAPI('mouse:move', { profileId, x, y, steps });
26
+ const result = await callAPI('mouse:move', { profileId, x, y, steps }, { timeoutMs: INPUT_ACTION_TIMEOUT_MS });
21
27
  console.log(JSON.stringify(result, null, 2));
22
28
  } else if (sub === 'click') {
23
29
  // Use existing click command? We already have click command for element clicking.
@@ -33,7 +39,7 @@ export async function handleMouseCommand(args) {
33
39
  const button = buttonIdx >= 0 ? args[buttonIdx + 1] : 'left';
34
40
  const clicks = clicksIdx >= 0 ? parseInt(args[clicksIdx + 1]) : 1;
35
41
  const delay = delayIdx >= 0 ? parseInt(args[delayIdx + 1]) : undefined;
36
- const result = await callAPI('mouse:click', { profileId, x, y, button, clicks, delay });
42
+ const result = await callAPI('mouse:click', { profileId, x, y, button, clicks, delay }, { timeoutMs: INPUT_ACTION_TIMEOUT_MS });
37
43
  console.log(JSON.stringify(result, null, 2));
38
44
  } else if (sub === 'wheel') {
39
45
  const deltaXIdx = args.indexOf('--deltax');
@@ -41,7 +47,7 @@ export async function handleMouseCommand(args) {
41
47
  if (deltaXIdx === -1 && deltaYIdx === -1) throw new Error('Usage: camo mouse wheel [profileId] [--deltax <px>] [--deltay <px>]');
42
48
  const deltaX = deltaXIdx >= 0 ? parseInt(args[deltaXIdx + 1]) : 0;
43
49
  const deltaY = deltaYIdx >= 0 ? parseInt(args[deltaYIdx + 1]) : 0;
44
- const result = await callAPI('mouse:wheel', { profileId, deltaX, deltaY });
50
+ const result = await callAPI('mouse:wheel', { profileId, deltaX, deltaY }, { timeoutMs: INPUT_ACTION_TIMEOUT_MS });
45
51
  console.log(JSON.stringify(result, null, 2));
46
52
  } else {
47
53
  throw new Error('Usage: camo mouse <move|click|wheel> [profileId] [options]');
@@ -40,14 +40,28 @@ export const XHS_CHECKPOINTS = {
40
40
  ],
41
41
  };
42
42
 
43
- export async function detectCheckpoint({ profileId, platform = 'xiaohongshu' }) {
44
- if (platform !== 'xiaohongshu') {
45
- return asErrorPayload('UNSUPPORTED_PLATFORM', `Unsupported platform: ${platform}`);
46
- }
47
-
43
+ export async function detectCheckpoint({ profileId, platform = 'generic' }) {
48
44
  try {
49
45
  const session = await ensureActiveSession(profileId);
50
46
  const resolvedProfile = session.profileId || profileId;
47
+
48
+ if (platform !== 'xiaohongshu') {
49
+ const url = await getCurrentUrl(resolvedProfile);
50
+ return {
51
+ ok: true,
52
+ code: 'CHECKPOINT_DETECTED',
53
+ message: 'Checkpoint detected',
54
+ data: {
55
+ profileId: resolvedProfile,
56
+ platform,
57
+ checkpoint: 'unknown',
58
+ url,
59
+ signals: [],
60
+ selectorHits: {},
61
+ },
62
+ };
63
+ }
64
+
51
65
  const [url, snapshot] = await Promise.all([
52
66
  getCurrentUrl(resolvedProfile),
53
67
  getDomSnapshotByProfile(resolvedProfile),
@@ -103,7 +117,7 @@ export async function captureCheckpoint({
103
117
  profileId,
104
118
  containerId = null,
105
119
  selector = null,
106
- platform = 'xiaohongshu',
120
+ platform = 'generic',
107
121
  }) {
108
122
  try {
109
123
  const session = await ensureActiveSession(profileId);
@@ -139,7 +153,7 @@ export async function restoreCheckpoint({
139
153
  containerId = null,
140
154
  selector = null,
141
155
  targetCheckpoint = null,
142
- platform = 'xiaohongshu',
156
+ platform = 'generic',
143
157
  }) {
144
158
  try {
145
159
  const session = await ensureActiveSession(profileId);