dsh-client-auto-continue 0.8.2 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -16
- package/README.zh.md +23 -7
- package/lib/client.js +107 -39
- package/lib/client.js.map +3 -3
- package/lib/index.js +81 -31
- package/lib/types/client/locales.d.ts +7 -5
- package/lib/types/client/settings-card.d.ts +1 -0
- package/lib/types/index.d.ts +9 -1
- package/lib/types/shared/core.d.ts +26 -2
- package/package.json +2 -2
- package/src/client/index.ts +12 -1
- package/src/client/locales.ts +16 -1
- package/src/client/settings-card.tsx +46 -18
- package/src/client/styles.ts +1 -0
- package/src/host/engine.ts +47 -13
- package/src/index.ts +10 -6
- package/src/shared/core.ts +53 -13
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { useEffect, useState, type ReactNode } from 'react';
|
|
11
11
|
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
12
|
-
import {
|
|
12
|
+
import { type AutoContinueSettings } from './engine.ts';
|
|
13
13
|
import { createSnapshotStore, type SettingsScope, type SnapshotStore } from './dsh-store-compat.ts';
|
|
14
14
|
import {
|
|
15
15
|
pausedSessions,
|
|
@@ -50,6 +50,7 @@ export interface AutoContinueSettingsCardState extends CardShell {
|
|
|
50
50
|
freshMs: CardFieldState;
|
|
51
51
|
verbose: CardFieldState;
|
|
52
52
|
classify: CardFieldState;
|
|
53
|
+
retryableErrorPatterns: CardFieldState;
|
|
53
54
|
backoffFactor: CardFieldState;
|
|
54
55
|
backoffMaxMs: CardFieldState;
|
|
55
56
|
notify: CardFieldState;
|
|
@@ -94,6 +95,7 @@ export class AutoContinueSettingsCardController {
|
|
|
94
95
|
numberField('freshMs', 0),
|
|
95
96
|
booleanField('verbose'),
|
|
96
97
|
booleanField('classify'),
|
|
98
|
+
textField('retryableErrorPatterns'),
|
|
97
99
|
numberField('backoffFactor', 1),
|
|
98
100
|
numberField('backoffMaxMs', 0),
|
|
99
101
|
booleanField('notify'),
|
|
@@ -125,6 +127,7 @@ export class AutoContinueSettingsCardController {
|
|
|
125
127
|
freshMs: this.form.field('freshMs'),
|
|
126
128
|
verbose: this.form.field('verbose'),
|
|
127
129
|
classify: this.form.field('classify'),
|
|
130
|
+
retryableErrorPatterns: this.form.field('retryableErrorPatterns'),
|
|
128
131
|
backoffFactor: this.form.field('backoffFactor'),
|
|
129
132
|
backoffMaxMs: this.form.field('backoffMaxMs'),
|
|
130
133
|
notify: this.form.field('notify'),
|
|
@@ -230,7 +233,8 @@ interface FieldProps {
|
|
|
230
233
|
}
|
|
231
234
|
|
|
232
235
|
/** A staged value field; `numeric` only hints the keypad, which drafts a field accepts is decided by its spec. */
|
|
233
|
-
function ValueField(props: FieldProps & { numeric?: boolean; placeholder?: string }) {
|
|
236
|
+
function ValueField(props: FieldProps & { numeric?: boolean; multiline?: boolean; placeholder?: string }) {
|
|
237
|
+
const className = props.invalid ? 'dshAcInput dshAcInputInvalid' : 'dshAcInput';
|
|
234
238
|
return (
|
|
235
239
|
<div className="dshAcField">
|
|
236
240
|
<div className="dshAcHead">
|
|
@@ -244,17 +248,30 @@ function ValueField(props: FieldProps & { numeric?: boolean; placeholder?: strin
|
|
|
244
248
|
</span>
|
|
245
249
|
) : null}
|
|
246
250
|
</div>
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
251
|
+
{props.multiline === true ? (
|
|
252
|
+
<textarea
|
|
253
|
+
id={props.id}
|
|
254
|
+
className={`${className} dshAcTextArea`}
|
|
255
|
+
aria-invalid={props.invalid || undefined}
|
|
256
|
+
value={props.text}
|
|
257
|
+
placeholder={props.placeholder ?? ''}
|
|
258
|
+
disabled={props.disabled}
|
|
259
|
+
rows={4}
|
|
260
|
+
onChange={(event) => props.onEdit(event.target.value)}
|
|
261
|
+
/>
|
|
262
|
+
) : (
|
|
263
|
+
<input
|
|
264
|
+
id={props.id}
|
|
265
|
+
className={className}
|
|
266
|
+
type="text"
|
|
267
|
+
inputMode={props.numeric === true ? 'numeric' : undefined}
|
|
268
|
+
aria-invalid={props.invalid || undefined}
|
|
269
|
+
value={props.text}
|
|
270
|
+
placeholder={props.placeholder ?? ''}
|
|
271
|
+
disabled={props.disabled}
|
|
272
|
+
onChange={(event) => props.onEdit(event.target.value)}
|
|
273
|
+
/>
|
|
274
|
+
)}
|
|
258
275
|
<p className={props.invalid ? 'dshAcInvalid' : 'dshAcHint'}>
|
|
259
276
|
{props.invalid ? props.t('chrome.invalidNumber') : props.hint}
|
|
260
277
|
</p>
|
|
@@ -435,7 +452,7 @@ export function AutoContinueSettingsCard(props: AutoContinueSettingsCardProps) {
|
|
|
435
452
|
{...shared}
|
|
436
453
|
{...state.continueText}
|
|
437
454
|
onEdit={(text) => props.edit('continueText', text)}
|
|
438
|
-
placeholder={
|
|
455
|
+
placeholder={t('default.continueText')}
|
|
439
456
|
onReset={() => props.resetField('continueText')}
|
|
440
457
|
/>
|
|
441
458
|
<ValueField
|
|
@@ -445,7 +462,7 @@ export function AutoContinueSettingsCard(props: AutoContinueSettingsCardProps) {
|
|
|
445
462
|
{...shared}
|
|
446
463
|
{...state.continueTextMaxTokens}
|
|
447
464
|
onEdit={(text) => props.edit('continueTextMaxTokens', text)}
|
|
448
|
-
placeholder={
|
|
465
|
+
placeholder={t('default.continueTextMaxTokens')}
|
|
449
466
|
onReset={() => props.resetField('continueTextMaxTokens')}
|
|
450
467
|
/>
|
|
451
468
|
<BooleanField
|
|
@@ -464,7 +481,7 @@ export function AutoContinueSettingsCard(props: AutoContinueSettingsCardProps) {
|
|
|
464
481
|
{...shared}
|
|
465
482
|
{...state.guardPendingText}
|
|
466
483
|
onEdit={(text) => props.edit('guardPendingText', text)}
|
|
467
|
-
placeholder={
|
|
484
|
+
placeholder={t('default.guardPendingText')}
|
|
468
485
|
onReset={() => props.resetField('guardPendingText')}
|
|
469
486
|
/>
|
|
470
487
|
<ValueField
|
|
@@ -474,7 +491,7 @@ export function AutoContinueSettingsCard(props: AutoContinueSettingsCardProps) {
|
|
|
474
491
|
{...shared}
|
|
475
492
|
{...state.guardDoneText}
|
|
476
493
|
onEdit={(text) => props.edit('guardDoneText', text)}
|
|
477
|
-
placeholder={
|
|
494
|
+
placeholder={t('default.guardDoneText')}
|
|
478
495
|
onReset={() => props.resetField('guardDoneText')}
|
|
479
496
|
/>
|
|
480
497
|
<ValueField
|
|
@@ -554,6 +571,17 @@ export function AutoContinueSettingsCard(props: AutoContinueSettingsCardProps) {
|
|
|
554
571
|
onEdit={(text) => props.edit('classify', text)}
|
|
555
572
|
onReset={() => props.resetField('classify')}
|
|
556
573
|
/>
|
|
574
|
+
<ValueField
|
|
575
|
+
id="auto-continue-retryable-error-patterns"
|
|
576
|
+
label={t('field.retryableErrorPatterns')}
|
|
577
|
+
hint={t('field.retryableErrorPatternsHint')}
|
|
578
|
+
multiline
|
|
579
|
+
{...shared}
|
|
580
|
+
{...state.retryableErrorPatterns}
|
|
581
|
+
onEdit={(text) => props.edit('retryableErrorPatterns', text)}
|
|
582
|
+
placeholder="Upstream rejected the request as invalid"
|
|
583
|
+
onReset={() => props.resetField('retryableErrorPatterns')}
|
|
584
|
+
/>
|
|
557
585
|
<ValueField
|
|
558
586
|
id="auto-continue-backoff-factor"
|
|
559
587
|
label={t('field.backoffFactor')}
|
|
@@ -649,7 +677,7 @@ export function AutoContinueSettingsCard(props: AutoContinueSettingsCardProps) {
|
|
|
649
677
|
{...shared}
|
|
650
678
|
{...state.loopText}
|
|
651
679
|
onEdit={(text) => props.edit('loopText', text)}
|
|
652
|
-
placeholder={
|
|
680
|
+
placeholder={t('default.loopText')}
|
|
653
681
|
onReset={() => props.resetField('loopText')}
|
|
654
682
|
/>
|
|
655
683
|
<LivePanels t={t} />
|
package/src/client/styles.ts
CHANGED
|
@@ -117,6 +117,7 @@ const css = `
|
|
|
117
117
|
.dshAcInput:focus-visible { border-color: var(--dsw-alias-brand-primary); outline: none; }
|
|
118
118
|
.dshAcInput:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
|
|
119
119
|
.dshAcInputInvalid { border-color: var(--dsw-alias-label-error); }
|
|
120
|
+
.dshAcTextArea { box-sizing: border-box; height: auto; min-height: 84px; padding: 8px 12px; resize: vertical; }
|
|
120
121
|
.dshAcSelect {
|
|
121
122
|
border: 1px solid var(--dsw-alias-border-l2);
|
|
122
123
|
background: var(--dsw-alias-bg-layer-3);
|
package/src/host/engine.ts
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
todayKey,
|
|
35
35
|
toolResultFacts,
|
|
36
36
|
type AutoContinueConfig,
|
|
37
|
+
type AutoContinueLocale,
|
|
37
38
|
type DayStats,
|
|
38
39
|
type FailureFacts,
|
|
39
40
|
type SessionState,
|
|
@@ -42,6 +43,35 @@ import {
|
|
|
42
43
|
type TemplateContext,
|
|
43
44
|
} from '../shared/core.ts';
|
|
44
45
|
|
|
46
|
+
const NOTICE_COPY = {
|
|
47
|
+
zh: {
|
|
48
|
+
notContinuedTitle: 'dsh-auto-continue: 未自动继续',
|
|
49
|
+
permanentErrorBody: (sessionId: SessionId, summary: string) =>
|
|
50
|
+
`${sessionId}: 永久性错误 ${summary},需要人工处理`,
|
|
51
|
+
resumeAction: '立即续跑',
|
|
52
|
+
pauseAction: '暂停该会话 1 小时',
|
|
53
|
+
continuedTitle: 'dsh-auto-continue: 已自动继续',
|
|
54
|
+
continuedBody: (sessionId: SessionId, text: string, count: number) =>
|
|
55
|
+
`${sessionId}: 已发送「${text}」(第 ${count} 次连续)`,
|
|
56
|
+
stoppedTitle: 'dsh-auto-continue: 已停止自动继续',
|
|
57
|
+
stoppedBody: (sessionId: SessionId, count: number) =>
|
|
58
|
+
`${sessionId}: 连续失败 ${count} 次, 需要人工介入`,
|
|
59
|
+
},
|
|
60
|
+
en: {
|
|
61
|
+
notContinuedTitle: 'dsh-auto-continue: Not continued',
|
|
62
|
+
permanentErrorBody: (sessionId: SessionId, summary: string) =>
|
|
63
|
+
`${sessionId}: Permanent error ${summary}; manual intervention required`,
|
|
64
|
+
resumeAction: 'Resume now',
|
|
65
|
+
pauseAction: 'Pause this session for 1 hour',
|
|
66
|
+
continuedTitle: 'dsh-auto-continue: Continued automatically',
|
|
67
|
+
continuedBody: (sessionId: SessionId, text: string, count: number) =>
|
|
68
|
+
`${sessionId}: Sent "${text}" (consecutive attempt ${count})`,
|
|
69
|
+
stoppedTitle: 'dsh-auto-continue: Auto-continue stopped',
|
|
70
|
+
stoppedBody: (sessionId: SessionId, count: number) =>
|
|
71
|
+
`${sessionId}: ${count} consecutive failures; manual intervention required`,
|
|
72
|
+
},
|
|
73
|
+
} as const satisfies Record<AutoContinueLocale, Record<string, unknown>>;
|
|
74
|
+
|
|
45
75
|
/** 通知桥事件: host 引擎产生, browser 侧订阅展示(Notification / 动作按钮)。 */
|
|
46
76
|
export interface HostNotice {
|
|
47
77
|
/** 稳定标识(供 browser 去重)。 */
|
|
@@ -459,15 +489,16 @@ export class AutoContinueRunner {
|
|
|
459
489
|
|
|
460
490
|
private onTurnFailure(sessionId: SessionId, reason: string, failure: FailureFacts): void {
|
|
461
491
|
const config = this.getConfig();
|
|
462
|
-
if (config.classify && !isTransientFailure(failure)) {
|
|
492
|
+
if (config.classify && !isTransientFailure(failure, config.retryableErrorPatterns)) {
|
|
493
|
+
const copy = NOTICE_COPY[config.locale];
|
|
463
494
|
const summary = `${failure.code}${failure.status !== undefined ? ` (HTTP ${failure.status})` : ''}`;
|
|
464
495
|
this.log(`跳过 ${sessionId}(${reason}): 永久性失败 ${summary} — ${failure.message}`);
|
|
465
496
|
this.bumpStat({ skipped: 1, code: failure.code });
|
|
466
497
|
if (config.notify) {
|
|
467
498
|
this.notify(
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
this.notifyOptions(sessionId),
|
|
499
|
+
copy.notContinuedTitle,
|
|
500
|
+
copy.permanentErrorBody(sessionId, summary),
|
|
501
|
+
this.notifyOptions(sessionId, config.locale),
|
|
471
502
|
);
|
|
472
503
|
}
|
|
473
504
|
return;
|
|
@@ -476,11 +507,12 @@ export class AutoContinueRunner {
|
|
|
476
507
|
}
|
|
477
508
|
|
|
478
509
|
/** 通知操作按钮与回调(「立即续跑」/「暂停该会话 1 小时」)。 */
|
|
479
|
-
private notifyOptions(sessionId: SessionId): NotifyOptions {
|
|
510
|
+
private notifyOptions(sessionId: SessionId, locale: AutoContinueLocale): NotifyOptions {
|
|
511
|
+
const copy = NOTICE_COPY[locale];
|
|
480
512
|
return {
|
|
481
513
|
actions: [
|
|
482
|
-
{ action: 'resume', title:
|
|
483
|
-
{ action: 'pause1h', title:
|
|
514
|
+
{ action: 'resume', title: copy.resumeAction },
|
|
515
|
+
{ action: 'pause1h', title: copy.pauseAction },
|
|
484
516
|
],
|
|
485
517
|
onAction: (action) => this.onNotifyAction(sessionId, action),
|
|
486
518
|
};
|
|
@@ -668,20 +700,22 @@ export class AutoContinueRunner {
|
|
|
668
700
|
this.bumpStat({ sent: 1, ...(state.lastFailure !== undefined ? { code: state.lastFailure.code } : {}) });
|
|
669
701
|
this.log(`已自动发送「${text}」到 ${sessionId}(${reason}), 第 ${state.consecutive} 次连续`);
|
|
670
702
|
if (config.notify) {
|
|
703
|
+
const copy = NOTICE_COPY[config.locale];
|
|
671
704
|
this.notify(
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
this.notifyOptions(sessionId),
|
|
705
|
+
copy.continuedTitle,
|
|
706
|
+
copy.continuedBody(sessionId, text, state.consecutive),
|
|
707
|
+
this.notifyOptions(sessionId, config.locale),
|
|
675
708
|
);
|
|
676
709
|
}
|
|
677
710
|
if (state.consecutive >= config.maxConsecutive) {
|
|
678
711
|
this.bumpStat({ gaveUp: 1 });
|
|
679
712
|
this.log(`达到连续上限 ${config.maxConsecutive} 次, 停止自动继续 ${sessionId}`);
|
|
680
713
|
if (config.notify) {
|
|
714
|
+
const copy = NOTICE_COPY[config.locale];
|
|
681
715
|
this.notify(
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
this.notifyOptions(sessionId),
|
|
716
|
+
copy.stoppedTitle,
|
|
717
|
+
copy.stoppedBody(sessionId, state.consecutive),
|
|
718
|
+
this.notifyOptions(sessionId, config.locale),
|
|
685
719
|
);
|
|
686
720
|
}
|
|
687
721
|
}
|
package/src/index.ts
CHANGED
|
@@ -24,22 +24,24 @@ import type {} from '@deepseek-ai/dsh-session';
|
|
|
24
24
|
/** Settings namespace of the auto-continue plugin (lowercase kebab-case). */
|
|
25
25
|
export const AUTO_CONTINUE_NS = 'auto-continue';
|
|
26
26
|
|
|
27
|
-
/** Wire schema
|
|
27
|
+
/** Wire schema; blank localized text fields tell resolveConfig() to select the active locale's defaults. */
|
|
28
28
|
export const AutoContinueSchema = z.object({
|
|
29
|
+
/** Active browser/UI locale mirrored by the client. */
|
|
30
|
+
locale: z.string().default('zh'),
|
|
29
31
|
/** Text automatically sent after an interruption. */
|
|
30
|
-
continueText: z.string().default('
|
|
32
|
+
continueText: z.string().default(''),
|
|
31
33
|
/** Text sent when the output token ceiling is reached (same placeholders as `continueText`). */
|
|
32
|
-
continueTextMaxTokens: z.string().default('
|
|
34
|
+
continueTextMaxTokens: z.string().default(''),
|
|
33
35
|
/** Idempotency guard: inspect the last tool call before resuming and steer the model. */
|
|
34
36
|
guardTools: z.boolean().default(true),
|
|
35
37
|
/** Guard text appended when the last tool call has no confirmed result (it may have partially executed). */
|
|
36
38
|
guardPendingText: z
|
|
37
39
|
.string()
|
|
38
|
-
.default('
|
|
40
|
+
.default(''),
|
|
39
41
|
/** Guard text appended when the last tool call completed successfully (don't rerun it). */
|
|
40
42
|
guardDoneText: z
|
|
41
43
|
.string()
|
|
42
|
-
.default('
|
|
44
|
+
.default(''),
|
|
43
45
|
/** Grace period after an interruption before auto-sending (ms). */
|
|
44
46
|
graceMs: z.natural().default(3000),
|
|
45
47
|
/** Minimum interval between two auto-continues per session (ms). */
|
|
@@ -56,6 +58,8 @@ export const AutoContinueSchema = z.object({
|
|
|
56
58
|
verbose: z.boolean().default(true),
|
|
57
59
|
/** Classify failures: auto-continue transient errors only; permanent ones are skipped and notified. */
|
|
58
60
|
classify: z.boolean().default(true),
|
|
61
|
+
/** Provider-specific message/code/status fragments that explicitly count as retryable, one literal per line. */
|
|
62
|
+
retryableErrorPatterns: z.string().default(''),
|
|
59
63
|
/** Cooldown multiplier per consecutive failure (adaptive backoff). */
|
|
60
64
|
backoffFactor: z.natural().min(1).default(2),
|
|
61
65
|
/** Cap on the effective backoff interval (ms). */
|
|
@@ -79,7 +83,7 @@ export const AutoContinueSchema = z.object({
|
|
|
79
83
|
/** Text sent after the loop guard cancels and restarts a turn (supports {tool}). */
|
|
80
84
|
loopText: z
|
|
81
85
|
.string()
|
|
82
|
-
.default('
|
|
86
|
+
.default(''),
|
|
83
87
|
});
|
|
84
88
|
|
|
85
89
|
/**
|
package/src/shared/core.ts
CHANGED
|
@@ -6,8 +6,34 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session/types';
|
|
8
8
|
|
|
9
|
+
/** Supported UI/config locales. Any unknown browser locale falls back to Chinese. */
|
|
10
|
+
export type AutoContinueLocale = 'en' | 'zh';
|
|
11
|
+
|
|
12
|
+
/** Locale-owned defaults for the user-editable text fields. */
|
|
13
|
+
export const LOCALIZED_TEXT_DEFAULTS = {
|
|
14
|
+
zh: {
|
|
15
|
+
continueText: '继续',
|
|
16
|
+
continueTextMaxTokens: '继续',
|
|
17
|
+
guardPendingText: '(上一步工具「{tool}」可能未完成, 先确认状态再继续, 不要重复执行)',
|
|
18
|
+
guardDoneText: '(上一步工具「{tool}」已完成, 结果: {result}; 不要重复执行, 直接继续)',
|
|
19
|
+
loopText: '(检测到你可能陷入循环, 请停止重复刚才的动作, 换一种方式继续)',
|
|
20
|
+
},
|
|
21
|
+
en: {
|
|
22
|
+
continueText: 'Continue',
|
|
23
|
+
continueTextMaxTokens: 'Continue',
|
|
24
|
+
guardPendingText:
|
|
25
|
+
'(The previous tool "{tool}" may not have completed. Check its state before continuing and do not run it again.)',
|
|
26
|
+
guardDoneText:
|
|
27
|
+
'(The previous tool "{tool}" completed successfully. Result: {result}; do not run it again. Continue from there.)',
|
|
28
|
+
loopText:
|
|
29
|
+
'(You may be stuck in a loop. Stop repeating the last action and continue with a different approach.)',
|
|
30
|
+
},
|
|
31
|
+
} as const satisfies Record<AutoContinueLocale, Record<string, string>>;
|
|
32
|
+
|
|
9
33
|
/** The `auto-continue` settings section (all fields optional on the wire; the host schema carries defaults). */
|
|
10
34
|
export interface AutoContinueSettings {
|
|
35
|
+
/** Active browser/UI locale mirrored by the client. */
|
|
36
|
+
locale?: AutoContinueLocale;
|
|
11
37
|
/** Text automatically sent after an interruption. */
|
|
12
38
|
continueText?: string;
|
|
13
39
|
/** Text sent when the output token ceiling is reached (same placeholders as `continueText`). */
|
|
@@ -34,6 +60,8 @@ export interface AutoContinueSettings {
|
|
|
34
60
|
verbose?: boolean;
|
|
35
61
|
/** Classify failures: auto-continue transient errors only; permanent ones (auth/balance/model) are skipped and notified. */
|
|
36
62
|
classify?: boolean;
|
|
63
|
+
/** Provider-specific message/code/status fragments that explicitly count as retryable, one literal per line. */
|
|
64
|
+
retryableErrorPatterns?: string;
|
|
37
65
|
/** Cooldown multiplier per consecutive failure (adaptive backoff). */
|
|
38
66
|
backoffFactor?: number;
|
|
39
67
|
/** Cap on the effective backoff interval (ms). */
|
|
@@ -61,13 +89,11 @@ export interface AutoContinueSettings {
|
|
|
61
89
|
/** Fully resolved configuration (built-in defaults + user overrides). */
|
|
62
90
|
export type AutoContinueConfig = Required<AutoContinueSettings>;
|
|
63
91
|
|
|
64
|
-
/**
|
|
92
|
+
/** Effective built-in defaults; localized text fields use Chinese until a browser locale is mirrored. */
|
|
65
93
|
export const DEFAULT_CONFIG: AutoContinueConfig = {
|
|
66
|
-
|
|
67
|
-
|
|
94
|
+
locale: 'zh',
|
|
95
|
+
...LOCALIZED_TEXT_DEFAULTS.zh,
|
|
68
96
|
guardTools: true,
|
|
69
|
-
guardPendingText: '(上一步工具「{tool}」可能未完成, 先确认状态再继续, 不要重复执行)',
|
|
70
|
-
guardDoneText: '(上一步工具「{tool}」已完成, 结果: {result}; 不要重复执行, 直接继续)',
|
|
71
97
|
graceMs: 3000,
|
|
72
98
|
cooldownMs: 20000,
|
|
73
99
|
maxConsecutive: 3,
|
|
@@ -76,6 +102,7 @@ export const DEFAULT_CONFIG: AutoContinueConfig = {
|
|
|
76
102
|
freshMs: 15 * 60 * 1000,
|
|
77
103
|
verbose: true,
|
|
78
104
|
classify: true,
|
|
105
|
+
retryableErrorPatterns: '',
|
|
79
106
|
backoffFactor: 2,
|
|
80
107
|
backoffMaxMs: 300000,
|
|
81
108
|
notify: false,
|
|
@@ -86,7 +113,6 @@ export const DEFAULT_CONFIG: AutoContinueConfig = {
|
|
|
86
113
|
loopShortCount: 12,
|
|
87
114
|
loopRepeatText: 4,
|
|
88
115
|
loopToolRepeat: 5,
|
|
89
|
-
loopText: '(检测到你可能陷入循环, 请停止重复刚才的动作, 换一种方式继续)',
|
|
90
116
|
};
|
|
91
117
|
|
|
92
118
|
function numberOr(value: unknown, fallback: number): number {
|
|
@@ -100,23 +126,26 @@ function booleanOr(value: unknown, fallback: boolean): boolean {
|
|
|
100
126
|
/** Resolve a (possibly partial / not-yet-loaded) settings section to a full config. */
|
|
101
127
|
export function resolveConfig(section: AutoContinueSettings | undefined): AutoContinueConfig {
|
|
102
128
|
const value = section ?? {};
|
|
129
|
+
const locale: AutoContinueLocale = value.locale === 'en' ? 'en' : 'zh';
|
|
130
|
+
const localized = LOCALIZED_TEXT_DEFAULTS[locale];
|
|
103
131
|
const text =
|
|
104
132
|
typeof value.continueText === 'string' && value.continueText.trim() !== ''
|
|
105
133
|
? value.continueText
|
|
106
|
-
:
|
|
134
|
+
: localized.continueText;
|
|
107
135
|
const maxTokensText =
|
|
108
136
|
typeof value.continueTextMaxTokens === 'string' && value.continueTextMaxTokens.trim() !== ''
|
|
109
137
|
? value.continueTextMaxTokens
|
|
110
|
-
:
|
|
138
|
+
: localized.continueTextMaxTokens;
|
|
111
139
|
const guardPendingText =
|
|
112
140
|
typeof value.guardPendingText === 'string' && value.guardPendingText.trim() !== ''
|
|
113
141
|
? value.guardPendingText
|
|
114
|
-
:
|
|
142
|
+
: localized.guardPendingText;
|
|
115
143
|
const guardDoneText =
|
|
116
144
|
typeof value.guardDoneText === 'string' && value.guardDoneText.trim() !== ''
|
|
117
145
|
? value.guardDoneText
|
|
118
|
-
:
|
|
146
|
+
: localized.guardDoneText;
|
|
119
147
|
return {
|
|
148
|
+
locale,
|
|
120
149
|
continueText: text,
|
|
121
150
|
continueTextMaxTokens: maxTokensText,
|
|
122
151
|
guardTools: booleanOr(value.guardTools, DEFAULT_CONFIG.guardTools),
|
|
@@ -130,6 +159,10 @@ export function resolveConfig(section: AutoContinueSettings | undefined): AutoCo
|
|
|
130
159
|
freshMs: numberOr(value.freshMs, DEFAULT_CONFIG.freshMs),
|
|
131
160
|
verbose: booleanOr(value.verbose, DEFAULT_CONFIG.verbose),
|
|
132
161
|
classify: booleanOr(value.classify, DEFAULT_CONFIG.classify),
|
|
162
|
+
retryableErrorPatterns:
|
|
163
|
+
typeof value.retryableErrorPatterns === 'string'
|
|
164
|
+
? value.retryableErrorPatterns.trim()
|
|
165
|
+
: DEFAULT_CONFIG.retryableErrorPatterns,
|
|
133
166
|
backoffFactor: Math.max(1, numberOr(value.backoffFactor, DEFAULT_CONFIG.backoffFactor)),
|
|
134
167
|
backoffMaxMs: numberOr(value.backoffMaxMs, DEFAULT_CONFIG.backoffMaxMs),
|
|
135
168
|
notify: booleanOr(value.notify, DEFAULT_CONFIG.notify),
|
|
@@ -143,7 +176,7 @@ export function resolveConfig(section: AutoContinueSettings | undefined): AutoCo
|
|
|
143
176
|
loopText:
|
|
144
177
|
typeof value.loopText === 'string' && value.loopText.trim() !== ''
|
|
145
178
|
? value.loopText
|
|
146
|
-
:
|
|
179
|
+
: localized.loopText,
|
|
147
180
|
};
|
|
148
181
|
}
|
|
149
182
|
|
|
@@ -171,11 +204,18 @@ export interface FailureFacts {
|
|
|
171
204
|
|
|
172
205
|
/**
|
|
173
206
|
* 错误分类: 该失败是否值得自动继续。
|
|
207
|
+
* 用户填写的 provider 专属文本片段优先覆盖内置结果; 未命中时,
|
|
174
208
|
* 永久性失败(认证/余额/模型不存在/上下文超限等)重试也不会成功, 应跳过并通知用户;
|
|
175
209
|
* 其余(网络、超时、5xx、429 等)视为临时性失败, 允许自动恢复。
|
|
176
210
|
*/
|
|
177
|
-
export function isTransientFailure(failure: FailureFacts): boolean {
|
|
178
|
-
const haystack = `${failure.code} ${failure.message}`.toLowerCase();
|
|
211
|
+
export function isTransientFailure(failure: FailureFacts, retryableErrorPatterns = ''): boolean {
|
|
212
|
+
const haystack = `${failure.code} ${failure.status ?? ''} ${failure.message}`.toLowerCase();
|
|
213
|
+
const explicitlyRetryable = retryableErrorPatterns
|
|
214
|
+
.split(/\r?\n/)
|
|
215
|
+
.map((pattern) => pattern.trim().toLowerCase())
|
|
216
|
+
.filter((pattern) => pattern !== '')
|
|
217
|
+
.some((pattern) => haystack.includes(pattern));
|
|
218
|
+
if (explicitlyRetryable) return true;
|
|
179
219
|
const status = failure.status;
|
|
180
220
|
if (status !== undefined && (status === 401 || status === 403)) return false;
|
|
181
221
|
const permanent =
|