zames_pro 2.80.8 → 2.81.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/CHANGELOG.md +36 -0
- package/README.md +71 -21
- package/dist/browser-login.js +177 -0
- package/dist/browser-pure.js +112 -8
- package/dist/browser.js +274 -16
- package/dist/commands.js +24 -8
- package/dist/config.js +106 -2
- package/dist/glm-browser.js +1766 -0
- package/dist/glm-chats.js +172 -0
- package/dist/glm-flags.js +75 -0
- package/dist/glm-net-capture.js +338 -0
- package/dist/glm-ui.js +171 -0
- package/dist/i18n/common.js +18 -2
- package/dist/i18n/config.js +41 -1
- package/dist/i18n/doctor.js +11 -6
- package/dist/i18n/glm.js +86 -0
- package/dist/i18n/help.js +14 -10
- package/dist/i18n/qwen.js +47 -15
- package/dist/i18n/self-review.js +2 -2
- package/dist/i18n.js +2 -0
- package/dist/index.js +325 -114
- package/dist/provider-handoff.js +11 -0
- package/dist/providers.js +65 -0
- package/dist/qwen-browser.js +274 -30
- package/dist/send-runtime.js +112 -0
- package/dist/send-telemetry.js +99 -0
- package/dist/send-throttle.js +108 -0
- package/dist/system-prompt.js +23 -2
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.81.0] - 2026-10-21
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- auto-compact by default, GLM reasoning effort low
|
|
15
|
+
- **glm:** tell the model it runs on the operator's machine; fix the runtime rewrite URL match
|
|
16
|
+
- **glm:** neutralise z.ai's agent runtime so zames tools actually run
|
|
17
|
+
- **glm:** run with a visible browser by default (the site blocks headless)
|
|
18
|
+
- **glm:** thinking is always on and cannot be toggled off
|
|
19
|
+
- **glm:** verify mode flags and attach files through the upload button
|
|
20
|
+
- send telemetry for all providers and adaptive throttle (enabled for GLM)
|
|
21
|
+
- GLM (chat.z.ai) provider with anonymized recon fixtures
|
|
22
|
+
- browser-based login (auto-login first, headed window fallback, headless after)
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **glm:** force web search off and report a busy page instead of a missing button
|
|
27
|
+
- **glm:** no web-search noise, quota fails fast, the model is told not to browse
|
|
28
|
+
- **glm:** the smoke sends the system-prompt into the same chat; the mode check reads the rewritten body
|
|
29
|
+
- **glm:** a signed-in session is not reported as a guest
|
|
30
|
+
- drop the unused headless flag
|
|
31
|
+
- **glm:** word the mode warning accurately (the smoke passed with it on)
|
|
32
|
+
- persist the sign-in session cookie so it survives a restart
|
|
33
|
+
- a successful auto-login saves the credentials it used
|
|
34
|
+
- per-provider credentials are writable (missing from the config schema)
|
|
35
|
+
- DeepSeek signs in through the browser window before the terminal form
|
|
36
|
+
- the TUI login fallback prefills the active provider's credentials
|
|
37
|
+
- doctor renders the display row and the adaptive pause
|
|
38
|
+
- **glm:** report the slider captcha instead of a silent no-answer
|
|
39
|
+
- handoff label and live config for more than two providers
|
|
40
|
+
- per-provider auth marker, credentials and shared headless-UA cache
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- provider capabilities instead of qwen-only checks
|
|
45
|
+
|
|
10
46
|
## [2.80.8] - 2026-10-20
|
|
11
47
|
|
|
12
48
|
### Changed
|
package/README.md
CHANGED
|
@@ -65,8 +65,9 @@ zames does not call the model API. It launches a headless Chromium with a
|
|
|
65
65
|
persistent profile, signs in to a chat web app like a human, types the task
|
|
66
66
|
into the chat box, and reads the answer back.
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
**DeepSeek** (`chat.deepseek.com`, the default)
|
|
68
|
+
Three chat backends are available, switched with `/provider <deepseek|qwen|glm>`:
|
|
69
|
+
**DeepSeek** (`chat.deepseek.com`, the default), **Qwen** (`chat.qwen.ai`)
|
|
70
|
+
and **GLM** (`chat.z.ai`).
|
|
70
71
|
The switch is **live** — zames closes the current backend and opens the other
|
|
71
72
|
one in place, no restart needed. Each keeps its own persistent profile
|
|
72
73
|
(`~/.zames/profile` and `~/.zames/qwen-profile`), so switching never signs the
|
|
@@ -97,7 +98,7 @@ Key pieces:
|
|
|
97
98
|
|
|
98
99
|
- [Quick start](#quick-start) · [Features](#features) · [Why zames?](#why-zames) · [Requirements](#requirements)
|
|
99
100
|
- [How it works](#how-it-works) · [Installation](#installation) · [Signing in](#signing-in) · [Usage](#usage)
|
|
100
|
-
- [Switching providers (DeepSeek / Qwen)](#switching-providers-deepseek--qwen)
|
|
101
|
+
- [Switching providers (DeepSeek / Qwen / GLM)](#switching-providers-deepseek--qwen--glm)
|
|
101
102
|
- [Tools](#tools) · [Slash commands](#slash-commands)
|
|
102
103
|
- [Project context, skills and memory](#project-context-skills-and-memory) · [MCP (external tools)](#mcp-external-tools) · [Configuration](#configuration)
|
|
103
104
|
- [FAQ](#faq) · [Links](#links) · [License](#license)
|
|
@@ -152,13 +153,18 @@ Key pieces:
|
|
|
152
153
|
The browser runs **headless by default**. When the active backend requires a
|
|
153
154
|
sign-in, zames:
|
|
154
155
|
|
|
155
|
-
1. reuses the session stored in the persistent profile
|
|
156
|
-
|
|
156
|
+
1. reuses the session stored in the persistent profile
|
|
157
|
+
(`~/.zames/profile`, `~/.zames/qwen-profile` or `~/.zames/glm-profile`)
|
|
158
|
+
if it is still valid;
|
|
157
159
|
2. otherwise signs in automatically with the saved credentials
|
|
158
|
-
(`browser.auth.username` / `browser.auth.password`
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
160
|
+
(`browser.auth.username` / `browser.auth.password` for DeepSeek,
|
|
161
|
+
`qwen.auth.*` / `glm.auth.*` for the others);
|
|
162
|
+
3. otherwise opens a **visible browser window** so YOU sign in there — a
|
|
163
|
+
captcha (DeepSeek and z.ai both use one) cannot be typed into a terminal.
|
|
164
|
+
zames notices the sign-in and returns to headless; the credentials you
|
|
165
|
+
typed are saved for the next silent auto-login;
|
|
166
|
+
4. only when no window can be shown (no display, a non-interactive run) does it
|
|
167
|
+
fall back to the terminal form.
|
|
162
168
|
|
|
163
169
|
To sign in by hand (for example, if the site shows a captcha), run with a
|
|
164
170
|
visible window:
|
|
@@ -178,7 +184,7 @@ version). You do not need `--headed` just to log in.
|
|
|
178
184
|
Credentials and toggles can also be edited from `/config`
|
|
179
185
|
(`browser.auth.username`, `browser.auth.password`, `browser.auth.saveSession`).
|
|
180
186
|
|
|
181
|
-
## Switching providers (DeepSeek / Qwen)
|
|
187
|
+
## Switching providers (DeepSeek / Qwen / GLM)
|
|
182
188
|
|
|
183
189
|
zames can drive either **DeepSeek** (`chat.deepseek.com`, the default) or
|
|
184
190
|
**Qwen** (`chat.qwen.ai`):
|
|
@@ -206,6 +212,25 @@ Notes:
|
|
|
206
212
|
GitHub — no phone required). zames auto-logins from the saved credentials or
|
|
207
213
|
asks in the terminal, exactly like DeepSeek. `/model` changes the Qwen model
|
|
208
214
|
at runtime.
|
|
215
|
+
- **GLM** (`chat.z.ai`) is the third backend. Its first message from a fresh
|
|
216
|
+
profile is gated by a slider captcha, so sign in once in the visible window;
|
|
217
|
+
after that the stored session is reused. `/model` lists and picks GLM
|
|
218
|
+
models too.
|
|
219
|
+
- **GLM runs with a VISIBLE browser window.** Measured: the SAME signed-in
|
|
220
|
+
session is fine in a headed window but every send from HEADLESS is stopped by
|
|
221
|
+
the site's slider captcha (a different puzzle each time, so it cannot be
|
|
222
|
+
solved programmatically). zames therefore launches GLM headed by default and
|
|
223
|
+
prints a note about it; `--headless` still forces headless (sends will hit
|
|
224
|
+
the captcha). `--headed` is accepted everywhere.
|
|
225
|
+
- **Send telemetry**: every send is recorded in
|
|
226
|
+
`~/.zames/logs/sends-<provider>.jsonl` (timing, mode flags, the outcome and
|
|
227
|
+
the server error — never the prompt or the answer). Turn it off with
|
|
228
|
+
`/config set browser.sendTelemetry false`. `/limits` shows the recent sends
|
|
229
|
+
and the current pause. GLM additionally uses an **adaptive pause**: it starts
|
|
230
|
+
at 0 and backs off from a REAL rate limit.
|
|
231
|
+
- **Warning**: automating a web chat may violate the service's terms of use and
|
|
232
|
+
can get an account blocked. GLM (z.ai) has an official API — prefer it for
|
|
233
|
+
heavy automation.
|
|
209
234
|
- File attachments on Qwen go through the mode menu's **Upload attachment**
|
|
210
235
|
item; DeepSeek uses its own paperclip.
|
|
211
236
|
- `/doctor` shows the active provider and the UI health of that provider's
|
|
@@ -648,11 +673,14 @@ zames не вызывает API модели. Он запускает headless C
|
|
|
648
673
|
профилем, входит в веб-чат как человек, печатает задачу в поле чата и читает
|
|
649
674
|
ответ обратно.
|
|
650
675
|
|
|
651
|
-
Доступны
|
|
652
|
-
|
|
676
|
+
Доступны три бэкенда, переключаются командой
|
|
677
|
+
`/provider <deepseek|qwen|glm>`:
|
|
678
|
+
**DeepSeek** (`chat.deepseek.com`, по умолчанию), **Qwen** (`chat.qwen.ai`)
|
|
679
|
+
и **GLM** (`chat.z.ai`).
|
|
653
680
|
Переключение **живое** — zames закрывает текущий бэкенд и открывает другой на
|
|
654
681
|
месте, без перезапуска. У каждого свой постоянный профиль
|
|
655
|
-
(`~/.zames/profile
|
|
682
|
+
(`~/.zames/profile`, `~/.zames/qwen-profile`, `~/.zames/glm-profile`),
|
|
683
|
+
поэтому переключение не
|
|
656
684
|
разлогинивает другой.
|
|
657
685
|
|
|
658
686
|
<p align="center">
|
|
@@ -682,7 +710,7 @@ zames не вызывает API модели. Он запускает headless C
|
|
|
682
710
|
|
|
683
711
|
- [Быстрый старт](#быстрый-старт) · [Возможности](#возможности) · [Почему zames?](#почему-zames) · [Требования](#требования)
|
|
684
712
|
- [Как это работает](#как-это-работает) · [Установка](#установка) · [Вход в аккаунт](#вход-в-аккаунт) · [Использование](#использование)
|
|
685
|
-
- [Переключение провайдера (DeepSeek / Qwen)](#переключение-провайдера-deepseek--qwen)
|
|
713
|
+
- [Переключение провайдера (DeepSeek / Qwen / GLM)](#переключение-провайдера-deepseek--qwen--glm)
|
|
686
714
|
- [Инструменты](#инструменты) · [Слэш-команды](#слэш-команды)
|
|
687
715
|
- [Контекст проекта, навыки и память](#контекст-проекта-навыки-и-память) · [MCP (внешние инструменты)](#mcp-внешние-инструменты) · [Конфигурация](#конфигурация)
|
|
688
716
|
- [FAQ](#faq) · [Ссылки](#ссылки) · [Лицензия](#лицензия)
|
|
@@ -740,13 +768,17 @@ zames не вызывает API модели. Он запускает headless C
|
|
|
740
768
|
Браузер по умолчанию работает **headless**. Когда активный бэкенд требует
|
|
741
769
|
входа, zames:
|
|
742
770
|
|
|
743
|
-
1. переиспользует сессию из постоянного профиля
|
|
744
|
-
|
|
771
|
+
1. переиспользует сессию из постоянного профиля
|
|
772
|
+
(`~/.zames/profile`, `~/.zames/qwen-profile` или `~/.zames/glm-profile`),
|
|
773
|
+
если она ещё валидна;
|
|
745
774
|
2. иначе входит автоматически по сохранённым учётным данным
|
|
746
|
-
(`browser.auth.
|
|
747
|
-
3. иначе
|
|
748
|
-
|
|
749
|
-
|
|
775
|
+
(`browser.auth.*` у DeepSeek, `qwen.auth.*` / `glm.auth.*` у остальных);
|
|
776
|
+
3. иначе открывает **видимое окно браузера**, чтобы вы вошли сами — капчу
|
|
777
|
+
(она есть и у DeepSeek, и у z.ai) в терминал не ввести. zames замечает
|
|
778
|
+
вход и возвращается в headless; введённые вами учётные данные сохраняются
|
|
779
|
+
для следующего тихого автовхода;
|
|
780
|
+
4. и только если окно показать нельзя (нет дисплея, неинтерактивный запуск),
|
|
781
|
+
переходит к форме в терминале.
|
|
750
782
|
|
|
751
783
|
Чтобы войти руками (например, если сайт показывает капчу), запустите с
|
|
752
784
|
видимым окном:
|
|
@@ -766,7 +798,7 @@ Headless работает из коробки: headless Chrome обычно пр
|
|
|
766
798
|
Учётные данные и переключатели можно также править из `/config`
|
|
767
799
|
(`browser.auth.username`, `browser.auth.password`, `browser.auth.saveSession`).
|
|
768
800
|
|
|
769
|
-
## Переключение провайдера (DeepSeek / Qwen)
|
|
801
|
+
## Переключение провайдера (DeepSeek / Qwen / GLM)
|
|
770
802
|
|
|
771
803
|
zames умеет работать с **DeepSeek** (`chat.deepseek.com`, по умолчанию) и
|
|
772
804
|
**Qwen** (`chat.qwen.ai`):
|
|
@@ -793,6 +825,24 @@ zames умеет работать с **DeepSeek** (`chat.deepseek.com`, по у
|
|
|
793
825
|
несколько сообщений в день; для долгих прогонов войдите (email / Google /
|
|
794
826
|
GitHub — телефон не нужен). zames сам логинится по сохранённым кредам или
|
|
795
827
|
спросит в терминале, как и с DeepSeek. `/model` меняет модель Qwen на лету.
|
|
828
|
+
- **GLM** (`chat.z.ai`) — третий бэкенд. Первое сообщение с нового
|
|
829
|
+
профиля закрыто капчей-слайдером, поэтому войдите один раз в видимом окне;
|
|
830
|
+
дальше сохранённая сессия переиспользуется. `/model` работает и для GLM.
|
|
831
|
+
- **GLM работает с ВИДИМЫМ окном браузера.** Замерено: одна и та же
|
|
832
|
+
залогиненная сессия в окне отправляет нормально, а КАЖДАЯ отправка из
|
|
833
|
+
headless упирается в капчу-слайдер сайта (картинка каждый раз новая, поэтому
|
|
834
|
+
программно её не пройти). Поэтому zames запускает GLM с окном по умолчанию и
|
|
835
|
+
пишет об этом при старте; `--headless` по-прежнему форсирует headless
|
|
836
|
+
(отправки будут упираться в капчу). `--headed` работает везде.
|
|
837
|
+
- **Телеметрия отправок**: каждая отправка пишется в
|
|
838
|
+
`~/.zames/logs/sends-<provider>.jsonl` (тайминги, флаги режимов, исход и
|
|
839
|
+
ошибка сервера — без промптов и ответов). Отключается через
|
|
840
|
+
`/config set browser.sendTelemetry false`. Команда `/limits` показывает
|
|
841
|
+
последние отправки и текущую паузу. У GLM вдобавок **адаптивная пауза**: она
|
|
842
|
+
начинается с 0 и растёт только после НАСТОЯЩЕГО лимита.
|
|
843
|
+
- **Предупреждение**: автоматизация веб-чата может нарушать условия
|
|
844
|
+
использования сервиса и привести к блокировке аккаунта. У GLM (z.ai) есть
|
|
845
|
+
официальный API — для тяжёлой автоматизации лучше он.
|
|
796
846
|
- Вложения в Qwen идут через пункт **Upload attachment** в меню режима; в
|
|
797
847
|
DeepSeek — своя скрепка.
|
|
798
848
|
- `/doctor` показывает активный провайдер и состояние UI его страницы.
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// Interactive, BROWSER-BASED sign-in — shared by every provider.
|
|
2
|
+
//
|
|
3
|
+
// WHY: DeepSeek (and now z.ai) often answers a headless login with a captcha
|
|
4
|
+
// that neither a terminal prompt nor a filled form can pass. A human has to
|
|
5
|
+
// sign in inside a VISIBLE window; zames then notices the session and goes
|
|
6
|
+
// back to headless. Before this module each provider had its own ad-hoc flow,
|
|
7
|
+
// so the policy lived in three places and none of them offered the window.
|
|
8
|
+
//
|
|
9
|
+
// The module is PURE in the sense that it owns NO Playwright and NO disk: the
|
|
10
|
+
// browser supplies `probeLogin`, `relaunch`, `openLoginPage`, ... and the
|
|
11
|
+
// decision sequence below is unit-tested with injected doubles
|
|
12
|
+
// (test/browser-login.test.ts).
|
|
13
|
+
//
|
|
14
|
+
// Order (the operator's requirement):
|
|
15
|
+
// 1. probeLogin() === 'in' -> done, no window at all
|
|
16
|
+
// 2. autoLogin() with saved creds -> done (this is the FIRST step after
|
|
17
|
+
// the probe; credentials are permanent)
|
|
18
|
+
// 3. guest is allowed and no login was requested -> work as a guest
|
|
19
|
+
// 4. not interactive -> LoginRequiredError (fast, no window)
|
|
20
|
+
// 5. no display -> fall back to the in-TUI forms
|
|
21
|
+
// 6. headless -> relaunchSafe(headed), sign in
|
|
22
|
+
// 7. wait until probeLogin() === 'in' (poll), the operator closes the window
|
|
23
|
+
// (= done) or presses Esc (= cancel)
|
|
24
|
+
// 8. save credentials captured from the form, persist the session marker
|
|
25
|
+
// 9. not stayHeaded -> relaunchSafe(headless); if the
|
|
26
|
+
// session did not carry over, go back
|
|
27
|
+
// to headed ONCE and stay there.
|
|
28
|
+
/** A sign-in is required and no interactive window can be offered. */
|
|
29
|
+
export class LoginRequiredError extends Error {
|
|
30
|
+
constructor(provider) {
|
|
31
|
+
super(provider + ': a sign-in is required (non-interactive)');
|
|
32
|
+
this.name = 'LoginRequiredError';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** The operator cancelled the sign-in (Esc / Ctrl+C). */
|
|
36
|
+
export class LoginCancelledError extends Error {
|
|
37
|
+
constructor() {
|
|
38
|
+
super('sign-in cancelled');
|
|
39
|
+
this.name = 'LoginCancelledError';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export async function interactiveBrowserLogin(deps) {
|
|
43
|
+
// 1) A live session: nothing to do (and no window flashes).
|
|
44
|
+
const first = await deps.probeLogin().catch(() => 'unknown');
|
|
45
|
+
if (first === 'in')
|
|
46
|
+
return 'already';
|
|
47
|
+
// 2) Saved credentials — the FIRST fallback. They are permanent (the
|
|
48
|
+
// operator's decision), so a logged-out profile signs in silently.
|
|
49
|
+
const auto = await deps.autoLogin().catch(() => false);
|
|
50
|
+
if (auto) {
|
|
51
|
+
await deps.persistSession().catch(() => { });
|
|
52
|
+
return 'auto';
|
|
53
|
+
}
|
|
54
|
+
// 3) A guest is usable and no window is needed.
|
|
55
|
+
if (deps.guestAllowed)
|
|
56
|
+
return 'guest';
|
|
57
|
+
// 4) Non-interactive: fail fast instead of hanging on a window nobody sees.
|
|
58
|
+
if (!deps.interactive)
|
|
59
|
+
throw new LoginRequiredError(deps.provider);
|
|
60
|
+
// 5) No display: the in-TUI form is the accepted fallback (DESIGN-NOTES
|
|
61
|
+
// "Login stays inside the TUI").
|
|
62
|
+
if (!deps.canShowWindow) {
|
|
63
|
+
if (deps.tuiLogin) {
|
|
64
|
+
const ok = await deps.tuiLogin().catch(() => false);
|
|
65
|
+
if (ok) {
|
|
66
|
+
await deps.persistSession().catch(() => { });
|
|
67
|
+
return 'tui';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
throw new LoginRequiredError(deps.provider);
|
|
71
|
+
}
|
|
72
|
+
// 6) Open the visible window and wait for the human.
|
|
73
|
+
const inWindow = await runHeadedLogin(deps);
|
|
74
|
+
if (!inWindow) {
|
|
75
|
+
if (deps.isAborted())
|
|
76
|
+
throw new LoginCancelledError();
|
|
77
|
+
throw new LoginRequiredError(deps.provider);
|
|
78
|
+
}
|
|
79
|
+
await deps.persistSession().catch(() => { });
|
|
80
|
+
// 7) Back to headless unless the operator asked for a visible window.
|
|
81
|
+
if (!deps.stayHeaded) {
|
|
82
|
+
await deps.relaunch(false).catch(() => { });
|
|
83
|
+
const back = await deps.probeLogin().catch(() => 'unknown');
|
|
84
|
+
if (back !== 'in') {
|
|
85
|
+
// The session did not carry over (a session cookie or an SPA token was
|
|
86
|
+
// lost on the relaunch). Stay visible ONCE — never loop.
|
|
87
|
+
await deps.relaunch(true).catch(() => { });
|
|
88
|
+
deps.notice('login.headless_failed');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
await deps.gotoChat().catch(() => { });
|
|
92
|
+
return 'window';
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Show the window, drive the human through the sign-in and return true when
|
|
96
|
+
* the session became active (or the operator closed the window = "done").
|
|
97
|
+
*/
|
|
98
|
+
async function runHeadedLogin(deps) {
|
|
99
|
+
if (!deps.stayHeaded)
|
|
100
|
+
await deps.relaunch(true).catch(() => { });
|
|
101
|
+
await deps.openLoginPage().catch(() => { });
|
|
102
|
+
await deps.prefill().catch(() => { });
|
|
103
|
+
await deps.captureCredentials().catch(() => { });
|
|
104
|
+
deps.notice('login.browser_prompt');
|
|
105
|
+
const deadline = deps.now() + deps.timeoutMs;
|
|
106
|
+
while (deps.now() < deadline) {
|
|
107
|
+
if (deps.isAborted())
|
|
108
|
+
return false;
|
|
109
|
+
const r = await deps.probeLogin().catch(() => 'unknown');
|
|
110
|
+
if (r === 'in')
|
|
111
|
+
return true;
|
|
112
|
+
// A closed window is the operator's "I am done" signal: the browser object
|
|
113
|
+
// may still hold the session cookies, so verify once more and accept.
|
|
114
|
+
if (deps.windowClosed()) {
|
|
115
|
+
deps.notice('login.window_closed');
|
|
116
|
+
const again = await deps.probeLogin().catch(() => 'unknown');
|
|
117
|
+
return again === 'in';
|
|
118
|
+
}
|
|
119
|
+
await deps.sleep(1000);
|
|
120
|
+
}
|
|
121
|
+
deps.notice('login.timeout');
|
|
122
|
+
const last = await deps.probeLogin().catch(() => 'unknown');
|
|
123
|
+
return last === 'in';
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* PURE: can a browser window be shown? Only a PRELIMINARY filter — WSL2 often
|
|
127
|
+
* exports DISPLAY/WAYLAND_DISPLAY even when WSLg is broken, so the headed
|
|
128
|
+
* launch itself must still be wrapped in try/catch with a timeout.
|
|
129
|
+
*/
|
|
130
|
+
export function canShowWindow(env, platform) {
|
|
131
|
+
if (env.ZAMES_NONINTERACTIVE_LOGIN === '1')
|
|
132
|
+
return false;
|
|
133
|
+
if (env.CI === 'true' || env.CI === '1')
|
|
134
|
+
return false;
|
|
135
|
+
if (platform === 'win32' || platform === 'darwin')
|
|
136
|
+
return true;
|
|
137
|
+
// Linux/X11/Wayland: a display variable must be set.
|
|
138
|
+
if (env.DISPLAY || env.WAYLAND_DISPLAY)
|
|
139
|
+
return true;
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* PURE: is this run allowed to open an interactive window at all? One-shot
|
|
144
|
+
* (--task), a scheduler job and a non-TTY all say no; the env kill switch
|
|
145
|
+
* ZAMES_NONINTERACTIVE_LOGIN=1 says no too (the smoke scripts set it so a
|
|
146
|
+
* cold login fails fast instead of hanging).
|
|
147
|
+
*/
|
|
148
|
+
export function loginIsInteractive(opts) {
|
|
149
|
+
if (opts.env.ZAMES_NONINTERACTIVE_LOGIN === '1')
|
|
150
|
+
return false;
|
|
151
|
+
if (opts.oneShot)
|
|
152
|
+
return false;
|
|
153
|
+
if (opts.scheduled)
|
|
154
|
+
return false;
|
|
155
|
+
return opts.isTTY;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* PURE: classify a login state from the cheap signals. The rule that matters
|
|
159
|
+
* is that the INPUT FIELD alone proves nothing: a guest sees it too, so the
|
|
160
|
+
* classifier relies on the API role / a token / an explicit sign-in control.
|
|
161
|
+
*/
|
|
162
|
+
export function classifyLoginProbe(input) {
|
|
163
|
+
const role = String(input.role || '').toLowerCase();
|
|
164
|
+
if (role === 'user' || role === 'admin')
|
|
165
|
+
return 'in';
|
|
166
|
+
if (role === 'guest')
|
|
167
|
+
return input.signInVisible ? 'out' : 'in';
|
|
168
|
+
if (input.captcha)
|
|
169
|
+
return 'challenge';
|
|
170
|
+
if (input.hasToken === true)
|
|
171
|
+
return 'in';
|
|
172
|
+
if (input.hasToken === false)
|
|
173
|
+
return 'out';
|
|
174
|
+
if (input.signInVisible)
|
|
175
|
+
return 'out';
|
|
176
|
+
return 'unknown';
|
|
177
|
+
}
|
package/dist/browser-pure.js
CHANGED
|
@@ -106,23 +106,49 @@ export const USER_DATA_DIR = path.join(os.homedir(), '.zames', 'profile');
|
|
|
106
106
|
// relaunch" dance happens only once (it otherwise doubled every headless
|
|
107
107
|
// start). Keying by the executable path means a Playwright update (new engine
|
|
108
108
|
// path) invalidates the cache and we re-derive the UA once for the new build.
|
|
109
|
+
//
|
|
110
|
+
// The cache holds ONE entry PER ENGINE, not per provider: the UA depends on
|
|
111
|
+
// the browser build, not on the site. It used to be a single {engine,ua}
|
|
112
|
+
// record with providers appending a suffix (`engine:qwen`), so each provider
|
|
113
|
+
// overwrote the other's entry and a /provider switch relaunched the browser
|
|
114
|
+
// once per side. The file now maps engine key -> UA; a legacy single record is
|
|
115
|
+
// still read.
|
|
109
116
|
const HEADLESS_UA_CACHE = path.join(os.homedir(), '.zames', 'headless-ua.json');
|
|
110
117
|
// Pure: pick a reusable UA from the cache file's text for the given engine, or
|
|
111
118
|
// null. Exported so the policy is unit-tested without touching ~/.zames.
|
|
112
119
|
export function parseHeadlessUACache(raw, engine) {
|
|
113
120
|
try {
|
|
114
121
|
const j = JSON.parse(raw);
|
|
115
|
-
if (j
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
+
if (!j || typeof j !== 'object')
|
|
123
|
+
return null;
|
|
124
|
+
const fromMap = j.engines && typeof j.engines === 'object' ? j.engines[engine] : '';
|
|
125
|
+
const legacy = j.engine === engine ? j.ua : '';
|
|
126
|
+
const ua = fromMap || legacy;
|
|
127
|
+
if (typeof ua === 'string' && ua && !/Headless/i.test(ua))
|
|
128
|
+
return ua;
|
|
122
129
|
}
|
|
123
130
|
catch { }
|
|
124
131
|
return null;
|
|
125
132
|
}
|
|
133
|
+
// Pure: merge a UA into the cache file's text, keeping every OTHER engine's
|
|
134
|
+
// entry. Exported so the multi-engine contract is unit-tested.
|
|
135
|
+
export function mergeHeadlessUACache(raw, engine, ua) {
|
|
136
|
+
let engines = {};
|
|
137
|
+
try {
|
|
138
|
+
const j = JSON.parse(raw);
|
|
139
|
+
if (j && j.engines && typeof j.engines === 'object') {
|
|
140
|
+
engines = { ...j.engines };
|
|
141
|
+
}
|
|
142
|
+
else if (j && typeof j.engine === 'string' && typeof j.ua === 'string') {
|
|
143
|
+
// Migrate a legacy single record into the map.
|
|
144
|
+
engines[j.engine] = j.ua;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch { }
|
|
148
|
+
if (engine)
|
|
149
|
+
engines[engine] = ua;
|
|
150
|
+
return JSON.stringify({ engines }, null, 2);
|
|
151
|
+
}
|
|
126
152
|
export function headlessEngineKey() {
|
|
127
153
|
try {
|
|
128
154
|
return chromium.executablePath();
|
|
@@ -147,7 +173,12 @@ export async function writeCachedHeadlessUA(engine, ua) {
|
|
|
147
173
|
return;
|
|
148
174
|
try {
|
|
149
175
|
await fs.mkdir(path.dirname(HEADLESS_UA_CACHE), { recursive: true });
|
|
150
|
-
|
|
176
|
+
let prev = '';
|
|
177
|
+
try {
|
|
178
|
+
prev = await fs.readFile(HEADLESS_UA_CACHE, 'utf-8');
|
|
179
|
+
}
|
|
180
|
+
catch { }
|
|
181
|
+
await fs.writeFile(HEADLESS_UA_CACHE, mergeHeadlessUACache(prev, engine, ua), 'utf-8');
|
|
151
182
|
}
|
|
152
183
|
catch { }
|
|
153
184
|
}
|
|
@@ -169,7 +200,80 @@ export function sanitizeHeadlessUA(ua) {
|
|
|
169
200
|
}
|
|
170
201
|
// Written after a successful sign-in so the next launch knows a session was
|
|
171
202
|
// stored in the persistent profile (used by /doctor and diagnostics).
|
|
203
|
+
//
|
|
204
|
+
// The marker is PER PROVIDER (`{ providers: { deepseek: {at}, qwen: {at} } }`).
|
|
205
|
+
// It used to be one shared file: Qwen wrote `{provider:'qwen'}` and DeepSeek a
|
|
206
|
+
// bare `{at}`, each overwriting the other, so /doctor and the startup banner
|
|
207
|
+
// reported "no session" for whichever provider had signed in second. A legacy
|
|
208
|
+
// record without `providers` still reads: `{provider:'qwen'}` means Qwen and a
|
|
209
|
+
// bare `{at}` means DeepSeek (the only writer before the second backend).
|
|
172
210
|
export const AUTH_MARKER_FILE = path.join(os.homedir(), '.zames', 'auth.json');
|
|
211
|
+
/**
|
|
212
|
+
* PURE: does the marker text say `provider` has a stored session? Legacy
|
|
213
|
+
* shapes are honored (see the comment above AUTH_MARKER_FILE).
|
|
214
|
+
*/
|
|
215
|
+
export function authMarkerHas(raw, provider) {
|
|
216
|
+
if (!raw)
|
|
217
|
+
return false;
|
|
218
|
+
let j;
|
|
219
|
+
try {
|
|
220
|
+
j = JSON.parse(raw);
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
if (!j || typeof j !== 'object')
|
|
226
|
+
return false;
|
|
227
|
+
if (j.providers && typeof j.providers === 'object') {
|
|
228
|
+
return !!j.providers[provider];
|
|
229
|
+
}
|
|
230
|
+
if (typeof j.provider === 'string')
|
|
231
|
+
return j.provider === provider;
|
|
232
|
+
// Legacy DeepSeek marker: a bare `{at}` with no provider field.
|
|
233
|
+
return provider === 'deepseek' && typeof j.at === 'string';
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* PURE: merge a "signed in at" timestamp for `provider` into the marker text,
|
|
237
|
+
* KEEPING every other provider's entry. A legacy record is migrated.
|
|
238
|
+
*/
|
|
239
|
+
export function mergeAuthMarker(raw, provider, at) {
|
|
240
|
+
const providers = {};
|
|
241
|
+
try {
|
|
242
|
+
const j = JSON.parse(raw);
|
|
243
|
+
if (j && j.providers && typeof j.providers === 'object') {
|
|
244
|
+
for (const [k, v] of Object.entries(j.providers))
|
|
245
|
+
providers[k] = v || {};
|
|
246
|
+
}
|
|
247
|
+
else if (j && typeof j === 'object') {
|
|
248
|
+
if (j.provider === 'qwen')
|
|
249
|
+
providers.qwen = { at: j.at };
|
|
250
|
+
else if (typeof j.at === 'string')
|
|
251
|
+
providers.deepseek = { at: j.at };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch { }
|
|
255
|
+
providers[provider] = { at };
|
|
256
|
+
return JSON.stringify({ providers }, null, 2);
|
|
257
|
+
}
|
|
258
|
+
// Best-effort: read the marker text from disk ('' when absent/unreadable).
|
|
259
|
+
export async function readAuthMarker() {
|
|
260
|
+
try {
|
|
261
|
+
return await fs.readFile(AUTH_MARKER_FILE, 'utf-8');
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
return '';
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// Best-effort: mark `provider` as signed in, keeping the other providers'
|
|
268
|
+
// entries. Never throws (a marker is informational, not load-bearing).
|
|
269
|
+
export async function writeAuthMarker(provider) {
|
|
270
|
+
try {
|
|
271
|
+
await fs.mkdir(path.dirname(AUTH_MARKER_FILE), { recursive: true });
|
|
272
|
+
const prev = await readAuthMarker();
|
|
273
|
+
await fs.writeFile(AUTH_MARKER_FILE, mergeAuthMarker(prev, provider, new Date().toISOString()), 'utf-8');
|
|
274
|
+
}
|
|
275
|
+
catch { }
|
|
276
|
+
}
|
|
173
277
|
export function isGenerationIncompleteText(text) {
|
|
174
278
|
const t = String(text || '');
|
|
175
279
|
return (GENERATION_ERR_RE.test(t) ||
|