surf-cli 2.9.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 +61 -4
- package/dist/content/accessibility-tree.js +11 -0
- package/dist/content/accessibility-tree.js.map +1 -0
- package/dist/content/visual-indicator.js +111 -0
- package/dist/content/visual-indicator.js.map +1 -0
- package/dist/manifest.json +11 -2
- package/dist/options/options.js +3 -3
- package/dist/options/options.js.map +1 -1
- package/dist/service-worker/index.js +61 -261
- package/dist/service-worker/index.js.map +1 -1
- package/native/activity-journal.cjs +55 -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 +254 -664
- package/native/cli.cjs +100 -273
- package/native/do-executor.cjs +52 -475
- package/native/do-parser.cjs +8 -249
- package/native/host-helpers.cjs +32 -15
- package/native/host-sessions.cjs +6 -1
- package/native/host.cjs +228 -6
- package/native/network-export.cjs +20 -17
- package/native/network-store.cjs +38 -58
- 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-authoring.cjs +44 -0
- package/native/playbook-cli.cjs +157 -0
- package/native/playbook-client.cjs +259 -0
- package/native/playbook-receipts.cjs +109 -0
- package/native/playbook-records.cjs +208 -0
- package/native/playbook-runtime.cjs +177 -0
- package/native/playbooks.cjs +235 -0
- package/native/private-state.cjs +156 -0
- package/native/redaction.cjs +104 -0
- package/native/workflow-definition.cjs +369 -0
- package/native/workflow-runtime.cjs +225 -0
- package/package.json +2 -1
- package/playbooks/page/ops/read.json +22 -0
- package/playbooks/page/playbook.json +7 -0
- package/skills/surf/SKILL.md +72 -1
- package/dist/content/index.js +0 -116
- package/dist/content/index.js.map +0 -1
|
@@ -1,703 +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,
|
|
96
|
+
beforeSubmit,
|
|
97
|
+
afterSubmit,
|
|
98
|
+
startUrl,
|
|
661
99
|
log = () => {},
|
|
662
100
|
signal,
|
|
663
101
|
} = options;
|
|
664
|
-
|
|
665
|
-
const guardedUploadFile = uploadFile
|
|
666
|
-
? (...args) => raceAbort(() => uploadFile(...args), signal)
|
|
667
|
-
: uploadFile;
|
|
668
|
-
const startTime = Date.now();
|
|
669
|
-
log("Starting ChatGPT query");
|
|
670
|
-
const { cookies } = await raceAbort(getCookies, signal);
|
|
671
|
-
if (!hasRequiredCookies(cookies)) {
|
|
672
|
-
throw new Error("ChatGPT login required");
|
|
673
|
-
}
|
|
674
|
-
log(`Got ${cookies.length} cookies`);
|
|
675
|
-
const tabInfo = await raceAbort(createTab, signal);
|
|
676
|
-
const { tabId } = tabInfo;
|
|
677
|
-
if (!tabId) {
|
|
678
|
-
throw new Error("Failed to create ChatGPT tab");
|
|
679
|
-
}
|
|
680
|
-
log(`Created tab ${tabId}`);
|
|
681
|
-
|
|
682
|
-
const cdp = (expr) => raceAbort(() => cdpEvaluate(tabId, expr), signal);
|
|
683
|
-
const inputCdp = (method, params) => raceAbort(() => cdpCommand(tabId, method, params), signal);
|
|
684
|
-
|
|
102
|
+
|
|
685
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 });
|
|
686
126
|
await waitForPageLoad(cdp, 45000, signal);
|
|
687
127
|
log("Page loaded");
|
|
688
128
|
if (await isCloudflareBlocked(cdp)) {
|
|
689
|
-
throw
|
|
129
|
+
throw codedError("Cloudflare challenge detected - complete in browser", "cloudflare");
|
|
690
130
|
}
|
|
691
131
|
const loginStatus = await checkLoginStatus(cdp);
|
|
692
132
|
if (loginStatus.status === 0) {
|
|
693
|
-
throw
|
|
133
|
+
throw codedError(
|
|
694
134
|
loginStatus.error
|
|
695
135
|
? `ChatGPT login check failed: ${loginStatus.error}`
|
|
696
|
-
: "ChatGPT login check failed"
|
|
136
|
+
: "ChatGPT login check failed",
|
|
137
|
+
"auth",
|
|
697
138
|
);
|
|
698
139
|
}
|
|
699
140
|
if (loginStatus.status !== 200 || loginStatus.hasLoginCta) {
|
|
700
|
-
throw
|
|
141
|
+
throw codedError("ChatGPT login required", "auth");
|
|
701
142
|
}
|
|
702
143
|
log("Login verified");
|
|
703
144
|
const promptReady = await waitForPromptReady(cdp, 30000, signal);
|
|
@@ -705,13 +146,21 @@ async function query(options) {
|
|
|
705
146
|
throw new Error("Prompt textarea not ready");
|
|
706
147
|
}
|
|
707
148
|
log("Prompt ready");
|
|
149
|
+
let modelVerified = null;
|
|
150
|
+
let effortVerified = null;
|
|
708
151
|
if (model) {
|
|
709
|
-
|
|
710
|
-
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}`);
|
|
711
158
|
}
|
|
712
159
|
if (file) {
|
|
713
160
|
if (!uploadFile) {
|
|
714
|
-
throw new Error(
|
|
161
|
+
throw new Error(
|
|
162
|
+
"ChatGPT file upload unavailable: native host did not provide upload callback",
|
|
163
|
+
);
|
|
715
164
|
}
|
|
716
165
|
const files = Array.isArray(file) ? file : [file];
|
|
717
166
|
const absFiles = files.map((filePath) => path.resolve(process.cwd(), filePath));
|
|
@@ -729,39 +178,180 @@ async function query(options) {
|
|
|
729
178
|
await typePrompt(cdp, inputCdp, prompt, signal);
|
|
730
179
|
log("Prompt typed");
|
|
731
180
|
const baseline = normalizeResponseSnapshot(await readChatGPTResponseSnapshot(cdp));
|
|
181
|
+
if (beforeSubmit) await raceAbort(beforeSubmit, signal);
|
|
732
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
|
+
}
|
|
733
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;
|
|
734
269
|
const response = await waitForResponse(
|
|
735
270
|
cdp,
|
|
736
|
-
timeout,
|
|
737
|
-
baseline
|
|
738
|
-
baseline
|
|
739
|
-
signal
|
|
271
|
+
Math.max(0, timeout - elapsedSinceSend),
|
|
272
|
+
baseline?.latestAssistant,
|
|
273
|
+
baseline?.assistantCount,
|
|
274
|
+
signal,
|
|
275
|
+
baseline ? undefined : promptEcho,
|
|
740
276
|
);
|
|
741
277
|
log(`Response received (${response.text.length} chars)`);
|
|
742
278
|
return {
|
|
743
279
|
response: response.text,
|
|
744
|
-
model: model || "current",
|
|
745
280
|
messageId: response.messageId,
|
|
746
281
|
tookMs: Date.now() - startTime,
|
|
747
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
|
+
};
|
|
748
326
|
} finally {
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
327
|
+
if (tabId) {
|
|
328
|
+
try {
|
|
329
|
+
await closeTab(tabId);
|
|
330
|
+
} catch (error) {
|
|
331
|
+
log(`Failed to close ChatGPT tab ${tabId}: ${error?.message || error}`);
|
|
332
|
+
}
|
|
753
333
|
}
|
|
754
334
|
}
|
|
755
335
|
}
|
|
756
336
|
|
|
757
337
|
module.exports = {
|
|
758
338
|
query,
|
|
339
|
+
dispatch,
|
|
340
|
+
harvest,
|
|
759
341
|
hasRequiredCookies,
|
|
760
342
|
cleanChatGPTResponseText,
|
|
761
343
|
extractLatestAssistantSnapshot,
|
|
344
|
+
normalizeChatGPTEffortChoice,
|
|
762
345
|
normalizeChatGPTModelChoice,
|
|
346
|
+
resolveChatGPTEffortMenuOption,
|
|
763
347
|
resolveChatGPTModelMenuOption,
|
|
764
348
|
isNewAssistantContent,
|
|
765
349
|
isChatGPTResponseComplete,
|
|
350
|
+
isCloudflareBlocked,
|
|
351
|
+
normalizePromptEcho,
|
|
352
|
+
matchesPromptEcho,
|
|
353
|
+
extractConversationUrl,
|
|
354
|
+
verifyChatGPTEffortSelection,
|
|
355
|
+
verifyChatGPTModelSelection,
|
|
766
356
|
CHATGPT_URL,
|
|
767
357
|
};
|