@xmanrui/dsh-im 4.3.0 → 4.4.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 +951 -626
- package/lib/index.js +233 -233
- package/package.json +1 -1
- package/plugin-src/client/access-policy-settings.js +351 -0
- package/plugin-src/client/channel-card-meta.js +2 -1
- package/plugin-src/client/channels/dingtalk/api.js +5 -0
- package/plugin-src/client/channels/dingtalk/index.js +1 -0
- package/plugin-src/client/channels/feishu/api.js +5 -0
- package/plugin-src/client/channels/feishu/index.js +1 -0
- package/plugin-src/client/channels/qq/api.js +5 -0
- package/plugin-src/client/channels/qq/index.js +1 -0
- package/plugin-src/client/channels/shared/token-api.js +5 -0
- package/plugin-src/client/channels/shared/token-channel.js +1 -0
- package/plugin-src/client/channels/telegram/api.js +2 -17
- package/plugin-src/client/channels/telegram/index.js +0 -112
- package/plugin-src/client/channels/telegram/styles.js +0 -28
- package/plugin-src/client/channels/wecom/api.js +5 -0
- package/plugin-src/client/channels/wecom/index.js +1 -0
- package/plugin-src/client/channels/weixin/api.js +5 -0
- package/plugin-src/client/channels/weixin/index.js +1 -0
- package/plugin-src/client/channels/whatsapp/api.js +4 -10
- package/plugin-src/client/channels/whatsapp/index.js +1 -125
- package/plugin-src/client/channels/whatsapp/styles.js +0 -25
- package/plugin-src/client/delivery-settings.js +61 -9
- package/plugin-src/client/i18n.js +56 -1
- package/plugin-src/client/index.js +1 -0
- package/plugin-src/client/styles.js +50 -4
- package/plugin-src/host/channels/dingtalk/production.mjs +12 -1
- package/plugin-src/host/channels/dingtalk/rpc.mjs +11 -0
- package/plugin-src/host/channels/feishu/production.mjs +39 -1
- package/plugin-src/host/channels/feishu/rpc.mjs +19 -2
- package/plugin-src/host/channels/qq/production.mjs +12 -1
- package/plugin-src/host/channels/qq/rpc.mjs +11 -0
- package/plugin-src/host/channels/shared/access-policy-production.mjs +137 -0
- package/plugin-src/host/channels/shared/access-policy-rpc.mjs +17 -0
- package/plugin-src/host/channels/shared/production.mjs +15 -1
- package/plugin-src/host/channels/shared/rpc.mjs +9 -0
- package/plugin-src/host/channels/slack/production.mjs +12 -1
- package/plugin-src/host/channels/slack/rpc.mjs +10 -0
- package/plugin-src/host/channels/telegram/rpc.mjs +2 -49
- package/plugin-src/host/channels/wecom/production.mjs +12 -1
- package/plugin-src/host/channels/wecom/rpc.mjs +11 -0
- package/plugin-src/host/channels/weixin/production.mjs +12 -1
- package/plugin-src/host/channels/weixin/rpc.mjs +11 -0
- package/plugin-src/host/channels/whatsapp/production.mjs +16 -2
- package/plugin-src/host/channels/whatsapp/rpc.mjs +10 -15
- package/src/channels/dingtalk/dingtalk-bridge.mjs +61 -9
- package/src/channels/dingtalk/dingtalk-runtime.mjs +4 -0
- package/src/channels/discord/discord-runtime.mjs +29 -1
- package/src/channels/feishu/bridge.mjs +188 -80
- package/src/channels/feishu/feishu-runtime.mjs +4 -0
- package/src/channels/qq/qq-bridge.mjs +59 -17
- package/src/channels/qq/qq-runtime.mjs +20 -2
- package/src/channels/shared/access-policy.mjs +210 -0
- package/src/channels/shared/bot-workspace-store.mjs +176 -33
- package/src/channels/shared/command-permission.mjs +32 -0
- package/src/channels/shared/compact-command.mjs +5 -2
- package/src/channels/shared/i18n-en/shared-c.mjs +2 -0
- package/src/channels/shared/inbound-access.mjs +43 -0
- package/src/channels/shared/text-harness-bridge.mjs +47 -4
- package/src/channels/shared/workspace-command.mjs +10 -1
- package/src/channels/slack/slack-runtime.mjs +4 -0
- package/src/channels/telegram/telegram-runtime.mjs +5 -13
- package/src/channels/wecom/wecom-bridge.mjs +51 -4
- package/src/channels/wecom/wecom-runtime.mjs +4 -0
- package/src/channels/weixin/weixin-bridge.mjs +59 -16
- package/src/channels/weixin/weixin-runtime.mjs +4 -0
- package/src/channels/whatsapp/whatsapp-runtime.mjs +42 -20
|
@@ -59,6 +59,11 @@ import {
|
|
|
59
59
|
setLastMessageFailure,
|
|
60
60
|
} from '../shared/message-failure.mjs';
|
|
61
61
|
import { beginStatusReaction } from '../shared/status-reaction.mjs';
|
|
62
|
+
import {
|
|
63
|
+
COMMAND_PERMISSION_DENIED_MESSAGE,
|
|
64
|
+
evaluateInboundAccess,
|
|
65
|
+
} from '../shared/inbound-access.mjs';
|
|
66
|
+
import { isSharedLocalCommand } from '../shared/command-permission.mjs';
|
|
62
67
|
import {
|
|
63
68
|
MENU_PAGE_SIZE,
|
|
64
69
|
PRESET_FOLLOW_DEFAULT_SENTINEL,
|
|
@@ -135,6 +140,17 @@ const ARCHIVED_COMMAND = /^\/archived(?:\s+(on|off))?$/i;
|
|
|
135
140
|
/** Matches fast card commands that should not be queued behind a running task. */
|
|
136
141
|
const CARD_COMMAND = /^\/(?:m(?:enu)?|new|help|status|compact|(?:sessionlist|sessions)(?:\s|$)|workspacelist|watchlist|archived(?:\s+(on|off))?)$/i;
|
|
137
142
|
|
|
143
|
+
function isFeishuLocalCommand(text, { hasImages = false, hasFiles = false } = {}) {
|
|
144
|
+
if (hasImages || hasFiles || typeof text !== 'string') return false;
|
|
145
|
+
const command = text.trim();
|
|
146
|
+
return MENU_COMMAND.test(command)
|
|
147
|
+
|| REPAIR_COMMAND_PREFIX.test(command)
|
|
148
|
+
|| WATCH_COMMAND.test(command)
|
|
149
|
+
|| UNWATCH_COMMAND.test(command)
|
|
150
|
+
|| WATCHLIST_COMMAND.test(command)
|
|
151
|
+
|| ARCHIVED_COMMAND.test(command);
|
|
152
|
+
}
|
|
153
|
+
|
|
138
154
|
/** Canonical workspace/session help advertised by every bridge family. */
|
|
139
155
|
const WORKSPACE_HELP_LINES = [
|
|
140
156
|
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
|
|
@@ -385,6 +401,7 @@ export class FeishuHarnessBridge {
|
|
|
385
401
|
#harness;
|
|
386
402
|
#state;
|
|
387
403
|
#contextEnhancement;
|
|
404
|
+
#accessPolicy;
|
|
388
405
|
#queues = new Map();
|
|
389
406
|
#batchInputs = new BatchInputManager();
|
|
390
407
|
#pendingInteractions = new Map();
|
|
@@ -451,6 +468,7 @@ export class FeishuHarnessBridge {
|
|
|
451
468
|
harness,
|
|
452
469
|
state,
|
|
453
470
|
contextEnhancement,
|
|
471
|
+
accessPolicy,
|
|
454
472
|
status,
|
|
455
473
|
allowedSenderOpenIds = new Set(),
|
|
456
474
|
botId,
|
|
@@ -488,6 +506,7 @@ export class FeishuHarnessBridge {
|
|
|
488
506
|
this.#harness = harness;
|
|
489
507
|
this.#state = state;
|
|
490
508
|
this.#contextEnhancement = contextEnhancement;
|
|
509
|
+
this.#accessPolicy = accessPolicy;
|
|
491
510
|
this.#status = status;
|
|
492
511
|
this.#allowedSenderOpenIds = allowedSenderOpenIds;
|
|
493
512
|
this.#botId = nonEmptyString(botId);
|
|
@@ -526,10 +545,10 @@ export class FeishuHarnessBridge {
|
|
|
526
545
|
if (this.#signal?.aborted) return Promise.resolve();
|
|
527
546
|
const messageId = nonEmptyString(event?.message?.message_id);
|
|
528
547
|
if (!messageId || isBotSender(event)) return Promise.resolve();
|
|
529
|
-
if (!isAllowedSender(event, this.#allowedSenderOpenIds)) {
|
|
548
|
+
if (!this.#accessPolicy && !isAllowedSender(event, this.#allowedSenderOpenIds)) {
|
|
530
549
|
this.#status.messagesRejected += 1;
|
|
531
550
|
this.#status.lastRejectedAt = new Date().toISOString();
|
|
532
|
-
this.#logger.warn?.('[dsh-feishu] ignored a message from a sender outside the allowlist');
|
|
551
|
+
this.#logger.warn?.('[dsh-feishu] ignored a message from a sender outside the legacy allowlist');
|
|
533
552
|
return Promise.resolve();
|
|
534
553
|
}
|
|
535
554
|
const addressed = this.#isAddressed(event);
|
|
@@ -551,6 +570,28 @@ export class FeishuHarnessBridge {
|
|
|
551
570
|
return Promise.resolve();
|
|
552
571
|
}
|
|
553
572
|
|
|
573
|
+
const commandMessage = extractInboundMessage(event, this.#client);
|
|
574
|
+
const commandText = nonEmptyString(commandMessage.content) ?? '';
|
|
575
|
+
const hasImages = hasInboundImages(commandMessage);
|
|
576
|
+
const hasFiles = hasInboundFiles(commandMessage);
|
|
577
|
+
const conversationType = event.message.chat_type === 'p2p' ? 'direct'
|
|
578
|
+
: event.message.chat_type === 'group' ? 'group' : null;
|
|
579
|
+
const access = evaluateInboundAccess(this.#accessPolicy, {
|
|
580
|
+
conversationType,
|
|
581
|
+
senderIds: senderOpenId(event),
|
|
582
|
+
text: commandText,
|
|
583
|
+
hasImages,
|
|
584
|
+
hasFiles,
|
|
585
|
+
isCommand: isSharedLocalCommand(commandText, {
|
|
586
|
+
hasImages,
|
|
587
|
+
hasFiles,
|
|
588
|
+
}) || isFeishuLocalCommand(commandText, { hasImages, hasFiles })
|
|
589
|
+
|| (!hasImages && !hasFiles && NUMBER_REPLY.test(commandText) && this.#menus.has(key)),
|
|
590
|
+
});
|
|
591
|
+
if (!access.allowed) {
|
|
592
|
+
this.#acceptedMessageIds.set(messageId, null);
|
|
593
|
+
return this.#finishAccessDecision(event, messageId, access);
|
|
594
|
+
}
|
|
554
595
|
if (event.message.chat_type === 'p2p') {
|
|
555
596
|
const chatId = nonEmptyString(event.message.chat_id);
|
|
556
597
|
if (chatId) rememberConnectionTestTarget(this.#state, { chatId });
|
|
@@ -558,11 +599,9 @@ export class FeishuHarnessBridge {
|
|
|
558
599
|
|
|
559
600
|
this.#acceptedMessageIds.set(messageId, captureContextEnhancement(
|
|
560
601
|
this.#contextEnhancement,
|
|
561
|
-
|
|
602
|
+
conversationType,
|
|
562
603
|
));
|
|
563
604
|
const processingReaction = this.#beginReaction(messageId);
|
|
564
|
-
const commandMessage = extractInboundMessage(event, this.#client);
|
|
565
|
-
const commandText = nonEmptyString(commandMessage.content) ?? '';
|
|
566
605
|
const batchText = event.message.message_type === 'text'
|
|
567
606
|
? nonEmptyString(extractText(event)) ?? ''
|
|
568
607
|
: '';
|
|
@@ -774,7 +813,7 @@ export class FeishuHarnessBridge {
|
|
|
774
813
|
await this.#state.markSeen(messageId);
|
|
775
814
|
this.#status.lastMessageAt = new Date().toISOString();
|
|
776
815
|
this.#status.messagesReceived += 1;
|
|
777
|
-
if (result?.message) await this.#send(event.message.chat_id, result.message);
|
|
816
|
+
if (result?.message) await this.#send(event.message.chat_id, result.message, { replyTo: event.message.message_id });
|
|
778
817
|
this.#status.lastError = null;
|
|
779
818
|
})
|
|
780
819
|
.then(() => this.#finishReaction(messageId, processingReaction, 'DONE'))
|
|
@@ -792,6 +831,38 @@ export class FeishuHarnessBridge {
|
|
|
792
831
|
return current;
|
|
793
832
|
}
|
|
794
833
|
|
|
834
|
+
#finishAccessDecision(event, messageId, access) {
|
|
835
|
+
let current;
|
|
836
|
+
current = Promise.resolve().then(async () => {
|
|
837
|
+
if (this.#state.hasSeen(messageId)) return;
|
|
838
|
+
await this.#state.markSeen(messageId);
|
|
839
|
+
if (access.reason === 'command-not-allowed') {
|
|
840
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
841
|
+
this.#status.messagesReceived += 1;
|
|
842
|
+
await this.#send(
|
|
843
|
+
event.message.chat_id,
|
|
844
|
+
t(COMMAND_PERMISSION_DENIED_MESSAGE),
|
|
845
|
+
{ replyTo: event.message.message_id },
|
|
846
|
+
);
|
|
847
|
+
this.#status.messagesReplied += 1;
|
|
848
|
+
this.#status.lastReplyAt = new Date().toISOString();
|
|
849
|
+
} else {
|
|
850
|
+
this.#status.messagesRejected += 1;
|
|
851
|
+
this.#status.lastRejectedAt = new Date().toISOString();
|
|
852
|
+
}
|
|
853
|
+
this.#status.lastError = null;
|
|
854
|
+
}).catch((error) => {
|
|
855
|
+
if (this.#signal?.aborted) return;
|
|
856
|
+
this.#status.lastError = error?.message ?? String(error);
|
|
857
|
+
this.#logger.warn?.('[dsh-feishu] failed to apply inbound access policy');
|
|
858
|
+
}).finally(() => {
|
|
859
|
+
this.#acceptedMessageIds.delete(messageId);
|
|
860
|
+
this.#commandTasks.delete(current);
|
|
861
|
+
});
|
|
862
|
+
this.#commandTasks.add(current);
|
|
863
|
+
return current;
|
|
864
|
+
}
|
|
865
|
+
|
|
795
866
|
#enqueueMessage(event, messageId, key, processingReaction, {
|
|
796
867
|
releaseMessageId = true,
|
|
797
868
|
alreadyRecorded = false,
|
|
@@ -843,7 +914,7 @@ export class FeishuHarnessBridge {
|
|
|
843
914
|
const text = options.appendMessage
|
|
844
915
|
? `${messageFailureText(failure)}\n\n${options.appendMessage}`
|
|
845
916
|
: messageFailureText(failure);
|
|
846
|
-
await this.#send(chatId, text).catch(() => undefined);
|
|
917
|
+
await this.#send(chatId, text, { replyTo: options.replyTo }).catch(() => undefined);
|
|
847
918
|
return failure;
|
|
848
919
|
}
|
|
849
920
|
|
|
@@ -874,6 +945,7 @@ export class FeishuHarnessBridge {
|
|
|
874
945
|
await this.#send(
|
|
875
946
|
event.message.chat_id,
|
|
876
947
|
failureText,
|
|
948
|
+
{ replyTo: event.message.message_id },
|
|
877
949
|
).catch(() => undefined);
|
|
878
950
|
}
|
|
879
951
|
|
|
@@ -1489,10 +1561,11 @@ export class FeishuHarnessBridge {
|
|
|
1489
1561
|
?? nonEmptyString(event?.operator?.operator_id?.user_id)
|
|
1490
1562
|
?? nonEmptyString(event?.open_id)
|
|
1491
1563
|
?? nonEmptyString(event?.user_id);
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
this.#
|
|
1564
|
+
if (!operatorOpenId) return Promise.resolve();
|
|
1565
|
+
if (!this.#accessPolicy
|
|
1566
|
+
&& !this.#allowedSenderOpenIds.has('*')
|
|
1567
|
+
&& !this.#allowedSenderOpenIds.has(operatorOpenId)) {
|
|
1568
|
+
this.#logger.warn?.('[dsh-feishu] ignoring card action from an unallowed legacy sender');
|
|
1496
1569
|
return Promise.resolve();
|
|
1497
1570
|
}
|
|
1498
1571
|
const actionValue = callbackObject(event?.action?.value);
|
|
@@ -1531,16 +1604,36 @@ export class FeishuHarnessBridge {
|
|
|
1531
1604
|
?? nonEmptyString(event?.message_id);
|
|
1532
1605
|
const route = messageId ? this.#cardKeys.get(messageId) : null;
|
|
1533
1606
|
if (!route) {
|
|
1607
|
+
// A route is also the trusted direct/group scope for the unified policy.
|
|
1608
|
+
// Without it, fail closed instead of producing an unauthorised side effect.
|
|
1609
|
+
if (this.#accessPolicy) return Promise.resolve();
|
|
1610
|
+
// Legacy callers without a unified policy still receive the current
|
|
1611
|
+
// expired-card guidance introduced by the upstream thread-reply fix.
|
|
1534
1612
|
// The card predates this process (the in-memory mapping resets on
|
|
1535
1613
|
// restart) or never came from us: nudge instead of staying silent.
|
|
1536
1614
|
const chatId = nonEmptyString(event?.context?.open_chat_id)
|
|
1537
1615
|
?? nonEmptyString(event?.open_chat_id)
|
|
1538
1616
|
?? nonEmptyString(event?.chat_id);
|
|
1539
1617
|
if (chatId) {
|
|
1540
|
-
this.#send(chatId, t('这个菜单已过期,请回复 /m 重新打开。')).catch(() => undefined);
|
|
1618
|
+
this.#send(chatId, t('这个菜单已过期,请回复 /m 重新打开。'), { replyTo: messageId }).catch(() => undefined);
|
|
1541
1619
|
}
|
|
1542
1620
|
return Promise.resolve();
|
|
1543
1621
|
}
|
|
1622
|
+
const conversationType = route.key.startsWith('p2p:') ? 'direct'
|
|
1623
|
+
: route.key.startsWith('group:') ? 'group' : null;
|
|
1624
|
+
const access = evaluateInboundAccess(this.#accessPolicy, {
|
|
1625
|
+
conversationType,
|
|
1626
|
+
senderIds: operatorOpenId,
|
|
1627
|
+
isCommand: true,
|
|
1628
|
+
});
|
|
1629
|
+
if (!access.allowed) {
|
|
1630
|
+
if (access.reason === 'command-not-allowed') {
|
|
1631
|
+
return this.#send(route.chatId, t(COMMAND_PERMISSION_DENIED_MESSAGE))
|
|
1632
|
+
.catch(() => undefined);
|
|
1633
|
+
}
|
|
1634
|
+
this.#logger.warn?.('[dsh-feishu] ignoring card action blocked by access policy');
|
|
1635
|
+
return Promise.resolve();
|
|
1636
|
+
}
|
|
1544
1637
|
// A used card is recent even if it was first created long ago.
|
|
1545
1638
|
this.#cardKeys.delete(messageId);
|
|
1546
1639
|
this.#cardKeys.set(messageId, route);
|
|
@@ -1590,7 +1683,7 @@ export class FeishuHarnessBridge {
|
|
|
1590
1683
|
await this.#sendSteer(entry, formText);
|
|
1591
1684
|
return;
|
|
1592
1685
|
}
|
|
1593
|
-
await this.#send(entry.chatId, t('请输入补充指令后再提交。'));
|
|
1686
|
+
await this.#send(entry.chatId, t('请输入补充指令后再提交。'), { replyTo: entry.messageId ?? null });
|
|
1594
1687
|
return;
|
|
1595
1688
|
}
|
|
1596
1689
|
}
|
|
@@ -1630,7 +1723,7 @@ export class FeishuHarnessBridge {
|
|
|
1630
1723
|
}
|
|
1631
1724
|
this.#logger.warn?.('[dsh-feishu] card action queue is full; dropping callbacks');
|
|
1632
1725
|
let tracked;
|
|
1633
|
-
tracked = this.#send(entry.chatId, t('操作过于频繁,请稍后再试。'))
|
|
1726
|
+
tracked = this.#send(entry.chatId, t('操作过于频繁,请稍后再试。'), { replyTo: entry.messageId ?? null })
|
|
1634
1727
|
.catch(() => undefined)
|
|
1635
1728
|
.finally(() => {
|
|
1636
1729
|
this.#cardActionTasks.delete(tracked);
|
|
@@ -1711,7 +1804,7 @@ export class FeishuHarnessBridge {
|
|
|
1711
1804
|
})
|
|
1712
1805
|
.catch(async (error) => {
|
|
1713
1806
|
if (this.#signal?.aborted) return;
|
|
1714
|
-
await this.#sendFailure(entry.chatId, error, { logLabel: 'card action' });
|
|
1807
|
+
await this.#sendFailure(entry.chatId, error, { logLabel: 'card action', replyTo: entry.messageId });
|
|
1715
1808
|
})
|
|
1716
1809
|
.finally(() => {
|
|
1717
1810
|
if (this.#cardActionInFlight.get(dedupeKey) === tracked) {
|
|
@@ -1752,10 +1845,13 @@ export class FeishuHarnessBridge {
|
|
|
1752
1845
|
sessionPage = 0,
|
|
1753
1846
|
selections = [],
|
|
1754
1847
|
}) {
|
|
1848
|
+
// Confirmations triggered by a card interaction stay anchored to the
|
|
1849
|
+
// card's message so they land inside the same Feishu topic.
|
|
1850
|
+
const reply = (text) => this.#send(chatId, text, { replyTo: messageId });
|
|
1755
1851
|
if (action === 'sessions' || /^sessions:\d+$/.test(action)) {
|
|
1756
1852
|
const page = action === 'sessions' ? 0 : Number(action.slice('sessions:'.length));
|
|
1757
1853
|
await this.#showSessions(
|
|
1758
|
-
{ chatId, key },
|
|
1854
|
+
{ chatId, key, replyTo: messageId },
|
|
1759
1855
|
sessionWorkspace,
|
|
1760
1856
|
page,
|
|
1761
1857
|
{ updateMessageId: messageId },
|
|
@@ -1763,17 +1859,17 @@ export class FeishuHarnessBridge {
|
|
|
1763
1859
|
return;
|
|
1764
1860
|
}
|
|
1765
1861
|
if (action === 'workspaces') {
|
|
1766
|
-
await this.#showWorkspaces({ chatId, key }, { updateMessageId: messageId });
|
|
1862
|
+
await this.#showWorkspaces({ chatId, key, replyTo: messageId }, { updateMessageId: messageId });
|
|
1767
1863
|
return;
|
|
1768
1864
|
}
|
|
1769
1865
|
if (action === 'watchlist') {
|
|
1770
|
-
await this.#showWatchList(key, chatId, { updateMessageId: messageId });
|
|
1866
|
+
await this.#showWatchList(key, chatId, { updateMessageId: messageId, replyTo: messageId });
|
|
1771
1867
|
return;
|
|
1772
1868
|
}
|
|
1773
1869
|
// 多选关注下拉:action=watch_add / watch_remove,选中项在 selections 数组
|
|
1774
1870
|
if (action === 'watch_add' || action === 'watch_remove') {
|
|
1775
1871
|
if (selections.length === 0) {
|
|
1776
|
-
await
|
|
1872
|
+
await reply(t('请先选择至少一个会话。'));
|
|
1777
1873
|
return;
|
|
1778
1874
|
}
|
|
1779
1875
|
let changed = 0;
|
|
@@ -1794,6 +1890,7 @@ export class FeishuHarnessBridge {
|
|
|
1794
1890
|
const result = await this.#runWatch(key, chatId, sessionId, {
|
|
1795
1891
|
notify: false,
|
|
1796
1892
|
validatedTarget,
|
|
1893
|
+
replyTo: messageId,
|
|
1797
1894
|
});
|
|
1798
1895
|
if (result.changed) changed += 1;
|
|
1799
1896
|
else if (!result.ok) failed += 1;
|
|
@@ -1819,54 +1916,53 @@ export class FeishuHarnessBridge {
|
|
|
1819
1916
|
? t('所选会话已在关注列表中。')
|
|
1820
1917
|
: t('所选会话已不在关注列表中。');
|
|
1821
1918
|
try {
|
|
1822
|
-
await this.#showWatchList(key, chatId, { updateMessageId: messageId });
|
|
1919
|
+
await this.#showWatchList(key, chatId, { updateMessageId: messageId, replyTo: messageId });
|
|
1823
1920
|
} catch (error) {
|
|
1824
1921
|
this.#logger.warn?.('[dsh-feishu] watch list refresh failed:', error.message);
|
|
1825
1922
|
}
|
|
1826
|
-
await
|
|
1923
|
+
await reply(summary).catch((error) => {
|
|
1827
1924
|
this.#logger.warn?.('[dsh-feishu] watch batch summary failed:', error.message);
|
|
1828
1925
|
});
|
|
1829
1926
|
return;
|
|
1830
1927
|
}
|
|
1831
1928
|
if (action === 'new') {
|
|
1832
1929
|
if (this.#queues.has(key) || this.#hasPendingInteraction(key)) {
|
|
1833
|
-
await
|
|
1930
|
+
await reply(t('当前任务仍在运行,请先停止任务或等待任务完成后再开启新会话。'));
|
|
1834
1931
|
return;
|
|
1835
1932
|
}
|
|
1836
1933
|
await this.#state.clearSession(key);
|
|
1837
|
-
await
|
|
1838
|
-
await this.#sendMenuCard(key, chatId, { updateMessageId: messageId });
|
|
1934
|
+
await reply(t('已开启全新 Harness 会话。'));
|
|
1935
|
+
await this.#sendMenuCard(key, chatId, { updateMessageId: messageId, replyTo: messageId });
|
|
1839
1936
|
return;
|
|
1840
1937
|
}
|
|
1841
1938
|
if (action === 'use:current') {
|
|
1842
1939
|
const sessionId = this.#state.sessionFor(key);
|
|
1843
1940
|
if (typeof sessionId !== 'string' || !sessionId) {
|
|
1844
|
-
await
|
|
1941
|
+
await reply(t('当前没有绑定的会话,请先从会话列表选择。'));
|
|
1845
1942
|
return;
|
|
1846
1943
|
}
|
|
1847
|
-
await
|
|
1944
|
+
await reply(t('已就绪,直接发消息即可继续当前会话。'));
|
|
1848
1945
|
return;
|
|
1849
1946
|
}
|
|
1850
1947
|
if (action === 'archive_toggle' || action === 'archive:on' || action === 'archive:off') {
|
|
1851
1948
|
const next = action === 'archive:on' ? true : action === 'archive:off' ? false : !(this.#state?.includesArchivedSessions?.() ?? false);
|
|
1852
1949
|
await this.#state?.setIncludeArchivedSessions?.(next);
|
|
1853
|
-
await
|
|
1854
|
-
chatId,
|
|
1950
|
+
await reply(
|
|
1855
1951
|
next ? t('已开启:会话列表包含归档会话。') : t('已关闭:会话列表隐藏归档会话。'),
|
|
1856
1952
|
);
|
|
1857
|
-
await this.#sendMenuCard(key, chatId, { updateMessageId: messageId });
|
|
1953
|
+
await this.#sendMenuCard(key, chatId, { updateMessageId: messageId, replyTo: messageId });
|
|
1858
1954
|
return;
|
|
1859
1955
|
}
|
|
1860
1956
|
if (action === 'repair') {
|
|
1861
|
-
await
|
|
1957
|
+
await reply(t('修复需在私聊中验证接入者身份,请直接发送 /repair 开始。'));
|
|
1862
1958
|
return;
|
|
1863
1959
|
}
|
|
1864
1960
|
if (action === 'compact') {
|
|
1865
|
-
await this.#handleCompact(key, chatId);
|
|
1961
|
+
await this.#handleCompact(key, chatId, messageId);
|
|
1866
1962
|
return;
|
|
1867
1963
|
}
|
|
1868
1964
|
if (action === 'stop') {
|
|
1869
|
-
await this.#handleStop(key, chatId);
|
|
1965
|
+
await this.#handleStop(key, chatId, messageId);
|
|
1870
1966
|
return;
|
|
1871
1967
|
}
|
|
1872
1968
|
if (action === 'steer') {
|
|
@@ -1877,10 +1973,10 @@ export class FeishuHarnessBridge {
|
|
|
1877
1973
|
if (action.startsWith('steer:')) {
|
|
1878
1974
|
const raw = action.slice('steer:'.length);
|
|
1879
1975
|
if (raw === 'custom') {
|
|
1880
|
-
await this.#sendCard(chatId, customSteerCard(), { key, updateMessageId: messageId });
|
|
1976
|
+
await this.#sendCard(chatId, customSteerCard(), { key, updateMessageId: messageId, replyTo: messageId });
|
|
1881
1977
|
return;
|
|
1882
1978
|
}
|
|
1883
|
-
await this.#sendSteer({ key, chatId }, raw);
|
|
1979
|
+
await this.#sendSteer({ key, chatId, messageId }, raw);
|
|
1884
1980
|
return;
|
|
1885
1981
|
}
|
|
1886
1982
|
if (action === 'presets') {
|
|
@@ -1900,7 +1996,7 @@ export class FeishuHarnessBridge {
|
|
|
1900
1996
|
return;
|
|
1901
1997
|
}
|
|
1902
1998
|
if (action === 'back_to_menu') {
|
|
1903
|
-
await this.#sendMenuCard(key, chatId, { updateMessageId: messageId });
|
|
1999
|
+
await this.#sendMenuCard(key, chatId, { updateMessageId: messageId, replyTo: messageId });
|
|
1904
2000
|
return;
|
|
1905
2001
|
}
|
|
1906
2002
|
if (action === 'preset_default') {
|
|
@@ -1923,18 +2019,18 @@ export class FeishuHarnessBridge {
|
|
|
1923
2019
|
return;
|
|
1924
2020
|
}
|
|
1925
2021
|
if (action.startsWith('use:')) {
|
|
1926
|
-
await this.#bindSession(key, chatId, action.slice('use:'.length), { updateMessageId: messageId });
|
|
2022
|
+
await this.#bindSession(key, chatId, action.slice('use:'.length), { updateMessageId: messageId, replyTo: messageId });
|
|
1927
2023
|
return;
|
|
1928
2024
|
}
|
|
1929
2025
|
if (action.startsWith('workspace:')) {
|
|
1930
|
-
await this.#switchWorkspace(key, chatId, action.slice('workspace:'.length), { updateMessageId: messageId });
|
|
2026
|
+
await this.#switchWorkspace(key, chatId, action.slice('workspace:'.length), { updateMessageId: messageId, replyTo: messageId });
|
|
1931
2027
|
return;
|
|
1932
2028
|
}
|
|
1933
2029
|
if (action.startsWith('unwatch:')) {
|
|
1934
|
-
const result = await this.#runUnwatch(key, chatId, action.slice('unwatch:'.length));
|
|
2030
|
+
const result = await this.#runUnwatch(key, chatId, action.slice('unwatch:'.length), { replyTo: messageId });
|
|
1935
2031
|
if (result.ok && messageId) {
|
|
1936
2032
|
await this.#showSessions(
|
|
1937
|
-
{ chatId, key },
|
|
2033
|
+
{ chatId, key, replyTo: messageId },
|
|
1938
2034
|
sessionWorkspace,
|
|
1939
2035
|
sessionPage,
|
|
1940
2036
|
{ updateMessageId: messageId },
|
|
@@ -1946,10 +2042,11 @@ export class FeishuHarnessBridge {
|
|
|
1946
2042
|
const sessionId = action.slice('watch:'.length);
|
|
1947
2043
|
const result = await this.#runWatch(key, chatId, sessionId, {
|
|
1948
2044
|
workspaceHint: sessionWorkspace,
|
|
2045
|
+
replyTo: messageId,
|
|
1949
2046
|
});
|
|
1950
2047
|
if (result.ok && messageId) {
|
|
1951
2048
|
await this.#showSessions(
|
|
1952
|
-
{ chatId, key },
|
|
2049
|
+
{ chatId, key, replyTo: messageId },
|
|
1953
2050
|
sessionWorkspace,
|
|
1954
2051
|
sessionPage,
|
|
1955
2052
|
{ updateMessageId: messageId },
|
|
@@ -1978,48 +2075,50 @@ export class FeishuHarnessBridge {
|
|
|
1978
2075
|
}
|
|
1979
2076
|
|
|
1980
2077
|
async #handleMenuPick(menu, number, { chatId, key, event }) {
|
|
2078
|
+
const replyTo = event?.message?.message_id ?? null;
|
|
2079
|
+
const reply = (text) => this.#send(chatId, text, { replyTo });
|
|
1981
2080
|
if (menu.kind === 'menu') {
|
|
1982
2081
|
// Number fallback for the total menu:
|
|
1983
2082
|
// 1=工作区列表 2=新会话 3=会话列表 4=状态 5=修复 6=帮助
|
|
1984
2083
|
const actions = ['workspaces', 'new', 'sessions', 'status', 'repair', 'help'];
|
|
1985
2084
|
const action = actions[number - 1];
|
|
1986
2085
|
if (!action) {
|
|
1987
|
-
await
|
|
2086
|
+
await reply(t('菜单没有这个编号,回复 /m 重新打开。'));
|
|
1988
2087
|
return;
|
|
1989
2088
|
}
|
|
1990
2089
|
if (action === 'repair') {
|
|
1991
2090
|
await this.#handleRepairCommand(event, '/repair');
|
|
1992
2091
|
return;
|
|
1993
2092
|
}
|
|
1994
|
-
await this.#handleCardAction(action, { chatId, key });
|
|
2093
|
+
await this.#handleCardAction(action, { chatId, key, messageId: replyTo });
|
|
1995
2094
|
return;
|
|
1996
2095
|
}
|
|
1997
2096
|
if (menu.kind === 'sessions') {
|
|
1998
2097
|
const session = menu.sessions[number - 1];
|
|
1999
2098
|
if (!session?.sessionId) {
|
|
2000
|
-
await
|
|
2099
|
+
await reply(t('本页只有 {count} 个会话,回复 /sessionlist 重新查看。', { count: menu.sessions.length }));
|
|
2001
2100
|
return;
|
|
2002
2101
|
}
|
|
2003
2102
|
// The number label sits on the session (bind) button of the row.
|
|
2004
|
-
await this.#handleCardAction(`use:${session.sessionId}`, { chatId, key });
|
|
2103
|
+
await this.#handleCardAction(`use:${session.sessionId}`, { chatId, key, messageId: replyTo });
|
|
2005
2104
|
return;
|
|
2006
2105
|
}
|
|
2007
2106
|
if (menu.kind === 'workspaces') {
|
|
2008
2107
|
const workspace = menu.paths[number - 1];
|
|
2009
2108
|
if (!workspace) {
|
|
2010
|
-
await
|
|
2109
|
+
await reply(t('只有 {count} 个工作区,回复 /workspacelist 重新查看。', { count: menu.paths.length }));
|
|
2011
2110
|
return;
|
|
2012
2111
|
}
|
|
2013
|
-
await this.#handleCardAction(`workspace:${workspace}`, { chatId, key });
|
|
2112
|
+
await this.#handleCardAction(`workspace:${workspace}`, { chatId, key, messageId: replyTo });
|
|
2014
2113
|
return;
|
|
2015
2114
|
}
|
|
2016
2115
|
if (menu.kind === 'watches') {
|
|
2017
2116
|
const entry = menu.entries[number - 1];
|
|
2018
2117
|
if (!entry?.sessionId) {
|
|
2019
|
-
await
|
|
2118
|
+
await reply(t('关注列表只有 {count} 个会话。', { count: menu.entries.length }));
|
|
2020
2119
|
return;
|
|
2021
2120
|
}
|
|
2022
|
-
await this.#handleCardAction(`unwatch:${entry.sessionId}`, { chatId, key });
|
|
2121
|
+
await this.#handleCardAction(`unwatch:${entry.sessionId}`, { chatId, key, messageId: replyTo });
|
|
2023
2122
|
}
|
|
2024
2123
|
}
|
|
2025
2124
|
|
|
@@ -2075,7 +2174,7 @@ export class FeishuHarnessBridge {
|
|
|
2075
2174
|
},
|
|
2076
2175
|
);
|
|
2077
2176
|
} catch (error) {
|
|
2078
|
-
await this.#sendFailure(chatId, error, { logLabel: 'session list' });
|
|
2177
|
+
await this.#sendFailure(chatId, error, { logLabel: 'session list', replyTo });
|
|
2079
2178
|
}
|
|
2080
2179
|
}
|
|
2081
2180
|
|
|
@@ -2092,35 +2191,37 @@ export class FeishuHarnessBridge {
|
|
|
2092
2191
|
{ key, updateMessageId, replyTo },
|
|
2093
2192
|
);
|
|
2094
2193
|
} catch (error) {
|
|
2095
|
-
await this.#sendFailure(chatId, error, { logLabel: 'workspace list' });
|
|
2194
|
+
await this.#sendFailure(chatId, error, { logLabel: 'workspace list', replyTo });
|
|
2096
2195
|
}
|
|
2097
2196
|
}
|
|
2098
2197
|
|
|
2099
|
-
async #bindSession(key, chatId, sessionId, { updateMessageId = null } = {}) {
|
|
2198
|
+
async #bindSession(key, chatId, sessionId, { updateMessageId = null, replyTo = null } = {}) {
|
|
2100
2199
|
try {
|
|
2101
2200
|
const bound = await this.#harness.bindWorkspaceSession(key, sessionId);
|
|
2102
2201
|
const title = String(bound?.title ?? '').replace(/\s+/gu, ' ').trim() || t('暂无标题');
|
|
2103
2202
|
await this.#send(chatId, [
|
|
2104
2203
|
t('已绑定会话「{title}」\nID:{id}', { title, id: bound?.sessionId ?? sessionId }),
|
|
2105
2204
|
t('发送 /history 查看最近对话。'),
|
|
2106
|
-
].join('\n'));
|
|
2107
|
-
await this.#sendMenuCard(key, chatId, { updateMessageId });
|
|
2205
|
+
].join('\n'), { replyTo });
|
|
2206
|
+
await this.#sendMenuCard(key, chatId, { updateMessageId, replyTo });
|
|
2108
2207
|
} catch (error) {
|
|
2109
2208
|
await this.#sendFailure(chatId, error, {
|
|
2110
2209
|
logLabel: 'session binding',
|
|
2210
|
+
replyTo,
|
|
2111
2211
|
userMessage: t('绑定失败:{message}', { message: safeErrorText(error) }),
|
|
2112
2212
|
});
|
|
2113
2213
|
}
|
|
2114
2214
|
}
|
|
2115
2215
|
|
|
2116
|
-
async #switchWorkspace(key, chatId, workspace, { updateMessageId = null } = {}) {
|
|
2216
|
+
async #switchWorkspace(key, chatId, workspace, { updateMessageId = null, replyTo = null } = {}) {
|
|
2117
2217
|
try {
|
|
2118
2218
|
const current = await this.#harness.switchWorkspace(workspace);
|
|
2119
|
-
await this.#send(chatId, t('工作区已切换为:{workspace}', { workspace: current }));
|
|
2120
|
-
await this.#sendMenuCard(key, chatId, { updateMessageId });
|
|
2219
|
+
await this.#send(chatId, t('工作区已切换为:{workspace}', { workspace: current }), { replyTo });
|
|
2220
|
+
await this.#sendMenuCard(key, chatId, { updateMessageId, replyTo });
|
|
2121
2221
|
} catch (error) {
|
|
2122
2222
|
await this.#sendFailure(chatId, error, {
|
|
2123
2223
|
logLabel: 'workspace switch',
|
|
2224
|
+
replyTo,
|
|
2124
2225
|
userMessage: t('切换失败:{message}', { message: safeErrorText(error) }),
|
|
2125
2226
|
});
|
|
2126
2227
|
}
|
|
@@ -2344,7 +2445,7 @@ export class FeishuHarnessBridge {
|
|
|
2344
2445
|
catalog._currentId = settings.agentPreset;
|
|
2345
2446
|
await this.#sendCard(chatId, presetCard(catalog), { key, updateMessageId });
|
|
2346
2447
|
} catch (error) {
|
|
2347
|
-
await this.#sendFailure(chatId, error, { logLabel: 'preset card' });
|
|
2448
|
+
await this.#sendFailure(chatId, error, { logLabel: 'preset card', replyTo: updateMessageId });
|
|
2348
2449
|
}
|
|
2349
2450
|
}
|
|
2350
2451
|
|
|
@@ -2369,7 +2470,7 @@ export class FeishuHarnessBridge {
|
|
|
2369
2470
|
}
|
|
2370
2471
|
await this.#sendCard(chatId, modelCard(catalog), { key, updateMessageId });
|
|
2371
2472
|
} catch (error) {
|
|
2372
|
-
await this.#sendFailure(chatId, error, { logLabel: 'model card' });
|
|
2473
|
+
await this.#sendFailure(chatId, error, { logLabel: 'model card', replyTo: updateMessageId });
|
|
2373
2474
|
}
|
|
2374
2475
|
}
|
|
2375
2476
|
|
|
@@ -2397,7 +2498,7 @@ export class FeishuHarnessBridge {
|
|
|
2397
2498
|
}
|
|
2398
2499
|
await this.#send(chatId, lines.join('\n'), { replyTo });
|
|
2399
2500
|
} catch (error) {
|
|
2400
|
-
await this.#sendFailure(chatId, error, { logLabel: 'status text' });
|
|
2501
|
+
await this.#sendFailure(chatId, error, { logLabel: 'status text', replyTo });
|
|
2401
2502
|
}
|
|
2402
2503
|
}
|
|
2403
2504
|
|
|
@@ -2449,7 +2550,7 @@ export class FeishuHarnessBridge {
|
|
|
2449
2550
|
|
|
2450
2551
|
await this.#sendCard(chatId, statusCard(info), { key, updateMessageId });
|
|
2451
2552
|
} catch (error) {
|
|
2452
|
-
await this.#sendFailure(chatId, error, { logLabel: 'status card' });
|
|
2553
|
+
await this.#sendFailure(chatId, error, { logLabel: 'status card', replyTo: updateMessageId });
|
|
2453
2554
|
}
|
|
2454
2555
|
}
|
|
2455
2556
|
|
|
@@ -2467,21 +2568,21 @@ export class FeishuHarnessBridge {
|
|
|
2467
2568
|
/**
|
|
2468
2569
|
* Run the /compact command and show the result.
|
|
2469
2570
|
*/
|
|
2470
|
-
async #handleCompact(key, chatId) {
|
|
2571
|
+
async #handleCompact(key, chatId, replyTo = null) {
|
|
2471
2572
|
try {
|
|
2472
2573
|
const result = await runCompactCommand(
|
|
2473
2574
|
'/compact', this.#harness, this.#state, key, { signal: this.#signal },
|
|
2474
2575
|
);
|
|
2475
|
-
await this.#send(chatId, result?.message || t('上下文压缩失败。'));
|
|
2576
|
+
await this.#send(chatId, result?.message || t('上下文压缩失败。'), { replyTo });
|
|
2476
2577
|
} catch (error) {
|
|
2477
|
-
await this.#sendFailure(chatId, error, { logLabel: 'compact' });
|
|
2578
|
+
await this.#sendFailure(chatId, error, { logLabel: 'compact', replyTo });
|
|
2478
2579
|
}
|
|
2479
2580
|
}
|
|
2480
2581
|
|
|
2481
2582
|
/**
|
|
2482
2583
|
* Stop the running task in the bound session (mirrors `/stop`).
|
|
2483
2584
|
*/
|
|
2484
|
-
async #handleStop(key, chatId) {
|
|
2585
|
+
async #handleStop(key, chatId, replyTo = null) {
|
|
2485
2586
|
try {
|
|
2486
2587
|
const result = await runControlCommand(
|
|
2487
2588
|
'/stop', this.#harness, this.#state, key, {
|
|
@@ -2495,9 +2596,9 @@ export class FeishuHarnessBridge {
|
|
|
2495
2596
|
this.#approvals.closeRoute(key),
|
|
2496
2597
|
]);
|
|
2497
2598
|
}
|
|
2498
|
-
await this.#send(chatId, result?.message || t('/stop 执行完成。'));
|
|
2599
|
+
await this.#send(chatId, result?.message || t('/stop 执行完成。'), { replyTo });
|
|
2499
2600
|
} catch (error) {
|
|
2500
|
-
await this.#sendFailure(chatId, error, { logLabel: 'stop' });
|
|
2601
|
+
await this.#sendFailure(chatId, error, { logLabel: 'stop', replyTo });
|
|
2501
2602
|
}
|
|
2502
2603
|
}
|
|
2503
2604
|
|
|
@@ -2522,26 +2623,26 @@ export class FeishuHarnessBridge {
|
|
|
2522
2623
|
control: { owner: this, key },
|
|
2523
2624
|
},
|
|
2524
2625
|
);
|
|
2525
|
-
await this.#send(chatId, result?.message || t('已提交补充指令。'));
|
|
2626
|
+
await this.#send(chatId, result?.message || t('已提交补充指令。'), { replyTo: entry.messageId ?? null });
|
|
2526
2627
|
}
|
|
2527
2628
|
|
|
2528
2629
|
/**
|
|
2529
2630
|
* Reset the preset to follow the Host default.
|
|
2530
2631
|
*/
|
|
2531
|
-
async #handlePresetDefault(key, chatId, { updateMessageId = null } = {}) {
|
|
2632
|
+
async #handlePresetDefault(key, chatId, { updateMessageId = null, replyTo = null } = {}) {
|
|
2532
2633
|
try {
|
|
2533
2634
|
const result = await runPresetCommand(
|
|
2534
2635
|
'/preset --default', this.#harness, this.#state, key, { signal: this.#signal },
|
|
2535
2636
|
);
|
|
2536
2637
|
for (const reply of result?.messages ?? [result?.message]) {
|
|
2537
|
-
if (reply) await this.#send(chatId, reply);
|
|
2638
|
+
if (reply) await this.#send(chatId, reply, { replyTo });
|
|
2538
2639
|
}
|
|
2539
2640
|
} catch (error) {
|
|
2540
|
-
await this.#sendFailure(chatId, error, { logLabel: 'preset reset' });
|
|
2641
|
+
await this.#sendFailure(chatId, error, { logLabel: 'preset reset', replyTo: updateMessageId });
|
|
2541
2642
|
return;
|
|
2542
2643
|
}
|
|
2543
2644
|
try {
|
|
2544
|
-
await this.#sendMenuCard(key, chatId, { updateMessageId });
|
|
2645
|
+
await this.#sendMenuCard(key, chatId, { updateMessageId, replyTo });
|
|
2545
2646
|
} catch (error) {
|
|
2546
2647
|
this.#logger.warn?.('[dsh-feishu] menu refresh failed after preset reset:', error.message);
|
|
2547
2648
|
}
|
|
@@ -2550,21 +2651,21 @@ export class FeishuHarnessBridge {
|
|
|
2550
2651
|
/**
|
|
2551
2652
|
* Handle preset selection from the preset dropdown.
|
|
2552
2653
|
*/
|
|
2553
|
-
async #handlePresetSelect(key, chatId, presetId, { updateMessageId = null } = {}) {
|
|
2654
|
+
async #handlePresetSelect(key, chatId, presetId, { updateMessageId = null, replyTo = null } = {}) {
|
|
2554
2655
|
try {
|
|
2555
2656
|
const selector = /^\d+$/u.test(presetId) ? `id:${presetId}` : presetId;
|
|
2556
2657
|
const result = await runPresetCommand(
|
|
2557
2658
|
`/preset ${selector}`, this.#harness, this.#state, key, { signal: this.#signal },
|
|
2558
2659
|
);
|
|
2559
2660
|
for (const reply of result?.messages ?? [result?.message]) {
|
|
2560
|
-
if (reply) await this.#send(chatId, reply);
|
|
2661
|
+
if (reply) await this.#send(chatId, reply, { replyTo });
|
|
2561
2662
|
}
|
|
2562
2663
|
} catch (error) {
|
|
2563
|
-
await this.#sendFailure(chatId, error, { logLabel: 'preset selection' });
|
|
2664
|
+
await this.#sendFailure(chatId, error, { logLabel: 'preset selection', replyTo: updateMessageId });
|
|
2564
2665
|
return;
|
|
2565
2666
|
}
|
|
2566
2667
|
try {
|
|
2567
|
-
await this.#sendMenuCard(key, chatId, { updateMessageId });
|
|
2668
|
+
await this.#sendMenuCard(key, chatId, { updateMessageId, replyTo });
|
|
2568
2669
|
} catch (error) {
|
|
2569
2670
|
this.#logger.warn?.('[dsh-feishu] menu refresh failed after preset select:', error.message);
|
|
2570
2671
|
}
|
|
@@ -2579,7 +2680,7 @@ export class FeishuHarnessBridge {
|
|
|
2579
2680
|
* catalog validation, busy checks, pending-interaction checks and the
|
|
2580
2681
|
* session binding lock stay in one place.
|
|
2581
2682
|
*/
|
|
2582
|
-
async #handleModelSelect(key, chatId, modelId, { updateMessageId = null } = {}) {
|
|
2683
|
+
async #handleModelSelect(key, chatId, modelId, { updateMessageId = null, replyTo = null } = {}) {
|
|
2583
2684
|
try {
|
|
2584
2685
|
const result = await runModelCommand(
|
|
2585
2686
|
`/model ${modelId}`, this.#harness, this.#state, key, {
|
|
@@ -2589,14 +2690,14 @@ export class FeishuHarnessBridge {
|
|
|
2589
2690
|
},
|
|
2590
2691
|
);
|
|
2591
2692
|
for (const reply of result?.messages ?? [result?.message]) {
|
|
2592
|
-
if (reply) await this.#send(chatId, reply);
|
|
2693
|
+
if (reply) await this.#send(chatId, reply, { replyTo });
|
|
2593
2694
|
}
|
|
2594
2695
|
} catch (error) {
|
|
2595
|
-
await this.#sendFailure(chatId, error, { logLabel: 'model selection' });
|
|
2696
|
+
await this.#sendFailure(chatId, error, { logLabel: 'model selection', replyTo: updateMessageId });
|
|
2596
2697
|
return;
|
|
2597
2698
|
}
|
|
2598
2699
|
try {
|
|
2599
|
-
await this.#sendMenuCard(key, chatId, { updateMessageId });
|
|
2700
|
+
await this.#sendMenuCard(key, chatId, { updateMessageId, replyTo });
|
|
2600
2701
|
} catch (error) {
|
|
2601
2702
|
this.#logger.warn?.('[dsh-feishu] menu refresh failed after model select:', error.message);
|
|
2602
2703
|
}
|
|
@@ -2867,6 +2968,13 @@ export class FeishuHarnessBridge {
|
|
|
2867
2968
|
chatId,
|
|
2868
2969
|
lastSeq,
|
|
2869
2970
|
...(watchStartedAt !== null ? { watchStartedAt } : {}),
|
|
2971
|
+
// Remember where the watch was created so completion pushes can be
|
|
2972
|
+
// delivered as replies inside the same Feishu topic.
|
|
2973
|
+
...(replyTo
|
|
2974
|
+
? { replyToMessageId: replyTo }
|
|
2975
|
+
: existingEntry?.replyToMessageId
|
|
2976
|
+
? { replyToMessageId: existingEntry.replyToMessageId }
|
|
2977
|
+
: {}),
|
|
2870
2978
|
});
|
|
2871
2979
|
} catch (error) {
|
|
2872
2980
|
await reply(t('关注失败:{message}', { message: safeErrorText(error) }));
|
|
@@ -2992,7 +3100,7 @@ export class FeishuHarnessBridge {
|
|
|
2992
3100
|
await this.#sendCard(
|
|
2993
3101
|
entry.chatId,
|
|
2994
3102
|
completionCard(sessionId, entry.title, reason),
|
|
2995
|
-
{ key },
|
|
3103
|
+
{ key, replyTo: entry.replyToMessageId ?? null },
|
|
2996
3104
|
);
|
|
2997
3105
|
const current = this.#state.watchEntry?.(key, sessionId);
|
|
2998
3106
|
if (!current
|