dsh-client-auto-continue 0.4.4 → 0.5.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/lib/client.js CHANGED
@@ -27,13 +27,21 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
27
27
  var index_exports = {};
28
28
  __export(index_exports, {
29
29
  apply: () => apply,
30
- inject: () => inject
30
+ fillTemplate: () => fillTemplate,
31
+ inject: () => inject,
32
+ pauseSession: () => pauseSession,
33
+ pausedSessions: () => pausedSessions,
34
+ readTodayStats: () => readTodayStats,
35
+ resetTodayStats: () => resetTodayStats,
36
+ sessionPauseUntil: () => sessionPauseUntil,
37
+ unpauseSession: () => unpauseSession
31
38
  });
32
39
  module.exports = __toCommonJS(index_exports);
33
40
 
34
41
  // src/client/engine.ts
35
42
  var DEFAULT_CONFIG = {
36
43
  continueText: "继续",
44
+ continueTextMaxTokens: "继续",
37
45
  graceMs: 3e3,
38
46
  cooldownMs: 2e4,
39
47
  maxConsecutive: 3,
@@ -46,7 +54,8 @@ var DEFAULT_CONFIG = {
46
54
  classify: true,
47
55
  backoffFactor: 2,
48
56
  backoffMaxMs: 3e5,
49
- notify: false
57
+ notify: false,
58
+ paused: false
50
59
  };
51
60
  function numberOr(value, fallback) {
52
61
  return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
@@ -57,8 +66,10 @@ function booleanOr(value, fallback) {
57
66
  function resolveConfig(section) {
58
67
  const value = section ?? {};
59
68
  const text = typeof value.continueText === "string" && value.continueText.trim() !== "" ? value.continueText : DEFAULT_CONFIG.continueText;
69
+ const maxTokensText = typeof value.continueTextMaxTokens === "string" && value.continueTextMaxTokens.trim() !== "" ? value.continueTextMaxTokens : DEFAULT_CONFIG.continueTextMaxTokens;
60
70
  return {
61
71
  continueText: text,
72
+ continueTextMaxTokens: maxTokensText,
62
73
  graceMs: numberOr(value.graceMs, DEFAULT_CONFIG.graceMs),
63
74
  cooldownMs: numberOr(value.cooldownMs, DEFAULT_CONFIG.cooldownMs),
64
75
  maxConsecutive: Math.max(1, numberOr(value.maxConsecutive, DEFAULT_CONFIG.maxConsecutive)),
@@ -71,7 +82,8 @@ function resolveConfig(section) {
71
82
  classify: booleanOr(value.classify, DEFAULT_CONFIG.classify),
72
83
  backoffFactor: Math.max(1, numberOr(value.backoffFactor, DEFAULT_CONFIG.backoffFactor)),
73
84
  backoffMaxMs: numberOr(value.backoffMaxMs, DEFAULT_CONFIG.backoffMaxMs),
74
- notify: booleanOr(value.notify, DEFAULT_CONFIG.notify)
85
+ notify: booleanOr(value.notify, DEFAULT_CONFIG.notify),
86
+ paused: booleanOr(value.paused, DEFAULT_CONFIG.paused)
75
87
  };
76
88
  }
77
89
  function isNonHumanReason(kind) {
@@ -87,24 +99,47 @@ function isTransientFailure(failure) {
87
99
  function isTransientAgentError(message) {
88
100
  return /network|timeout|timed ?out|econn|etimedout|socket|5\d\d|\b429\b|upstream|temporar/i.test(message);
89
101
  }
90
- function notify(title, body) {
102
+ function notify(title, body, options) {
91
103
  try {
92
104
  const N = globalThis.Notification;
93
105
  if (typeof N === "undefined") return;
94
106
  const permission = N.permission;
107
+ const create = () => {
108
+ const instance = new N(title, {
109
+ body,
110
+ ...options?.actions !== void 0 && options.actions.length > 0 ? { actions: options.actions } : {}
111
+ });
112
+ const target = instance;
113
+ target.onclick = () => {
114
+ try {
115
+ globalThis.focus?.();
116
+ } catch {
117
+ }
118
+ };
119
+ if (options?.onAction !== void 0) {
120
+ target.onaction = (event) => options.onAction?.(event.action);
121
+ }
122
+ };
95
123
  if (permission === "granted") {
96
- new N(title, { body });
124
+ create();
97
125
  } else if (permission === "default") {
98
126
  void N.requestPermission?.().then((result) => {
99
- if (result === "granted") new N(title, { body });
127
+ if (result === "granted") create();
100
128
  }).catch(() => {
101
129
  });
102
130
  }
103
131
  } catch {
104
132
  }
105
133
  }
106
- function fillTemplate(template, facts, tool, turn) {
107
- 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) : "");
134
+ function formatElapsed(ms) {
135
+ if (ms === void 0 || !Number.isFinite(ms) || ms < 0) return "";
136
+ if (ms < 1e3) return `${Math.round(ms)}ms`;
137
+ const s = Math.round(ms / 1e3);
138
+ if (s < 60) return `${s}s`;
139
+ return `${Math.floor(s / 60)}m${s % 60 > 0 ? `${s % 60}s` : ""}`;
140
+ }
141
+ function fillTemplate(template, ctx) {
142
+ return template.replace(/\{code\}/g, ctx.facts?.code ?? "").replace(/\{message\}/g, ctx.facts?.message ?? "").replace(/\{status\}/g, ctx.facts?.status !== void 0 ? String(ctx.facts.status) : "").replace(/\{tool\}/g, ctx.tool ?? "").replace(/\{turn\}/g, ctx.turn !== void 0 ? String(ctx.turn) : "").replace(/\{errorCount\}/g, ctx.errorCount !== void 0 ? String(ctx.errorCount) : "").replace(/\{sessionTitle\}/g, ctx.sessionTitle ?? "").replace(/\{elapsed\}/g, formatElapsed(ctx.elapsedMs));
108
143
  }
109
144
  function effectiveCooldown(consecutive, base, factor, max) {
110
145
  const multiplier = Math.pow(factor, consecutive);
@@ -151,6 +186,92 @@ function writeLastSend(sessionId, at) {
151
186
  } catch {
152
187
  }
153
188
  }
189
+ var pauseKey = (sessionId) => `${lockPrefix}pause:${sessionId}`;
190
+ function pauseSession(sessionId, ms) {
191
+ try {
192
+ localStorage.setItem(pauseKey(sessionId), String(Date.now() + ms));
193
+ } catch {
194
+ }
195
+ }
196
+ function unpauseSession(sessionId) {
197
+ try {
198
+ localStorage.removeItem(pauseKey(sessionId));
199
+ } catch {
200
+ }
201
+ }
202
+ function sessionPauseUntil(sessionId) {
203
+ try {
204
+ return Number(localStorage.getItem(pauseKey(sessionId)) ?? 0) || 0;
205
+ } catch {
206
+ return 0;
207
+ }
208
+ }
209
+ function pausedSessions() {
210
+ const out = [];
211
+ const now = Date.now();
212
+ try {
213
+ for (let i = 0; i < localStorage.length; i += 1) {
214
+ const key = localStorage.key(i);
215
+ if (key === null || !key.startsWith(`${lockPrefix}pause:`)) continue;
216
+ const sessionId = key.slice(lockPrefix.length + "pause:".length);
217
+ const until = Number(localStorage.getItem(key) ?? 0) || 0;
218
+ if (until > now) out.push({ sessionId, until });
219
+ else localStorage.removeItem(key);
220
+ }
221
+ } catch {
222
+ }
223
+ return out;
224
+ }
225
+ var statsKey = `${lockPrefix}stats`;
226
+ var STATS_MAX_DAYS = 90;
227
+ function todayKey() {
228
+ const d = /* @__PURE__ */ new Date();
229
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
230
+ const dd = String(d.getDate()).padStart(2, "0");
231
+ return `${d.getFullYear()}-${mm}-${dd}`;
232
+ }
233
+ function readStats() {
234
+ try {
235
+ const raw = localStorage.getItem(statsKey);
236
+ if (raw === null) return [];
237
+ const parsed = JSON.parse(raw);
238
+ if (!Array.isArray(parsed)) return [];
239
+ return parsed.filter(
240
+ (item) => typeof item === "object" && item !== null && typeof item.date === "string"
241
+ );
242
+ } catch {
243
+ return [];
244
+ }
245
+ }
246
+ function writeStats(list) {
247
+ try {
248
+ localStorage.setItem(statsKey, JSON.stringify(list));
249
+ } catch {
250
+ }
251
+ }
252
+ function bumpStat(delta) {
253
+ const list = readStats();
254
+ let day = list.find((item) => item.date === todayKey());
255
+ if (day === void 0) {
256
+ day = { date: todayKey(), sent: 0, skipped: 0, recovered: 0, failed: 0, gaveUp: 0, byCode: {} };
257
+ list.unshift(day);
258
+ }
259
+ if (delta.sent !== void 0) day.sent += delta.sent;
260
+ if (delta.skipped !== void 0) day.skipped += delta.skipped;
261
+ if (delta.recovered !== void 0) day.recovered += delta.recovered;
262
+ if (delta.failed !== void 0) day.failed += delta.failed;
263
+ if (delta.gaveUp !== void 0) day.gaveUp += delta.gaveUp;
264
+ if (delta.code !== void 0) day.byCode[delta.code] = (day.byCode[delta.code] ?? 0) + 1;
265
+ writeStats(list.slice(0, STATS_MAX_DAYS));
266
+ }
267
+ function readTodayStats() {
268
+ const today = todayKey();
269
+ const found = readStats().find((item) => item.date === today);
270
+ return found ?? { date: today, sent: 0, skipped: 0, recovered: 0, failed: 0, gaveUp: 0, byCode: {} };
271
+ }
272
+ function resetTodayStats() {
273
+ writeStats(readStats().filter((item) => item.date !== todayKey()));
274
+ }
154
275
  var freshState = () => ({
155
276
  consecutive: 0,
156
277
  lastAutoAt: 0,
@@ -161,9 +282,12 @@ var freshState = () => ({
161
282
  queued: 0,
162
283
  subagent: false,
163
284
  lastFailure: void 0,
285
+ lastFailureAt: 0,
164
286
  lastTool: void 0,
165
- lastTurn: void 0
287
+ lastTurn: void 0,
288
+ pendingRecoveryAt: 0
166
289
  });
290
+ var RECOVERY_WINDOW_MS = 10 * 60 * 1e3;
167
291
  function isOurEcho(state, event) {
168
292
  if (event.type !== "user/message") return false;
169
293
  const message = event.data;
@@ -210,6 +334,8 @@ var AutoContinueRunner = class {
210
334
  this.hostAbort = new AbortController();
211
335
  this.disposed = false;
212
336
  this.reconnectScans = 0;
337
+ /** 会话标题缓存(来自 session.list 投影, {sessionTitle} 占位符用)。 */
338
+ this.titles = /* @__PURE__ */ new Map();
213
339
  const config = this.getConfig();
214
340
  void this.runMux();
215
341
  void this.runHost();
@@ -295,11 +421,14 @@ var AutoContinueRunner = class {
295
421
  if (reason.kind === "completed") {
296
422
  state.consecutive = 0;
297
423
  state.lastFailure = void 0;
424
+ this.noteRecovery(sessionId, "completed");
298
425
  } else if (reason.kind === "aborted") {
299
426
  state.consecutive = 0;
427
+ state.pendingRecoveryAt = 0;
300
428
  } else if (reason.kind === "blocked") {
301
429
  } else if (reason.kind === "interrupted") {
302
430
  state.consecutive = 0;
431
+ state.pendingRecoveryAt = 0;
303
432
  } else if (reason.kind === "error") {
304
433
  const error = reason.error;
305
434
  state.lastFailure = {
@@ -308,8 +437,12 @@ var AutoContinueRunner = class {
308
437
  ...typeof error.status === "number" ? { status: error.status } : {}
309
438
  };
310
439
  state.lastTurn = event.data.turn;
440
+ state.lastFailureAt = Date.now();
441
+ this.noteRecovery(sessionId, "error");
311
442
  this.onTurnFailure(sessionId, "turn/end:error", state.lastFailure);
312
443
  } else if (reason.kind === "max-tokens") {
444
+ state.lastFailureAt = Date.now();
445
+ this.noteRecovery(sessionId, "error");
313
446
  this.schedule(sessionId, "turn/end:max-tokens");
314
447
  }
315
448
  break;
@@ -340,8 +473,13 @@ var AutoContinueRunner = class {
340
473
  this.log(`host/agent-error(${frame.sessionId}): ${frame.message}`);
341
474
  if (this.getConfig().classify && !isTransientAgentError(frame.message)) {
342
475
  this.log(`跳过 ${frame.sessionId}: 永久性 agent 错误 — ${frame.message}`);
476
+ bumpStat({ skipped: 1 });
343
477
  if (this.getConfig().notify) {
344
- notify("dsh-auto-continue: 未自动继续", `${frame.sessionId}: 永久性 agent 错误 ${frame.message.slice(0, 120)}`);
478
+ notify(
479
+ "dsh-auto-continue: 未自动继续",
480
+ `${frame.sessionId}: 永久性 agent 错误 ${frame.message.slice(0, 120)}`,
481
+ this.notifyOptions(frame.sessionId)
482
+ );
345
483
  }
346
484
  break;
347
485
  }
@@ -362,13 +500,61 @@ var AutoContinueRunner = class {
362
500
  if (config.classify && !isTransientFailure(failure)) {
363
501
  const summary = `${failure.code}${failure.status !== void 0 ? ` (HTTP ${failure.status})` : ""}`;
364
502
  this.log(`跳过 ${sessionId}(${reason}): 永久性失败 ${summary} — ${failure.message}`);
503
+ bumpStat({ skipped: 1, code: failure.code });
365
504
  if (config.notify) {
366
- notify("dsh-auto-continue: 未自动继续", `${sessionId}: 永久性错误 ${summary},需要人工处理`);
505
+ notify(
506
+ "dsh-auto-continue: 未自动继续",
507
+ `${sessionId}: 永久性错误 ${summary},需要人工处理`,
508
+ this.notifyOptions(sessionId)
509
+ );
367
510
  }
368
511
  return;
369
512
  }
370
513
  this.schedule(sessionId, reason);
371
514
  }
515
+ /** 通知操作按钮与回调(「立即续跑」/「暂停该会话 1 小时」)。 */
516
+ notifyOptions(sessionId) {
517
+ return {
518
+ actions: [
519
+ { action: "resume", title: "立即续跑" },
520
+ { action: "pause1h", title: "暂停该会话 1 小时" }
521
+ ],
522
+ onAction: (action) => this.onNotifyAction(sessionId, action)
523
+ };
524
+ }
525
+ onNotifyAction(sessionId, action) {
526
+ if (action === "resume") {
527
+ this.log(`通知按钮: 立即续跑 ${sessionId}`);
528
+ void this.resumeNow(sessionId);
529
+ } else if (action === "pause1h") {
530
+ this.log(`通知按钮: 暂停 ${sessionId} 1 小时`);
531
+ pauseSession(sessionId, 60 * 60 * 1e3);
532
+ this.cancelPending(sessionId, "通知按钮暂停该会话");
533
+ }
534
+ }
535
+ /** 恢复结果记账: 自动发送后窗口内的回合结束, 判定恢复成功或失败。 */
536
+ noteRecovery(sessionId, outcome) {
537
+ const state = this.state(sessionId);
538
+ if (state.pendingRecoveryAt === 0) return;
539
+ if (Date.now() - state.pendingRecoveryAt > RECOVERY_WINDOW_MS) {
540
+ state.pendingRecoveryAt = 0;
541
+ return;
542
+ }
543
+ state.pendingRecoveryAt = 0;
544
+ bumpStat(outcome === "completed" ? { recovered: 1 } : { failed: 1 });
545
+ this.log(`恢复结果(${sessionId}): ${outcome === "completed" ? "成功" : "失败"}`);
546
+ }
547
+ /** 立即为该会话发送一次自动继续(无视冷却与连续上限; 由通知按钮触发)。 */
548
+ async resumeNow(sessionId) {
549
+ if (this.disposed) return;
550
+ const state = this.state(sessionId);
551
+ if (state.subagent) return;
552
+ if (state.pendingTimer !== void 0) {
553
+ clearTimeout(state.pendingTimer);
554
+ state.pendingTimer = void 0;
555
+ }
556
+ await this.fire(sessionId, "manual:notification", true);
557
+ }
372
558
  /** 本会话当前生效的冷却间隔(自适应退避)。 */
373
559
  cooldownFor(state) {
374
560
  const config = this.getConfig();
@@ -383,6 +569,14 @@ var AutoContinueRunner = class {
383
569
  const state = this.state(sessionId);
384
570
  const config = this.getConfig();
385
571
  if (state.subagent) return;
572
+ if (config.paused) {
573
+ this.log(`跳过 ${sessionId}(${reason}): 全局暂停中`);
574
+ return;
575
+ }
576
+ if (Date.now() < sessionPauseUntil(sessionId)) {
577
+ this.log(`跳过 ${sessionId}(${reason}): 会话暂停中`);
578
+ return;
579
+ }
386
580
  if (state.pendingTimer !== void 0) return;
387
581
  if (Date.now() - state.lastAttemptAt < this.cooldownFor(state)) return;
388
582
  if (state.consecutive >= config.maxConsecutive) {
@@ -398,8 +592,9 @@ var AutoContinueRunner = class {
398
592
  void this.fire(sessionId, reason);
399
593
  }, config.graceMs);
400
594
  state.pendingTimer = timer;
595
+ const template = reason.includes("max-tokens") ? config.continueTextMaxTokens : config.continueText;
401
596
  this.log(
402
- `检测到非人为中断 ${sessionId}(${reason}), ${config.graceMs}ms 后自动发送「${config.continueText}」`
597
+ `检测到非人为中断 ${sessionId}(${reason}), ${config.graceMs}ms 后自动发送「${template}」`
403
598
  );
404
599
  }
405
600
  cancelPending(sessionId, why) {
@@ -409,7 +604,7 @@ var AutoContinueRunner = class {
409
604
  state.pendingTimer = void 0;
410
605
  this.log(`取消 ${sessionId} 的自动继续(${why})`);
411
606
  }
412
- async fire(sessionId, reason) {
607
+ async fire(sessionId, reason, force = false) {
413
608
  if (this.disposed) return;
414
609
  const state = this.state(sessionId);
415
610
  const config = this.getConfig();
@@ -427,7 +622,7 @@ var AutoContinueRunner = class {
427
622
  this.log(`跳过 ${sessionId}: 已有排队消息`);
428
623
  return;
429
624
  }
430
- if (Date.now() - readLastSend(sessionId) < this.cooldownFor(state)) {
625
+ if (!force && Date.now() - readLastSend(sessionId) < this.cooldownFor(state)) {
431
626
  this.log(`跳过 ${sessionId}: 其他标签页刚发送过`);
432
627
  return;
433
628
  }
@@ -435,7 +630,23 @@ var AutoContinueRunner = class {
435
630
  this.log(`跳过 ${sessionId}: 其他标签页正在发送`);
436
631
  return;
437
632
  }
438
- const text = fillTemplate(config.continueText, state.lastFailure, state.lastTool, state.lastTurn);
633
+ const template = reason.includes("max-tokens") ? config.continueTextMaxTokens : config.continueText;
634
+ let sessionTitle;
635
+ if (template.includes("{sessionTitle}")) {
636
+ sessionTitle = this.titles.get(sessionId);
637
+ if (sessionTitle === void 0) {
638
+ const info = await this.fetchSessionInfo(sessionId);
639
+ sessionTitle = info?.title;
640
+ }
641
+ }
642
+ const text = fillTemplate(template, {
643
+ facts: state.lastFailure,
644
+ tool: state.lastTool,
645
+ turn: state.lastTurn,
646
+ errorCount: state.consecutive + 1,
647
+ sessionTitle,
648
+ elapsedMs: state.lastFailureAt > 0 ? Date.now() - state.lastFailureAt : void 0
649
+ });
439
650
  const zone = clientTimeZone();
440
651
  state.lastAttemptAt = Date.now();
441
652
  try {
@@ -450,15 +661,26 @@ var AutoContinueRunner = class {
450
661
  state.consecutive += 1;
451
662
  state.lastAutoAt = now;
452
663
  state.lastSentText = text;
664
+ state.pendingRecoveryAt = now;
453
665
  writeLastSend(sessionId, now);
666
+ bumpStat({ sent: 1, ...state.lastFailure !== void 0 ? { code: state.lastFailure.code } : {} });
454
667
  this.log(`已自动发送「${text}」到 ${sessionId}(${reason}), 第 ${state.consecutive} 次连续`);
455
668
  if (config.notify) {
456
- notify("dsh-auto-continue: 已自动继续", `${sessionId}: 已发送「${text}」(第 ${state.consecutive} 次连续)`);
669
+ notify(
670
+ "dsh-auto-continue: 已自动继续",
671
+ `${sessionId}: 已发送「${text}」(第 ${state.consecutive} 次连续)`,
672
+ this.notifyOptions(sessionId)
673
+ );
457
674
  }
458
675
  if (state.consecutive >= config.maxConsecutive) {
676
+ bumpStat({ gaveUp: 1 });
459
677
  this.log(`达到连续上限 ${config.maxConsecutive} 次, 停止自动继续 ${sessionId}`);
460
678
  if (config.notify) {
461
- notify("dsh-auto-continue: 已停止自动继续", `${sessionId}: 连续失败 ${state.consecutive} 次, 需要人工介入`);
679
+ notify(
680
+ "dsh-auto-continue: 已停止自动继续",
681
+ `${sessionId}: 连续失败 ${state.consecutive} 次, 需要人工介入`,
682
+ this.notifyOptions(sessionId)
683
+ );
462
684
  }
463
685
  }
464
686
  } else {
@@ -472,18 +694,26 @@ var AutoContinueRunner = class {
472
694
  releaseSend(sessionId);
473
695
  }
474
696
  }
475
- async runningViaList(sessionId) {
697
+ /** 查一次 session.list, 顺带缓存该会话的标题。 */
698
+ async fetchSessionInfo(sessionId) {
476
699
  try {
477
700
  const response = await this.api.sessions.list({});
478
701
  if (!response.result.ok) return void 0;
479
702
  const item = response.result.value.items.find(
480
703
  (summary) => summary.sessionId === sessionId
481
704
  );
482
- return item === void 0 ? void 0 : item.running;
705
+ if (item === void 0) return void 0;
706
+ const title = item.projections?.values?.title;
707
+ if (typeof title === "string" && title !== "") this.titles.set(sessionId, title);
708
+ return { running: item.running, title: typeof title === "string" ? title : void 0 };
483
709
  } catch {
484
710
  return void 0;
485
711
  }
486
712
  }
713
+ async runningViaList(sessionId) {
714
+ const info = await this.fetchSessionInfo(sessionId);
715
+ return info?.running;
716
+ }
487
717
  // ---------- 启动/重连扫描 ----------
488
718
  scheduleReconnectScan() {
489
719
  this.reconnectScans += 1;
@@ -518,9 +748,14 @@ var AutoContinueRunner = class {
518
748
  */
519
749
  async scanInterrupted() {
520
750
  const config = this.getConfig();
751
+ if (config.paused) return true;
521
752
  const response = await this.api.sessions.list({});
522
753
  if (!response.result.ok) return false;
523
754
  const items = response.result.value.items;
755
+ for (const summary of items) {
756
+ const title = summary.projections?.values?.title;
757
+ if (typeof title === "string" && title !== "") this.titles.set(summary.sessionId, title);
758
+ }
524
759
  const candidates = items.filter((summary) => !summary.running && summary.parentSessionId === void 0).slice(0, config.scanLimit);
525
760
  const now = Date.now();
526
761
  for (const summary of candidates) {
@@ -529,6 +764,7 @@ var AutoContinueRunner = class {
529
764
  if (state.pendingTimer !== void 0) continue;
530
765
  if (state.consecutive >= config.maxConsecutive) continue;
531
766
  if (now - state.lastAttemptAt < this.cooldownFor(state)) continue;
767
+ if (now < sessionPauseUntil(summary.sessionId)) continue;
532
768
  let events;
533
769
  try {
534
770
  const page = await this.api.sessions.history({
@@ -572,8 +808,12 @@ var AutoContinueRunner = class {
572
808
  var zh = {
573
809
  "card.title": "自动继续",
574
810
  "card.description": "请求因网络等原因(非人为)中断后, 自动发送「继续」续跑。",
811
+ "field.paused": "暂停自动继续",
812
+ "field.pausedHint": "全局暂停: 实时与扫描都不会再自动发送, 已排队的待发送也会取消。",
575
813
  "field.continueText": "继续文本",
576
814
  "field.continueTextHint": "中断后自动发送的消息内容。",
815
+ "field.continueTextMaxTokens": "超限时的继续文本",
816
+ "field.continueTextMaxTokensHint": "达到输出 token 上限时自动发送的文本, 支持与继续文本相同的占位符。",
577
817
  "field.graceMs": "宽限期 (ms)",
578
818
  "field.graceMsHint": "检测到中断后等待的时长; 期间宿主自行恢复则取消。",
579
819
  "field.cooldownMs": "冷却时间 (ms)",
@@ -599,7 +839,21 @@ var zh = {
599
839
  "field.backoffMaxMs": "最大退避间隔 (ms)",
600
840
  "field.backoffMaxMsHint": "自适应退避的上限, 防止等待过久。",
601
841
  "field.notify": "浏览器通知",
602
- "field.notifyHint": "自动继续成功/放弃/遇到永久性错误时弹出浏览器通知。",
842
+ "field.notifyHint": "自动继续成功/放弃/遇到永久性错误时弹出浏览器通知, 通知带「立即续跑」与「暂停该会话 1 小时」按钮。",
843
+ "stats.title": "今日统计",
844
+ "stats.sent": "自动继续",
845
+ "stats.skipped": "跳过(永久错误)",
846
+ "stats.recovered": "恢复成功",
847
+ "stats.failed": "继续后仍失败",
848
+ "stats.gaveUp": "停止(达上限)",
849
+ "stats.byCode": "错误码分布",
850
+ "stats.empty": "今天还没有自动继续记录。",
851
+ "stats.reset": "清零",
852
+ "pause.title": "已暂停会话",
853
+ "pause.none": "没有暂停中的会话。",
854
+ "pause.clearAll": "全部解除",
855
+ "pause.unpause": "解除",
856
+ "pause.minutes": "分钟",
603
857
  "chrome.collapse": "收起设置",
604
858
  "chrome.expand": "展开设置",
605
859
  "chrome.unsaved": "未保存",
@@ -618,8 +872,12 @@ var zh = {
618
872
  var en = {
619
873
  "card.title": "Auto continue",
620
874
  "card.description": "When a request is interrupted by a non-human cause, automatically send 「继续」 to resume.",
875
+ "field.paused": "Pause auto-continue",
876
+ "field.pausedHint": "Globally pause: no live or scan auto-send fires, and queued pending sends are cancelled.",
621
877
  "field.continueText": "Continue text",
622
878
  "field.continueTextHint": "Message automatically sent after an interruption.",
879
+ "field.continueTextMaxTokens": "Continue text (max tokens)",
880
+ "field.continueTextMaxTokensHint": "Text sent when the output token ceiling is reached; same placeholders as the continue text.",
623
881
  "field.graceMs": "Grace period (ms)",
624
882
  "field.graceMsHint": "Wait after an interruption; cancelled if the host recovers on its own.",
625
883
  "field.cooldownMs": "Cooldown (ms)",
@@ -645,7 +903,21 @@ var en = {
645
903
  "field.backoffMaxMs": "Max backoff (ms)",
646
904
  "field.backoffMaxMsHint": "Cap on the adaptive backoff interval.",
647
905
  "field.notify": "Browser notifications",
648
- "field.notifyHint": "Notify when auto-continue fires, gives up, or hits a permanent error.",
906
+ "field.notifyHint": 'Notify when auto-continue fires, gives up, or hits a permanent error; notifications carry "Resume now" and "Pause this session 1h" buttons.',
907
+ "stats.title": "Today's stats",
908
+ "stats.sent": "Auto-continued",
909
+ "stats.skipped": "Skipped (permanent)",
910
+ "stats.recovered": "Recovered",
911
+ "stats.failed": "Failed after",
912
+ "stats.gaveUp": "Gave up (cap)",
913
+ "stats.byCode": "By error code",
914
+ "stats.empty": "No auto-continue activity today.",
915
+ "stats.reset": "Reset",
916
+ "pause.title": "Paused sessions",
917
+ "pause.none": "No sessions paused.",
918
+ "pause.clearAll": "Clear all",
919
+ "pause.unpause": "Resume",
920
+ "pause.minutes": "min",
649
921
  "chrome.collapse": "Hide settings",
650
922
  "chrome.expand": "Show settings",
651
923
  "chrome.unsaved": "Unsaved",
@@ -978,6 +1250,32 @@ var css = `
978
1250
  .dshAcSelect:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
979
1251
  .dshAcInvalid { color: var(--dsw-alias-label-error); margin: 0; font-size: 12px; line-height: 1.5; }
980
1252
  .dshAcHint { color: var(--dsw-alias-label-tertiary); margin: 0; font-size: 12px; line-height: 1.5; }
1253
+ .dshAcPanel { border-top: 1px solid var(--dsw-alias-border-l2); flex-direction: column; gap: 8px; padding: 12px 0; display: flex; }
1254
+ .dshAcPanelHead { align-items: center; gap: 8px; display: flex; }
1255
+ .dshAcPanelTitle { color: var(--dsw-alias-label-primary); flex: 1; font-size: 13px; font-weight: 600; line-height: 1.5; }
1256
+ .dshAcStats { gap: 4px 16px; margin: 0; grid-template-columns: repeat(2, minmax(0, 1fr)); display: grid; }
1257
+ .dshAcStats > div { justify-content: space-between; gap: 8px; display: flex; }
1258
+ .dshAcStats dt { color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 1.5; }
1259
+ .dshAcStats dd { color: var(--dsw-alias-label-primary); margin: 0; font-size: 12px; font-weight: 600; line-height: 1.5; }
1260
+ .dshAcCodes { flex-wrap: wrap; align-items: center; gap: 6px; display: flex; }
1261
+ .dshAcCode {
1262
+ white-space: nowrap;
1263
+ background: var(--dsw-alias-bg-module-platform);
1264
+ color: var(--dsw-alias-label-secondary);
1265
+ border-radius: 999px;
1266
+ padding: 1px 8px;
1267
+ font-size: 11px;
1268
+ font-weight: 500;
1269
+ line-height: 17px;
1270
+ }
1271
+ .dshAcPauseList { flex-direction: column; gap: 4px; margin: 0; padding: 0; list-style: none; display: flex; }
1272
+ .dshAcPauseList li { align-items: center; gap: 8px; display: flex; }
1273
+ .dshAcPauseId {
1274
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
1275
+ color: var(--dsw-alias-label-primary);
1276
+ font-size: 12px;
1277
+ line-height: 1.5;
1278
+ }
981
1279
  `;
982
1280
  function injectStyles() {
983
1281
  if (typeof document === "undefined") return;
@@ -998,7 +1296,9 @@ var AutoContinueSettingsCardController = class {
998
1296
  */
999
1297
  constructor(scope) {
1000
1298
  this.form = new CardForm(scope, [
1299
+ booleanField("paused"),
1001
1300
  textField("continueText"),
1301
+ textField("continueTextMaxTokens"),
1002
1302
  numberField("graceMs", 0),
1003
1303
  numberField("cooldownMs", 0),
1004
1304
  numberField("maxConsecutive", 1),
@@ -1018,7 +1318,9 @@ var AutoContinueSettingsCardController = class {
1018
1318
  projection() {
1019
1319
  return {
1020
1320
  ...this.form.shell(),
1321
+ paused: this.form.field("paused"),
1021
1322
  continueText: this.form.field("continueText"),
1323
+ continueTextMaxTokens: this.form.field("continueTextMaxTokens"),
1022
1324
  graceMs: this.form.field("graceMs"),
1023
1325
  cooldownMs: this.form.field("cooldownMs"),
1024
1326
  maxConsecutive: this.form.field("maxConsecutive"),
@@ -1141,6 +1443,112 @@ function BooleanField(props) {
1141
1443
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dshAcHint", children: props.hint })
1142
1444
  ] });
1143
1445
  }
1446
+ function LivePanels(props) {
1447
+ const { t } = props;
1448
+ const [, refresh] = (0, import_react.useState)(0);
1449
+ (0, import_react.useEffect)(() => {
1450
+ const timer = setInterval(() => refresh((value) => value + 1), 5e3);
1451
+ return () => clearInterval(timer);
1452
+ }, []);
1453
+ const stats = readTodayStats();
1454
+ const hasStats = stats.sent + stats.skipped + stats.recovered + stats.failed + stats.gaveUp > 0;
1455
+ const codes = Object.entries(stats.byCode).sort((a, b) => b[1] - a[1]).slice(0, 5);
1456
+ const paused = pausedSessions();
1457
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1458
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: "dshAcPanel", children: [
1459
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshAcPanelHead", children: [
1460
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dshAcPanelTitle", children: t("stats.title") }),
1461
+ hasStats ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1462
+ "button",
1463
+ {
1464
+ type: "button",
1465
+ className: "dshAcReset",
1466
+ onClick: () => {
1467
+ resetTodayStats();
1468
+ refresh((value) => value + 1);
1469
+ },
1470
+ children: t("stats.reset")
1471
+ }
1472
+ ) : null
1473
+ ] }),
1474
+ !hasStats ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dshAcHint", children: t("stats.empty") }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1475
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("dl", { className: "dshAcStats", children: [
1476
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
1477
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", { children: t("stats.sent") }),
1478
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", { children: stats.sent })
1479
+ ] }),
1480
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
1481
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", { children: t("stats.recovered") }),
1482
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", { children: stats.recovered })
1483
+ ] }),
1484
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
1485
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", { children: t("stats.failed") }),
1486
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", { children: stats.failed })
1487
+ ] }),
1488
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
1489
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", { children: t("stats.skipped") }),
1490
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", { children: stats.skipped })
1491
+ ] }),
1492
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
1493
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", { children: t("stats.gaveUp") }),
1494
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", { children: stats.gaveUp })
1495
+ ] })
1496
+ ] }),
1497
+ codes.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshAcCodes", children: [
1498
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "dshAcHint", children: [
1499
+ t("stats.byCode"),
1500
+ ":"
1501
+ ] }),
1502
+ codes.map(([code, count]) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "dshAcCode", children: [
1503
+ code,
1504
+ " ×",
1505
+ count
1506
+ ] }, code))
1507
+ ] }) : null
1508
+ ] })
1509
+ ] }),
1510
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: "dshAcPanel", children: [
1511
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshAcPanelHead", children: [
1512
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dshAcPanelTitle", children: t("pause.title") }),
1513
+ paused.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1514
+ "button",
1515
+ {
1516
+ type: "button",
1517
+ className: "dshAcReset",
1518
+ onClick: () => {
1519
+ for (const item of paused) unpauseSession(item.sessionId);
1520
+ refresh((value) => value + 1);
1521
+ },
1522
+ children: t("pause.clearAll")
1523
+ }
1524
+ ) : null
1525
+ ] }),
1526
+ paused.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dshAcHint", children: t("pause.none") }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: "dshAcPauseList", children: paused.map((item) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { children: [
1527
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "dshAcPauseId", children: [
1528
+ item.sessionId.slice(0, 8),
1529
+ "…"
1530
+ ] }),
1531
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "dshAcHint", children: [
1532
+ Math.max(1, Math.ceil((item.until - Date.now()) / 6e4)),
1533
+ " ",
1534
+ t("pause.minutes")
1535
+ ] }),
1536
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1537
+ "button",
1538
+ {
1539
+ type: "button",
1540
+ className: "dshAcReset",
1541
+ onClick: () => {
1542
+ unpauseSession(item.sessionId);
1543
+ refresh((value) => value + 1);
1544
+ },
1545
+ children: t("pause.unpause")
1546
+ }
1547
+ )
1548
+ ] }, item.sessionId)) })
1549
+ ] })
1550
+ ] });
1551
+ }
1144
1552
  function AutoContinueSettingsCard(props) {
1145
1553
  const { t } = props;
1146
1554
  const state = props.useAutoContinueSettingsCard((snapshot) => snapshot);
@@ -1156,6 +1564,18 @@ function AutoContinueSettingsCard(props) {
1156
1564
  onSave: props.save,
1157
1565
  onDiscard: props.discard,
1158
1566
  children: [
1567
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1568
+ BooleanField,
1569
+ {
1570
+ id: "auto-continue-paused",
1571
+ label: t("field.paused"),
1572
+ hint: t("field.pausedHint"),
1573
+ ...shared,
1574
+ ...state.paused,
1575
+ onEdit: (text) => props.edit("paused", text),
1576
+ onReset: () => props.resetField("paused")
1577
+ }
1578
+ ),
1159
1579
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1160
1580
  ValueField,
1161
1581
  {
@@ -1168,6 +1588,18 @@ function AutoContinueSettingsCard(props) {
1168
1588
  onReset: () => props.resetField("continueText")
1169
1589
  }
1170
1590
  ),
1591
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1592
+ ValueField,
1593
+ {
1594
+ id: "auto-continue-continue-text-max-tokens",
1595
+ label: t("field.continueTextMaxTokens"),
1596
+ hint: t("field.continueTextMaxTokensHint"),
1597
+ ...shared,
1598
+ ...state.continueTextMaxTokens,
1599
+ onEdit: (text) => props.edit("continueTextMaxTokens", text),
1600
+ onReset: () => props.resetField("continueTextMaxTokens")
1601
+ }
1602
+ ),
1171
1603
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1172
1604
  ValueField,
1173
1605
  {
@@ -1332,7 +1764,8 @@ function AutoContinueSettingsCard(props) {
1332
1764
  onEdit: (text) => props.edit("notify", text),
1333
1765
  onReset: () => props.resetField("notify")
1334
1766
  }
1335
- )
1767
+ ),
1768
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LivePanels, { t })
1336
1769
  ]
1337
1770
  }
1338
1771
  );