dsh-client-auto-continue 0.8.1 → 0.9.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 +15 -3
- package/README.zh.md +15 -3
- package/lib/client.js +50 -4
- package/lib/client.js.map +3 -3
- package/lib/index.js +9 -3
- package/lib/types/client/dsh-store-compat.d.ts +39 -0
- package/lib/types/client/index.d.ts +1 -1
- package/lib/types/client/locales.d.ts +2 -0
- package/lib/types/client/settings-card.d.ts +2 -1
- package/lib/types/client/settings-form.d.ts +1 -1
- package/lib/types/index.d.ts +4 -0
- package/lib/types/shared/core.d.ts +4 -1
- package/package.json +2 -4
- package/src/client/dsh-store-compat.ts +57 -0
- package/src/client/index.ts +1 -1
- package/src/client/locales.ts +4 -0
- package/src/client/settings-card.tsx +41 -13
- package/src/client/settings-form.ts +1 -1
- package/src/client/styles.ts +1 -0
- package/src/host/engine.ts +1 -1
- package/src/index.ts +2 -0
- package/src/shared/core.ts +16 -3
package/lib/index.js
CHANGED
|
@@ -112,6 +112,7 @@ var DEFAULT_CONFIG = {
|
|
|
112
112
|
freshMs: 15 * 60 * 1e3,
|
|
113
113
|
verbose: true,
|
|
114
114
|
classify: true,
|
|
115
|
+
retryableErrorPatterns: "",
|
|
115
116
|
backoffFactor: 2,
|
|
116
117
|
backoffMaxMs: 3e5,
|
|
117
118
|
notify: false,
|
|
@@ -150,6 +151,7 @@ function resolveConfig(section) {
|
|
|
150
151
|
freshMs: numberOr(value.freshMs, DEFAULT_CONFIG.freshMs),
|
|
151
152
|
verbose: booleanOr(value.verbose, DEFAULT_CONFIG.verbose),
|
|
152
153
|
classify: booleanOr(value.classify, DEFAULT_CONFIG.classify),
|
|
154
|
+
retryableErrorPatterns: typeof value.retryableErrorPatterns === "string" ? value.retryableErrorPatterns.trim() : DEFAULT_CONFIG.retryableErrorPatterns,
|
|
153
155
|
backoffFactor: Math.max(1, numberOr(value.backoffFactor, DEFAULT_CONFIG.backoffFactor)),
|
|
154
156
|
backoffMaxMs: numberOr(value.backoffMaxMs, DEFAULT_CONFIG.backoffMaxMs),
|
|
155
157
|
notify: booleanOr(value.notify, DEFAULT_CONFIG.notify),
|
|
@@ -166,8 +168,10 @@ function resolveConfig(section) {
|
|
|
166
168
|
function isNonHumanReason(kind) {
|
|
167
169
|
return kind === "error" || kind === "interrupted" || kind === "max-tokens";
|
|
168
170
|
}
|
|
169
|
-
function isTransientFailure(failure) {
|
|
170
|
-
const haystack = `${failure.code} ${failure.message}`.toLowerCase();
|
|
171
|
+
function isTransientFailure(failure, retryableErrorPatterns = "") {
|
|
172
|
+
const haystack = `${failure.code} ${failure.status ?? ""} ${failure.message}`.toLowerCase();
|
|
173
|
+
const explicitlyRetryable = retryableErrorPatterns.split(/\r?\n/).map((pattern) => pattern.trim().toLowerCase()).filter((pattern) => pattern !== "").some((pattern) => haystack.includes(pattern));
|
|
174
|
+
if (explicitlyRetryable) return true;
|
|
171
175
|
const status = failure.status;
|
|
172
176
|
if (status !== void 0 && (status === 401 || status === 403)) return false;
|
|
173
177
|
const permanent = /auth|unauthor|forbidden|credential|api[_-]?key|permission/i.test(haystack) || /insufficient.*(balance|quota)|billing|payment|quota.*exceeded.*(?!retry)/i.test(haystack) || /model.*not[_-]?found|unknown[_-]?model|model[_-]?not[_-]?found|not.*support.*model/i.test(haystack) || /context.*(length|limit|overflow|exceed)|token.*limit|max.*context/i.test(haystack) || /invalid[_-]?request|bad[_-]?request/i.test(haystack);
|
|
@@ -564,7 +568,7 @@ ${event.data.arguments}`;
|
|
|
564
568
|
// ---------- host 帧 ----------
|
|
565
569
|
onTurnFailure(sessionId, reason, failure) {
|
|
566
570
|
const config = this.getConfig();
|
|
567
|
-
if (config.classify && !isTransientFailure(failure)) {
|
|
571
|
+
if (config.classify && !isTransientFailure(failure, config.retryableErrorPatterns)) {
|
|
568
572
|
const summary = `${failure.code}${failure.status !== void 0 ? ` (HTTP ${failure.status})` : ""}`;
|
|
569
573
|
this.log(`跳过 ${sessionId}(${reason}): 永久性失败 ${summary} — ${failure.message}`);
|
|
570
574
|
this.bumpStat({ skipped: 1, code: failure.code });
|
|
@@ -913,6 +917,8 @@ var AutoContinueSchema = z2.object({
|
|
|
913
917
|
verbose: z2.boolean().default(true),
|
|
914
918
|
/** Classify failures: auto-continue transient errors only; permanent ones are skipped and notified. */
|
|
915
919
|
classify: z2.boolean().default(true),
|
|
920
|
+
/** Provider-specific message/code/status fragments that explicitly count as retryable, one literal per line. */
|
|
921
|
+
retryableErrorPatterns: z2.string().default(""),
|
|
916
922
|
/** Cooldown multiplier per consecutive failure (adaptive backoff). */
|
|
917
923
|
backoffFactor: z2.natural().min(1).default(2),
|
|
918
924
|
/** Cap on the effective backoff interval (ms). */
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshot-store bridge across the two public DSH client module layouts.
|
|
3
|
+
*
|
|
4
|
+
* DSH 0.1.2 moved the store engine from the dynamic
|
|
5
|
+
* `@deepseek-ai/dsh-client-runtime/client` row into the shell-seeded
|
|
6
|
+
* `@deepseek-ai/dsh-client-store` platform module. Keep the probe dynamic so
|
|
7
|
+
* esbuild does not turn both candidates into eager top-level requires: the
|
|
8
|
+
* loader must only resolve the module that exists in the running DSH cohort.
|
|
9
|
+
*/
|
|
10
|
+
/** Writable observable snapshot used by the settings card. */
|
|
11
|
+
export interface SnapshotStore<T> {
|
|
12
|
+
getSnapshot(): T;
|
|
13
|
+
subscribe(listener: () => void): () => void;
|
|
14
|
+
update(mutator: (draft: T) => void): void;
|
|
15
|
+
set(next: T): void;
|
|
16
|
+
}
|
|
17
|
+
/** Settings state consumed by the staged form. */
|
|
18
|
+
export interface SettingsScopeSnapshot<T> {
|
|
19
|
+
status: 'loading' | 'ready' | 'unavailable';
|
|
20
|
+
value: T | undefined;
|
|
21
|
+
base: unknown;
|
|
22
|
+
user: unknown;
|
|
23
|
+
revision: number | undefined;
|
|
24
|
+
writable: boolean;
|
|
25
|
+
mode: 'host' | 'memory';
|
|
26
|
+
}
|
|
27
|
+
/** Stable subset shared by the legacy and DSH 0.1.2 settings scopes. */
|
|
28
|
+
export interface SettingsScope<T> {
|
|
29
|
+
getSnapshot(): SettingsScopeSnapshot<T>;
|
|
30
|
+
subscribe(listener: () => void): () => void;
|
|
31
|
+
set(field: string, value: unknown): Promise<void>;
|
|
32
|
+
unset(field: string): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
export declare const createSnapshotStore: <T>(init: T, options?: {
|
|
35
|
+
flush?: "raf" | "sync";
|
|
36
|
+
persist?: {
|
|
37
|
+
name: string;
|
|
38
|
+
};
|
|
39
|
+
}) => SnapshotStore<T>;
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* action endpoint,
|
|
10
10
|
* - feeds the card's stats / paused-sessions panels from the bridge state.
|
|
11
11
|
*/
|
|
12
|
-
import type { ClientContext } from '@deepseek-ai/
|
|
12
|
+
import type { Context as ClientContext } from '@deepseek-ai/cordis';
|
|
13
13
|
import { type SettingsCardKey } from './locales.ts';
|
|
14
14
|
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
15
15
|
interface LocaleNamespaceMap {
|
|
@@ -35,6 +35,8 @@ export declare const zh: {
|
|
|
35
35
|
'field.verboseHint': string;
|
|
36
36
|
'field.classify': string;
|
|
37
37
|
'field.classifyHint': string;
|
|
38
|
+
'field.retryableErrorPatterns': string;
|
|
39
|
+
'field.retryableErrorPatternsHint': string;
|
|
38
40
|
'field.backoffFactor': string;
|
|
39
41
|
'field.backoffFactorHint': string;
|
|
40
42
|
'field.backoffMaxMs': string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type SettingsScope, type SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client';
|
|
2
1
|
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
3
2
|
import { type AutoContinueSettings } from './engine.ts';
|
|
3
|
+
import { type SettingsScope, type SnapshotStore } from './dsh-store-compat.ts';
|
|
4
4
|
import { type CardActions, type CardFieldState, type CardShell } from './settings-form.ts';
|
|
5
5
|
/** What the auto-continue card renders. */
|
|
6
6
|
export interface AutoContinueSettingsCardState extends CardShell {
|
|
@@ -18,6 +18,7 @@ export interface AutoContinueSettingsCardState extends CardShell {
|
|
|
18
18
|
freshMs: CardFieldState;
|
|
19
19
|
verbose: CardFieldState;
|
|
20
20
|
classify: CardFieldState;
|
|
21
|
+
retryableErrorPatterns: CardFieldState;
|
|
21
22
|
backoffFactor: CardFieldState;
|
|
22
23
|
backoffMaxMs: CardFieldState;
|
|
23
24
|
notify: CardFieldState;
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* default — and whether the user layer carries it (presence, not value
|
|
11
11
|
* equality, marks an override).
|
|
12
12
|
*/
|
|
13
|
-
import type { SettingsScope, SnapshotStore } from '
|
|
13
|
+
import type { SettingsScope, SnapshotStore } from './dsh-store-compat.ts';
|
|
14
14
|
/** The write one field's staged text performs when the card is saved. */
|
|
15
15
|
export type FieldWrite = {
|
|
16
16
|
kind: 'set';
|
package/lib/types/index.d.ts
CHANGED
|
@@ -41,6 +41,8 @@ export declare const AutoContinueSchema: z<Schemastery.ObjectS<{
|
|
|
41
41
|
verbose: z<boolean, boolean>;
|
|
42
42
|
/** Classify failures: auto-continue transient errors only; permanent ones are skipped and notified. */
|
|
43
43
|
classify: z<boolean, boolean>;
|
|
44
|
+
/** Provider-specific message/code/status fragments that explicitly count as retryable, one literal per line. */
|
|
45
|
+
retryableErrorPatterns: z<string, string>;
|
|
44
46
|
/** Cooldown multiplier per consecutive failure (adaptive backoff). */
|
|
45
47
|
backoffFactor: z<number, number>;
|
|
46
48
|
/** Cap on the effective backoff interval (ms). */
|
|
@@ -90,6 +92,8 @@ export declare const AutoContinueSchema: z<Schemastery.ObjectS<{
|
|
|
90
92
|
verbose: z<boolean, boolean>;
|
|
91
93
|
/** Classify failures: auto-continue transient errors only; permanent ones are skipped and notified. */
|
|
92
94
|
classify: z<boolean, boolean>;
|
|
95
|
+
/** Provider-specific message/code/status fragments that explicitly count as retryable, one literal per line. */
|
|
96
|
+
retryableErrorPatterns: z<string, string>;
|
|
93
97
|
/** Cooldown multiplier per consecutive failure (adaptive backoff). */
|
|
94
98
|
backoffFactor: z<number, number>;
|
|
95
99
|
/** Cap on the effective backoff interval (ms). */
|
|
@@ -33,6 +33,8 @@ export interface AutoContinueSettings {
|
|
|
33
33
|
verbose?: boolean;
|
|
34
34
|
/** Classify failures: auto-continue transient errors only; permanent ones (auth/balance/model) are skipped and notified. */
|
|
35
35
|
classify?: boolean;
|
|
36
|
+
/** Provider-specific message/code/status fragments that explicitly count as retryable, one literal per line. */
|
|
37
|
+
retryableErrorPatterns?: string;
|
|
36
38
|
/** Cooldown multiplier per consecutive failure (adaptive backoff). */
|
|
37
39
|
backoffFactor?: number;
|
|
38
40
|
/** Cap on the effective backoff interval (ms). */
|
|
@@ -81,10 +83,11 @@ export interface FailureFacts {
|
|
|
81
83
|
}
|
|
82
84
|
/**
|
|
83
85
|
* 错误分类: 该失败是否值得自动继续。
|
|
86
|
+
* 用户填写的 provider 专属文本片段优先覆盖内置结果; 未命中时,
|
|
84
87
|
* 永久性失败(认证/余额/模型不存在/上下文超限等)重试也不会成功, 应跳过并通知用户;
|
|
85
88
|
* 其余(网络、超时、5xx、429 等)视为临时性失败, 允许自动恢复。
|
|
86
89
|
*/
|
|
87
|
-
export declare function isTransientFailure(failure: FailureFacts): boolean;
|
|
90
|
+
export declare function isTransientFailure(failure: FailureFacts, retryableErrorPatterns?: string): boolean;
|
|
88
91
|
/**
|
|
89
92
|
* host/agent-error 消息分类: 仅明确属于网络/传输类的临时错误才自动继续。
|
|
90
93
|
* 其余(序列化失败、配置/宿主内部错误等)视为永久性——重试无益, 且用户停止导致的
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-client-auto-continue",
|
|
3
3
|
"description": "DSH Web UI plugin: automatically sends \"继续\" (continue) when a request is interrupted by network errors or other non-human causes",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.9.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
|
@@ -36,7 +36,6 @@
|
|
|
36
36
|
"platform": "web",
|
|
37
37
|
"inject": [
|
|
38
38
|
"@deepseek-ai/dsh-client-connection",
|
|
39
|
-
"@deepseek-ai/dsh-client-runtime",
|
|
40
39
|
"@deepseek-ai/dsh-client-locale",
|
|
41
40
|
"@deepseek-ai/dsh-client-ui-settings",
|
|
42
41
|
"@deepseek-ai/dsh-client-ui-settings-plugins"
|
|
@@ -47,7 +46,7 @@
|
|
|
47
46
|
"build": "node build.mjs && tsc -p tsconfig.build.json",
|
|
48
47
|
"watch": "node build.mjs --watch",
|
|
49
48
|
"typecheck": "tsc --noEmit",
|
|
50
|
-
"test": "node tests/simulate-host.mjs",
|
|
49
|
+
"test": "node tests/simulate-host.mjs && node tests/simulate-client-loader.mjs",
|
|
51
50
|
"prepack": "npm run build"
|
|
52
51
|
},
|
|
53
52
|
"keywords": [
|
|
@@ -70,7 +69,6 @@
|
|
|
70
69
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
71
70
|
"@deepseek-ai/dsh-client-connection": "^0.1.0-rc.7",
|
|
72
71
|
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.7",
|
|
73
|
-
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.7",
|
|
74
72
|
"@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.7",
|
|
75
73
|
"@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.0-rc.7",
|
|
76
74
|
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.7",
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshot-store bridge across the two public DSH client module layouts.
|
|
3
|
+
*
|
|
4
|
+
* DSH 0.1.2 moved the store engine from the dynamic
|
|
5
|
+
* `@deepseek-ai/dsh-client-runtime/client` row into the shell-seeded
|
|
6
|
+
* `@deepseek-ai/dsh-client-store` platform module. Keep the probe dynamic so
|
|
7
|
+
* esbuild does not turn both candidates into eager top-level requires: the
|
|
8
|
+
* loader must only resolve the module that exists in the running DSH cohort.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Writable observable snapshot used by the settings card. */
|
|
12
|
+
export interface SnapshotStore<T> {
|
|
13
|
+
getSnapshot(): T;
|
|
14
|
+
subscribe(listener: () => void): () => void;
|
|
15
|
+
update(mutator: (draft: T) => void): void;
|
|
16
|
+
set(next: T): void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Settings state consumed by the staged form. */
|
|
20
|
+
export interface SettingsScopeSnapshot<T> {
|
|
21
|
+
status: 'loading' | 'ready' | 'unavailable';
|
|
22
|
+
value: T | undefined;
|
|
23
|
+
base: unknown;
|
|
24
|
+
user: unknown;
|
|
25
|
+
revision: number | undefined;
|
|
26
|
+
writable: boolean;
|
|
27
|
+
mode: 'host' | 'memory';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Stable subset shared by the legacy and DSH 0.1.2 settings scopes. */
|
|
31
|
+
export interface SettingsScope<T> {
|
|
32
|
+
getSnapshot(): SettingsScopeSnapshot<T>;
|
|
33
|
+
subscribe(listener: () => void): () => void;
|
|
34
|
+
set(field: string, value: unknown): Promise<void>;
|
|
35
|
+
unset(field: string): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface SnapshotStoreModule {
|
|
39
|
+
createSnapshotStore<T>(
|
|
40
|
+
init: T,
|
|
41
|
+
options?: { flush?: 'raf' | 'sync'; persist?: { name: string } },
|
|
42
|
+
): SnapshotStore<T>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function resolveSnapshotStore(): SnapshotStoreModule {
|
|
46
|
+
// String assembly is intentional: it preserves the lazy try/fallback in the
|
|
47
|
+
// emitted client bundle instead of letting the bundler resolve both names.
|
|
48
|
+
const current = ['@deepseek-ai/dsh-client', '-store'].join('');
|
|
49
|
+
const legacy = ['@deepseek-ai/dsh-client-runtime', '/client'].join('');
|
|
50
|
+
try {
|
|
51
|
+
return require(current) as SnapshotStoreModule;
|
|
52
|
+
} catch {
|
|
53
|
+
return require(legacy) as SnapshotStoreModule;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const { createSnapshotStore } = resolveSnapshotStore();
|
package/src/client/index.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* action endpoint,
|
|
10
10
|
* - feeds the card's stats / paused-sessions panels from the bridge state.
|
|
11
11
|
*/
|
|
12
|
-
import type { ClientContext } from '@deepseek-ai/
|
|
12
|
+
import type { Context as ClientContext } from '@deepseek-ai/cordis';
|
|
13
13
|
// Type-only: pulls the locale plugin's Context merge (ctx.locale).
|
|
14
14
|
import type {} from '@deepseek-ai/dsh-client-locale/client';
|
|
15
15
|
// Type-only: pulls the settings-surface SlotMap merge and ctx.settingsScope.
|
package/src/client/locales.ts
CHANGED
|
@@ -36,6 +36,8 @@ export const zh = {
|
|
|
36
36
|
'field.verboseHint': '在浏览器控制台输出 [auto-continue] 日志。',
|
|
37
37
|
'field.classify': '错误分类',
|
|
38
38
|
'field.classifyHint': '仅自动恢复临时性错误(网络/超时/5xx 等); 认证/余额/模型不存在等永久性错误跳过并通知。',
|
|
39
|
+
'field.retryableErrorPatterns': '自定义可恢复错误',
|
|
40
|
+
'field.retryableErrorPatternsHint': '每行一个大小写不敏感的普通文本片段; 命中错误码、HTTP 状态或消息时覆盖内置分类。请只填 provider 稳定且足够具体的文案, 过宽会重复请求。',
|
|
39
41
|
'field.backoffFactor': '退避系数',
|
|
40
42
|
'field.backoffFactorHint': '连续失败时冷却间隔的倍率(如 2 表示 20s→40s→80s 递增)。',
|
|
41
43
|
'field.backoffMaxMs': '最大退避间隔 (ms)',
|
|
@@ -122,6 +124,8 @@ export const en: Record<SettingsCardKey, string> = {
|
|
|
122
124
|
'field.verboseHint': 'Log [auto-continue] lines to the browser console.',
|
|
123
125
|
'field.classify': 'Classify errors',
|
|
124
126
|
'field.classifyHint': 'Auto-resume transient failures only (network/timeout/5xx…); auth, balance and model errors are skipped and notified.',
|
|
127
|
+
'field.retryableErrorPatterns': 'Custom retryable errors',
|
|
128
|
+
'field.retryableErrorPatternsHint': 'One case-insensitive literal per line. A match in the error code, HTTP status, or message overrides built-in classification. Use only stable, provider-specific text; broad matches can repeat requests.',
|
|
125
129
|
'field.backoffFactor': 'Backoff factor',
|
|
126
130
|
'field.backoffFactorHint': 'Cooldown multiplier per consecutive failure (2 = 20s→40s→80s…).',
|
|
127
131
|
'field.backoffMaxMs': 'Max backoff (ms)',
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
* tokens so the card follows the active theme.
|
|
9
9
|
*/
|
|
10
10
|
import { useEffect, useState, type ReactNode } from 'react';
|
|
11
|
-
import { createSnapshotStore, type SettingsScope, type SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client';
|
|
12
11
|
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
13
12
|
import { DEFAULT_CONFIG, type AutoContinueSettings } from './engine.ts';
|
|
13
|
+
import { createSnapshotStore, type SettingsScope, type SnapshotStore } from './dsh-store-compat.ts';
|
|
14
14
|
import {
|
|
15
15
|
pausedSessions,
|
|
16
16
|
readTodayStats,
|
|
@@ -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>
|
|
@@ -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')}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* default — and whether the user layer carries it (presence, not value
|
|
11
11
|
* equality, marks an override).
|
|
12
12
|
*/
|
|
13
|
-
import type { SettingsScope, SnapshotStore } from '
|
|
13
|
+
import type { SettingsScope, SnapshotStore } from './dsh-store-compat.ts';
|
|
14
14
|
|
|
15
15
|
/** The write one field's staged text performs when the card is saved. */
|
|
16
16
|
export type FieldWrite = { kind: 'set'; value: unknown } | { kind: 'clear' };
|
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
|
@@ -459,7 +459,7 @@ export class AutoContinueRunner {
|
|
|
459
459
|
|
|
460
460
|
private onTurnFailure(sessionId: SessionId, reason: string, failure: FailureFacts): void {
|
|
461
461
|
const config = this.getConfig();
|
|
462
|
-
if (config.classify && !isTransientFailure(failure)) {
|
|
462
|
+
if (config.classify && !isTransientFailure(failure, config.retryableErrorPatterns)) {
|
|
463
463
|
const summary = `${failure.code}${failure.status !== undefined ? ` (HTTP ${failure.status})` : ''}`;
|
|
464
464
|
this.log(`跳过 ${sessionId}(${reason}): 永久性失败 ${summary} — ${failure.message}`);
|
|
465
465
|
this.bumpStat({ skipped: 1, code: failure.code });
|
package/src/index.ts
CHANGED
|
@@ -56,6 +56,8 @@ export const AutoContinueSchema = z.object({
|
|
|
56
56
|
verbose: z.boolean().default(true),
|
|
57
57
|
/** Classify failures: auto-continue transient errors only; permanent ones are skipped and notified. */
|
|
58
58
|
classify: z.boolean().default(true),
|
|
59
|
+
/** Provider-specific message/code/status fragments that explicitly count as retryable, one literal per line. */
|
|
60
|
+
retryableErrorPatterns: z.string().default(''),
|
|
59
61
|
/** Cooldown multiplier per consecutive failure (adaptive backoff). */
|
|
60
62
|
backoffFactor: z.natural().min(1).default(2),
|
|
61
63
|
/** Cap on the effective backoff interval (ms). */
|
package/src/shared/core.ts
CHANGED
|
@@ -34,6 +34,8 @@ export interface AutoContinueSettings {
|
|
|
34
34
|
verbose?: boolean;
|
|
35
35
|
/** Classify failures: auto-continue transient errors only; permanent ones (auth/balance/model) are skipped and notified. */
|
|
36
36
|
classify?: boolean;
|
|
37
|
+
/** Provider-specific message/code/status fragments that explicitly count as retryable, one literal per line. */
|
|
38
|
+
retryableErrorPatterns?: string;
|
|
37
39
|
/** Cooldown multiplier per consecutive failure (adaptive backoff). */
|
|
38
40
|
backoffFactor?: number;
|
|
39
41
|
/** Cap on the effective backoff interval (ms). */
|
|
@@ -76,6 +78,7 @@ export const DEFAULT_CONFIG: AutoContinueConfig = {
|
|
|
76
78
|
freshMs: 15 * 60 * 1000,
|
|
77
79
|
verbose: true,
|
|
78
80
|
classify: true,
|
|
81
|
+
retryableErrorPatterns: '',
|
|
79
82
|
backoffFactor: 2,
|
|
80
83
|
backoffMaxMs: 300000,
|
|
81
84
|
notify: false,
|
|
@@ -130,6 +133,10 @@ export function resolveConfig(section: AutoContinueSettings | undefined): AutoCo
|
|
|
130
133
|
freshMs: numberOr(value.freshMs, DEFAULT_CONFIG.freshMs),
|
|
131
134
|
verbose: booleanOr(value.verbose, DEFAULT_CONFIG.verbose),
|
|
132
135
|
classify: booleanOr(value.classify, DEFAULT_CONFIG.classify),
|
|
136
|
+
retryableErrorPatterns:
|
|
137
|
+
typeof value.retryableErrorPatterns === 'string'
|
|
138
|
+
? value.retryableErrorPatterns.trim()
|
|
139
|
+
: DEFAULT_CONFIG.retryableErrorPatterns,
|
|
133
140
|
backoffFactor: Math.max(1, numberOr(value.backoffFactor, DEFAULT_CONFIG.backoffFactor)),
|
|
134
141
|
backoffMaxMs: numberOr(value.backoffMaxMs, DEFAULT_CONFIG.backoffMaxMs),
|
|
135
142
|
notify: booleanOr(value.notify, DEFAULT_CONFIG.notify),
|
|
@@ -171,11 +178,18 @@ export interface FailureFacts {
|
|
|
171
178
|
|
|
172
179
|
/**
|
|
173
180
|
* 错误分类: 该失败是否值得自动继续。
|
|
181
|
+
* 用户填写的 provider 专属文本片段优先覆盖内置结果; 未命中时,
|
|
174
182
|
* 永久性失败(认证/余额/模型不存在/上下文超限等)重试也不会成功, 应跳过并通知用户;
|
|
175
183
|
* 其余(网络、超时、5xx、429 等)视为临时性失败, 允许自动恢复。
|
|
176
184
|
*/
|
|
177
|
-
export function isTransientFailure(failure: FailureFacts): boolean {
|
|
178
|
-
const haystack = `${failure.code} ${failure.message}`.toLowerCase();
|
|
185
|
+
export function isTransientFailure(failure: FailureFacts, retryableErrorPatterns = ''): boolean {
|
|
186
|
+
const haystack = `${failure.code} ${failure.status ?? ''} ${failure.message}`.toLowerCase();
|
|
187
|
+
const explicitlyRetryable = retryableErrorPatterns
|
|
188
|
+
.split(/\r?\n/)
|
|
189
|
+
.map((pattern) => pattern.trim().toLowerCase())
|
|
190
|
+
.filter((pattern) => pattern !== '')
|
|
191
|
+
.some((pattern) => haystack.includes(pattern));
|
|
192
|
+
if (explicitlyRetryable) return true;
|
|
179
193
|
const status = failure.status;
|
|
180
194
|
if (status !== undefined && (status === 401 || status === 403)) return false;
|
|
181
195
|
const permanent =
|
|
@@ -456,4 +470,3 @@ export function isOurEcho(state: SessionState, event: SessionEvent): boolean {
|
|
|
456
470
|
.join('');
|
|
457
471
|
return text === state.lastSentText;
|
|
458
472
|
}
|
|
459
|
-
|