zames_pro 2.89.0 → 2.90.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 +47 -0
- package/README.md +32 -585
- package/dist/agent-loop.js +47 -4
- package/dist/ask-retry.js +49 -0
- package/dist/ask-state.js +80 -0
- package/dist/browser-base.js +69 -0
- package/dist/browser.js +170 -41
- package/dist/command-registry.js +58 -0
- package/dist/commands.js +25 -2
- package/dist/completion-check.js +77 -0
- package/dist/config-schema.js +8 -0
- package/dist/config.js +14 -0
- package/dist/dev-commands.js +27 -0
- package/dist/dev-watch.js +96 -0
- package/dist/first-run.js +80 -0
- package/dist/glm-browser.js +133 -32
- package/dist/i18n/common.js +6 -3
- package/dist/i18n/config.js +16 -0
- package/dist/i18n/wizard.js +25 -0
- package/dist/i18n.js +6 -1
- package/dist/index.js +1289 -919
- package/dist/info-commands.js +61 -0
- package/dist/input/layout.js +38 -1
- package/dist/input.js +2 -2
- package/dist/password-store.js +48 -0
- package/dist/prompt-budget.js +23 -0
- package/dist/provider-errors.js +53 -0
- package/dist/provider-fallback.js +46 -0
- package/dist/qwen-browser.js +155 -39
- package/dist/run-task.js +2 -1
- package/dist/scheduler-commands.js +41 -0
- package/dist/send-slot.js +36 -0
- package/dist/session-commands.js +42 -0
- package/dist/spinner.js +3 -3
- package/dist/swallow.js +45 -0
- package/dist/system-prompt.js +2 -2
- package/dist/tree-commands.js +36 -0
- package/dist/tui/FullScreenApp.js +13 -4
- package/dist/tui/InputLine.js +20 -3
- package/dist/tui/LogView.js +11 -5
- package/dist/tui/StatusBar.js +4 -1
- package/dist/tui/TuiApp.js +11 -2
- package/dist/tui/layout.js +14 -1
- package/dist/tui/store.js +2 -1
- package/dist/tui/ui-colors.js +2 -0
- package/dist/width.js +10 -5
- package/llms-full.txt +16 -292
- package/package.json +2 -2
package/dist/qwen-browser.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
// answer -> SSE choices[].delta.content where phase != thinking_summary
|
|
16
16
|
// The site sits behind Alibaba's WAF, which is exactly why we drive a REAL
|
|
17
17
|
// browser: the page solves its own JS challenge and carries the tokens.
|
|
18
|
+
import { noteSwallowed } from './swallow.js';
|
|
18
19
|
import fs from 'fs/promises';
|
|
19
20
|
import path from 'path';
|
|
20
21
|
import os from 'os';
|
|
@@ -57,6 +58,8 @@ function capNetBody(body) {
|
|
|
57
58
|
}
|
|
58
59
|
export class QwenBrowser {
|
|
59
60
|
providerName = 'qwen';
|
|
61
|
+
/** The chat a send must join (set by the agent loop; see types.ts). */
|
|
62
|
+
resumeChatId = null;
|
|
60
63
|
headless;
|
|
61
64
|
debug;
|
|
62
65
|
channel;
|
|
@@ -136,6 +139,8 @@ export class QwenBrowser {
|
|
|
136
139
|
// askRetries.
|
|
137
140
|
maxContextLimitRetries;
|
|
138
141
|
onContextLimit;
|
|
142
|
+
/** N8: switch to an authorized fallback provider (see agent-loop). */
|
|
143
|
+
onProviderExhausted = null;
|
|
139
144
|
constructor({ headless = false, debug = false, channel = null, answerTimeoutMs = 180000, askRetries = 3, stabilityChecks = 2, stabilityDelayMs = 400, minSendIntervalMs = 15000, rateLimitWaitMs = 300000, maxRateLimitRetries = 6, maxServerBusyRetries = 5, serverBusyWaitMs = 3000, maxIncompleteRetries = 4, incompleteWaitMs = 2000, maxContextLimitRetries = 1, onContextLimit, startWaitMs = 30000, sendButtonWaitMs = 8000, model = '', deepThinking = false, webSearch = true, auth, locale, onAuthSave, onLoginPrompt, loginTimeoutMs = 600000, userAgent, } = {}) {
|
|
140
145
|
this.headless = headless;
|
|
141
146
|
this.debug = debug;
|
|
@@ -212,7 +217,9 @@ export class QwenBrowser {
|
|
|
212
217
|
this.onNotice(text);
|
|
213
218
|
return;
|
|
214
219
|
}
|
|
215
|
-
catch {
|
|
220
|
+
catch (e) {
|
|
221
|
+
noteSwallowed('_notice', e, this.debug);
|
|
222
|
+
}
|
|
216
223
|
}
|
|
217
224
|
console.error(text);
|
|
218
225
|
}
|
|
@@ -221,14 +228,18 @@ export class QwenBrowser {
|
|
|
221
228
|
try {
|
|
222
229
|
this.onSendState(state);
|
|
223
230
|
}
|
|
224
|
-
catch {
|
|
231
|
+
catch (e) {
|
|
232
|
+
noteSwallowed('_emitSendState', e, this.debug);
|
|
233
|
+
}
|
|
225
234
|
}
|
|
226
235
|
}
|
|
227
236
|
async _setBusyTitle(busy) {
|
|
228
237
|
try {
|
|
229
238
|
await this.page.evaluate(setBusyTitleInDom, busy);
|
|
230
239
|
}
|
|
231
|
-
catch {
|
|
240
|
+
catch (e) {
|
|
241
|
+
noteSwallowed('_setBusyTitle', e, this.debug);
|
|
242
|
+
}
|
|
232
243
|
}
|
|
233
244
|
sendIntervalMs() {
|
|
234
245
|
return this.minSendIntervalMs;
|
|
@@ -307,6 +318,16 @@ export class QwenBrowser {
|
|
|
307
318
|
// usually already usable, and every later step (_awaitInput, waitForLogin)
|
|
308
319
|
// polls. Bailing out here turned a transient slowness into a hard startup
|
|
309
320
|
// failure.
|
|
321
|
+
// A relaunch lands on the BASE url: re-open the dialogue the operator was
|
|
322
|
+
// in, or a pending resume id, instead of silently starting a new chat.
|
|
323
|
+
const back = this.resumeChatId || (await this.getCurrentChatId().catch(() => null));
|
|
324
|
+
if (back) {
|
|
325
|
+
const ok = await this.openChat(back).catch(() => false);
|
|
326
|
+
if (ok) {
|
|
327
|
+
await this._dismissOverlays();
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
310
331
|
await this.page
|
|
311
332
|
.goto(CHAT_URL, { waitUntil: 'domcontentloaded' })
|
|
312
333
|
.catch(() => { });
|
|
@@ -317,7 +338,9 @@ export class QwenBrowser {
|
|
|
317
338
|
if (this.context)
|
|
318
339
|
await this.context.close();
|
|
319
340
|
}
|
|
320
|
-
catch {
|
|
341
|
+
catch (e) {
|
|
342
|
+
noteSwallowed('restart', e, this.debug);
|
|
343
|
+
}
|
|
321
344
|
await this._launchOnce();
|
|
322
345
|
await this._fixHeadlessUserAgent();
|
|
323
346
|
}
|
|
@@ -357,7 +380,9 @@ export class QwenBrowser {
|
|
|
357
380
|
await this.page.waitForTimeout(400);
|
|
358
381
|
}
|
|
359
382
|
}
|
|
360
|
-
catch {
|
|
383
|
+
catch (e) {
|
|
384
|
+
noteSwallowed('_dismissOverlays', e, this.debug);
|
|
385
|
+
}
|
|
361
386
|
}
|
|
362
387
|
async close() {
|
|
363
388
|
// A session cookie (expires = -1) is dropped when the context closes, so a
|
|
@@ -383,12 +408,16 @@ export class QwenBrowser {
|
|
|
383
408
|
}
|
|
384
409
|
}
|
|
385
410
|
}
|
|
386
|
-
catch {
|
|
411
|
+
catch (e) {
|
|
412
|
+
noteSwallowed('close', e, this.debug);
|
|
413
|
+
}
|
|
387
414
|
try {
|
|
388
415
|
if (this.context)
|
|
389
416
|
await this.context.close();
|
|
390
417
|
}
|
|
391
|
-
catch {
|
|
418
|
+
catch (e) {
|
|
419
|
+
noteSwallowed('close', e, this.debug);
|
|
420
|
+
}
|
|
392
421
|
}
|
|
393
422
|
_installNetHook() {
|
|
394
423
|
const pg = this.page;
|
|
@@ -412,7 +441,9 @@ export class QwenBrowser {
|
|
|
412
441
|
if (h['authorization'])
|
|
413
442
|
this._apiAuth = h['authorization'];
|
|
414
443
|
}
|
|
415
|
-
catch {
|
|
444
|
+
catch (e) {
|
|
445
|
+
noteSwallowed('_installNetHook', e, this.debug);
|
|
446
|
+
}
|
|
416
447
|
});
|
|
417
448
|
}
|
|
418
449
|
async _onResponse(resp) {
|
|
@@ -484,7 +515,9 @@ export class QwenBrowser {
|
|
|
484
515
|
this._netCaptureAt = Date.now();
|
|
485
516
|
}
|
|
486
517
|
}
|
|
487
|
-
catch {
|
|
518
|
+
catch (e) {
|
|
519
|
+
noteSwallowed('_onResponse', e, this.debug);
|
|
520
|
+
}
|
|
488
521
|
}
|
|
489
522
|
// Are we signed in? Detected from the PAGE, the same way a human sees it: a
|
|
490
523
|
// signed-out visitor has the header "Log in" / "Sign up" buttons, a signed-in
|
|
@@ -522,7 +555,9 @@ export class QwenBrowser {
|
|
|
522
555
|
return true;
|
|
523
556
|
}
|
|
524
557
|
}
|
|
525
|
-
catch {
|
|
558
|
+
catch (e) {
|
|
559
|
+
noteSwallowed('isLoggedIn', e, this.debug);
|
|
560
|
+
}
|
|
526
561
|
await this.page.waitForTimeout(500);
|
|
527
562
|
}
|
|
528
563
|
return false;
|
|
@@ -598,7 +633,9 @@ export class QwenBrowser {
|
|
|
598
633
|
.catch(() => { });
|
|
599
634
|
}
|
|
600
635
|
}
|
|
601
|
-
catch {
|
|
636
|
+
catch (e) {
|
|
637
|
+
noteSwallowed('_browserLogin', e, this.debug);
|
|
638
|
+
}
|
|
602
639
|
},
|
|
603
640
|
captureCredentials: async () => {
|
|
604
641
|
try {
|
|
@@ -610,7 +647,9 @@ export class QwenBrowser {
|
|
|
610
647
|
try {
|
|
611
648
|
this.onAuthSave?.(user, '');
|
|
612
649
|
}
|
|
613
|
-
catch {
|
|
650
|
+
catch (e) {
|
|
651
|
+
noteSwallowed('_browserLogin', e, this.debug);
|
|
652
|
+
}
|
|
614
653
|
});
|
|
615
654
|
await this.page.evaluate(() => {
|
|
616
655
|
const w = window;
|
|
@@ -627,11 +666,15 @@ export class QwenBrowser {
|
|
|
627
666
|
if (fn && e)
|
|
628
667
|
fn(e.value || '');
|
|
629
668
|
}
|
|
630
|
-
catch {
|
|
669
|
+
catch (e) {
|
|
670
|
+
noteSwallowed('_browserLogin', e, this.debug);
|
|
671
|
+
}
|
|
631
672
|
}, true);
|
|
632
673
|
});
|
|
633
674
|
}
|
|
634
|
-
catch {
|
|
675
|
+
catch (e) {
|
|
676
|
+
noteSwallowed('_browserLogin', e, this.debug);
|
|
677
|
+
}
|
|
635
678
|
},
|
|
636
679
|
persistSession: () => this._persistSessionIfNeeded(),
|
|
637
680
|
notice: (key) => this._notice(this._t(key)),
|
|
@@ -659,12 +702,16 @@ export class QwenBrowser {
|
|
|
659
702
|
if (session.length)
|
|
660
703
|
this._carryCookies = session;
|
|
661
704
|
}
|
|
662
|
-
catch {
|
|
705
|
+
catch (e) {
|
|
706
|
+
noteSwallowed('relaunchSafe', e, this.debug);
|
|
707
|
+
}
|
|
663
708
|
try {
|
|
664
709
|
if (this.context)
|
|
665
710
|
await this.context.close();
|
|
666
711
|
}
|
|
667
|
-
catch {
|
|
712
|
+
catch (e) {
|
|
713
|
+
noteSwallowed('relaunchSafe', e, this.debug);
|
|
714
|
+
}
|
|
668
715
|
const deadline = Date.now() + 15000;
|
|
669
716
|
while (Date.now() < deadline) {
|
|
670
717
|
if (!(await profileLooksLocked(QWEN_USER_DATA_DIR)))
|
|
@@ -736,7 +783,9 @@ export class QwenBrowser {
|
|
|
736
783
|
await email.fill(this.auth.username).catch(() => { });
|
|
737
784
|
}
|
|
738
785
|
}
|
|
739
|
-
catch {
|
|
786
|
+
catch (e) {
|
|
787
|
+
noteSwallowed('waitForLogin', e, this.debug);
|
|
788
|
+
}
|
|
740
789
|
},
|
|
741
790
|
captureCredentials: async () => { },
|
|
742
791
|
persistSession: () => this._persistSessionIfNeeded(),
|
|
@@ -807,7 +856,9 @@ export class QwenBrowser {
|
|
|
807
856
|
try {
|
|
808
857
|
this.onAuthSave?.(this.auth.username, this.auth.password);
|
|
809
858
|
}
|
|
810
|
-
catch {
|
|
859
|
+
catch (e) {
|
|
860
|
+
noteSwallowed('_autoLogin', e, this.debug);
|
|
861
|
+
}
|
|
811
862
|
}
|
|
812
863
|
return ok;
|
|
813
864
|
}
|
|
@@ -894,7 +945,9 @@ export class QwenBrowser {
|
|
|
894
945
|
try {
|
|
895
946
|
this.onAuthSave(addr, '');
|
|
896
947
|
}
|
|
897
|
-
catch {
|
|
948
|
+
catch (e) {
|
|
949
|
+
noteSwallowed('_promptAndLogin', e, this.debug);
|
|
950
|
+
}
|
|
898
951
|
}
|
|
899
952
|
}
|
|
900
953
|
else {
|
|
@@ -925,7 +978,9 @@ export class QwenBrowser {
|
|
|
925
978
|
await this.page.waitForTimeout(2500);
|
|
926
979
|
}
|
|
927
980
|
}
|
|
928
|
-
catch {
|
|
981
|
+
catch (e) {
|
|
982
|
+
noteSwallowed('_openLoginPage', e, this.debug);
|
|
983
|
+
}
|
|
929
984
|
}
|
|
930
985
|
async _fillEmailAndContinue(email) {
|
|
931
986
|
const input = this.page
|
|
@@ -958,7 +1013,9 @@ export class QwenBrowser {
|
|
|
958
1013
|
await this.page.waitForTimeout(1500);
|
|
959
1014
|
}
|
|
960
1015
|
}
|
|
961
|
-
catch {
|
|
1016
|
+
catch (e) {
|
|
1017
|
+
noteSwallowed('_switchToPasswordForm', e, this.debug);
|
|
1018
|
+
}
|
|
962
1019
|
}
|
|
963
1020
|
// Submit the one-time code. Qwen renders EITHER six single-character boxes
|
|
964
1021
|
// OR one code input; handle both. Best-effort (the exact markup may change).
|
|
@@ -992,7 +1049,9 @@ export class QwenBrowser {
|
|
|
992
1049
|
await submit.click({ timeout: 3000 }).catch(() => { });
|
|
993
1050
|
}
|
|
994
1051
|
}
|
|
995
|
-
catch {
|
|
1052
|
+
catch (e) {
|
|
1053
|
+
noteSwallowed('_submitCode', e, this.debug);
|
|
1054
|
+
}
|
|
996
1055
|
}
|
|
997
1056
|
async _waitLoggedIn(timeoutMs) {
|
|
998
1057
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -1011,7 +1070,9 @@ export class QwenBrowser {
|
|
|
1011
1070
|
await btn.click({ timeout: 2500 });
|
|
1012
1071
|
clicked = true;
|
|
1013
1072
|
}
|
|
1014
|
-
catch {
|
|
1073
|
+
catch (e) {
|
|
1074
|
+
noteSwallowed('newChat', e, this.debug);
|
|
1075
|
+
}
|
|
1015
1076
|
let ok = false;
|
|
1016
1077
|
if (clicked) {
|
|
1017
1078
|
const deadline = Date.now() + 5000;
|
|
@@ -1050,7 +1111,9 @@ export class QwenBrowser {
|
|
|
1050
1111
|
if (await loc.isVisible({ timeout }).catch(() => false))
|
|
1051
1112
|
return loc;
|
|
1052
1113
|
}
|
|
1053
|
-
catch {
|
|
1114
|
+
catch (e) {
|
|
1115
|
+
noteSwallowed('_findVisible', e, this.debug);
|
|
1116
|
+
}
|
|
1054
1117
|
}
|
|
1055
1118
|
return null;
|
|
1056
1119
|
}
|
|
@@ -1088,7 +1151,9 @@ export class QwenBrowser {
|
|
|
1088
1151
|
this._lastHistoryError = res.error;
|
|
1089
1152
|
}
|
|
1090
1153
|
}
|
|
1091
|
-
catch {
|
|
1154
|
+
catch (e) {
|
|
1155
|
+
noteSwallowed('listChats', e, this.debug);
|
|
1156
|
+
}
|
|
1092
1157
|
// Fallback: scrape the sidebar (works for a guest with a rendered list).
|
|
1093
1158
|
await this._ensureSidebarOpen();
|
|
1094
1159
|
try {
|
|
@@ -1112,7 +1177,9 @@ export class QwenBrowser {
|
|
|
1112
1177
|
await this.page.waitForTimeout(500);
|
|
1113
1178
|
return;
|
|
1114
1179
|
}
|
|
1115
|
-
catch {
|
|
1180
|
+
catch (e) {
|
|
1181
|
+
noteSwallowed('_ensureSidebarOpen', e, this.debug);
|
|
1182
|
+
}
|
|
1116
1183
|
}
|
|
1117
1184
|
}
|
|
1118
1185
|
async openChat(id) {
|
|
@@ -1321,14 +1388,18 @@ export class QwenBrowser {
|
|
|
1321
1388
|
}
|
|
1322
1389
|
}
|
|
1323
1390
|
}
|
|
1324
|
-
catch {
|
|
1391
|
+
catch (e) {
|
|
1392
|
+
noteSwallowed('stopGeneration', e, this.debug);
|
|
1393
|
+
}
|
|
1325
1394
|
const btn = await this._findVisible(STOP_SELECTORS, 500);
|
|
1326
1395
|
if (btn) {
|
|
1327
1396
|
try {
|
|
1328
1397
|
await btn.click({ timeout: 1500 });
|
|
1329
1398
|
return true;
|
|
1330
1399
|
}
|
|
1331
|
-
catch {
|
|
1400
|
+
catch (e) {
|
|
1401
|
+
noteSwallowed('stopGeneration', e, this.debug);
|
|
1402
|
+
}
|
|
1332
1403
|
}
|
|
1333
1404
|
const clicked = await this.page
|
|
1334
1405
|
.evaluate(clickStopInDom, 'send-button|stop-button')
|
|
@@ -1339,7 +1410,9 @@ export class QwenBrowser {
|
|
|
1339
1410
|
await this.page.keyboard.press('Escape');
|
|
1340
1411
|
return true;
|
|
1341
1412
|
}
|
|
1342
|
-
catch {
|
|
1413
|
+
catch (e) {
|
|
1414
|
+
noteSwallowed('stopGeneration', e, this.debug);
|
|
1415
|
+
}
|
|
1343
1416
|
return false;
|
|
1344
1417
|
}
|
|
1345
1418
|
getToggleStatesSync() {
|
|
@@ -1400,7 +1473,9 @@ export class QwenBrowser {
|
|
|
1400
1473
|
if (ok)
|
|
1401
1474
|
this._appliedThinking = this.deepThinking ? 1 : 0;
|
|
1402
1475
|
}
|
|
1403
|
-
catch {
|
|
1476
|
+
catch (e) {
|
|
1477
|
+
noteSwallowed('_applyThinkingMode', e, this.debug);
|
|
1478
|
+
}
|
|
1404
1479
|
}
|
|
1405
1480
|
// Enable web search via the "+" (mode) menu item, reading its selected state
|
|
1406
1481
|
// first so a manual choice is not flipped back. The item shows a check/active
|
|
@@ -1449,7 +1524,9 @@ export class QwenBrowser {
|
|
|
1449
1524
|
// Close the menu (the send button lives behind it).
|
|
1450
1525
|
await this.page.keyboard.press('Escape').catch(() => { });
|
|
1451
1526
|
}
|
|
1452
|
-
catch {
|
|
1527
|
+
catch (e) {
|
|
1528
|
+
noteSwallowed('_applyWebSearch', e, this.debug);
|
|
1529
|
+
}
|
|
1453
1530
|
}
|
|
1454
1531
|
// Click the first visible menu option whose label matches `labelRe`.
|
|
1455
1532
|
async _clickMenuOption(labelRe) {
|
|
@@ -1516,7 +1593,9 @@ export class QwenBrowser {
|
|
|
1516
1593
|
await this.page.keyboard.press('Escape').catch(() => { });
|
|
1517
1594
|
}
|
|
1518
1595
|
}
|
|
1519
|
-
catch {
|
|
1596
|
+
catch (e) {
|
|
1597
|
+
noteSwallowed('_applyModel', e, this.debug);
|
|
1598
|
+
}
|
|
1520
1599
|
}
|
|
1521
1600
|
async dumpDom(filePath) {
|
|
1522
1601
|
if (!this.page)
|
|
@@ -1574,7 +1653,9 @@ export class QwenBrowser {
|
|
|
1574
1653
|
try {
|
|
1575
1654
|
this.onSendPause(secs);
|
|
1576
1655
|
}
|
|
1577
|
-
catch {
|
|
1656
|
+
catch (e) {
|
|
1657
|
+
noteSwallowed('_waitForSendSlot', e, this.debug);
|
|
1658
|
+
}
|
|
1578
1659
|
}
|
|
1579
1660
|
};
|
|
1580
1661
|
report(gap);
|
|
@@ -1617,6 +1698,8 @@ export class QwenBrowser {
|
|
|
1617
1698
|
// The exhausted-context recovery is a COMPACTION (a new chat), not a
|
|
1618
1699
|
// resend, so it has its own budget.
|
|
1619
1700
|
let contextLimitRetries = 0;
|
|
1701
|
+
// N8: the fallback switch is tried ONCE per ask().
|
|
1702
|
+
let fallbackAttempted = false;
|
|
1620
1703
|
this._askInFlight++;
|
|
1621
1704
|
try {
|
|
1622
1705
|
while (attempt < this.askRetries) {
|
|
@@ -1725,6 +1808,27 @@ export class QwenBrowser {
|
|
|
1725
1808
|
})));
|
|
1726
1809
|
}
|
|
1727
1810
|
}
|
|
1811
|
+
// N8: the active provider exhausted its budgets. Give the injected
|
|
1812
|
+
// hook ONE chance to continue on an AUTHORIZED fallback provider; it
|
|
1813
|
+
// switches the backend and the SAME prompt is resent there.
|
|
1814
|
+
if (!fallbackAttempted && this.onProviderExhausted) {
|
|
1815
|
+
fallbackAttempted = true;
|
|
1816
|
+
let switched = false;
|
|
1817
|
+
try {
|
|
1818
|
+
switched = await this.onProviderExhausted(lastErr);
|
|
1819
|
+
}
|
|
1820
|
+
catch (e) {
|
|
1821
|
+
noteSwallowed('_onProviderExhausted', e, this.debug);
|
|
1822
|
+
}
|
|
1823
|
+
if (switched) {
|
|
1824
|
+
attempt = 0;
|
|
1825
|
+
rateLimitRetries = 0;
|
|
1826
|
+
serverBusyRetries = 0;
|
|
1827
|
+
incompleteRetries = 0;
|
|
1828
|
+
contextLimitRetries = 0;
|
|
1829
|
+
continue;
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1728
1832
|
this._notice(theme.warn(this._t('qwen.ask_retry', {
|
|
1729
1833
|
attempt: attempt + 1,
|
|
1730
1834
|
max: this.askRetries,
|
|
@@ -1777,7 +1881,9 @@ export class QwenBrowser {
|
|
|
1777
1881
|
await this._awaitUploadPreview(files.map((f) => f.name), 5000);
|
|
1778
1882
|
return failed;
|
|
1779
1883
|
}
|
|
1780
|
-
catch {
|
|
1884
|
+
catch (e) {
|
|
1885
|
+
noteSwallowed('_attachFiles', e, this.debug);
|
|
1886
|
+
}
|
|
1781
1887
|
// Fallback: set the files directly on the hidden <input type=file>.
|
|
1782
1888
|
for (const sel of ATTACH_SELECTORS) {
|
|
1783
1889
|
const el = this.page.locator(sel).last();
|
|
@@ -1788,7 +1894,9 @@ export class QwenBrowser {
|
|
|
1788
1894
|
await this._awaitUploadPreview(files.map((f) => f.name), 3000);
|
|
1789
1895
|
return failed;
|
|
1790
1896
|
}
|
|
1791
|
-
catch {
|
|
1897
|
+
catch (e) {
|
|
1898
|
+
noteSwallowed('_attachFiles', e, this.debug);
|
|
1899
|
+
}
|
|
1792
1900
|
}
|
|
1793
1901
|
this._notice(this._t('qwen.attach_no_input'));
|
|
1794
1902
|
for (const f of files)
|
|
@@ -1807,7 +1915,9 @@ export class QwenBrowser {
|
|
|
1807
1915
|
await input.fill(text, { timeout: 5000 });
|
|
1808
1916
|
return;
|
|
1809
1917
|
}
|
|
1810
|
-
catch {
|
|
1918
|
+
catch (e) {
|
|
1919
|
+
noteSwallowed('_setInputText', e, this.debug);
|
|
1920
|
+
}
|
|
1811
1921
|
}
|
|
1812
1922
|
// contenteditable fallback: paste so a big block is inserted in one go.
|
|
1813
1923
|
await input.click({ timeout: 3000 }).catch(() => { });
|
|
@@ -1847,7 +1957,9 @@ export class QwenBrowser {
|
|
|
1847
1957
|
await this._awaitInput(10_000);
|
|
1848
1958
|
input = await this._findVisible(INPUT_SELECTORS, 3000);
|
|
1849
1959
|
}
|
|
1850
|
-
catch {
|
|
1960
|
+
catch (e) {
|
|
1961
|
+
noteSwallowed('_askOnce', e, this.debug);
|
|
1962
|
+
}
|
|
1851
1963
|
}
|
|
1852
1964
|
if (!input)
|
|
1853
1965
|
throw new Error(this._t('qwen.input_missing'));
|
|
@@ -1859,7 +1971,9 @@ export class QwenBrowser {
|
|
|
1859
1971
|
try {
|
|
1860
1972
|
this.onSendStart();
|
|
1861
1973
|
}
|
|
1862
|
-
catch {
|
|
1974
|
+
catch (e) {
|
|
1975
|
+
noteSwallowed('_askOnce', e, this.debug);
|
|
1976
|
+
}
|
|
1863
1977
|
}
|
|
1864
1978
|
this._emitSendState('generating');
|
|
1865
1979
|
void this._setBusyTitle(true);
|
|
@@ -1918,7 +2032,9 @@ export class QwenBrowser {
|
|
|
1918
2032
|
clicked = true;
|
|
1919
2033
|
break;
|
|
1920
2034
|
}
|
|
1921
|
-
catch {
|
|
2035
|
+
catch (e) {
|
|
2036
|
+
noteSwallowed('_askOnce', e, this.debug);
|
|
2037
|
+
}
|
|
1922
2038
|
}
|
|
1923
2039
|
// (2) Enter fallback — ONLY when no send button was clicked this round
|
|
1924
2040
|
// (a build that sends solely on Enter, or a missing button). This is
|
package/dist/run-task.js
CHANGED
|
@@ -284,7 +284,7 @@ async function evaluateGoal(browser, condition, config, transcript) {
|
|
|
284
284
|
}
|
|
285
285
|
export async function runTask(deps, browser, tools, taskText, workdir, opts, attachments = []) {
|
|
286
286
|
const { t, locale, debug, maxIter, config, runAgentLoop, createSpinner } = deps;
|
|
287
|
-
const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor, onChatReady, onAutoCompact = null, onContextLimit = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, goal = null, goalIsCondition = false, onGoalCleared = null, goalMaxTurns = GOAL_MAX_TURNS, goalQuery = null, todosQuery = null, selfImprovement = false, onSubagent = null, onAskUser = undefined, } = opts;
|
|
287
|
+
const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor, onChatReady, onAutoCompact = null, onContextLimit = null, onProviderExhausted = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, goal = null, goalIsCondition = false, onGoalCleared = null, goalMaxTurns = GOAL_MAX_TURNS, goalQuery = null, todosQuery = null, selfImprovement = false, onSubagent = null, onAskUser = undefined, } = opts;
|
|
288
288
|
// The ACTIVE goal for the duration of this run. It is mutable because a
|
|
289
289
|
// completion goal can be cleared mid-run (verdict MET/IMPOSSIBLE, or the
|
|
290
290
|
// operator runs `/goal clear` while the task is in flight). `goalQuery`
|
|
@@ -412,6 +412,7 @@ export async function runTask(deps, browser, tools, taskText, workdir, opts, att
|
|
|
412
412
|
// loop awaits it.
|
|
413
413
|
onAutoCompact,
|
|
414
414
|
onContextLimit,
|
|
415
|
+
onProviderExhausted,
|
|
415
416
|
autoCompactPct,
|
|
416
417
|
contextLimit,
|
|
417
418
|
getTokenUsage,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// N2 group (d): the SCHEDULER and CONTEXT commands — /compact, /goal, /loop,
|
|
2
|
+
// /cron, /jobs. They all mutate long-lived session state (the chat, the goal,
|
|
3
|
+
// the scheduler's timers), so the state is INJECTED. The registry owns the
|
|
4
|
+
// names and the argument text; the handlers stay thin wrappers over the
|
|
5
|
+
// existing implementations.
|
|
6
|
+
//
|
|
7
|
+
// `/goal` is special: setting a condition FALLS THROUGH to the task path (the
|
|
8
|
+
// condition itself becomes the turn), so its handler returns a directive the
|
|
9
|
+
// caller turns into `taskOverride` instead of finishing the loop.
|
|
10
|
+
/** The directive a `/goal <condition>` turn carries into the task path. */
|
|
11
|
+
export const GOAL_TASK_OVERRIDE = 'Work toward this completion goal until it is satisfied. ' +
|
|
12
|
+
'Do not call respond with a final summary until the condition is met.';
|
|
13
|
+
/**
|
|
14
|
+
* Build the group. `/goal` cannot be a plain fire-and-forget handler: the
|
|
15
|
+
* caller needs to know whether a directive was set, so the returned list is
|
|
16
|
+
* paired with `goalDirective()` below.
|
|
17
|
+
*/
|
|
18
|
+
export function createSchedulerCommands(deps) {
|
|
19
|
+
return [
|
|
20
|
+
{ name: 'compact', handler: () => deps.compact() },
|
|
21
|
+
{
|
|
22
|
+
name: 'goal',
|
|
23
|
+
handler: async (ctx) => {
|
|
24
|
+
const res = await deps.goal(ctx.input);
|
|
25
|
+
// 'set' is signalled through the shared state; see goalDirective().
|
|
26
|
+
void res;
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
{ name: 'loop', handler: (ctx) => deps.loop(ctx.rest) },
|
|
30
|
+
{ name: 'cron', handler: (ctx) => deps.cron(ctx.rest) },
|
|
31
|
+
{ name: 'jobs', handler: (ctx) => deps.jobs(ctx.rest) },
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* After dispatching group (d), the caller asks this: a freshly SET goal must
|
|
36
|
+
* turn the condition itself into the next task instead of returning to the
|
|
37
|
+
* prompt (Claude Code parity).
|
|
38
|
+
*/
|
|
39
|
+
export function goalDirective(deps, wasSet) {
|
|
40
|
+
return wasSet && deps.currentGoal() ? GOAL_TASK_OVERRIDE : null;
|
|
41
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// N1 stage 3: the SEND SLOT — the pause before a send that keeps the chat's
|
|
2
|
+
// rate limit happy.
|
|
3
|
+
//
|
|
4
|
+
// Every provider had its own `_waitForSendSlot()` with the same shape (skip a
|
|
5
|
+
// non-agent send, compute the remaining gap, report the animated countdown,
|
|
6
|
+
// mark the state 'paused', sleep interruptibly) and each computed the gap its
|
|
7
|
+
// own way (DeepSeek: interval + thinking bonus; Qwen: interval; GLM: the
|
|
8
|
+
// adaptive sendRuntime). The MECHANICS are shared here; the provider injects
|
|
9
|
+
// the gap and the plumbing.
|
|
10
|
+
/**
|
|
11
|
+
* Wait for the send slot. Returns true when the wait was INTERRUPTED (the
|
|
12
|
+
* caller must then abort the send).
|
|
13
|
+
*/
|
|
14
|
+
export async function waitForSendSlot(agent, deps) {
|
|
15
|
+
if (!agent)
|
|
16
|
+
return false;
|
|
17
|
+
const gap = deps.gapMs();
|
|
18
|
+
if (gap <= 0)
|
|
19
|
+
return false;
|
|
20
|
+
const report = (leftMs) => {
|
|
21
|
+
const secs = Math.max(0, Math.ceil(leftMs / 1000));
|
|
22
|
+
if (deps.onPause) {
|
|
23
|
+
try {
|
|
24
|
+
deps.onPause(secs);
|
|
25
|
+
}
|
|
26
|
+
catch { }
|
|
27
|
+
}
|
|
28
|
+
else if (deps.onPauseText) {
|
|
29
|
+
deps.onPauseText(secs);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
report(gap);
|
|
33
|
+
if (deps.emitState)
|
|
34
|
+
deps.emitState('paused');
|
|
35
|
+
return await deps.sleep(gap, report);
|
|
36
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// N2 group (b): the SESSION and CHAT commands — /new (/clear), /sessions,
|
|
2
|
+
// /chats, /last, /resume-id. The handlers drive the live browser and the
|
|
3
|
+
// session store, so all of that is INJECTED. The ROUTING (a command name, its
|
|
4
|
+
// arguments, the aliases) is the registry's job and is unit-tested.
|
|
5
|
+
import { theme } from './theme.js';
|
|
6
|
+
export function createSessionCommands(deps) {
|
|
7
|
+
return [
|
|
8
|
+
{
|
|
9
|
+
name: 'new',
|
|
10
|
+
aliases: ['clear'],
|
|
11
|
+
handler: () => deps.newChat(),
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: 'sessions',
|
|
15
|
+
aliases: ['session'],
|
|
16
|
+
handler: (ctx) => deps.sessions(ctx.rest.toLowerCase()),
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: 'chats',
|
|
20
|
+
handler: (ctx) => deps.chats(ctx.rest.toLowerCase()),
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'last',
|
|
24
|
+
handler: () => deps.last(),
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'resume-id',
|
|
28
|
+
handler: async (ctx) => {
|
|
29
|
+
const id = deps.parseResumeId(ctx.rest);
|
|
30
|
+
if (!id) {
|
|
31
|
+
deps.printResumeUsage();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
await deps.resumeId(id);
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
/** The `<id>` usage line, localized (shared by the registry handler and tests). */
|
|
40
|
+
export function resumeIdUsage(t) {
|
|
41
|
+
return theme.error(t('sessions.resume_id_usage'));
|
|
42
|
+
}
|
package/dist/spinner.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import ora from 'ora';
|
|
2
2
|
import { theme, divider } from './theme.js';
|
|
3
3
|
import { renderMarkdown } from './markdown.js';
|
|
4
|
-
import { translate } from './i18n.js';
|
|
4
|
+
import { translate, DEFAULT_LOCALE } from './i18n.js';
|
|
5
5
|
import { truncateToWidth, wrapToWidth } from './input/layout.js';
|
|
6
6
|
import { contentWidth } from './width.js';
|
|
7
7
|
// Phrases for the "agent is working" spinner. Chosen in random order.
|
|
8
8
|
// The text comes from i18n via the key 'spinner.phrases' (strings separated by "|").
|
|
9
|
-
export function randomThinkingPhrase(locale =
|
|
9
|
+
export function randomThinkingPhrase(locale = DEFAULT_LOCALE) {
|
|
10
10
|
const raw = translate(locale)('spinner.phrases');
|
|
11
11
|
const phrases = raw.split('|').filter((s) => s.trim().length);
|
|
12
12
|
if (!phrases.length)
|
|
@@ -28,7 +28,7 @@ export function renderDots(n) {
|
|
|
28
28
|
const d = DOTS[n] ?? '';
|
|
29
29
|
return theme.brown(d + DOTS_PAD.slice(d.length));
|
|
30
30
|
}
|
|
31
|
-
export function createSpinner(locale =
|
|
31
|
+
export function createSpinner(locale = DEFAULT_LOCALE) {
|
|
32
32
|
// Set by the caller (index.ts) to the same todos summary the LineEditor
|
|
33
33
|
// shows, so the non-TTY path has parity.
|
|
34
34
|
let onTasksQuery = null;
|