dsh-client-auto-continue 0.2.2 → 0.3.1
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 +22 -1
- package/README.zh.md +22 -1
- package/docs/banner-dark.svg +25 -0
- package/docs/banner-zh-dark.svg +25 -0
- package/docs/banner-zh.svg +25 -0
- package/docs/banner.svg +25 -0
- package/docs/demo-zh.svg +50 -0
- package/docs/demo.svg +50 -0
- package/docs/social-preview.png +0 -0
- package/docs/social-preview.svg +43 -0
- package/lib/client.js +170 -9
- package/lib/client.js.map +2 -2
- package/lib/index.js +9 -1
- package/lib/types/client/engine.d.ts +31 -0
- package/lib/types/client/locales.d.ts +8 -0
- package/lib/types/client/settings-card.d.ts +4 -0
- package/lib/types/index.d.ts +16 -0
- package/package.json +2 -1
package/lib/client.js
CHANGED
|
@@ -42,7 +42,11 @@ var DEFAULT_CONFIG = {
|
|
|
42
42
|
freshMs: 15 * 60 * 1e3,
|
|
43
43
|
reconnectScanDelayMs: 5e3,
|
|
44
44
|
reconnectBackoffMs: 3e3,
|
|
45
|
-
verbose: true
|
|
45
|
+
verbose: true,
|
|
46
|
+
classify: true,
|
|
47
|
+
backoffFactor: 2,
|
|
48
|
+
backoffMaxMs: 3e5,
|
|
49
|
+
notify: false
|
|
46
50
|
};
|
|
47
51
|
function numberOr(value, fallback) {
|
|
48
52
|
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
@@ -63,12 +67,46 @@ function resolveConfig(section) {
|
|
|
63
67
|
freshMs: numberOr(value.freshMs, DEFAULT_CONFIG.freshMs),
|
|
64
68
|
reconnectScanDelayMs: numberOr(value.reconnectScanDelayMs, DEFAULT_CONFIG.reconnectScanDelayMs),
|
|
65
69
|
reconnectBackoffMs: numberOr(value.reconnectBackoffMs, DEFAULT_CONFIG.reconnectBackoffMs),
|
|
66
|
-
verbose: booleanOr(value.verbose, DEFAULT_CONFIG.verbose)
|
|
70
|
+
verbose: booleanOr(value.verbose, DEFAULT_CONFIG.verbose),
|
|
71
|
+
classify: booleanOr(value.classify, DEFAULT_CONFIG.classify),
|
|
72
|
+
backoffFactor: Math.max(1, numberOr(value.backoffFactor, DEFAULT_CONFIG.backoffFactor)),
|
|
73
|
+
backoffMaxMs: numberOr(value.backoffMaxMs, DEFAULT_CONFIG.backoffMaxMs),
|
|
74
|
+
notify: booleanOr(value.notify, DEFAULT_CONFIG.notify)
|
|
67
75
|
};
|
|
68
76
|
}
|
|
69
77
|
function isNonHumanReason(kind) {
|
|
70
78
|
return kind === "error" || kind === "interrupted" || kind === "max-tokens";
|
|
71
79
|
}
|
|
80
|
+
function isTransientFailure(failure) {
|
|
81
|
+
const haystack = `${failure.code} ${failure.message}`.toLowerCase();
|
|
82
|
+
const status = failure.status;
|
|
83
|
+
if (status !== void 0 && (status === 401 || status === 403)) return false;
|
|
84
|
+
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);
|
|
85
|
+
return !permanent;
|
|
86
|
+
}
|
|
87
|
+
function notify(title, body) {
|
|
88
|
+
try {
|
|
89
|
+
const N = globalThis.Notification;
|
|
90
|
+
if (typeof N === "undefined") return;
|
|
91
|
+
const permission = N.permission;
|
|
92
|
+
if (permission === "granted") {
|
|
93
|
+
new N(title, { body });
|
|
94
|
+
} else if (permission === "default") {
|
|
95
|
+
void N.requestPermission?.().then((result) => {
|
|
96
|
+
if (result === "granted") new N(title, { body });
|
|
97
|
+
}).catch(() => {
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function fillTemplate(template, facts, tool, turn) {
|
|
104
|
+
return template.replace(/\{code\}/g, facts?.code ?? "").replace(/\{message\}/g, facts?.message ?? "").replace(/\{status\}/g, facts?.status !== void 0 ? String(facts.status) : "").replace(/\{tool\}/g, tool ?? "").replace(/\{turn\}/g, turn !== void 0 ? String(turn) : "");
|
|
105
|
+
}
|
|
106
|
+
function effectiveCooldown(consecutive, base, factor, max) {
|
|
107
|
+
const multiplier = Math.pow(factor, consecutive);
|
|
108
|
+
return Math.min(Math.max(base, base * multiplier), Math.max(base, max));
|
|
109
|
+
}
|
|
72
110
|
function sleep(ms) {
|
|
73
111
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
74
112
|
}
|
|
@@ -118,7 +156,10 @@ var freshState = () => ({
|
|
|
118
156
|
pendingTimer: void 0,
|
|
119
157
|
running: void 0,
|
|
120
158
|
queued: 0,
|
|
121
|
-
subagent: false
|
|
159
|
+
subagent: false,
|
|
160
|
+
lastFailure: void 0,
|
|
161
|
+
lastTool: void 0,
|
|
162
|
+
lastTurn: void 0
|
|
122
163
|
});
|
|
123
164
|
function isOurEcho(state, event) {
|
|
124
165
|
if (event.type !== "user/message") return false;
|
|
@@ -220,6 +261,10 @@ var AutoContinueRunner = class {
|
|
|
220
261
|
onMuxFrame(frame) {
|
|
221
262
|
switch (frame.type) {
|
|
222
263
|
case "session/event":
|
|
264
|
+
if (frame.event.type === "tool/call") {
|
|
265
|
+
const name = frame.event.data.name;
|
|
266
|
+
if (typeof name === "string") this.state(frame.sessionId).lastTool = name;
|
|
267
|
+
}
|
|
223
268
|
this.onSessionEvent(frame.sessionId, frame.event);
|
|
224
269
|
break;
|
|
225
270
|
case "session/queue":
|
|
@@ -246,9 +291,19 @@ var AutoContinueRunner = class {
|
|
|
246
291
|
const reason = event.data.reason;
|
|
247
292
|
if (reason.kind === "completed") {
|
|
248
293
|
state.consecutive = 0;
|
|
294
|
+
state.lastFailure = void 0;
|
|
249
295
|
} else if (reason.kind === "aborted") {
|
|
250
296
|
state.consecutive = 0;
|
|
251
297
|
} else if (reason.kind === "blocked") {
|
|
298
|
+
} else if (reason.kind === "error") {
|
|
299
|
+
const error = reason.error;
|
|
300
|
+
state.lastFailure = {
|
|
301
|
+
code: typeof error.code === "string" ? error.code : "UNKNOWN",
|
|
302
|
+
message: typeof error.message === "string" ? error.message : String(error),
|
|
303
|
+
...typeof error.status === "number" ? { status: error.status } : {}
|
|
304
|
+
};
|
|
305
|
+
state.lastTurn = event.data.turn;
|
|
306
|
+
this.onTurnFailure(sessionId, "turn/end:error", state.lastFailure);
|
|
252
307
|
} else if (isNonHumanReason(reason.kind)) {
|
|
253
308
|
this.schedule(sessionId, `turn/end:${reason.kind}`);
|
|
254
309
|
}
|
|
@@ -289,12 +344,35 @@ var AutoContinueRunner = class {
|
|
|
289
344
|
}
|
|
290
345
|
}
|
|
291
346
|
// ---------- 调度 ----------
|
|
347
|
+
/** 回合失败入口: 先做错误分类, 永久性失败跳过并通知, 临时性失败走正常调度。 */
|
|
348
|
+
onTurnFailure(sessionId, reason, failure) {
|
|
349
|
+
const config = this.getConfig();
|
|
350
|
+
if (config.classify && !isTransientFailure(failure)) {
|
|
351
|
+
const summary = `${failure.code}${failure.status !== void 0 ? ` (HTTP ${failure.status})` : ""}`;
|
|
352
|
+
this.log(`跳过 ${sessionId}(${reason}): 永久性失败 ${summary} — ${failure.message}`);
|
|
353
|
+
if (config.notify) {
|
|
354
|
+
notify("dsh-auto-continue: 未自动继续", `${sessionId}: 永久性错误 ${summary},需要人工处理`);
|
|
355
|
+
}
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
this.schedule(sessionId, reason);
|
|
359
|
+
}
|
|
360
|
+
/** 本会话当前生效的冷却间隔(自适应退避)。 */
|
|
361
|
+
cooldownFor(state) {
|
|
362
|
+
const config = this.getConfig();
|
|
363
|
+
return effectiveCooldown(
|
|
364
|
+
state.consecutive,
|
|
365
|
+
config.cooldownMs,
|
|
366
|
+
config.backoffFactor,
|
|
367
|
+
config.backoffMaxMs
|
|
368
|
+
);
|
|
369
|
+
}
|
|
292
370
|
schedule(sessionId, reason) {
|
|
293
371
|
const state = this.state(sessionId);
|
|
294
372
|
const config = this.getConfig();
|
|
295
373
|
if (state.subagent) return;
|
|
296
374
|
if (state.pendingTimer !== void 0) return;
|
|
297
|
-
if (Date.now() - state.lastAttemptAt <
|
|
375
|
+
if (Date.now() - state.lastAttemptAt < this.cooldownFor(state)) return;
|
|
298
376
|
if (state.consecutive >= config.maxConsecutive) {
|
|
299
377
|
this.log(
|
|
300
378
|
`跳过 ${sessionId}(${reason}): 已连续自动继续 ${state.consecutive} 次, 等待用户介入或成功回合`
|
|
@@ -337,7 +415,7 @@ var AutoContinueRunner = class {
|
|
|
337
415
|
this.log(`跳过 ${sessionId}: 已有排队消息`);
|
|
338
416
|
return;
|
|
339
417
|
}
|
|
340
|
-
if (Date.now() - readLastSend(sessionId) <
|
|
418
|
+
if (Date.now() - readLastSend(sessionId) < this.cooldownFor(state)) {
|
|
341
419
|
this.log(`跳过 ${sessionId}: 其他标签页刚发送过`);
|
|
342
420
|
return;
|
|
343
421
|
}
|
|
@@ -345,7 +423,7 @@ var AutoContinueRunner = class {
|
|
|
345
423
|
this.log(`跳过 ${sessionId}: 其他标签页正在发送`);
|
|
346
424
|
return;
|
|
347
425
|
}
|
|
348
|
-
const text = config.continueText;
|
|
426
|
+
const text = fillTemplate(config.continueText, state.lastFailure, state.lastTool, state.lastTurn);
|
|
349
427
|
const zone = clientTimeZone();
|
|
350
428
|
state.lastAttemptAt = Date.now();
|
|
351
429
|
try {
|
|
@@ -362,6 +440,15 @@ var AutoContinueRunner = class {
|
|
|
362
440
|
state.lastSentText = text;
|
|
363
441
|
writeLastSend(sessionId, now);
|
|
364
442
|
this.log(`已自动发送「${text}」到 ${sessionId}(${reason}), 第 ${state.consecutive} 次连续`);
|
|
443
|
+
if (config.notify) {
|
|
444
|
+
notify("dsh-auto-continue: 已自动继续", `${sessionId}: 已发送「${text}」(第 ${state.consecutive} 次连续)`);
|
|
445
|
+
}
|
|
446
|
+
if (state.consecutive >= config.maxConsecutive) {
|
|
447
|
+
this.log(`达到连续上限 ${config.maxConsecutive} 次, 停止自动继续 ${sessionId}`);
|
|
448
|
+
if (config.notify) {
|
|
449
|
+
notify("dsh-auto-continue: 已停止自动继续", `${sessionId}: 连续失败 ${state.consecutive} 次, 需要人工介入`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
365
452
|
} else {
|
|
366
453
|
this.log(
|
|
367
454
|
`发送失败 ${sessionId}: ${response.result.error.code} ${response.result.error.message}`
|
|
@@ -429,7 +516,7 @@ var AutoContinueRunner = class {
|
|
|
429
516
|
const state = this.state(summary.sessionId);
|
|
430
517
|
if (state.pendingTimer !== void 0) continue;
|
|
431
518
|
if (state.consecutive >= config.maxConsecutive) continue;
|
|
432
|
-
if (now - state.lastAttemptAt <
|
|
519
|
+
if (now - state.lastAttemptAt < this.cooldownFor(state)) continue;
|
|
433
520
|
let events;
|
|
434
521
|
try {
|
|
435
522
|
const page = await this.api.sessions.history({
|
|
@@ -493,6 +580,14 @@ var zh = {
|
|
|
493
580
|
"field.reconnectBackoffMsHint": "事件流断开后的重连间隔。",
|
|
494
581
|
"field.verbose": "详细日志",
|
|
495
582
|
"field.verboseHint": "在浏览器控制台输出 [auto-continue] 日志。",
|
|
583
|
+
"field.classify": "错误分类",
|
|
584
|
+
"field.classifyHint": "仅自动恢复临时性错误(网络/超时/5xx 等); 认证/余额/模型不存在等永久性错误跳过并通知。",
|
|
585
|
+
"field.backoffFactor": "退避系数",
|
|
586
|
+
"field.backoffFactorHint": "连续失败时冷却间隔的倍率(如 2 表示 20s→40s→80s 递增)。",
|
|
587
|
+
"field.backoffMaxMs": "最大退避间隔 (ms)",
|
|
588
|
+
"field.backoffMaxMsHint": "自适应退避的上限, 防止等待过久。",
|
|
589
|
+
"field.notify": "浏览器通知",
|
|
590
|
+
"field.notifyHint": "自动继续成功/放弃/遇到永久性错误时弹出浏览器通知。",
|
|
496
591
|
"chrome.collapse": "收起设置",
|
|
497
592
|
"chrome.expand": "展开设置",
|
|
498
593
|
"chrome.unsaved": "未保存",
|
|
@@ -531,6 +626,14 @@ var en = {
|
|
|
531
626
|
"field.reconnectBackoffMsHint": "Interval between event-stream reconnect attempts.",
|
|
532
627
|
"field.verbose": "Verbose logs",
|
|
533
628
|
"field.verboseHint": "Log [auto-continue] lines to the browser console.",
|
|
629
|
+
"field.classify": "Classify errors",
|
|
630
|
+
"field.classifyHint": "Auto-resume transient failures only (network/timeout/5xx…); auth, balance and model errors are skipped and notified.",
|
|
631
|
+
"field.backoffFactor": "Backoff factor",
|
|
632
|
+
"field.backoffFactorHint": "Cooldown multiplier per consecutive failure (2 = 20s→40s→80s…).",
|
|
633
|
+
"field.backoffMaxMs": "Max backoff (ms)",
|
|
634
|
+
"field.backoffMaxMsHint": "Cap on the adaptive backoff interval.",
|
|
635
|
+
"field.notify": "Browser notifications",
|
|
636
|
+
"field.notifyHint": "Notify when auto-continue fires, gives up, or hits a permanent error.",
|
|
534
637
|
"chrome.collapse": "Hide settings",
|
|
535
638
|
"chrome.expand": "Show settings",
|
|
536
639
|
"chrome.unsaved": "Unsaved",
|
|
@@ -892,7 +995,11 @@ var AutoContinueSettingsCardController = class {
|
|
|
892
995
|
numberField("freshMs", 0),
|
|
893
996
|
numberField("reconnectScanDelayMs", 0),
|
|
894
997
|
numberField("reconnectBackoffMs", 0),
|
|
895
|
-
booleanField("verbose")
|
|
998
|
+
booleanField("verbose"),
|
|
999
|
+
booleanField("classify"),
|
|
1000
|
+
numberField("backoffFactor", 1),
|
|
1001
|
+
numberField("backoffMaxMs", 0),
|
|
1002
|
+
booleanField("notify")
|
|
896
1003
|
]);
|
|
897
1004
|
this.store = this.form.bind(() => this.projection(), import_client.createSnapshotStore);
|
|
898
1005
|
}
|
|
@@ -908,7 +1015,11 @@ var AutoContinueSettingsCardController = class {
|
|
|
908
1015
|
freshMs: this.form.field("freshMs"),
|
|
909
1016
|
reconnectScanDelayMs: this.form.field("reconnectScanDelayMs"),
|
|
910
1017
|
reconnectBackoffMs: this.form.field("reconnectBackoffMs"),
|
|
911
|
-
verbose: this.form.field("verbose")
|
|
1018
|
+
verbose: this.form.field("verbose"),
|
|
1019
|
+
classify: this.form.field("classify"),
|
|
1020
|
+
backoffFactor: this.form.field("backoffFactor"),
|
|
1021
|
+
backoffMaxMs: this.form.field("backoffMaxMs"),
|
|
1022
|
+
notify: this.form.field("notify")
|
|
912
1023
|
};
|
|
913
1024
|
}
|
|
914
1025
|
/**
|
|
@@ -1159,6 +1270,56 @@ function AutoContinueSettingsCard(props) {
|
|
|
1159
1270
|
onEdit: (text) => props.edit("verbose", text),
|
|
1160
1271
|
onReset: () => props.resetField("verbose")
|
|
1161
1272
|
}
|
|
1273
|
+
),
|
|
1274
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1275
|
+
BooleanField,
|
|
1276
|
+
{
|
|
1277
|
+
id: "auto-continue-classify",
|
|
1278
|
+
label: t("field.classify"),
|
|
1279
|
+
hint: t("field.classifyHint"),
|
|
1280
|
+
...shared,
|
|
1281
|
+
...state.classify,
|
|
1282
|
+
onEdit: (text) => props.edit("classify", text),
|
|
1283
|
+
onReset: () => props.resetField("classify")
|
|
1284
|
+
}
|
|
1285
|
+
),
|
|
1286
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1287
|
+
ValueField,
|
|
1288
|
+
{
|
|
1289
|
+
id: "auto-continue-backoff-factor",
|
|
1290
|
+
label: t("field.backoffFactor"),
|
|
1291
|
+
hint: t("field.backoffFactorHint"),
|
|
1292
|
+
numeric: true,
|
|
1293
|
+
...shared,
|
|
1294
|
+
...state.backoffFactor,
|
|
1295
|
+
onEdit: (text) => props.edit("backoffFactor", text),
|
|
1296
|
+
onReset: () => props.resetField("backoffFactor")
|
|
1297
|
+
}
|
|
1298
|
+
),
|
|
1299
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1300
|
+
ValueField,
|
|
1301
|
+
{
|
|
1302
|
+
id: "auto-continue-backoff-max",
|
|
1303
|
+
label: t("field.backoffMaxMs"),
|
|
1304
|
+
hint: t("field.backoffMaxMsHint"),
|
|
1305
|
+
numeric: true,
|
|
1306
|
+
...shared,
|
|
1307
|
+
...state.backoffMaxMs,
|
|
1308
|
+
onEdit: (text) => props.edit("backoffMaxMs", text),
|
|
1309
|
+
onReset: () => props.resetField("backoffMaxMs")
|
|
1310
|
+
}
|
|
1311
|
+
),
|
|
1312
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1313
|
+
BooleanField,
|
|
1314
|
+
{
|
|
1315
|
+
id: "auto-continue-notify",
|
|
1316
|
+
label: t("field.notify"),
|
|
1317
|
+
hint: t("field.notifyHint"),
|
|
1318
|
+
...shared,
|
|
1319
|
+
...state.notify,
|
|
1320
|
+
onEdit: (text) => props.edit("notify", text),
|
|
1321
|
+
onReset: () => props.resetField("notify")
|
|
1322
|
+
}
|
|
1162
1323
|
)
|
|
1163
1324
|
]
|
|
1164
1325
|
}
|