chatgpt-webui-mcp 0.1.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/.env.example +50 -0
- package/INSTALL.md +65 -0
- package/LICENSE +21 -0
- package/README.md +228 -0
- package/deploy/systemd/chatgpt-webui-mcp-sse.sh +16 -0
- package/deploy/systemd/chatgpt-webui-mcp.env.example +28 -0
- package/deploy/systemd/chatgpt-webui-mcp.service +15 -0
- package/dist/chatgpt-webui-client.d.ts +43 -0
- package/dist/chatgpt-webui-client.js +1284 -0
- package/dist/chatgpt-webui-client.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +700 -0
- package/dist/index.js.map +1 -0
- package/dist/self-test.d.ts +1 -0
- package/dist/self-test.js +72 -0
- package/dist/self-test.js.map +1 -0
- package/package.json +50 -0
- package/src/chatgpt-webui-client.ts +1688 -0
- package/src/index.ts +858 -0
- package/src/self-test.ts +86 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,1284 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { Session, } from "httpcloak";
|
|
4
|
+
const DEFAULT_BASE_URL = "https://chatgpt.com";
|
|
5
|
+
const DEFAULT_MODEL = "auto";
|
|
6
|
+
const CHAT_REQUIREMENTS_PATH = "/backend-api/sentinel/chat-requirements";
|
|
7
|
+
const DEFAULT_HTTPCLOAK_PRESET = "chrome-145";
|
|
8
|
+
const DEFAULT_TRANSPORT = "camofox";
|
|
9
|
+
const DEFAULT_CAMOFOX_BASE_URL = "http://127.0.0.1:9377";
|
|
10
|
+
const DEFAULT_CAMOFOX_USER_ID = "chatgpt-webui-mcp";
|
|
11
|
+
const DEFAULT_CAMOFOX_SESSION_KEY = "chatgpt-webui";
|
|
12
|
+
const DEFAULT_CAMOFOX_WAIT_TIMEOUT_MS = 5400000;
|
|
13
|
+
const DEFAULT_CAMOFOX_WORKSPACE = "PRO";
|
|
14
|
+
const DEFAULT_IMAGE_SCREENSHOT_FALLBACK = false;
|
|
15
|
+
function readTokenFromFile(filePath) {
|
|
16
|
+
const path = filePath.trim();
|
|
17
|
+
if (!path) {
|
|
18
|
+
return "";
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
return readFileSync(path, "utf8").trim();
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return "";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function normalizeBaseUrl(baseUrl) {
|
|
28
|
+
return baseUrl.replace(/\/+$/, "");
|
|
29
|
+
}
|
|
30
|
+
function normalizeTransport(raw) {
|
|
31
|
+
const normalized = String(raw ?? DEFAULT_TRANSPORT).trim().toLowerCase();
|
|
32
|
+
if (normalized === "httpcloak") {
|
|
33
|
+
return "httpcloak";
|
|
34
|
+
}
|
|
35
|
+
return "camofox";
|
|
36
|
+
}
|
|
37
|
+
function safeJsonParse(raw) {
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(raw);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function summarizeErrorPayload(raw) {
|
|
46
|
+
const parsed = safeJsonParse(raw);
|
|
47
|
+
const detail = parsed?.detail ?? parsed?.message ?? parsed?.error;
|
|
48
|
+
if (detail && typeof detail === "string") {
|
|
49
|
+
return detail;
|
|
50
|
+
}
|
|
51
|
+
const compact = raw.replace(/\s+/g, " ").trim();
|
|
52
|
+
return compact.slice(0, 300);
|
|
53
|
+
}
|
|
54
|
+
function parseSseEvents(raw) {
|
|
55
|
+
const events = [];
|
|
56
|
+
const lines = raw.split(/\r?\n/);
|
|
57
|
+
for (const line of lines) {
|
|
58
|
+
if (!line.startsWith("data:")) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const payload = line.slice(5).trim();
|
|
62
|
+
if (!payload || payload === "[DONE]") {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const parsed = safeJsonParse(payload);
|
|
66
|
+
if (parsed) {
|
|
67
|
+
events.push(parsed);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return events;
|
|
71
|
+
}
|
|
72
|
+
function parseOptionalBoolean(raw) {
|
|
73
|
+
if (raw === undefined) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
const normalized = raw.trim().toLowerCase();
|
|
77
|
+
if (!normalized) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
if (["1", "true", "yes", "on"].includes(normalized)) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
if (["0", "false", "no", "off"].includes(normalized)) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
function parsePositiveNumber(raw) {
|
|
89
|
+
if (raw === undefined) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
const normalized = raw.trim();
|
|
93
|
+
if (!normalized) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
const parsed = Number(normalized);
|
|
97
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
return parsed;
|
|
101
|
+
}
|
|
102
|
+
function parseNonNegativeInteger(raw) {
|
|
103
|
+
if (raw === undefined) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
const normalized = raw.trim();
|
|
107
|
+
if (!normalized) {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
const parsed = Number(normalized);
|
|
111
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
return parsed;
|
|
115
|
+
}
|
|
116
|
+
function buildHttpcloakSessionOptionsFromEnv() {
|
|
117
|
+
const preset = String(process.env.CHATGPT_HTTPCLOAK_PRESET ?? DEFAULT_HTTPCLOAK_PRESET).trim();
|
|
118
|
+
const sessionOptions = {
|
|
119
|
+
preset: preset || DEFAULT_HTTPCLOAK_PRESET,
|
|
120
|
+
};
|
|
121
|
+
const httpVersion = String(process.env.CHATGPT_HTTPCLOAK_HTTP_VERSION ?? "").trim();
|
|
122
|
+
if (httpVersion) {
|
|
123
|
+
sessionOptions.httpVersion = httpVersion;
|
|
124
|
+
}
|
|
125
|
+
const proxy = String(process.env.CHATGPT_HTTPCLOAK_PROXY ?? "").trim();
|
|
126
|
+
if (proxy) {
|
|
127
|
+
sessionOptions.proxy = proxy;
|
|
128
|
+
}
|
|
129
|
+
const tcpProxy = String(process.env.CHATGPT_HTTPCLOAK_TCP_PROXY ?? "").trim();
|
|
130
|
+
if (tcpProxy) {
|
|
131
|
+
sessionOptions.tcpProxy = tcpProxy;
|
|
132
|
+
}
|
|
133
|
+
const udpProxy = String(process.env.CHATGPT_HTTPCLOAK_UDP_PROXY ?? "").trim();
|
|
134
|
+
if (udpProxy) {
|
|
135
|
+
sessionOptions.udpProxy = udpProxy;
|
|
136
|
+
}
|
|
137
|
+
const echConfigDomain = String(process.env.CHATGPT_HTTPCLOAK_ECH_CONFIG_DOMAIN ?? "").trim();
|
|
138
|
+
if (echConfigDomain) {
|
|
139
|
+
sessionOptions.echConfigDomain = echConfigDomain;
|
|
140
|
+
}
|
|
141
|
+
const timeoutSeconds = parsePositiveNumber(process.env.CHATGPT_HTTPCLOAK_TIMEOUT_SECONDS);
|
|
142
|
+
if (timeoutSeconds !== undefined) {
|
|
143
|
+
sessionOptions.timeout = timeoutSeconds;
|
|
144
|
+
}
|
|
145
|
+
const quicIdleTimeout = parsePositiveNumber(process.env.CHATGPT_HTTPCLOAK_QUIC_IDLE_TIMEOUT_SECONDS);
|
|
146
|
+
if (quicIdleTimeout !== undefined) {
|
|
147
|
+
sessionOptions.quicIdleTimeout = quicIdleTimeout;
|
|
148
|
+
}
|
|
149
|
+
const tlsOnly = parseOptionalBoolean(process.env.CHATGPT_HTTPCLOAK_TLS_ONLY);
|
|
150
|
+
if (tlsOnly !== undefined) {
|
|
151
|
+
sessionOptions.tlsOnly = tlsOnly;
|
|
152
|
+
}
|
|
153
|
+
const verifyTls = parseOptionalBoolean(process.env.CHATGPT_HTTPCLOAK_VERIFY_TLS);
|
|
154
|
+
if (verifyTls !== undefined) {
|
|
155
|
+
sessionOptions.verify = verifyTls;
|
|
156
|
+
}
|
|
157
|
+
const allowRedirects = parseOptionalBoolean(process.env.CHATGPT_HTTPCLOAK_ALLOW_REDIRECTS);
|
|
158
|
+
if (allowRedirects !== undefined) {
|
|
159
|
+
sessionOptions.allowRedirects = allowRedirects;
|
|
160
|
+
}
|
|
161
|
+
const preferIpv4 = parseOptionalBoolean(process.env.CHATGPT_HTTPCLOAK_PREFER_IPV4);
|
|
162
|
+
if (preferIpv4 !== undefined) {
|
|
163
|
+
sessionOptions.preferIpv4 = preferIpv4;
|
|
164
|
+
}
|
|
165
|
+
const retry = parseNonNegativeInteger(process.env.CHATGPT_HTTPCLOAK_RETRY);
|
|
166
|
+
if (retry !== undefined) {
|
|
167
|
+
sessionOptions.retry = retry;
|
|
168
|
+
}
|
|
169
|
+
const maxRedirects = parseNonNegativeInteger(process.env.CHATGPT_HTTPCLOAK_MAX_REDIRECTS);
|
|
170
|
+
if (maxRedirects !== undefined) {
|
|
171
|
+
sessionOptions.maxRedirects = maxRedirects;
|
|
172
|
+
}
|
|
173
|
+
return sessionOptions;
|
|
174
|
+
}
|
|
175
|
+
function toErrorMessage(error) {
|
|
176
|
+
if (error instanceof Error) {
|
|
177
|
+
return error.message;
|
|
178
|
+
}
|
|
179
|
+
return String(error);
|
|
180
|
+
}
|
|
181
|
+
function sleep(ms) {
|
|
182
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
183
|
+
}
|
|
184
|
+
function parseSnapshotForRef(snapshot, role, label) {
|
|
185
|
+
const lines = snapshot.split(/\r?\n/);
|
|
186
|
+
for (const line of lines) {
|
|
187
|
+
const pattern = new RegExp(`${role}\\s+(?:\"([^\"]+)\"\\s+)?\\[(e\\d+)\\]`, "i");
|
|
188
|
+
const match = line.match(pattern);
|
|
189
|
+
if (!match) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const name = match[1] ?? "";
|
|
193
|
+
const ref = match[2] ?? "";
|
|
194
|
+
if (label.test(name)) {
|
|
195
|
+
return ref;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
function extractLikelyAssistantTextFromSnapshot(snapshot, prompt) {
|
|
201
|
+
const rawLines = snapshot.split(/\r?\n/);
|
|
202
|
+
const lines = rawLines
|
|
203
|
+
.map((line) => line.trim())
|
|
204
|
+
.filter(Boolean)
|
|
205
|
+
.map((line) => line.replace(/^[-]\s+/, ""));
|
|
206
|
+
const assistantChunks = [];
|
|
207
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
208
|
+
const line = lines[i] ?? "";
|
|
209
|
+
if (!/^heading\s+"ChatGPT said:"/i.test(line)) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
const chunkLines = [];
|
|
213
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
214
|
+
const candidate = lines[j] ?? "";
|
|
215
|
+
if (/^heading\s+"/i.test(candidate) ||
|
|
216
|
+
/^button\s+"/i.test(candidate) ||
|
|
217
|
+
/^(article|complementary|dialog|main|banner):/i.test(candidate)) {
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
if (/^(paragraph|text):\s*/i.test(candidate)) {
|
|
221
|
+
let value = candidate.replace(/^(paragraph|text):\s*/i, "").trim();
|
|
222
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
223
|
+
value = value.slice(1, -1);
|
|
224
|
+
}
|
|
225
|
+
if (value && !/^Ask anything$/i.test(value)) {
|
|
226
|
+
chunkLines.push(value);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (chunkLines.length > 0) {
|
|
231
|
+
assistantChunks.push(chunkLines.join("\n"));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (assistantChunks.length > 0) {
|
|
235
|
+
return assistantChunks[assistantChunks.length - 1] ?? "";
|
|
236
|
+
}
|
|
237
|
+
const promptNormalized = prompt.trim();
|
|
238
|
+
const noisePatterns = [
|
|
239
|
+
/^By messaging ChatGPT/i,
|
|
240
|
+
/^Terms$/i,
|
|
241
|
+
/^Privacy Policy$/i,
|
|
242
|
+
/^Ask anything$/i,
|
|
243
|
+
/^What can I help with\?$/i,
|
|
244
|
+
/^Attach$/i,
|
|
245
|
+
/^Search$/i,
|
|
246
|
+
/^Study$/i,
|
|
247
|
+
/^Create image$/i,
|
|
248
|
+
/^Voice$/i,
|
|
249
|
+
/^Send prompt$/i,
|
|
250
|
+
/^Send message$/i,
|
|
251
|
+
/^Get a detailed report$/i,
|
|
252
|
+
/^Detailed report$/i,
|
|
253
|
+
/^Sources$/i,
|
|
254
|
+
/^Log in$/i,
|
|
255
|
+
/^Sign up for free$/i,
|
|
256
|
+
/^ChatGPT can make mistakes\./i,
|
|
257
|
+
];
|
|
258
|
+
const extracted = [];
|
|
259
|
+
for (const line of lines) {
|
|
260
|
+
if (!/^paragraph:\s*/i.test(line) && !/^text:\s*/i.test(line)) {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
let value = line;
|
|
264
|
+
value = value.replace(/^text:\s*/i, "");
|
|
265
|
+
value = value.replace(/^paragraph:\s*/i, "");
|
|
266
|
+
if (value.startsWith("\"") && value.endsWith("\"")) {
|
|
267
|
+
value = value.slice(1, -1);
|
|
268
|
+
}
|
|
269
|
+
const normalized = value.trim();
|
|
270
|
+
if (!normalized) {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (promptNormalized && normalized === promptNormalized) {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (noisePatterns.some((pattern) => pattern.test(normalized))) {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
extracted.push(normalized);
|
|
280
|
+
}
|
|
281
|
+
if (extracted.length === 0) {
|
|
282
|
+
return "";
|
|
283
|
+
}
|
|
284
|
+
extracted.sort((a, b) => b.length - a.length);
|
|
285
|
+
return extracted[0] ?? "";
|
|
286
|
+
}
|
|
287
|
+
function isLikelyModelOptionLabel(label) {
|
|
288
|
+
return (/\b(auto|instant|thinking|pro)\b/i.test(label) ||
|
|
289
|
+
/\b(gpt|o\d)\b/i.test(label) ||
|
|
290
|
+
/deep\s+research/i.test(label) ||
|
|
291
|
+
/legacy\s+models/i.test(label) ||
|
|
292
|
+
/alpha\s+models/i.test(label));
|
|
293
|
+
}
|
|
294
|
+
function normalizeWhitespace(value) {
|
|
295
|
+
return value.replace(/\s+/g, " ").trim();
|
|
296
|
+
}
|
|
297
|
+
function escapeRegExp(value) {
|
|
298
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
299
|
+
}
|
|
300
|
+
function snapshotIndicatesGenerationInProgress(snapshot) {
|
|
301
|
+
return (/button\s+"(?:Stop|Cancel)\b/i.test(snapshot) ||
|
|
302
|
+
/\b(Researching|Gathering sources|Working on your report|Thinking|Analyzing|Generating)\b/i.test(snapshot));
|
|
303
|
+
}
|
|
304
|
+
function snapshotIndicatesReadyForNextPrompt(snapshot) {
|
|
305
|
+
return (/button\s+"(?:Send prompt|Send message)"/i.test(snapshot) ||
|
|
306
|
+
/textbox\s+/i.test(snapshot) ||
|
|
307
|
+
/#prompt-textarea/i.test(snapshot));
|
|
308
|
+
}
|
|
309
|
+
function snapshotFatalUiError(snapshot) {
|
|
310
|
+
if (/\bSomething went wrong\b/i.test(snapshot)) {
|
|
311
|
+
return "something_went_wrong";
|
|
312
|
+
}
|
|
313
|
+
if (/\bUnable to load conversation\b/i.test(snapshot)) {
|
|
314
|
+
return "unable_to_load_conversation";
|
|
315
|
+
}
|
|
316
|
+
if (/\bYou have been logged out\b/i.test(snapshot)) {
|
|
317
|
+
return "session_logged_out";
|
|
318
|
+
}
|
|
319
|
+
if (/\bSession expired\b/i.test(snapshot)) {
|
|
320
|
+
return "session_expired";
|
|
321
|
+
}
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
function snapshotIndicatesLoginRequired(snapshot) {
|
|
325
|
+
return /button\s+"Log in"/i.test(snapshot) && /button\s+"Sign up for free"/i.test(snapshot);
|
|
326
|
+
}
|
|
327
|
+
function extractImageUrlsFromLinks(links) {
|
|
328
|
+
const output = [];
|
|
329
|
+
const seen = new Set();
|
|
330
|
+
for (const link of links) {
|
|
331
|
+
const url = String(link.url ?? "").trim();
|
|
332
|
+
if (!url || seen.has(url)) {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const isLikelyImageUrl = /\.(png|jpe?g|webp|gif)(?:[?#]|$)/i.test(url) ||
|
|
336
|
+
/(oaiusercontent|openaiusercontent|oaidalle|blob\.core\.windows\.net)/i.test(url) ||
|
|
337
|
+
/\/backend-api\/(files|asset)\//i.test(url);
|
|
338
|
+
if (!isLikelyImageUrl) {
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
seen.add(url);
|
|
342
|
+
output.push(url);
|
|
343
|
+
}
|
|
344
|
+
return output;
|
|
345
|
+
}
|
|
346
|
+
function extractVisitedUrlsFromStats(stats) {
|
|
347
|
+
const raw = stats.visitedUrls;
|
|
348
|
+
if (!Array.isArray(raw)) {
|
|
349
|
+
return [];
|
|
350
|
+
}
|
|
351
|
+
return raw
|
|
352
|
+
.filter((entry) => typeof entry === "string")
|
|
353
|
+
.map((entry) => entry.trim())
|
|
354
|
+
.filter(Boolean);
|
|
355
|
+
}
|
|
356
|
+
function extractUrlsFromSnapshot(snapshot) {
|
|
357
|
+
const output = [];
|
|
358
|
+
const seen = new Set();
|
|
359
|
+
for (const line of snapshot.split(/\r?\n/)) {
|
|
360
|
+
const match = line.match(/\b\/url:\s+"?([^"\s]+)"?/i);
|
|
361
|
+
if (!match) {
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
const url = String(match[1] ?? "").trim();
|
|
365
|
+
if (!url || seen.has(url)) {
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
seen.add(url);
|
|
369
|
+
output.push(url);
|
|
370
|
+
}
|
|
371
|
+
return output;
|
|
372
|
+
}
|
|
373
|
+
function parseCamofoxMenuItems(snapshot) {
|
|
374
|
+
const items = [];
|
|
375
|
+
const lines = snapshot.split(/\r?\n/);
|
|
376
|
+
const seenRefs = new Set();
|
|
377
|
+
const pushItem = (label, ref) => {
|
|
378
|
+
const normalizedLabel = normalizeWhitespace(label);
|
|
379
|
+
if (!normalizedLabel || !ref || seenRefs.has(ref) || !isLikelyModelOptionLabel(normalizedLabel)) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
seenRefs.add(ref);
|
|
383
|
+
items.push({ ref, label: normalizedLabel });
|
|
384
|
+
};
|
|
385
|
+
for (const line of lines) {
|
|
386
|
+
const menuMatch = line.match(/menuitem\s+"([^"]+)"\s+\[(e\d+)\]/i);
|
|
387
|
+
if (menuMatch) {
|
|
388
|
+
pushItem(menuMatch[1] ?? "", menuMatch[2] ?? "");
|
|
389
|
+
}
|
|
390
|
+
const buttonMatch = line.match(/button\s+"([^"]+)"\s+\[(e\d+)\]/i);
|
|
391
|
+
if (buttonMatch) {
|
|
392
|
+
pushItem(buttonMatch[1] ?? "", buttonMatch[2] ?? "");
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return items;
|
|
396
|
+
}
|
|
397
|
+
function modelSlugToLabelMatchers(modelSlug) {
|
|
398
|
+
const slug = modelSlug.trim().toLowerCase();
|
|
399
|
+
const known = {
|
|
400
|
+
"gpt-5-2": [/^Auto\b/i, /GPT-5\.2$/i],
|
|
401
|
+
"gpt-5-2-instant": [/^Instant\b/i, /GPT-5\.2\s+Instant/i],
|
|
402
|
+
"gpt-5-2-thinking": [/^Thinking\b/i, /GPT-5\.2\s+Thinking/i],
|
|
403
|
+
"gpt-5-2-pro": [/^Pro\b/i, /GPT-5\.2\s+Pro/i],
|
|
404
|
+
"gpt-5-1": [/GPT-5\.1$/i],
|
|
405
|
+
"gpt-5-1-instant": [/GPT-5\.1\s+Instant/i],
|
|
406
|
+
"gpt-5-1-thinking": [/GPT-5\.1\s+Thinking/i],
|
|
407
|
+
"gpt-5-1-pro": [/GPT-5\.1\s+Pro/i],
|
|
408
|
+
research: [/Deep\s+Research/i],
|
|
409
|
+
};
|
|
410
|
+
if (known[slug]) {
|
|
411
|
+
return known[slug];
|
|
412
|
+
}
|
|
413
|
+
const tokenMatcher = slug
|
|
414
|
+
.split("-")
|
|
415
|
+
.filter(Boolean)
|
|
416
|
+
.join(".*");
|
|
417
|
+
if (!tokenMatcher) {
|
|
418
|
+
return [];
|
|
419
|
+
}
|
|
420
|
+
return [new RegExp(tokenMatcher.replace(/\./g, "\\."), "i")];
|
|
421
|
+
}
|
|
422
|
+
function modelSlugToSubmenuMatchers(modelSlug) {
|
|
423
|
+
const slug = modelSlug.trim().toLowerCase();
|
|
424
|
+
if (!slug) {
|
|
425
|
+
return [];
|
|
426
|
+
}
|
|
427
|
+
if (slug.startsWith("gpt-5-1")) {
|
|
428
|
+
return [/Legacy models/i];
|
|
429
|
+
}
|
|
430
|
+
if (/alpha|preview|o\d|gpt-4\./i.test(slug)) {
|
|
431
|
+
return [/Alpha models/i, /Legacy models/i];
|
|
432
|
+
}
|
|
433
|
+
return [/Legacy models/i, /Alpha models/i];
|
|
434
|
+
}
|
|
435
|
+
function modeToDefaultModelSlug(mode) {
|
|
436
|
+
switch (mode) {
|
|
437
|
+
case "auto":
|
|
438
|
+
return "gpt-5-2";
|
|
439
|
+
case "instant":
|
|
440
|
+
return "gpt-5-2-instant";
|
|
441
|
+
case "thinking":
|
|
442
|
+
return "gpt-5-2-thinking";
|
|
443
|
+
case "pro":
|
|
444
|
+
return "gpt-5-2-pro";
|
|
445
|
+
default:
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
function parseConversationIdFromUrl(url) {
|
|
450
|
+
if (!url) {
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
const match = url.match(/\/c\/([^/?#]+)/i);
|
|
454
|
+
if (!match || !match[1]) {
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
return match[1];
|
|
458
|
+
}
|
|
459
|
+
function extractAssistantText(event) {
|
|
460
|
+
if (event.message?.author?.role !== "assistant") {
|
|
461
|
+
return "";
|
|
462
|
+
}
|
|
463
|
+
const parts = event.message?.content?.parts;
|
|
464
|
+
if (!Array.isArray(parts) || parts.length === 0) {
|
|
465
|
+
return "";
|
|
466
|
+
}
|
|
467
|
+
const values = parts
|
|
468
|
+
.map((part) => {
|
|
469
|
+
if (typeof part === "string") {
|
|
470
|
+
return part;
|
|
471
|
+
}
|
|
472
|
+
return "";
|
|
473
|
+
})
|
|
474
|
+
.filter(Boolean);
|
|
475
|
+
return values.join("\n").trim();
|
|
476
|
+
}
|
|
477
|
+
export class ChatgptWebuiClient {
|
|
478
|
+
#baseUrl;
|
|
479
|
+
#sessionToken;
|
|
480
|
+
#deviceId;
|
|
481
|
+
#transport;
|
|
482
|
+
#httpSession;
|
|
483
|
+
#camofoxBaseUrl;
|
|
484
|
+
#camofoxUserId;
|
|
485
|
+
#camofoxSessionKey;
|
|
486
|
+
#camofoxApiKey;
|
|
487
|
+
#camofoxWaitTimeoutMs;
|
|
488
|
+
#camofoxWorkspace;
|
|
489
|
+
#imageScreenshotFallback;
|
|
490
|
+
constructor(options = {}) {
|
|
491
|
+
this.#baseUrl = normalizeBaseUrl(options.baseUrl ?? process.env.CHATGPT_WEBUI_BASE_URL ?? DEFAULT_BASE_URL);
|
|
492
|
+
const tokenFromEnv = String(options.sessionToken ?? process.env.CHATGPT_SESSION_TOKEN ?? process.env.OPENAI_SESSION_TOKEN ?? "").trim();
|
|
493
|
+
const tokenFilePath = String(process.env.CHATGPT_SESSION_TOKEN_FILE ?? "").trim();
|
|
494
|
+
const tokenFromFile = tokenFromEnv ? "" : readTokenFromFile(tokenFilePath);
|
|
495
|
+
const sessionToken = tokenFromEnv || tokenFromFile;
|
|
496
|
+
this.#sessionToken = sessionToken;
|
|
497
|
+
this.#deviceId = crypto.randomUUID();
|
|
498
|
+
this.#transport = normalizeTransport(options.transport ?? process.env.CHATGPT_TRANSPORT);
|
|
499
|
+
this.#camofoxBaseUrl = normalizeBaseUrl(process.env.CHATGPT_BROWSER_BASE_URL ??
|
|
500
|
+
process.env.CHATGPT_CAMOFOX_BASE_URL ??
|
|
501
|
+
process.env.CAMOFOX_BASE_URL ??
|
|
502
|
+
DEFAULT_CAMOFOX_BASE_URL);
|
|
503
|
+
this.#camofoxUserId = String(process.env.CHATGPT_USER_ID ?? process.env.CHATGPT_CAMOFOX_USER_ID ?? DEFAULT_CAMOFOX_USER_ID).trim();
|
|
504
|
+
this.#camofoxSessionKey = String(process.env.CHATGPT_SESSION_KEY ?? process.env.CHATGPT_CAMOFOX_SESSION_KEY ?? DEFAULT_CAMOFOX_SESSION_KEY).trim();
|
|
505
|
+
this.#camofoxApiKey = String(process.env.CHATGPT_CAMOFOX_API_KEY ?? process.env.CAMOFOX_API_KEY ?? "").trim();
|
|
506
|
+
const waitTimeoutFromEnv = parsePositiveNumber(process.env.CHATGPT_WAIT_TIMEOUT_MS ?? process.env.CHATGPT_CAMOFOX_WAIT_TIMEOUT_MS);
|
|
507
|
+
this.#camofoxWaitTimeoutMs =
|
|
508
|
+
waitTimeoutFromEnv !== undefined ? Math.floor(waitTimeoutFromEnv) : DEFAULT_CAMOFOX_WAIT_TIMEOUT_MS;
|
|
509
|
+
this.#camofoxWorkspace = String(process.env.CHATGPT_WORKSPACE ?? process.env.CHATGPT_CAMOFOX_WORKSPACE ?? DEFAULT_CAMOFOX_WORKSPACE).trim();
|
|
510
|
+
this.#imageScreenshotFallback =
|
|
511
|
+
parseOptionalBoolean(process.env.CHATGPT_IMAGE_SCREENSHOT_FALLBACK) ?? DEFAULT_IMAGE_SCREENSHOT_FALLBACK;
|
|
512
|
+
if (!this.#sessionToken) {
|
|
513
|
+
throw new Error("CHATGPT_SESSION_TOKEN is required (cookie value of __Secure-next-auth.session-token) or set CHATGPT_SESSION_TOKEN_FILE");
|
|
514
|
+
}
|
|
515
|
+
this.#httpSession = null;
|
|
516
|
+
}
|
|
517
|
+
close() {
|
|
518
|
+
if (this.#httpSession) {
|
|
519
|
+
this.#httpSession.close();
|
|
520
|
+
this.#httpSession = null;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
#getHttpSession() {
|
|
524
|
+
if (!this.#httpSession) {
|
|
525
|
+
const session = new Session(buildHttpcloakSessionOptionsFromEnv());
|
|
526
|
+
session.headers.Origin = this.#baseUrl;
|
|
527
|
+
session.headers.Referer = `${this.#baseUrl}/`;
|
|
528
|
+
session.headers["Oai-Device-Id"] = this.#deviceId;
|
|
529
|
+
session.setCookie("__Secure-next-auth.session-token", this.#sessionToken);
|
|
530
|
+
this.#httpSession = session;
|
|
531
|
+
}
|
|
532
|
+
return this.#httpSession;
|
|
533
|
+
}
|
|
534
|
+
#headers(extra = {}) {
|
|
535
|
+
return {
|
|
536
|
+
Accept: "application/json",
|
|
537
|
+
"Content-Type": "application/json",
|
|
538
|
+
...extra,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
#requestOptions(extra = {}) {
|
|
542
|
+
return {
|
|
543
|
+
...extra,
|
|
544
|
+
cookies: {
|
|
545
|
+
"__Secure-next-auth.session-token": this.#sessionToken,
|
|
546
|
+
...(extra.cookies ?? {}),
|
|
547
|
+
},
|
|
548
|
+
headers: extra.headers ? { ...extra.headers } : undefined,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
async #get(url, options = {}) {
|
|
552
|
+
try {
|
|
553
|
+
return await this.#getHttpSession().get(url, this.#requestOptions(options));
|
|
554
|
+
}
|
|
555
|
+
catch (error) {
|
|
556
|
+
throw new Error(`httpcloak_get_failed: ${toErrorMessage(error)}`);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
async #post(url, options = {}) {
|
|
560
|
+
try {
|
|
561
|
+
return await this.#getHttpSession().post(url, this.#requestOptions(options));
|
|
562
|
+
}
|
|
563
|
+
catch (error) {
|
|
564
|
+
throw new Error(`httpcloak_post_failed: ${toErrorMessage(error)}`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
async #camofoxRequest(path, init = {}) {
|
|
568
|
+
const response = await fetch(`${this.#camofoxBaseUrl}${path}`, init);
|
|
569
|
+
const raw = await response.text();
|
|
570
|
+
const payload = safeJsonParse(raw);
|
|
571
|
+
if (!response.ok) {
|
|
572
|
+
throw new Error(`camofox_request_failed_${response.status}: ${summarizeErrorPayload(raw)}`);
|
|
573
|
+
}
|
|
574
|
+
if (payload === null) {
|
|
575
|
+
throw new Error(`camofox_invalid_json_response_for_${path}`);
|
|
576
|
+
}
|
|
577
|
+
return payload;
|
|
578
|
+
}
|
|
579
|
+
async #camofoxRequestBinary(path) {
|
|
580
|
+
const response = await fetch(`${this.#camofoxBaseUrl}${path}`);
|
|
581
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
582
|
+
if (!response.ok) {
|
|
583
|
+
const raw = buffer.toString("utf8");
|
|
584
|
+
throw new Error(`camofox_request_failed_${response.status}: ${summarizeErrorPayload(raw)}`);
|
|
585
|
+
}
|
|
586
|
+
return {
|
|
587
|
+
mimeType: response.headers.get("content-type") ?? "image/png",
|
|
588
|
+
base64: buffer.toString("base64"),
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
async #camofoxPost(path, body, headers = {}) {
|
|
592
|
+
return await this.#camofoxRequest(path, {
|
|
593
|
+
method: "POST",
|
|
594
|
+
headers: {
|
|
595
|
+
"content-type": "application/json",
|
|
596
|
+
...headers,
|
|
597
|
+
},
|
|
598
|
+
body: JSON.stringify(body),
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
async #camofoxDelete(path, body) {
|
|
602
|
+
await this.#camofoxRequest(path, {
|
|
603
|
+
method: "DELETE",
|
|
604
|
+
headers: {
|
|
605
|
+
"content-type": "application/json",
|
|
606
|
+
},
|
|
607
|
+
body: JSON.stringify(body),
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
async #camofoxCreateTab() {
|
|
611
|
+
const payload = await this.#camofoxPost("/tabs", {
|
|
612
|
+
userId: this.#camofoxUserId,
|
|
613
|
+
sessionKey: this.#camofoxSessionKey,
|
|
614
|
+
url: `${this.#baseUrl}/`,
|
|
615
|
+
});
|
|
616
|
+
const tabId = String(payload.tabId ?? "").trim();
|
|
617
|
+
if (!tabId) {
|
|
618
|
+
throw new Error("camofox_create_tab_failed_missing_tab_id");
|
|
619
|
+
}
|
|
620
|
+
return tabId;
|
|
621
|
+
}
|
|
622
|
+
async #camofoxDeleteTab(tabId) {
|
|
623
|
+
try {
|
|
624
|
+
await this.#camofoxDelete(`/tabs/${encodeURIComponent(tabId)}`, {
|
|
625
|
+
userId: this.#camofoxUserId,
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
catch {
|
|
629
|
+
// best-effort cleanup
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
async #camofoxImportSessionCookie() {
|
|
633
|
+
const expiry = Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30;
|
|
634
|
+
const headers = {};
|
|
635
|
+
if (this.#camofoxApiKey) {
|
|
636
|
+
headers.Authorization = `Bearer ${this.#camofoxApiKey}`;
|
|
637
|
+
}
|
|
638
|
+
try {
|
|
639
|
+
await this.#camofoxPost(`/sessions/${encodeURIComponent(this.#camofoxUserId)}/cookies`, {
|
|
640
|
+
cookies: [
|
|
641
|
+
{
|
|
642
|
+
name: "__Secure-next-auth.session-token",
|
|
643
|
+
value: this.#sessionToken,
|
|
644
|
+
domain: ".chatgpt.com",
|
|
645
|
+
path: "/",
|
|
646
|
+
expires: expiry,
|
|
647
|
+
httpOnly: true,
|
|
648
|
+
secure: true,
|
|
649
|
+
sameSite: "None",
|
|
650
|
+
},
|
|
651
|
+
],
|
|
652
|
+
}, headers);
|
|
653
|
+
}
|
|
654
|
+
catch (error) {
|
|
655
|
+
const message = toErrorMessage(error);
|
|
656
|
+
if (message.includes("camofox_request_failed_401") ||
|
|
657
|
+
message.includes("camofox_request_failed_403")) {
|
|
658
|
+
if (!this.#camofoxApiKey) {
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
throw error;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
async #camofoxNavigate(tabId, url) {
|
|
666
|
+
await this.#camofoxPost(`/tabs/${encodeURIComponent(tabId)}/navigate`, {
|
|
667
|
+
userId: this.#camofoxUserId,
|
|
668
|
+
url,
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
async #camofoxWait(tabId, timeoutMs = 5000, waitForNetwork = true) {
|
|
672
|
+
await this.#camofoxPost(`/tabs/${encodeURIComponent(tabId)}/wait`, {
|
|
673
|
+
userId: this.#camofoxUserId,
|
|
674
|
+
timeout: timeoutMs,
|
|
675
|
+
waitForNetwork,
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
async #camofoxTryWait(tabId, timeoutMs = 5000, waitForNetwork = true) {
|
|
679
|
+
try {
|
|
680
|
+
await this.#camofoxWait(tabId, timeoutMs, waitForNetwork);
|
|
681
|
+
}
|
|
682
|
+
catch {
|
|
683
|
+
// long-running generations can keep network activity alive for a long time
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
async #camofoxSnapshot(tabId) {
|
|
687
|
+
return await this.#camofoxRequest(`/tabs/${encodeURIComponent(tabId)}/snapshot?userId=${encodeURIComponent(this.#camofoxUserId)}`, {
|
|
688
|
+
method: "GET",
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
async #camofoxSnapshotText(tabId) {
|
|
692
|
+
const snapshot = await this.#camofoxSnapshot(tabId);
|
|
693
|
+
return String(snapshot.snapshot ?? "");
|
|
694
|
+
}
|
|
695
|
+
async #camofoxGetLinks(tabId) {
|
|
696
|
+
const payload = await this.#camofoxRequest(`/tabs/${encodeURIComponent(tabId)}/links?userId=${encodeURIComponent(this.#camofoxUserId)}`, {
|
|
697
|
+
method: "GET",
|
|
698
|
+
});
|
|
699
|
+
return Array.isArray(payload.links) ? payload.links : [];
|
|
700
|
+
}
|
|
701
|
+
async #camofoxGetVisitedUrls(tabId) {
|
|
702
|
+
const payload = await this.#camofoxRequest(`/tabs/${encodeURIComponent(tabId)}/stats?userId=${encodeURIComponent(this.#camofoxUserId)}`, {
|
|
703
|
+
method: "GET",
|
|
704
|
+
});
|
|
705
|
+
return extractVisitedUrlsFromStats(payload);
|
|
706
|
+
}
|
|
707
|
+
async #camofoxScreenshotDataUrl(tabId, fullPage = false) {
|
|
708
|
+
const payload = await this.#camofoxRequestBinary(`/tabs/${encodeURIComponent(tabId)}/screenshot?userId=${encodeURIComponent(this.#camofoxUserId)}&fullPage=${String(fullPage)}`);
|
|
709
|
+
return `data:${payload.mimeType};base64,${payload.base64}`;
|
|
710
|
+
}
|
|
711
|
+
async #camofoxClickRef(tabId, ref) {
|
|
712
|
+
await this.#camofoxPost(`/tabs/${encodeURIComponent(tabId)}/click`, {
|
|
713
|
+
userId: this.#camofoxUserId,
|
|
714
|
+
ref,
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
async #camofoxFindAndClick(tabId, role, label) {
|
|
718
|
+
const snapshotText = await this.#camofoxSnapshotText(tabId);
|
|
719
|
+
const ref = parseSnapshotForRef(snapshotText, role, label);
|
|
720
|
+
if (!ref) {
|
|
721
|
+
return false;
|
|
722
|
+
}
|
|
723
|
+
await this.#camofoxClickRef(tabId, ref);
|
|
724
|
+
return true;
|
|
725
|
+
}
|
|
726
|
+
async #camofoxFindAndClickAnyRole(tabId, roles, label) {
|
|
727
|
+
for (const role of roles) {
|
|
728
|
+
const clicked = await this.#camofoxFindAndClick(tabId, role, label);
|
|
729
|
+
if (clicked) {
|
|
730
|
+
return true;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
735
|
+
async #camofoxResolveWorkspace(tabId, workspace) {
|
|
736
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
737
|
+
const snapshotText = await this.#camofoxSnapshotText(tabId);
|
|
738
|
+
if (!/heading\s+"Select a workspace/i.test(snapshotText)) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
const escapedWorkspace = escapeRegExp(workspace);
|
|
742
|
+
const preferredRef = parseSnapshotForRef(snapshotText, "radio", new RegExp(`^${escapedWorkspace}$`, "i"));
|
|
743
|
+
const fallbackRef = parseSnapshotForRef(snapshotText, "radio", /.+/i);
|
|
744
|
+
const refToClick = preferredRef ?? fallbackRef;
|
|
745
|
+
if (!refToClick) {
|
|
746
|
+
throw new Error("camofox_workspace_selection_ref_not_found");
|
|
747
|
+
}
|
|
748
|
+
await this.#camofoxClickRef(tabId, refToClick);
|
|
749
|
+
await this.#camofoxTryWait(tabId, 10000);
|
|
750
|
+
}
|
|
751
|
+
const finalSnapshot = await this.#camofoxSnapshotText(tabId);
|
|
752
|
+
if (/heading\s+"Select a workspace/i.test(finalSnapshot)) {
|
|
753
|
+
throw new Error("camofox_workspace_selection_not_resolved");
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
async #camofoxDismissCookieDialogs(tabId) {
|
|
757
|
+
const cookieButtonMatchers = [/Accept all/i, /Reject non-essential/i, /^Close$/i, /Manage Cookies/i];
|
|
758
|
+
for (const matcher of cookieButtonMatchers) {
|
|
759
|
+
try {
|
|
760
|
+
const clicked = await this.#camofoxFindAndClick(tabId, "button", matcher);
|
|
761
|
+
if (clicked) {
|
|
762
|
+
await this.#camofoxWait(tabId, 2000);
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
catch {
|
|
767
|
+
// ignore and try next pattern
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
async #camofoxAssertAuthenticated(tabId) {
|
|
772
|
+
const snapshotText = await this.#camofoxSnapshotText(tabId);
|
|
773
|
+
if (snapshotIndicatesLoginRequired(snapshotText)) {
|
|
774
|
+
throw new Error("camofox_login_required");
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
async #camofoxOpenSidebar(tabId) {
|
|
778
|
+
const snapshotText = await this.#camofoxSnapshotText(tabId);
|
|
779
|
+
if (/button\s+"Close sidebar"/i.test(snapshotText)) {
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
const clicked = await this.#camofoxFindAndClick(tabId, "button", /Open sidebar/i);
|
|
783
|
+
if (clicked) {
|
|
784
|
+
await this.#camofoxWait(tabId, 3000);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
async #camofoxOpenModelMenu(tabId) {
|
|
788
|
+
const snapshotText = await this.#camofoxSnapshotText(tabId);
|
|
789
|
+
const modelRef = parseSnapshotForRef(snapshotText, "button", /Model selector/i) ??
|
|
790
|
+
parseSnapshotForRef(snapshotText, "button", /^(Auto|Instant|Thinking|Pro)$/i) ??
|
|
791
|
+
parseSnapshotForRef(snapshotText, "button", /GPT-5\.[12]/i);
|
|
792
|
+
if (!modelRef) {
|
|
793
|
+
throw new Error("camofox_model_selector_not_found");
|
|
794
|
+
}
|
|
795
|
+
await this.#camofoxClickRef(tabId, modelRef);
|
|
796
|
+
await this.#camofoxTryWait(tabId, 4000);
|
|
797
|
+
return await this.#camofoxSnapshotText(tabId);
|
|
798
|
+
}
|
|
799
|
+
async #camofoxSelectModel(tabId, modelSlug) {
|
|
800
|
+
const matchers = modelSlugToLabelMatchers(modelSlug);
|
|
801
|
+
const submenus = modelSlugToSubmenuMatchers(modelSlug);
|
|
802
|
+
const findAndSelectFromSnapshot = async (snapshotText) => {
|
|
803
|
+
const menuItems = parseCamofoxMenuItems(snapshotText);
|
|
804
|
+
if (menuItems.length === 0) {
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
807
|
+
const matched = menuItems.find((item) => matchers.some((matcher) => matcher.test(item.label)));
|
|
808
|
+
if (!matched) {
|
|
809
|
+
return false;
|
|
810
|
+
}
|
|
811
|
+
await this.#camofoxClickRef(tabId, matched.ref);
|
|
812
|
+
await this.#camofoxTryWait(tabId, 8000);
|
|
813
|
+
return true;
|
|
814
|
+
};
|
|
815
|
+
const initialSnapshotText = await this.#camofoxOpenModelMenu(tabId);
|
|
816
|
+
if (await findAndSelectFromSnapshot(initialSnapshotText)) {
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
for (const submenuMatcher of submenus) {
|
|
820
|
+
const opened = await this.#camofoxFindAndClickAnyRole(tabId, ["button", "menuitem"], submenuMatcher);
|
|
821
|
+
if (!opened) {
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
await this.#camofoxTryWait(tabId, 3500);
|
|
825
|
+
const submenuSnapshot = await this.#camofoxSnapshotText(tabId);
|
|
826
|
+
if (await findAndSelectFromSnapshot(submenuSnapshot)) {
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
const fallbackSnapshot = await this.#camofoxSnapshotText(tabId);
|
|
831
|
+
const available = parseCamofoxMenuItems(fallbackSnapshot)
|
|
832
|
+
.map((item) => item.label)
|
|
833
|
+
.join(", ");
|
|
834
|
+
throw new Error(`camofox_model_not_found_for_slug_${modelSlug}; available: ${available || "none"}`);
|
|
835
|
+
}
|
|
836
|
+
async #camofoxEnableDeepResearch(tabId, siteMode) {
|
|
837
|
+
await this.#camofoxOpenSidebar(tabId);
|
|
838
|
+
let clicked = await this.#camofoxFindAndClick(tabId, "link", /Deep research/i);
|
|
839
|
+
if (!clicked) {
|
|
840
|
+
clicked = await this.#camofoxFindAndClickAnyRole(tabId, ["button", "menuitem"], /^Deep research$/i);
|
|
841
|
+
}
|
|
842
|
+
if (!clicked) {
|
|
843
|
+
throw new Error("camofox_deep_research_entry_not_found");
|
|
844
|
+
}
|
|
845
|
+
await this.#camofoxTryWait(tabId, 15000);
|
|
846
|
+
if (!siteMode) {
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
const openSites = await this.#camofoxFindAndClick(tabId, "button", /^Sites,/i);
|
|
850
|
+
if (!openSites) {
|
|
851
|
+
throw new Error("camofox_deep_research_sites_button_not_found");
|
|
852
|
+
}
|
|
853
|
+
await this.#camofoxTryWait(tabId, 4000);
|
|
854
|
+
const targetLabel = siteMode === "specific_sites" ? /Specific sites/i : /Search the web/i;
|
|
855
|
+
const selected = await this.#camofoxFindAndClick(tabId, "menuitem", targetLabel);
|
|
856
|
+
if (!selected) {
|
|
857
|
+
throw new Error(`camofox_deep_research_sites_option_not_found_${siteMode}`);
|
|
858
|
+
}
|
|
859
|
+
await this.#camofoxTryWait(tabId, 3000);
|
|
860
|
+
}
|
|
861
|
+
async #camofoxEnableCreateImage(tabId) {
|
|
862
|
+
const snapshotText = await this.#camofoxSnapshotText(tabId);
|
|
863
|
+
if (parseSnapshotForRef(snapshotText, "button", /Create images?, click to remove/i)) {
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
const enabled = await this.#camofoxFindAndClickAnyRole(tabId, ["button", "menuitem"], /^Create images?$/i);
|
|
867
|
+
if (!enabled) {
|
|
868
|
+
throw new Error("camofox_create_image_control_not_found");
|
|
869
|
+
}
|
|
870
|
+
await this.#camofoxTryWait(tabId, 3000);
|
|
871
|
+
}
|
|
872
|
+
async #camofoxSetReasoningEffort(tabId, effort) {
|
|
873
|
+
if (!effort) {
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
const snapshotText = await this.#camofoxSnapshotText(tabId);
|
|
877
|
+
const selectedExtendedRef = parseSnapshotForRef(snapshotText, "button", /Extended thinking, click to remove/i);
|
|
878
|
+
const selectedThinkingRef = parseSnapshotForRef(snapshotText, "button", /Thinking, click to remove/i);
|
|
879
|
+
if (effort === "none") {
|
|
880
|
+
const selectedRef = selectedExtendedRef ?? selectedThinkingRef;
|
|
881
|
+
if (selectedRef) {
|
|
882
|
+
await this.#camofoxClickRef(tabId, selectedRef);
|
|
883
|
+
await this.#camofoxWait(tabId, 3000);
|
|
884
|
+
}
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
if (effort === "standard") {
|
|
888
|
+
if (selectedExtendedRef) {
|
|
889
|
+
await this.#camofoxClickRef(tabId, selectedExtendedRef);
|
|
890
|
+
await this.#camofoxWait(tabId, 2500);
|
|
891
|
+
}
|
|
892
|
+
const snapshotAfterRemove = await this.#camofoxSnapshotText(tabId);
|
|
893
|
+
if (parseSnapshotForRef(snapshotAfterRemove, "button", /Thinking, click to remove/i)) {
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
const enabledThinking = await this.#camofoxFindAndClick(tabId, "button", /^Thinking$/i);
|
|
897
|
+
if (enabledThinking) {
|
|
898
|
+
await this.#camofoxWait(tabId, 3000);
|
|
899
|
+
}
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
if (effort === "extended") {
|
|
903
|
+
if (selectedExtendedRef) {
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
if (selectedThinkingRef) {
|
|
907
|
+
await this.#camofoxClickRef(tabId, selectedThinkingRef);
|
|
908
|
+
await this.#camofoxWait(tabId, 2500);
|
|
909
|
+
}
|
|
910
|
+
const enabled = await this.#camofoxFindAndClick(tabId, "button", /^Extended thinking$/i);
|
|
911
|
+
if (enabled) {
|
|
912
|
+
await this.#camofoxWait(tabId, 3000);
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
const altEnabled = await this.#camofoxFindAndClickAnyRole(tabId, ["button", "menuitem"], /Extended/i);
|
|
916
|
+
if (altEnabled) {
|
|
917
|
+
await this.#camofoxTryWait(tabId, 3000);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
async #camofoxType(tabId, selector, text) {
|
|
922
|
+
try {
|
|
923
|
+
await this.#camofoxPost(`/tabs/${encodeURIComponent(tabId)}/type`, {
|
|
924
|
+
userId: this.#camofoxUserId,
|
|
925
|
+
selector,
|
|
926
|
+
text,
|
|
927
|
+
});
|
|
928
|
+
return true;
|
|
929
|
+
}
|
|
930
|
+
catch {
|
|
931
|
+
return false;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
async #camofoxTypeWithFallback(tabId, prompt) {
|
|
935
|
+
const selectors = ["#prompt-textarea", "div#prompt-textarea", "textarea[name='prompt-textarea']", "textarea"];
|
|
936
|
+
for (const selector of selectors) {
|
|
937
|
+
const ok = await this.#camofoxType(tabId, selector, prompt);
|
|
938
|
+
if (ok) {
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
const snapshot = await this.#camofoxSnapshot(tabId);
|
|
943
|
+
const snapshotText = String(snapshot.snapshot ?? "");
|
|
944
|
+
const textboxRef = parseSnapshotForRef(snapshotText, "textbox", /.*/i);
|
|
945
|
+
if (!textboxRef) {
|
|
946
|
+
throw new Error("camofox_prompt_input_not_found");
|
|
947
|
+
}
|
|
948
|
+
await this.#camofoxPost(`/tabs/${encodeURIComponent(tabId)}/type`, {
|
|
949
|
+
userId: this.#camofoxUserId,
|
|
950
|
+
ref: textboxRef,
|
|
951
|
+
text: prompt,
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
async #camofoxSubmitPrompt(tabId) {
|
|
955
|
+
try {
|
|
956
|
+
await this.#camofoxPost(`/tabs/${encodeURIComponent(tabId)}/press`, {
|
|
957
|
+
userId: this.#camofoxUserId,
|
|
958
|
+
key: "Enter",
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
catch {
|
|
962
|
+
// keep going; click fallback below
|
|
963
|
+
}
|
|
964
|
+
await sleep(600);
|
|
965
|
+
const snapshot = await this.#camofoxSnapshot(tabId);
|
|
966
|
+
const snapshotText = String(snapshot.snapshot ?? "");
|
|
967
|
+
if (snapshotIndicatesGenerationInProgress(snapshotText)) {
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
const sendRef = parseSnapshotForRef(snapshotText, "button", /send prompt/i) ??
|
|
971
|
+
parseSnapshotForRef(snapshotText, "button", /send message/i) ??
|
|
972
|
+
parseSnapshotForRef(snapshotText, "button", /get a detailed report/i) ??
|
|
973
|
+
parseSnapshotForRef(snapshotText, "button", /detailed report/i);
|
|
974
|
+
if (!sendRef) {
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
try {
|
|
978
|
+
await this.#camofoxPost(`/tabs/${encodeURIComponent(tabId)}/click`, {
|
|
979
|
+
userId: this.#camofoxUserId,
|
|
980
|
+
ref: sendRef,
|
|
981
|
+
});
|
|
982
|
+
await this.#camofoxTryWait(tabId, 2000, false);
|
|
983
|
+
}
|
|
984
|
+
catch {
|
|
985
|
+
// ignore click fallback failure
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
async #camofoxClickContinueGeneratingIfPresent(tabId, snapshotText) {
|
|
989
|
+
const continueRef = parseSnapshotForRef(snapshotText, "button", /Continue generating/i);
|
|
990
|
+
if (!continueRef) {
|
|
991
|
+
return false;
|
|
992
|
+
}
|
|
993
|
+
await this.#camofoxClickRef(tabId, continueRef);
|
|
994
|
+
await this.#camofoxTryWait(tabId, 4000, false);
|
|
995
|
+
return true;
|
|
996
|
+
}
|
|
997
|
+
async #camofoxPollAssistantText(tabId, prompt, timeoutMs, options = {}) {
|
|
998
|
+
const allowEmptyResult = options.allowEmptyResult === true;
|
|
999
|
+
const startedAt = Date.now();
|
|
1000
|
+
let lastCandidate = "";
|
|
1001
|
+
let lastCandidateAt = 0;
|
|
1002
|
+
let lastConversationId = null;
|
|
1003
|
+
let idleTicks = 0;
|
|
1004
|
+
let seenGenerationIndicator = false;
|
|
1005
|
+
const minimumSettleMs = 6000;
|
|
1006
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
1007
|
+
await this.#camofoxTryWait(tabId, 5000, false);
|
|
1008
|
+
const snapshot = await this.#camofoxSnapshot(tabId);
|
|
1009
|
+
const snapshotText = String(snapshot.snapshot ?? "");
|
|
1010
|
+
const stillGenerating = snapshotIndicatesGenerationInProgress(snapshotText);
|
|
1011
|
+
if (stillGenerating) {
|
|
1012
|
+
seenGenerationIndicator = true;
|
|
1013
|
+
}
|
|
1014
|
+
if (await this.#camofoxClickContinueGeneratingIfPresent(tabId, snapshotText)) {
|
|
1015
|
+
idleTicks = 0;
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
const fatalUiError = snapshotFatalUiError(snapshotText);
|
|
1019
|
+
if (fatalUiError && !lastCandidate) {
|
|
1020
|
+
throw new Error(`camofox_ui_error_${fatalUiError}`);
|
|
1021
|
+
}
|
|
1022
|
+
if (snapshotIndicatesLoginRequired(snapshotText) && !lastCandidate) {
|
|
1023
|
+
throw new Error("camofox_login_required");
|
|
1024
|
+
}
|
|
1025
|
+
const candidate = extractLikelyAssistantTextFromSnapshot(snapshotText, prompt);
|
|
1026
|
+
const conversationId = parseConversationIdFromUrl(snapshot.url);
|
|
1027
|
+
if (conversationId) {
|
|
1028
|
+
lastConversationId = conversationId;
|
|
1029
|
+
}
|
|
1030
|
+
const canTrustCandidate = seenGenerationIndicator || Boolean(lastConversationId);
|
|
1031
|
+
if (canTrustCandidate && candidate && candidate !== lastCandidate) {
|
|
1032
|
+
lastCandidate = candidate;
|
|
1033
|
+
lastCandidateAt = Date.now();
|
|
1034
|
+
idleTicks = 0;
|
|
1035
|
+
}
|
|
1036
|
+
else if (!stillGenerating) {
|
|
1037
|
+
idleTicks += 1;
|
|
1038
|
+
}
|
|
1039
|
+
else {
|
|
1040
|
+
idleTicks = 0;
|
|
1041
|
+
}
|
|
1042
|
+
if (lastCandidate &&
|
|
1043
|
+
!stillGenerating &&
|
|
1044
|
+
Date.now() - lastCandidateAt >= minimumSettleMs &&
|
|
1045
|
+
(snapshotIndicatesReadyForNextPrompt(snapshotText) || idleTicks >= 3)) {
|
|
1046
|
+
return { text: lastCandidate, conversationId: lastConversationId };
|
|
1047
|
+
}
|
|
1048
|
+
if (allowEmptyResult &&
|
|
1049
|
+
canTrustCandidate &&
|
|
1050
|
+
!stillGenerating &&
|
|
1051
|
+
(snapshotIndicatesReadyForNextPrompt(snapshotText) || idleTicks >= 3)) {
|
|
1052
|
+
return { text: "", conversationId: lastConversationId };
|
|
1053
|
+
}
|
|
1054
|
+
await sleep(1500);
|
|
1055
|
+
}
|
|
1056
|
+
if (lastCandidate) {
|
|
1057
|
+
return { text: lastCandidate, conversationId: lastConversationId };
|
|
1058
|
+
}
|
|
1059
|
+
if (allowEmptyResult && lastConversationId) {
|
|
1060
|
+
return { text: "", conversationId: lastConversationId };
|
|
1061
|
+
}
|
|
1062
|
+
throw new Error("camofox_assistant_response_timeout");
|
|
1063
|
+
}
|
|
1064
|
+
#conversationUrl(conversationId) {
|
|
1065
|
+
const id = String(conversationId ?? "").trim();
|
|
1066
|
+
if (!id) {
|
|
1067
|
+
return `${this.#baseUrl}/`;
|
|
1068
|
+
}
|
|
1069
|
+
return `${this.#baseUrl}/c/${encodeURIComponent(id)}`;
|
|
1070
|
+
}
|
|
1071
|
+
async #askViaCamofox(input) {
|
|
1072
|
+
let tabId = null;
|
|
1073
|
+
const workspace = String(input.workspace ?? this.#camofoxWorkspace).trim() || this.#camofoxWorkspace;
|
|
1074
|
+
const effectiveTimeout = typeof input.waitTimeoutMs === "number" && input.waitTimeoutMs > 0
|
|
1075
|
+
? Math.floor(input.waitTimeoutMs)
|
|
1076
|
+
: this.#camofoxWaitTimeoutMs;
|
|
1077
|
+
const explicitModel = input.model?.trim() || null;
|
|
1078
|
+
const modeModel = modeToDefaultModelSlug(input.modelMode);
|
|
1079
|
+
const effectiveModelSlug = explicitModel ?? modeModel;
|
|
1080
|
+
const wantsImageGeneration = input.createImage === true;
|
|
1081
|
+
const shouldUseDeepResearch = input.deepResearch === true ||
|
|
1082
|
+
input.deepResearchSiteMode !== undefined ||
|
|
1083
|
+
effectiveModelSlug === "research";
|
|
1084
|
+
if (wantsImageGeneration && shouldUseDeepResearch) {
|
|
1085
|
+
throw new Error("camofox_invalid_mode_combination_create_image_and_deep_research");
|
|
1086
|
+
}
|
|
1087
|
+
try {
|
|
1088
|
+
tabId = await this.#camofoxCreateTab();
|
|
1089
|
+
await this.#camofoxImportSessionCookie();
|
|
1090
|
+
await this.#camofoxNavigate(tabId, this.#conversationUrl(input.conversationId));
|
|
1091
|
+
await this.#camofoxTryWait(tabId, 8000);
|
|
1092
|
+
await this.#camofoxResolveWorkspace(tabId, workspace);
|
|
1093
|
+
await this.#camofoxDismissCookieDialogs(tabId);
|
|
1094
|
+
await this.#camofoxAssertAuthenticated(tabId);
|
|
1095
|
+
if (shouldUseDeepResearch) {
|
|
1096
|
+
await this.#camofoxEnableDeepResearch(tabId, input.deepResearchSiteMode);
|
|
1097
|
+
}
|
|
1098
|
+
if (wantsImageGeneration) {
|
|
1099
|
+
await this.#camofoxEnableCreateImage(tabId);
|
|
1100
|
+
}
|
|
1101
|
+
if (effectiveModelSlug && effectiveModelSlug !== "research") {
|
|
1102
|
+
await this.#camofoxSelectModel(tabId, effectiveModelSlug);
|
|
1103
|
+
}
|
|
1104
|
+
if (input.reasoningEffort) {
|
|
1105
|
+
await this.#camofoxSetReasoningEffort(tabId, input.reasoningEffort);
|
|
1106
|
+
}
|
|
1107
|
+
await this.#camofoxTypeWithFallback(tabId, input.prompt);
|
|
1108
|
+
await this.#camofoxSubmitPrompt(tabId);
|
|
1109
|
+
const polled = await this.#camofoxPollAssistantText(tabId, input.prompt, effectiveTimeout, {
|
|
1110
|
+
allowEmptyResult: wantsImageGeneration,
|
|
1111
|
+
});
|
|
1112
|
+
let imageUrls;
|
|
1113
|
+
if (wantsImageGeneration) {
|
|
1114
|
+
await this.#camofoxTryWait(tabId, 8000, false);
|
|
1115
|
+
const links = await this.#camofoxGetLinks(tabId);
|
|
1116
|
+
const visitedUrls = await this.#camofoxGetVisitedUrls(tabId);
|
|
1117
|
+
const postSnapshot = await this.#camofoxSnapshotText(tabId);
|
|
1118
|
+
const snapshotUrls = extractUrlsFromSnapshot(postSnapshot).map((url) => ({ url }));
|
|
1119
|
+
const visitedUrlLinks = visitedUrls.map((url) => ({ url }));
|
|
1120
|
+
imageUrls = extractImageUrlsFromLinks([...links, ...snapshotUrls, ...visitedUrlLinks]);
|
|
1121
|
+
if (imageUrls.length === 0 && this.#imageScreenshotFallback) {
|
|
1122
|
+
try {
|
|
1123
|
+
imageUrls = [await this.#camofoxScreenshotDataUrl(tabId, false)];
|
|
1124
|
+
}
|
|
1125
|
+
catch {
|
|
1126
|
+
// best-effort fallback
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
return {
|
|
1131
|
+
text: polled.text,
|
|
1132
|
+
conversationId: polled.conversationId ?? input.conversationId ?? null,
|
|
1133
|
+
parentMessageId: null,
|
|
1134
|
+
model: effectiveModelSlug ?? DEFAULT_MODEL,
|
|
1135
|
+
imageUrls,
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
finally {
|
|
1139
|
+
if (tabId) {
|
|
1140
|
+
await this.#camofoxDeleteTab(tabId);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
async getSession() {
|
|
1145
|
+
const response = await this.#get(`${this.#baseUrl}/api/auth/session`, {
|
|
1146
|
+
headers: this.#headers(),
|
|
1147
|
+
});
|
|
1148
|
+
const raw = response.text;
|
|
1149
|
+
const payload = safeJsonParse(raw) ?? {};
|
|
1150
|
+
if (!response.ok || !payload.accessToken) {
|
|
1151
|
+
throw new Error(`failed_to_get_access_token_from_session_cookie_${response.statusCode}: ${summarizeErrorPayload(raw)}`);
|
|
1152
|
+
}
|
|
1153
|
+
return payload;
|
|
1154
|
+
}
|
|
1155
|
+
async getModels() {
|
|
1156
|
+
const session = await this.getSession();
|
|
1157
|
+
const response = await this.#get(`${this.#baseUrl}/backend-api/models`, {
|
|
1158
|
+
headers: this.#headers({ Authorization: `Bearer ${session.accessToken ?? ""}` }),
|
|
1159
|
+
});
|
|
1160
|
+
const raw = response.text;
|
|
1161
|
+
const payload = safeJsonParse(raw) ?? {};
|
|
1162
|
+
if (!response.ok) {
|
|
1163
|
+
throw new Error(`models_request_failed_${response.statusCode}: ${summarizeErrorPayload(raw)}`);
|
|
1164
|
+
}
|
|
1165
|
+
return payload;
|
|
1166
|
+
}
|
|
1167
|
+
async #getChatRequirementsToken(accessToken) {
|
|
1168
|
+
try {
|
|
1169
|
+
const response = await this.#post(`${this.#baseUrl}${CHAT_REQUIREMENTS_PATH}`, {
|
|
1170
|
+
headers: this.#headers({ Authorization: `Bearer ${accessToken}` }),
|
|
1171
|
+
json: {},
|
|
1172
|
+
});
|
|
1173
|
+
if (!response.ok) {
|
|
1174
|
+
return null;
|
|
1175
|
+
}
|
|
1176
|
+
const payload = safeJsonParse(response.text) ?? {};
|
|
1177
|
+
return payload.token ?? null;
|
|
1178
|
+
}
|
|
1179
|
+
catch {
|
|
1180
|
+
return null;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
async ask(input) {
|
|
1184
|
+
const prompt = input.prompt.trim();
|
|
1185
|
+
if (!prompt) {
|
|
1186
|
+
throw new Error("missing_prompt");
|
|
1187
|
+
}
|
|
1188
|
+
const deepResearchRequested = input.deepResearch === true || input.deepResearchSiteMode !== undefined || input.model?.trim() === "research";
|
|
1189
|
+
if (input.createImage && deepResearchRequested) {
|
|
1190
|
+
throw new Error("invalid_mode_combination_create_image_and_deep_research");
|
|
1191
|
+
}
|
|
1192
|
+
const requestedModel = deepResearchRequested
|
|
1193
|
+
? "research"
|
|
1194
|
+
: input.model?.trim() || modeToDefaultModelSlug(input.modelMode) || undefined;
|
|
1195
|
+
if (this.#transport === "camofox") {
|
|
1196
|
+
return await this.#askViaCamofox({
|
|
1197
|
+
...input,
|
|
1198
|
+
prompt,
|
|
1199
|
+
model: requestedModel,
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
if (input.createImage) {
|
|
1203
|
+
throw new Error("httpcloak_create_image_not_supported_use_camofox");
|
|
1204
|
+
}
|
|
1205
|
+
const session = await this.getSession();
|
|
1206
|
+
const accessToken = session.accessToken;
|
|
1207
|
+
const requirementsToken = await this.#getChatRequirementsToken(accessToken);
|
|
1208
|
+
const parentMessageId = input.parentMessageId ?? crypto.randomUUID();
|
|
1209
|
+
const userMessageId = crypto.randomUUID();
|
|
1210
|
+
const body = {
|
|
1211
|
+
action: "next",
|
|
1212
|
+
messages: [
|
|
1213
|
+
{
|
|
1214
|
+
id: userMessageId,
|
|
1215
|
+
author: { role: "user" },
|
|
1216
|
+
content: {
|
|
1217
|
+
content_type: "text",
|
|
1218
|
+
parts: [prompt],
|
|
1219
|
+
},
|
|
1220
|
+
},
|
|
1221
|
+
],
|
|
1222
|
+
parent_message_id: parentMessageId,
|
|
1223
|
+
conversation_id: input.conversationId ?? undefined,
|
|
1224
|
+
model: requestedModel ?? DEFAULT_MODEL,
|
|
1225
|
+
history_and_training_disabled: false,
|
|
1226
|
+
timezone_offset_min: new Date().getTimezoneOffset() * -1,
|
|
1227
|
+
suggestions: [],
|
|
1228
|
+
websocket_request_id: crypto.randomUUID(),
|
|
1229
|
+
conversation_mode: { kind: "primary_assistant" },
|
|
1230
|
+
};
|
|
1231
|
+
const headers = this.#headers({
|
|
1232
|
+
Accept: "text/event-stream",
|
|
1233
|
+
Authorization: `Bearer ${accessToken}`,
|
|
1234
|
+
});
|
|
1235
|
+
if (requirementsToken) {
|
|
1236
|
+
headers["OpenAI-Sentinel-Chat-Requirements-Token"] = requirementsToken;
|
|
1237
|
+
}
|
|
1238
|
+
const response = await this.#post(`${this.#baseUrl}/backend-api/conversation`, {
|
|
1239
|
+
headers,
|
|
1240
|
+
json: body,
|
|
1241
|
+
});
|
|
1242
|
+
const raw = response.text;
|
|
1243
|
+
if (!response.ok) {
|
|
1244
|
+
throw new Error(`conversation_request_failed_${response.statusCode}: ${summarizeErrorPayload(raw)}`);
|
|
1245
|
+
}
|
|
1246
|
+
const events = parseSseEvents(raw);
|
|
1247
|
+
if (events.length === 0) {
|
|
1248
|
+
throw new Error("empty_conversation_stream");
|
|
1249
|
+
}
|
|
1250
|
+
const streamError = events.find((event) => event.error)?.error;
|
|
1251
|
+
if (streamError) {
|
|
1252
|
+
throw new Error(`conversation_stream_error: ${streamError}`);
|
|
1253
|
+
}
|
|
1254
|
+
let text = "";
|
|
1255
|
+
let conversationId = null;
|
|
1256
|
+
let outputParentMessageId = null;
|
|
1257
|
+
let model = null;
|
|
1258
|
+
for (const event of events) {
|
|
1259
|
+
const candidate = extractAssistantText(event);
|
|
1260
|
+
if (candidate) {
|
|
1261
|
+
text = candidate;
|
|
1262
|
+
}
|
|
1263
|
+
if (event.conversation_id) {
|
|
1264
|
+
conversationId = event.conversation_id;
|
|
1265
|
+
}
|
|
1266
|
+
if (event.message?.id) {
|
|
1267
|
+
outputParentMessageId = event.message.id;
|
|
1268
|
+
}
|
|
1269
|
+
if (event.message?.metadata?.model_slug) {
|
|
1270
|
+
model = event.message.metadata.model_slug;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
if (!text) {
|
|
1274
|
+
throw new Error("assistant_response_not_found_in_stream");
|
|
1275
|
+
}
|
|
1276
|
+
return {
|
|
1277
|
+
text,
|
|
1278
|
+
conversationId,
|
|
1279
|
+
parentMessageId: outputParentMessageId,
|
|
1280
|
+
model,
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
//# sourceMappingURL=chatgpt-webui-client.js.map
|