surf-cli 2.10.0 → 2.12.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 +46 -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 +86 -3
- package/native/host-helpers.cjs +38 -1
- package/native/host-sessions.cjs +1 -0
- package/native/host.cjs +30 -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/playbook-cli.cjs +3 -2
- package/native/playbook-runtime.cjs +29 -2
- package/native/playbooks.cjs +13 -0
- package/native/workflow-definition.cjs +1 -0
- package/native/workflow-script-runtime.cjs +294 -0
- package/package.json +20 -5
- package/pi-extension/surf.ts +230 -0
- package/skills/surf/SKILL.md +55 -6
|
@@ -1,704 +1,144 @@
|
|
|
1
1
|
const path = require("path");
|
|
2
|
-
const {
|
|
2
|
+
const { raceAbort, throwIfAborted } = require("./abort.cjs");
|
|
3
|
+
const {
|
|
4
|
+
checkLoginStatus,
|
|
5
|
+
clickSend,
|
|
6
|
+
delay,
|
|
7
|
+
evaluate,
|
|
8
|
+
isCloudflareBlocked,
|
|
9
|
+
normalizeChatGPTEffortChoice,
|
|
10
|
+
normalizeChatGPTModelChoice,
|
|
11
|
+
resolveChatGPTEffortMenuOption,
|
|
12
|
+
resolveChatGPTModelMenuOption,
|
|
13
|
+
selectEffort,
|
|
14
|
+
selectModel,
|
|
15
|
+
typePrompt,
|
|
16
|
+
verifyChatGPTEffortSelection,
|
|
17
|
+
verifyChatGPTModelSelection,
|
|
18
|
+
waitForPageLoad,
|
|
19
|
+
waitForPromptReady,
|
|
20
|
+
} = require("./chatgpt-client-ui.cjs");
|
|
21
|
+
const {
|
|
22
|
+
cleanChatGPTResponseText,
|
|
23
|
+
extractLatestAssistantSnapshot,
|
|
24
|
+
isChatGPTResponseComplete,
|
|
25
|
+
isNewAssistantContent,
|
|
26
|
+
matchesPromptEcho,
|
|
27
|
+
normalizePromptEcho,
|
|
28
|
+
normalizeResponseSnapshot,
|
|
29
|
+
readChatGPTResponseSnapshot,
|
|
30
|
+
waitForResponse,
|
|
31
|
+
} = require("./chatgpt-client-response.cjs");
|
|
3
32
|
|
|
4
33
|
const CHATGPT_URL = "https://chatgpt.com/";
|
|
5
|
-
|
|
6
|
-
const SELECTORS = {
|
|
7
|
-
promptTextarea: '#prompt-textarea, [data-testid="composer-textarea"], textarea[name="prompt-textarea"], .ProseMirror, [contenteditable="true"][data-virtualkeyboard="true"]',
|
|
8
|
-
sendButton: 'button[data-testid="send-button"], button[data-testid*="composer-send"], form button[type="submit"]',
|
|
9
|
-
modelButton: '[data-testid="model-switcher-dropdown-button"]',
|
|
10
|
-
assistantMessage: '[data-message-author-role="assistant"], [data-turn="assistant"], [data-testid*="assistant-message"], [data-testid*="assistant-turn"], [data-testid*="assistant-response"]',
|
|
11
|
-
assistantContent: '.markdown, [data-message-content], .prose, [class*="markdown"], [dir="auto"]',
|
|
12
|
-
stopButton: '[data-testid="stop-button"], [data-testid*="stop"], button[aria-label*="Stop"], button[aria-label*="stop"]',
|
|
13
|
-
finishedActions: '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"]',
|
|
14
|
-
conversationTurn: '[data-testid^="conversation-turn"], [data-testid*="conversation-turn"]',
|
|
15
|
-
cloudflareScript: 'script[src*="/challenge-platform/"]',
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
function delay(ms, signal) {
|
|
19
|
-
return abortableDelay(ms, signal);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function buildClickDispatcher() {
|
|
23
|
-
return `function dispatchClickSequence(target){
|
|
24
|
-
if(!target || !(target instanceof EventTarget)) return false;
|
|
25
|
-
const types = ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click'];
|
|
26
|
-
for (const type of types) {
|
|
27
|
-
const common = { bubbles: true, cancelable: true, view: window };
|
|
28
|
-
let event;
|
|
29
|
-
if (type.startsWith('pointer') && 'PointerEvent' in window) {
|
|
30
|
-
event = new PointerEvent(type, { ...common, pointerId: 1, pointerType: 'mouse' });
|
|
31
|
-
} else {
|
|
32
|
-
event = new MouseEvent(type, common);
|
|
33
|
-
}
|
|
34
|
-
target.dispatchEvent(event);
|
|
35
|
-
}
|
|
36
|
-
return true;
|
|
37
|
-
}`;
|
|
38
|
-
}
|
|
34
|
+
const RESPONSE_STARTED_AT = Symbol("responseStartedAt");
|
|
39
35
|
|
|
40
36
|
function hasRequiredCookies(cookies) {
|
|
41
37
|
if (!cookies || !Array.isArray(cookies)) return false;
|
|
42
38
|
return cookies.some(
|
|
43
|
-
(
|
|
44
|
-
typeof
|
|
45
|
-
Boolean(
|
|
46
|
-
(
|
|
47
|
-
/^__Secure-next-auth\.session-token\.\d+$/.test(
|
|
39
|
+
(cookie) =>
|
|
40
|
+
typeof cookie?.name === "string" &&
|
|
41
|
+
Boolean(cookie.value) &&
|
|
42
|
+
(cookie.name === "__Secure-next-auth.session-token" ||
|
|
43
|
+
/^__Secure-next-auth\.session-token\.\d+$/.test(cookie.name)),
|
|
48
44
|
);
|
|
49
45
|
}
|
|
50
46
|
|
|
51
|
-
function
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
"
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
"edit",
|
|
60
|
-
"retry",
|
|
61
|
-
"continue generating",
|
|
62
|
-
"share",
|
|
63
|
-
]);
|
|
64
|
-
|
|
65
|
-
const lines = [];
|
|
66
|
-
let inCodeFence = false;
|
|
67
|
-
|
|
68
|
-
for (const line of String(rawText).replace(/\r\n?/g, "\n").split("\n")) {
|
|
69
|
-
const trimmed = line.trim();
|
|
70
|
-
const isFenceLine = trimmed.startsWith("```");
|
|
71
|
-
const normalizedLine = inCodeFence || isFenceLine ? line.replace(/[\t ]+$/g, "") : line;
|
|
72
|
-
|
|
73
|
-
lines.push({
|
|
74
|
-
text: normalizedLine,
|
|
75
|
-
trimmed,
|
|
76
|
-
isChrome: trimmed.length > 0 && chromeLines.has(trimmed.toLowerCase()),
|
|
77
|
-
inCodeFence,
|
|
78
|
-
isFenceLine,
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
if (isFenceLine) {
|
|
82
|
-
inCodeFence = !inCodeFence;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
while (lines.length > 0 && lines[0].trimmed.length === 0) {
|
|
87
|
-
lines.shift();
|
|
88
|
-
}
|
|
89
|
-
while (lines.length > 0 && lines[lines.length - 1].trimmed.length === 0) {
|
|
90
|
-
lines.pop();
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
let trailingChromeStart = lines.length;
|
|
94
|
-
while (trailingChromeStart > 0) {
|
|
95
|
-
const line = lines[trailingChromeStart - 1];
|
|
96
|
-
if (line.inCodeFence || line.isFenceLine || !line.isChrome) break;
|
|
97
|
-
trailingChromeStart--;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const trailingChromeCount = lines.length - trailingChromeStart;
|
|
101
|
-
if (trailingChromeCount >= 2) {
|
|
102
|
-
lines.splice(trailingChromeStart);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
while (lines.length > 0 && lines[0].trimmed.length === 0) {
|
|
106
|
-
lines.shift();
|
|
107
|
-
}
|
|
108
|
-
while (lines.length > 0 && lines[lines.length - 1].trimmed.length === 0) {
|
|
109
|
-
lines.pop();
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return lines.map((line) => line.text).join("\n");
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function extractLatestAssistantSnapshot(candidates) {
|
|
116
|
-
if (!Array.isArray(candidates)) return null;
|
|
117
|
-
|
|
118
|
-
let latestEmptyAssistant = null;
|
|
119
|
-
|
|
120
|
-
for (let i = candidates.length - 1; i >= 0; i--) {
|
|
121
|
-
const candidate = candidates[i];
|
|
122
|
-
if (!candidate?.isAssistant) continue;
|
|
123
|
-
|
|
124
|
-
const snapshot = {
|
|
125
|
-
...candidate,
|
|
126
|
-
text: cleanChatGPTResponseText(candidate?.text || ""),
|
|
127
|
-
turnIndex: i,
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
if (snapshot.text) {
|
|
131
|
-
return snapshot;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (!latestEmptyAssistant) {
|
|
135
|
-
latestEmptyAssistant = snapshot;
|
|
136
|
-
}
|
|
47
|
+
function extractConversationUrl(value) {
|
|
48
|
+
try {
|
|
49
|
+
const url = new URL(String(value));
|
|
50
|
+
const match = url.pathname.match(/^\/c\/([^/]+)\/?$/);
|
|
51
|
+
if (url.protocol !== "https:" || url.hostname !== "chatgpt.com" || !match) return null;
|
|
52
|
+
return `${url.origin}/c/${match[1]}`;
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
137
55
|
}
|
|
138
|
-
|
|
139
|
-
return latestEmptyAssistant;
|
|
140
56
|
}
|
|
141
57
|
|
|
142
|
-
function
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
assistantCount: Array.isArray(candidates)
|
|
147
|
-
? candidates.filter((candidate) => candidate?.isAssistant).length
|
|
148
|
-
: 0,
|
|
149
|
-
stopVisible: Boolean(rawSnapshot?.stopVisible),
|
|
150
|
-
};
|
|
58
|
+
function codedError(message, code) {
|
|
59
|
+
const error = new Error(message);
|
|
60
|
+
error.code = code;
|
|
61
|
+
return error;
|
|
151
62
|
}
|
|
152
63
|
|
|
153
|
-
function
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
) {
|
|
159
|
-
|
|
160
|
-
if (!baselineAssistant) return true;
|
|
161
|
-
if (latestAssistant.messageId && baselineAssistant.messageId) {
|
|
162
|
-
if (latestAssistant.messageId !== baselineAssistant.messageId) {
|
|
163
|
-
return true;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const currentText = latestAssistant.text || "";
|
|
168
|
-
const baselineText = baselineAssistant.text || "";
|
|
169
|
-
|
|
170
|
-
if (assistantCount > baselineAssistantCount) {
|
|
171
|
-
if (latestAssistant.turnIndex !== baselineAssistant.turnIndex) {
|
|
172
|
-
return true;
|
|
173
|
-
}
|
|
174
|
-
if (currentText !== baselineText) {
|
|
175
|
-
return true;
|
|
176
|
-
}
|
|
177
|
-
return false;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (currentText !== baselineText) {
|
|
181
|
-
return true;
|
|
64
|
+
function classifyError(error, fallbackCode, preservedCodes = []) {
|
|
65
|
+
const classified = error instanceof Error ? error : new Error(String(error));
|
|
66
|
+
if (
|
|
67
|
+
classified.code !== "SURF_REQUEST_ABORTED" &&
|
|
68
|
+
!preservedCodes.includes(classified.code)
|
|
69
|
+
) {
|
|
70
|
+
classified.code = fallbackCode;
|
|
182
71
|
}
|
|
183
|
-
return
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function isChatGPTResponseComplete(snapshot, stableCycles, stableMs) {
|
|
187
|
-
if (!snapshot?.text) return false;
|
|
188
|
-
if (snapshot.stopVisible) return false;
|
|
189
|
-
if (snapshot.hasFinishedActions) return true;
|
|
190
|
-
return stableCycles >= 6 && stableMs >= 1200;
|
|
72
|
+
return classified;
|
|
191
73
|
}
|
|
192
74
|
|
|
193
|
-
async function
|
|
194
|
-
throwIfAborted(signal);
|
|
195
|
-
const result = await cdp(expression);
|
|
196
|
-
throwIfAborted(signal);
|
|
197
|
-
if (result.exceptionDetails) {
|
|
198
|
-
const desc = result.exceptionDetails.exception?.description ||
|
|
199
|
-
result.exceptionDetails.text ||
|
|
200
|
-
"Evaluation failed";
|
|
201
|
-
throw new Error(desc);
|
|
202
|
-
}
|
|
203
|
-
if (result.error) {
|
|
204
|
-
throw new Error(result.error);
|
|
205
|
-
}
|
|
206
|
-
return result.result?.value;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
async function waitForPageLoad(cdp, timeoutMs = 45000, signal) {
|
|
210
|
-
throwIfAborted(signal);
|
|
75
|
+
async function waitForConversationUrl(cdp, timeoutMs = 30000, signal) {
|
|
211
76
|
const deadline = Date.now() + timeoutMs;
|
|
212
77
|
while (Date.now() < deadline) {
|
|
213
|
-
const
|
|
214
|
-
if (
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
await delay(100, signal);
|
|
218
|
-
}
|
|
219
|
-
throw new Error("Page did not load in time");
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
async function isCloudflareBlocked(cdp) {
|
|
223
|
-
const title = await evaluate(cdp, "document.title.toLowerCase()");
|
|
224
|
-
if (title && title.includes("just a moment")) return true;
|
|
225
|
-
const hasScript = await evaluate(
|
|
226
|
-
cdp,
|
|
227
|
-
`Boolean(document.querySelector('${SELECTORS.cloudflareScript}'))`
|
|
228
|
-
);
|
|
229
|
-
return hasScript;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
async function checkLoginStatus(cdp) {
|
|
233
|
-
const result = await evaluate(
|
|
234
|
-
cdp,
|
|
235
|
-
`(async () => {
|
|
236
|
-
try {
|
|
237
|
-
const response = await fetch('/backend-api/me', {
|
|
238
|
-
cache: 'no-store',
|
|
239
|
-
credentials: 'include'
|
|
240
|
-
});
|
|
241
|
-
const hasLoginCta = Array.from(document.querySelectorAll('a[href*="/auth/login"], button'))
|
|
242
|
-
.some(el => {
|
|
243
|
-
const text = (el.textContent || '').toLowerCase().trim();
|
|
244
|
-
return text.startsWith('log in') || text.startsWith('sign in');
|
|
245
|
-
});
|
|
246
|
-
return {
|
|
247
|
-
status: response.status,
|
|
248
|
-
hasLoginCta,
|
|
249
|
-
url: location.href
|
|
250
|
-
};
|
|
251
|
-
} catch (e) {
|
|
252
|
-
return { status: 0, error: e.message, url: location.href };
|
|
253
|
-
}
|
|
254
|
-
})()`
|
|
255
|
-
);
|
|
256
|
-
return result || { status: 0 };
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
async function waitForPromptReady(cdp, timeoutMs = 30000, signal) {
|
|
260
|
-
throwIfAborted(signal);
|
|
261
|
-
const deadline = Date.now() + timeoutMs;
|
|
262
|
-
const selectors = JSON.stringify(SELECTORS.promptTextarea.split(", "));
|
|
263
|
-
while (Date.now() < deadline) {
|
|
264
|
-
const found = await evaluate(
|
|
265
|
-
cdp,
|
|
266
|
-
`(() => {
|
|
267
|
-
const selectors = ${selectors};
|
|
268
|
-
for (const selector of selectors) {
|
|
269
|
-
const node = document.querySelector(selector);
|
|
270
|
-
if (node && !node.hasAttribute('disabled')) {
|
|
271
|
-
return true;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
return false;
|
|
275
|
-
})()`
|
|
276
|
-
);
|
|
277
|
-
if (found) return true;
|
|
78
|
+
const conversationUrl = extractConversationUrl(await evaluate(cdp, "location.href", signal));
|
|
79
|
+
if (conversationUrl) return conversationUrl;
|
|
278
80
|
await delay(200, signal);
|
|
279
81
|
}
|
|
280
|
-
return
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function normalizeChatGPTModelChoice(desiredModel) {
|
|
284
|
-
const normalized = String(desiredModel || "")
|
|
285
|
-
.toLowerCase()
|
|
286
|
-
.replace(/[^a-z0-9]/g, "");
|
|
287
|
-
|
|
288
|
-
if (["instant", "gpt53"].includes(normalized)) return "instant";
|
|
289
|
-
if (["thinking", "gpt54thinking"].includes(normalized)) return "thinking";
|
|
290
|
-
if (["pro", "gpt54pro"].includes(normalized)) return "pro";
|
|
291
|
-
|
|
292
|
-
return normalized;
|
|
82
|
+
return null;
|
|
293
83
|
}
|
|
294
84
|
|
|
295
|
-
function
|
|
296
|
-
if (!Array.isArray(items)) return null;
|
|
297
|
-
|
|
298
|
-
const targetModel = normalizeChatGPTModelChoice(desiredModel);
|
|
299
|
-
|
|
300
|
-
return items.find((item) => {
|
|
301
|
-
if (item?.role !== "menuitemradio") return false;
|
|
302
|
-
if (typeof item?.testId !== "string" || !item.testId.startsWith("model-switcher-")) return false;
|
|
303
|
-
|
|
304
|
-
const label = normalizeChatGPTModelChoice(item.label || "");
|
|
305
|
-
const testId = normalizeChatGPTModelChoice(item.testId.replace(/^model-switcher-/, ""));
|
|
306
|
-
return label === targetModel || testId === targetModel;
|
|
307
|
-
}) || null;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
async function selectModel(cdp, desiredModel, timeoutMs = 8000, signal) {
|
|
311
|
-
throwIfAborted(signal);
|
|
312
|
-
const modelButton = await evaluate(
|
|
313
|
-
cdp,
|
|
314
|
-
`(() => {
|
|
315
|
-
const btn = document.querySelector('${SELECTORS.modelButton}');
|
|
316
|
-
return btn ? true : false;
|
|
317
|
-
})()`
|
|
318
|
-
);
|
|
319
|
-
if (!modelButton) {
|
|
320
|
-
throw new Error("Model selector button not found");
|
|
321
|
-
}
|
|
322
|
-
await evaluate(
|
|
323
|
-
cdp,
|
|
324
|
-
`(() => {
|
|
325
|
-
${buildClickDispatcher()}
|
|
326
|
-
const btn = document.querySelector('${SELECTORS.modelButton}');
|
|
327
|
-
if (btn) dispatchClickSequence(btn);
|
|
328
|
-
})()`
|
|
329
|
-
);
|
|
330
|
-
await delay(300, signal);
|
|
331
|
-
|
|
332
|
-
const normalizedModel = normalizeChatGPTModelChoice(desiredModel);
|
|
333
|
-
const deadline = Date.now() + timeoutMs;
|
|
334
|
-
|
|
335
|
-
while (Date.now() < deadline) {
|
|
336
|
-
const result = await evaluate(
|
|
337
|
-
cdp,
|
|
338
|
-
`(() => {
|
|
339
|
-
const menu = document.querySelector('[role="menu"][data-radix-menu-content]');
|
|
340
|
-
if (!menu) {
|
|
341
|
-
return { found: false, waiting: true };
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
return {
|
|
345
|
-
found: true,
|
|
346
|
-
items: Array.from(menu.children).map((item) => {
|
|
347
|
-
const primary = item.querySelector?.('.min-w-0 > span');
|
|
348
|
-
return {
|
|
349
|
-
role: item.getAttribute?.('role') || null,
|
|
350
|
-
label: (primary?.textContent || item.getAttribute?.('aria-label') || item.textContent || '').trim(),
|
|
351
|
-
testId: item.getAttribute?.('data-testid') || null,
|
|
352
|
-
};
|
|
353
|
-
}),
|
|
354
|
-
};
|
|
355
|
-
})()`
|
|
356
|
-
);
|
|
357
|
-
|
|
358
|
-
if (result && result.found) {
|
|
359
|
-
const match = resolveChatGPTModelMenuOption(result.items, normalizedModel);
|
|
360
|
-
if (match) {
|
|
361
|
-
await evaluate(
|
|
362
|
-
cdp,
|
|
363
|
-
`(() => {
|
|
364
|
-
${buildClickDispatcher()}
|
|
365
|
-
const menu = document.querySelector('[role="menu"][data-radix-menu-content]');
|
|
366
|
-
const item = menu?.querySelector('[data-testid="${match.testId}"]');
|
|
367
|
-
if (item) dispatchClickSequence(item);
|
|
368
|
-
})()`
|
|
369
|
-
);
|
|
370
|
-
await delay(200, signal);
|
|
371
|
-
return match.label;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
const available = Array.isArray(result.items)
|
|
375
|
-
? result.items
|
|
376
|
-
.filter((item) => item?.role === "menuitemradio" && typeof item?.testId === "string" && item.testId.startsWith("model-switcher-"))
|
|
377
|
-
.map((item) => item.label)
|
|
378
|
-
.filter(Boolean)
|
|
379
|
-
.join(", ")
|
|
380
|
-
: "";
|
|
381
|
-
throw new Error(
|
|
382
|
-
available
|
|
383
|
-
? `Model not found: ${desiredModel}. Available: ${available}`
|
|
384
|
-
: `Model not found: ${desiredModel}`
|
|
385
|
-
);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
await delay(100, signal);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
throw new Error(`Model not found: ${desiredModel} (timeout)`);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
async function typePrompt(cdp, inputCdp, prompt, signal) {
|
|
395
|
-
throwIfAborted(signal);
|
|
396
|
-
const selectors = JSON.stringify(SELECTORS.promptTextarea.split(", "));
|
|
397
|
-
const encodedPrompt = JSON.stringify(prompt);
|
|
398
|
-
const focused = await evaluate(
|
|
399
|
-
cdp,
|
|
400
|
-
`(() => {
|
|
401
|
-
${buildClickDispatcher()}
|
|
402
|
-
const selectors = ${selectors};
|
|
403
|
-
for (const selector of selectors) {
|
|
404
|
-
const node = document.querySelector(selector);
|
|
405
|
-
if (!node) continue;
|
|
406
|
-
dispatchClickSequence(node);
|
|
407
|
-
if (typeof node.focus === 'function') node.focus();
|
|
408
|
-
const doc = node.ownerDocument;
|
|
409
|
-
const selection = doc?.getSelection?.();
|
|
410
|
-
if (selection) {
|
|
411
|
-
const range = doc.createRange();
|
|
412
|
-
range.selectNodeContents(node);
|
|
413
|
-
range.collapse(false);
|
|
414
|
-
selection.removeAllRanges();
|
|
415
|
-
selection.addRange(range);
|
|
416
|
-
}
|
|
417
|
-
return true;
|
|
418
|
-
}
|
|
419
|
-
return false;
|
|
420
|
-
})()`
|
|
421
|
-
);
|
|
422
|
-
if (!focused) {
|
|
423
|
-
throw new Error("Failed to focus prompt textarea");
|
|
424
|
-
}
|
|
425
|
-
await inputCdp("Input.insertText", { text: prompt });
|
|
426
|
-
await delay(300, signal);
|
|
427
|
-
const verified = await evaluate(
|
|
428
|
-
cdp,
|
|
429
|
-
`(() => {
|
|
430
|
-
const selectors = ${selectors};
|
|
431
|
-
for (const selector of selectors) {
|
|
432
|
-
const node = document.querySelector(selector);
|
|
433
|
-
if (!node) continue;
|
|
434
|
-
const text = node.innerText || node.value || node.textContent || '';
|
|
435
|
-
if (text.trim().length > 0) return true;
|
|
436
|
-
}
|
|
437
|
-
return false;
|
|
438
|
-
})()`
|
|
439
|
-
);
|
|
440
|
-
if (!verified) {
|
|
441
|
-
await evaluate(
|
|
442
|
-
cdp,
|
|
443
|
-
`(() => {
|
|
444
|
-
const editor = document.querySelector('#prompt-textarea');
|
|
445
|
-
const fallback = document.querySelector('textarea[name="prompt-textarea"]');
|
|
446
|
-
if (fallback) {
|
|
447
|
-
fallback.value = ${encodedPrompt};
|
|
448
|
-
fallback.dispatchEvent(new InputEvent('input', { bubbles: true, data: ${encodedPrompt}, inputType: 'insertFromPaste' }));
|
|
449
|
-
}
|
|
450
|
-
if (editor) {
|
|
451
|
-
editor.textContent = ${encodedPrompt};
|
|
452
|
-
editor.dispatchEvent(new InputEvent('input', { bubbles: true, data: ${encodedPrompt}, inputType: 'insertFromPaste' }));
|
|
453
|
-
}
|
|
454
|
-
})()`
|
|
455
|
-
);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
async function clickSend(cdp, inputCdp, signal) {
|
|
460
|
-
throwIfAborted(signal);
|
|
461
|
-
const selectors = SELECTORS.sendButton.split(", ");
|
|
462
|
-
const selectorsJson = JSON.stringify(selectors);
|
|
463
|
-
const deadline = Date.now() + 8000;
|
|
464
|
-
while (Date.now() < deadline) {
|
|
465
|
-
const result = await evaluate(
|
|
466
|
-
cdp,
|
|
467
|
-
`(() => {
|
|
468
|
-
${buildClickDispatcher()}
|
|
469
|
-
const selectors = ${selectorsJson};
|
|
470
|
-
let button = null;
|
|
471
|
-
for (const selector of selectors) {
|
|
472
|
-
button = document.querySelector(selector);
|
|
473
|
-
if (button) break;
|
|
474
|
-
}
|
|
475
|
-
if (!button) return 'missing';
|
|
476
|
-
const disabled = button.hasAttribute('disabled') ||
|
|
477
|
-
button.getAttribute('aria-disabled') === 'true' ||
|
|
478
|
-
button.getAttribute('data-disabled') === 'true';
|
|
479
|
-
if (disabled) return 'disabled';
|
|
480
|
-
dispatchClickSequence(button);
|
|
481
|
-
return 'clicked';
|
|
482
|
-
})()`
|
|
483
|
-
);
|
|
484
|
-
if (result === "clicked") return true;
|
|
485
|
-
if (result === "missing") break;
|
|
486
|
-
await delay(100, signal);
|
|
487
|
-
}
|
|
488
|
-
await inputCdp("Input.dispatchKeyEvent", {
|
|
489
|
-
type: "keyDown",
|
|
490
|
-
key: "Enter",
|
|
491
|
-
code: "Enter",
|
|
492
|
-
windowsVirtualKeyCode: 13,
|
|
493
|
-
nativeVirtualKeyCode: 13,
|
|
494
|
-
text: "\r",
|
|
495
|
-
});
|
|
496
|
-
await inputCdp("Input.dispatchKeyEvent", {
|
|
497
|
-
type: "keyUp",
|
|
498
|
-
key: "Enter",
|
|
499
|
-
code: "Enter",
|
|
500
|
-
windowsVirtualKeyCode: 13,
|
|
501
|
-
nativeVirtualKeyCode: 13,
|
|
502
|
-
});
|
|
503
|
-
return true;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
async function readChatGPTResponseSnapshot(cdp) {
|
|
507
|
-
return evaluate(
|
|
508
|
-
cdp,
|
|
509
|
-
`(() => {
|
|
510
|
-
const scope = document.querySelector('main') || document;
|
|
511
|
-
const CONVERSATION_SELECTOR = ${JSON.stringify(SELECTORS.conversationTurn)};
|
|
512
|
-
const ASSISTANT_SELECTOR = ${JSON.stringify(SELECTORS.assistantMessage)};
|
|
513
|
-
const CONTENT_SELECTORS = ${JSON.stringify(SELECTORS.assistantContent.split(", "))};
|
|
514
|
-
const STOP_SELECTOR = ${JSON.stringify(SELECTORS.stopButton)};
|
|
515
|
-
const FINISHED_SELECTOR = ${JSON.stringify(SELECTORS.finishedActions)};
|
|
516
|
-
|
|
517
|
-
const toCandidate = (turnNode, messageRoot = null) => {
|
|
518
|
-
const resolvedMessageRoot = messageRoot || (turnNode.matches?.(ASSISTANT_SELECTOR)
|
|
519
|
-
? turnNode
|
|
520
|
-
: turnNode.querySelector(ASSISTANT_SELECTOR));
|
|
521
|
-
const searchRoot = resolvedMessageRoot || turnNode;
|
|
522
|
-
let contentRoot = null;
|
|
523
|
-
|
|
524
|
-
for (const selector of CONTENT_SELECTORS) {
|
|
525
|
-
const match = selector === '[dir="auto"]'
|
|
526
|
-
? (searchRoot.matches?.(selector) ? searchRoot : null)
|
|
527
|
-
: (searchRoot.matches?.(selector) ? searchRoot : searchRoot.querySelector(selector));
|
|
528
|
-
if (match) {
|
|
529
|
-
contentRoot = match;
|
|
530
|
-
break;
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
const role =
|
|
535
|
-
resolvedMessageRoot?.getAttribute('data-message-author-role') ||
|
|
536
|
-
turnNode.getAttribute('data-message-author-role') ||
|
|
537
|
-
null;
|
|
538
|
-
const turn =
|
|
539
|
-
resolvedMessageRoot?.getAttribute('data-turn') ||
|
|
540
|
-
turnNode.getAttribute('data-turn') ||
|
|
541
|
-
null;
|
|
542
|
-
const isAssistant =
|
|
543
|
-
role === 'assistant' ||
|
|
544
|
-
turn === 'assistant' ||
|
|
545
|
-
resolvedMessageRoot !== null;
|
|
546
|
-
const text = (contentRoot || turnNode).innerText || (contentRoot || turnNode).textContent || '';
|
|
547
|
-
const messageId =
|
|
548
|
-
resolvedMessageRoot?.getAttribute('data-message-id') ||
|
|
549
|
-
turnNode.getAttribute('data-message-id') ||
|
|
550
|
-
null;
|
|
551
|
-
const hasFinishedActions = Boolean(turnNode.querySelector(FINISHED_SELECTOR));
|
|
552
|
-
|
|
553
|
-
return {
|
|
554
|
-
role,
|
|
555
|
-
turn,
|
|
556
|
-
isAssistant,
|
|
557
|
-
text,
|
|
558
|
-
messageId,
|
|
559
|
-
hasFinishedActions,
|
|
560
|
-
};
|
|
561
|
-
};
|
|
562
|
-
|
|
563
|
-
let candidates = Array.from(scope.querySelectorAll(CONVERSATION_SELECTOR)).map((turnNode) =>
|
|
564
|
-
toCandidate(turnNode)
|
|
565
|
-
);
|
|
566
|
-
|
|
567
|
-
if (candidates.length === 0) {
|
|
568
|
-
candidates = Array.from(scope.querySelectorAll(ASSISTANT_SELECTOR)).map((messageRoot) =>
|
|
569
|
-
toCandidate(messageRoot, messageRoot)
|
|
570
|
-
);
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
return {
|
|
574
|
-
candidates,
|
|
575
|
-
stopVisible: Boolean(scope.querySelector(STOP_SELECTOR)),
|
|
576
|
-
};
|
|
577
|
-
})()`
|
|
578
|
-
);
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
async function waitForResponse(
|
|
582
|
-
cdp,
|
|
583
|
-
timeoutMs = 2700000,
|
|
584
|
-
baselineAssistant,
|
|
585
|
-
baselineAssistantCount,
|
|
586
|
-
signal
|
|
587
|
-
) {
|
|
588
|
-
throwIfAborted(signal);
|
|
589
|
-
const deadline = Date.now() + timeoutMs;
|
|
590
|
-
let previousText = "";
|
|
591
|
-
let stableCycles = 0;
|
|
592
|
-
let lastChangeAt = Date.now();
|
|
593
|
-
|
|
594
|
-
previousText = baselineAssistant?.text || "";
|
|
595
|
-
lastChangeAt = Date.now();
|
|
596
|
-
|
|
597
|
-
while (Date.now() < deadline) {
|
|
598
|
-
const snapshot = await readChatGPTResponseSnapshot(cdp);
|
|
599
|
-
|
|
600
|
-
if (!snapshot) {
|
|
601
|
-
await delay(400, signal);
|
|
602
|
-
continue;
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
const { latestAssistant, assistantCount, stopVisible } = normalizeResponseSnapshot(snapshot);
|
|
606
|
-
const currentText = latestAssistant?.text || "";
|
|
607
|
-
const hasNewAssistantContent = isNewAssistantContent(
|
|
608
|
-
latestAssistant,
|
|
609
|
-
baselineAssistant,
|
|
610
|
-
assistantCount,
|
|
611
|
-
baselineAssistantCount
|
|
612
|
-
);
|
|
613
|
-
|
|
614
|
-
if (!hasNewAssistantContent) {
|
|
615
|
-
await delay(400, signal);
|
|
616
|
-
continue;
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
if (currentText !== previousText) {
|
|
620
|
-
previousText = currentText;
|
|
621
|
-
stableCycles = 0;
|
|
622
|
-
lastChangeAt = Date.now();
|
|
623
|
-
} else if (currentText) {
|
|
624
|
-
stableCycles++;
|
|
625
|
-
} else {
|
|
626
|
-
stableCycles = 0;
|
|
627
|
-
lastChangeAt = Date.now();
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
const stableMs = Date.now() - lastChangeAt;
|
|
631
|
-
const completionSnapshot = latestAssistant
|
|
632
|
-
? { ...latestAssistant, stopVisible }
|
|
633
|
-
: { text: "", stopVisible, hasFinishedActions: false };
|
|
634
|
-
|
|
635
|
-
if (isChatGPTResponseComplete(completionSnapshot, stableCycles, stableMs)) {
|
|
636
|
-
return {
|
|
637
|
-
text: latestAssistant.text,
|
|
638
|
-
messageId: latestAssistant.messageId,
|
|
639
|
-
turnIndex: latestAssistant.turnIndex,
|
|
640
|
-
};
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
await delay(400, signal);
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
throw new Error("Response timeout");
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
async function query(options) {
|
|
85
|
+
async function dispatch(options) {
|
|
650
86
|
const {
|
|
651
87
|
prompt,
|
|
652
88
|
model,
|
|
89
|
+
effort,
|
|
653
90
|
file,
|
|
654
|
-
timeout = 2700000,
|
|
655
91
|
getCookies,
|
|
656
92
|
createTab,
|
|
657
|
-
closeTab,
|
|
658
93
|
cdpEvaluate,
|
|
659
94
|
cdpCommand,
|
|
660
95
|
uploadFile,
|
|
661
96
|
beforeSubmit,
|
|
97
|
+
afterSubmit,
|
|
98
|
+
startUrl,
|
|
662
99
|
log = () => {},
|
|
663
100
|
signal,
|
|
664
101
|
} = options;
|
|
665
|
-
|
|
666
|
-
const guardedUploadFile = uploadFile
|
|
667
|
-
? (...args) => raceAbort(() => uploadFile(...args), signal)
|
|
668
|
-
: uploadFile;
|
|
669
|
-
const startTime = Date.now();
|
|
670
|
-
log("Starting ChatGPT query");
|
|
671
|
-
const { cookies } = await raceAbort(getCookies, signal);
|
|
672
|
-
if (!hasRequiredCookies(cookies)) {
|
|
673
|
-
throw new Error("ChatGPT login required");
|
|
674
|
-
}
|
|
675
|
-
log(`Got ${cookies.length} cookies`);
|
|
676
|
-
const tabInfo = await raceAbort(createTab, signal);
|
|
677
|
-
const { tabId } = tabInfo;
|
|
678
|
-
if (!tabId) {
|
|
679
|
-
throw new Error("Failed to create ChatGPT tab");
|
|
680
|
-
}
|
|
681
|
-
log(`Created tab ${tabId}`);
|
|
682
|
-
|
|
683
|
-
const cdp = (expr) => raceAbort(() => cdpEvaluate(tabId, expr), signal);
|
|
684
|
-
const inputCdp = (method, params) => raceAbort(() => cdpCommand(tabId, method, params), signal);
|
|
685
|
-
|
|
102
|
+
|
|
686
103
|
try {
|
|
104
|
+
throwIfAborted(signal);
|
|
105
|
+
const guardedUploadFile = uploadFile
|
|
106
|
+
? (...args) => raceAbort(() => uploadFile(...args), signal)
|
|
107
|
+
: uploadFile;
|
|
108
|
+
log("Starting ChatGPT query");
|
|
109
|
+
const { cookies } = await raceAbort(getCookies, signal);
|
|
110
|
+
if (!hasRequiredCookies(cookies)) {
|
|
111
|
+
throw codedError("ChatGPT login required", "auth");
|
|
112
|
+
}
|
|
113
|
+
log(`Got ${cookies.length} cookies`);
|
|
114
|
+
const tabInfo = await raceAbort(createTab, signal);
|
|
115
|
+
const { tabId } = tabInfo;
|
|
116
|
+
if (!tabId) {
|
|
117
|
+
throw new Error("Failed to create ChatGPT tab");
|
|
118
|
+
}
|
|
119
|
+
log(`Created tab ${tabId}`);
|
|
120
|
+
|
|
121
|
+
const cdp = (expression) => raceAbort(() => cdpEvaluate(tabId, expression), signal);
|
|
122
|
+
const inputCdp = (method, params) =>
|
|
123
|
+
raceAbort(() => cdpCommand(tabId, method, params), signal);
|
|
124
|
+
|
|
125
|
+
if (startUrl) await inputCdp("Page.navigate", { url: startUrl });
|
|
687
126
|
await waitForPageLoad(cdp, 45000, signal);
|
|
688
127
|
log("Page loaded");
|
|
689
128
|
if (await isCloudflareBlocked(cdp)) {
|
|
690
|
-
throw
|
|
129
|
+
throw codedError("Cloudflare challenge detected - complete in browser", "cloudflare");
|
|
691
130
|
}
|
|
692
131
|
const loginStatus = await checkLoginStatus(cdp);
|
|
693
132
|
if (loginStatus.status === 0) {
|
|
694
|
-
throw
|
|
133
|
+
throw codedError(
|
|
695
134
|
loginStatus.error
|
|
696
135
|
? `ChatGPT login check failed: ${loginStatus.error}`
|
|
697
|
-
: "ChatGPT login check failed"
|
|
136
|
+
: "ChatGPT login check failed",
|
|
137
|
+
"auth",
|
|
698
138
|
);
|
|
699
139
|
}
|
|
700
140
|
if (loginStatus.status !== 200 || loginStatus.hasLoginCta) {
|
|
701
|
-
throw
|
|
141
|
+
throw codedError("ChatGPT login required", "auth");
|
|
702
142
|
}
|
|
703
143
|
log("Login verified");
|
|
704
144
|
const promptReady = await waitForPromptReady(cdp, 30000, signal);
|
|
@@ -706,13 +146,21 @@ async function query(options) {
|
|
|
706
146
|
throw new Error("Prompt textarea not ready");
|
|
707
147
|
}
|
|
708
148
|
log("Prompt ready");
|
|
149
|
+
let modelVerified = null;
|
|
150
|
+
let effortVerified = null;
|
|
709
151
|
if (model) {
|
|
710
|
-
|
|
711
|
-
log(`
|
|
152
|
+
modelVerified = await selectModel(cdp, model, 8000, signal);
|
|
153
|
+
log(`Verified model: ${modelVerified}`);
|
|
154
|
+
}
|
|
155
|
+
if (effort) {
|
|
156
|
+
effortVerified = await selectEffort(cdp, effort, 8000, signal);
|
|
157
|
+
log(`Verified effort: ${effortVerified}`);
|
|
712
158
|
}
|
|
713
159
|
if (file) {
|
|
714
160
|
if (!uploadFile) {
|
|
715
|
-
throw new Error(
|
|
161
|
+
throw new Error(
|
|
162
|
+
"ChatGPT file upload unavailable: native host did not provide upload callback",
|
|
163
|
+
);
|
|
716
164
|
}
|
|
717
165
|
const files = Array.isArray(file) ? file : [file];
|
|
718
166
|
const absFiles = files.map((filePath) => path.resolve(process.cwd(), filePath));
|
|
@@ -732,38 +180,178 @@ async function query(options) {
|
|
|
732
180
|
const baseline = normalizeResponseSnapshot(await readChatGPTResponseSnapshot(cdp));
|
|
733
181
|
if (beforeSubmit) await raceAbort(beforeSubmit, signal);
|
|
734
182
|
await clickSend(cdp, inputCdp, signal);
|
|
183
|
+
baseline[RESPONSE_STARTED_AT] = Date.now();
|
|
184
|
+
const promptEcho = normalizePromptEcho(prompt);
|
|
185
|
+
if (afterSubmit) {
|
|
186
|
+
await afterSubmit({ tabId, promptEcho, modelVerified, effortVerified });
|
|
187
|
+
}
|
|
735
188
|
log("Prompt sent, waiting for response...");
|
|
189
|
+
const conversationUrl = await waitForConversationUrl(cdp, 30000, signal);
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
tabId,
|
|
193
|
+
conversationUrl,
|
|
194
|
+
promptEcho,
|
|
195
|
+
model: model || "current",
|
|
196
|
+
modelVerified,
|
|
197
|
+
effortVerified,
|
|
198
|
+
baseline,
|
|
199
|
+
};
|
|
200
|
+
} catch (error) {
|
|
201
|
+
throw classifyError(error, "dispatch_failed", [
|
|
202
|
+
"auth",
|
|
203
|
+
"cloudflare",
|
|
204
|
+
"model_verification_failed",
|
|
205
|
+
]);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function harvest(options) {
|
|
210
|
+
const {
|
|
211
|
+
tabId: liveTabId,
|
|
212
|
+
conversationUrl,
|
|
213
|
+
promptEcho,
|
|
214
|
+
baseline,
|
|
215
|
+
timeout = 2700000,
|
|
216
|
+
createTab,
|
|
217
|
+
closeTab,
|
|
218
|
+
cdpEvaluate,
|
|
219
|
+
cdpCommand,
|
|
220
|
+
keepCreatedTabOpen = false,
|
|
221
|
+
log = () => {},
|
|
222
|
+
signal,
|
|
223
|
+
} = options;
|
|
224
|
+
const startTime = Date.now();
|
|
225
|
+
let tabId = liveTabId;
|
|
226
|
+
let ownsTab = false;
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
throwIfAborted(signal);
|
|
230
|
+
if (!tabId) {
|
|
231
|
+
if (!conversationUrl) {
|
|
232
|
+
throw new Error("ChatGPT conversation URL required for fresh-tab harvest");
|
|
233
|
+
}
|
|
234
|
+
const tabInfo = await raceAbort(createTab, signal);
|
|
235
|
+
tabId = tabInfo?.tabId;
|
|
236
|
+
if (!tabId) {
|
|
237
|
+
throw new Error("Failed to create ChatGPT tab");
|
|
238
|
+
}
|
|
239
|
+
ownsTab = true;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const cdp = (expression) => raceAbort(() => cdpEvaluate(tabId, expression), signal);
|
|
243
|
+
const inputCdp = (method, params) =>
|
|
244
|
+
raceAbort(() => cdpCommand(tabId, method, params), signal);
|
|
245
|
+
|
|
246
|
+
if (ownsTab) {
|
|
247
|
+
await inputCdp("Page.navigate", { url: conversationUrl });
|
|
248
|
+
await waitForPageLoad(cdp, 45000, signal);
|
|
249
|
+
if (await isCloudflareBlocked(cdp)) {
|
|
250
|
+
throw codedError("Cloudflare challenge detected - complete in browser", "cloudflare");
|
|
251
|
+
}
|
|
252
|
+
const loginStatus = await checkLoginStatus(cdp);
|
|
253
|
+
if (loginStatus.status === 0) {
|
|
254
|
+
throw codedError(
|
|
255
|
+
loginStatus.error
|
|
256
|
+
? `ChatGPT login check failed: ${loginStatus.error}`
|
|
257
|
+
: "ChatGPT login check failed",
|
|
258
|
+
"auth",
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
if (loginStatus.status !== 200 || loginStatus.hasLoginCta) {
|
|
262
|
+
throw codedError("ChatGPT login required", "auth");
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const elapsedSinceSend = baseline?.[RESPONSE_STARTED_AT]
|
|
267
|
+
? Date.now() - baseline[RESPONSE_STARTED_AT]
|
|
268
|
+
: 0;
|
|
736
269
|
const response = await waitForResponse(
|
|
737
270
|
cdp,
|
|
738
|
-
timeout,
|
|
739
|
-
baseline
|
|
740
|
-
baseline
|
|
741
|
-
signal
|
|
271
|
+
Math.max(0, timeout - elapsedSinceSend),
|
|
272
|
+
baseline?.latestAssistant,
|
|
273
|
+
baseline?.assistantCount,
|
|
274
|
+
signal,
|
|
275
|
+
baseline ? undefined : promptEcho,
|
|
742
276
|
);
|
|
743
277
|
log(`Response received (${response.text.length} chars)`);
|
|
744
278
|
return {
|
|
745
279
|
response: response.text,
|
|
746
|
-
model: model || "current",
|
|
747
280
|
messageId: response.messageId,
|
|
748
281
|
tookMs: Date.now() - startTime,
|
|
749
282
|
};
|
|
283
|
+
} catch (error) {
|
|
284
|
+
const fallbackCode = error?.message === "Response timeout" ? "timeout" : "harvest_failed";
|
|
285
|
+
throw classifyError(error, fallbackCode, ["auth", "cloudflare", "timeout"]);
|
|
286
|
+
} finally {
|
|
287
|
+
if (ownsTab && !keepCreatedTabOpen) {
|
|
288
|
+
try {
|
|
289
|
+
await closeTab(tabId);
|
|
290
|
+
} catch (error) {
|
|
291
|
+
log(`Failed to close ChatGPT tab ${tabId}: ${error?.message || error}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function query(options) {
|
|
298
|
+
const { closeTab, createTab, log = () => {}, signal, timeout = 2700000 } = options;
|
|
299
|
+
throwIfAborted(signal);
|
|
300
|
+
const startTime = Date.now();
|
|
301
|
+
let tabId = null;
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
const dispatched = await dispatch({
|
|
305
|
+
...options,
|
|
306
|
+
createTab: async () => {
|
|
307
|
+
const tabInfo = await createTab();
|
|
308
|
+
tabId = tabInfo?.tabId || null;
|
|
309
|
+
return tabInfo;
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
const result = await harvest({
|
|
313
|
+
...options,
|
|
314
|
+
tabId: dispatched.tabId,
|
|
315
|
+
conversationUrl: dispatched.conversationUrl,
|
|
316
|
+
promptEcho: dispatched.promptEcho,
|
|
317
|
+
baseline: dispatched.baseline,
|
|
318
|
+
timeout,
|
|
319
|
+
});
|
|
320
|
+
return {
|
|
321
|
+
response: result.response,
|
|
322
|
+
model: dispatched.model,
|
|
323
|
+
messageId: result.messageId,
|
|
324
|
+
tookMs: Date.now() - startTime,
|
|
325
|
+
};
|
|
750
326
|
} finally {
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
327
|
+
if (tabId) {
|
|
328
|
+
try {
|
|
329
|
+
await closeTab(tabId);
|
|
330
|
+
} catch (error) {
|
|
331
|
+
log(`Failed to close ChatGPT tab ${tabId}: ${error?.message || error}`);
|
|
332
|
+
}
|
|
755
333
|
}
|
|
756
334
|
}
|
|
757
335
|
}
|
|
758
336
|
|
|
759
337
|
module.exports = {
|
|
760
338
|
query,
|
|
339
|
+
dispatch,
|
|
340
|
+
harvest,
|
|
761
341
|
hasRequiredCookies,
|
|
762
342
|
cleanChatGPTResponseText,
|
|
763
343
|
extractLatestAssistantSnapshot,
|
|
344
|
+
normalizeChatGPTEffortChoice,
|
|
764
345
|
normalizeChatGPTModelChoice,
|
|
346
|
+
resolveChatGPTEffortMenuOption,
|
|
765
347
|
resolveChatGPTModelMenuOption,
|
|
766
348
|
isNewAssistantContent,
|
|
767
349
|
isChatGPTResponseComplete,
|
|
350
|
+
isCloudflareBlocked,
|
|
351
|
+
normalizePromptEcho,
|
|
352
|
+
matchesPromptEcho,
|
|
353
|
+
extractConversationUrl,
|
|
354
|
+
verifyChatGPTEffortSelection,
|
|
355
|
+
verifyChatGPTModelSelection,
|
|
768
356
|
CHATGPT_URL,
|
|
769
357
|
};
|