@qwen-code/channel-dingtalk 0.23.1-preview.2 → 0.23.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.
@@ -5,6 +5,7 @@ import type { BackgroundResponseContext, ChannelConfig, ChannelBaseOptions, Enve
5
5
  export declare class DingtalkChannel extends ChannelBase {
6
6
  private client;
7
7
  private readonly atSender;
8
+ private readonly displayLanguage;
8
9
  private connectionManager?;
9
10
  private seenMessages;
10
11
  private mentionTargets;
@@ -15,8 +16,11 @@ export declare class DingtalkChannel extends ChannelBase {
15
16
  /** Map conversationId → latest sessionWebhook URL for sending replies. */
16
17
  private webhooks;
17
18
  private activeReactionKeys;
19
+ private reactionStates;
18
20
  /** sessionId → reaction keys, so a dead session's reactions can be recalled. */
19
21
  private sessionReactionKeys;
22
+ /** Settles when the reaction cleanup queued by disconnect() has run. */
23
+ private disconnectDrain;
20
24
  /**
21
25
  * Real inbound message ids (insertion-ordered, size-capped). Unlike the
22
26
  * TTL-swept seenMessages dedup map, entries survive long queue waits, so a
@@ -94,15 +98,33 @@ export declare class DingtalkChannel extends ChannelBase {
94
98
  private attachReaction;
95
99
  private recallReaction;
96
100
  disconnect(): void;
101
+ waitForDisconnect(): Promise<void>;
97
102
  /** Stable API targets are conversation or user IDs, never webhook URLs. */
98
103
  private isStableTargetId;
99
104
  private reactionKey;
105
+ private forgetReactionState;
106
+ private enqueueReaction;
107
+ private scheduleReactionDrain;
108
+ private drainReactionState;
109
+ private finishReactionState;
100
110
  private rememberInboundMessageId;
101
111
  private logReactionFailure;
102
112
  private startReaction;
113
+ private replaceStatusReaction;
114
+ private phaseReactionTag;
115
+ private terminalReactionTag;
116
+ private finishReaction;
103
117
  private stopReaction;
104
118
  /** Recall reactions left behind when a session dies without terminal lifecycle events. */
105
119
  onSessionDied(sessionId: string): void;
120
+ /**
121
+ * A crashed standalone bridge never settles its in-flight turns, so their
122
+ * transient tags and ticking status cards would otherwise assert a live
123
+ * turn forever. Finish the tags without a terminal result and terminalize
124
+ * the cards as interrupted. Session state stays: crash recovery restores
125
+ * the sessions on a fresh bridge.
126
+ */
127
+ onBridgeDisconnected(): void;
106
128
  protected onSessionRetiring(sessionId: string): void;
107
129
  protected onTaskLifecycle(event: ChannelTaskLifecycleEvent): void;
108
130
  protected onPromptBufferDropped(_chatId: string, sessionId: string, messageIds: string[]): void;
@@ -1 +1 @@
1
- {"version":3,"file":"DingtalkAdapter.d.ts","sourceRoot":"","sources":["../src/DingtalkAdapter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"DingtalkAdapter.d.ts","sourceRoot":"","sources":["../src/DingtalkAdapter.ts"],"names":[],"mappings":"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"}
@@ -13,6 +13,7 @@ import { DingtalkConnectionManager, } from './DingtalkConnectionManager.js';
13
13
  import { DingtalkCardRequestError, DingtalkInteractiveCardClient, } from './interactive-card-client.js';
14
14
  import { parseDingtalkCardActorId, parseDingtalkCardCallback, parseDingtalkInteractiveCardConfig, } from './interactive-card-types.js';
15
15
  import { StatusCardController } from './status-card-controller.js';
16
+ import { isChinesePresentationLanguage, lifecyclePresentationPhase, presentationPhaseLabel, } from './presentation-phase.js';
16
17
  import { QuestionCardController } from './question-card-controller.js';
17
18
  import { DingtalkInteractionPresenter } from './interaction-presenter.js';
18
19
  function nonEmptyString(value) {
@@ -420,9 +421,15 @@ const BACKGROUND_RESPONSE_AGGREGATION_MAX_RETRIES = 3;
420
421
  const ACK_REACTION_NAME = '👀';
421
422
  const ACK_EMOTION_ID = '2659900';
422
423
  const ACK_EMOTION_BG_ID = 'im_bg_1';
424
+ const STATUS_EMOTION_ID = '34019';
425
+ const STATUS_EMOTION_BG_ID = 'im_bg_6';
426
+ const DONE_EMOTION_ID = '54054';
427
+ const DONE_EMOTION_BG_ID = 'im_bg_5';
423
428
  const EMOTION_API = 'https://api.dingtalk.com/v1.0/robot/emotion';
424
429
  const EMOTION_MAX_ATTEMPTS = 3;
425
430
  const EMOTION_RETRY_BASE_DELAY_MS = 250;
431
+ const EMOTION_FETCH_TIMEOUT_MS = 15_000;
432
+ const EMOTION_FINISH_MAX_ATTEMPTS = 3;
426
433
  const GROUP_MSG_API = 'https://api.dingtalk.com/v1.0/robot/groupMessages/send';
427
434
  const DIRECT_MSG_API = 'https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend';
428
435
  const PROACTIVE_MSG_KEY = 'sampleMarkdown'; // DingTalk's built-in {title, text} markdown template key
@@ -562,6 +569,25 @@ const FILE_INSTRUCTIONS = [
562
569
  'File paths containing ] are not supported.',
563
570
  'Do not claim delivery succeeded; DingTalk shows successful files separately and reports failures in the final text.',
564
571
  ].join('\n');
572
+ function statusEmotionTag(name) {
573
+ return {
574
+ name,
575
+ emotionId: STATUS_EMOTION_ID,
576
+ backgroundId: STATUS_EMOTION_BG_ID,
577
+ };
578
+ }
579
+ const EYE_TAG = {
580
+ name: ACK_REACTION_NAME,
581
+ emotionId: ACK_EMOTION_ID,
582
+ backgroundId: ACK_EMOTION_BG_ID,
583
+ };
584
+ const DONE_TAG = {
585
+ name: '✅ Done',
586
+ emotionId: DONE_EMOTION_ID,
587
+ backgroundId: DONE_EMOTION_BG_ID,
588
+ };
589
+ const FAILED_TAG = statusEmotionTag('❌ Failed');
590
+ const STOPPED_TAG = statusEmotionTag('⏹️ Stopped');
565
591
  function collectNonBotMentionIds(data) {
566
592
  if (!Array.isArray(data.atUsers) || typeof data.chatbotUserId !== 'string') {
567
593
  return [];
@@ -625,6 +651,7 @@ class ReplyTextDeliveryError extends Error {
625
651
  export class DingtalkChannel extends ChannelBase {
626
652
  client;
627
653
  atSender;
654
+ displayLanguage;
628
655
  connectionManager;
629
656
  seenMessages = new Map();
630
657
  mentionTargets = new Map();
@@ -635,8 +662,11 @@ export class DingtalkChannel extends ChannelBase {
635
662
  /** Map conversationId → latest sessionWebhook URL for sending replies. */
636
663
  webhooks = new Map();
637
664
  activeReactionKeys = new Set();
665
+ reactionStates = new Map();
638
666
  /** sessionId → reaction keys, so a dead session's reactions can be recalled. */
639
667
  sessionReactionKeys = new Map();
668
+ /** Settles when the reaction cleanup queued by disconnect() has run. */
669
+ disconnectDrain;
640
670
  /**
641
671
  * Real inbound message ids (insertion-ordered, size-capped). Unlike the
642
672
  * TTL-swept seenMessages dedup map, entries survive long queue waits, so a
@@ -677,6 +707,7 @@ export class DingtalkChannel extends ChannelBase {
677
707
  super(name, config, bridge, options);
678
708
  this.atSender =
679
709
  config['atSender'] === true;
710
+ this.displayLanguage = options?.displayLanguage;
680
711
  if (!this.config.instructions) {
681
712
  this.config.instructions = [
682
713
  '## DingTalk Channel',
@@ -727,6 +758,9 @@ export class DingtalkChannel extends ChannelBase {
727
758
  client: this.interactiveCardClient,
728
759
  cancelRun: (sessionId, runId) => this.requestPromptRunCancellation(sessionId, runId),
729
760
  ...(config.model ? { model: config.model } : {}),
761
+ ...(options?.displayLanguage
762
+ ? { language: options.displayLanguage }
763
+ : {}),
730
764
  onError: (operation, error) => {
731
765
  process.stderr.write(`[DingTalk:${this.name}] ${operation} failed: ${sanitizeLogText(String(error), 300)}\n`);
732
766
  },
@@ -747,6 +781,9 @@ export class DingtalkChannel extends ChannelBase {
747
781
  this.interactionPresenter = new DingtalkInteractionPresenter({
748
782
  statusCards: this.statusCardController,
749
783
  questionCards: this.questionCardController,
784
+ ...(options?.displayLanguage
785
+ ? { language: options.displayLanguage }
786
+ : {}),
750
787
  ...(config.blockStreaming !== 'on'
751
788
  ? {
752
789
  sendFallback: (chatId, text, sessionId, sourceLabel) => this.sendFallbackReply(chatId, text, sessionId, sourceLabel),
@@ -1439,16 +1476,16 @@ export class DingtalkChannel extends ChannelBase {
1439
1476
  getAccessToken() {
1440
1477
  return this.client.getConfig().access_token;
1441
1478
  }
1442
- async emotionApi(endpoint, msgId, conversationId) {
1479
+ async emotionApi(endpoint, msgId, conversationId, tag) {
1443
1480
  const robotCode = this.config.clientId;
1444
1481
  if (!robotCode || !msgId || !conversationId)
1445
- return;
1482
+ return false;
1446
1483
  try {
1447
1484
  const token = this.config.clientSecret
1448
1485
  ? await this.getProactiveToken()
1449
1486
  : this.getAccessToken();
1450
1487
  if (!token)
1451
- return;
1488
+ return false;
1452
1489
  for (let attempt = 0; attempt < EMOTION_MAX_ATTEMPTS; attempt++) {
1453
1490
  const resp = await fetch(`${EMOTION_API}/${endpoint}`, {
1454
1491
  method: 'POST',
@@ -1461,17 +1498,18 @@ export class DingtalkChannel extends ChannelBase {
1461
1498
  openMsgId: msgId,
1462
1499
  openConversationId: conversationId,
1463
1500
  emotionType: 2,
1464
- emotionName: ACK_REACTION_NAME,
1501
+ emotionName: tag.name,
1465
1502
  textEmotion: {
1466
- emotionId: ACK_EMOTION_ID,
1467
- emotionName: ACK_REACTION_NAME,
1468
- text: ACK_REACTION_NAME,
1469
- backgroundId: ACK_EMOTION_BG_ID,
1503
+ emotionId: tag.emotionId,
1504
+ emotionName: tag.name,
1505
+ text: tag.name,
1506
+ backgroundId: tag.backgroundId,
1470
1507
  },
1471
1508
  }),
1509
+ signal: AbortSignal.timeout(EMOTION_FETCH_TIMEOUT_MS),
1472
1510
  });
1473
1511
  if (resp.ok)
1474
- return;
1512
+ return true;
1475
1513
  const isTransient = resp.status === 429 || resp.status >= 500;
1476
1514
  if (isTransient && attempt < EMOTION_MAX_ATTEMPTS - 1) {
1477
1515
  await resp.body?.cancel();
@@ -1480,27 +1518,33 @@ export class DingtalkChannel extends ChannelBase {
1480
1518
  }
1481
1519
  const detail = sanitizeLogText(await resp.text().catch(() => ''), 500);
1482
1520
  process.stderr.write(`[DingTalk:${this.name}] emotion/${endpoint} failed after ${attempt + 1}/${EMOTION_MAX_ATTEMPTS} attempts: ${resp.status} ${detail}\n`);
1483
- return;
1521
+ return false;
1484
1522
  }
1485
1523
  }
1486
1524
  catch {
1487
1525
  // best-effort, don't break message flow
1488
1526
  }
1527
+ return false;
1489
1528
  }
1490
- async attachReaction(msgId, conversationId) {
1491
- await this.emotionApi('reply', msgId, conversationId);
1529
+ async attachReaction(msgId, conversationId, tag = EYE_TAG) {
1530
+ return this.emotionApi('reply', msgId, conversationId, tag);
1492
1531
  }
1493
- async recallReaction(msgId, conversationId) {
1494
- await this.emotionApi('recall', msgId, conversationId);
1532
+ async recallReaction(msgId, conversationId, tag = EYE_TAG) {
1533
+ return this.emotionApi('recall', msgId, conversationId, tag);
1495
1534
  }
1496
1535
  disconnect() {
1497
1536
  if (this.dedupTimer) {
1498
1537
  clearInterval(this.dedupTimer);
1499
1538
  }
1539
+ const reactionStates = [...this.reactionStates.values()];
1540
+ for (const state of reactionStates) {
1541
+ this.finishReaction(state.chatId, state.messageId, state.sessionId);
1542
+ }
1500
1543
  this.statusCardController?.dispose();
1501
1544
  this.drainBackgroundResponseAggregations();
1502
1545
  this.activeReactionKeys.clear();
1503
1546
  this.sessionReactionKeys.clear();
1547
+ this.reactionStates.clear();
1504
1548
  if (this.connectionManager) {
1505
1549
  this.connectionManager.stop();
1506
1550
  }
@@ -1508,6 +1552,10 @@ export class DingtalkChannel extends ChannelBase {
1508
1552
  this.client.disconnect();
1509
1553
  }
1510
1554
  process.stderr.write(`[DingTalk:${this.name}] Disconnected.\n`);
1555
+ this.disconnectDrain = Promise.allSettled(reactionStates.map((state) => state.tail)).then(() => undefined);
1556
+ }
1557
+ waitForDisconnect() {
1558
+ return this.disconnectDrain ?? Promise.resolve();
1511
1559
  }
1512
1560
  /** Stable API targets are conversation or user IDs, never webhook URLs. */
1513
1561
  isStableTargetId(chatId) {
@@ -1516,6 +1564,133 @@ export class DingtalkChannel extends ChannelBase {
1516
1564
  reactionKey(messageId, conversationId) {
1517
1565
  return `${conversationId}:${messageId}`;
1518
1566
  }
1567
+ forgetReactionState(state) {
1568
+ this.activeReactionKeys.delete(state.key);
1569
+ if (this.reactionStates.get(state.key) === state) {
1570
+ this.reactionStates.delete(state.key);
1571
+ }
1572
+ if (!state.sessionId)
1573
+ return;
1574
+ const keys = this.sessionReactionKeys.get(state.sessionId);
1575
+ keys?.delete(state.key);
1576
+ if (keys?.size === 0)
1577
+ this.sessionReactionKeys.delete(state.sessionId);
1578
+ }
1579
+ enqueueReaction(state, operation) {
1580
+ state.tail = state.tail.then(operation).catch((err) => {
1581
+ this.logReactionFailure('lifecycle tag update', err);
1582
+ });
1583
+ }
1584
+ scheduleReactionDrain(state) {
1585
+ if (state.drainScheduled)
1586
+ return;
1587
+ state.drainScheduled = true;
1588
+ this.enqueueReaction(state, async () => {
1589
+ try {
1590
+ await this.drainReactionState(state);
1591
+ }
1592
+ finally {
1593
+ state.drainScheduled = false;
1594
+ if (this.reactionStates.get(state.key) === state &&
1595
+ (state.finishing
1596
+ ? !state.finishBlocked
1597
+ : this.activeReactionKeys.has(state.key) &&
1598
+ state.desiredStatusTag &&
1599
+ state.desiredStatusTag.name !== state.statusTag?.name)) {
1600
+ this.scheduleReactionDrain(state);
1601
+ }
1602
+ }
1603
+ });
1604
+ }
1605
+ async drainReactionState(state) {
1606
+ while (true) {
1607
+ if (state.finishing) {
1608
+ await this.finishReactionState(state);
1609
+ return;
1610
+ }
1611
+ if (this.reactionStates.get(state.key) !== state ||
1612
+ !this.activeReactionKeys.has(state.key)) {
1613
+ return;
1614
+ }
1615
+ const desired = state.desiredStatusTag;
1616
+ if (!desired || desired.name === state.statusTag?.name)
1617
+ return;
1618
+ const latched = state.failedTransition;
1619
+ if (state.statusTag) {
1620
+ const from = state.statusTag.name;
1621
+ if (latched && latched.from === from && latched.to === desired.name) {
1622
+ // This exact replacement already failed; re-issuing it once per
1623
+ // lifecycle event hammers a failing emotion API on every streamed
1624
+ // chunk. A different desired tag retries the recall.
1625
+ state.desiredStatusTag = state.statusTag;
1626
+ return;
1627
+ }
1628
+ const revision = state.revision;
1629
+ if ((await this.recallReaction(state.messageId, state.chatId, state.statusTag)) === false) {
1630
+ state.failedTransition = { from, to: desired.name };
1631
+ if (state.revision !== revision)
1632
+ continue;
1633
+ state.desiredStatusTag = state.statusTag;
1634
+ return;
1635
+ }
1636
+ state.failedTransition = undefined;
1637
+ state.statusTag = undefined;
1638
+ continue;
1639
+ }
1640
+ if (latched &&
1641
+ latched.from === undefined &&
1642
+ latched.to === desired.name) {
1643
+ state.desiredStatusTag = undefined;
1644
+ return;
1645
+ }
1646
+ const revision = state.revision;
1647
+ if ((await this.attachReaction(state.messageId, state.chatId, desired)) ===
1648
+ false) {
1649
+ state.failedTransition = { from: undefined, to: desired.name };
1650
+ if (state.revision !== revision)
1651
+ continue;
1652
+ state.desiredStatusTag = undefined;
1653
+ return;
1654
+ }
1655
+ state.failedTransition = undefined;
1656
+ state.statusTag = desired;
1657
+ }
1658
+ }
1659
+ async finishReactionState(state) {
1660
+ let statusCleared = true;
1661
+ if (state.statusTag) {
1662
+ statusCleared =
1663
+ (await this.recallReaction(state.messageId, state.chatId, state.statusTag)) !== false;
1664
+ if (statusCleared)
1665
+ state.statusTag = undefined;
1666
+ }
1667
+ const eyeCleared = !state.eyeAttached ||
1668
+ (await this.recallReaction(state.messageId, state.chatId, EYE_TAG)) !==
1669
+ false;
1670
+ if (eyeCleared)
1671
+ state.eyeAttached = false;
1672
+ let terminalAttached = true;
1673
+ if (state.terminalTag && statusCleared && eyeCleared) {
1674
+ terminalAttached =
1675
+ (await this.attachReaction(state.messageId, state.chatId, state.terminalTag)) !== false;
1676
+ }
1677
+ if (!statusCleared || !eyeCleared || !terminalAttached) {
1678
+ // Keep the state registered: forgetting it here would leave a stale
1679
+ // phase tag nothing can ever recall. finishReaction re-arms the drain
1680
+ // (disconnect, session death) so the cleanup gets a second chance.
1681
+ state.finishAttempts = (state.finishAttempts ?? 0) + 1;
1682
+ if (state.finishAttempts >= EMOTION_FINISH_MAX_ATTEMPTS) {
1683
+ // A permanently failing emotion API (robot removed, expired token)
1684
+ // would otherwise hold the entry for the process lifetime.
1685
+ this.logReactionFailure('reaction cleanup', `abandoned after ${state.finishAttempts} attempts`);
1686
+ this.forgetReactionState(state);
1687
+ return;
1688
+ }
1689
+ state.finishBlocked = true;
1690
+ return;
1691
+ }
1692
+ this.forgetReactionState(state);
1693
+ }
1519
1694
  rememberInboundMessageId(msgId) {
1520
1695
  this.inboundMessageIds.delete(msgId);
1521
1696
  this.inboundMessageIds.add(msgId);
@@ -1540,6 +1715,19 @@ export class DingtalkChannel extends ChannelBase {
1540
1715
  if (this.activeReactionKeys.has(key))
1541
1716
  return;
1542
1717
  this.activeReactionKeys.add(key);
1718
+ const state = {
1719
+ key,
1720
+ messageId,
1721
+ chatId,
1722
+ ...(sessionId ? { sessionId } : {}),
1723
+ desiredStatusTag: this.phaseReactionTag('thinking'),
1724
+ finishing: false,
1725
+ drainScheduled: false,
1726
+ eyeAttached: false,
1727
+ revision: 0,
1728
+ tail: Promise.resolve(),
1729
+ };
1730
+ this.reactionStates.set(key, state);
1543
1731
  if (sessionId) {
1544
1732
  let keys = this.sessionReactionKeys.get(sessionId);
1545
1733
  if (!keys) {
@@ -1548,36 +1736,75 @@ export class DingtalkChannel extends ChannelBase {
1548
1736
  }
1549
1737
  keys.set(key, { messageId, chatId });
1550
1738
  }
1551
- this.attachReaction(messageId, chatId)
1552
- .then(() => {
1553
- if (!this.activeReactionKeys.has(key)) {
1554
- void this.recallReaction(messageId, chatId).catch((err) => {
1555
- this.logReactionFailure('late reaction recall', err);
1556
- });
1739
+ this.enqueueReaction(state, async () => {
1740
+ try {
1741
+ if ((await this.attachReaction(messageId, chatId, EYE_TAG)) === false) {
1742
+ this.forgetReactionState(state);
1743
+ return;
1744
+ }
1745
+ state.eyeAttached = true;
1746
+ this.scheduleReactionDrain(state);
1747
+ }
1748
+ catch (err) {
1749
+ this.forgetReactionState(state);
1750
+ this.logReactionFailure('reaction attach', err);
1557
1751
  }
1558
- })
1559
- .catch((err) => {
1560
- this.activeReactionKeys.delete(key);
1561
- this.logReactionFailure('reaction attach', err);
1562
1752
  });
1563
1753
  }
1564
- stopReaction(chatId, messageId, sessionId) {
1754
+ replaceStatusReaction(chatId, messageId, tag) {
1755
+ if (!messageId)
1756
+ return;
1757
+ const state = this.reactionStates.get(this.reactionKey(messageId, chatId));
1758
+ if (!state || !this.activeReactionKeys.has(state.key))
1759
+ return;
1760
+ state.desiredStatusTag = tag;
1761
+ state.revision++;
1762
+ this.scheduleReactionDrain(state);
1763
+ }
1764
+ phaseReactionTag(phase) {
1765
+ return statusEmotionTag(presentationPhaseLabel(phase, this.displayLanguage));
1766
+ }
1767
+ terminalReactionTag(type) {
1768
+ if (!isChinesePresentationLanguage(this.displayLanguage)) {
1769
+ return type === 'completed'
1770
+ ? DONE_TAG
1771
+ : type === 'failed'
1772
+ ? FAILED_TAG
1773
+ : STOPPED_TAG;
1774
+ }
1775
+ if (type === 'completed') {
1776
+ return { ...DONE_TAG, name: '✅ 已完成' };
1777
+ }
1778
+ return statusEmotionTag(type === 'failed' ? '❌ 失败' : '⏹️ 已停止');
1779
+ }
1780
+ finishReaction(chatId, messageId, sessionId, terminalTag) {
1565
1781
  if (!messageId || !this.isStableTargetId(chatId))
1566
1782
  return;
1567
1783
  const key = this.reactionKey(messageId, chatId);
1568
- if (sessionId) {
1569
- const keys = this.sessionReactionKeys.get(sessionId);
1570
- if (keys) {
1571
- keys.delete(key);
1572
- if (keys.size === 0)
1573
- this.sessionReactionKeys.delete(sessionId);
1574
- }
1784
+ const state = this.reactionStates.get(key);
1785
+ if (!state)
1786
+ return;
1787
+ if (state.finishing) {
1788
+ // A finish is either draining or blocked on a failed recall; only the
1789
+ // blocked case needs this trigger to retry the cleanup.
1790
+ if (!state.finishBlocked)
1791
+ return;
1792
+ state.finishBlocked = false;
1793
+ if (terminalTag)
1794
+ state.terminalTag = terminalTag;
1795
+ this.scheduleReactionDrain(state);
1796
+ return;
1575
1797
  }
1576
1798
  if (!this.activeReactionKeys.delete(key))
1577
1799
  return;
1578
- this.recallReaction(messageId, chatId).catch((err) => {
1579
- this.logReactionFailure('reaction recall', err);
1580
- });
1800
+ state.finishing = true;
1801
+ state.desiredStatusTag = undefined;
1802
+ state.terminalTag = terminalTag;
1803
+ state.revision++;
1804
+ this.scheduleReactionDrain(state);
1805
+ }
1806
+ stopReaction(chatId, messageId, sessionId) {
1807
+ this.finishReaction(chatId, messageId, sessionId);
1581
1808
  }
1582
1809
  /** Recall reactions left behind when a session dies without terminal lifecycle events. */
1583
1810
  onSessionDied(sessionId) {
@@ -1610,16 +1837,32 @@ export class DingtalkChannel extends ChannelBase {
1610
1837
  const keys = this.sessionReactionKeys.get(sessionId);
1611
1838
  if (keys) {
1612
1839
  this.sessionReactionKeys.delete(sessionId);
1613
- for (const [key, { messageId, chatId }] of keys) {
1614
- if (this.activeReactionKeys.delete(key)) {
1615
- void this.recallReaction(messageId, chatId).catch((err) => {
1616
- this.logReactionFailure('session-death reaction recall', err);
1617
- });
1618
- }
1840
+ for (const { messageId, chatId } of keys.values()) {
1841
+ this.finishReaction(chatId, messageId, sessionId);
1619
1842
  }
1620
1843
  }
1621
1844
  super.onSessionDied(sessionId);
1622
1845
  }
1846
+ /**
1847
+ * A crashed standalone bridge never settles its in-flight turns, so their
1848
+ * transient tags and ticking status cards would otherwise assert a live
1849
+ * turn forever. Finish the tags without a terminal result and terminalize
1850
+ * the cards as interrupted. Session state stays: crash recovery restores
1851
+ * the sessions on a fresh bridge.
1852
+ */
1853
+ onBridgeDisconnected() {
1854
+ for (const [sessionId, keys] of this.sessionReactionKeys) {
1855
+ this.sessionReactionKeys.delete(sessionId);
1856
+ for (const { messageId, chatId } of keys.values()) {
1857
+ this.finishReaction(chatId, messageId, sessionId);
1858
+ }
1859
+ }
1860
+ for (const [sessionId, runId] of this.cardRunBySession) {
1861
+ this.cardRunBySession.delete(sessionId);
1862
+ this.interactionPresenter?.terminalizeRun(runId, 'cancelled');
1863
+ this.cardRuns.delete(runId);
1864
+ }
1865
+ }
1623
1866
  onSessionRetiring(sessionId) {
1624
1867
  this.drainBackgroundResponseAggregations(sessionId);
1625
1868
  }
@@ -1642,10 +1885,18 @@ export class DingtalkChannel extends ChannelBase {
1642
1885
  }
1643
1886
  return;
1644
1887
  }
1888
+ const presentationPhase = lifecyclePresentationPhase(event);
1889
+ if (event.runId && presentationPhase) {
1890
+ this.interactionPresenter?.updateStatusCardPhase(event.runId, presentationPhase);
1891
+ }
1892
+ if (presentationPhase) {
1893
+ this.replaceStatusReaction(event.chatId, event.messageId, this.phaseReactionTag(presentationPhase));
1894
+ return;
1895
+ }
1645
1896
  if (isTerminalTaskLifecycleType(event.type)) {
1646
1897
  if (event.messageId)
1647
1898
  this.mentionTargets.delete(event.messageId);
1648
- this.stopReaction(event.chatId, event.messageId, event.sessionId);
1899
+ this.finishReaction(event.chatId, event.messageId, event.sessionId, this.terminalReactionTag(event.type));
1649
1900
  if (event.runId) {
1650
1901
  this.deleteFileProjectorsForRun(event.runId);
1651
1902
  if (event.type === 'failed') {