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