dsh-client-auto-continue 0.8.0 → 0.8.2
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 +2 -6
- package/README.zh.md +2 -6
- package/lib/client.js +14 -46
- package/lib/client.js.map +3 -3
- package/lib/index.js +5 -11
- package/lib/types/client/dsh-store-compat.d.ts +39 -0
- package/lib/types/client/engine.d.ts +5 -250
- package/lib/types/client/index.d.ts +1 -1
- package/lib/types/client/locales.d.ts +0 -4
- package/lib/types/client/settings-card.d.ts +1 -3
- package/lib/types/client/settings-form.d.ts +1 -1
- package/lib/types/host/engine.d.ts +1 -141
- package/lib/types/index.d.ts +0 -8
- package/lib/types/shared/core.d.ts +234 -0
- package/package.json +2 -4
- package/src/client/dsh-store-compat.ts +57 -0
- package/src/client/engine.ts +17 -1639
- package/src/client/index.ts +1 -1
- package/src/client/locales.ts +0 -8
- package/src/client/settings-card.tsx +1 -27
- package/src/client/settings-form.ts +1 -1
- package/src/host/engine.ts +24 -457
- package/src/index.ts +2 -5
- package/src/shared/core.ts +458 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/** 共享核心: 平台无关的纯逻辑与类型。
|
|
2
|
+
*
|
|
3
|
+
* 被 host 引擎(src/host/engine.ts)与浏览器半侧共用: 配置解析、错误分类、
|
|
4
|
+
* 模板填充、自适应退避、幂等护栏的工具结果提取、循环守卫的会话状态机,
|
|
5
|
+
* 以及回显识别。引擎迁入 host 后(0.8.0), 浏览器半侧只 re-export 本模块。
|
|
6
|
+
*/
|
|
7
|
+
import type { SessionEvent } from '@deepseek-ai/dsh-session/types';
|
|
8
|
+
/** The `auto-continue` settings section (all fields optional on the wire; the host schema carries defaults). */
|
|
9
|
+
export interface AutoContinueSettings {
|
|
10
|
+
/** Text automatically sent after an interruption. */
|
|
11
|
+
continueText?: string;
|
|
12
|
+
/** Text sent when the output token ceiling is reached (same placeholders as `continueText`). */
|
|
13
|
+
continueTextMaxTokens?: string;
|
|
14
|
+
/** Idempotency guard: inspect the last tool call before resuming and steer the model. */
|
|
15
|
+
guardTools?: boolean;
|
|
16
|
+
/** Guard text appended when the last tool call has no confirmed result (it may have partially executed). */
|
|
17
|
+
guardPendingText?: string;
|
|
18
|
+
/** Guard text appended when the last tool call completed successfully (don't rerun it). */
|
|
19
|
+
guardDoneText?: string;
|
|
20
|
+
/** Grace period after an interruption before auto-sending (ms). */
|
|
21
|
+
graceMs?: number;
|
|
22
|
+
/** Minimum interval between two auto-continues per session (ms). */
|
|
23
|
+
cooldownMs?: number;
|
|
24
|
+
/** Max consecutive auto-continues per session before stopping. */
|
|
25
|
+
maxConsecutive?: number;
|
|
26
|
+
/** Scan recently interrupted sessions on page load / reconnect. */
|
|
27
|
+
scanOnBoot?: boolean;
|
|
28
|
+
/** Max sessions the scan checks (most recently updated). */
|
|
29
|
+
scanLimit?: number;
|
|
30
|
+
/** Scan only considers interruptions inside this window (ms). */
|
|
31
|
+
freshMs?: number;
|
|
32
|
+
/** Log `[auto-continue]` lines to the browser console. */
|
|
33
|
+
verbose?: boolean;
|
|
34
|
+
/** Classify failures: auto-continue transient errors only; permanent ones (auth/balance/model) are skipped and notified. */
|
|
35
|
+
classify?: boolean;
|
|
36
|
+
/** Cooldown multiplier per consecutive failure (adaptive backoff). */
|
|
37
|
+
backoffFactor?: number;
|
|
38
|
+
/** Cap on the effective backoff interval (ms). */
|
|
39
|
+
backoffMaxMs?: number;
|
|
40
|
+
/** Show browser notifications for auto-continue events. */
|
|
41
|
+
notify?: boolean;
|
|
42
|
+
/** Globally pause auto-continue: no live or scan send, queued pending sends cancelled. */
|
|
43
|
+
paused?: boolean;
|
|
44
|
+
/** Loop guard: detect a running turn spinning in place (short talk without tools, or the same tool repeating) and restart it. */
|
|
45
|
+
loopGuard?: boolean;
|
|
46
|
+
/** A model message shorter than this many chars counts as a "short sentence" (loop signal). */
|
|
47
|
+
loopShortChars?: number;
|
|
48
|
+
/** Consecutive short sentences within this window (ms) with no tool call in between trip the loop guard. */
|
|
49
|
+
loopWindowMs?: number;
|
|
50
|
+
/** Consecutive short sentences trip the loop guard. */
|
|
51
|
+
loopShortCount?: number;
|
|
52
|
+
/** Consecutive identical tool calls with identical arguments AND identical results trip the loop guard. */
|
|
53
|
+
loopToolRepeat?: number;
|
|
54
|
+
/** Consecutive identical short sentences trip the loop guard (strongest spinning signal). */
|
|
55
|
+
loopRepeatText?: number;
|
|
56
|
+
/** Text sent after the loop guard cancels and restarts a turn (supports {tool}). */
|
|
57
|
+
loopText?: string;
|
|
58
|
+
}
|
|
59
|
+
/** Fully resolved configuration (built-in defaults + user overrides). */
|
|
60
|
+
export type AutoContinueConfig = Required<AutoContinueSettings>;
|
|
61
|
+
/** Built-in defaults — must match the host schema defaults in src/index.ts. */
|
|
62
|
+
export declare const DEFAULT_CONFIG: AutoContinueConfig;
|
|
63
|
+
/** Resolve a (possibly partial / not-yet-loaded) settings section to a full config. */
|
|
64
|
+
export declare function resolveConfig(section: AutoContinueSettings | undefined): AutoContinueConfig;
|
|
65
|
+
/**
|
|
66
|
+
* 视为「非人为中断」的回合结束原因, 用于启动/重连扫描。
|
|
67
|
+
* - `interrupted` 只由崩溃修复在宿主重载时写入(loop 永不实时发出), 因此仅在扫描路径处理;
|
|
68
|
+
* - 实时事件路径只对 `error` / `max-tokens` 自动续跑;
|
|
69
|
+
* - `aborted`(用户停止)与 `blocked`(策略拒绝)永不自动继续。
|
|
70
|
+
*/
|
|
71
|
+
type NonHumanReason = 'error' | 'interrupted' | 'max-tokens';
|
|
72
|
+
export declare function isNonHumanReason(kind: string): kind is NonHumanReason;
|
|
73
|
+
/** 一次回合失败的机器可读事实(turn/end error 的 LlmFailure 载荷)。 */
|
|
74
|
+
export interface FailureFacts {
|
|
75
|
+
/** 稳定机器路由码(如 UPSTREAM、RATE_LIMIT_EXCEEDED、INVALID_API_KEY)。 */
|
|
76
|
+
code: string;
|
|
77
|
+
/** 人类可读的失败描述。 */
|
|
78
|
+
message: string;
|
|
79
|
+
/** 供应商 HTTP 状态码(可用时)。 */
|
|
80
|
+
status?: number;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 错误分类: 该失败是否值得自动继续。
|
|
84
|
+
* 永久性失败(认证/余额/模型不存在/上下文超限等)重试也不会成功, 应跳过并通知用户;
|
|
85
|
+
* 其余(网络、超时、5xx、429 等)视为临时性失败, 允许自动恢复。
|
|
86
|
+
*/
|
|
87
|
+
export declare function isTransientFailure(failure: FailureFacts): boolean;
|
|
88
|
+
/**
|
|
89
|
+
* host/agent-error 消息分类: 仅明确属于网络/传输类的临时错误才自动继续。
|
|
90
|
+
* 其余(序列化失败、配置/宿主内部错误等)视为永久性——重试无益, 且用户停止导致的
|
|
91
|
+
* 序列化失败(如 Windows 下 abort 的 DOMException reason)绝不能自动续跑。
|
|
92
|
+
*/
|
|
93
|
+
export declare function isTransientAgentError(message: string): boolean;
|
|
94
|
+
/** 通知上的一个操作按钮(action 标识 + 显示文案)。 */
|
|
95
|
+
export interface NotifyAction {
|
|
96
|
+
/** 稳定动作标识, 点击时经 onAction 回调传出。 */
|
|
97
|
+
action: string;
|
|
98
|
+
/** 按钮显示文案。 */
|
|
99
|
+
title: string;
|
|
100
|
+
}
|
|
101
|
+
/** 通知的可选行为: 操作按钮列表与点击回调。 */
|
|
102
|
+
export interface NotifyOptions {
|
|
103
|
+
actions?: NotifyAction[];
|
|
104
|
+
onAction?: (action: string) => void;
|
|
105
|
+
}
|
|
106
|
+
/** 模板填充所需的上下文(全部可选, 缺失的占位符填为空串)。 */
|
|
107
|
+
export interface TemplateContext {
|
|
108
|
+
/** 失败事实(错误码/消息/HTTP 状态), 对应 {code}/{message}/{status}。 */
|
|
109
|
+
facts?: FailureFacts;
|
|
110
|
+
/** 失败前最后一次工具调用的名称, 对应 {tool}。 */
|
|
111
|
+
tool?: string;
|
|
112
|
+
/** 失败回合的编号, 对应 {turn}。 */
|
|
113
|
+
turn?: number;
|
|
114
|
+
/** 连续失败次数(含本次), 对应 {errorCount}。 */
|
|
115
|
+
errorCount?: number;
|
|
116
|
+
/** 会话标题(来自 session.list 投影, 可用时), 对应 {sessionTitle}。 */
|
|
117
|
+
sessionTitle?: string;
|
|
118
|
+
/** 自失败发生以来的毫秒数, 对应 {elapsed}。 */
|
|
119
|
+
elapsedMs?: number;
|
|
120
|
+
/** 上一步工具结果摘要(截断), 对应 {result}(护栏模板用)。 */
|
|
121
|
+
result?: string;
|
|
122
|
+
}
|
|
123
|
+
/** 用失败事实与回合信息填充 continueText 模板占位符({code}/{message}/{status}/{tool}/{turn}/{errorCount}/{sessionTitle}/{elapsed}/{result})。 */
|
|
124
|
+
export declare function fillTemplate(template: string, ctx: TemplateContext): string;
|
|
125
|
+
/** 上一步工具调用的判定结果: 是否已确认完成, 以及文本摘要。 */
|
|
126
|
+
export interface ToolResultFacts {
|
|
127
|
+
/** 工具是否成功完成(内部失败或 isError 视为未成功)。 */
|
|
128
|
+
ok: boolean;
|
|
129
|
+
/** 工具输出的文本摘要(截断)。 */
|
|
130
|
+
excerpt: string;
|
|
131
|
+
}
|
|
132
|
+
/** 从 tool/result 事件载荷提取成功与否与文本摘要。 */
|
|
133
|
+
export declare function toolResultFacts(data: {
|
|
134
|
+
error?: {
|
|
135
|
+
name?: string;
|
|
136
|
+
code?: string;
|
|
137
|
+
};
|
|
138
|
+
message?: {
|
|
139
|
+
content?: Array<{
|
|
140
|
+
type?: string;
|
|
141
|
+
content?: unknown;
|
|
142
|
+
isError?: boolean;
|
|
143
|
+
}>;
|
|
144
|
+
};
|
|
145
|
+
}): ToolResultFacts;
|
|
146
|
+
/** 自适应退避: 同一会话连续失败时的有效冷却间隔。 */
|
|
147
|
+
export declare function effectiveCooldown(consecutive: number, base: number, factor: number, max: number): number;
|
|
148
|
+
export declare function sleep(ms: number): Promise<void>;
|
|
149
|
+
/** 一天的自动继续统计(host 单实例内存态)。 */
|
|
150
|
+
export interface DayStats {
|
|
151
|
+
/** 本地日期 YYYY-MM-DD。 */
|
|
152
|
+
date: string;
|
|
153
|
+
/** 自动发送次数。 */
|
|
154
|
+
sent: number;
|
|
155
|
+
/** 因永久性错误跳过的次数。 */
|
|
156
|
+
skipped: number;
|
|
157
|
+
/** 发送后回合成功完成(恢复成功)的次数。 */
|
|
158
|
+
recovered: number;
|
|
159
|
+
/** 发送后再次失败的次数。 */
|
|
160
|
+
failed: number;
|
|
161
|
+
/** 达到连续上限而停止的次数(按停止事件计)。 */
|
|
162
|
+
gaveUp: number;
|
|
163
|
+
/** loop guard 打断并重启回合的次数。 */
|
|
164
|
+
looped: number;
|
|
165
|
+
/** 按错误码计数的失败分布。 */
|
|
166
|
+
byCode: Record<string, number>;
|
|
167
|
+
}
|
|
168
|
+
export declare function todayKey(): string;
|
|
169
|
+
/** 空统计桶。 */
|
|
170
|
+
export declare function emptyDayStats(): DayStats;
|
|
171
|
+
/** 每会话运行时状态。 */
|
|
172
|
+
export interface SessionState {
|
|
173
|
+
/** 连续自动「继续」次数; 成功回合或用户手动介入后归零。 */
|
|
174
|
+
consecutive: number;
|
|
175
|
+
/** 上次自动「继续」时间戳。 */
|
|
176
|
+
lastAutoAt: number;
|
|
177
|
+
/** 上次自动「继续」尝试(成功或失败)时间戳; 防止失败场景下的快速重试循环。 */
|
|
178
|
+
lastAttemptAt: number;
|
|
179
|
+
/** 我们上次自动发送的文本(用于识别自己的回显)。 */
|
|
180
|
+
lastSentText: string;
|
|
181
|
+
/** 宽限期定时器(进行中的待发送)。 */
|
|
182
|
+
pendingTimer: ReturnType<typeof setTimeout> | undefined;
|
|
183
|
+
/** 宿主权威 running 位(来自 host/session-status 与回合事件)。 */
|
|
184
|
+
running: boolean | undefined;
|
|
185
|
+
/** 当前排队消息数(来自 session/queue 帧)。 */
|
|
186
|
+
queued: number;
|
|
187
|
+
/** 子代理会话(host/session-added 带 parentSessionId)。 */
|
|
188
|
+
subagent: boolean;
|
|
189
|
+
/** 最近一次回合失败的事实(用于分类与模板填充)。 */
|
|
190
|
+
lastFailure: FailureFacts | undefined;
|
|
191
|
+
/** 最近一次失败的发生时间(模板 {elapsed} 与恢复统计用)。 */
|
|
192
|
+
lastFailureAt: number;
|
|
193
|
+
/** 失败前最后一次工具调用的名称(模板 {tool} 与幂等护栏用)。 */
|
|
194
|
+
lastTool: string | undefined;
|
|
195
|
+
/** 上一步工具调用的结果状态: 'pending' = 已发起未见结果(可能已部分执行)。 */
|
|
196
|
+
lastToolResult: 'pending' | ToolResultFacts | undefined;
|
|
197
|
+
/** 失败回合的编号(模板 {turn})。 */
|
|
198
|
+
lastTurn: number | undefined;
|
|
199
|
+
/** 我们最近一次自动发送的时间戳; 0 = 没有待确认的恢复。 */
|
|
200
|
+
pendingRecoveryAt: number;
|
|
201
|
+
/** 当前连续短句数(loop guard 信号 1: 空转)。 */
|
|
202
|
+
shortRun: number;
|
|
203
|
+
/** 最后一条短句的时间(时间窗判定用)。 */
|
|
204
|
+
lastShortAt: number;
|
|
205
|
+
/** 最后一条模型消息的文本(相同文本重复判定用)。 */
|
|
206
|
+
lastAssistantText: string;
|
|
207
|
+
/** 连续相同文本消息数(最强空转信号, 不限长度)。 */
|
|
208
|
+
sameTextRun: number;
|
|
209
|
+
/**
|
|
210
|
+
* 工具重复信号(loop guard 信号 2: 死循环)。
|
|
211
|
+
* 只有「同工具 + 同参数 + 同结果」的连续调用才累计; 参数或结果有变化视为有进展, 计数重置。
|
|
212
|
+
*/
|
|
213
|
+
toolRun: {
|
|
214
|
+
/** 工具名 + 参数(用于判定是否同一调用)。 */
|
|
215
|
+
key: string;
|
|
216
|
+
/** 连续相同调用数(结果确认后更新)。 */
|
|
217
|
+
count: number;
|
|
218
|
+
/** 上次该调用的结果摘要(比较用)。 */
|
|
219
|
+
lastResult: string | undefined;
|
|
220
|
+
/** 本次调用等待结果确认。 */
|
|
221
|
+
waiting: boolean;
|
|
222
|
+
} | undefined;
|
|
223
|
+
/** 本回合已触发过 loop guard(防重复打断)。 */
|
|
224
|
+
loopFired: boolean;
|
|
225
|
+
/** loop 重启的延迟定时器(冷却结束后再 schedule)。 */
|
|
226
|
+
loopRetryTimer: ReturnType<typeof setTimeout> | undefined;
|
|
227
|
+
/** 我们主动 cancel 过本回合(区分用户停止)。 */
|
|
228
|
+
loopCancelled: boolean;
|
|
229
|
+
}
|
|
230
|
+
export declare const freshState: () => SessionState;
|
|
231
|
+
export declare const RECOVERY_WINDOW_MS: number;
|
|
232
|
+
export declare const ECHO_WINDOW_MS: number;
|
|
233
|
+
export declare function isOurEcho(state: SessionState, event: SessionEvent): boolean;
|
|
234
|
+
export {};
|
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.8.
|
|
4
|
+
"version": "0.8.2",
|
|
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();
|