dsh-client-auto-continue 0.7.4 → 0.8.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.
@@ -663,7 +663,7 @@ interface SessionState {
663
663
  /** 我们上次自动发送的文本(用于识别自己的回显)。 */
664
664
  lastSentText: string;
665
665
  /** 宽限期定时器(进行中的待发送)。 */
666
- pendingTimer: number | undefined;
666
+ pendingTimer: ReturnType<typeof setTimeout> | undefined;
667
667
  /** 宿主权威 running 位(来自 host/session-status 与回合事件)。 */
668
668
  running: boolean | undefined;
669
669
  /** 当前排队消息数(来自 session/queue 帧)。 */
@@ -709,7 +709,7 @@ interface SessionState {
709
709
  /** 本回合已触发过 loop guard(防重复打断)。 */
710
710
  loopFired: boolean;
711
711
  /** loop 重启的延迟定时器(冷却结束后再 schedule)。 */
712
- loopRetryTimer: number | undefined;
712
+ loopRetryTimer: ReturnType<typeof setTimeout> | undefined;
713
713
  /** 我们主动 cancel 过本回合(区分用户停止)。 */
714
714
  loopCancelled: boolean;
715
715
  }
@@ -1361,6 +1361,14 @@ export class AutoContinueRunner {
1361
1361
  this.log(`跳过 ${sessionId}: 发送计数已达上限 ${config.maxConsecutive}, 等待用户介入或成功回合`);
1362
1362
  return;
1363
1363
  }
1364
+ // 宿主权威兜底: 历史里最后一条事件若正是同一文本的 user 消息, 说明它还在
1365
+ // 排队未被处理——不再叠加发送(issue #13 的 13 条排队场景)。
1366
+ // 若最后一条是回合结束等其他事件, 说明之前的同文本消息已被处理, 正常放行
1367
+ // (连续续跑不被误挡)。查询失败时放行(本地防线仍在)。
1368
+ if (!force && (await this.hostHasPendingSameText(sessionId, text))) {
1369
+ this.log(`跳过 ${sessionId}: 宿主队列里已有相同文本消息在排队`);
1370
+ return;
1371
+ }
1364
1372
  state.lastAttemptAt = Date.now(); // 先记账: 无论成败, 本次尝试都进入冷却
1365
1373
  try {
1366
1374
  const response = await this.api.sessions.prompt({
@@ -1453,8 +1461,30 @@ export class AutoContinueRunner {
1453
1461
  return { kind: 'failed', tool: state.lastTool };
1454
1462
  }
1455
1463
 
1456
- /** 会话标题缓存(来自 session.list 投影, {sessionTitle} 占位符用)。 */
1457
- private readonly titles = new Map<SessionId, string>();
1464
+ /**
1465
+ * 宿主权威兜底: 历史里最后一条事件是否就是同一文本的 user 消息。
1466
+ * 是 = 它还在排队未被处理, 不应再叠加发送; 否(回合结束等其他事件)= 放行。
1467
+ * 查询失败时返回 false(放行, 本地防线仍在)。
1468
+ */
1469
+ private async hostHasPendingSameText(sessionId: SessionId, text: string): Promise<boolean> {
1470
+ try {
1471
+ const response = await this.api.sessions.history({ sessionId, maxMessages: 10 });
1472
+ if (!response.result.ok) return false;
1473
+ const events = response.result.value.events;
1474
+ const last = events[events.length - 1]?.event;
1475
+ if (last === undefined || last.type !== 'user/message') return false;
1476
+ if (last.data.source?.kind !== 'user') return false;
1477
+ const lastText = (last.data.content ?? [])
1478
+ .filter((part): part is { type: 'text'; text: string } => part.type === 'text')
1479
+ .map((part) => part.text)
1480
+ .join('');
1481
+ return lastText === text;
1482
+ } catch {
1483
+ return false;
1484
+ }
1485
+ }
1486
+
1487
+ /** 会话标题缓存(来自 session.list 投影, {sessionTitle} 占位符用)。 */ private readonly titles = new Map<SessionId, string>();
1458
1488
 
1459
1489
  /** 查一次 session.list, 顺带缓存该会话的标题。 */
1460
1490
  private async fetchSessionInfo(
@@ -1,37 +1,33 @@
1
1
  /**
2
- * Auto-continue plugin, browser half.
2
+ * Auto-continue plugin, browser half (thin shell).
3
3
  *
4
- * - Runs the auto-continue engine over the live mux + host event streams.
5
- * - Registers the `auto-continue` settings card into the plugin-configuration
6
- * section (`settings.plugin.item`), editing the same namespace the engine
7
- * reads every behavior knob is configurable from the GUI.
4
+ * Since 0.8.0 the auto-continue ENGINE runs inside the host process (single
5
+ * instance see src/host/engine.ts), so this half only:
6
+ * - registers the `auto-continue` settings card (`settings.plugin.item`),
7
+ * - subscribes to the host status bridge (SSE) and shows browser
8
+ * notifications with action buttons (Resume now / Pause 1h) via the bridge
9
+ * action endpoint,
10
+ * - feeds the card's stats / paused-sessions panels from the bridge state.
8
11
  */
9
12
  import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
10
- import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client';
11
13
  // Type-only: pulls the locale plugin's Context merge (ctx.locale).
12
14
  import type {} from '@deepseek-ai/dsh-client-locale/client';
13
15
  // Type-only: pulls the settings-surface SlotMap merge and ctx.settingsScope.
14
16
  import type {} from '@deepseek-ai/dsh-client-ui-settings/client';
15
17
  // Type-only: pulls the `settings.plugin.item` SlotMap merge.
16
18
  import type {} from '@deepseek-ai/dsh-client-ui-settings-plugins/client';
17
- import { AutoContinueRunner, resolveConfig, type AutoContinueSettings } from './engine.ts';
19
+ import { type AutoContinueSettings } from './engine.ts';
18
20
  import { en, zh, type SettingsCardKey } from './locales.ts';
19
21
  import {
20
22
  AutoContinueSettingsCard,
21
23
  AutoContinueSettingsCardController,
22
24
  } from './settings-card.tsx';
23
-
24
- /** 客户端根上下文的 connection 服务(由 dsh-client-connection 挂载)。 */
25
- declare module '@deepseek-ai/cordis' {
26
- interface Context {
27
- connection: ConnectionHandle;
28
- }
29
- }
25
+ import { startBridge } from './bridge.ts';
30
26
 
31
27
  /** Dictionary namespace owned by this plugin. */
32
28
  const NS = 'auto-continue';
33
29
 
34
- /** Settings namespace the engine reads and the settings card edits. */
30
+ /** Settings namespace the settings card edits (the host engine reads it). */
35
31
  const SETTINGS_NS = 'auto-continue';
36
32
 
37
33
  declare module '@deepseek-ai/dsh-client-ui-slots' {
@@ -42,39 +38,33 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
42
38
  }
43
39
 
44
40
  /** Services required by this plugin. */
45
- export const inject = ['slots', 'locale', 'connection', 'settingsScope'];
41
+ export const inject = ['slots', 'locale', 'settingsScope'];
46
42
 
47
- // 浏览器侧辅助(设置卡片与模拟测试共用): 暂停控制与统计读取。
43
+ // 浏览器侧辅助(设置卡片用): 桥状态读取与暂停解除。
48
44
  export {
49
- fillTemplate,
50
- pauseSession,
51
45
  pausedSessions,
52
46
  readTodayStats,
53
47
  resetTodayStats,
54
- sessionPauseUntil,
55
48
  unpauseSession,
56
- } from './engine.ts';
57
-
58
- /** 当前 runner(HMR 重载时先销毁旧的再建新的)。 */
59
- let current: AutoContinueRunner | null = null;
49
+ } from './bridge.ts';
60
50
 
61
51
  /**
62
- * Plugin body: mount the engine and the settings card.
52
+ * Plugin body: settings card + host status bridge (notifications, stats,
53
+ * paused sessions).
63
54
  * @param ctx - client root context.
64
55
  */
65
56
  export function apply(ctx: ClientContext): void {
66
57
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'auto-continue: dictionaries');
67
58
 
68
- // Engine: reads the settings scope live, so GUI changes apply immediately.
69
- const scope = ctx.settingsScope.bind<AutoContinueSettings>({ namespace: SETTINGS_NS });
70
- current?.dispose();
71
- current = new AutoContinueRunner(ctx.connection.api, () => resolveConfig(scope.getSnapshot().value));
59
+ // 状态桥: 订阅 host 的通知与运行时状态, 弹浏览器通知并驱动卡片面板。
60
+ ctx.effect(() => startBridge(), 'auto-continue: host bridge');
72
61
 
73
62
  // Plugin configuration card: one staged form over the `auto-continue`
74
63
  // settings namespace, contributed to the plugin-configuration section
75
64
  // (Settings → Plugins). Since DSH 0.1.0-rc.7 `settings.plugin.item` is a
76
65
  // keyed slot dispatched by the settings namespace it edits, so the entry
77
66
  // registers with `key` (the namespace), like the official cards.
67
+ const scope = ctx.settingsScope.bind<AutoContinueSettings>({ namespace: SETTINGS_NS });
78
68
  const controller = new AutoContinueSettingsCardController(scope);
79
69
  ctx.slots.inject('settings.plugin.item', () =>
80
70
  ctx.slots.register(
@@ -10,14 +10,14 @@
10
10
  import { useEffect, useState, type ReactNode } from 'react';
11
11
  import { createSnapshotStore, type SettingsScope, type SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client';
12
12
  import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
13
+ import { DEFAULT_CONFIG, type AutoContinueSettings } from './engine.ts';
13
14
  import {
14
- DEFAULT_CONFIG,
15
15
  pausedSessions,
16
16
  readTodayStats,
17
17
  resetTodayStats,
18
+ subscribeBridge,
18
19
  unpauseSession,
19
- type AutoContinueSettings,
20
- } from './engine.ts';
20
+ } from './bridge.ts';
21
21
  import type { SettingsCardKey } from './locales.ts';
22
22
  import {
23
23
  booleanField,
@@ -304,8 +304,13 @@ function LivePanels(props: { t: (key: SettingsCardKey) => string }) {
304
304
  const { t } = props;
305
305
  const [, refresh] = useState(0);
306
306
  useEffect(() => {
307
+ // host 状态桥推送时刷新; 5 秒轮询兜底(桥短暂断线时)
308
+ const unsubscribe = subscribeBridge(() => refresh((value) => value + 1));
307
309
  const timer = setInterval(() => refresh((value) => value + 1), 5000);
308
- return () => clearInterval(timer);
310
+ return () => {
311
+ unsubscribe();
312
+ clearInterval(timer);
313
+ };
309
314
  }, []);
310
315
  const stats = readTodayStats();
311
316
  const hasStats = stats.sent + stats.skipped + stats.recovered + stats.failed + stats.gaveUp + stats.looped > 0;