@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,480 @@
|
|
|
1
|
+
import { InputNotFoundError, ProviderApiKeyRequiredError, ProviderAutomationBlockedError, ProviderPermissionDeniedError, ProviderRateLimitedError, ResponseExtractionError, ResponseTimeoutError, } from "../core/errors.js";
|
|
2
|
+
import { typeMultiline } from "../core/keyboard.js";
|
|
3
|
+
import { gotoOrThrowNetworkError } from "../core/navigate.js";
|
|
4
|
+
import { sleep, waitUntilStable } from "../core/response-watcher.js";
|
|
5
|
+
/**
|
|
6
|
+
* Google AI Studio (aistudio.google.com/prompts/new_chat) provider
|
|
7
|
+
* adapter.
|
|
8
|
+
*
|
|
9
|
+
* This is what users colloquially mean by "Gemini" — the free web-chat
|
|
10
|
+
* interface that lets you talk to Gemini models without an API key
|
|
11
|
+
* (the "No API key selected" button on the page is a separate, optional
|
|
12
|
+
* feature for users who want to use their own key; the page is fully
|
|
13
|
+
* usable without it). chat-web does NOT target gemini.google.com — in
|
|
14
|
+
* practice many networks block that domain while AI Studio reaches
|
|
15
|
+
* fine, and AI Studio is the surface users actually expect.
|
|
16
|
+
*
|
|
17
|
+
* Observed structure (2026-05):
|
|
18
|
+
* - Composer: `<ms-prompt-box>` (Angular Material) wrapping a textarea
|
|
19
|
+
* with `aria="Enter a prompt"`, `placeholder="Start typing..."`.
|
|
20
|
+
* - Run button: NO aria-label; visible text is "Run" + two Material
|
|
21
|
+
* icon font ligatures (`keyboard_command_key`, `keyboard_return`).
|
|
22
|
+
* `type="submit"`. Use `getByRole("button", { name: /^Run$/ })`.
|
|
23
|
+
* - Stop: the same submit button flips to a stop state; its
|
|
24
|
+
* aria-label changes to include "Stop". Detect via aria-label
|
|
25
|
+
* substring.
|
|
26
|
+
* - Assistant turn: `<ms-chat-turn>` containing a
|
|
27
|
+
* `.chat-turn-container.model` with `.turn-content`. innerText
|
|
28
|
+
* leaks "Model HH:MM AM/PM" headers and Material icon font
|
|
29
|
+
* ligatures (`edit`, `more_vert`, `error`, ...) as plain text —
|
|
30
|
+
* stripped by {@link stripChromeFromTurn}.
|
|
31
|
+
* - Conversation id: `/prompts/{slug}` once the prompt is saved; the
|
|
32
|
+
* placeholder `/prompts/new_chat` does NOT count.
|
|
33
|
+
*/
|
|
34
|
+
export class GeminiAdapter {
|
|
35
|
+
name = "gemini";
|
|
36
|
+
homeUrl = "https://aistudio.google.com/prompts/new_chat";
|
|
37
|
+
async open(page) {
|
|
38
|
+
await gotoOrThrowNetworkError(page, this.homeUrl, this.name);
|
|
39
|
+
await this.waitForComposerReady(page).catch(() => undefined);
|
|
40
|
+
}
|
|
41
|
+
async isLoggedIn(page) {
|
|
42
|
+
const url = page.url();
|
|
43
|
+
if (url.includes("accounts.google.com") || url.includes("/signin/"))
|
|
44
|
+
return false;
|
|
45
|
+
const candidates = [
|
|
46
|
+
'ms-prompt-box textarea',
|
|
47
|
+
'textarea[aria-label="Enter a prompt"]',
|
|
48
|
+
'textarea[aria-label*="prompt" i]',
|
|
49
|
+
'textarea',
|
|
50
|
+
];
|
|
51
|
+
for (const sel of candidates) {
|
|
52
|
+
const visible = await page.locator(sel).first().isVisible().catch(() => false);
|
|
53
|
+
if (visible)
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
async findInput(page) {
|
|
59
|
+
const candidates = [
|
|
60
|
+
page.locator('ms-prompt-box textarea').first(),
|
|
61
|
+
page.locator('textarea[aria-label="Enter a prompt"]').first(),
|
|
62
|
+
page.locator('textarea[aria-label*="prompt" i]').first(),
|
|
63
|
+
page.locator('textarea[placeholder*="Start typing" i]').first(),
|
|
64
|
+
page.locator('textarea').first(),
|
|
65
|
+
];
|
|
66
|
+
for (const locator of candidates) {
|
|
67
|
+
if (await locator.isVisible().catch(() => false))
|
|
68
|
+
return locator;
|
|
69
|
+
}
|
|
70
|
+
throw new InputNotFoundError(this.name);
|
|
71
|
+
}
|
|
72
|
+
async findSendButton(page) {
|
|
73
|
+
// The Run button has NO aria-label — getByRole("button", { name: /^Run$/ })
|
|
74
|
+
// resolves the accessible name from visible "Run" text, then we fall
|
|
75
|
+
// back to type=submit / :has-text("Run").
|
|
76
|
+
const candidates = [
|
|
77
|
+
page.getByRole("button", { name: /^Run$/i }).first(),
|
|
78
|
+
page.locator('ms-prompt-box button[type="submit"]').first(),
|
|
79
|
+
page.locator('button[type="submit"]').first(),
|
|
80
|
+
page.locator('ms-prompt-box button:has-text("Run")').first(),
|
|
81
|
+
];
|
|
82
|
+
for (const locator of candidates) {
|
|
83
|
+
if (await locator.isVisible().catch(() => false))
|
|
84
|
+
return locator;
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
async sendMessage(page, message) {
|
|
89
|
+
// CRITICAL: AI Studio defaults to "Grounding with Google Search"
|
|
90
|
+
// enabled on every new chat. With Grounding on, the page calls a
|
|
91
|
+
// Google Search backend BEFORE invoking the model — and on many
|
|
92
|
+
// user networks (especially behind DPI boxes that drop Google
|
|
93
|
+
// Search Suggest / search infrastructure traffic) that call hangs
|
|
94
|
+
// silently, leaving the UI stuck on "Thinking" forever even though
|
|
95
|
+
// the model invocation request (GenerateContent / streamGenerateContent)
|
|
96
|
+
// is never made. Disabling Grounding makes a simple "1+1=" return
|
|
97
|
+
// "2" instantly, since there's no pre-flight search step.
|
|
98
|
+
//
|
|
99
|
+
// We close the Grounding chip if it's present. Idempotent: if the
|
|
100
|
+
// chip has already been removed (sticky across the profile's
|
|
101
|
+
// session storage), this is a no-op.
|
|
102
|
+
await this.disableGroundingWithGoogleSearch(page).catch(() => undefined);
|
|
103
|
+
const input = await this.findInput(page);
|
|
104
|
+
await input.click();
|
|
105
|
+
// typeMultiline preserves newlines correctly. Angular Material's
|
|
106
|
+
// form state requires real input events (locator.fill() leaves Run
|
|
107
|
+
// disabled forever).
|
|
108
|
+
await typeMultiline(page, message);
|
|
109
|
+
await sleep(200);
|
|
110
|
+
const send = await this.findSendButton(page);
|
|
111
|
+
if (send && (await send.isEnabled().catch(() => false))) {
|
|
112
|
+
await send.click().catch(async () => {
|
|
113
|
+
await this.pressRunShortcut(page);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
await this.pressRunShortcut(page);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* AI Studio shows a "Grounding with Google Search" chip on every new
|
|
122
|
+
* chat by default. The chip has a close (×) button with
|
|
123
|
+
* `aria-label="Remove Grounding with Google Search"`. Clicking it
|
|
124
|
+
* disables the grounding step for this conversation — and removes a
|
|
125
|
+
* silent hang point on networks that can't reach Google Search.
|
|
126
|
+
*/
|
|
127
|
+
async disableGroundingWithGoogleSearch(page) {
|
|
128
|
+
const closer = page
|
|
129
|
+
.locator('button[aria-label="Remove Grounding with Google Search"]')
|
|
130
|
+
.first();
|
|
131
|
+
if (await closer.isVisible().catch(() => false)) {
|
|
132
|
+
await closer.click({ force: true }).catch(() => undefined);
|
|
133
|
+
await sleep(150);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async extractLastAssistantMessage(page) {
|
|
137
|
+
const candidates = [
|
|
138
|
+
page.locator('ms-chat-turn .chat-turn-container.model .turn-content').last(),
|
|
139
|
+
page.locator('.chat-turn-container.model .turn-content').last(),
|
|
140
|
+
page.locator('ms-chat-turn .turn-content').last(),
|
|
141
|
+
page.locator('ms-chat-turn').last(),
|
|
142
|
+
];
|
|
143
|
+
for (const locator of candidates) {
|
|
144
|
+
if (await locator.count().then((n) => n > 0).catch(() => false)) {
|
|
145
|
+
const raw = await locator.innerText().catch(() => "");
|
|
146
|
+
const cleaned = stripChromeFromTurn(raw);
|
|
147
|
+
if (cleaned)
|
|
148
|
+
return cleaned;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
throw new ResponseExtractionError(this.name);
|
|
152
|
+
}
|
|
153
|
+
async waitForResponse(page, options = {}) {
|
|
154
|
+
// AI Studio defaults to a high "Thinking level" + Grounding with
|
|
155
|
+
// Google Search (the chip is on at first visit) — a single prompt
|
|
156
|
+
// routinely takes 2-4 minutes. Bump the default budget to 5 min so
|
|
157
|
+
// we don't time out before the model finishes streaming. Callers
|
|
158
|
+
// who want a tighter cap pass an explicit timeoutMs.
|
|
159
|
+
const timeoutMs = options.timeoutMs ?? 5 * 60 * 1000;
|
|
160
|
+
const stableMs = options.stableMs ?? 2_000;
|
|
161
|
+
const deadline = Date.now() + timeoutMs;
|
|
162
|
+
// Phase 1: wait for streaming to begin (stop button appears).
|
|
163
|
+
await this.waitForStopButton(page, { timeoutMs: 15_000 }).catch(() => undefined);
|
|
164
|
+
// Phase 2: wait for streaming to end.
|
|
165
|
+
const remaining = Math.max(deadline - Date.now(), 5_000);
|
|
166
|
+
await this.waitForStopButtonGone(page, remaining).catch(() => undefined);
|
|
167
|
+
// Phase 3: text stability — but treat the "Thinking" placeholder
|
|
168
|
+
// (rendered while AI Studio is generating) as "not yet" so we don't
|
|
169
|
+
// accidentally return that as the final answer.
|
|
170
|
+
const stable = await waitUntilStable(async () => {
|
|
171
|
+
const text = await this.extractLastAssistantMessage(page).catch(() => "");
|
|
172
|
+
if (isThinkingPlaceholder(text))
|
|
173
|
+
return "";
|
|
174
|
+
return text;
|
|
175
|
+
}, {
|
|
176
|
+
timeoutMs: Math.max(deadline - Date.now(), 1_500),
|
|
177
|
+
stableMs,
|
|
178
|
+
signal: options.signal,
|
|
179
|
+
onProgress: options.onProgress,
|
|
180
|
+
});
|
|
181
|
+
if (!stable) {
|
|
182
|
+
// Distinguish "took too long" from "AI Studio's WAA anti-abuse
|
|
183
|
+
// silently blocked the request". If the page never navigated off
|
|
184
|
+
// /prompts/new_chat AND the model turn body is still "Thinking",
|
|
185
|
+
// the JS pipeline never actually invoked the model (it's stuck
|
|
186
|
+
// in the WAA challenge retry loop — see the lesson doc). Throw a
|
|
187
|
+
// typed error with an actionable hint instead of a vague timeout.
|
|
188
|
+
if (await this.looksAutomationBlocked(page)) {
|
|
189
|
+
throw new ProviderAutomationBlockedError(this.name, "The model invocation request was never made — Google's anti-abuse challenge (WAA) likely blocked it.");
|
|
190
|
+
}
|
|
191
|
+
throw new ResponseTimeoutError(this.name, timeoutMs);
|
|
192
|
+
}
|
|
193
|
+
// AI Studio renders upstream errors AS the model turn body (so
|
|
194
|
+
// "stable" successfully extracts an "answer" that's actually an
|
|
195
|
+
// error message). Catch the well-known patterns and throw typed
|
|
196
|
+
// errors instead of surfacing the error string as the model's reply.
|
|
197
|
+
await this.throwIfKnownUpstreamError(page, stable);
|
|
198
|
+
return stable;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Inspect the freshly-extracted "assistant" text and raise a typed
|
|
202
|
+
* error if it's really an upstream failure that AI Studio just
|
|
203
|
+
* rendered in the model turn slot.
|
|
204
|
+
*
|
|
205
|
+
* Known patterns:
|
|
206
|
+
* - "An internal error has occurred." → typically quota /
|
|
207
|
+
* server-side error (the user has reproduced this when their
|
|
208
|
+
* free-tier daily quota was exhausted).
|
|
209
|
+
* - "Failed to generate content: permission denied." → API key
|
|
210
|
+
* missing or model not enabled on the account.
|
|
211
|
+
*
|
|
212
|
+
* The actual root cause for "internal error" varies (preview model
|
|
213
|
+
* outage, daily quota cap, region restriction, transient backend
|
|
214
|
+
* error). We don't try to disambiguate further — the hint just lists
|
|
215
|
+
* the common ones so the user knows where to look.
|
|
216
|
+
*/
|
|
217
|
+
async throwIfKnownUpstreamError(page, text) {
|
|
218
|
+
const lower = text.toLowerCase();
|
|
219
|
+
if (lower.includes("an internal error has occurred")) {
|
|
220
|
+
const cause = await this.detectFailureCause(page);
|
|
221
|
+
if (cause === "no-api-key") {
|
|
222
|
+
throw new ProviderApiKeyRequiredError(this.name, 'AI Studio shows "No API key selected"; configure an API key to run prompts.');
|
|
223
|
+
}
|
|
224
|
+
if (cause === "permission-denied") {
|
|
225
|
+
throw new ProviderPermissionDeniedError(this.name, text);
|
|
226
|
+
}
|
|
227
|
+
// No specific UI indicator — surface as a rate-limited / quota
|
|
228
|
+
// problem since that's the most common cause of an opaque
|
|
229
|
+
// "internal error" on the free tier.
|
|
230
|
+
throw new ProviderRateLimitedError(this.name);
|
|
231
|
+
}
|
|
232
|
+
if (lower.includes("failed to generate content")) {
|
|
233
|
+
if (lower.includes("permission denied")) {
|
|
234
|
+
throw new ProviderPermissionDeniedError(this.name, text);
|
|
235
|
+
}
|
|
236
|
+
throw new ProviderPermissionDeniedError(this.name, text);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
async detectFailureCause(page) {
|
|
240
|
+
const noKey = await page
|
|
241
|
+
.locator('button[aria-label="No API key selected"]')
|
|
242
|
+
.first()
|
|
243
|
+
.isVisible()
|
|
244
|
+
.catch(() => false);
|
|
245
|
+
if (noKey)
|
|
246
|
+
return "no-api-key";
|
|
247
|
+
const found = await page
|
|
248
|
+
.evaluate(() => {
|
|
249
|
+
const t = (document.body.innerText || "").toLowerCase();
|
|
250
|
+
if (t.includes("permission denied"))
|
|
251
|
+
return "permission-denied";
|
|
252
|
+
if (t.includes("no api key") || t.includes("get api key"))
|
|
253
|
+
return "no-api-key";
|
|
254
|
+
return "";
|
|
255
|
+
})
|
|
256
|
+
.catch(() => "");
|
|
257
|
+
if (found === "permission-denied" || found === "no-api-key")
|
|
258
|
+
return found;
|
|
259
|
+
return "";
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Heuristic: are we stuck in the WAA-blocked state where AI Studio
|
|
263
|
+
* never moves past "Thinking" and the URL still says new_chat? Used
|
|
264
|
+
* to surface a clearer error than a generic timeout.
|
|
265
|
+
*/
|
|
266
|
+
async looksAutomationBlocked(page) {
|
|
267
|
+
const url = page.url();
|
|
268
|
+
if (!url.includes("/prompts/new_chat"))
|
|
269
|
+
return false;
|
|
270
|
+
const text = await this.extractLastAssistantMessage(page).catch(() => "");
|
|
271
|
+
return isThinkingPlaceholder(text);
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* AI Studio's per-prompt URL is `/prompts/{slug}` after the prompt is
|
|
275
|
+
* saved. Until then the URL stays at `/prompts/new_chat` (no
|
|
276
|
+
* conversation id).
|
|
277
|
+
*/
|
|
278
|
+
getConversationId(page) {
|
|
279
|
+
const url = page.url();
|
|
280
|
+
const match = url.match(/\/prompts\/([^/?#]+)/);
|
|
281
|
+
if (!match)
|
|
282
|
+
return null;
|
|
283
|
+
const slug = match[1];
|
|
284
|
+
if (slug === "new_chat" || slug === "")
|
|
285
|
+
return null;
|
|
286
|
+
return slug;
|
|
287
|
+
}
|
|
288
|
+
async newChat(page) {
|
|
289
|
+
const candidates = [
|
|
290
|
+
page.getByRole("button", { name: /new (chat|prompt)|新对话|新建对话/i }).first(),
|
|
291
|
+
page.getByRole("link", { name: /new (chat|prompt)|新对话/i }).first(),
|
|
292
|
+
page.locator('a[href*="/prompts/new_chat"]').first(),
|
|
293
|
+
];
|
|
294
|
+
for (const locator of candidates) {
|
|
295
|
+
if (await locator.isVisible().catch(() => false)) {
|
|
296
|
+
await locator.click().catch(() => undefined);
|
|
297
|
+
await page.waitForTimeout(500);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
await this.open(page);
|
|
302
|
+
}
|
|
303
|
+
async diagnose(page) {
|
|
304
|
+
const [loggedIn, sendBtn] = await Promise.all([
|
|
305
|
+
this.isLoggedIn(page),
|
|
306
|
+
this.findSendButton(page).then((l) => l !== null),
|
|
307
|
+
]);
|
|
308
|
+
let inputFound = false;
|
|
309
|
+
try {
|
|
310
|
+
await this.findInput(page);
|
|
311
|
+
inputFound = true;
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
inputFound = false;
|
|
315
|
+
}
|
|
316
|
+
const assistantMessageCount = await page
|
|
317
|
+
.locator("ms-chat-turn, ms-prompt-chunk")
|
|
318
|
+
.count()
|
|
319
|
+
.catch(() => 0);
|
|
320
|
+
const stopButtonFound = await this.isStopButtonVisible(page);
|
|
321
|
+
let lastAssistantLength = 0;
|
|
322
|
+
try {
|
|
323
|
+
const text = await this.extractLastAssistantMessage(page);
|
|
324
|
+
lastAssistantLength = text.length;
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
lastAssistantLength = 0;
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
loggedIn,
|
|
331
|
+
inputFound,
|
|
332
|
+
sendButtonFound: sendBtn,
|
|
333
|
+
assistantMessageCount,
|
|
334
|
+
stopButtonFound,
|
|
335
|
+
lastAssistantLength,
|
|
336
|
+
pageUrl: page.url(),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
async pressRunShortcut(page) {
|
|
340
|
+
// AI Studio binds Cmd/Ctrl+Enter to Run.
|
|
341
|
+
const isMac = process.platform === "darwin";
|
|
342
|
+
await page.keyboard.press(isMac ? "Meta+Enter" : "Control+Enter");
|
|
343
|
+
}
|
|
344
|
+
async waitForComposerReady(page, timeoutMs = 15_000) {
|
|
345
|
+
const sel = 'ms-prompt-box textarea, textarea[aria-label="Enter a prompt"], textarea[aria-label*="prompt" i], textarea';
|
|
346
|
+
await page.locator(sel).first().waitFor({ state: "visible", timeout: timeoutMs });
|
|
347
|
+
}
|
|
348
|
+
async waitForStopButton(page, options = {}) {
|
|
349
|
+
const timeoutMs = options.timeoutMs ?? 15_000;
|
|
350
|
+
const deadline = Date.now() + timeoutMs;
|
|
351
|
+
while (Date.now() < deadline) {
|
|
352
|
+
if (await this.isStopButtonVisible(page))
|
|
353
|
+
return;
|
|
354
|
+
await sleep(150);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
async waitForStopButtonGone(page, timeoutMs = 5_000) {
|
|
358
|
+
const deadline = Date.now() + timeoutMs;
|
|
359
|
+
let consecutiveGone = 0;
|
|
360
|
+
while (Date.now() < deadline) {
|
|
361
|
+
const visible = await this.isStopButtonVisible(page);
|
|
362
|
+
if (!visible) {
|
|
363
|
+
consecutiveGone += 1;
|
|
364
|
+
if (consecutiveGone >= 3)
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
consecutiveGone = 0;
|
|
369
|
+
}
|
|
370
|
+
await sleep(200);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
async isStopButtonVisible(page) {
|
|
374
|
+
// AI Studio's Stop button has NO aria-label — its visible text is
|
|
375
|
+
// "progress_activity Stop" (a Material icon ligature + the word
|
|
376
|
+
// "Stop"). The accessible name resolves to "Stop" via the visible
|
|
377
|
+
// text, so getByRole(name=/Stop/) catches it. We keep the
|
|
378
|
+
// aria-label fallbacks in case Google adds them later.
|
|
379
|
+
const stopByRole = await page
|
|
380
|
+
.getByRole("button", { name: /^Stop$/i })
|
|
381
|
+
.first()
|
|
382
|
+
.isVisible()
|
|
383
|
+
.catch(() => false);
|
|
384
|
+
if (stopByRole)
|
|
385
|
+
return true;
|
|
386
|
+
const stopByText = await page
|
|
387
|
+
.locator('button:has-text("Stop")')
|
|
388
|
+
.first()
|
|
389
|
+
.isVisible()
|
|
390
|
+
.catch(() => false);
|
|
391
|
+
if (stopByText)
|
|
392
|
+
return true;
|
|
393
|
+
const selectors = [
|
|
394
|
+
'button[aria-label*="Stop" i]',
|
|
395
|
+
'button[aria-label*="Cancel" i]',
|
|
396
|
+
'button[aria-label*="停止" i]',
|
|
397
|
+
'button[data-test-id="stop-button"]',
|
|
398
|
+
];
|
|
399
|
+
for (const sel of selectors) {
|
|
400
|
+
const visible = await page.locator(sel).first().isVisible().catch(() => false);
|
|
401
|
+
if (visible)
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Strip the UI chrome that leaks into `innerText` on AI Studio model
|
|
409
|
+
* turns. Removes:
|
|
410
|
+
* - The "Model HH:MM AM/PM" turn header
|
|
411
|
+
* - Single-line Material icon font ligatures (rendered as plain text
|
|
412
|
+
* words because the icon-font substitution doesn't affect text
|
|
413
|
+
* extraction): edit, more_vert, error, content_copy, thumb_up,
|
|
414
|
+
* thumb_down, refresh, delete, close, expand_more, expand_less,
|
|
415
|
+
* code, play_arrow, stop, menu.
|
|
416
|
+
*
|
|
417
|
+
* Does NOT remove lines where the same word appears INSIDE a sentence
|
|
418
|
+
* (e.g. "an error occurred during parsing" stays as-is).
|
|
419
|
+
*/
|
|
420
|
+
export function stripChromeFromTurn(text) {
|
|
421
|
+
if (!text)
|
|
422
|
+
return "";
|
|
423
|
+
const lines = text.split(/\r?\n/);
|
|
424
|
+
const HEADER = /^(Model|User|System)\s+\d{1,2}:\d{2}\s*(AM|PM)?\s*$/i;
|
|
425
|
+
const ICON_LIGATURES = new Set([
|
|
426
|
+
"edit",
|
|
427
|
+
"more_vert",
|
|
428
|
+
"error",
|
|
429
|
+
"content_copy",
|
|
430
|
+
"thumb_up",
|
|
431
|
+
"thumb_down",
|
|
432
|
+
"refresh",
|
|
433
|
+
"delete",
|
|
434
|
+
"close",
|
|
435
|
+
"expand_more",
|
|
436
|
+
"expand_less",
|
|
437
|
+
"code",
|
|
438
|
+
"play_arrow",
|
|
439
|
+
"stop",
|
|
440
|
+
"menu",
|
|
441
|
+
]);
|
|
442
|
+
const kept = [];
|
|
443
|
+
for (const raw of lines) {
|
|
444
|
+
const trimmed = raw.trim();
|
|
445
|
+
if (!trimmed) {
|
|
446
|
+
kept.push(raw);
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
if (HEADER.test(trimmed))
|
|
450
|
+
continue;
|
|
451
|
+
if (ICON_LIGATURES.has(trimmed))
|
|
452
|
+
continue;
|
|
453
|
+
kept.push(raw);
|
|
454
|
+
}
|
|
455
|
+
return kept.join("\n").trim();
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* AI Studio renders "Thinking" / "Thought for Ns" / "正在思考" while the
|
|
459
|
+
* model is reasoning before its first content token. Those strings
|
|
460
|
+
* appear in the model turn's innerText for a while; treating them as
|
|
461
|
+
* "the final answer" is the same class of bug as ChatGPT's pre-stream
|
|
462
|
+
* `Thinking` placeholder.
|
|
463
|
+
*/
|
|
464
|
+
function isThinkingPlaceholder(text) {
|
|
465
|
+
const t = text.trim();
|
|
466
|
+
if (!t)
|
|
467
|
+
return true;
|
|
468
|
+
if (t.length > 80)
|
|
469
|
+
return false;
|
|
470
|
+
const lower = t.toLowerCase();
|
|
471
|
+
return (lower === "thinking" ||
|
|
472
|
+
lower === "thinking…" ||
|
|
473
|
+
lower === "thinking..." ||
|
|
474
|
+
lower.startsWith("thinking\n") ||
|
|
475
|
+
lower.startsWith("thought for ") ||
|
|
476
|
+
lower.startsWith("reasoning") ||
|
|
477
|
+
lower.startsWith("思考") ||
|
|
478
|
+
lower.startsWith("正在思考"));
|
|
479
|
+
}
|
|
480
|
+
//# sourceMappingURL=gemini.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gemini.js","sourceRoot":"","sources":["../../src/providers/gemini.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { ChatGPTAdapter } from "./chatgpt.js";
|
|
2
|
+
export { DeepSeekAdapter } from "./deepseek.js";
|
|
3
|
+
export { GeminiAdapter } from "./gemini.js";
|
|
4
|
+
/**
|
|
5
|
+
* Register all bundled providers. Call this once at process startup
|
|
6
|
+
* (CLI / daemon entrypoints already do so).
|
|
7
|
+
*/
|
|
8
|
+
export declare function registerBuiltinProviders(): void;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { registerProvider } from "../core/provider.js";
|
|
2
|
+
import { ChatGPTAdapter } from "./chatgpt.js";
|
|
3
|
+
import { DeepSeekAdapter } from "./deepseek.js";
|
|
4
|
+
import { GeminiAdapter } from "./gemini.js";
|
|
5
|
+
export { ChatGPTAdapter } from "./chatgpt.js";
|
|
6
|
+
export { DeepSeekAdapter } from "./deepseek.js";
|
|
7
|
+
export { GeminiAdapter } from "./gemini.js";
|
|
8
|
+
/**
|
|
9
|
+
* Register all bundled providers. Call this once at process startup
|
|
10
|
+
* (CLI / daemon entrypoints already do so).
|
|
11
|
+
*/
|
|
12
|
+
export function registerBuiltinProviders() {
|
|
13
|
+
registerProvider(new ChatGPTAdapter());
|
|
14
|
+
registerProvider(new DeepSeekAdapter());
|
|
15
|
+
registerProvider(new GeminiAdapter());
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/providers/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAEvD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C;;;GAGG;AACH,MAAM,UAAU,wBAAwB;IACtC,gBAAgB,CAAC,IAAI,cAAc,EAAE,CAAC,CAAC;IACvC,gBAAgB,CAAC,IAAI,eAAe,EAAE,CAAC,CAAC;IACxC,gBAAgB,CAAC,IAAI,aAAa,EAAE,CAAC,CAAC;AACxC,CAAC"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { BrowserContext, Page } from "playwright";
|
|
2
|
+
import { type LaunchOptions } from "./core/browser.js";
|
|
3
|
+
import { type Logger } from "./core/logger.js";
|
|
4
|
+
import { type ChatProvider, type ProviderDiagnostics } from "./core/provider.js";
|
|
5
|
+
import { type PageSnapshot } from "./core/snapshot.js";
|
|
6
|
+
export interface SessionOpenOptions {
|
|
7
|
+
/** Run Chromium headless. Defaults to headed (RFC §19.3). */
|
|
8
|
+
headless?: boolean;
|
|
9
|
+
/** Override the logger. */
|
|
10
|
+
logger?: Logger;
|
|
11
|
+
/** Extra Chromium args / viewport overrides forwarded to the browser launcher. */
|
|
12
|
+
launch?: Omit<LaunchOptions, "headless" | "profileManager">;
|
|
13
|
+
/** Auto-open the provider home URL after launch. Default true. */
|
|
14
|
+
autoOpen?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface SendOptions {
|
|
17
|
+
/** Start a fresh chat before sending (clears the conversation context). */
|
|
18
|
+
newChat?: boolean;
|
|
19
|
+
/** Hard upper bound on the response wait. Forwarded to the provider. */
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
/** How long the assistant text must stop changing before we declare done. */
|
|
22
|
+
stableMs?: number;
|
|
23
|
+
/** AbortSignal — aborts the wait but does NOT undo the network message. */
|
|
24
|
+
signal?: AbortSignal;
|
|
25
|
+
/** Streaming progress callback. */
|
|
26
|
+
onProgress?: (text: string) => void;
|
|
27
|
+
}
|
|
28
|
+
export interface SendResult {
|
|
29
|
+
/** 0-based index of this turn within the session's lifetime. */
|
|
30
|
+
turnIndex: number;
|
|
31
|
+
message: string;
|
|
32
|
+
response: string;
|
|
33
|
+
/** Wall-clock duration of this send (ms). */
|
|
34
|
+
durationMs: number;
|
|
35
|
+
/**
|
|
36
|
+
* Provider-side conversation id, when the provider exposes one. For
|
|
37
|
+
* ChatGPT this is the UUID at `chatgpt.com/c/{uuid}`. Stable across
|
|
38
|
+
* turns within the same conversation, undefined on the first response
|
|
39
|
+
* if the provider hasn't navigated to its conversation URL yet.
|
|
40
|
+
*/
|
|
41
|
+
conversationId?: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* A long-lived chat session bound to one provider account.
|
|
45
|
+
*
|
|
46
|
+
* Think of it as "the thing you'd get if you opened ChatGPT in a browser
|
|
47
|
+
* tab and kept it open" — same persistent profile, same Chromium context,
|
|
48
|
+
* same conversation memory. Multi-turn dialogue happens by calling
|
|
49
|
+
* `session.send()` repeatedly. `session.newChat()` starts a fresh
|
|
50
|
+
* conversation but keeps the browser alive.
|
|
51
|
+
*
|
|
52
|
+
* RFC §19.2: a single account must not be hit concurrently. We enforce
|
|
53
|
+
* this by serialising `send` / `newChat` through an internal FIFO queue,
|
|
54
|
+
* so callers can safely fire-and-await from multiple async paths.
|
|
55
|
+
*/
|
|
56
|
+
export declare class ChatSession {
|
|
57
|
+
private readonly providerImpl;
|
|
58
|
+
private readonly context;
|
|
59
|
+
private readonly pageImpl;
|
|
60
|
+
readonly userDataDir: string;
|
|
61
|
+
private readonly logger;
|
|
62
|
+
/** Open and return a ready-to-use session. */
|
|
63
|
+
static open(providerName: string, options?: SessionOpenOptions): Promise<ChatSession>;
|
|
64
|
+
/**
|
|
65
|
+
* Build a session around an already-launched browser context. Mostly
|
|
66
|
+
* used by tests / advanced callers that manage the browser themselves;
|
|
67
|
+
* regular code should go through `ChatSession.open()`.
|
|
68
|
+
*/
|
|
69
|
+
static fromBrowser(args: {
|
|
70
|
+
provider: ChatProvider;
|
|
71
|
+
context: BrowserContext;
|
|
72
|
+
page: Page;
|
|
73
|
+
userDataDir: string;
|
|
74
|
+
logger?: Logger;
|
|
75
|
+
}): ChatSession;
|
|
76
|
+
private turnCounter;
|
|
77
|
+
private queue;
|
|
78
|
+
private closed;
|
|
79
|
+
/** Provider-side conversation id (e.g. ChatGPT `/c/{uuid}`), set after the first turn lands. */
|
|
80
|
+
private providerConversationId;
|
|
81
|
+
private constructor();
|
|
82
|
+
get provider(): string;
|
|
83
|
+
get page(): Page;
|
|
84
|
+
get turns(): number;
|
|
85
|
+
get isClosed(): boolean;
|
|
86
|
+
/** Quick check: is the provider profile signed in? */
|
|
87
|
+
isLoggedIn(): Promise<boolean>;
|
|
88
|
+
/**
|
|
89
|
+
* Send one message and await the assistant's full reply.
|
|
90
|
+
*
|
|
91
|
+
* Calls are serialised per session — overlapping `send`s from different
|
|
92
|
+
* callers won't trample each other; they queue up and run in order.
|
|
93
|
+
*/
|
|
94
|
+
send(message: string, options?: SendOptions): Promise<SendResult>;
|
|
95
|
+
/**
|
|
96
|
+
* Provider-side conversation id (e.g. ChatGPT's /c/{uuid}). `undefined`
|
|
97
|
+
* before the first turn has landed, then stable for the rest of this
|
|
98
|
+
* `ChatSession` unless `newChat()` is called.
|
|
99
|
+
*/
|
|
100
|
+
get conversationId(): string | undefined;
|
|
101
|
+
/**
|
|
102
|
+
* Read the conversation id from the current page (provider permitting)
|
|
103
|
+
* and store it on the session. Idempotent — repeated calls within the
|
|
104
|
+
* same conversation will see the same id; the first call captures it.
|
|
105
|
+
*/
|
|
106
|
+
private captureConversationId;
|
|
107
|
+
/** Start a fresh conversation while keeping the session/browser open. */
|
|
108
|
+
newChat(): Promise<void>;
|
|
109
|
+
/** Provider-specific health probe (input found, send button found, ...). */
|
|
110
|
+
diagnose(): Promise<ProviderDiagnostics>;
|
|
111
|
+
/** Snapshot of interactive DOM elements (ref-based). */
|
|
112
|
+
snapshot(): Promise<PageSnapshot>;
|
|
113
|
+
/** Close the browser context. Safe to call multiple times. */
|
|
114
|
+
close(): Promise<void>;
|
|
115
|
+
/** Enables `await using session = …` (TC39 explicit resource management). */
|
|
116
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
117
|
+
private assertOpen;
|
|
118
|
+
private enqueue;
|
|
119
|
+
}
|
|
120
|
+
/** Convenience wrapper: open → callback → close, regardless of throws. */
|
|
121
|
+
export declare function withSession<T>(providerName: string, body: (session: ChatSession) => Promise<T>, options?: SessionOpenOptions): Promise<T>;
|