surf-cli 2.10.0 → 2.11.0
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/README.md +13 -0
- package/native/chatgpt-client-response.cjs +336 -0
- package/native/chatgpt-client-selection.cjs +119 -0
- package/native/chatgpt-client-ui.cjs +481 -0
- package/native/chatgpt-client.cjs +252 -664
- package/native/cli.cjs +53 -0
- package/native/host-helpers.cjs +26 -1
- package/native/host-sessions.cjs +1 -0
- package/native/host.cjs +29 -5
- package/native/oracle-cli.cjs +434 -0
- package/native/oracle-context.cjs +311 -0
- package/native/oracle-host.cjs +301 -0
- package/native/oracle-jobs.cjs +253 -0
- package/native/workflow-definition.cjs +1 -0
- package/package.json +1 -1
- package/skills/surf/SKILL.md +31 -0
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
const { abortableDelay, throwIfAborted } = require("./abort.cjs");
|
|
2
|
+
const {
|
|
3
|
+
CHATGPT_EFFORT_CHOICES,
|
|
4
|
+
boundedOptionLabels,
|
|
5
|
+
normalizeChatGPTEffortChoice,
|
|
6
|
+
normalizeChatGPTModelChoice,
|
|
7
|
+
resolveChatGPTEffortMenuOption,
|
|
8
|
+
resolveChatGPTModelMenuOption,
|
|
9
|
+
verifyChatGPTEffortSelection,
|
|
10
|
+
verifyChatGPTModelSelection,
|
|
11
|
+
} = require("./chatgpt-client-selection.cjs");
|
|
12
|
+
|
|
13
|
+
const SELECTORS = {
|
|
14
|
+
promptTextarea:
|
|
15
|
+
'#prompt-textarea, [data-testid="composer-textarea"], textarea[name="prompt-textarea"], .ProseMirror, [contenteditable="true"][data-virtualkeyboard="true"]',
|
|
16
|
+
promptEditor: "#prompt-textarea",
|
|
17
|
+
promptFallback: 'textarea[name="prompt-textarea"]',
|
|
18
|
+
loginCta: 'a[href*="/auth/login"], button',
|
|
19
|
+
sendButton:
|
|
20
|
+
'button[data-testid="send-button"], button[data-testid*="composer-send"], form button[type="submit"]',
|
|
21
|
+
modelButton: '[data-testid="model-switcher-dropdown-button"]',
|
|
22
|
+
modelMenu: '[role="menu"][data-radix-menu-content]',
|
|
23
|
+
modelMenuItem: '[role="menuitemradio"][data-testid^="model-switcher-"]',
|
|
24
|
+
menuItemPrimaryLabel: '.min-w-0 > span',
|
|
25
|
+
effortButton:
|
|
26
|
+
'[data-testid="composer-footer-actions"] button[aria-haspopup="menu"], button.__composer-pill[aria-haspopup="menu"], .__composer-pill-composite button[aria-haspopup="menu"]',
|
|
27
|
+
effortMenu: '[role="menu"], [data-radix-collection-root], [role="group"]',
|
|
28
|
+
effortMenuItem: 'button, [role="menuitem"], [role="menuitemradio"]',
|
|
29
|
+
effortMenuLabel: '.__menu-label, [class*="menu-label"]',
|
|
30
|
+
effortSubmenuTrigger: '[role="menuitem"][aria-haspopup="menu"], button[aria-haspopup="menu"]',
|
|
31
|
+
selectedMenuIndicator:
|
|
32
|
+
'[aria-checked="true"], [aria-selected="true"], [data-selected="true"], [data-state="checked"], [data-state="selected"], [data-state="on"]',
|
|
33
|
+
assistantMessage:
|
|
34
|
+
'[data-message-author-role="assistant"], [data-turn="assistant"], [data-testid*="assistant-message"], [data-testid*="assistant-turn"], [data-testid*="assistant-response"]',
|
|
35
|
+
assistantContent:
|
|
36
|
+
'.markdown, [data-message-content], .prose, [class*="markdown"], [dir="auto"]',
|
|
37
|
+
stopButton:
|
|
38
|
+
'[data-testid="stop-button"], [data-testid*="stop"], button[aria-label*="Stop"], button[aria-label*="stop"]',
|
|
39
|
+
finishedActions:
|
|
40
|
+
'button[data-testid="copy-turn-action-button"], button[data-testid="good-response-turn-action-button"], button[data-testid*="turn-action"], button[aria-label*="Copy"], button[aria-label*="copy"], button[aria-label*="Read aloud"], button[aria-label*="read aloud"]',
|
|
41
|
+
conversationTurn: '[data-testid^="conversation-turn"], [data-testid*="conversation-turn"]',
|
|
42
|
+
cloudflareScript: 'script[src*="/challenge-platform/"]',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function delay(ms, signal) {
|
|
46
|
+
return abortableDelay(ms, signal);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildClickDispatcher() {
|
|
50
|
+
return `function dispatchClickSequence(target){
|
|
51
|
+
if(!target || !(target instanceof EventTarget)) return false;
|
|
52
|
+
const types = ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click'];
|
|
53
|
+
for (const type of types) {
|
|
54
|
+
const common = { bubbles: true, cancelable: true, view: window };
|
|
55
|
+
let event;
|
|
56
|
+
if (type.startsWith('pointer') && 'PointerEvent' in window) {
|
|
57
|
+
event = new PointerEvent(type, { ...common, pointerId: 1, pointerType: 'mouse' });
|
|
58
|
+
} else {
|
|
59
|
+
event = new MouseEvent(type, common);
|
|
60
|
+
}
|
|
61
|
+
target.dispatchEvent(event);
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function evaluate(cdp, expression, signal) {
|
|
68
|
+
throwIfAborted(signal);
|
|
69
|
+
const result = await cdp(expression);
|
|
70
|
+
throwIfAborted(signal);
|
|
71
|
+
if (result.exceptionDetails) {
|
|
72
|
+
const desc =
|
|
73
|
+
result.exceptionDetails.exception?.description ||
|
|
74
|
+
result.exceptionDetails.text ||
|
|
75
|
+
"Evaluation failed";
|
|
76
|
+
throw new Error(desc);
|
|
77
|
+
}
|
|
78
|
+
if (result.error) {
|
|
79
|
+
throw new Error(result.error);
|
|
80
|
+
}
|
|
81
|
+
return result.result?.value;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function waitForPageLoad(cdp, timeoutMs = 45000, signal) {
|
|
85
|
+
throwIfAborted(signal);
|
|
86
|
+
const deadline = Date.now() + timeoutMs;
|
|
87
|
+
while (Date.now() < deadline) {
|
|
88
|
+
const ready = await evaluate(cdp, "document.readyState");
|
|
89
|
+
if (ready === "complete" || ready === "interactive") {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
await delay(100, signal);
|
|
93
|
+
}
|
|
94
|
+
throw new Error("Page did not load in time");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function isCloudflareBlocked(cdp) {
|
|
98
|
+
const title = await evaluate(cdp, "document.title.toLowerCase()");
|
|
99
|
+
if (title && (title.includes("just a moment") || title.includes("verify you are human"))) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
return evaluate(
|
|
103
|
+
cdp,
|
|
104
|
+
`(() => {
|
|
105
|
+
const hasPrompt = Boolean(document.querySelector(${JSON.stringify(SELECTORS.promptTextarea)}));
|
|
106
|
+
if (hasPrompt) return false;
|
|
107
|
+
const text = (document.body?.innerText || '').toLowerCase();
|
|
108
|
+
const challengeText = [
|
|
109
|
+
'checking if the site connection is secure',
|
|
110
|
+
'verify you are human',
|
|
111
|
+
'review the security of your connection',
|
|
112
|
+
'needs to review the security of your connection',
|
|
113
|
+
'cloudflare ray id'
|
|
114
|
+
];
|
|
115
|
+
return challengeText.some(marker => text.includes(marker))
|
|
116
|
+
|| Boolean(document.querySelector('input[name="cf-turnstile-response"], .cf-turnstile, #challenge-stage, iframe[src*="challenges.cloudflare.com"]'));
|
|
117
|
+
})()`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function checkLoginStatus(cdp) {
|
|
122
|
+
const result = await evaluate(
|
|
123
|
+
cdp,
|
|
124
|
+
`(async () => {
|
|
125
|
+
try {
|
|
126
|
+
const response = await fetch('/backend-api/me', {
|
|
127
|
+
cache: 'no-store',
|
|
128
|
+
credentials: 'include'
|
|
129
|
+
});
|
|
130
|
+
const hasLoginCta = Array.from(document.querySelectorAll(${JSON.stringify(SELECTORS.loginCta)}))
|
|
131
|
+
.some(el => {
|
|
132
|
+
const text = (el.textContent || '').toLowerCase().trim();
|
|
133
|
+
return text.startsWith('log in') || text.startsWith('sign in');
|
|
134
|
+
});
|
|
135
|
+
return {
|
|
136
|
+
status: response.status,
|
|
137
|
+
hasLoginCta,
|
|
138
|
+
url: location.href
|
|
139
|
+
};
|
|
140
|
+
} catch (e) {
|
|
141
|
+
return { status: 0, error: e.message, url: location.href };
|
|
142
|
+
}
|
|
143
|
+
})()`,
|
|
144
|
+
);
|
|
145
|
+
return result || { status: 0 };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function waitForPromptReady(cdp, timeoutMs = 30000, signal) {
|
|
149
|
+
throwIfAborted(signal);
|
|
150
|
+
const deadline = Date.now() + timeoutMs;
|
|
151
|
+
const selectors = JSON.stringify(SELECTORS.promptTextarea.split(", "));
|
|
152
|
+
while (Date.now() < deadline) {
|
|
153
|
+
const found = await evaluate(
|
|
154
|
+
cdp,
|
|
155
|
+
`(() => {
|
|
156
|
+
const selectors = ${selectors};
|
|
157
|
+
for (const selector of selectors) {
|
|
158
|
+
const node = document.querySelector(selector);
|
|
159
|
+
if (node && !node.hasAttribute('disabled')) {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return false;
|
|
164
|
+
})()`,
|
|
165
|
+
);
|
|
166
|
+
if (found) return true;
|
|
167
|
+
await delay(200, signal);
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function verificationError(kind, requested, items = [], invalid = false) {
|
|
173
|
+
const safeRequested = String(requested || "").replace(/\s+/g, " ").trim().slice(0, 80);
|
|
174
|
+
const available = boundedOptionLabels(items);
|
|
175
|
+
const accepted = kind === "effort" ? ` Accepted: ${CHATGPT_EFFORT_CHOICES.join(", ")}.` : "";
|
|
176
|
+
const availableMessage = available.length > 0 ? ` Available: ${available.join(", ")}.` : "";
|
|
177
|
+
const error = new Error(
|
|
178
|
+
invalid
|
|
179
|
+
? `Invalid ChatGPT effort "${safeRequested}".${accepted}`
|
|
180
|
+
: `ChatGPT ${kind} verification failed for "${safeRequested}".${accepted}${availableMessage}`,
|
|
181
|
+
);
|
|
182
|
+
error.code = "model_verification_failed";
|
|
183
|
+
return error;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function readPicker(cdp, kind, click = false) {
|
|
187
|
+
const selector = kind === "model" ? SELECTORS.modelButton : SELECTORS.effortButton;
|
|
188
|
+
return evaluate(
|
|
189
|
+
cdp,
|
|
190
|
+
`(() => {
|
|
191
|
+
${buildClickDispatcher()}
|
|
192
|
+
const kind = ${JSON.stringify(kind)};
|
|
193
|
+
const nodes = Array.from(document.querySelectorAll(${JSON.stringify(selector)})).filter((node) => {
|
|
194
|
+
if (kind === 'model') return true;
|
|
195
|
+
const value = ((node.getAttribute?.('aria-label') || '') + ' ' + (node.textContent || '')).toLowerCase();
|
|
196
|
+
return value.includes('thinking') || value.includes('pro');
|
|
197
|
+
});
|
|
198
|
+
const items = nodes.map((node) => {
|
|
199
|
+
const text = (node.textContent || '').replace(/\\s+/g, ' ').trim();
|
|
200
|
+
const aria = (node.getAttribute?.('aria-label') || '').replace(/\\s+/g, ' ').trim();
|
|
201
|
+
const title = (node.getAttribute?.('title') || '').replace(/\\s+/g, ' ').trim();
|
|
202
|
+
return {
|
|
203
|
+
role: node.getAttribute?.('role') || (node.tagName === 'BUTTON' ? 'button' : null),
|
|
204
|
+
label: [text, aria, title].filter(Boolean).join(' | ').slice(0, 240),
|
|
205
|
+
displayLabel: (text || aria || title).slice(0, 80),
|
|
206
|
+
testId: node.getAttribute?.('data-testid') || null,
|
|
207
|
+
};
|
|
208
|
+
});
|
|
209
|
+
if (${click} && nodes.length === 1) dispatchClickSequence(nodes[0]);
|
|
210
|
+
return { items };
|
|
211
|
+
})()`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function readMenu(cdp, kind, allowSubmenu) {
|
|
216
|
+
const isModel = kind === "model";
|
|
217
|
+
const menuSelector = isModel ? SELECTORS.modelMenu : SELECTORS.effortMenu;
|
|
218
|
+
const itemSelector = isModel ? SELECTORS.modelMenuItem : SELECTORS.effortMenuItem;
|
|
219
|
+
return evaluate(
|
|
220
|
+
cdp,
|
|
221
|
+
`(() => {
|
|
222
|
+
${buildClickDispatcher()}
|
|
223
|
+
const isModel = ${isModel};
|
|
224
|
+
const choices = ${JSON.stringify(CHATGPT_EFFORT_CHOICES)};
|
|
225
|
+
const containers = Array.from(document.querySelectorAll(${JSON.stringify(menuSelector)}));
|
|
226
|
+
const normalize = (value) => String(value || '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
|
|
227
|
+
let menu = isModel ? containers[0] : containers.find((container) => {
|
|
228
|
+
const label = normalize(container.querySelector?.(${JSON.stringify(SELECTORS.effortMenuLabel)})?.textContent);
|
|
229
|
+
const levels = new Set(Array.from(container.querySelectorAll(${JSON.stringify(itemSelector)}))
|
|
230
|
+
.flatMap((item) => normalize(item.textContent).split(/\\s+/))
|
|
231
|
+
.filter((word) => choices.includes(word)));
|
|
232
|
+
return label.includes('thinking time') || levels.size >= 2;
|
|
233
|
+
});
|
|
234
|
+
if (!menu && !isModel && ${allowSubmenu}) {
|
|
235
|
+
for (const container of containers) {
|
|
236
|
+
const trigger = Array.from(container.querySelectorAll(${JSON.stringify(SELECTORS.effortSubmenuTrigger)}))
|
|
237
|
+
.find((item) => {
|
|
238
|
+
const label = normalize((item.getAttribute?.('aria-label') || '') + ' ' + (item.textContent || ''));
|
|
239
|
+
return label.includes('thinking time') || label.includes('reasoning effort');
|
|
240
|
+
});
|
|
241
|
+
if (trigger) {
|
|
242
|
+
dispatchClickSequence(trigger);
|
|
243
|
+
return { found: false, submenuOpened: true, items: [] };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (!menu) return { found: false, items: [] };
|
|
248
|
+
const items = Array.from(menu.querySelectorAll(${JSON.stringify(itemSelector)})).map((item) => {
|
|
249
|
+
const primary = isModel ? item.querySelector?.(${JSON.stringify(SELECTORS.menuItemPrimaryLabel)}) : null;
|
|
250
|
+
const label = (primary?.textContent || item.getAttribute?.('aria-label') || item.textContent || '')
|
|
251
|
+
.replace(/\\s+/g, ' ').trim().slice(0, 80);
|
|
252
|
+
const state = (item.getAttribute?.('data-state') || '').toLowerCase();
|
|
253
|
+
const selected = item.getAttribute?.('aria-checked') === 'true' ||
|
|
254
|
+
item.getAttribute?.('aria-selected') === 'true' || item.getAttribute?.('data-selected') === 'true' ||
|
|
255
|
+
['checked', 'selected', 'on'].includes(state) ||
|
|
256
|
+
Boolean(item.querySelector?.(${JSON.stringify(SELECTORS.selectedMenuIndicator)}));
|
|
257
|
+
return {
|
|
258
|
+
role: item.getAttribute?.('role') || (item.tagName === 'BUTTON' ? 'button' : null),
|
|
259
|
+
label,
|
|
260
|
+
testId: item.getAttribute?.('data-testid') || null,
|
|
261
|
+
selected,
|
|
262
|
+
};
|
|
263
|
+
});
|
|
264
|
+
return { found: true, items };
|
|
265
|
+
})()`,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function waitForMenu(cdp, kind, timeoutMs, signal) {
|
|
270
|
+
const deadline = Date.now() + timeoutMs;
|
|
271
|
+
let allowSubmenu = true;
|
|
272
|
+
while (Date.now() < deadline) {
|
|
273
|
+
const result = await readMenu(cdp, kind, allowSubmenu);
|
|
274
|
+
if (result?.found) return result;
|
|
275
|
+
if (result?.submenuOpened) allowSubmenu = false;
|
|
276
|
+
await delay(100, signal);
|
|
277
|
+
}
|
|
278
|
+
return { found: false, items: [] };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function clickMenuItem(cdp, kind, match) {
|
|
282
|
+
const isModel = kind === "model";
|
|
283
|
+
const menuSelector = isModel ? SELECTORS.modelMenu : SELECTORS.effortMenu;
|
|
284
|
+
const itemSelector = isModel ? SELECTORS.modelMenuItem : SELECTORS.effortMenuItem;
|
|
285
|
+
return evaluate(
|
|
286
|
+
cdp,
|
|
287
|
+
`(() => {
|
|
288
|
+
${buildClickDispatcher()}
|
|
289
|
+
const expectedTestId = ${JSON.stringify(match.testId)};
|
|
290
|
+
const expectedLabel = ${JSON.stringify(match.label)};
|
|
291
|
+
const items = Array.from(new Set(
|
|
292
|
+
Array.from(document.querySelectorAll(${JSON.stringify(menuSelector)}))
|
|
293
|
+
.flatMap((menu) => Array.from(menu.querySelectorAll(${JSON.stringify(itemSelector)}))),
|
|
294
|
+
));
|
|
295
|
+
const matches = items.filter((item) => {
|
|
296
|
+
if (expectedTestId) return item.getAttribute?.('data-testid') === expectedTestId;
|
|
297
|
+
const primary = ${isModel} ? item.querySelector?.(${JSON.stringify(SELECTORS.menuItemPrimaryLabel)}) : null;
|
|
298
|
+
const label = (primary?.textContent || item.getAttribute?.('aria-label') || item.textContent || '')
|
|
299
|
+
.replace(/\\s+/g, ' ').trim().slice(0, 80);
|
|
300
|
+
return label === expectedLabel;
|
|
301
|
+
});
|
|
302
|
+
return matches.length === 1 ? dispatchClickSequence(matches[0]) : false;
|
|
303
|
+
})()`,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function selectModel(cdp, desiredModel, timeoutMs = 8000, signal) {
|
|
308
|
+
throwIfAborted(signal);
|
|
309
|
+
const picker = await readPicker(cdp, "model", true);
|
|
310
|
+
if (picker?.items?.length !== 1) throw verificationError("model", desiredModel);
|
|
311
|
+
await delay(300, signal);
|
|
312
|
+
const menu = await waitForMenu(cdp, "model", timeoutMs, signal);
|
|
313
|
+
const match = resolveChatGPTModelMenuOption(menu.items, desiredModel);
|
|
314
|
+
if (!match || !(await clickMenuItem(cdp, "model", match))) {
|
|
315
|
+
throw verificationError("model", desiredModel, menu.items);
|
|
316
|
+
}
|
|
317
|
+
await delay(200, signal);
|
|
318
|
+
const state = await readPicker(cdp, "model");
|
|
319
|
+
const verified = verifyChatGPTModelSelection(state?.items, desiredModel);
|
|
320
|
+
if (!verified) throw verificationError("model", desiredModel, menu.items);
|
|
321
|
+
return verified.displayLabel || verified.label;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function selectEffort(cdp, desiredEffort, timeoutMs = 8000, signal) {
|
|
325
|
+
throwIfAborted(signal);
|
|
326
|
+
const normalizedEffort = normalizeChatGPTEffortChoice(desiredEffort);
|
|
327
|
+
if (!normalizedEffort) throw verificationError("effort", desiredEffort, [], true);
|
|
328
|
+
const picker = await readPicker(cdp, "effort", true);
|
|
329
|
+
if (picker?.items?.length !== 1) throw verificationError("effort", desiredEffort);
|
|
330
|
+
await delay(300, signal);
|
|
331
|
+
const menu = await waitForMenu(cdp, "effort", timeoutMs, signal);
|
|
332
|
+
const match = resolveChatGPTEffortMenuOption(menu.items, normalizedEffort);
|
|
333
|
+
if (!match || !(await clickMenuItem(cdp, "effort", match))) {
|
|
334
|
+
throw verificationError("effort", desiredEffort, menu.items);
|
|
335
|
+
}
|
|
336
|
+
await delay(200, signal);
|
|
337
|
+
const pillState = await readPicker(cdp, "effort");
|
|
338
|
+
const pillVerified = verifyChatGPTEffortSelection(pillState?.items, normalizedEffort);
|
|
339
|
+
if (pillVerified) return pillVerified.displayLabel || pillVerified.label;
|
|
340
|
+
|
|
341
|
+
const reopened = await readPicker(cdp, "effort", true);
|
|
342
|
+
if (reopened?.items?.length !== 1) throw verificationError("effort", desiredEffort, menu.items);
|
|
343
|
+
const readbackMenu = await waitForMenu(cdp, "effort", timeoutMs, signal);
|
|
344
|
+
const verified = verifyChatGPTEffortSelection(
|
|
345
|
+
readbackMenu.items.filter((item) => item.selected),
|
|
346
|
+
normalizedEffort,
|
|
347
|
+
);
|
|
348
|
+
if (!verified) throw verificationError("effort", desiredEffort, readbackMenu.items);
|
|
349
|
+
return verified.label;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function typePrompt(cdp, inputCdp, prompt, signal) {
|
|
353
|
+
throwIfAborted(signal);
|
|
354
|
+
const selectors = JSON.stringify(SELECTORS.promptTextarea.split(", "));
|
|
355
|
+
const encodedPrompt = JSON.stringify(prompt);
|
|
356
|
+
const focused = await evaluate(
|
|
357
|
+
cdp,
|
|
358
|
+
`(() => {
|
|
359
|
+
${buildClickDispatcher()}
|
|
360
|
+
const selectors = ${selectors};
|
|
361
|
+
for (const selector of selectors) {
|
|
362
|
+
const node = document.querySelector(selector);
|
|
363
|
+
if (!node) continue;
|
|
364
|
+
dispatchClickSequence(node);
|
|
365
|
+
if (typeof node.focus === 'function') node.focus();
|
|
366
|
+
const doc = node.ownerDocument;
|
|
367
|
+
const selection = doc?.getSelection?.();
|
|
368
|
+
if (selection) {
|
|
369
|
+
const range = doc.createRange();
|
|
370
|
+
range.selectNodeContents(node);
|
|
371
|
+
range.collapse(false);
|
|
372
|
+
selection.removeAllRanges();
|
|
373
|
+
selection.addRange(range);
|
|
374
|
+
}
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
return false;
|
|
378
|
+
})()`,
|
|
379
|
+
);
|
|
380
|
+
if (!focused) {
|
|
381
|
+
throw new Error("Failed to focus prompt textarea");
|
|
382
|
+
}
|
|
383
|
+
await inputCdp("Input.insertText", { text: prompt });
|
|
384
|
+
await delay(300, signal);
|
|
385
|
+
const verified = await evaluate(
|
|
386
|
+
cdp,
|
|
387
|
+
`(() => {
|
|
388
|
+
const selectors = ${selectors};
|
|
389
|
+
for (const selector of selectors) {
|
|
390
|
+
const node = document.querySelector(selector);
|
|
391
|
+
if (!node) continue;
|
|
392
|
+
const text = node.innerText || node.value || node.textContent || '';
|
|
393
|
+
if (text.trim().length > 0) return true;
|
|
394
|
+
}
|
|
395
|
+
return false;
|
|
396
|
+
})()`,
|
|
397
|
+
);
|
|
398
|
+
if (!verified) {
|
|
399
|
+
await evaluate(
|
|
400
|
+
cdp,
|
|
401
|
+
`(() => {
|
|
402
|
+
const editor = document.querySelector(${JSON.stringify(SELECTORS.promptEditor)});
|
|
403
|
+
const fallback = document.querySelector(${JSON.stringify(SELECTORS.promptFallback)});
|
|
404
|
+
if (fallback) {
|
|
405
|
+
fallback.value = ${encodedPrompt};
|
|
406
|
+
fallback.dispatchEvent(new InputEvent('input', { bubbles: true, data: ${encodedPrompt}, inputType: 'insertFromPaste' }));
|
|
407
|
+
}
|
|
408
|
+
if (editor) {
|
|
409
|
+
editor.textContent = ${encodedPrompt};
|
|
410
|
+
editor.dispatchEvent(new InputEvent('input', { bubbles: true, data: ${encodedPrompt}, inputType: 'insertFromPaste' }));
|
|
411
|
+
}
|
|
412
|
+
})()`,
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function clickSend(cdp, inputCdp, signal) {
|
|
418
|
+
throwIfAborted(signal);
|
|
419
|
+
const selectorsJson = JSON.stringify(SELECTORS.sendButton.split(", "));
|
|
420
|
+
const deadline = Date.now() + 8000;
|
|
421
|
+
while (Date.now() < deadline) {
|
|
422
|
+
const result = await evaluate(
|
|
423
|
+
cdp,
|
|
424
|
+
`(() => {
|
|
425
|
+
${buildClickDispatcher()}
|
|
426
|
+
const selectors = ${selectorsJson};
|
|
427
|
+
let button = null;
|
|
428
|
+
for (const selector of selectors) {
|
|
429
|
+
button = document.querySelector(selector);
|
|
430
|
+
if (button) break;
|
|
431
|
+
}
|
|
432
|
+
if (!button) return 'missing';
|
|
433
|
+
const disabled = button.hasAttribute('disabled') ||
|
|
434
|
+
button.getAttribute('aria-disabled') === 'true' ||
|
|
435
|
+
button.getAttribute('data-disabled') === 'true';
|
|
436
|
+
if (disabled) return 'disabled';
|
|
437
|
+
dispatchClickSequence(button);
|
|
438
|
+
return 'clicked';
|
|
439
|
+
})()`,
|
|
440
|
+
);
|
|
441
|
+
if (result === "clicked") return true;
|
|
442
|
+
if (result === "missing") break;
|
|
443
|
+
await delay(100, signal);
|
|
444
|
+
}
|
|
445
|
+
await inputCdp("Input.dispatchKeyEvent", {
|
|
446
|
+
type: "keyDown",
|
|
447
|
+
key: "Enter",
|
|
448
|
+
code: "Enter",
|
|
449
|
+
windowsVirtualKeyCode: 13,
|
|
450
|
+
nativeVirtualKeyCode: 13,
|
|
451
|
+
text: "\r",
|
|
452
|
+
});
|
|
453
|
+
await inputCdp("Input.dispatchKeyEvent", {
|
|
454
|
+
type: "keyUp",
|
|
455
|
+
key: "Enter",
|
|
456
|
+
code: "Enter",
|
|
457
|
+
windowsVirtualKeyCode: 13,
|
|
458
|
+
nativeVirtualKeyCode: 13,
|
|
459
|
+
});
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
module.exports = {
|
|
464
|
+
SELECTORS,
|
|
465
|
+
checkLoginStatus,
|
|
466
|
+
clickSend,
|
|
467
|
+
delay,
|
|
468
|
+
evaluate,
|
|
469
|
+
isCloudflareBlocked,
|
|
470
|
+
normalizeChatGPTEffortChoice,
|
|
471
|
+
normalizeChatGPTModelChoice,
|
|
472
|
+
resolveChatGPTEffortMenuOption,
|
|
473
|
+
resolveChatGPTModelMenuOption,
|
|
474
|
+
selectEffort,
|
|
475
|
+
selectModel,
|
|
476
|
+
verifyChatGPTEffortSelection,
|
|
477
|
+
verifyChatGPTModelSelection,
|
|
478
|
+
typePrompt,
|
|
479
|
+
waitForPageLoad,
|
|
480
|
+
waitForPromptReady,
|
|
481
|
+
};
|