surf-cli 2.7.1 → 2.7.2
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/native/chatgpt-client.cjs +376 -104
- package/package.json +1 -1
- package/native/edited.png +0 -0
|
@@ -4,13 +4,11 @@ const SELECTORS = {
|
|
|
4
4
|
promptTextarea: '#prompt-textarea, [data-testid="composer-textarea"], textarea[name="prompt-textarea"], .ProseMirror, [contenteditable="true"][data-virtualkeyboard="true"]',
|
|
5
5
|
sendButton: 'button[data-testid="send-button"], button[data-testid*="composer-send"], form button[type="submit"]',
|
|
6
6
|
modelButton: '[data-testid="model-switcher-dropdown-button"]',
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
conversationTurn: 'article[data-testid^="conversation-turn"], div[data-testid^="conversation-turn"]',
|
|
13
|
-
fileInput: 'input[type="file"]',
|
|
7
|
+
assistantMessage: '[data-message-author-role="assistant"], [data-turn="assistant"], [data-testid*="assistant-message"], [data-testid*="assistant-turn"], [data-testid*="assistant-response"]',
|
|
8
|
+
assistantContent: '.markdown, [data-message-content], .prose, [class*="markdown"], [dir="auto"]',
|
|
9
|
+
stopButton: '[data-testid="stop-button"], [data-testid*="stop"], button[aria-label*="Stop"], button[aria-label*="stop"]',
|
|
10
|
+
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"]',
|
|
11
|
+
conversationTurn: '[data-testid^="conversation-turn"], [data-testid*="conversation-turn"]',
|
|
14
12
|
cloudflareScript: 'script[src*="/challenge-platform/"]',
|
|
15
13
|
};
|
|
16
14
|
|
|
@@ -38,10 +36,155 @@ function buildClickDispatcher() {
|
|
|
38
36
|
|
|
39
37
|
function hasRequiredCookies(cookies) {
|
|
40
38
|
if (!cookies || !Array.isArray(cookies)) return false;
|
|
41
|
-
|
|
42
|
-
(c) =>
|
|
39
|
+
return cookies.some(
|
|
40
|
+
(c) =>
|
|
41
|
+
typeof c?.name === "string" &&
|
|
42
|
+
Boolean(c.value) &&
|
|
43
|
+
(c.name === "__Secure-next-auth.session-token" ||
|
|
44
|
+
/^__Secure-next-auth\.session-token\.\d+$/.test(c.name))
|
|
43
45
|
);
|
|
44
|
-
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function cleanChatGPTResponseText(rawText) {
|
|
49
|
+
if (!rawText) return "";
|
|
50
|
+
|
|
51
|
+
const chromeLines = new Set([
|
|
52
|
+
"copy",
|
|
53
|
+
"good response",
|
|
54
|
+
"bad response",
|
|
55
|
+
"read aloud",
|
|
56
|
+
"edit",
|
|
57
|
+
"retry",
|
|
58
|
+
"continue generating",
|
|
59
|
+
"share",
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
const lines = [];
|
|
63
|
+
let inCodeFence = false;
|
|
64
|
+
|
|
65
|
+
for (const line of String(rawText).replace(/\r\n?/g, "\n").split("\n")) {
|
|
66
|
+
const trimmed = line.trim();
|
|
67
|
+
const isFenceLine = trimmed.startsWith("```");
|
|
68
|
+
const normalizedLine = inCodeFence || isFenceLine ? line.replace(/[\t ]+$/g, "") : line;
|
|
69
|
+
|
|
70
|
+
lines.push({
|
|
71
|
+
text: normalizedLine,
|
|
72
|
+
trimmed,
|
|
73
|
+
isChrome: trimmed.length > 0 && chromeLines.has(trimmed.toLowerCase()),
|
|
74
|
+
inCodeFence,
|
|
75
|
+
isFenceLine,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (isFenceLine) {
|
|
79
|
+
inCodeFence = !inCodeFence;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
while (lines.length > 0 && lines[0].trimmed.length === 0) {
|
|
84
|
+
lines.shift();
|
|
85
|
+
}
|
|
86
|
+
while (lines.length > 0 && lines[lines.length - 1].trimmed.length === 0) {
|
|
87
|
+
lines.pop();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let trailingChromeStart = lines.length;
|
|
91
|
+
while (trailingChromeStart > 0) {
|
|
92
|
+
const line = lines[trailingChromeStart - 1];
|
|
93
|
+
if (line.inCodeFence || line.isFenceLine || !line.isChrome) break;
|
|
94
|
+
trailingChromeStart--;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const trailingChromeCount = lines.length - trailingChromeStart;
|
|
98
|
+
if (trailingChromeCount >= 2) {
|
|
99
|
+
lines.splice(trailingChromeStart);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
while (lines.length > 0 && lines[0].trimmed.length === 0) {
|
|
103
|
+
lines.shift();
|
|
104
|
+
}
|
|
105
|
+
while (lines.length > 0 && lines[lines.length - 1].trimmed.length === 0) {
|
|
106
|
+
lines.pop();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return lines.map((line) => line.text).join("\n");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function extractLatestAssistantSnapshot(candidates) {
|
|
113
|
+
if (!Array.isArray(candidates)) return null;
|
|
114
|
+
|
|
115
|
+
let latestEmptyAssistant = null;
|
|
116
|
+
|
|
117
|
+
for (let i = candidates.length - 1; i >= 0; i--) {
|
|
118
|
+
const candidate = candidates[i];
|
|
119
|
+
if (!candidate?.isAssistant) continue;
|
|
120
|
+
|
|
121
|
+
const snapshot = {
|
|
122
|
+
...candidate,
|
|
123
|
+
text: cleanChatGPTResponseText(candidate?.text || ""),
|
|
124
|
+
turnIndex: i,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
if (snapshot.text) {
|
|
128
|
+
return snapshot;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!latestEmptyAssistant) {
|
|
132
|
+
latestEmptyAssistant = snapshot;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return latestEmptyAssistant;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function normalizeResponseSnapshot(rawSnapshot) {
|
|
140
|
+
const candidates = rawSnapshot?.candidates;
|
|
141
|
+
return {
|
|
142
|
+
latestAssistant: extractLatestAssistantSnapshot(candidates),
|
|
143
|
+
assistantCount: Array.isArray(candidates)
|
|
144
|
+
? candidates.filter((candidate) => candidate?.isAssistant).length
|
|
145
|
+
: 0,
|
|
146
|
+
stopVisible: Boolean(rawSnapshot?.stopVisible),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isNewAssistantContent(
|
|
151
|
+
latestAssistant,
|
|
152
|
+
baselineAssistant,
|
|
153
|
+
assistantCount = 0,
|
|
154
|
+
baselineAssistantCount = 0
|
|
155
|
+
) {
|
|
156
|
+
if (!latestAssistant) return false;
|
|
157
|
+
if (!baselineAssistant) return true;
|
|
158
|
+
if (latestAssistant.messageId && baselineAssistant.messageId) {
|
|
159
|
+
if (latestAssistant.messageId !== baselineAssistant.messageId) {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const currentText = latestAssistant.text || "";
|
|
165
|
+
const baselineText = baselineAssistant.text || "";
|
|
166
|
+
|
|
167
|
+
if (assistantCount > baselineAssistantCount) {
|
|
168
|
+
if (latestAssistant.turnIndex !== baselineAssistant.turnIndex) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
if (currentText !== baselineText) {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (currentText !== baselineText) {
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function isChatGPTResponseComplete(snapshot, stableCycles, stableMs) {
|
|
184
|
+
if (!snapshot?.text) return false;
|
|
185
|
+
if (snapshot.stopVisible) return false;
|
|
186
|
+
if (snapshot.hasFinishedActions) return true;
|
|
187
|
+
return stableCycles >= 6 && stableMs >= 1200;
|
|
45
188
|
}
|
|
46
189
|
|
|
47
190
|
async function evaluate(cdp, expression) {
|
|
@@ -130,6 +273,33 @@ async function waitForPromptReady(cdp, timeoutMs = 30000) {
|
|
|
130
273
|
return false;
|
|
131
274
|
}
|
|
132
275
|
|
|
276
|
+
function normalizeChatGPTModelChoice(desiredModel) {
|
|
277
|
+
const normalized = String(desiredModel || "")
|
|
278
|
+
.toLowerCase()
|
|
279
|
+
.replace(/[^a-z0-9]/g, "");
|
|
280
|
+
|
|
281
|
+
if (["instant", "gpt53"].includes(normalized)) return "instant";
|
|
282
|
+
if (["thinking", "gpt54thinking"].includes(normalized)) return "thinking";
|
|
283
|
+
if (["pro", "gpt54pro"].includes(normalized)) return "pro";
|
|
284
|
+
|
|
285
|
+
return normalized;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function resolveChatGPTModelMenuOption(items, desiredModel) {
|
|
289
|
+
if (!Array.isArray(items)) return null;
|
|
290
|
+
|
|
291
|
+
const targetModel = normalizeChatGPTModelChoice(desiredModel);
|
|
292
|
+
|
|
293
|
+
return items.find((item) => {
|
|
294
|
+
if (item?.role !== "menuitemradio") return false;
|
|
295
|
+
if (typeof item?.testId !== "string" || !item.testId.startsWith("model-switcher-")) return false;
|
|
296
|
+
|
|
297
|
+
const label = normalizeChatGPTModelChoice(item.label || "");
|
|
298
|
+
const testId = normalizeChatGPTModelChoice(item.testId.replace(/^model-switcher-/, ""));
|
|
299
|
+
return label === targetModel || testId === targetModel;
|
|
300
|
+
}) || null;
|
|
301
|
+
}
|
|
302
|
+
|
|
133
303
|
async function selectModel(cdp, desiredModel, timeoutMs = 8000) {
|
|
134
304
|
const modelButton = await evaluate(
|
|
135
305
|
cdp,
|
|
@@ -150,57 +320,66 @@ async function selectModel(cdp, desiredModel, timeoutMs = 8000) {
|
|
|
150
320
|
})()`
|
|
151
321
|
);
|
|
152
322
|
await delay(300);
|
|
153
|
-
|
|
154
|
-
const normalizedModel = desiredModel
|
|
323
|
+
|
|
324
|
+
const normalizedModel = normalizeChatGPTModelChoice(desiredModel);
|
|
155
325
|
const deadline = Date.now() + timeoutMs;
|
|
156
|
-
|
|
326
|
+
|
|
157
327
|
while (Date.now() < deadline) {
|
|
158
328
|
const result = await evaluate(
|
|
159
329
|
cdp,
|
|
160
330
|
`(() => {
|
|
161
|
-
|
|
162
|
-
const targetModel = ${JSON.stringify(normalizedModel)};
|
|
163
|
-
const menuSelector = '${SELECTORS.menuContainer}';
|
|
164
|
-
const itemSelector = '${SELECTORS.menuItem}';
|
|
165
|
-
const normalize = (text) => (text || '').toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
166
|
-
|
|
167
|
-
const menu = document.querySelector(menuSelector);
|
|
331
|
+
const menu = document.querySelector('[role="menu"][data-radix-menu-content]');
|
|
168
332
|
if (!menu) {
|
|
169
333
|
return { found: false, waiting: true };
|
|
170
334
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
if (bestMatch) {
|
|
186
|
-
dispatchClickSequence(bestMatch);
|
|
187
|
-
return { found: true, success: true, label: bestMatch.textContent?.trim() };
|
|
188
|
-
}
|
|
189
|
-
return { found: true, success: false, error: 'No matching model in menu' };
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
found: true,
|
|
338
|
+
items: Array.from(menu.children).map((item) => {
|
|
339
|
+
const primary = item.querySelector?.('.min-w-0 > span');
|
|
340
|
+
return {
|
|
341
|
+
role: item.getAttribute?.('role') || null,
|
|
342
|
+
label: (primary?.textContent || item.getAttribute?.('aria-label') || item.textContent || '').trim(),
|
|
343
|
+
testId: item.getAttribute?.('data-testid') || null,
|
|
344
|
+
};
|
|
345
|
+
}),
|
|
346
|
+
};
|
|
190
347
|
})()`
|
|
191
348
|
);
|
|
192
|
-
|
|
349
|
+
|
|
193
350
|
if (result && result.found) {
|
|
194
|
-
|
|
351
|
+
const match = resolveChatGPTModelMenuOption(result.items, normalizedModel);
|
|
352
|
+
if (match) {
|
|
353
|
+
await evaluate(
|
|
354
|
+
cdp,
|
|
355
|
+
`(() => {
|
|
356
|
+
${buildClickDispatcher()}
|
|
357
|
+
const menu = document.querySelector('[role="menu"][data-radix-menu-content]');
|
|
358
|
+
const item = menu?.querySelector('[data-testid="${match.testId}"]');
|
|
359
|
+
if (item) dispatchClickSequence(item);
|
|
360
|
+
})()`
|
|
361
|
+
);
|
|
195
362
|
await delay(200);
|
|
196
|
-
return
|
|
363
|
+
return match.label;
|
|
197
364
|
}
|
|
198
|
-
|
|
365
|
+
|
|
366
|
+
const available = Array.isArray(result.items)
|
|
367
|
+
? result.items
|
|
368
|
+
.filter((item) => item?.role === "menuitemradio" && typeof item?.testId === "string" && item.testId.startsWith("model-switcher-"))
|
|
369
|
+
.map((item) => item.label)
|
|
370
|
+
.filter(Boolean)
|
|
371
|
+
.join(", ")
|
|
372
|
+
: "";
|
|
373
|
+
throw new Error(
|
|
374
|
+
available
|
|
375
|
+
? `Model not found: ${desiredModel}. Available: ${available}`
|
|
376
|
+
: `Model not found: ${desiredModel}`
|
|
377
|
+
);
|
|
199
378
|
}
|
|
200
|
-
|
|
379
|
+
|
|
201
380
|
await delay(100);
|
|
202
381
|
}
|
|
203
|
-
|
|
382
|
+
|
|
204
383
|
throw new Error(`Model not found: ${desiredModel} (timeout)`);
|
|
205
384
|
}
|
|
206
385
|
|
|
@@ -314,78 +493,144 @@ async function clickSend(cdp, inputCdp) {
|
|
|
314
493
|
return true;
|
|
315
494
|
}
|
|
316
495
|
|
|
317
|
-
async function
|
|
496
|
+
async function readChatGPTResponseSnapshot(cdp) {
|
|
497
|
+
return evaluate(
|
|
498
|
+
cdp,
|
|
499
|
+
`(() => {
|
|
500
|
+
const scope = document.querySelector('main') || document;
|
|
501
|
+
const CONVERSATION_SELECTOR = ${JSON.stringify(SELECTORS.conversationTurn)};
|
|
502
|
+
const ASSISTANT_SELECTOR = ${JSON.stringify(SELECTORS.assistantMessage)};
|
|
503
|
+
const CONTENT_SELECTORS = ${JSON.stringify(SELECTORS.assistantContent.split(", "))};
|
|
504
|
+
const STOP_SELECTOR = ${JSON.stringify(SELECTORS.stopButton)};
|
|
505
|
+
const FINISHED_SELECTOR = ${JSON.stringify(SELECTORS.finishedActions)};
|
|
506
|
+
|
|
507
|
+
const toCandidate = (turnNode, messageRoot = null) => {
|
|
508
|
+
const resolvedMessageRoot = messageRoot || (turnNode.matches?.(ASSISTANT_SELECTOR)
|
|
509
|
+
? turnNode
|
|
510
|
+
: turnNode.querySelector(ASSISTANT_SELECTOR));
|
|
511
|
+
const searchRoot = resolvedMessageRoot || turnNode;
|
|
512
|
+
let contentRoot = null;
|
|
513
|
+
|
|
514
|
+
for (const selector of CONTENT_SELECTORS) {
|
|
515
|
+
const match = selector === '[dir="auto"]'
|
|
516
|
+
? (searchRoot.matches?.(selector) ? searchRoot : null)
|
|
517
|
+
: (searchRoot.matches?.(selector) ? searchRoot : searchRoot.querySelector(selector));
|
|
518
|
+
if (match) {
|
|
519
|
+
contentRoot = match;
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const role =
|
|
525
|
+
resolvedMessageRoot?.getAttribute('data-message-author-role') ||
|
|
526
|
+
turnNode.getAttribute('data-message-author-role') ||
|
|
527
|
+
null;
|
|
528
|
+
const turn =
|
|
529
|
+
resolvedMessageRoot?.getAttribute('data-turn') ||
|
|
530
|
+
turnNode.getAttribute('data-turn') ||
|
|
531
|
+
null;
|
|
532
|
+
const isAssistant =
|
|
533
|
+
role === 'assistant' ||
|
|
534
|
+
turn === 'assistant' ||
|
|
535
|
+
resolvedMessageRoot !== null;
|
|
536
|
+
const text = (contentRoot || turnNode).innerText || (contentRoot || turnNode).textContent || '';
|
|
537
|
+
const messageId =
|
|
538
|
+
resolvedMessageRoot?.getAttribute('data-message-id') ||
|
|
539
|
+
turnNode.getAttribute('data-message-id') ||
|
|
540
|
+
null;
|
|
541
|
+
const hasFinishedActions = Boolean(turnNode.querySelector(FINISHED_SELECTOR));
|
|
542
|
+
|
|
543
|
+
return {
|
|
544
|
+
role,
|
|
545
|
+
turn,
|
|
546
|
+
isAssistant,
|
|
547
|
+
text,
|
|
548
|
+
messageId,
|
|
549
|
+
hasFinishedActions,
|
|
550
|
+
};
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
let candidates = Array.from(scope.querySelectorAll(CONVERSATION_SELECTOR)).map((turnNode) =>
|
|
554
|
+
toCandidate(turnNode)
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
if (candidates.length === 0) {
|
|
558
|
+
candidates = Array.from(scope.querySelectorAll(ASSISTANT_SELECTOR)).map((messageRoot) =>
|
|
559
|
+
toCandidate(messageRoot, messageRoot)
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return {
|
|
564
|
+
candidates,
|
|
565
|
+
stopVisible: Boolean(scope.querySelector(STOP_SELECTOR)),
|
|
566
|
+
};
|
|
567
|
+
})()`
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
async function waitForResponse(
|
|
572
|
+
cdp,
|
|
573
|
+
timeoutMs = 2700000,
|
|
574
|
+
baselineAssistant,
|
|
575
|
+
baselineAssistantCount
|
|
576
|
+
) {
|
|
318
577
|
const deadline = Date.now() + timeoutMs;
|
|
319
|
-
let
|
|
578
|
+
let previousText = "";
|
|
320
579
|
let stableCycles = 0;
|
|
321
|
-
const requiredStableCycles = 6;
|
|
322
|
-
const minStableMs = 1200;
|
|
323
580
|
let lastChangeAt = Date.now();
|
|
581
|
+
|
|
582
|
+
previousText = baselineAssistant?.text || "";
|
|
583
|
+
lastChangeAt = Date.now();
|
|
584
|
+
|
|
324
585
|
while (Date.now() < deadline) {
|
|
325
|
-
const snapshot = await
|
|
326
|
-
|
|
327
|
-
`(() => {
|
|
328
|
-
const CONVERSATION_SELECTOR = '${SELECTORS.conversationTurn}';
|
|
329
|
-
const ASSISTANT_SELECTOR = '${SELECTORS.assistantMessage}';
|
|
330
|
-
const STOP_SELECTOR = '${SELECTORS.stopButton}';
|
|
331
|
-
const FINISHED_SELECTOR = '${SELECTORS.finishedActions}';
|
|
332
|
-
const isAssistantTurn = (node) => {
|
|
333
|
-
if (!(node instanceof HTMLElement)) return false;
|
|
334
|
-
const role = (node.getAttribute('data-message-author-role') || '').toLowerCase();
|
|
335
|
-
if (role === 'assistant') return true;
|
|
336
|
-
const turn = (node.getAttribute('data-turn') || '').toLowerCase();
|
|
337
|
-
if (turn === 'assistant') return true;
|
|
338
|
-
return Boolean(node.querySelector(ASSISTANT_SELECTOR));
|
|
339
|
-
};
|
|
340
|
-
const turns = Array.from(document.querySelectorAll(CONVERSATION_SELECTOR));
|
|
341
|
-
let lastAssistantTurn = null;
|
|
342
|
-
for (let i = turns.length - 1; i >= 0; i--) {
|
|
343
|
-
if (isAssistantTurn(turns[i])) {
|
|
344
|
-
lastAssistantTurn = turns[i];
|
|
345
|
-
break;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
if (!lastAssistantTurn) {
|
|
349
|
-
return { text: '', stopVisible: Boolean(document.querySelector(STOP_SELECTOR)), finished: false };
|
|
350
|
-
}
|
|
351
|
-
const messageRoot = lastAssistantTurn.querySelector(ASSISTANT_SELECTOR) || lastAssistantTurn;
|
|
352
|
-
const contentRoot = messageRoot.querySelector('.markdown') ||
|
|
353
|
-
messageRoot.querySelector('[data-message-content]') ||
|
|
354
|
-
messageRoot.querySelector('.prose') ||
|
|
355
|
-
messageRoot;
|
|
356
|
-
const text = (contentRoot?.innerText || contentRoot?.textContent || '').trim();
|
|
357
|
-
const stopVisible = Boolean(document.querySelector(STOP_SELECTOR));
|
|
358
|
-
const finished = Boolean(lastAssistantTurn.querySelector(FINISHED_SELECTOR));
|
|
359
|
-
const messageId = messageRoot.getAttribute('data-message-id') || null;
|
|
360
|
-
return { text, stopVisible, finished, messageId, turnIndex: turns.length - 1 };
|
|
361
|
-
})()`
|
|
362
|
-
);
|
|
586
|
+
const snapshot = await readChatGPTResponseSnapshot(cdp);
|
|
587
|
+
|
|
363
588
|
if (!snapshot) {
|
|
364
589
|
await delay(400);
|
|
365
590
|
continue;
|
|
366
591
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
592
|
+
|
|
593
|
+
const { latestAssistant, assistantCount, stopVisible } = normalizeResponseSnapshot(snapshot);
|
|
594
|
+
const currentText = latestAssistant?.text || "";
|
|
595
|
+
const hasNewAssistantContent = isNewAssistantContent(
|
|
596
|
+
latestAssistant,
|
|
597
|
+
baselineAssistant,
|
|
598
|
+
assistantCount,
|
|
599
|
+
baselineAssistantCount
|
|
600
|
+
);
|
|
601
|
+
|
|
602
|
+
if (!hasNewAssistantContent) {
|
|
603
|
+
await delay(400);
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (currentText !== previousText) {
|
|
608
|
+
previousText = currentText;
|
|
370
609
|
stableCycles = 0;
|
|
371
610
|
lastChangeAt = Date.now();
|
|
372
|
-
} else {
|
|
611
|
+
} else if (currentText) {
|
|
373
612
|
stableCycles++;
|
|
613
|
+
} else {
|
|
614
|
+
stableCycles = 0;
|
|
615
|
+
lastChangeAt = Date.now();
|
|
374
616
|
}
|
|
617
|
+
|
|
375
618
|
const stableMs = Date.now() - lastChangeAt;
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
}
|
|
619
|
+
const completionSnapshot = latestAssistant
|
|
620
|
+
? { ...latestAssistant, stopVisible }
|
|
621
|
+
: { text: "", stopVisible, hasFinishedActions: false };
|
|
622
|
+
|
|
623
|
+
if (isChatGPTResponseComplete(completionSnapshot, stableCycles, stableMs)) {
|
|
624
|
+
return {
|
|
625
|
+
text: latestAssistant.text,
|
|
626
|
+
messageId: latestAssistant.messageId,
|
|
627
|
+
turnIndex: latestAssistant.turnIndex,
|
|
628
|
+
};
|
|
386
629
|
}
|
|
630
|
+
|
|
387
631
|
await delay(400);
|
|
388
632
|
}
|
|
633
|
+
|
|
389
634
|
throw new Error("Response timeout");
|
|
390
635
|
}
|
|
391
636
|
|
|
@@ -426,6 +671,13 @@ async function query(options) {
|
|
|
426
671
|
throw new Error("Cloudflare challenge detected - complete in browser");
|
|
427
672
|
}
|
|
428
673
|
const loginStatus = await checkLoginStatus(cdp);
|
|
674
|
+
if (loginStatus.status === 0) {
|
|
675
|
+
throw new Error(
|
|
676
|
+
loginStatus.error
|
|
677
|
+
? `ChatGPT login check failed: ${loginStatus.error}`
|
|
678
|
+
: "ChatGPT login check failed"
|
|
679
|
+
);
|
|
680
|
+
}
|
|
429
681
|
if (loginStatus.status !== 200 || loginStatus.hasLoginCta) {
|
|
430
682
|
throw new Error("ChatGPT login required");
|
|
431
683
|
}
|
|
@@ -444,9 +696,15 @@ async function query(options) {
|
|
|
444
696
|
}
|
|
445
697
|
await typePrompt(cdp, inputCdp, prompt);
|
|
446
698
|
log("Prompt typed");
|
|
699
|
+
const baseline = normalizeResponseSnapshot(await readChatGPTResponseSnapshot(cdp));
|
|
447
700
|
await clickSend(cdp, inputCdp);
|
|
448
701
|
log("Prompt sent, waiting for response...");
|
|
449
|
-
const response = await waitForResponse(
|
|
702
|
+
const response = await waitForResponse(
|
|
703
|
+
cdp,
|
|
704
|
+
timeout,
|
|
705
|
+
baseline.latestAssistant,
|
|
706
|
+
baseline.assistantCount
|
|
707
|
+
);
|
|
450
708
|
log(`Response received (${response.text.length} chars)`);
|
|
451
709
|
return {
|
|
452
710
|
response: response.text,
|
|
@@ -455,8 +713,22 @@ async function query(options) {
|
|
|
455
713
|
tookMs: Date.now() - startTime,
|
|
456
714
|
};
|
|
457
715
|
} finally {
|
|
458
|
-
|
|
716
|
+
try {
|
|
717
|
+
await closeTab(tabId);
|
|
718
|
+
} catch (error) {
|
|
719
|
+
log(`Failed to close ChatGPT tab ${tabId}: ${error?.message || error}`);
|
|
720
|
+
}
|
|
459
721
|
}
|
|
460
722
|
}
|
|
461
723
|
|
|
462
|
-
module.exports = {
|
|
724
|
+
module.exports = {
|
|
725
|
+
query,
|
|
726
|
+
hasRequiredCookies,
|
|
727
|
+
cleanChatGPTResponseText,
|
|
728
|
+
extractLatestAssistantSnapshot,
|
|
729
|
+
normalizeChatGPTModelChoice,
|
|
730
|
+
resolveChatGPTModelMenuOption,
|
|
731
|
+
isNewAssistantContent,
|
|
732
|
+
isChatGPTResponseComplete,
|
|
733
|
+
CHATGPT_URL,
|
|
734
|
+
};
|
package/package.json
CHANGED
package/native/edited.png
DELETED
|
Binary file
|