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/dist/browser.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { chromium, } from 'playwright';
2
+ import { interactiveBrowserLogin, loginIsInteractive, canShowWindow, } from './browser-login.js';
2
3
  import { extractAnswer, extractTokenUsage, dumpNetBody, isFinishedWithoutAnswer, isFinishedWithAnswer, } from './net-capture.js';
3
4
  import { rewriteFailedAttachments } from './commands.js';
4
5
  import { awaitUploadPreview, readAttachBuffers } from './attach-flow.js';
@@ -20,7 +21,7 @@ export { STOP_NAME_RE, CONTINUE_NAME_RE };
20
21
  // imported for the class below and RE-EXPORTED here, so the public API of this
21
22
  // module is unchanged (tests and other modules import these names from
22
23
  // './browser.js').
23
- import { askPassword, USER_DATA_DIR, parseHeadlessUACache, headlessEngineKey, readCachedHeadlessUA, writeCachedHeadlessUA, headlessUACacheReady, sanitizeHeadlessUA, AUTH_MARKER_FILE, isGenerationIncompleteText, isRateLimitText, isServerBusyText, normText, cleanAnswer, RateLimitError, ServerBusyError, GenerationIncompleteError, cleanSingletonFiles, profileLooksLocked, nukeProfile, } from './browser-pure.js';
24
+ import { askPassword, USER_DATA_DIR, parseHeadlessUACache, headlessEngineKey, readCachedHeadlessUA, writeCachedHeadlessUA, headlessUACacheReady, sanitizeHeadlessUA, writeAuthMarker, isGenerationIncompleteText, isRateLimitText, isServerBusyText, normText, cleanAnswer, RateLimitError, ServerBusyError, GenerationIncompleteError, cleanSingletonFiles, profileLooksLocked, nukeProfile, } from './browser-pure.js';
24
25
  export { askPassword, parseHeadlessUACache, headlessUACacheReady, sanitizeHeadlessUA, isGenerationIncompleteText, isRateLimitText, isServerBusyText, normText, RateLimitError, ServerBusyError, GenerationIncompleteError, };
25
26
  export class DeepSeekBrowser {
26
27
  providerName = 'deepseek';
@@ -161,7 +162,13 @@ export class DeepSeekBrowser {
161
162
  // must be stripped (DeepSeek's CDN answers 403 to it); computed in the
162
163
  // constructor, overridable by an explicit UA in the options/config.
163
164
  userAgent;
164
- constructor({ headless = false, debug = false, channel = 'chrome', answerTimeoutMs = 180000, askRetries = 3, stabilityChecks = 2, stabilityDelayMs = 400, minSendIntervalMs = 15000, thinkingExtraMs = 2000, rateLimitWaitMs = 300000, maxRateLimitRetries = 6, maxServerBusyRetries = 5, serverBusyWaitMs = 3000, maxIncompleteRetries = 4, incompleteWaitMs = 2000, autoContinue = true, continueMinGapMs = 1500, deepThinking = false, webSearch = true, auth, locale, onAuthSave, onLoginPrompt, onMfaPrompt, userAgent, } = {}) {
165
+ /** Login-window timeout (ms) from browser.loginTimeoutMs. */
166
+ loginTimeoutMs;
167
+ /** True once the operator closed the browser window (counts as "done"). */
168
+ _windowClosed;
169
+ /** Session cookies captured before a relaunch (no `expires` -> in-memory). */
170
+ _carryCookies;
171
+ constructor({ headless = false, debug = false, channel = 'chrome', answerTimeoutMs = 180000, askRetries = 3, stabilityChecks = 2, stabilityDelayMs = 400, minSendIntervalMs = 15000, thinkingExtraMs = 2000, rateLimitWaitMs = 300000, maxRateLimitRetries = 6, maxServerBusyRetries = 5, serverBusyWaitMs = 3000, maxIncompleteRetries = 4, incompleteWaitMs = 2000, autoContinue = true, continueMinGapMs = 1500, loginTimeoutMs = 600000, deepThinking = false, webSearch = true, auth, locale, onAuthSave, onLoginPrompt, onMfaPrompt, userAgent, } = {}) {
165
172
  this.headless = headless;
166
173
  this.debug = debug;
167
174
  this.channel = channel;
@@ -179,6 +186,9 @@ export class DeepSeekBrowser {
179
186
  this.incompleteWaitMs = incompleteWaitMs;
180
187
  this.autoContinue = autoContinue;
181
188
  this.continueMinGapMs = continueMinGapMs;
189
+ this.loginTimeoutMs = loginTimeoutMs;
190
+ this._windowClosed = false;
191
+ this._carryCookies = null;
182
192
  this.deepThinking = deepThinking;
183
193
  this.webSearch = webSearch;
184
194
  this.auth = {
@@ -508,6 +518,230 @@ export class DeepSeekBrowser {
508
518
  await this._launchOnce();
509
519
  await this._fixHeadlessUserAgent();
510
520
  }
521
+ /**
522
+ * PURE-ish gate: may the login open a visible window? False for --task, a
523
+ * scheduler job, a non-TTY or ZAMES_NONINTERACTIVE_LOGIN=1 (the smoke
524
+ * scripts set it so a cold login fails fast instead of hanging), and when no
525
+ * display is available at all.
526
+ */
527
+ canShowLoginWindow() {
528
+ const interactive = loginIsInteractive({
529
+ isTTY: !!(process.stdin.isTTY && process.stdout.isTTY),
530
+ oneShot: false,
531
+ scheduled: false,
532
+ env: process.env,
533
+ });
534
+ if (!interactive)
535
+ return false;
536
+ return canShowWindow(process.env, process.platform);
537
+ }
538
+ /**
539
+ * The human-in-the-window fallback: DeepSeek shows a captcha often enough
540
+ * that a terminal prompt cannot pass it, so the operator signs in inside a
541
+ * VISIBLE browser window and zames then returns to headless. The decision
542
+ * sequence is the shared interactiveBrowserLogin; this method only supplies
543
+ * the DeepSeek-specific callbacks.
544
+ */
545
+ async _browserLogin() {
546
+ const stayHeaded = !this.headless;
547
+ const interactive = loginIsInteractive({
548
+ isTTY: !!(process.stdin.isTTY && process.stdout.isTTY),
549
+ oneShot: false,
550
+ scheduled: false,
551
+ env: process.env,
552
+ });
553
+ await interactiveBrowserLogin({
554
+ provider: 'DeepSeek',
555
+ probeLogin: () => this.probeLogin(),
556
+ autoLogin: () => this._autoLogin(),
557
+ relaunch: (h) => this.relaunchSafe(h),
558
+ openLoginPage: () => this._openLoginPage(),
559
+ gotoChat: async () => {
560
+ await this.page
561
+ .goto('https://chat.deepseek.com/', {
562
+ waitUntil: 'domcontentloaded',
563
+ timeout: 30000,
564
+ })
565
+ .catch(() => { });
566
+ },
567
+ prefill: async () => {
568
+ try {
569
+ if (!this.auth.username)
570
+ return;
571
+ const u = await this._findVisible(LOGIN_SELECTORS, 1500);
572
+ if (u)
573
+ await u.fill(this.auth.username).catch(() => { });
574
+ }
575
+ catch { }
576
+ },
577
+ captureCredentials: async () => {
578
+ try {
579
+ await this.page.exposeFunction('__zamesDsCreds', (u, p) => {
580
+ const user = String(u || '').trim();
581
+ const pass = String(p || '');
582
+ if (!user)
583
+ return;
584
+ this.auth.username = user;
585
+ if (pass)
586
+ this.auth.password = pass;
587
+ try {
588
+ this.onAuthSave?.(user, pass);
589
+ }
590
+ catch { }
591
+ });
592
+ await this.page.evaluate(() => {
593
+ const w = window;
594
+ if (w.__zamesDsHooked)
595
+ return;
596
+ w.__zamesDsHooked = true;
597
+ document.addEventListener('submit', (ev) => {
598
+ try {
599
+ const form = ev.target;
600
+ if (!form)
601
+ return;
602
+ const u = form.querySelector('input');
603
+ const p = form.querySelector('input[type="password"]');
604
+ const fn = window.__zamesDsCreds;
605
+ if (fn)
606
+ fn(u ? u.value || '' : '', p ? p.value || '' : '');
607
+ }
608
+ catch { }
609
+ }, true);
610
+ });
611
+ }
612
+ catch { }
613
+ },
614
+ persistSession: () => this._persistSessionIfNeeded(),
615
+ notice: (key) => console.log(theme.system(this._t(key))),
616
+ isAborted: () => this._abort,
617
+ windowClosed: () => this._windowClosed,
618
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
619
+ now: () => Date.now(),
620
+ interactive,
621
+ stayHeaded,
622
+ guestAllowed: false,
623
+ canShowWindow: true,
624
+ timeoutMs: this.loginTimeoutMs,
625
+ });
626
+ }
627
+ /**
628
+ * A FAST login probe (no long waits). The old isLoggedIn() waited 2 s for
629
+ * the input to appear, which is a false "logged out" on a slow network.
630
+ * The signal is a storage token plus a cheap API answer; the DOM is a
631
+ * fallback only.
632
+ */
633
+ async probeLogin() {
634
+ if (!this.page)
635
+ return 'unknown';
636
+ try {
637
+ const r = await this.page.evaluate(async () => {
638
+ const hasToken = (() => {
639
+ try {
640
+ return (!!localStorage.getItem('userToken') ||
641
+ !!localStorage.getItem('token'));
642
+ }
643
+ catch {
644
+ return false;
645
+ }
646
+ })();
647
+ let api = false;
648
+ try {
649
+ const res = await fetch('/api/v0/users/current', {
650
+ credentials: 'include',
651
+ headers: { accept: 'application/json' },
652
+ });
653
+ api = res.ok;
654
+ }
655
+ catch { }
656
+ const body = document.body ? document.body.innerText : '';
657
+ const signInVisible = /log ?in|sign ?in|войти/i.test(body);
658
+ return { hasToken, api, signInVisible };
659
+ });
660
+ if (r.hasToken && r.api)
661
+ return 'in';
662
+ if (r.hasToken)
663
+ return 'in';
664
+ if (r.signInVisible)
665
+ return 'out';
666
+ return 'unknown';
667
+ }
668
+ catch {
669
+ return 'unknown';
670
+ }
671
+ }
672
+ /**
673
+ * Relaunch the persistent context in another headless mode. A SEPARATE path
674
+ * from _launchOnce: it must not clean the Singleton files or nuke the
675
+ * profile, it waits for the previous context to release the lock and it
676
+ * re-applies session-only cookies (a cookie with no `expires` is dropped on
677
+ * close and the SPA would answer 401).
678
+ */
679
+ async relaunchSafe(headless) {
680
+ try {
681
+ const cookies = await this.context.cookies();
682
+ const session = cookies.filter((c) => c.expires === -1 || c.expires < 0);
683
+ if (session.length)
684
+ this._carryCookies = session;
685
+ }
686
+ catch { }
687
+ try {
688
+ if (this.context)
689
+ await this.context.close();
690
+ }
691
+ catch { }
692
+ const deadline = Date.now() + 15000;
693
+ while (Date.now() < deadline) {
694
+ if (!(await profileLooksLocked()))
695
+ break;
696
+ await new Promise((r) => setTimeout(r, 200));
697
+ }
698
+ this.headless = headless;
699
+ this.userAgent = this.userAgent || '';
700
+ const options = {
701
+ headless,
702
+ slowMo: 10,
703
+ args: ['--disable-blink-features=AutomationControlled'],
704
+ };
705
+ if (this.channel)
706
+ options.channel = this.channel;
707
+ if (this.userAgent)
708
+ options.userAgent = this.userAgent;
709
+ try {
710
+ this.context = await chromium.launchPersistentContext(USER_DATA_DIR, options);
711
+ }
712
+ catch (e) {
713
+ throw new Error('deepseek: the browser profile is busy (' + e.message + ')');
714
+ }
715
+ if (this._carryCookies && this._carryCookies.length) {
716
+ await this.context.addCookies(this._carryCookies).catch(() => { });
717
+ }
718
+ this.page = this.context.pages()[0] || (await this.context.newPage());
719
+ this._windowClosed = false;
720
+ this.page.on('close', () => {
721
+ this._windowClosed = true;
722
+ });
723
+ this.context.on('close', () => {
724
+ this._windowClosed = true;
725
+ });
726
+ this._netHookInstalled = false;
727
+ this._netHookPage = null;
728
+ this._installNetHook();
729
+ }
730
+ /** Open DeepSeek's sign-in page (it is the same origin). */
731
+ async _openLoginPage() {
732
+ try {
733
+ if (!/sign_in|login/i.test(this.page.url())) {
734
+ const btn = this.page
735
+ .getByRole('button', { name: /log ?in|sign ?in|войти/i })
736
+ .first();
737
+ if (await btn.count().catch(() => 0)) {
738
+ await btn.click({ timeout: 3000 }).catch(() => { });
739
+ await this.page.waitForTimeout(1500);
740
+ }
741
+ }
742
+ }
743
+ catch { }
744
+ }
511
745
  async waitForLogin() {
512
746
  // 1) Already signed in (session cookie in the persistent profile)?
513
747
  if (await this.isLoggedIn()) {
@@ -515,11 +749,8 @@ export class DeepSeekBrowser {
515
749
  console.error('auth: session already active');
516
750
  return;
517
751
  }
518
- // 2) Try to sign in automatically with the saved credentials. The
519
- // password lives in the config (~/.zames/config.json) and is only used
520
- // when the session is logged out — i.e. exactly the "re-login without
521
- // asking" case. If the login form is absent (e.g. DeepSeek uses a
522
- // captcha / another provider), this fails and we fall through.
752
+ // 2) The saved credentials. They are permanent, so a logged-out profile
753
+ // signs in silently — this is the FIRST fallback.
523
754
  if (this.auth.username && this.auth.password) {
524
755
  console.log(theme.system(this._t('auth.auto_login')));
525
756
  const ok = await this._autoLogin().catch((e) => {
@@ -531,9 +762,17 @@ export class DeepSeekBrowser {
531
762
  return;
532
763
  }
533
764
  }
534
- // 3) Ask the operator for credentials in the terminal, try them, and
535
- // only then fall back to the manual hint. Interactive only: in a
536
- // pipe/redirect (or headless without credentials) we print a hint.
765
+ // 3) A HUMAN signs in inside a VISIBLE browser window — DeepSeek shows a
766
+ // captcha that neither a terminal prompt nor a filled form can pass.
767
+ // The shared module decides whether a window can be shown, waits for
768
+ // the sign-in and returns to headless afterwards. A visible window is
769
+ // kept when the operator asked for it (--headed / --dev / hotReload).
770
+ if (this.canShowLoginWindow()) {
771
+ await this._browserLogin();
772
+ return;
773
+ }
774
+ // 4) No window (no display / non-interactive): the in-terminal form is the
775
+ // accepted fallback, then a manual hint.
537
776
  const asked = await this._promptAndLogin().catch((e) => {
538
777
  if (this.debug)
539
778
  console.error('auth: prompt error:', e.message);
@@ -541,7 +780,6 @@ export class DeepSeekBrowser {
541
780
  });
542
781
  if (asked)
543
782
  return;
544
- // 4) Last resort: manual sign-in in the browser window.
545
783
  console.log('\n' + theme.warn(this._t('auth.need_login')));
546
784
  if (this.headless) {
547
785
  console.log(' ' + this._t('auth.manual_hint_headless'));
@@ -928,11 +1166,7 @@ export class DeepSeekBrowser {
928
1166
  return;
929
1167
  if (!(await this.isLoggedIn().catch(() => false)))
930
1168
  return;
931
- try {
932
- await fs.mkdir(path.join(os.homedir(), '.zames'), { recursive: true });
933
- await fs.writeFile(AUTH_MARKER_FILE, JSON.stringify({ at: new Date().toISOString() }, null, 2), 'utf-8');
934
- }
935
- catch { }
1169
+ await writeAuthMarker('deepseek');
936
1170
  }
937
1171
  async isLoggedIn() {
938
1172
  // Single combined query with a short timeout: the old loop waited up to
@@ -2510,6 +2744,30 @@ export class DeepSeekBrowser {
2510
2744
  return this._lastTokenUsage;
2511
2745
  }
2512
2746
  async close() {
2747
+ // A session cookie (expires = -1) is dropped when the context closes, so a
2748
+ // sign-in would not survive the process. Re-add it as a PERSISTENT cookie
2749
+ // before closing: the token then lives in the profile and the next launch
2750
+ // is signed in without a window.
2751
+ try {
2752
+ if (this.context) {
2753
+ const cookies = await this.context.cookies();
2754
+ const session = cookies.filter((c) => c.expires === -1 || c.expires < 0);
2755
+ if (session.length) {
2756
+ const keepUntil = Math.floor((Date.now() + 15552000000) / 1000);
2757
+ await this.context.addCookies(session.map((c) => ({
2758
+ name: c.name,
2759
+ value: c.value,
2760
+ domain: c.domain,
2761
+ path: c.path,
2762
+ httpOnly: c.httpOnly,
2763
+ secure: c.secure,
2764
+ sameSite: c.sameSite,
2765
+ expires: keepUntil,
2766
+ })));
2767
+ }
2768
+ }
2769
+ }
2770
+ catch { }
2513
2771
  try {
2514
2772
  if (this.context)
2515
2773
  await this.context.close();
package/dist/commands.js CHANGED
@@ -1,4 +1,4 @@
1
- import { providerLabel } from './providers.js';
1
+ import { providerLabel, providerHasModelPicker } from './providers.js';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
4
  import { isImageName } from './attachments.js';
@@ -322,8 +322,19 @@ export function renderDoctor(d, t = translate('en')) {
322
322
  row(true, 'git remote', d.hasOrigin ? t('doctor.origin_ok') : t('doctor.origin_none'));
323
323
  }
324
324
  if (typeof d.minSendIntervalMs === 'number') {
325
- row(d.minSendIntervalMs > 0, 'send pause', t('doctor.send_pause', {
326
- n: String(Math.round(d.minSendIntervalMs / 1000)),
325
+ // An adaptive provider legitimately sits at 0 (it backs off only after a
326
+ // REAL limit), so 0 must not be a WARN there.
327
+ row(d.minSendIntervalMs > 0 || !!d.adaptiveThrottle, 'send pause', d.adaptiveThrottle
328
+ ? t('doctor.adaptive_pause', {
329
+ n: String(Math.round(d.minSendIntervalMs / 1000)),
330
+ })
331
+ : t('doctor.send_pause', {
332
+ n: String(Math.round(d.minSendIntervalMs / 1000)),
333
+ }));
334
+ }
335
+ if (typeof d.displayAvailable === 'boolean') {
336
+ row(d.displayAvailable, 'display', t('doctor.display', {
337
+ v: d.displayAvailable ? t('common.on') : t('common.off'),
327
338
  }));
328
339
  }
329
340
  if (typeof d.sshRemote === 'boolean') {
@@ -342,9 +353,12 @@ export function renderDoctor(d, t = translate('en')) {
342
353
  const summary = missing.length === 0
343
354
  ? t('doctor.ui_ok')
344
355
  : missingCritical.length > 0
345
- ? t('doctor.ui_broken', { v: missingCritical.join(', ') })
356
+ ? t('doctor.ui_broken', {
357
+ v: missingCritical.join(', '),
358
+ provider: providerLabel(d.provider || 'deepseek'),
359
+ })
346
360
  : t('doctor.ui_degraded', { v: missing.join(', ') });
347
- row(missing.length === 0, 'deepseek ui', summary);
361
+ row(missing.length === 0, providerLabel(d.provider || 'deepseek').toLowerCase() + ' ui', summary);
348
362
  }
349
363
  return t('doctor.title') + NL + rows.join(NL);
350
364
  }
@@ -1105,9 +1119,11 @@ export const SLASH_COMMANDS = [
1105
1119
  export function buildSlashCommandHints(opts) {
1106
1120
  const { devMode, t } = opts;
1107
1121
  const table = opts.commands ?? SLASH_COMMANDS;
1108
- // `/model` picks a QWEN model; DeepSeek has no such command. Hide it unless
1109
- // the Qwen provider is active (or the caller did not pass a provider).
1110
- const hideModel = opts.provider !== undefined && opts.provider !== 'qwen';
1122
+ // `/model` needs a provider with a MODEL PICKER (Qwen, GLM); DeepSeek has
1123
+ // none. The capability flag is the registry's, so a new backend only
1124
+ // declares it once. Hide it unless such a provider is active (or the caller
1125
+ // did not pass a provider).
1126
+ const hideModel = opts.provider !== undefined && !providerHasModelPicker(opts.provider);
1111
1127
  const base = table
1112
1128
  .filter((c) => devMode || !isDevOnlyCommand(c.name))
1113
1129
  .filter((c) => !(hideModel && c.name === '/model'))
package/dist/config.js CHANGED
@@ -38,6 +38,24 @@ export const DEFAULTS = {
38
38
  // give it generous room. A signed-in session answers in seconds; this only
39
39
  // bounds the worst case.
40
40
  answerTimeoutMs: 900000,
41
+ // Qwen's credentials live HERE, not in browser.auth: sharing one pair
42
+ // meant a DeepSeek login was typed into the Qwen form.
43
+ auth: { username: '', password: '', saveSession: true },
44
+ // Adaptive throttling is a later step for Qwen (its fixed 4 s gap is
45
+ // measured and works); telemetry is still recorded.
46
+ adaptiveThrottle: false,
47
+ },
48
+ glm: {
49
+ model: '',
50
+ // GLM windows are per-model; the default matches GLM-5.3-Flash (128K).
51
+ // `/model` re-derives it from the picked id (glmContextWindow).
52
+ contextLimit: 128_000,
53
+ // The operator's decision: no fixed pause for GLM. The adaptive throttle
54
+ // (on by default) reacts to a REAL rate limit instead.
55
+ minSendIntervalMs: 0,
56
+ answerTimeoutMs: 300000,
57
+ auth: { username: '', password: '', saveSession: true },
58
+ adaptiveThrottle: true,
41
59
  },
42
60
  // Headless by default: the browser runs without a visible window. Set
43
61
  // headless: false (or pass --headed) to watch/debug the page.
@@ -70,6 +88,9 @@ export const DEFAULTS = {
70
88
  maxIncompleteRetries: 4,
71
89
  incompleteWaitMs: 2000,
72
90
  autoContinue: true,
91
+ // How long the interactive sign-in window waits for the operator before it
92
+ // gives up (a captcha is solved by hand, so this is generous).
93
+ loginTimeoutMs: 600000,
73
94
  continueMinGapMs: 1500,
74
95
  // The agent-loop watchdog for ONE ask(). It only bounds a SILENT send:
75
96
  // a turn the server already ENDED with no answer (FINISHED + truncated
@@ -77,8 +98,16 @@ export const DEFAULTS = {
77
98
  // not waited out. 240s used to mean a 4-minute spinner per dead turn.
78
99
  askDeadlineMs: 120000,
79
100
  maxAfterToolRetries: 6,
80
- autoCompact: false,
81
- autoCompactPct: 95,
101
+ // Auto-compact is ON by default: a provider's context window is small and
102
+ // per-model (GLM: 32k-128k), and running out of it mid-task is a hard stop
103
+ // that loses the work, while a compaction at a safe seam only costs one
104
+ // extra send. The threshold is 85 rather than the old 95 because ONE large
105
+ // tool result can overflow the window before the seam is reached. The loop
106
+ // already guards the dangerous cases: it never compacts mid-generation or
107
+ // during a rate-limit wait, and it disables itself for the rest of the
108
+ // task if the fresh chat did not actually open.
109
+ autoCompact: true,
110
+ autoCompactPct: 85,
82
111
  resendPromptOnResume: false,
83
112
  // Subagents on by default: the Task tool is what keeps heavy exploration
84
113
  // out of the main context, and the main chat is restored afterwards.
@@ -86,6 +115,7 @@ export const DEFAULTS = {
86
115
  maxSubagents: 3,
87
116
  deepThinking: false,
88
117
  webSearch: true,
118
+ sendTelemetry: true,
89
119
  auth: {
90
120
  username: '',
91
121
  password: '',
@@ -149,6 +179,8 @@ export function loadConfig() {
149
179
  export function providerAskDeadlineMs(cfg) {
150
180
  if (cfg.provider === 'qwen')
151
181
  return cfg.qwen.answerTimeoutMs + 120_000;
182
+ if (cfg.provider === 'glm')
183
+ return cfg.glm.answerTimeoutMs + 120_000;
152
184
  return cfg.browser.askDeadlineMs;
153
185
  }
154
186
  function deepMerge(target, source) {
@@ -236,6 +268,34 @@ export const CONFIG_SCHEMA = [
236
268
  labelKey: 'cfg.f.browser_authSaveSession',
237
269
  groupKey: 'cfg.group.browser',
238
270
  },
271
+ // Each non-DeepSeek provider owns its credentials, so a DeepSeek login is
272
+ // never typed into a Qwen/GLM form (and vice versa). They MUST be in the
273
+ // schema: writeConfigValue rejects an unknown key, which silently dropped
274
+ // every GLM/Qwen credential saved from the login window.
275
+ {
276
+ path: 'qwen.auth.username',
277
+ type: 'string',
278
+ labelKey: 'cfg.f.qwen_authUsername',
279
+ groupKey: 'cfg.group.browser',
280
+ },
281
+ {
282
+ path: 'qwen.auth.password',
283
+ type: 'string',
284
+ labelKey: 'cfg.f.qwen_authPassword',
285
+ groupKey: 'cfg.group.browser',
286
+ },
287
+ {
288
+ path: 'glm.auth.username',
289
+ type: 'string',
290
+ labelKey: 'cfg.f.glm_authUsername',
291
+ groupKey: 'cfg.group.browser',
292
+ },
293
+ {
294
+ path: 'glm.auth.password',
295
+ type: 'string',
296
+ labelKey: 'cfg.f.glm_authPassword',
297
+ groupKey: 'cfg.group.browser',
298
+ },
239
299
  {
240
300
  path: 'provider',
241
301
  type: 'enum',
@@ -273,6 +333,36 @@ export const CONFIG_SCHEMA = [
273
333
  labelKey: 'cfg.f.qwen_answerTimeoutMs',
274
334
  groupKey: 'cfg.group.browser',
275
335
  },
336
+ {
337
+ path: 'glm.model',
338
+ type: 'string',
339
+ labelKey: 'cfg.f.glm_model',
340
+ groupKey: 'cfg.group.browser',
341
+ },
342
+ {
343
+ path: 'glm.contextLimit',
344
+ type: 'number',
345
+ min: 10_000,
346
+ max: 10_000_000,
347
+ labelKey: 'cfg.f.glm_contextLimit',
348
+ groupKey: 'cfg.group.browser',
349
+ },
350
+ {
351
+ path: 'glm.minSendIntervalMs',
352
+ type: 'number',
353
+ min: 0,
354
+ max: 600000,
355
+ labelKey: 'cfg.f.glm_minSendIntervalMs',
356
+ groupKey: 'cfg.group.browser',
357
+ },
358
+ {
359
+ path: 'glm.answerTimeoutMs',
360
+ type: 'number',
361
+ min: 1000,
362
+ max: 3600000,
363
+ labelKey: 'cfg.f.glm_answerTimeoutMs',
364
+ groupKey: 'cfg.group.browser',
365
+ },
276
366
  {
277
367
  path: 'debug',
278
368
  type: 'boolean',
@@ -405,6 +495,20 @@ export const CONFIG_SCHEMA = [
405
495
  labelKey: 'cfg.f.browser_autoContinue',
406
496
  groupKey: 'cfg.group.browser',
407
497
  },
498
+ {
499
+ path: 'browser.sendTelemetry',
500
+ type: 'boolean',
501
+ labelKey: 'cfg.f.browser_sendTelemetry',
502
+ groupKey: 'cfg.group.browser',
503
+ },
504
+ {
505
+ path: 'browser.loginTimeoutMs',
506
+ type: 'number',
507
+ min: 30_000,
508
+ max: 3_600_000,
509
+ labelKey: 'cfg.f.browser_loginTimeoutMs',
510
+ groupKey: 'cfg.group.browser',
511
+ },
408
512
  {
409
513
  path: 'browser.continueMinGapMs',
410
514
  type: 'number',