@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.
- package/dist/DingtalkAdapter.d.ts +22 -0
- package/dist/DingtalkAdapter.d.ts.map +1 -1
- package/dist/DingtalkAdapter.js +293 -42
- package/dist/DingtalkAdapter.js.map +1 -1
- package/dist/interaction-presenter.d.ts +9 -0
- package/dist/interaction-presenter.d.ts.map +1 -1
- package/dist/interaction-presenter.js +25 -2
- package/dist/interaction-presenter.js.map +1 -1
- package/dist/presentation-phase.d.ts +7 -0
- package/dist/presentation-phase.d.ts.map +1 -0
- package/dist/presentation-phase.js +85 -0
- package/dist/presentation-phase.js.map +1 -0
- package/dist/status-card-controller.d.ts +4 -0
- package/dist/status-card-controller.d.ts.map +1 -1
- package/dist/status-card-controller.js +68 -7
- package/dist/status-card-controller.js.map +1 -1
- package/package.json +2 -2
|
@@ -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":"AAYA,OAAO,EACL,WAAW,EAMZ,MAAM,yBAAyB,CAAC;AA6BjC,OAAO,EAEL,6BAA6B,EAC9B,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAIL,KAAK,oBAAoB,EACzB,KAAK,0BAA0B,EAEhC,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"DingtalkAdapter.d.ts","sourceRoot":"","sources":["../src/DingtalkAdapter.ts"],"names":[],"mappings":"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"}
|
package/dist/DingtalkAdapter.js
CHANGED
|
@@ -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:
|
|
1501
|
+
emotionName: tag.name,
|
|
1465
1502
|
textEmotion: {
|
|
1466
|
-
emotionId:
|
|
1467
|
-
emotionName:
|
|
1468
|
-
text:
|
|
1469
|
-
backgroundId:
|
|
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
|
-
|
|
1529
|
+
async attachReaction(msgId, conversationId, tag = EYE_TAG) {
|
|
1530
|
+
return this.emotionApi('reply', msgId, conversationId, tag);
|
|
1492
1531
|
}
|
|
1493
|
-
async recallReaction(msgId, conversationId) {
|
|
1494
|
-
|
|
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.
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
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
|
-
|
|
1579
|
-
|
|
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
|
|
1614
|
-
|
|
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.
|
|
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') {
|