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.
@@ -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 { DEFAULT_CONFIG, type AutoContinueSettings } from './engine.ts';
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
- <input
248
- id={props.id}
249
- className={props.invalid ? 'dshAcInput dshAcInputInvalid' : 'dshAcInput'}
250
- type="text"
251
- inputMode={props.numeric === true ? 'numeric' : undefined}
252
- aria-invalid={props.invalid || undefined}
253
- value={props.text}
254
- placeholder={props.placeholder ?? ''}
255
- disabled={props.disabled}
256
- onChange={(event) => props.onEdit(event.target.value)}
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={DEFAULT_CONFIG.continueText}
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={DEFAULT_CONFIG.continueTextMaxTokens}
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={DEFAULT_CONFIG.guardPendingText}
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={DEFAULT_CONFIG.guardDoneText}
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={DEFAULT_CONFIG.loopText}
680
+ placeholder={t('default.loopText')}
653
681
  onReset={() => props.resetField('loopText')}
654
682
  />
655
683
  <LivePanels t={t} />
@@ -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);
@@ -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
- 'dsh-auto-continue: 未自动继续',
469
- `${sessionId}: 永久性错误 ${summary},需要人工处理`,
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: '暂停该会话 1 小时' },
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
- 'dsh-auto-continue: 已自动继续',
673
- `${sessionId}: 已发送「${text}」(第 ${state.consecutive} 次连续)`,
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
- 'dsh-auto-continue: 已停止自动继续',
683
- `${sessionId}: 连续失败 ${state.consecutive} 次, 需要人工介入`,
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 of the auto-continue section; defaults are the plugin's built-in values. */
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('(上一步工具「{tool}」可能未完成, 先确认状态再继续, 不要重复执行)'),
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('(上一步工具「{tool}」已完成, 结果: {result}; 不要重复执行, 直接继续)'),
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
  /**
@@ -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
- /** Built-in defaults must match the host schema defaults in src/index.ts. */
92
+ /** Effective built-in defaults; localized text fields use Chinese until a browser locale is mirrored. */
65
93
  export const DEFAULT_CONFIG: AutoContinueConfig = {
66
- continueText: '继续',
67
- continueTextMaxTokens: '继续',
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
- : DEFAULT_CONFIG.continueText;
134
+ : localized.continueText;
107
135
  const maxTokensText =
108
136
  typeof value.continueTextMaxTokens === 'string' && value.continueTextMaxTokens.trim() !== ''
109
137
  ? value.continueTextMaxTokens
110
- : DEFAULT_CONFIG.continueTextMaxTokens;
138
+ : localized.continueTextMaxTokens;
111
139
  const guardPendingText =
112
140
  typeof value.guardPendingText === 'string' && value.guardPendingText.trim() !== ''
113
141
  ? value.guardPendingText
114
- : DEFAULT_CONFIG.guardPendingText;
142
+ : localized.guardPendingText;
115
143
  const guardDoneText =
116
144
  typeof value.guardDoneText === 'string' && value.guardDoneText.trim() !== ''
117
145
  ? value.guardDoneText
118
- : DEFAULT_CONFIG.guardDoneText;
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
- : DEFAULT_CONFIG.loopText,
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 =