@love-moon/chat-web 0.3.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/CHANGELOG.md +8 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +142 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/doctor.d.ts +27 -0
- package/dist/commands/doctor.js +116 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/index.d.ts +3 -0
- package/dist/commands/index.js +4 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/info.d.ts +32 -0
- package/dist/commands/info.js +81 -0
- package/dist/commands/info.js.map +1 -0
- package/dist/commands/login.d.ts +19 -0
- package/dist/commands/login.js +61 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/core/browser.d.ts +70 -0
- package/dist/core/browser.js +96 -0
- package/dist/core/browser.js.map +1 -0
- package/dist/core/errors.d.ts +60 -0
- package/dist/core/errors.js +153 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/install-chromium.d.ts +55 -0
- package/dist/core/install-chromium.js +156 -0
- package/dist/core/install-chromium.js.map +1 -0
- package/dist/core/keyboard.d.ts +39 -0
- package/dist/core/keyboard.js +54 -0
- package/dist/core/keyboard.js.map +1 -0
- package/dist/core/locator-score.d.ts +41 -0
- package/dist/core/locator-score.js +101 -0
- package/dist/core/locator-score.js.map +1 -0
- package/dist/core/logger.d.ts +10 -0
- package/dist/core/logger.js +38 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/navigate.d.ts +52 -0
- package/dist/core/navigate.js +102 -0
- package/dist/core/navigate.js.map +1 -0
- package/dist/core/paths.d.ts +12 -0
- package/dist/core/paths.js +30 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/core/profile-manager.d.ts +13 -0
- package/dist/core/profile-manager.js +44 -0
- package/dist/core/profile-manager.js.map +1 -0
- package/dist/core/provider.d.ts +64 -0
- package/dist/core/provider.js +31 -0
- package/dist/core/provider.js.map +1 -0
- package/dist/core/response-watcher.d.ts +35 -0
- package/dist/core/response-watcher.js +70 -0
- package/dist/core/response-watcher.js.map +1 -0
- package/dist/core/snapshot.d.ts +38 -0
- package/dist/core/snapshot.js +137 -0
- package/dist/core/snapshot.js.map +1 -0
- package/dist/core/sse-parser.d.ts +20 -0
- package/dist/core/sse-parser.js +49 -0
- package/dist/core/sse-parser.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/chatgpt-sse-collector.d.ts +76 -0
- package/dist/providers/chatgpt-sse-collector.js +298 -0
- package/dist/providers/chatgpt-sse-collector.js.map +1 -0
- package/dist/providers/chatgpt.d.ts +56 -0
- package/dist/providers/chatgpt.js +357 -0
- package/dist/providers/chatgpt.js.map +1 -0
- package/dist/providers/deepseek.d.ts +22 -0
- package/dist/providers/deepseek.js +153 -0
- package/dist/providers/deepseek.js.map +1 -0
- package/dist/providers/gemini.d.ts +102 -0
- package/dist/providers/gemini.js +480 -0
- package/dist/providers/gemini.js.map +1 -0
- package/dist/providers/index.d.ts +8 -0
- package/dist/providers/index.js +17 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/session.d.ts +121 -0
- package/dist/session.js +242 -0
- package/dist/session.js.map +1 -0
- package/package.json +47 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { parseSSE } from "../core/sse-parser.js";
|
|
2
|
+
/**
|
|
3
|
+
* URL pattern for ChatGPT's streaming conversation endpoint. The exact
|
|
4
|
+
* path has shifted historically (`/backend-api/conversation`,
|
|
5
|
+
* `/backend-api/f/conversation`, `/backend-api/lat/r/conversation`, ...);
|
|
6
|
+
* `conversation` has stayed as the final path segment in every form.
|
|
7
|
+
*
|
|
8
|
+
* NOTE: must NOT match the sibling JSON endpoints `/conversation/init`,
|
|
9
|
+
* `/conversation/prepare`, `/conversation/textdocs/...` — those land
|
|
10
|
+
* before the real SSE response and would otherwise be captured first.
|
|
11
|
+
*/
|
|
12
|
+
export const CHATGPT_CONVERSATION_URL = /\/backend-api\/(?:[^/]+\/)*conversation(?:\?|$)/;
|
|
13
|
+
/**
|
|
14
|
+
* Listens to ChatGPT's streaming conversation responses and reconstructs
|
|
15
|
+
* the assistant's original markdown (code fences, list prefixes, table
|
|
16
|
+
* pipes, link URLs — everything that `innerText` would have stripped).
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* const collector = new ChatGPTSSECollector();
|
|
20
|
+
* collector.attach(page);
|
|
21
|
+
* …
|
|
22
|
+
* const wait = collector.beginTurn();
|
|
23
|
+
* await adapter.sendMessage(page, "...");
|
|
24
|
+
* const markdown = await wait; // resolves to the assistant's raw markdown
|
|
25
|
+
*
|
|
26
|
+
* Robust to multiple SSE formats observed in the wild:
|
|
27
|
+
* - Full message snapshots: `{message: {author, content: {parts}, status}, ...}`
|
|
28
|
+
* - v1 delta encoding: `{p, o: "append", v}` followed by `{v}` shorthand frames
|
|
29
|
+
* - Terminators: `[DONE]` or `{"type": "message_stream_complete"}`
|
|
30
|
+
*/
|
|
31
|
+
export class ChatGPTSSECollector {
|
|
32
|
+
detach;
|
|
33
|
+
pendingTurn;
|
|
34
|
+
currentTurn = freshTurn();
|
|
35
|
+
/** Attach to a Playwright Page. Idempotent. */
|
|
36
|
+
attach(page) {
|
|
37
|
+
if (this.detach)
|
|
38
|
+
return;
|
|
39
|
+
const handler = (response) => {
|
|
40
|
+
void this.maybeConsume(response);
|
|
41
|
+
};
|
|
42
|
+
page.on("response", handler);
|
|
43
|
+
this.detach = () => page.off("response", handler);
|
|
44
|
+
}
|
|
45
|
+
dispose() {
|
|
46
|
+
this.detach?.();
|
|
47
|
+
this.detach = undefined;
|
|
48
|
+
if (this.pendingTurn?.timer)
|
|
49
|
+
clearTimeout(this.pendingTurn.timer);
|
|
50
|
+
this.pendingTurn = undefined;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Start watching for the next conversation response. Returns a promise
|
|
54
|
+
* that resolves to the accumulated markdown once that response finishes
|
|
55
|
+
* streaming (or rejects on the supplied timeout).
|
|
56
|
+
*/
|
|
57
|
+
beginTurn(options = {}) {
|
|
58
|
+
// Reject any prior unresolved turn first — we only track one in flight.
|
|
59
|
+
if (this.pendingTurn) {
|
|
60
|
+
this.pendingTurn.reject(new Error("Superseded by a new turn"));
|
|
61
|
+
if (this.pendingTurn.timer)
|
|
62
|
+
clearTimeout(this.pendingTurn.timer);
|
|
63
|
+
}
|
|
64
|
+
this.currentTurn = freshTurn();
|
|
65
|
+
const timeoutMs = options.timeoutMs ?? 180_000;
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
const timer = setTimeout(() => {
|
|
68
|
+
if (this.pendingTurn) {
|
|
69
|
+
const partial = this.bestAssistantText();
|
|
70
|
+
this.pendingTurn = undefined;
|
|
71
|
+
// Resolve with whatever we got rather than rejecting — callers
|
|
72
|
+
// can decide to fall back. Empty string means "no SSE captured".
|
|
73
|
+
resolve(partial);
|
|
74
|
+
}
|
|
75
|
+
}, timeoutMs);
|
|
76
|
+
this.pendingTurn = { resolve, reject, captured: false, timer };
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Assistant text accumulated for the CURRENT turn only.
|
|
81
|
+
*
|
|
82
|
+
* Returns "" until SSE events for the current turn arrive. NEVER returns
|
|
83
|
+
* text from a previous turn — that would leak stale answers into the
|
|
84
|
+
* next prompt's extraction fallback. Bug fixed in this method: an
|
|
85
|
+
* earlier implementation also returned the latest *finalized* turn's
|
|
86
|
+
* text, which made the DOM-fallback path silently substitute the prior
|
|
87
|
+
* turn's reply when the current turn's SSE was empty / racy.
|
|
88
|
+
*/
|
|
89
|
+
getCurrentTurnText() {
|
|
90
|
+
return this.bestAssistantText();
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* @deprecated Use {@link getCurrentTurnText}. Kept as a hard error so
|
|
94
|
+
* callers fail loudly rather than silently consume stale data.
|
|
95
|
+
*/
|
|
96
|
+
getLastAssistantText() {
|
|
97
|
+
throw new Error("ChatGPTSSECollector.getLastAssistantText() was removed; use getCurrentTurnText() — it never returns stale text from a prior turn.");
|
|
98
|
+
}
|
|
99
|
+
/** Has the current turn's SSE stream ended? */
|
|
100
|
+
isStreamComplete() {
|
|
101
|
+
return this.currentTurn.streamComplete;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Apply a raw SSE body directly. Exposed for tests and for callers that
|
|
105
|
+
* want to feed in a recorded transcript.
|
|
106
|
+
*/
|
|
107
|
+
ingest(body) {
|
|
108
|
+
const events = parseSSE(body);
|
|
109
|
+
for (const ev of events)
|
|
110
|
+
this.applyEvent(ev);
|
|
111
|
+
this.currentTurn.streamComplete = true;
|
|
112
|
+
this.finishPendingTurn();
|
|
113
|
+
}
|
|
114
|
+
async maybeConsume(response) {
|
|
115
|
+
if (!this.pendingTurn || this.pendingTurn.captured)
|
|
116
|
+
return;
|
|
117
|
+
const url = response.url();
|
|
118
|
+
if (!CHATGPT_CONVERSATION_URL.test(url))
|
|
119
|
+
return;
|
|
120
|
+
if (response.request().method() !== "POST")
|
|
121
|
+
return;
|
|
122
|
+
// Only the streaming reply matters. ChatGPT's sibling POSTs to the
|
|
123
|
+
// same path family (`/conversation/init`, `/conversation/prepare`)
|
|
124
|
+
// return `application/json` and would otherwise race us to claim the
|
|
125
|
+
// pendingTurn before the real `text/event-stream` arrives.
|
|
126
|
+
const contentType = response.headers()["content-type"] ?? "";
|
|
127
|
+
if (!contentType.includes("event-stream"))
|
|
128
|
+
return;
|
|
129
|
+
// Claim this response so concurrent matches don't double-process.
|
|
130
|
+
this.pendingTurn.captured = true;
|
|
131
|
+
let body;
|
|
132
|
+
try {
|
|
133
|
+
// `response.finished()` resolves only after the entire body has been
|
|
134
|
+
// fetched (the network-level "request finished" signal). For chunked
|
|
135
|
+
// SSE this is non-trivial: the `response` event fires when headers
|
|
136
|
+
// arrive, but the body keeps streaming. Without this wait, certain
|
|
137
|
+
// Playwright builds let `response.text()` return whatever was
|
|
138
|
+
// buffered at handler-time — usually a partial SSE that cuts the
|
|
139
|
+
// assistant reply mid-token.
|
|
140
|
+
await response.finished().catch(() => undefined);
|
|
141
|
+
body = await response.text();
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
// Network aborted etc. Don't reject — let the caller fall back.
|
|
145
|
+
this.finishPendingTurnWithError(err);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
this.ingest(body);
|
|
149
|
+
}
|
|
150
|
+
applyEvent(ev) {
|
|
151
|
+
this.currentTurn.rawEvents.push(ev);
|
|
152
|
+
const raw = ev.data.trim();
|
|
153
|
+
if (raw === "[DONE]") {
|
|
154
|
+
this.currentTurn.streamComplete = true;
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
let parsed;
|
|
158
|
+
try {
|
|
159
|
+
parsed = JSON.parse(raw);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
// delta_encoding marker: `event: delta_encoding\ndata: "v1"`. The
|
|
163
|
+
// bare-string data isn't valid as a top-level JSON value in our
|
|
164
|
+
// pipeline (it parses but doesn't carry payload), so ignore quietly.
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (!parsed || typeof parsed !== "object")
|
|
168
|
+
return;
|
|
169
|
+
const obj = parsed;
|
|
170
|
+
// Case 1: full message snapshot.
|
|
171
|
+
if (isMessageEnvelope(obj)) {
|
|
172
|
+
const msg = obj.message;
|
|
173
|
+
const id = String(msg.id ?? `snap-${this.currentTurn.messages.size}`);
|
|
174
|
+
const role = (msg.author?.role ?? "");
|
|
175
|
+
const content = msg.content;
|
|
176
|
+
const rawParts = Array.isArray(content?.parts) ? content.parts : [];
|
|
177
|
+
const parts = rawParts.map((p) => (typeof p === "string" ? p : ""));
|
|
178
|
+
const status = String(msg.status ?? "in_progress");
|
|
179
|
+
this.currentTurn.messages.set(id, { parts, status, authorRole: role });
|
|
180
|
+
this.currentTurn.lastDeltaTargetId = id;
|
|
181
|
+
if (status === "finished_successfully" && role === "assistant") {
|
|
182
|
+
// Don't set streamComplete here — a later [DONE] is the real signal.
|
|
183
|
+
}
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
// Case 2: terminator frame.
|
|
187
|
+
if (obj.type === "message_stream_complete") {
|
|
188
|
+
this.currentTurn.streamComplete = true;
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
// Case 3: v1 delta encoding.
|
|
192
|
+
if ("v" in obj) {
|
|
193
|
+
this.applyDelta(obj);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
// Case 4: anything else (heartbeats, moderation hints, …) — ignore.
|
|
197
|
+
}
|
|
198
|
+
applyDelta(obj) {
|
|
199
|
+
const path = typeof obj.p === "string" ? obj.p : this.currentTurn.lastDeltaPath;
|
|
200
|
+
const op = typeof obj.o === "string" ? obj.o : this.currentTurn.lastDeltaOp ?? "append";
|
|
201
|
+
if (path !== undefined)
|
|
202
|
+
this.currentTurn.lastDeltaPath = path;
|
|
203
|
+
this.currentTurn.lastDeltaOp = op;
|
|
204
|
+
// PATCH first: ChatGPT delivers the message's CLOSING tokens (the
|
|
205
|
+
// final "```", the status flip to "finished_successfully", end_turn,
|
|
206
|
+
// metadata, ...) inside one wrapper frame:
|
|
207
|
+
// {"p":"", "o":"patch", "v":[ {p,o,v}, {p,o,v}, ... ]}
|
|
208
|
+
// The wrapper's own `p` is empty, so we MUST recurse before we
|
|
209
|
+
// bail on the parts-path check below — otherwise the closing
|
|
210
|
+
// backticks of code blocks (and any trailing content) get dropped.
|
|
211
|
+
if (op === "patch" && Array.isArray(obj.v)) {
|
|
212
|
+
for (const sub of obj.v) {
|
|
213
|
+
if (sub && typeof sub === "object")
|
|
214
|
+
this.applyDelta(sub);
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
// Beyond patches, we only care about deltas that mutate assistant
|
|
219
|
+
// message text. ChatGPT uses paths shaped like
|
|
220
|
+
// "/message/content/parts/0".
|
|
221
|
+
const partMatch = path?.match(/parts\/(\d+)/);
|
|
222
|
+
if (!partMatch)
|
|
223
|
+
return;
|
|
224
|
+
const idx = parseInt(partMatch[1], 10);
|
|
225
|
+
// Identify the target message. v1 delta frames often omit the id, in
|
|
226
|
+
// which case the prior snapshot's id is the active one.
|
|
227
|
+
const id = this.currentTurn.lastDeltaTargetId ?? "__current";
|
|
228
|
+
let snap = this.currentTurn.messages.get(id);
|
|
229
|
+
if (!snap) {
|
|
230
|
+
snap = { parts: [], status: "in_progress", authorRole: "assistant" };
|
|
231
|
+
this.currentTurn.messages.set(id, snap);
|
|
232
|
+
}
|
|
233
|
+
while (snap.parts.length <= idx)
|
|
234
|
+
snap.parts.push("");
|
|
235
|
+
const v = obj.v;
|
|
236
|
+
if (op === "append") {
|
|
237
|
+
const piece = typeof v === "string" ? v : "";
|
|
238
|
+
snap.parts[idx] = (snap.parts[idx] ?? "") + piece;
|
|
239
|
+
}
|
|
240
|
+
else if (op === "replace") {
|
|
241
|
+
const piece = typeof v === "string" ? v : "";
|
|
242
|
+
snap.parts[idx] = piece;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
finishPendingTurn() {
|
|
246
|
+
if (!this.pendingTurn)
|
|
247
|
+
return;
|
|
248
|
+
const text = this.bestAssistantText();
|
|
249
|
+
if (this.pendingTurn.timer)
|
|
250
|
+
clearTimeout(this.pendingTurn.timer);
|
|
251
|
+
const { resolve } = this.pendingTurn;
|
|
252
|
+
this.pendingTurn = undefined;
|
|
253
|
+
resolve(text);
|
|
254
|
+
}
|
|
255
|
+
finishPendingTurnWithError(_err) {
|
|
256
|
+
if (!this.pendingTurn)
|
|
257
|
+
return;
|
|
258
|
+
if (this.pendingTurn.timer)
|
|
259
|
+
clearTimeout(this.pendingTurn.timer);
|
|
260
|
+
const { resolve } = this.pendingTurn;
|
|
261
|
+
this.pendingTurn = undefined;
|
|
262
|
+
// Surface as empty so the caller falls back to innerText extraction.
|
|
263
|
+
resolve("");
|
|
264
|
+
}
|
|
265
|
+
/** Best-effort assistant text from the current turn's snapshots. */
|
|
266
|
+
bestAssistantText() {
|
|
267
|
+
// Prefer the most recently-seen finished assistant message; fall back
|
|
268
|
+
// to whatever's most recent in any status.
|
|
269
|
+
let lastFinished;
|
|
270
|
+
let lastAny;
|
|
271
|
+
for (const snap of this.currentTurn.messages.values()) {
|
|
272
|
+
if (snap.authorRole !== "assistant")
|
|
273
|
+
continue;
|
|
274
|
+
lastAny = snap;
|
|
275
|
+
if (snap.status === "finished_successfully")
|
|
276
|
+
lastFinished = snap;
|
|
277
|
+
}
|
|
278
|
+
const target = lastFinished ?? lastAny;
|
|
279
|
+
if (!target)
|
|
280
|
+
return "";
|
|
281
|
+
return target.parts.join("");
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function freshTurn() {
|
|
285
|
+
return {
|
|
286
|
+
messages: new Map(),
|
|
287
|
+
streamComplete: false,
|
|
288
|
+
rawEvents: [],
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function isMessageEnvelope(obj) {
|
|
292
|
+
const msg = obj.message;
|
|
293
|
+
if (!msg || typeof msg !== "object")
|
|
294
|
+
return false;
|
|
295
|
+
const author = msg.author;
|
|
296
|
+
return Boolean(author && typeof author === "object");
|
|
297
|
+
}
|
|
298
|
+
//# sourceMappingURL=chatgpt-sse-collector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chatgpt-sse-collector.js","sourceRoot":"","sources":["../../src/providers/chatgpt-sse-collector.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Locator, Page } from "playwright";
|
|
2
|
+
import type { ChatProvider, ProviderDiagnostics, WaitOptions } from "../core/provider.js";
|
|
3
|
+
/**
|
|
4
|
+
* ChatGPT (chatgpt.com) provider adapter.
|
|
5
|
+
*
|
|
6
|
+
* Selectors are intentionally written as fallbacks (RFC §8) — when one
|
|
7
|
+
* breaks we fall back to the next. The strongest contracts we rely on:
|
|
8
|
+
* - the prompt input is a textarea or contenteditable in the viewport
|
|
9
|
+
* - assistant messages carry data-message-author-role="assistant"
|
|
10
|
+
* - while streaming, a "Stop generating" button is present
|
|
11
|
+
*
|
|
12
|
+
* Reply extraction has two paths (see `extractLastAssistantMessage`):
|
|
13
|
+
* 1. SSE collector — listens to `/backend-api/.../conversation` and
|
|
14
|
+
* reconstructs the model's original markdown (bullets, fences,
|
|
15
|
+
* tables, link URLs). Preferred.
|
|
16
|
+
* 2. DOM `innerText` fallback — used when SSE parsing returned nothing
|
|
17
|
+
* (endpoint changed, request errored, ...).
|
|
18
|
+
*/
|
|
19
|
+
export declare class ChatGPTAdapter implements ChatProvider {
|
|
20
|
+
readonly name = "chatgpt";
|
|
21
|
+
readonly homeUrl = "https://chatgpt.com/";
|
|
22
|
+
/** One collector per Page. Lazily attached on `open()`. */
|
|
23
|
+
private collectors;
|
|
24
|
+
/**
|
|
25
|
+
* Promise that resolves to the assistant markdown for the currently
|
|
26
|
+
* in-flight turn (between `sendMessage` and `waitForResponse`).
|
|
27
|
+
* Per-page so concurrent pages don't collide.
|
|
28
|
+
*/
|
|
29
|
+
private pendingTurns;
|
|
30
|
+
open(page: Page): Promise<void>;
|
|
31
|
+
isLoggedIn(page: Page): Promise<boolean>;
|
|
32
|
+
findInput(page: Page): Promise<Locator>;
|
|
33
|
+
findSendButton(page: Page): Promise<Locator | null>;
|
|
34
|
+
sendMessage(page: Page, message: string): Promise<void>;
|
|
35
|
+
/** Wait up to `timeoutMs` for the ProseMirror composer to be ready. */
|
|
36
|
+
private waitForComposerReady;
|
|
37
|
+
extractLastAssistantMessage(page: Page): Promise<string>;
|
|
38
|
+
waitForResponse(page: Page, options?: WaitOptions): Promise<string>;
|
|
39
|
+
/**
|
|
40
|
+
* Extract the ChatGPT conversation UUID from the page URL.
|
|
41
|
+
*
|
|
42
|
+
* Once a turn lands, ChatGPT navigates the page to
|
|
43
|
+
* `https://chatgpt.com/c/{conversation-uuid}`
|
|
44
|
+
* The fresh-chat URL (`/` or `/?model=...`) has no UUID — return `null`
|
|
45
|
+
* in that case. The UUID is the natural cross-process session id and is
|
|
46
|
+
* what we plumb into ai-sdk's session model and onward to the UI.
|
|
47
|
+
*/
|
|
48
|
+
getConversationId(page: Page): string | null;
|
|
49
|
+
newChat(page: Page): Promise<void>;
|
|
50
|
+
diagnose(page: Page): Promise<ProviderDiagnostics>;
|
|
51
|
+
/** Look up (and lazily create + attach) the SSE collector for a Page. */
|
|
52
|
+
private ensureCollector;
|
|
53
|
+
private waitForStopButton;
|
|
54
|
+
private waitForStopButtonGone;
|
|
55
|
+
private isStopButtonVisible;
|
|
56
|
+
}
|