@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.
Files changed (77) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +142 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/commands/doctor.d.ts +27 -0
  6. package/dist/commands/doctor.js +116 -0
  7. package/dist/commands/doctor.js.map +1 -0
  8. package/dist/commands/index.d.ts +3 -0
  9. package/dist/commands/index.js +4 -0
  10. package/dist/commands/index.js.map +1 -0
  11. package/dist/commands/info.d.ts +32 -0
  12. package/dist/commands/info.js +81 -0
  13. package/dist/commands/info.js.map +1 -0
  14. package/dist/commands/login.d.ts +19 -0
  15. package/dist/commands/login.js +61 -0
  16. package/dist/commands/login.js.map +1 -0
  17. package/dist/core/browser.d.ts +70 -0
  18. package/dist/core/browser.js +96 -0
  19. package/dist/core/browser.js.map +1 -0
  20. package/dist/core/errors.d.ts +60 -0
  21. package/dist/core/errors.js +153 -0
  22. package/dist/core/errors.js.map +1 -0
  23. package/dist/core/install-chromium.d.ts +55 -0
  24. package/dist/core/install-chromium.js +156 -0
  25. package/dist/core/install-chromium.js.map +1 -0
  26. package/dist/core/keyboard.d.ts +39 -0
  27. package/dist/core/keyboard.js +54 -0
  28. package/dist/core/keyboard.js.map +1 -0
  29. package/dist/core/locator-score.d.ts +41 -0
  30. package/dist/core/locator-score.js +101 -0
  31. package/dist/core/locator-score.js.map +1 -0
  32. package/dist/core/logger.d.ts +10 -0
  33. package/dist/core/logger.js +38 -0
  34. package/dist/core/logger.js.map +1 -0
  35. package/dist/core/navigate.d.ts +52 -0
  36. package/dist/core/navigate.js +102 -0
  37. package/dist/core/navigate.js.map +1 -0
  38. package/dist/core/paths.d.ts +12 -0
  39. package/dist/core/paths.js +30 -0
  40. package/dist/core/paths.js.map +1 -0
  41. package/dist/core/profile-manager.d.ts +13 -0
  42. package/dist/core/profile-manager.js +44 -0
  43. package/dist/core/profile-manager.js.map +1 -0
  44. package/dist/core/provider.d.ts +64 -0
  45. package/dist/core/provider.js +31 -0
  46. package/dist/core/provider.js.map +1 -0
  47. package/dist/core/response-watcher.d.ts +35 -0
  48. package/dist/core/response-watcher.js +70 -0
  49. package/dist/core/response-watcher.js.map +1 -0
  50. package/dist/core/snapshot.d.ts +38 -0
  51. package/dist/core/snapshot.js +137 -0
  52. package/dist/core/snapshot.js.map +1 -0
  53. package/dist/core/sse-parser.d.ts +20 -0
  54. package/dist/core/sse-parser.js +49 -0
  55. package/dist/core/sse-parser.js.map +1 -0
  56. package/dist/index.d.ts +33 -0
  57. package/dist/index.js +40 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/providers/chatgpt-sse-collector.d.ts +76 -0
  60. package/dist/providers/chatgpt-sse-collector.js +298 -0
  61. package/dist/providers/chatgpt-sse-collector.js.map +1 -0
  62. package/dist/providers/chatgpt.d.ts +56 -0
  63. package/dist/providers/chatgpt.js +357 -0
  64. package/dist/providers/chatgpt.js.map +1 -0
  65. package/dist/providers/deepseek.d.ts +22 -0
  66. package/dist/providers/deepseek.js +153 -0
  67. package/dist/providers/deepseek.js.map +1 -0
  68. package/dist/providers/gemini.d.ts +102 -0
  69. package/dist/providers/gemini.js +480 -0
  70. package/dist/providers/gemini.js.map +1 -0
  71. package/dist/providers/index.d.ts +8 -0
  72. package/dist/providers/index.js +17 -0
  73. package/dist/providers/index.js.map +1 -0
  74. package/dist/session.d.ts +121 -0
  75. package/dist/session.js +242 -0
  76. package/dist/session.js.map +1 -0
  77. package/package.json +47 -0
@@ -0,0 +1,357 @@
1
+ import { InputNotFoundError, 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
+ import { ChatGPTSSECollector } from "./chatgpt-sse-collector.js";
6
+ /**
7
+ * ChatGPT (chatgpt.com) provider adapter.
8
+ *
9
+ * Selectors are intentionally written as fallbacks (RFC §8) — when one
10
+ * breaks we fall back to the next. The strongest contracts we rely on:
11
+ * - the prompt input is a textarea or contenteditable in the viewport
12
+ * - assistant messages carry data-message-author-role="assistant"
13
+ * - while streaming, a "Stop generating" button is present
14
+ *
15
+ * Reply extraction has two paths (see `extractLastAssistantMessage`):
16
+ * 1. SSE collector — listens to `/backend-api/.../conversation` and
17
+ * reconstructs the model's original markdown (bullets, fences,
18
+ * tables, link URLs). Preferred.
19
+ * 2. DOM `innerText` fallback — used when SSE parsing returned nothing
20
+ * (endpoint changed, request errored, ...).
21
+ */
22
+ export class ChatGPTAdapter {
23
+ name = "chatgpt";
24
+ homeUrl = "https://chatgpt.com/";
25
+ /** One collector per Page. Lazily attached on `open()`. */
26
+ collectors = new WeakMap();
27
+ /**
28
+ * Promise that resolves to the assistant markdown for the currently
29
+ * in-flight turn (between `sendMessage` and `waitForResponse`).
30
+ * Per-page so concurrent pages don't collide.
31
+ */
32
+ pendingTurns = new WeakMap();
33
+ async open(page) {
34
+ // Install the SSE collector BEFORE navigating, so we capture any
35
+ // initial streaming responses (e.g. if the previous conversation was
36
+ // mid-stream when we attached). Idempotent across re-opens.
37
+ this.ensureCollector(page);
38
+ // gotoWithRetry uses waitUntil:"commit" + exponential-backoff retries.
39
+ // ChatGPT loads a lot from CDNs; relying on `load`/`domcontentloaded`
40
+ // wedges the open() on slow / proxied networks. The composer wait
41
+ // below is the real readiness check.
42
+ await gotoOrThrowNetworkError(page, this.homeUrl, this.name);
43
+ await this.waitForComposerReady(page).catch(() => undefined);
44
+ }
45
+ async isLoggedIn(page) {
46
+ // The signed-in state always exposes the ProseMirror composer.
47
+ // We probe the strongest identifier first (id=prompt-textarea), then
48
+ // softer fallbacks for layout changes.
49
+ const candidates = [
50
+ '#prompt-textarea[contenteditable="true"]',
51
+ 'div[role="textbox"][contenteditable="true"]',
52
+ '[contenteditable="true"]',
53
+ ];
54
+ for (const sel of candidates) {
55
+ const loc = page.locator(sel).first();
56
+ const visible = await loc.isVisible().catch(() => false);
57
+ if (visible)
58
+ return true;
59
+ }
60
+ return false;
61
+ }
62
+ async findInput(page) {
63
+ // IMPORTANT: do NOT include `textarea` as a candidate. ChatGPT renders
64
+ // a `<textarea class="wcDTda_fallbackTextarea">` sibling to the
65
+ // ProseMirror editor for accessibility, but clicking it is intercepted
66
+ // by the ProseMirror placeholder `<p>`, leading to a 30s timeout.
67
+ const candidates = [
68
+ page.locator('#prompt-textarea[contenteditable="true"]').first(),
69
+ page.locator('div[role="textbox"][contenteditable="true"]').first(),
70
+ page.locator('[contenteditable="true"]').first(),
71
+ ];
72
+ for (const locator of candidates) {
73
+ if (await locator.isVisible().catch(() => false)) {
74
+ return locator;
75
+ }
76
+ }
77
+ throw new InputNotFoundError(this.name);
78
+ }
79
+ async findSendButton(page) {
80
+ const candidates = [
81
+ // Most stable: the explicit id ChatGPT uses on the composer submit.
82
+ page.locator("button#composer-submit-button").first(),
83
+ page.locator('button[aria-label="Send prompt"]').first(),
84
+ page.locator('button[data-testid="send-button"]').first(),
85
+ page.locator('button[aria-label*="Send" i]').first(),
86
+ page.locator('button[aria-label*="发送" i]').first(),
87
+ page.locator('form button[type="submit"]').first(),
88
+ ];
89
+ for (const locator of candidates) {
90
+ if (await locator.isVisible().catch(() => false)) {
91
+ return locator;
92
+ }
93
+ }
94
+ return null;
95
+ }
96
+ async sendMessage(page, message) {
97
+ const input = await this.findInput(page);
98
+ // Arm the SSE collector BEFORE we submit so we don't miss the first
99
+ // delta frames. The promise is stashed per-page and consumed by
100
+ // `extractLastAssistantMessage`/`waitForResponse`.
101
+ const collector = this.ensureCollector(page);
102
+ this.pendingTurns.set(page, collector.beginTurn());
103
+ // ProseMirror: click to focus (force:true bypasses the placeholder
104
+ // intercept check — the placeholder lives inside the editor, so a
105
+ // click through it still lands in the right place), then type via the
106
+ // keyboard so the editor's input handlers fire. `locator.fill` is a
107
+ // no-op on contenteditable and `insertText` skips IME handling.
108
+ await input.click({ force: true });
109
+ await input.focus().catch(() => undefined);
110
+ // CRITICAL: use `typeMultiline`, not `page.keyboard.type(message)`.
111
+ // ChatGPT's ProseMirror binds Enter to "submit prompt"; `keyboard.type`
112
+ // emits a literal Enter key event for every "\n" in the message, which
113
+ // causes multi-line prompts to be submitted line-by-line. The helper
114
+ // converts newlines into Shift+Enter (soft line break) presses.
115
+ await typeMultiline(page, message);
116
+ // Prefer clicking the send button — it gives us a cleaner state
117
+ // transition (the button toggles to "Stop generating" while streaming).
118
+ const send = await this.findSendButton(page);
119
+ if (send && (await send.isEnabled().catch(() => false))) {
120
+ await send.click({ force: true }).catch(async () => {
121
+ await page.keyboard.press("Enter");
122
+ });
123
+ }
124
+ else {
125
+ await page.keyboard.press("Enter");
126
+ }
127
+ }
128
+ /** Wait up to `timeoutMs` for the ProseMirror composer to be ready. */
129
+ async waitForComposerReady(page, timeoutMs = 15_000) {
130
+ const sel = '#prompt-textarea[contenteditable="true"], div[role="textbox"][contenteditable="true"]';
131
+ await page.locator(sel).first().waitFor({ state: "visible", timeout: timeoutMs });
132
+ }
133
+ async extractLastAssistantMessage(page) {
134
+ // Path 1: SSE collector. When wiring is healthy this gives us the
135
+ // model's original markdown (code fences, list prefixes, link URLs).
136
+ //
137
+ // IMPORTANT: use `getCurrentTurnText`, not `getLastAssistantText`.
138
+ // The latter used to return the most-recent *finalized* turn's text,
139
+ // which silently leaked the previous turn's answer when the current
140
+ // turn's SSE was empty / racy (task 09b34cf4-… symptom: "1+1=2" reply
141
+ // showing up under a long arXiv prompt).
142
+ const collector = this.collectors.get(page);
143
+ if (collector) {
144
+ const sseText = collector.getCurrentTurnText();
145
+ if (sseText && sseText.trim())
146
+ return sseText.trim();
147
+ }
148
+ // Path 2: DOM innerText fallback. Loses markdown structure but is the
149
+ // safety net when the streaming endpoint changes / is unreachable /
150
+ // hasn't finished yet.
151
+ const candidates = [
152
+ page.locator('[data-message-author-role="assistant"]').last(),
153
+ page.locator("main article").last(),
154
+ page.locator(".markdown, .prose").last(),
155
+ ];
156
+ for (const locator of candidates) {
157
+ if (await locator.count().then((n) => n > 0).catch(() => false)) {
158
+ const text = await locator.innerText().catch(() => "");
159
+ if (text && text.trim())
160
+ return text.trim();
161
+ }
162
+ }
163
+ throw new ResponseExtractionError(this.name);
164
+ }
165
+ async waitForResponse(page, options = {}) {
166
+ const timeoutMs = options.timeoutMs ?? 120_000;
167
+ const stableMs = options.stableMs ?? 2_000;
168
+ const deadline = Date.now() + timeoutMs;
169
+ // Path A: if `sendMessage` armed the SSE collector, the most reliable
170
+ // "done" signal is the SSE response finishing. It also returns the
171
+ // model's original markdown, which is what we ultimately want.
172
+ const pending = this.pendingTurns.get(page);
173
+ if (pending) {
174
+ this.pendingTurns.delete(page);
175
+ const winner = await Promise.race([
176
+ pending.then((text) => ({ kind: "sse", text })),
177
+ new Promise((resolve) => setTimeout(() => resolve({ kind: "timeout" }), timeoutMs)),
178
+ ]);
179
+ if (winner.kind === "sse" && winner.text && winner.text.trim()) {
180
+ return winner.text.trim();
181
+ }
182
+ // Either timeout or empty SSE → fall through to the DOM fallback.
183
+ }
184
+ // Path B (fallback): RFC §10 — use the stop-button toggle + text
185
+ // stability. We get here when either the SSE wasn't armed (e.g. an
186
+ // older Page that bypassed `sendMessage`) or the SSE returned empty.
187
+ // Phase 1: wait for streaming to actually begin (stop button appears).
188
+ await this.waitForStopButton(page, { timeoutMs: 15_000 }).catch(() => undefined);
189
+ // Phase 2: wait until the stop button is gone (= generation ended).
190
+ const remaining = Math.max(deadline - Date.now(), 5_000);
191
+ await this.waitForStopButtonGone(page, remaining).catch(() => undefined);
192
+ // Phase 3: text-stability sanity check, with the rest of the budget.
193
+ const stable = await waitUntilStable(async () => {
194
+ const text = await this.extractLastAssistantMessage(page).catch(() => "");
195
+ if (isThinkingPlaceholder(text))
196
+ return "";
197
+ return text;
198
+ }, {
199
+ timeoutMs: Math.max(deadline - Date.now(), 1_500),
200
+ stableMs,
201
+ signal: options.signal,
202
+ onProgress: options.onProgress,
203
+ });
204
+ if (!stable) {
205
+ throw new ResponseTimeoutError(this.name, timeoutMs);
206
+ }
207
+ return stable;
208
+ }
209
+ /**
210
+ * Extract the ChatGPT conversation UUID from the page URL.
211
+ *
212
+ * Once a turn lands, ChatGPT navigates the page to
213
+ * `https://chatgpt.com/c/{conversation-uuid}`
214
+ * The fresh-chat URL (`/` or `/?model=...`) has no UUID — return `null`
215
+ * in that case. The UUID is the natural cross-process session id and is
216
+ * what we plumb into ai-sdk's session model and onward to the UI.
217
+ */
218
+ getConversationId(page) {
219
+ const url = page.url();
220
+ // The path can have a trailing slash, query string, or hash; pull
221
+ // out exactly the path segment after `/c/`.
222
+ const match = url.match(/\/c\/([0-9a-fA-F-]{8,})/);
223
+ return match ? match[1] : null;
224
+ }
225
+ async newChat(page) {
226
+ const candidates = [
227
+ page.locator('a[href="/"]').first(),
228
+ page.getByRole("button", { name: /new chat/i }).first(),
229
+ page.getByRole("link", { name: /new chat/i }).first(),
230
+ ];
231
+ for (const locator of candidates) {
232
+ if (await locator.isVisible().catch(() => false)) {
233
+ await locator.click().catch(() => undefined);
234
+ return;
235
+ }
236
+ }
237
+ // Fallback: just navigate to the home URL.
238
+ await this.open(page);
239
+ }
240
+ async diagnose(page) {
241
+ const [loggedIn, sendBtn, assistantCount, stopFound] = await Promise.all([
242
+ this.isLoggedIn(page),
243
+ this.findSendButton(page).then((l) => l !== null),
244
+ page.locator('[data-message-author-role="assistant"]').count().catch(() => 0),
245
+ page
246
+ .locator('button[aria-label*="Stop" i], button[aria-label*="停止" i]')
247
+ .first()
248
+ .isVisible()
249
+ .catch(() => false),
250
+ ]);
251
+ let inputFound = false;
252
+ try {
253
+ await this.findInput(page);
254
+ inputFound = true;
255
+ }
256
+ catch {
257
+ inputFound = false;
258
+ }
259
+ let lastAssistantLength = 0;
260
+ try {
261
+ const text = await this.extractLastAssistantMessage(page);
262
+ lastAssistantLength = text.length;
263
+ }
264
+ catch {
265
+ lastAssistantLength = 0;
266
+ }
267
+ return {
268
+ loggedIn,
269
+ inputFound,
270
+ sendButtonFound: sendBtn,
271
+ assistantMessageCount: assistantCount,
272
+ stopButtonFound: stopFound,
273
+ lastAssistantLength,
274
+ pageUrl: page.url(),
275
+ };
276
+ }
277
+ /** Look up (and lazily create + attach) the SSE collector for a Page. */
278
+ ensureCollector(page) {
279
+ let c = this.collectors.get(page);
280
+ if (!c) {
281
+ c = new ChatGPTSSECollector();
282
+ c.attach(page);
283
+ this.collectors.set(page, c);
284
+ // Drop our reference when the page closes so the collector can GC.
285
+ page.once("close", () => {
286
+ c?.dispose();
287
+ this.collectors.delete(page);
288
+ this.pendingTurns.delete(page);
289
+ });
290
+ }
291
+ return c;
292
+ }
293
+ async waitForStopButton(page, options = {}) {
294
+ const timeoutMs = options.timeoutMs ?? 15_000;
295
+ const deadline = Date.now() + timeoutMs;
296
+ while (Date.now() < deadline) {
297
+ if (await this.isStopButtonVisible(page))
298
+ return;
299
+ await sleep(150);
300
+ }
301
+ }
302
+ async waitForStopButtonGone(page, timeoutMs = 5_000) {
303
+ const deadline = Date.now() + timeoutMs;
304
+ let consecutiveGone = 0;
305
+ while (Date.now() < deadline) {
306
+ const visible = await this.isStopButtonVisible(page);
307
+ if (!visible) {
308
+ // Require a few consecutive "gone" samples to avoid catching a
309
+ // brief click-debounce gap during the chain-of-thought / answer
310
+ // handoff that happens on reasoning models.
311
+ consecutiveGone += 1;
312
+ if (consecutiveGone >= 3)
313
+ return;
314
+ }
315
+ else {
316
+ consecutiveGone = 0;
317
+ }
318
+ await sleep(200);
319
+ }
320
+ }
321
+ async isStopButtonVisible(page) {
322
+ // ChatGPT exposes the stop-generating control as the composer-submit
323
+ // button in "stop" mode. The aria-label changes ("Stop generating",
324
+ // "Stop streaming", "停止生成", ...). We probe the data-* fingerprint
325
+ // first, then fall back to aria-label heuristics.
326
+ const selectors = [
327
+ "button#composer-submit-button[data-state=stop]",
328
+ 'button[data-testid="stop-button"]',
329
+ 'button[aria-label*="Stop" i]',
330
+ 'button[aria-label*="停止" i]',
331
+ ];
332
+ for (const sel of selectors) {
333
+ const visible = await page.locator(sel).first().isVisible().catch(() => false);
334
+ if (visible)
335
+ return true;
336
+ }
337
+ return false;
338
+ }
339
+ }
340
+ function isThinkingPlaceholder(text) {
341
+ const t = text.trim();
342
+ if (!t)
343
+ return true;
344
+ if (t.length > 80)
345
+ return false;
346
+ // Lower-cased prefix match covers ChatGPT's reasoning placeholders.
347
+ const lower = t.toLowerCase();
348
+ return (lower === "thinking" ||
349
+ lower === "thinking…" ||
350
+ lower === "thinking..." ||
351
+ lower.startsWith("thinking\n") ||
352
+ lower.startsWith("thought for ") ||
353
+ lower.startsWith("reasoning") ||
354
+ lower.startsWith("思考") ||
355
+ lower.startsWith("正在思考"));
356
+ }
357
+ //# sourceMappingURL=chatgpt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chatgpt.js","sourceRoot":"","sources":["../../src/providers/chatgpt.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import type { Locator, Page } from "playwright";
2
+ import type { ChatProvider, ProviderDiagnostics, WaitOptions } from "../core/provider.js";
3
+ /**
4
+ * DeepSeek (chat.deepseek.com) provider adapter.
5
+ *
6
+ * Per RFC §13.2 this is a sibling of ChatGPTAdapter, not a subclass —
7
+ * they implement the same interface but the selectors and login probes
8
+ * are different enough that sharing logic would be a footgun.
9
+ */
10
+ export declare class DeepSeekAdapter implements ChatProvider {
11
+ readonly name = "deepseek";
12
+ readonly homeUrl = "https://chat.deepseek.com/";
13
+ open(page: Page): Promise<void>;
14
+ isLoggedIn(page: Page): Promise<boolean>;
15
+ findInput(page: Page): Promise<Locator>;
16
+ findSendButton(page: Page): Promise<Locator | null>;
17
+ sendMessage(page: Page, message: string): Promise<void>;
18
+ extractLastAssistantMessage(page: Page): Promise<string>;
19
+ waitForResponse(page: Page, options?: WaitOptions): Promise<string>;
20
+ newChat(page: Page): Promise<void>;
21
+ diagnose(page: Page): Promise<ProviderDiagnostics>;
22
+ }
@@ -0,0 +1,153 @@
1
+ import { InputNotFoundError, ResponseExtractionError, ResponseTimeoutError, } from "../core/errors.js";
2
+ import { sleep, waitUntilStable } from "../core/response-watcher.js";
3
+ /**
4
+ * DeepSeek (chat.deepseek.com) provider adapter.
5
+ *
6
+ * Per RFC §13.2 this is a sibling of ChatGPTAdapter, not a subclass —
7
+ * they implement the same interface but the selectors and login probes
8
+ * are different enough that sharing logic would be a footgun.
9
+ */
10
+ export class DeepSeekAdapter {
11
+ name = "deepseek";
12
+ homeUrl = "https://chat.deepseek.com/";
13
+ async open(page) {
14
+ await page.goto(this.homeUrl, { waitUntil: "domcontentloaded" }).catch(() => {
15
+ return page.goto(this.homeUrl, { waitUntil: "load", timeout: 30_000 });
16
+ });
17
+ }
18
+ async isLoggedIn(page) {
19
+ // DeepSeek's signed-out state shows a phone/login form. The signed-in
20
+ // state shows the chat composer (textarea) immediately.
21
+ const composer = page
22
+ .locator('textarea[placeholder*="DeepSeek" i], textarea[placeholder*="给 DeepSeek" i], textarea')
23
+ .first();
24
+ return await composer.isVisible().catch(() => false);
25
+ }
26
+ async findInput(page) {
27
+ const candidates = [
28
+ page.locator('textarea[placeholder*="DeepSeek" i]').first(),
29
+ page.locator('textarea[placeholder*="给 DeepSeek" i]').first(),
30
+ page.locator('textarea').first(),
31
+ page.locator('[contenteditable="true"]').first(),
32
+ page.locator('[role="textbox"]').first(),
33
+ ];
34
+ for (const locator of candidates) {
35
+ if (await locator.isVisible().catch(() => false)) {
36
+ return locator;
37
+ }
38
+ }
39
+ throw new InputNotFoundError(this.name);
40
+ }
41
+ async findSendButton(page) {
42
+ const candidates = [
43
+ page.locator('div[role="button"][aria-disabled="false"]').last(),
44
+ page.locator('button[aria-label*="send" i]').first(),
45
+ page.locator('button[aria-label*="发送" i]').first(),
46
+ page.locator('button:has(svg)').last(),
47
+ ];
48
+ for (const locator of candidates) {
49
+ if (await locator.isVisible().catch(() => false)) {
50
+ return locator;
51
+ }
52
+ }
53
+ return null;
54
+ }
55
+ async sendMessage(page, message) {
56
+ const input = await this.findInput(page);
57
+ await input.click();
58
+ const filled = await input.fill(message).then(() => true, () => false);
59
+ if (!filled) {
60
+ await page.keyboard.insertText(message);
61
+ }
62
+ // DeepSeek accepts Enter for submission; the dedicated send button
63
+ // sometimes only enables after a small debounce, so Enter is more
64
+ // reliable as the primary path.
65
+ await page.keyboard.press("Enter");
66
+ }
67
+ async extractLastAssistantMessage(page) {
68
+ const candidates = [
69
+ // DeepSeek doesn't expose data-message-author-role consistently,
70
+ // so we walk the message list and pick the last non-user message.
71
+ page.locator('div[class*="message"][class*="assistant" i]').last(),
72
+ page.locator('.markdown, .ds-markdown, .prose').last(),
73
+ page.locator('main article').last(),
74
+ ];
75
+ for (const locator of candidates) {
76
+ if (await locator.count().then((n) => n > 0).catch(() => false)) {
77
+ const text = await locator.innerText().catch(() => "");
78
+ if (text && text.trim())
79
+ return text.trim();
80
+ }
81
+ }
82
+ throw new ResponseExtractionError(this.name);
83
+ }
84
+ async waitForResponse(page, options = {}) {
85
+ const timeoutMs = options.timeoutMs ?? 120_000;
86
+ const stableMs = options.stableMs ?? 2_000;
87
+ await sleep(500);
88
+ const stable = await waitUntilStable(() => this.extractLastAssistantMessage(page).catch(() => ""), {
89
+ timeoutMs,
90
+ stableMs,
91
+ signal: options.signal,
92
+ onProgress: options.onProgress,
93
+ });
94
+ if (!stable) {
95
+ throw new ResponseTimeoutError(this.name, timeoutMs);
96
+ }
97
+ return stable;
98
+ }
99
+ async newChat(page) {
100
+ const candidates = [
101
+ page.getByRole("button", { name: /new chat|新对话|新建对话/i }).first(),
102
+ page.getByRole("link", { name: /new chat|新对话/i }).first(),
103
+ ];
104
+ for (const locator of candidates) {
105
+ if (await locator.isVisible().catch(() => false)) {
106
+ await locator.click().catch(() => undefined);
107
+ return;
108
+ }
109
+ }
110
+ await this.open(page);
111
+ }
112
+ async diagnose(page) {
113
+ const [loggedIn, sendBtn] = await Promise.all([
114
+ this.isLoggedIn(page),
115
+ this.findSendButton(page).then((l) => l !== null),
116
+ ]);
117
+ let inputFound = false;
118
+ try {
119
+ await this.findInput(page);
120
+ inputFound = true;
121
+ }
122
+ catch {
123
+ inputFound = false;
124
+ }
125
+ let assistantMessageCount = 0;
126
+ try {
127
+ assistantMessageCount = await page
128
+ .locator('.markdown, .ds-markdown, .prose')
129
+ .count();
130
+ }
131
+ catch {
132
+ assistantMessageCount = 0;
133
+ }
134
+ let lastAssistantLength = 0;
135
+ try {
136
+ const text = await this.extractLastAssistantMessage(page);
137
+ lastAssistantLength = text.length;
138
+ }
139
+ catch {
140
+ lastAssistantLength = 0;
141
+ }
142
+ return {
143
+ loggedIn,
144
+ inputFound,
145
+ sendButtonFound: sendBtn,
146
+ assistantMessageCount,
147
+ stopButtonFound: false,
148
+ lastAssistantLength,
149
+ pageUrl: page.url(),
150
+ };
151
+ }
152
+ }
153
+ //# sourceMappingURL=deepseek.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deepseek.js","sourceRoot":"","sources":["../../src/providers/deepseek.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,102 @@
1
+ import type { Locator, Page } from "playwright";
2
+ import type { ChatProvider, ProviderDiagnostics, WaitOptions } from "../core/provider.js";
3
+ /**
4
+ * Google AI Studio (aistudio.google.com/prompts/new_chat) provider
5
+ * adapter.
6
+ *
7
+ * This is what users colloquially mean by "Gemini" — the free web-chat
8
+ * interface that lets you talk to Gemini models without an API key
9
+ * (the "No API key selected" button on the page is a separate, optional
10
+ * feature for users who want to use their own key; the page is fully
11
+ * usable without it). chat-web does NOT target gemini.google.com — in
12
+ * practice many networks block that domain while AI Studio reaches
13
+ * fine, and AI Studio is the surface users actually expect.
14
+ *
15
+ * Observed structure (2026-05):
16
+ * - Composer: `<ms-prompt-box>` (Angular Material) wrapping a textarea
17
+ * with `aria="Enter a prompt"`, `placeholder="Start typing..."`.
18
+ * - Run button: NO aria-label; visible text is "Run" + two Material
19
+ * icon font ligatures (`keyboard_command_key`, `keyboard_return`).
20
+ * `type="submit"`. Use `getByRole("button", { name: /^Run$/ })`.
21
+ * - Stop: the same submit button flips to a stop state; its
22
+ * aria-label changes to include "Stop". Detect via aria-label
23
+ * substring.
24
+ * - Assistant turn: `<ms-chat-turn>` containing a
25
+ * `.chat-turn-container.model` with `.turn-content`. innerText
26
+ * leaks "Model HH:MM AM/PM" headers and Material icon font
27
+ * ligatures (`edit`, `more_vert`, `error`, ...) as plain text —
28
+ * stripped by {@link stripChromeFromTurn}.
29
+ * - Conversation id: `/prompts/{slug}` once the prompt is saved; the
30
+ * placeholder `/prompts/new_chat` does NOT count.
31
+ */
32
+ export declare class GeminiAdapter implements ChatProvider {
33
+ readonly name = "gemini";
34
+ readonly homeUrl = "https://aistudio.google.com/prompts/new_chat";
35
+ open(page: Page): Promise<void>;
36
+ isLoggedIn(page: Page): Promise<boolean>;
37
+ findInput(page: Page): Promise<Locator>;
38
+ findSendButton(page: Page): Promise<Locator | null>;
39
+ sendMessage(page: Page, message: string): Promise<void>;
40
+ /**
41
+ * AI Studio shows a "Grounding with Google Search" chip on every new
42
+ * chat by default. The chip has a close (×) button with
43
+ * `aria-label="Remove Grounding with Google Search"`. Clicking it
44
+ * disables the grounding step for this conversation — and removes a
45
+ * silent hang point on networks that can't reach Google Search.
46
+ */
47
+ private disableGroundingWithGoogleSearch;
48
+ extractLastAssistantMessage(page: Page): Promise<string>;
49
+ waitForResponse(page: Page, options?: WaitOptions): Promise<string>;
50
+ /**
51
+ * Inspect the freshly-extracted "assistant" text and raise a typed
52
+ * error if it's really an upstream failure that AI Studio just
53
+ * rendered in the model turn slot.
54
+ *
55
+ * Known patterns:
56
+ * - "An internal error has occurred." → typically quota /
57
+ * server-side error (the user has reproduced this when their
58
+ * free-tier daily quota was exhausted).
59
+ * - "Failed to generate content: permission denied." → API key
60
+ * missing or model not enabled on the account.
61
+ *
62
+ * The actual root cause for "internal error" varies (preview model
63
+ * outage, daily quota cap, region restriction, transient backend
64
+ * error). We don't try to disambiguate further — the hint just lists
65
+ * the common ones so the user knows where to look.
66
+ */
67
+ private throwIfKnownUpstreamError;
68
+ private detectFailureCause;
69
+ /**
70
+ * Heuristic: are we stuck in the WAA-blocked state where AI Studio
71
+ * never moves past "Thinking" and the URL still says new_chat? Used
72
+ * to surface a clearer error than a generic timeout.
73
+ */
74
+ private looksAutomationBlocked;
75
+ /**
76
+ * AI Studio's per-prompt URL is `/prompts/{slug}` after the prompt is
77
+ * saved. Until then the URL stays at `/prompts/new_chat` (no
78
+ * conversation id).
79
+ */
80
+ getConversationId(page: Page): string | null;
81
+ newChat(page: Page): Promise<void>;
82
+ diagnose(page: Page): Promise<ProviderDiagnostics>;
83
+ private pressRunShortcut;
84
+ private waitForComposerReady;
85
+ private waitForStopButton;
86
+ private waitForStopButtonGone;
87
+ private isStopButtonVisible;
88
+ }
89
+ /**
90
+ * Strip the UI chrome that leaks into `innerText` on AI Studio model
91
+ * turns. Removes:
92
+ * - The "Model HH:MM AM/PM" turn header
93
+ * - Single-line Material icon font ligatures (rendered as plain text
94
+ * words because the icon-font substitution doesn't affect text
95
+ * extraction): edit, more_vert, error, content_copy, thumb_up,
96
+ * thumb_down, refresh, delete, close, expand_more, expand_less,
97
+ * code, play_arrow, stop, menu.
98
+ *
99
+ * Does NOT remove lines where the same word appears INSIDE a sentence
100
+ * (e.g. "an error occurred during parsing" stays as-is).
101
+ */
102
+ export declare function stripChromeFromTurn(text: string): string;