zames_pro 2.80.8 → 2.81.1
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 +50 -0
- package/README.md +71 -21
- package/dist/browser-login.js +179 -0
- package/dist/browser-pure.js +112 -8
- package/dist/browser.js +274 -16
- package/dist/commands.js +24 -8
- package/dist/config-command.js +5 -1
- package/dist/config.js +110 -2
- package/dist/glm-browser.js +1770 -0
- package/dist/glm-chats.js +170 -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 +332 -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/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,
|
|
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
|
-
|
|
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)
|
|
519
|
-
//
|
|
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)
|
|
535
|
-
//
|
|
536
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
326
|
-
|
|
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', {
|
|
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`
|
|
1109
|
-
//
|
|
1110
|
-
|
|
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-command.js
CHANGED
|
@@ -90,7 +90,11 @@ export async function handleConfigCommand(input, deps) {
|
|
|
90
90
|
}
|
|
91
91
|
print(theme.assistant(t('cfg.saved', {
|
|
92
92
|
v: key,
|
|
93
|
-
|
|
93
|
+
// Mask a SECRET here too: this line goes to the terminal scrollback,
|
|
94
|
+
// the transcript and screenshots. `configGetValue` already masks the
|
|
95
|
+
// password for /config get and the list, but the save confirmation
|
|
96
|
+
// echoed it in the clear.
|
|
97
|
+
value: configGetValue(key, getValue(key), t),
|
|
94
98
|
file: homeConfigPath,
|
|
95
99
|
})));
|
|
96
100
|
return;
|
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
|
-
|
|
81
|
-
|
|
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,11 @@ export const DEFAULTS = {
|
|
|
86
115
|
maxSubagents: 3,
|
|
87
116
|
deepThinking: false,
|
|
88
117
|
webSearch: true,
|
|
118
|
+
// OFF by default: the telemetry file only serves diagnosis and manual
|
|
119
|
+
// calibration of the send pauses (the adaptive throttle needs the EVENTS,
|
|
120
|
+
// not the file, and keeps working). A regular user gains nothing from it,
|
|
121
|
+
// so it is opted into with `/config set browser.sendTelemetry true`.
|
|
122
|
+
sendTelemetry: false,
|
|
89
123
|
auth: {
|
|
90
124
|
username: '',
|
|
91
125
|
password: '',
|
|
@@ -149,6 +183,8 @@ export function loadConfig() {
|
|
|
149
183
|
export function providerAskDeadlineMs(cfg) {
|
|
150
184
|
if (cfg.provider === 'qwen')
|
|
151
185
|
return cfg.qwen.answerTimeoutMs + 120_000;
|
|
186
|
+
if (cfg.provider === 'glm')
|
|
187
|
+
return cfg.glm.answerTimeoutMs + 120_000;
|
|
152
188
|
return cfg.browser.askDeadlineMs;
|
|
153
189
|
}
|
|
154
190
|
function deepMerge(target, source) {
|
|
@@ -236,6 +272,34 @@ export const CONFIG_SCHEMA = [
|
|
|
236
272
|
labelKey: 'cfg.f.browser_authSaveSession',
|
|
237
273
|
groupKey: 'cfg.group.browser',
|
|
238
274
|
},
|
|
275
|
+
// Each non-DeepSeek provider owns its credentials, so a DeepSeek login is
|
|
276
|
+
// never typed into a Qwen/GLM form (and vice versa). They MUST be in the
|
|
277
|
+
// schema: writeConfigValue rejects an unknown key, which silently dropped
|
|
278
|
+
// every GLM/Qwen credential saved from the login window.
|
|
279
|
+
{
|
|
280
|
+
path: 'qwen.auth.username',
|
|
281
|
+
type: 'string',
|
|
282
|
+
labelKey: 'cfg.f.qwen_authUsername',
|
|
283
|
+
groupKey: 'cfg.group.browser',
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
path: 'qwen.auth.password',
|
|
287
|
+
type: 'string',
|
|
288
|
+
labelKey: 'cfg.f.qwen_authPassword',
|
|
289
|
+
groupKey: 'cfg.group.browser',
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
path: 'glm.auth.username',
|
|
293
|
+
type: 'string',
|
|
294
|
+
labelKey: 'cfg.f.glm_authUsername',
|
|
295
|
+
groupKey: 'cfg.group.browser',
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
path: 'glm.auth.password',
|
|
299
|
+
type: 'string',
|
|
300
|
+
labelKey: 'cfg.f.glm_authPassword',
|
|
301
|
+
groupKey: 'cfg.group.browser',
|
|
302
|
+
},
|
|
239
303
|
{
|
|
240
304
|
path: 'provider',
|
|
241
305
|
type: 'enum',
|
|
@@ -273,6 +337,36 @@ export const CONFIG_SCHEMA = [
|
|
|
273
337
|
labelKey: 'cfg.f.qwen_answerTimeoutMs',
|
|
274
338
|
groupKey: 'cfg.group.browser',
|
|
275
339
|
},
|
|
340
|
+
{
|
|
341
|
+
path: 'glm.model',
|
|
342
|
+
type: 'string',
|
|
343
|
+
labelKey: 'cfg.f.glm_model',
|
|
344
|
+
groupKey: 'cfg.group.browser',
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
path: 'glm.contextLimit',
|
|
348
|
+
type: 'number',
|
|
349
|
+
min: 10_000,
|
|
350
|
+
max: 10_000_000,
|
|
351
|
+
labelKey: 'cfg.f.glm_contextLimit',
|
|
352
|
+
groupKey: 'cfg.group.browser',
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
path: 'glm.minSendIntervalMs',
|
|
356
|
+
type: 'number',
|
|
357
|
+
min: 0,
|
|
358
|
+
max: 600000,
|
|
359
|
+
labelKey: 'cfg.f.glm_minSendIntervalMs',
|
|
360
|
+
groupKey: 'cfg.group.browser',
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
path: 'glm.answerTimeoutMs',
|
|
364
|
+
type: 'number',
|
|
365
|
+
min: 1000,
|
|
366
|
+
max: 3600000,
|
|
367
|
+
labelKey: 'cfg.f.glm_answerTimeoutMs',
|
|
368
|
+
groupKey: 'cfg.group.browser',
|
|
369
|
+
},
|
|
276
370
|
{
|
|
277
371
|
path: 'debug',
|
|
278
372
|
type: 'boolean',
|
|
@@ -405,6 +499,20 @@ export const CONFIG_SCHEMA = [
|
|
|
405
499
|
labelKey: 'cfg.f.browser_autoContinue',
|
|
406
500
|
groupKey: 'cfg.group.browser',
|
|
407
501
|
},
|
|
502
|
+
{
|
|
503
|
+
path: 'browser.sendTelemetry',
|
|
504
|
+
type: 'boolean',
|
|
505
|
+
labelKey: 'cfg.f.browser_sendTelemetry',
|
|
506
|
+
groupKey: 'cfg.group.browser',
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
path: 'browser.loginTimeoutMs',
|
|
510
|
+
type: 'number',
|
|
511
|
+
min: 30_000,
|
|
512
|
+
max: 3_600_000,
|
|
513
|
+
labelKey: 'cfg.f.browser_loginTimeoutMs',
|
|
514
|
+
groupKey: 'cfg.group.browser',
|
|
515
|
+
},
|
|
408
516
|
{
|
|
409
517
|
path: 'browser.continueMinGapMs',
|
|
410
518
|
type: 'number',
|