wtagent 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +2 -2
- package/src/browser/base-web-adapter.js +4 -34
- package/src/browser/chatgpt-web-adapter.js +4 -217
- package/src/browser/deepseek-web-adapter.js +0 -106
- package/src/browser/glm-web-adapter.js +2 -89
- package/src/browser/grok-web-adapter.js +145 -0
- package/src/browser/kimi-web-adapter.js +2 -74
- package/src/browser/provider-registry.js +8 -50
- package/src/cli/i18n.js +949 -0
- package/src/cli/main.js +125 -186
- package/src/cli/prompt-input.js +349 -151
- package/src/cli/render-events.js +45 -35
- package/src/cli/self-update.js +10 -3
- package/src/runtime/agent-runtime.js +14 -51
- package/src/shared/limits.js +2 -9
- package/src/browser/mode-selection.js +0 -214
- package/src/cli/mode-choice.js +0 -25
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Use your web AI account as a local CLI agent.
|
|
4
4
|
|
|
5
|
-
WTAgent connects ChatGPT, Claude, DeepSeek, Gemini, Kimi, or GLM Web to your local project: the model reasons in the browser while WTAgent reads and writes local files and runs commands on your machine.
|
|
5
|
+
WTAgent connects ChatGPT, Claude, DeepSeek, Gemini, Grok, Kimi, or GLM Web to your local project: the model reasons in the browser while WTAgent reads and writes local files and runs commands on your machine.
|
|
6
6
|
|
|
7
7
|
[中文](#中文)
|
|
8
8
|
|
|
@@ -57,7 +57,7 @@ Multiline paste and `↑` / `↓` input history are supported. Press `Ctrl+C` or
|
|
|
57
57
|
|
|
58
58
|
## 中文
|
|
59
59
|
|
|
60
|
-
WTAgent 将 ChatGPT、Claude、DeepSeek、Gemini、Kimi 或 GLM 网页聊天连接到本地项目:模型在浏览器中思考,WTAgent 在你的电脑上读写本地文件并运行命令。
|
|
60
|
+
WTAgent 将 ChatGPT、Claude、DeepSeek、Gemini、Grok、Kimi 或 GLM 网页聊天连接到本地项目:模型在浏览器中思考,WTAgent 在你的电脑上读写本地文件并运行命令。
|
|
61
61
|
|
|
62
62
|
### 快速开始
|
|
63
63
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wtagent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Turn your own web AI session into a local tool-using agent.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
],
|
|
31
31
|
"scripts": {
|
|
32
32
|
"test": "node --test",
|
|
33
|
-
"test:unit": "node --test test/protocol.test.js test/policy.test.js test/tools.test.js test/prompt-builder.test.js test/session-export.test.js test/platform.test.js test/cli.test.js test/at-files.test.js test/self-update.test.js test/startup-notices.test.js test/provider-registry.test.js test/claude-adapter.test.js test/deepseek-adapter.test.js test/gemini-adapter.test.js test/kimi-adapter.test.js test/glm-adapter.test.js",
|
|
33
|
+
"test:unit": "node --test test/protocol.test.js test/policy.test.js test/tools.test.js test/prompt-builder.test.js test/session-export.test.js test/platform.test.js test/cli.test.js test/at-files.test.js test/self-update.test.js test/startup-notices.test.js test/provider-registry.test.js test/claude-adapter.test.js test/deepseek-adapter.test.js test/gemini-adapter.test.js test/grok-adapter.test.js test/kimi-adapter.test.js test/glm-adapter.test.js",
|
|
34
34
|
"test:integration": "node --test test/runtime.test.js",
|
|
35
35
|
"test:windows": "node --test test/windows-command-launcher.test.js test/windows-diagnostics.test.js test/windows-search-path.test.js test/atomic-write.test.js test/process-utils.test.js test/cdp-state.test.js test/windows-fixtures.test.js test/windows-package-smoke.test.js",
|
|
36
36
|
"check": "node scripts/check-syntax.js",
|
|
@@ -5,7 +5,6 @@ import { launchAndConnectCdpChrome } from "./cdp-browser.js";
|
|
|
5
5
|
import { discoverChromeExecutable } from "../platform/chrome-discovery.js";
|
|
6
6
|
import { ensureDirectory } from "../platform/paths.js";
|
|
7
7
|
import { BrowserAdapterError } from "../shared/errors.js";
|
|
8
|
-
import { runModeSelection } from "./mode-selection.js";
|
|
9
8
|
|
|
10
9
|
// Playwright error messages for a dead transport. The Chrome process itself is
|
|
11
10
|
// usually still alive (e.g. the connection died while the Mac slept); these
|
|
@@ -76,9 +75,10 @@ function sameConversationUrl(left, right) {
|
|
|
76
75
|
// The runtime talks to this class only through its public methods; everything
|
|
77
76
|
// that varies between providers (ChatGPT, DeepSeek, …) is isolated behind the
|
|
78
77
|
// overridable "primitive" methods below. A concrete provider subclass supplies
|
|
79
|
-
// its base URL, DOM locators, message-identity extraction
|
|
80
|
-
//
|
|
81
|
-
//
|
|
78
|
+
// its base URL, DOM locators, and message-identity extraction; it must NOT
|
|
79
|
+
// re-implement the turn-completion loop, send/auth/reconnect flow, or the
|
|
80
|
+
// WTAgent <agent_response> protocol timing. Model selection deliberately stays
|
|
81
|
+
// outside adapters: users choose their model directly on the provider website.
|
|
82
82
|
//
|
|
83
83
|
// IMPORTANT (JavaScript semantics): primitives that the base dispatches to a
|
|
84
84
|
// provider are declared as ordinary (non-#private) methods. `#private` methods
|
|
@@ -113,7 +113,6 @@ export class BaseWebAdapter {
|
|
|
113
113
|
this.assistantCountBeforeSend = 0;
|
|
114
114
|
this.sentUserTurn = null;
|
|
115
115
|
this.lastAssistantMessageId = null;
|
|
116
|
-
this.lastModeSelection = null;
|
|
117
116
|
}
|
|
118
117
|
|
|
119
118
|
// ---- provider primitives (override in subclasses) ----------------------
|
|
@@ -203,23 +202,6 @@ export class BaseWebAdapter {
|
|
|
203
202
|
return null;
|
|
204
203
|
}
|
|
205
204
|
|
|
206
|
-
// Port consumed by runModeSelection(). Default reports no switcher, so
|
|
207
|
-
// selectMode() resolves to "switcher_not_found" and never blocks a provider
|
|
208
|
-
// that has no model picker.
|
|
209
|
-
modeSelectionPort() {
|
|
210
|
-
return {
|
|
211
|
-
alreadyOnMode: async () => false,
|
|
212
|
-
hasSwitcher: async () => false,
|
|
213
|
-
openMenu: async () => {},
|
|
214
|
-
readOptions: async () => [],
|
|
215
|
-
clickOption: async () => false,
|
|
216
|
-
waitClosed: async () => false,
|
|
217
|
-
waitSelected: async () => false,
|
|
218
|
-
closeMenu: async () => {},
|
|
219
|
-
writeDiagnostics: async (label) => this.writeDiagnostics(label),
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
|
|
223
205
|
// Best-effort composer file upload. Default: nothing attached.
|
|
224
206
|
async attachFiles(_files) {
|
|
225
207
|
return { attached: [], failed: [] };
|
|
@@ -521,18 +503,6 @@ export class BaseWebAdapter {
|
|
|
521
503
|
}
|
|
522
504
|
}
|
|
523
505
|
|
|
524
|
-
async selectMode(mode) {
|
|
525
|
-
this.requirePage();
|
|
526
|
-
if (!mode) {
|
|
527
|
-
return { status: "skipped", requested: mode, attempts: 0 };
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
const port = this.modeSelectionPort();
|
|
531
|
-
const result = await runModeSelection(port, mode);
|
|
532
|
-
this.lastModeSelection = result;
|
|
533
|
-
return result;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
506
|
async getConversationUrl() {
|
|
537
507
|
this.requirePage();
|
|
538
508
|
return this.page.url();
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import { BaseWebAdapter, firstVisible, hasCompleteAgentEnvelope } from "./base-web-adapter.js";
|
|
2
2
|
import { BrowserAdapterError } from "../shared/errors.js";
|
|
3
3
|
import { isUsageLimitNotice } from "../shared/usage-limit.js";
|
|
4
|
-
import {
|
|
5
|
-
chooseModeOption,
|
|
6
|
-
labelMatchesToken,
|
|
7
|
-
slugMatchesToken,
|
|
8
|
-
normalizeToken,
|
|
9
|
-
} from "./mode-selection.js";
|
|
10
4
|
|
|
11
5
|
// Re-exported so existing importers (the runtime and browser-adapter tests)
|
|
12
6
|
// keep importing it from here even though it now lives on the provider-agnostic
|
|
@@ -21,10 +15,10 @@ function parseConversationTurn(value) {
|
|
|
21
15
|
}
|
|
22
16
|
|
|
23
17
|
// ChatGPT-specific implementation of the WebModelAdapter contract. Only the
|
|
24
|
-
// provider primitives (DOM locators, message identity,
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
18
|
+
// provider primitives (DOM locators, message identity, upload, overlay
|
|
19
|
+
// handling, usage-limit card) live here; all orchestration — launch/auth/send/
|
|
20
|
+
// reconnect and the turn-completion loop — is inherited from BaseWebAdapter.
|
|
21
|
+
// Model choice is intentionally left to the user in the ChatGPT website.
|
|
28
22
|
export class ChatGPTWebAdapter extends BaseWebAdapter {
|
|
29
23
|
constructor(options = {}) {
|
|
30
24
|
super({
|
|
@@ -337,211 +331,4 @@ export class ChatGPTWebAdapter extends BaseWebAdapter {
|
|
|
337
331
|
}).catch(() => null);
|
|
338
332
|
}
|
|
339
333
|
|
|
340
|
-
// DOM port for mode-selection.js. Stable attribute slugs are preferred;
|
|
341
|
-
// exact visible labels are a fallback for current menus that omit those
|
|
342
|
-
// attributes. Disabled state is still read from DOM state, never from text.
|
|
343
|
-
modeSelectionPort() {
|
|
344
|
-
const page = this.page;
|
|
345
|
-
return {
|
|
346
|
-
alreadyOnMode: async (requested) => {
|
|
347
|
-
const token = normalizeToken(requested);
|
|
348
|
-
const switcher = await this.#findModelSwitcher();
|
|
349
|
-
const switcherLabel = switcher
|
|
350
|
-
? await switcher.innerText().catch(() => "")
|
|
351
|
-
: "";
|
|
352
|
-
if (labelMatchesToken(switcherLabel, token)) {
|
|
353
|
-
return true;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const messages = this.assistantMessages();
|
|
357
|
-
if (await messages.count() === 0) {
|
|
358
|
-
return false;
|
|
359
|
-
}
|
|
360
|
-
const slug = await messages.last()
|
|
361
|
-
.getAttribute("data-message-model-slug")
|
|
362
|
-
.catch(() => null);
|
|
363
|
-
return Boolean(slug) && slugMatchesToken(slug, token);
|
|
364
|
-
},
|
|
365
|
-
hasSwitcher: async () => Boolean(await this.#findModelSwitcher()),
|
|
366
|
-
openMenu: async (requested) => {
|
|
367
|
-
const switcher = await this.#findModelSwitcher();
|
|
368
|
-
if (switcher) {
|
|
369
|
-
const opened = await switcher.click({ timeout: 5_000 })
|
|
370
|
-
.then(() => true)
|
|
371
|
-
.catch(() => false);
|
|
372
|
-
if (!opened) {
|
|
373
|
-
await switcher.click({ force: true, timeout: 5_000 })
|
|
374
|
-
.catch(() => null);
|
|
375
|
-
}
|
|
376
|
-
// Wait for the Radix menu portal to attach before reading options.
|
|
377
|
-
await page.locator('[role="menu"]:visible [role="menuitem"]:visible, [role="menu"]:visible [role="menuitemradio"]:visible')
|
|
378
|
-
.first().waitFor({ state: "visible", timeout: 5_000 })
|
|
379
|
-
.catch(() => null);
|
|
380
|
-
await this.#revealNestedModeOption(requested);
|
|
381
|
-
}
|
|
382
|
-
},
|
|
383
|
-
readOptions: async () => this.#readModeOptions(),
|
|
384
|
-
clickOption: async (index) => {
|
|
385
|
-
const option = this.#modeOptionLocators().nth(index);
|
|
386
|
-
try {
|
|
387
|
-
await option.click({ timeout: 5_000 });
|
|
388
|
-
return true;
|
|
389
|
-
} catch {
|
|
390
|
-
return false;
|
|
391
|
-
}
|
|
392
|
-
},
|
|
393
|
-
waitClosed: async () => {
|
|
394
|
-
const ok = await page.locator('[role="menu"]').first()
|
|
395
|
-
.waitFor({ state: "hidden", timeout: 5_000 })
|
|
396
|
-
.then(() => true)
|
|
397
|
-
.catch(() => false);
|
|
398
|
-
return ok;
|
|
399
|
-
},
|
|
400
|
-
waitSelected: async (requested) => {
|
|
401
|
-
const token = normalizeToken(requested);
|
|
402
|
-
const deadline = Date.now() + 5_000;
|
|
403
|
-
while (Date.now() < deadline) {
|
|
404
|
-
const switcher = await this.#findModelSwitcher(500);
|
|
405
|
-
const label = switcher
|
|
406
|
-
? await switcher.innerText().catch(() => "")
|
|
407
|
-
: "";
|
|
408
|
-
if (labelMatchesToken(label, token)) {
|
|
409
|
-
return true;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
const selectedOption = (await this.#readModeOptions()).find(
|
|
413
|
-
(option) => option.selected
|
|
414
|
-
&& (
|
|
415
|
-
slugMatchesToken(option.slug, token)
|
|
416
|
-
|| labelMatchesToken(option.label, token)
|
|
417
|
-
),
|
|
418
|
-
);
|
|
419
|
-
if (selectedOption) {
|
|
420
|
-
return true;
|
|
421
|
-
}
|
|
422
|
-
await page.waitForTimeout(100);
|
|
423
|
-
}
|
|
424
|
-
return false;
|
|
425
|
-
},
|
|
426
|
-
closeMenu: async () => {
|
|
427
|
-
await page.keyboard.press("Escape").catch(() => null);
|
|
428
|
-
await page.waitForTimeout(150);
|
|
429
|
-
},
|
|
430
|
-
writeDiagnostics: async (label) => this.writeDiagnostics(label),
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// ---- ChatGPT-only mode-picker helpers (private) ------------------------
|
|
435
|
-
|
|
436
|
-
#modeOptionLocators() {
|
|
437
|
-
return this.page.locator(
|
|
438
|
-
'[role="menu"]:visible [role="menuitemradio"]:visible, '
|
|
439
|
-
+ '[role="menu"]:visible [role="menuitem"]:visible',
|
|
440
|
-
);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
async #revealNestedModeOption(requested) {
|
|
444
|
-
const present = (options) => {
|
|
445
|
-
const choice = chooseModeOption(options, requested);
|
|
446
|
-
return choice.status !== "unavailable";
|
|
447
|
-
};
|
|
448
|
-
|
|
449
|
-
let options = await this.#readModeOptions();
|
|
450
|
-
if (present(options)) {
|
|
451
|
-
return;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
const submenuTriggers = this.page.locator(
|
|
455
|
-
'[role="menu"]:visible [role="menuitem"][aria-haspopup="menu"]:visible',
|
|
456
|
-
);
|
|
457
|
-
const count = await submenuTriggers.count().catch(() => 0);
|
|
458
|
-
for (let index = 0; index < count; index += 1) {
|
|
459
|
-
const trigger = submenuTriggers.nth(index);
|
|
460
|
-
await trigger.dispatchEvent("pointermove", { pointerType: "mouse" })
|
|
461
|
-
.catch(() => null);
|
|
462
|
-
await this.page.waitForTimeout(250);
|
|
463
|
-
options = await this.#readModeOptions();
|
|
464
|
-
if (present(options)) {
|
|
465
|
-
return;
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// Enumerates the open menu's options into { index, slug, label, disabled }.
|
|
471
|
-
// `slug` is the first stable attribute found and may be empty in current
|
|
472
|
-
// ChatGPT menus; `disabled` is read from ARIA / data-state, never from text.
|
|
473
|
-
async #readModeOptions() {
|
|
474
|
-
const locator = this.#modeOptionLocators();
|
|
475
|
-
const count = await locator.count().catch(() => 0);
|
|
476
|
-
const options = [];
|
|
477
|
-
for (let index = 0; index < count; index += 1) {
|
|
478
|
-
const item = locator.nth(index);
|
|
479
|
-
const [testId, dataValue, dataTestValue, id, ariaDisabled, ariaChecked, dataDisabled, dataState, label] =
|
|
480
|
-
await Promise.all([
|
|
481
|
-
item.getAttribute("data-testid").catch(() => null),
|
|
482
|
-
item.getAttribute("data-value").catch(() => null),
|
|
483
|
-
item.getAttribute("data-test-value").catch(() => null),
|
|
484
|
-
item.getAttribute("id").catch(() => null),
|
|
485
|
-
item.getAttribute("aria-disabled").catch(() => null),
|
|
486
|
-
item.getAttribute("aria-checked").catch(() => null),
|
|
487
|
-
item.getAttribute("data-disabled").catch(() => null),
|
|
488
|
-
item.getAttribute("data-state").catch(() => null),
|
|
489
|
-
item.innerText().catch(() => ""),
|
|
490
|
-
]);
|
|
491
|
-
const slug = [testId, dataValue, dataTestValue, id].find(Boolean) ?? "";
|
|
492
|
-
const disabled = ariaDisabled === "true"
|
|
493
|
-
|| dataDisabled === "true"
|
|
494
|
-
|| dataDisabled === ""
|
|
495
|
-
|| dataState === "disabled"
|
|
496
|
-
|| !await item.isEnabled().catch(() => true);
|
|
497
|
-
const selected = ariaChecked === "true" || dataState === "checked";
|
|
498
|
-
options.push({
|
|
499
|
-
index,
|
|
500
|
-
slug,
|
|
501
|
-
label: (label ?? "").trim(),
|
|
502
|
-
disabled,
|
|
503
|
-
selected,
|
|
504
|
-
});
|
|
505
|
-
}
|
|
506
|
-
return options;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
async #findModelSwitcher(timeoutMs = 45_000) {
|
|
510
|
-
const deadline = Date.now() + timeoutMs;
|
|
511
|
-
|
|
512
|
-
while (Date.now() < deadline) {
|
|
513
|
-
const direct = await firstVisible([
|
|
514
|
-
this.page.locator('[data-testid="model-switcher-dropdown-button"]'),
|
|
515
|
-
this.page.locator(
|
|
516
|
-
'main button.__composer-pill[aria-haspopup="menu"]',
|
|
517
|
-
),
|
|
518
|
-
this.page.locator('button[aria-label*="model" i]'),
|
|
519
|
-
this.page.locator('button[aria-label*="模式"]'),
|
|
520
|
-
]);
|
|
521
|
-
if (direct) {
|
|
522
|
-
return direct;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
const menuButtons = this.page.locator(
|
|
526
|
-
'main button[aria-haspopup="menu"]:not([data-testid^="history-item-"])',
|
|
527
|
-
);
|
|
528
|
-
const count = await menuButtons.count();
|
|
529
|
-
for (let index = 0; index < count; index += 1) {
|
|
530
|
-
const button = menuButtons.nth(index);
|
|
531
|
-
if (!await button.isVisible().catch(() => false)) continue;
|
|
532
|
-
const text = (await button.innerText().catch(() => "")).trim();
|
|
533
|
-
const label = await button.getAttribute("aria-label").catch(() => "");
|
|
534
|
-
if (
|
|
535
|
-
/^(chatgpt|gpt(?:-[\w.]+)?|auto|instant|thinking|pro)$/i.test(text)
|
|
536
|
-
|| /model|模型|模式/i.test(label ?? "")
|
|
537
|
-
) {
|
|
538
|
-
return button;
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
await this.page.waitForTimeout(250);
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
return null;
|
|
546
|
-
}
|
|
547
334
|
}
|
|
@@ -34,112 +34,6 @@ export class DeepSeekWebAdapter extends BaseWebAdapter {
|
|
|
34
34
|
return /^\/a\/chat\/s\//;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
// DeepSeek has no ChatGPT-style model dropdown; instead a new conversation
|
|
38
|
-
// exposes mode chips (快速/专家/识图) and toggles (深度思考/智能搜索). The
|
|
39
|
-
// registry's defaultMode ("expert-thinking") asks for 专家模式 (Expert) +
|
|
40
|
-
// 深度思考 (Deep Thinking) — WTAgent's preferred DeepSeek setup, applied
|
|
41
|
-
// silently on every fresh conversation (no interactive picker). Any other
|
|
42
|
-
// requested value keeps the site's current setting.
|
|
43
|
-
//
|
|
44
|
-
// Selection is LANGUAGE-INDEPENDENT and idempotent:
|
|
45
|
-
// - the chips are role="radio" in a fixed order (fast, expert, image), so
|
|
46
|
-
// the expert chip is the second radio; visible labels are only used to
|
|
47
|
-
// double-check the position when they match a locale we know
|
|
48
|
-
// - after expert is active DeepSeek shows exactly ONE .ds-toggle-button
|
|
49
|
-
// (deep thinking); fast mode shows two (deep thinking + web search), so
|
|
50
|
-
// the toggle count itself proves the chip switch landed. Clicking the
|
|
51
|
-
// single remaining toggle needs no label at all.
|
|
52
|
-
// Selection is best-effort — a UI change never aborts the run; it reports
|
|
53
|
-
// "unresolved" and keeps the current mode, like runModeSelection.
|
|
54
|
-
async selectMode(mode) {
|
|
55
|
-
this.requirePage();
|
|
56
|
-
if (mode !== "expert-thinking") {
|
|
57
|
-
return { status: "skipped", requested: mode, attempts: 0 };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const steps = [];
|
|
61
|
-
const chip = await this.#findExpertChip();
|
|
62
|
-
if (!chip) {
|
|
63
|
-
steps.push({ ok: false, label: "expert-chip" });
|
|
64
|
-
} else {
|
|
65
|
-
steps.push(await this.#ensureChipChecked(chip, "expert-chip"));
|
|
66
|
-
if (steps[0].ok) {
|
|
67
|
-
steps.push(await this.#ensureSingleThinkingToggle());
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const failed = steps.filter((s) => !s.ok);
|
|
72
|
-
if (failed.length > 0) {
|
|
73
|
-
await this.writeDiagnostics("deepseek-mode-partial");
|
|
74
|
-
return {
|
|
75
|
-
status: "unresolved",
|
|
76
|
-
requested: mode,
|
|
77
|
-
selectedLabel: "expert + deep-thinking",
|
|
78
|
-
attempts: 1,
|
|
79
|
-
reason: `Could not confirm: ${failed.map((s) => s.label).join(", ")}.`,
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
return {
|
|
83
|
-
status: "select",
|
|
84
|
-
requested: mode,
|
|
85
|
-
selectedLabel: "expert + deep-thinking",
|
|
86
|
-
attempts: 1,
|
|
87
|
-
reason: "Selected expert mode with deep thinking.",
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Locates the expert chip without relying on its locale: try known labels
|
|
92
|
-
// first, then fall back to the fixed chip order (fast, expert, image).
|
|
93
|
-
async #findExpertChip() {
|
|
94
|
-
const radios = this.page.locator('[role="radio"]');
|
|
95
|
-
const count = await radios.count().catch(() => 0);
|
|
96
|
-
if (count === 0) {
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
const labelPattern = /专家|expert/i;
|
|
100
|
-
for (let index = 0; index < count; index += 1) {
|
|
101
|
-
const text = (await radios.nth(index).innerText().catch(() => "")).trim();
|
|
102
|
-
if (labelPattern.test(text)) {
|
|
103
|
-
return radios.nth(index);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
// Positional fallback: 快速/专家/识图 — the expert chip is the 2nd radio.
|
|
107
|
-
return count >= 2 ? radios.nth(1) : null;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Clicks a chip unless it is already aria-checked. Returns { ok, label }.
|
|
111
|
-
async #ensureChipChecked(chip, label) {
|
|
112
|
-
if (await chip.getAttribute("aria-checked").catch(() => null) === "true") {
|
|
113
|
-
return { ok: true, label };
|
|
114
|
-
}
|
|
115
|
-
await chip.click({ timeout: 5_000 }).catch(() => null);
|
|
116
|
-
await this.page.waitForTimeout(400);
|
|
117
|
-
const checked = await chip.getAttribute("aria-checked").catch(() => null);
|
|
118
|
-
return { ok: checked === "true", label };
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// In expert mode exactly ONE toggle (deep thinking) exists; in fast mode
|
|
122
|
-
// there are two. Clicking the single remaining toggle therefore never needs
|
|
123
|
-
// a label. The toggle count also verifies the chip switch actually landed.
|
|
124
|
-
async #ensureSingleThinkingToggle() {
|
|
125
|
-
const toggles = this.page.locator(".ds-toggle-button");
|
|
126
|
-
const count = await toggles.count().catch(() => 0);
|
|
127
|
-
if (count !== 1) {
|
|
128
|
-
return { ok: false, label: "deep-thinking-toggle" };
|
|
129
|
-
}
|
|
130
|
-
const toggle = toggles.nth(0);
|
|
131
|
-
const isSelected = async () => (
|
|
132
|
-
(await toggle.getAttribute("class").catch(() => "") ?? "")
|
|
133
|
-
.includes("ds-toggle-button--selected")
|
|
134
|
-
);
|
|
135
|
-
if (await isSelected()) {
|
|
136
|
-
return { ok: true, label: "deep-thinking-toggle" };
|
|
137
|
-
}
|
|
138
|
-
await toggle.click({ timeout: 5_000 }).catch(() => null);
|
|
139
|
-
await this.page.waitForTimeout(400);
|
|
140
|
-
return { ok: await isSelected(), label: "deep-thinking-toggle" };
|
|
141
|
-
}
|
|
142
|
-
|
|
143
37
|
composerLocators() {
|
|
144
38
|
return [
|
|
145
39
|
this.page.locator('textarea[name="search"]'),
|
|
@@ -5,11 +5,6 @@ export { isConnectionLostError } from "./base-web-adapter.js";
|
|
|
5
5
|
|
|
6
6
|
const GLM_URL = "https://chat.z.ai/";
|
|
7
7
|
|
|
8
|
-
// Preferred models, newest first: try GLM-5.3 when present, else GLM-5.2. The
|
|
9
|
-
// site sometimes exposes 5.3 and sometimes only 5.2, so selection walks this
|
|
10
|
-
// list and clicks the first one present in the menu.
|
|
11
|
-
const PREFERRED_MODELS = ["GLM-5.3", "GLM-5.2"];
|
|
12
|
-
|
|
13
8
|
// GLM / Z.ai (chat.z.ai) adapter.
|
|
14
9
|
//
|
|
15
10
|
// chat.z.ai is an Open WebUI (Svelte) frontend, verified against the live app:
|
|
@@ -21,8 +16,8 @@ const PREFERRED_MODELS = ["GLM-5.3", "GLM-5.2"];
|
|
|
21
16
|
// - assistant answer markdown is `.markdown-prose` / `.prose`; a "思考过程"
|
|
22
17
|
// (deep-thinking) block may precede it and is excluded when reading the reply
|
|
23
18
|
// - a live conversation URL is /c/<uuid>
|
|
24
|
-
// - model
|
|
25
|
-
//
|
|
19
|
+
// - model choice is intentionally left to the user on chat.z.ai; WTAgent does
|
|
20
|
+
// not inspect or override the site's current model selection
|
|
26
21
|
// - Cloudflare guards the site; the base throwIfBlockedPage surfaces the
|
|
27
22
|
// window so the user can pass the check (wtagent's own CDP launch is not
|
|
28
23
|
// fingerprinted the way headless automation is)
|
|
@@ -161,86 +156,4 @@ export class GLMWebAdapter extends BaseWebAdapter {
|
|
|
161
156
|
return 120;
|
|
162
157
|
}
|
|
163
158
|
|
|
164
|
-
// Selects the newest available model. The registry's defaultMode "latest" maps
|
|
165
|
-
// to PREFERRED_MODELS (GLM-5.3, else GLM-5.2). Best-effort and non-throwing.
|
|
166
|
-
//
|
|
167
|
-
// The switcher is `button.modelSelectorButton`; opening it lists options whose
|
|
168
|
-
// visible text is the exact model name. After clicking, the switcher label
|
|
169
|
-
// becomes the selected model name.
|
|
170
|
-
async selectMode(mode) {
|
|
171
|
-
this.requirePage();
|
|
172
|
-
if (mode !== "latest") {
|
|
173
|
-
return { status: "skipped", requested: mode, attempts: 0 };
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const switcher = this.page.locator("button.modelSelectorButton").first();
|
|
177
|
-
await switcher.waitFor({ state: "visible", timeout: 10_000 }).catch(() => null);
|
|
178
|
-
if (await switcher.count().catch(() => 0) === 0) {
|
|
179
|
-
await this.writeDiagnostics("glm-model-switcher-not-found");
|
|
180
|
-
return {
|
|
181
|
-
status: "switcher_not_found",
|
|
182
|
-
requested: mode,
|
|
183
|
-
attempts: 0,
|
|
184
|
-
reason: "Model switcher was not found.",
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const current = (await switcher.innerText().catch(() => "")).trim();
|
|
189
|
-
// Already on the most-preferred model that exists? If the current label is
|
|
190
|
-
// the first preferred model, nothing to do.
|
|
191
|
-
if (current.startsWith(PREFERRED_MODELS[0])) {
|
|
192
|
-
return {
|
|
193
|
-
status: "already",
|
|
194
|
-
requested: mode,
|
|
195
|
-
selectedLabel: PREFERRED_MODELS[0],
|
|
196
|
-
attempts: 0,
|
|
197
|
-
reason: `Already using ${PREFERRED_MODELS[0]}.`,
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
for (const model of PREFERRED_MODELS) {
|
|
202
|
-
await switcher.click({ timeout: 5_000 }).catch(() => null);
|
|
203
|
-
await this.page.waitForTimeout(600);
|
|
204
|
-
const option = this.page.getByText(model, { exact: true }).first();
|
|
205
|
-
if (await option.count().catch(() => 0) === 0) {
|
|
206
|
-
// Not in the menu; close and try the next preferred model.
|
|
207
|
-
await this.page.keyboard.press("Escape").catch(() => null);
|
|
208
|
-
continue;
|
|
209
|
-
}
|
|
210
|
-
await option.click({ timeout: 5_000 }).catch(() => null);
|
|
211
|
-
await this.page.waitForTimeout(600);
|
|
212
|
-
const after = (await switcher.innerText().catch(() => "")).trim();
|
|
213
|
-
if (after.startsWith(model)) {
|
|
214
|
-
// The model menu stays open after a selection; a click in the page
|
|
215
|
-
// center dismisses it so it does not cover the composer.
|
|
216
|
-
await this.#dismissModelMenu();
|
|
217
|
-
return {
|
|
218
|
-
status: current.startsWith(model) ? "already" : "select",
|
|
219
|
-
requested: mode,
|
|
220
|
-
selectedLabel: model,
|
|
221
|
-
attempts: 1,
|
|
222
|
-
reason: `Selected ${model}.`,
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
await this.#dismissModelMenu();
|
|
228
|
-
await this.writeDiagnostics("glm-mode-latest-unresolved");
|
|
229
|
-
return {
|
|
230
|
-
status: "unresolved",
|
|
231
|
-
requested: mode,
|
|
232
|
-
attempts: 1,
|
|
233
|
-
reason: `Could not select any of: ${PREFERRED_MODELS.join(", ")}.`,
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
async #dismissModelMenu() {
|
|
238
|
-
const viewport = this.page.viewportSize?.() ?? { width: 1280, height: 800 };
|
|
239
|
-
await this.page.mouse.click(
|
|
240
|
-
Math.floor(viewport.width / 2),
|
|
241
|
-
Math.floor(viewport.height / 2),
|
|
242
|
-
).catch(() => null);
|
|
243
|
-
await this.page.keyboard.press("Escape").catch(() => null);
|
|
244
|
-
await this.page.waitForTimeout(200);
|
|
245
|
-
}
|
|
246
159
|
}
|