@xmanrui/dsh-im 4.2.1 → 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/README.en.md +4 -2
- package/README.md +4 -2
- package/lib/client.js +1267 -787
- package/lib/index.js +237 -235
- 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/context-enhancement.js +207 -102
- package/plugin-src/client/delivery-settings.js +61 -9
- package/plugin-src/client/i18n.js +71 -3
- package/plugin-src/client/index.js +1 -0
- package/plugin-src/client/styles.js +59 -8
- 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/scripts/verify-package.mjs +4 -3
- 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 +286 -120
- package/src/channels/feishu/feishu-runtime.mjs +4 -0
- package/src/channels/feishu/message-utils.mjs +5 -0
- package/src/channels/qq/qq-bridge.mjs +59 -17
- package/src/channels/qq/qq-runtime.mjs +25 -3
- 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/context-enhancement.mjs +73 -20
- 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
|
: '';
|
|
@@ -686,7 +725,7 @@ export class FeishuHarnessBridge {
|
|
|
686
725
|
? pending.queue
|
|
687
726
|
: null,
|
|
688
727
|
isQuestionPending: () => this.#pendingInteractions.has(key),
|
|
689
|
-
send: (text) => this.#send(event.message.chat_id, text),
|
|
728
|
+
send: (text) => this.#send(event.message.chat_id, text, { replyTo: event.message.message_id }),
|
|
690
729
|
});
|
|
691
730
|
if (approvalReply) {
|
|
692
731
|
const processing = approvalReply.process(async () => {
|
|
@@ -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
|
|
|
@@ -851,7 +922,7 @@ export class FeishuHarnessBridge {
|
|
|
851
922
|
if (error?.code === 'turn-stopped') {
|
|
852
923
|
await this.#removeProcessingReaction(messageId, processingReaction);
|
|
853
924
|
if (error?.batchInputMessage) {
|
|
854
|
-
await this.#send(event.message.chat_id, error.batchInputMessage).catch(() => undefined);
|
|
925
|
+
await this.#send(event.message.chat_id, error.batchInputMessage, { replyTo: event.message.message_id }).catch(() => undefined);
|
|
855
926
|
}
|
|
856
927
|
return;
|
|
857
928
|
}
|
|
@@ -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
|
|
|
@@ -940,7 +1012,7 @@ export class FeishuHarnessBridge {
|
|
|
940
1012
|
]);
|
|
941
1013
|
}
|
|
942
1014
|
for (const reply of result?.messages ?? [result?.message]) {
|
|
943
|
-
if (reply) await this.#send(event.message.chat_id, reply);
|
|
1015
|
+
if (reply) await this.#send(event.message.chat_id, reply, { replyTo: event.message.message_id });
|
|
944
1016
|
}
|
|
945
1017
|
this.#status.lastError = null;
|
|
946
1018
|
}
|
|
@@ -964,7 +1036,7 @@ export class FeishuHarnessBridge {
|
|
|
964
1036
|
// accept() 侧已用 nonEmptyString(content) 判定,两侧保持一致。
|
|
965
1037
|
const commandText = !hasImages && !hasFiles && text ? text.trim() : null;
|
|
966
1038
|
if (!text && !hasImages && !hasFiles) {
|
|
967
|
-
await this.#send(event.message.chat_id, t('目前支持文字、图片和文件消息。'));
|
|
1039
|
+
await this.#send(event.message.chat_id, t('目前支持文字、图片和文件消息。'), { replyTo: event.message.message_id });
|
|
968
1040
|
return;
|
|
969
1041
|
}
|
|
970
1042
|
|
|
@@ -973,11 +1045,11 @@ export class FeishuHarnessBridge {
|
|
|
973
1045
|
return;
|
|
974
1046
|
}
|
|
975
1047
|
if (commandText === '/help') {
|
|
976
|
-
await this.#send(event.message.chat_id, menuHelpText());
|
|
1048
|
+
await this.#send(event.message.chat_id, menuHelpText(), { replyTo: event.message.message_id });
|
|
977
1049
|
return;
|
|
978
1050
|
}
|
|
979
1051
|
if (MENU_COMMAND.test(commandText)) {
|
|
980
|
-
await this.#sendMenuCard(key, event.message.chat_id);
|
|
1052
|
+
await this.#sendMenuCard(key, event.message.chat_id, { replyTo: event.message.message_id });
|
|
981
1053
|
return;
|
|
982
1054
|
}
|
|
983
1055
|
if (commandText === '/new') {
|
|
@@ -985,53 +1057,54 @@ export class FeishuHarnessBridge {
|
|
|
985
1057
|
await this.#send(
|
|
986
1058
|
event.message.chat_id,
|
|
987
1059
|
t('当前任务仍在运行,请先停止任务或等待任务完成后再开启新会话。'),
|
|
1060
|
+
{ replyTo: event.message.message_id },
|
|
988
1061
|
);
|
|
989
1062
|
return;
|
|
990
1063
|
}
|
|
991
1064
|
await this.#state.clearSession(key);
|
|
992
|
-
await this.#send(event.message.chat_id, t('已开启全新 Harness 会话。'));
|
|
993
|
-
await this.#sendMenuCard(key, event.message.chat_id);
|
|
1065
|
+
await this.#send(event.message.chat_id, t('已开启全新 Harness 会话。'), { replyTo: event.message.message_id });
|
|
1066
|
+
await this.#sendMenuCard(key, event.message.chat_id, { replyTo: event.message.message_id });
|
|
994
1067
|
return;
|
|
995
1068
|
}
|
|
996
1069
|
if (commandText === '/status') {
|
|
997
|
-
await this.#showStatusText(key, event.message.chat_id);
|
|
1070
|
+
await this.#showStatusText(key, event.message.chat_id, event.message.message_id);
|
|
998
1071
|
return;
|
|
999
1072
|
}
|
|
1000
1073
|
if (commandText === '/compact') {
|
|
1001
1074
|
const compactCommand = await runCompactCommand(commandText, this.#harness, this.#state, key, { signal: this.#signal });
|
|
1002
1075
|
if (compactCommand) {
|
|
1003
|
-
await this.#send(event.message.chat_id, compactCommand.message);
|
|
1076
|
+
await this.#send(event.message.chat_id, compactCommand.message, { replyTo: event.message.message_id });
|
|
1004
1077
|
}
|
|
1005
1078
|
return;
|
|
1006
1079
|
}
|
|
1007
1080
|
if (SESSION_LIST_PREFIX.test(commandText)) {
|
|
1008
1081
|
const selector = commandText.replace(/^\/(?:sessionlist|sessions)/i, '').trim() || null;
|
|
1009
|
-
await this.#showSessions({ chatId: event.message.chat_id, key }, selector, 0);
|
|
1082
|
+
await this.#showSessions({ chatId: event.message.chat_id, key, replyTo: event.message.message_id }, selector, 0);
|
|
1010
1083
|
return;
|
|
1011
1084
|
}
|
|
1012
1085
|
if (WORKSPACE_LIST_COMMAND.test(commandText)) {
|
|
1013
|
-
await this.#showWorkspaces({ chatId: event.message.chat_id, key });
|
|
1086
|
+
await this.#showWorkspaces({ chatId: event.message.chat_id, key, replyTo: event.message.message_id });
|
|
1014
1087
|
return;
|
|
1015
1088
|
}
|
|
1016
1089
|
if (WATCH_COMMAND.test(commandText)) {
|
|
1017
1090
|
const target = (WATCH_COMMAND.exec(commandText)?.[1] ?? '').trim() || null;
|
|
1018
|
-
await this.#runWatch(key, event.message.chat_id, target);
|
|
1091
|
+
await this.#runWatch(key, event.message.chat_id, target, { replyTo: event.message.message_id });
|
|
1019
1092
|
return;
|
|
1020
1093
|
}
|
|
1021
1094
|
if (UNWATCH_COMMAND.test(commandText)) {
|
|
1022
1095
|
const target = (UNWATCH_COMMAND.exec(commandText)?.[1] ?? '').trim() || null;
|
|
1023
|
-
await this.#runUnwatch(key, event.message.chat_id, target);
|
|
1096
|
+
await this.#runUnwatch(key, event.message.chat_id, target, { replyTo: event.message.message_id });
|
|
1024
1097
|
return;
|
|
1025
1098
|
}
|
|
1026
1099
|
if (WATCHLIST_COMMAND.test(commandText)) {
|
|
1027
|
-
await this.#showWatchList(key, event.message.chat_id);
|
|
1100
|
+
await this.#showWatchList(key, event.message.chat_id, { replyTo: event.message.message_id });
|
|
1028
1101
|
return;
|
|
1029
1102
|
}
|
|
1030
1103
|
if (ARCHIVED_COMMAND.test(commandText)) {
|
|
1031
1104
|
const match = ARCHIVED_COMMAND.exec(commandText);
|
|
1032
1105
|
const value = match[1]?.toLowerCase();
|
|
1033
1106
|
if (value !== 'on' && value !== 'off') {
|
|
1034
|
-
await this.#send(event.message.chat_id, t('用法:/archived on(包含归档会话)或 /archived off(隐藏归档会话)'));
|
|
1107
|
+
await this.#send(event.message.chat_id, t('用法:/archived on(包含归档会话)或 /archived off(隐藏归档会话)'), { replyTo: event.message.message_id });
|
|
1035
1108
|
return;
|
|
1036
1109
|
}
|
|
1037
1110
|
if (typeof this.#state?.setIncludeArchivedSessions === 'function') {
|
|
@@ -1040,6 +1113,7 @@ export class FeishuHarnessBridge {
|
|
|
1040
1113
|
await this.#send(
|
|
1041
1114
|
event.message.chat_id,
|
|
1042
1115
|
value === 'on' ? t('已开启:会话列表包含归档会话。') : t('已关闭:会话列表隐藏归档会话。'),
|
|
1116
|
+
{ replyTo: event.message.message_id },
|
|
1043
1117
|
);
|
|
1044
1118
|
return;
|
|
1045
1119
|
}
|
|
@@ -1059,7 +1133,7 @@ export class FeishuHarnessBridge {
|
|
|
1059
1133
|
: await runWorkspaceCommand(text, this.#harness, key);
|
|
1060
1134
|
if (workspaceCommand) {
|
|
1061
1135
|
for (const reply of workspaceCommand.messages ?? [workspaceCommand.message]) {
|
|
1062
|
-
await this.#send(event.message.chat_id, reply);
|
|
1136
|
+
await this.#send(event.message.chat_id, reply, { replyTo: event.message.message_id });
|
|
1063
1137
|
}
|
|
1064
1138
|
return;
|
|
1065
1139
|
}
|
|
@@ -1073,7 +1147,7 @@ export class FeishuHarnessBridge {
|
|
|
1073
1147
|
{ signal: this.#signal },
|
|
1074
1148
|
);
|
|
1075
1149
|
if (compactCommand) {
|
|
1076
|
-
await this.#send(event.message.chat_id, compactCommand.message);
|
|
1150
|
+
await this.#send(event.message.chat_id, compactCommand.message, { replyTo: event.message.message_id });
|
|
1077
1151
|
return;
|
|
1078
1152
|
}
|
|
1079
1153
|
|
|
@@ -1487,10 +1561,11 @@ export class FeishuHarnessBridge {
|
|
|
1487
1561
|
?? nonEmptyString(event?.operator?.operator_id?.user_id)
|
|
1488
1562
|
?? nonEmptyString(event?.open_id)
|
|
1489
1563
|
?? nonEmptyString(event?.user_id);
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
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');
|
|
1494
1569
|
return Promise.resolve();
|
|
1495
1570
|
}
|
|
1496
1571
|
const actionValue = callbackObject(event?.action?.value);
|
|
@@ -1529,16 +1604,36 @@ export class FeishuHarnessBridge {
|
|
|
1529
1604
|
?? nonEmptyString(event?.message_id);
|
|
1530
1605
|
const route = messageId ? this.#cardKeys.get(messageId) : null;
|
|
1531
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.
|
|
1532
1612
|
// The card predates this process (the in-memory mapping resets on
|
|
1533
1613
|
// restart) or never came from us: nudge instead of staying silent.
|
|
1534
1614
|
const chatId = nonEmptyString(event?.context?.open_chat_id)
|
|
1535
1615
|
?? nonEmptyString(event?.open_chat_id)
|
|
1536
1616
|
?? nonEmptyString(event?.chat_id);
|
|
1537
1617
|
if (chatId) {
|
|
1538
|
-
this.#send(chatId, t('这个菜单已过期,请回复 /m 重新打开。')).catch(() => undefined);
|
|
1618
|
+
this.#send(chatId, t('这个菜单已过期,请回复 /m 重新打开。'), { replyTo: messageId }).catch(() => undefined);
|
|
1539
1619
|
}
|
|
1540
1620
|
return Promise.resolve();
|
|
1541
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
|
+
}
|
|
1542
1637
|
// A used card is recent even if it was first created long ago.
|
|
1543
1638
|
this.#cardKeys.delete(messageId);
|
|
1544
1639
|
this.#cardKeys.set(messageId, route);
|
|
@@ -1588,7 +1683,7 @@ export class FeishuHarnessBridge {
|
|
|
1588
1683
|
await this.#sendSteer(entry, formText);
|
|
1589
1684
|
return;
|
|
1590
1685
|
}
|
|
1591
|
-
await this.#send(entry.chatId, t('请输入补充指令后再提交。'));
|
|
1686
|
+
await this.#send(entry.chatId, t('请输入补充指令后再提交。'), { replyTo: entry.messageId ?? null });
|
|
1592
1687
|
return;
|
|
1593
1688
|
}
|
|
1594
1689
|
}
|
|
@@ -1628,7 +1723,7 @@ export class FeishuHarnessBridge {
|
|
|
1628
1723
|
}
|
|
1629
1724
|
this.#logger.warn?.('[dsh-feishu] card action queue is full; dropping callbacks');
|
|
1630
1725
|
let tracked;
|
|
1631
|
-
tracked = this.#send(entry.chatId, t('操作过于频繁,请稍后再试。'))
|
|
1726
|
+
tracked = this.#send(entry.chatId, t('操作过于频繁,请稍后再试。'), { replyTo: entry.messageId ?? null })
|
|
1632
1727
|
.catch(() => undefined)
|
|
1633
1728
|
.finally(() => {
|
|
1634
1729
|
this.#cardActionTasks.delete(tracked);
|
|
@@ -1709,7 +1804,7 @@ export class FeishuHarnessBridge {
|
|
|
1709
1804
|
})
|
|
1710
1805
|
.catch(async (error) => {
|
|
1711
1806
|
if (this.#signal?.aborted) return;
|
|
1712
|
-
await this.#sendFailure(entry.chatId, error, { logLabel: 'card action' });
|
|
1807
|
+
await this.#sendFailure(entry.chatId, error, { logLabel: 'card action', replyTo: entry.messageId });
|
|
1713
1808
|
})
|
|
1714
1809
|
.finally(() => {
|
|
1715
1810
|
if (this.#cardActionInFlight.get(dedupeKey) === tracked) {
|
|
@@ -1750,10 +1845,13 @@ export class FeishuHarnessBridge {
|
|
|
1750
1845
|
sessionPage = 0,
|
|
1751
1846
|
selections = [],
|
|
1752
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 });
|
|
1753
1851
|
if (action === 'sessions' || /^sessions:\d+$/.test(action)) {
|
|
1754
1852
|
const page = action === 'sessions' ? 0 : Number(action.slice('sessions:'.length));
|
|
1755
1853
|
await this.#showSessions(
|
|
1756
|
-
{ chatId, key },
|
|
1854
|
+
{ chatId, key, replyTo: messageId },
|
|
1757
1855
|
sessionWorkspace,
|
|
1758
1856
|
page,
|
|
1759
1857
|
{ updateMessageId: messageId },
|
|
@@ -1761,17 +1859,17 @@ export class FeishuHarnessBridge {
|
|
|
1761
1859
|
return;
|
|
1762
1860
|
}
|
|
1763
1861
|
if (action === 'workspaces') {
|
|
1764
|
-
await this.#showWorkspaces({ chatId, key }, { updateMessageId: messageId });
|
|
1862
|
+
await this.#showWorkspaces({ chatId, key, replyTo: messageId }, { updateMessageId: messageId });
|
|
1765
1863
|
return;
|
|
1766
1864
|
}
|
|
1767
1865
|
if (action === 'watchlist') {
|
|
1768
|
-
await this.#showWatchList(key, chatId, { updateMessageId: messageId });
|
|
1866
|
+
await this.#showWatchList(key, chatId, { updateMessageId: messageId, replyTo: messageId });
|
|
1769
1867
|
return;
|
|
1770
1868
|
}
|
|
1771
1869
|
// 多选关注下拉:action=watch_add / watch_remove,选中项在 selections 数组
|
|
1772
1870
|
if (action === 'watch_add' || action === 'watch_remove') {
|
|
1773
1871
|
if (selections.length === 0) {
|
|
1774
|
-
await
|
|
1872
|
+
await reply(t('请先选择至少一个会话。'));
|
|
1775
1873
|
return;
|
|
1776
1874
|
}
|
|
1777
1875
|
let changed = 0;
|
|
@@ -1792,6 +1890,7 @@ export class FeishuHarnessBridge {
|
|
|
1792
1890
|
const result = await this.#runWatch(key, chatId, sessionId, {
|
|
1793
1891
|
notify: false,
|
|
1794
1892
|
validatedTarget,
|
|
1893
|
+
replyTo: messageId,
|
|
1795
1894
|
});
|
|
1796
1895
|
if (result.changed) changed += 1;
|
|
1797
1896
|
else if (!result.ok) failed += 1;
|
|
@@ -1817,54 +1916,53 @@ export class FeishuHarnessBridge {
|
|
|
1817
1916
|
? t('所选会话已在关注列表中。')
|
|
1818
1917
|
: t('所选会话已不在关注列表中。');
|
|
1819
1918
|
try {
|
|
1820
|
-
await this.#showWatchList(key, chatId, { updateMessageId: messageId });
|
|
1919
|
+
await this.#showWatchList(key, chatId, { updateMessageId: messageId, replyTo: messageId });
|
|
1821
1920
|
} catch (error) {
|
|
1822
1921
|
this.#logger.warn?.('[dsh-feishu] watch list refresh failed:', error.message);
|
|
1823
1922
|
}
|
|
1824
|
-
await
|
|
1923
|
+
await reply(summary).catch((error) => {
|
|
1825
1924
|
this.#logger.warn?.('[dsh-feishu] watch batch summary failed:', error.message);
|
|
1826
1925
|
});
|
|
1827
1926
|
return;
|
|
1828
1927
|
}
|
|
1829
1928
|
if (action === 'new') {
|
|
1830
1929
|
if (this.#queues.has(key) || this.#hasPendingInteraction(key)) {
|
|
1831
|
-
await
|
|
1930
|
+
await reply(t('当前任务仍在运行,请先停止任务或等待任务完成后再开启新会话。'));
|
|
1832
1931
|
return;
|
|
1833
1932
|
}
|
|
1834
1933
|
await this.#state.clearSession(key);
|
|
1835
|
-
await
|
|
1836
|
-
await this.#sendMenuCard(key, chatId, { updateMessageId: messageId });
|
|
1934
|
+
await reply(t('已开启全新 Harness 会话。'));
|
|
1935
|
+
await this.#sendMenuCard(key, chatId, { updateMessageId: messageId, replyTo: messageId });
|
|
1837
1936
|
return;
|
|
1838
1937
|
}
|
|
1839
1938
|
if (action === 'use:current') {
|
|
1840
1939
|
const sessionId = this.#state.sessionFor(key);
|
|
1841
1940
|
if (typeof sessionId !== 'string' || !sessionId) {
|
|
1842
|
-
await
|
|
1941
|
+
await reply(t('当前没有绑定的会话,请先从会话列表选择。'));
|
|
1843
1942
|
return;
|
|
1844
1943
|
}
|
|
1845
|
-
await
|
|
1944
|
+
await reply(t('已就绪,直接发消息即可继续当前会话。'));
|
|
1846
1945
|
return;
|
|
1847
1946
|
}
|
|
1848
1947
|
if (action === 'archive_toggle' || action === 'archive:on' || action === 'archive:off') {
|
|
1849
1948
|
const next = action === 'archive:on' ? true : action === 'archive:off' ? false : !(this.#state?.includesArchivedSessions?.() ?? false);
|
|
1850
1949
|
await this.#state?.setIncludeArchivedSessions?.(next);
|
|
1851
|
-
await
|
|
1852
|
-
chatId,
|
|
1950
|
+
await reply(
|
|
1853
1951
|
next ? t('已开启:会话列表包含归档会话。') : t('已关闭:会话列表隐藏归档会话。'),
|
|
1854
1952
|
);
|
|
1855
|
-
await this.#sendMenuCard(key, chatId, { updateMessageId: messageId });
|
|
1953
|
+
await this.#sendMenuCard(key, chatId, { updateMessageId: messageId, replyTo: messageId });
|
|
1856
1954
|
return;
|
|
1857
1955
|
}
|
|
1858
1956
|
if (action === 'repair') {
|
|
1859
|
-
await
|
|
1957
|
+
await reply(t('修复需在私聊中验证接入者身份,请直接发送 /repair 开始。'));
|
|
1860
1958
|
return;
|
|
1861
1959
|
}
|
|
1862
1960
|
if (action === 'compact') {
|
|
1863
|
-
await this.#handleCompact(key, chatId);
|
|
1961
|
+
await this.#handleCompact(key, chatId, messageId);
|
|
1864
1962
|
return;
|
|
1865
1963
|
}
|
|
1866
1964
|
if (action === 'stop') {
|
|
1867
|
-
await this.#handleStop(key, chatId);
|
|
1965
|
+
await this.#handleStop(key, chatId, messageId);
|
|
1868
1966
|
return;
|
|
1869
1967
|
}
|
|
1870
1968
|
if (action === 'steer') {
|
|
@@ -1875,10 +1973,10 @@ export class FeishuHarnessBridge {
|
|
|
1875
1973
|
if (action.startsWith('steer:')) {
|
|
1876
1974
|
const raw = action.slice('steer:'.length);
|
|
1877
1975
|
if (raw === 'custom') {
|
|
1878
|
-
await this.#sendCard(chatId, customSteerCard(), { key, updateMessageId: messageId });
|
|
1976
|
+
await this.#sendCard(chatId, customSteerCard(), { key, updateMessageId: messageId, replyTo: messageId });
|
|
1879
1977
|
return;
|
|
1880
1978
|
}
|
|
1881
|
-
await this.#sendSteer({ key, chatId }, raw);
|
|
1979
|
+
await this.#sendSteer({ key, chatId, messageId }, raw);
|
|
1882
1980
|
return;
|
|
1883
1981
|
}
|
|
1884
1982
|
if (action === 'presets') {
|
|
@@ -1898,7 +1996,7 @@ export class FeishuHarnessBridge {
|
|
|
1898
1996
|
return;
|
|
1899
1997
|
}
|
|
1900
1998
|
if (action === 'back_to_menu') {
|
|
1901
|
-
await this.#sendMenuCard(key, chatId, { updateMessageId: messageId });
|
|
1999
|
+
await this.#sendMenuCard(key, chatId, { updateMessageId: messageId, replyTo: messageId });
|
|
1902
2000
|
return;
|
|
1903
2001
|
}
|
|
1904
2002
|
if (action === 'preset_default') {
|
|
@@ -1921,18 +2019,18 @@ export class FeishuHarnessBridge {
|
|
|
1921
2019
|
return;
|
|
1922
2020
|
}
|
|
1923
2021
|
if (action.startsWith('use:')) {
|
|
1924
|
-
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 });
|
|
1925
2023
|
return;
|
|
1926
2024
|
}
|
|
1927
2025
|
if (action.startsWith('workspace:')) {
|
|
1928
|
-
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 });
|
|
1929
2027
|
return;
|
|
1930
2028
|
}
|
|
1931
2029
|
if (action.startsWith('unwatch:')) {
|
|
1932
|
-
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 });
|
|
1933
2031
|
if (result.ok && messageId) {
|
|
1934
2032
|
await this.#showSessions(
|
|
1935
|
-
{ chatId, key },
|
|
2033
|
+
{ chatId, key, replyTo: messageId },
|
|
1936
2034
|
sessionWorkspace,
|
|
1937
2035
|
sessionPage,
|
|
1938
2036
|
{ updateMessageId: messageId },
|
|
@@ -1944,10 +2042,11 @@ export class FeishuHarnessBridge {
|
|
|
1944
2042
|
const sessionId = action.slice('watch:'.length);
|
|
1945
2043
|
const result = await this.#runWatch(key, chatId, sessionId, {
|
|
1946
2044
|
workspaceHint: sessionWorkspace,
|
|
2045
|
+
replyTo: messageId,
|
|
1947
2046
|
});
|
|
1948
2047
|
if (result.ok && messageId) {
|
|
1949
2048
|
await this.#showSessions(
|
|
1950
|
-
{ chatId, key },
|
|
2049
|
+
{ chatId, key, replyTo: messageId },
|
|
1951
2050
|
sessionWorkspace,
|
|
1952
2051
|
sessionPage,
|
|
1953
2052
|
{ updateMessageId: messageId },
|
|
@@ -1976,48 +2075,50 @@ export class FeishuHarnessBridge {
|
|
|
1976
2075
|
}
|
|
1977
2076
|
|
|
1978
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 });
|
|
1979
2080
|
if (menu.kind === 'menu') {
|
|
1980
2081
|
// Number fallback for the total menu:
|
|
1981
2082
|
// 1=工作区列表 2=新会话 3=会话列表 4=状态 5=修复 6=帮助
|
|
1982
2083
|
const actions = ['workspaces', 'new', 'sessions', 'status', 'repair', 'help'];
|
|
1983
2084
|
const action = actions[number - 1];
|
|
1984
2085
|
if (!action) {
|
|
1985
|
-
await
|
|
2086
|
+
await reply(t('菜单没有这个编号,回复 /m 重新打开。'));
|
|
1986
2087
|
return;
|
|
1987
2088
|
}
|
|
1988
2089
|
if (action === 'repair') {
|
|
1989
2090
|
await this.#handleRepairCommand(event, '/repair');
|
|
1990
2091
|
return;
|
|
1991
2092
|
}
|
|
1992
|
-
await this.#handleCardAction(action, { chatId, key });
|
|
2093
|
+
await this.#handleCardAction(action, { chatId, key, messageId: replyTo });
|
|
1993
2094
|
return;
|
|
1994
2095
|
}
|
|
1995
2096
|
if (menu.kind === 'sessions') {
|
|
1996
2097
|
const session = menu.sessions[number - 1];
|
|
1997
2098
|
if (!session?.sessionId) {
|
|
1998
|
-
await
|
|
2099
|
+
await reply(t('本页只有 {count} 个会话,回复 /sessionlist 重新查看。', { count: menu.sessions.length }));
|
|
1999
2100
|
return;
|
|
2000
2101
|
}
|
|
2001
2102
|
// The number label sits on the session (bind) button of the row.
|
|
2002
|
-
await this.#handleCardAction(`use:${session.sessionId}`, { chatId, key });
|
|
2103
|
+
await this.#handleCardAction(`use:${session.sessionId}`, { chatId, key, messageId: replyTo });
|
|
2003
2104
|
return;
|
|
2004
2105
|
}
|
|
2005
2106
|
if (menu.kind === 'workspaces') {
|
|
2006
2107
|
const workspace = menu.paths[number - 1];
|
|
2007
2108
|
if (!workspace) {
|
|
2008
|
-
await
|
|
2109
|
+
await reply(t('只有 {count} 个工作区,回复 /workspacelist 重新查看。', { count: menu.paths.length }));
|
|
2009
2110
|
return;
|
|
2010
2111
|
}
|
|
2011
|
-
await this.#handleCardAction(`workspace:${workspace}`, { chatId, key });
|
|
2112
|
+
await this.#handleCardAction(`workspace:${workspace}`, { chatId, key, messageId: replyTo });
|
|
2012
2113
|
return;
|
|
2013
2114
|
}
|
|
2014
2115
|
if (menu.kind === 'watches') {
|
|
2015
2116
|
const entry = menu.entries[number - 1];
|
|
2016
2117
|
if (!entry?.sessionId) {
|
|
2017
|
-
await
|
|
2118
|
+
await reply(t('关注列表只有 {count} 个会话。', { count: menu.entries.length }));
|
|
2018
2119
|
return;
|
|
2019
2120
|
}
|
|
2020
|
-
await this.#handleCardAction(`unwatch:${entry.sessionId}`, { chatId, key });
|
|
2121
|
+
await this.#handleCardAction(`unwatch:${entry.sessionId}`, { chatId, key, messageId: replyTo });
|
|
2021
2122
|
}
|
|
2022
2123
|
}
|
|
2023
2124
|
|
|
@@ -2030,7 +2131,7 @@ export class FeishuHarnessBridge {
|
|
|
2030
2131
|
}
|
|
2031
2132
|
|
|
2032
2133
|
async #showSessions(
|
|
2033
|
-
{ chatId, key },
|
|
2134
|
+
{ chatId, key, replyTo = null },
|
|
2034
2135
|
selector,
|
|
2035
2136
|
page = 0,
|
|
2036
2137
|
{ updateMessageId = null } = {},
|
|
@@ -2039,14 +2140,14 @@ export class FeishuHarnessBridge {
|
|
|
2039
2140
|
const signal = this.#cardDataSignal();
|
|
2040
2141
|
const resolved = await resolveSessionListWorkspace(selector ?? '', this.#harness, { signal });
|
|
2041
2142
|
if (resolved.error) {
|
|
2042
|
-
await this.#send(chatId, resolved.error);
|
|
2143
|
+
await this.#send(chatId, resolved.error, { replyTo });
|
|
2043
2144
|
return;
|
|
2044
2145
|
}
|
|
2045
2146
|
const listed = await this.#harness.listWorkspaceSessions(resolved.workspace, { signal });
|
|
2046
2147
|
const sessions = this.#visibleSessions(Array.isArray(listed?.sessions) ? listed.sessions : []);
|
|
2047
2148
|
const workspace = listed?.workspace ?? resolved.workspace;
|
|
2048
2149
|
if (sessions.length === 0) {
|
|
2049
|
-
await this.#send(chatId, t('工作区:{workspace}\n该工作区暂无会话。', { workspace }));
|
|
2150
|
+
await this.#send(chatId, t('工作区:{workspace}\n该工作区暂无会话。', { workspace }), { replyTo });
|
|
2050
2151
|
return;
|
|
2051
2152
|
}
|
|
2052
2153
|
const pageCount = Math.ceil(sessions.length / MENU_PAGE_SIZE);
|
|
@@ -2065,6 +2166,7 @@ export class FeishuHarnessBridge {
|
|
|
2065
2166
|
{
|
|
2066
2167
|
key,
|
|
2067
2168
|
updateMessageId,
|
|
2169
|
+
replyTo,
|
|
2068
2170
|
// Keep the canonical selector result for later page callbacks. The
|
|
2069
2171
|
// list response's workspace is display data and is not authoritative.
|
|
2070
2172
|
sessionWorkspace: resolved.workspace,
|
|
@@ -2072,11 +2174,11 @@ export class FeishuHarnessBridge {
|
|
|
2072
2174
|
},
|
|
2073
2175
|
);
|
|
2074
2176
|
} catch (error) {
|
|
2075
|
-
await this.#sendFailure(chatId, error, { logLabel: 'session list' });
|
|
2177
|
+
await this.#sendFailure(chatId, error, { logLabel: 'session list', replyTo });
|
|
2076
2178
|
}
|
|
2077
2179
|
}
|
|
2078
2180
|
|
|
2079
|
-
async #showWorkspaces({ chatId, key }, { updateMessageId = null } = {}) {
|
|
2181
|
+
async #showWorkspaces({ chatId, key, replyTo = null }, { updateMessageId = null } = {}) {
|
|
2080
2182
|
try {
|
|
2081
2183
|
const { current, paths } = await workspacePathSnapshot(
|
|
2082
2184
|
this.#harness,
|
|
@@ -2086,38 +2188,40 @@ export class FeishuHarnessBridge {
|
|
|
2086
2188
|
await this.#sendCard(
|
|
2087
2189
|
chatId,
|
|
2088
2190
|
workspaceListCard(paths, current),
|
|
2089
|
-
{ key, updateMessageId },
|
|
2191
|
+
{ key, updateMessageId, replyTo },
|
|
2090
2192
|
);
|
|
2091
2193
|
} catch (error) {
|
|
2092
|
-
await this.#sendFailure(chatId, error, { logLabel: 'workspace list' });
|
|
2194
|
+
await this.#sendFailure(chatId, error, { logLabel: 'workspace list', replyTo });
|
|
2093
2195
|
}
|
|
2094
2196
|
}
|
|
2095
2197
|
|
|
2096
|
-
async #bindSession(key, chatId, sessionId, { updateMessageId = null } = {}) {
|
|
2198
|
+
async #bindSession(key, chatId, sessionId, { updateMessageId = null, replyTo = null } = {}) {
|
|
2097
2199
|
try {
|
|
2098
2200
|
const bound = await this.#harness.bindWorkspaceSession(key, sessionId);
|
|
2099
2201
|
const title = String(bound?.title ?? '').replace(/\s+/gu, ' ').trim() || t('暂无标题');
|
|
2100
2202
|
await this.#send(chatId, [
|
|
2101
2203
|
t('已绑定会话「{title}」\nID:{id}', { title, id: bound?.sessionId ?? sessionId }),
|
|
2102
2204
|
t('发送 /history 查看最近对话。'),
|
|
2103
|
-
].join('\n'));
|
|
2104
|
-
await this.#sendMenuCard(key, chatId, { updateMessageId });
|
|
2205
|
+
].join('\n'), { replyTo });
|
|
2206
|
+
await this.#sendMenuCard(key, chatId, { updateMessageId, replyTo });
|
|
2105
2207
|
} catch (error) {
|
|
2106
2208
|
await this.#sendFailure(chatId, error, {
|
|
2107
2209
|
logLabel: 'session binding',
|
|
2210
|
+
replyTo,
|
|
2108
2211
|
userMessage: t('绑定失败:{message}', { message: safeErrorText(error) }),
|
|
2109
2212
|
});
|
|
2110
2213
|
}
|
|
2111
2214
|
}
|
|
2112
2215
|
|
|
2113
|
-
async #switchWorkspace(key, chatId, workspace, { updateMessageId = null } = {}) {
|
|
2216
|
+
async #switchWorkspace(key, chatId, workspace, { updateMessageId = null, replyTo = null } = {}) {
|
|
2114
2217
|
try {
|
|
2115
2218
|
const current = await this.#harness.switchWorkspace(workspace);
|
|
2116
|
-
await this.#send(chatId, t('工作区已切换为:{workspace}', { workspace: current }));
|
|
2117
|
-
await this.#sendMenuCard(key, chatId, { updateMessageId });
|
|
2219
|
+
await this.#send(chatId, t('工作区已切换为:{workspace}', { workspace: current }), { replyTo });
|
|
2220
|
+
await this.#sendMenuCard(key, chatId, { updateMessageId, replyTo });
|
|
2118
2221
|
} catch (error) {
|
|
2119
2222
|
await this.#sendFailure(chatId, error, {
|
|
2120
2223
|
logLabel: 'workspace switch',
|
|
2224
|
+
replyTo,
|
|
2121
2225
|
userMessage: t('切换失败:{message}', { message: safeErrorText(error) }),
|
|
2122
2226
|
});
|
|
2123
2227
|
}
|
|
@@ -2144,6 +2248,7 @@ export class FeishuHarnessBridge {
|
|
|
2144
2248
|
|
|
2145
2249
|
async #sendCard(chatId, cardJson, options = {}) {
|
|
2146
2250
|
const updateMessageId = nonEmptyString(options.updateMessageId);
|
|
2251
|
+
const replyTo = nonEmptyString(options.replyTo);
|
|
2147
2252
|
|
|
2148
2253
|
if (updateMessageId) {
|
|
2149
2254
|
try {
|
|
@@ -2161,6 +2266,28 @@ export class FeishuHarnessBridge {
|
|
|
2161
2266
|
}
|
|
2162
2267
|
}
|
|
2163
2268
|
|
|
2269
|
+
// A brand-new card triggered by an inbound message is delivered as a
|
|
2270
|
+
// threaded reply so it lands inside the same Feishu topic. Falls back to
|
|
2271
|
+
// a plain chat message when the referenced message is gone.
|
|
2272
|
+
const content = cardJson;
|
|
2273
|
+
if (replyTo) {
|
|
2274
|
+
try {
|
|
2275
|
+
const response = await this.#client.im.v1.message.reply({
|
|
2276
|
+
path: { message_id: replyTo },
|
|
2277
|
+
data: { msg_type: 'interactive', content },
|
|
2278
|
+
});
|
|
2279
|
+
if (response?.code && response.code !== 0) {
|
|
2280
|
+
throw new Error(`Feishu card reply failed: ${response.msg || response.code}`);
|
|
2281
|
+
}
|
|
2282
|
+
const repliedMessageId = nonEmptyString(response?.data?.message_id);
|
|
2283
|
+
if (repliedMessageId) {
|
|
2284
|
+
this.#rememberCardRoute(repliedMessageId, chatId, options);
|
|
2285
|
+
return repliedMessageId;
|
|
2286
|
+
}
|
|
2287
|
+
} catch (error) {
|
|
2288
|
+
this.#logger.warn?.('[dsh-feishu] threaded card reply failed; sending a plain card:', error?.message ?? String(error));
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2164
2291
|
const response = await this.#client.im.v1.message.create({
|
|
2165
2292
|
params: { receive_id_type: 'chat_id' },
|
|
2166
2293
|
data: { receive_id: chatId, msg_type: 'interactive', content: cardJson },
|
|
@@ -2173,7 +2300,7 @@ export class FeishuHarnessBridge {
|
|
|
2173
2300
|
return messageId;
|
|
2174
2301
|
}
|
|
2175
2302
|
|
|
2176
|
-
async #sendMenuCard(key, chatId, { updateMessageId = null } = {}) {
|
|
2303
|
+
async #sendMenuCard(key, chatId, { updateMessageId = null, replyTo = null } = {}) {
|
|
2177
2304
|
let currentSessionId = null;
|
|
2178
2305
|
let directSessionTitle = null;
|
|
2179
2306
|
try {
|
|
@@ -2268,7 +2395,7 @@ export class FeishuHarnessBridge {
|
|
|
2268
2395
|
currentSession: currentSessionId ? { id: currentSessionId, title: currentSessionTitle } : null,
|
|
2269
2396
|
sessions, archiveVisible, presetCatalog, modelCatalog,
|
|
2270
2397
|
}),
|
|
2271
|
-
{ key, updateMessageId },
|
|
2398
|
+
{ key, updateMessageId, replyTo },
|
|
2272
2399
|
);
|
|
2273
2400
|
}
|
|
2274
2401
|
|
|
@@ -2318,7 +2445,7 @@ export class FeishuHarnessBridge {
|
|
|
2318
2445
|
catalog._currentId = settings.agentPreset;
|
|
2319
2446
|
await this.#sendCard(chatId, presetCard(catalog), { key, updateMessageId });
|
|
2320
2447
|
} catch (error) {
|
|
2321
|
-
await this.#sendFailure(chatId, error, { logLabel: 'preset card' });
|
|
2448
|
+
await this.#sendFailure(chatId, error, { logLabel: 'preset card', replyTo: updateMessageId });
|
|
2322
2449
|
}
|
|
2323
2450
|
}
|
|
2324
2451
|
|
|
@@ -2343,14 +2470,14 @@ export class FeishuHarnessBridge {
|
|
|
2343
2470
|
}
|
|
2344
2471
|
await this.#sendCard(chatId, modelCard(catalog), { key, updateMessageId });
|
|
2345
2472
|
} catch (error) {
|
|
2346
|
-
await this.#sendFailure(chatId, error, { logLabel: 'model card' });
|
|
2473
|
+
await this.#sendFailure(chatId, error, { logLabel: 'model card', replyTo: updateMessageId });
|
|
2347
2474
|
}
|
|
2348
2475
|
}
|
|
2349
2476
|
|
|
2350
2477
|
/**
|
|
2351
2478
|
* Gather system status and show the status card.
|
|
2352
2479
|
*/
|
|
2353
|
-
async #showStatusText(key, chatId) {
|
|
2480
|
+
async #showStatusText(key, chatId, replyTo = null) {
|
|
2354
2481
|
try {
|
|
2355
2482
|
await this.#harness.ensureRunning({ signal: this.#signal });
|
|
2356
2483
|
const lines = [t('连接正常')];
|
|
@@ -2369,9 +2496,9 @@ export class FeishuHarnessBridge {
|
|
|
2369
2496
|
: (settings.agentPreset || t('跟随默认')),
|
|
2370
2497
|
}));
|
|
2371
2498
|
}
|
|
2372
|
-
await this.#send(chatId, lines.join('\n'));
|
|
2499
|
+
await this.#send(chatId, lines.join('\n'), { replyTo });
|
|
2373
2500
|
} catch (error) {
|
|
2374
|
-
await this.#sendFailure(chatId, error, { logLabel: 'status text' });
|
|
2501
|
+
await this.#sendFailure(chatId, error, { logLabel: 'status text', replyTo });
|
|
2375
2502
|
}
|
|
2376
2503
|
}
|
|
2377
2504
|
|
|
@@ -2423,7 +2550,7 @@ export class FeishuHarnessBridge {
|
|
|
2423
2550
|
|
|
2424
2551
|
await this.#sendCard(chatId, statusCard(info), { key, updateMessageId });
|
|
2425
2552
|
} catch (error) {
|
|
2426
|
-
await this.#sendFailure(chatId, error, { logLabel: 'status card' });
|
|
2553
|
+
await this.#sendFailure(chatId, error, { logLabel: 'status card', replyTo: updateMessageId });
|
|
2427
2554
|
}
|
|
2428
2555
|
}
|
|
2429
2556
|
|
|
@@ -2441,21 +2568,21 @@ export class FeishuHarnessBridge {
|
|
|
2441
2568
|
/**
|
|
2442
2569
|
* Run the /compact command and show the result.
|
|
2443
2570
|
*/
|
|
2444
|
-
async #handleCompact(key, chatId) {
|
|
2571
|
+
async #handleCompact(key, chatId, replyTo = null) {
|
|
2445
2572
|
try {
|
|
2446
2573
|
const result = await runCompactCommand(
|
|
2447
2574
|
'/compact', this.#harness, this.#state, key, { signal: this.#signal },
|
|
2448
2575
|
);
|
|
2449
|
-
await this.#send(chatId, result?.message || t('上下文压缩失败。'));
|
|
2576
|
+
await this.#send(chatId, result?.message || t('上下文压缩失败。'), { replyTo });
|
|
2450
2577
|
} catch (error) {
|
|
2451
|
-
await this.#sendFailure(chatId, error, { logLabel: 'compact' });
|
|
2578
|
+
await this.#sendFailure(chatId, error, { logLabel: 'compact', replyTo });
|
|
2452
2579
|
}
|
|
2453
2580
|
}
|
|
2454
2581
|
|
|
2455
2582
|
/**
|
|
2456
2583
|
* Stop the running task in the bound session (mirrors `/stop`).
|
|
2457
2584
|
*/
|
|
2458
|
-
async #handleStop(key, chatId) {
|
|
2585
|
+
async #handleStop(key, chatId, replyTo = null) {
|
|
2459
2586
|
try {
|
|
2460
2587
|
const result = await runControlCommand(
|
|
2461
2588
|
'/stop', this.#harness, this.#state, key, {
|
|
@@ -2469,9 +2596,9 @@ export class FeishuHarnessBridge {
|
|
|
2469
2596
|
this.#approvals.closeRoute(key),
|
|
2470
2597
|
]);
|
|
2471
2598
|
}
|
|
2472
|
-
await this.#send(chatId, result?.message || t('/stop 执行完成。'));
|
|
2599
|
+
await this.#send(chatId, result?.message || t('/stop 执行完成。'), { replyTo });
|
|
2473
2600
|
} catch (error) {
|
|
2474
|
-
await this.#sendFailure(chatId, error, { logLabel: 'stop' });
|
|
2601
|
+
await this.#sendFailure(chatId, error, { logLabel: 'stop', replyTo });
|
|
2475
2602
|
}
|
|
2476
2603
|
}
|
|
2477
2604
|
|
|
@@ -2496,26 +2623,26 @@ export class FeishuHarnessBridge {
|
|
|
2496
2623
|
control: { owner: this, key },
|
|
2497
2624
|
},
|
|
2498
2625
|
);
|
|
2499
|
-
await this.#send(chatId, result?.message || t('已提交补充指令。'));
|
|
2626
|
+
await this.#send(chatId, result?.message || t('已提交补充指令。'), { replyTo: entry.messageId ?? null });
|
|
2500
2627
|
}
|
|
2501
2628
|
|
|
2502
2629
|
/**
|
|
2503
2630
|
* Reset the preset to follow the Host default.
|
|
2504
2631
|
*/
|
|
2505
|
-
async #handlePresetDefault(key, chatId, { updateMessageId = null } = {}) {
|
|
2632
|
+
async #handlePresetDefault(key, chatId, { updateMessageId = null, replyTo = null } = {}) {
|
|
2506
2633
|
try {
|
|
2507
2634
|
const result = await runPresetCommand(
|
|
2508
2635
|
'/preset --default', this.#harness, this.#state, key, { signal: this.#signal },
|
|
2509
2636
|
);
|
|
2510
2637
|
for (const reply of result?.messages ?? [result?.message]) {
|
|
2511
|
-
if (reply) await this.#send(chatId, reply);
|
|
2638
|
+
if (reply) await this.#send(chatId, reply, { replyTo });
|
|
2512
2639
|
}
|
|
2513
2640
|
} catch (error) {
|
|
2514
|
-
await this.#sendFailure(chatId, error, { logLabel: 'preset reset' });
|
|
2641
|
+
await this.#sendFailure(chatId, error, { logLabel: 'preset reset', replyTo: updateMessageId });
|
|
2515
2642
|
return;
|
|
2516
2643
|
}
|
|
2517
2644
|
try {
|
|
2518
|
-
await this.#sendMenuCard(key, chatId, { updateMessageId });
|
|
2645
|
+
await this.#sendMenuCard(key, chatId, { updateMessageId, replyTo });
|
|
2519
2646
|
} catch (error) {
|
|
2520
2647
|
this.#logger.warn?.('[dsh-feishu] menu refresh failed after preset reset:', error.message);
|
|
2521
2648
|
}
|
|
@@ -2524,21 +2651,21 @@ export class FeishuHarnessBridge {
|
|
|
2524
2651
|
/**
|
|
2525
2652
|
* Handle preset selection from the preset dropdown.
|
|
2526
2653
|
*/
|
|
2527
|
-
async #handlePresetSelect(key, chatId, presetId, { updateMessageId = null } = {}) {
|
|
2654
|
+
async #handlePresetSelect(key, chatId, presetId, { updateMessageId = null, replyTo = null } = {}) {
|
|
2528
2655
|
try {
|
|
2529
2656
|
const selector = /^\d+$/u.test(presetId) ? `id:${presetId}` : presetId;
|
|
2530
2657
|
const result = await runPresetCommand(
|
|
2531
2658
|
`/preset ${selector}`, this.#harness, this.#state, key, { signal: this.#signal },
|
|
2532
2659
|
);
|
|
2533
2660
|
for (const reply of result?.messages ?? [result?.message]) {
|
|
2534
|
-
if (reply) await this.#send(chatId, reply);
|
|
2661
|
+
if (reply) await this.#send(chatId, reply, { replyTo });
|
|
2535
2662
|
}
|
|
2536
2663
|
} catch (error) {
|
|
2537
|
-
await this.#sendFailure(chatId, error, { logLabel: 'preset selection' });
|
|
2664
|
+
await this.#sendFailure(chatId, error, { logLabel: 'preset selection', replyTo: updateMessageId });
|
|
2538
2665
|
return;
|
|
2539
2666
|
}
|
|
2540
2667
|
try {
|
|
2541
|
-
await this.#sendMenuCard(key, chatId, { updateMessageId });
|
|
2668
|
+
await this.#sendMenuCard(key, chatId, { updateMessageId, replyTo });
|
|
2542
2669
|
} catch (error) {
|
|
2543
2670
|
this.#logger.warn?.('[dsh-feishu] menu refresh failed after preset select:', error.message);
|
|
2544
2671
|
}
|
|
@@ -2553,7 +2680,7 @@ export class FeishuHarnessBridge {
|
|
|
2553
2680
|
* catalog validation, busy checks, pending-interaction checks and the
|
|
2554
2681
|
* session binding lock stay in one place.
|
|
2555
2682
|
*/
|
|
2556
|
-
async #handleModelSelect(key, chatId, modelId, { updateMessageId = null } = {}) {
|
|
2683
|
+
async #handleModelSelect(key, chatId, modelId, { updateMessageId = null, replyTo = null } = {}) {
|
|
2557
2684
|
try {
|
|
2558
2685
|
const result = await runModelCommand(
|
|
2559
2686
|
`/model ${modelId}`, this.#harness, this.#state, key, {
|
|
@@ -2563,14 +2690,14 @@ export class FeishuHarnessBridge {
|
|
|
2563
2690
|
},
|
|
2564
2691
|
);
|
|
2565
2692
|
for (const reply of result?.messages ?? [result?.message]) {
|
|
2566
|
-
if (reply) await this.#send(chatId, reply);
|
|
2693
|
+
if (reply) await this.#send(chatId, reply, { replyTo });
|
|
2567
2694
|
}
|
|
2568
2695
|
} catch (error) {
|
|
2569
|
-
await this.#sendFailure(chatId, error, { logLabel: 'model selection' });
|
|
2696
|
+
await this.#sendFailure(chatId, error, { logLabel: 'model selection', replyTo: updateMessageId });
|
|
2570
2697
|
return;
|
|
2571
2698
|
}
|
|
2572
2699
|
try {
|
|
2573
|
-
await this.#sendMenuCard(key, chatId, { updateMessageId });
|
|
2700
|
+
await this.#sendMenuCard(key, chatId, { updateMessageId, replyTo });
|
|
2574
2701
|
} catch (error) {
|
|
2575
2702
|
this.#logger.warn?.('[dsh-feishu] menu refresh failed after model select:', error.message);
|
|
2576
2703
|
}
|
|
@@ -2787,11 +2914,12 @@ export class FeishuHarnessBridge {
|
|
|
2787
2914
|
notify = true,
|
|
2788
2915
|
validatedTarget = null,
|
|
2789
2916
|
workspaceHint = null,
|
|
2917
|
+
replyTo = null,
|
|
2790
2918
|
} = {}) {
|
|
2791
2919
|
const watchRequestedAt = Date.now();
|
|
2792
2920
|
const reply = async (message) => {
|
|
2793
2921
|
if (!notify) return;
|
|
2794
|
-
await this.#send(chatId, message).catch((error) => {
|
|
2922
|
+
await this.#send(chatId, message, { replyTo }).catch((error) => {
|
|
2795
2923
|
this.#logger.warn?.('[dsh-feishu] watch notification failed:', error.message);
|
|
2796
2924
|
});
|
|
2797
2925
|
};
|
|
@@ -2840,6 +2968,13 @@ export class FeishuHarnessBridge {
|
|
|
2840
2968
|
chatId,
|
|
2841
2969
|
lastSeq,
|
|
2842
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
|
+
: {}),
|
|
2843
2978
|
});
|
|
2844
2979
|
} catch (error) {
|
|
2845
2980
|
await reply(t('关注失败:{message}', { message: safeErrorText(error) }));
|
|
@@ -2854,10 +2989,10 @@ export class FeishuHarnessBridge {
|
|
|
2854
2989
|
return { ok: true, changed: !existingEntry, entry: this.#state.watchEntry?.(key, resolved.sessionId) };
|
|
2855
2990
|
}
|
|
2856
2991
|
|
|
2857
|
-
async #runUnwatch(key, chatId, target, { notify = true } = {}) {
|
|
2992
|
+
async #runUnwatch(key, chatId, target, { notify = true, replyTo = null } = {}) {
|
|
2858
2993
|
const reply = async (message) => {
|
|
2859
2994
|
if (!notify) return;
|
|
2860
|
-
await this.#send(chatId, message).catch((error) => {
|
|
2995
|
+
await this.#send(chatId, message, { replyTo }).catch((error) => {
|
|
2861
2996
|
this.#logger.warn?.('[dsh-feishu] unwatch notification failed:', error.message);
|
|
2862
2997
|
});
|
|
2863
2998
|
};
|
|
@@ -2883,7 +3018,7 @@ export class FeishuHarnessBridge {
|
|
|
2883
3018
|
return { ok: true, changed: true, entry };
|
|
2884
3019
|
}
|
|
2885
3020
|
|
|
2886
|
-
async #showWatchList(key, chatId, { updateMessageId = null } = {}) {
|
|
3021
|
+
async #showWatchList(key, chatId, { updateMessageId = null, replyTo = null } = {}) {
|
|
2887
3022
|
const entries = this.#state.watchEntries?.(key) ?? [];
|
|
2888
3023
|
// 收集可选会话(用于「添加关注」多选下拉);失败则传空数组 → 只渲染移除/列表。
|
|
2889
3024
|
let availableSessions = [];
|
|
@@ -2913,6 +3048,7 @@ export class FeishuHarnessBridge {
|
|
|
2913
3048
|
{
|
|
2914
3049
|
key,
|
|
2915
3050
|
updateMessageId,
|
|
3051
|
+
replyTo,
|
|
2916
3052
|
sessionWorkspace: currentWorkspace,
|
|
2917
3053
|
},
|
|
2918
3054
|
);
|
|
@@ -2964,7 +3100,7 @@ export class FeishuHarnessBridge {
|
|
|
2964
3100
|
await this.#sendCard(
|
|
2965
3101
|
entry.chatId,
|
|
2966
3102
|
completionCard(sessionId, entry.title, reason),
|
|
2967
|
-
{ key },
|
|
3103
|
+
{ key, replyTo: entry.replyToMessageId ?? null },
|
|
2968
3104
|
);
|
|
2969
3105
|
const current = this.#state.watchEntry?.(key, sessionId);
|
|
2970
3106
|
if (!current
|
|
@@ -3068,17 +3204,18 @@ export class FeishuHarnessBridge {
|
|
|
3068
3204
|
actor: senderOpenId(event),
|
|
3069
3205
|
chatId: event.message.chat_id,
|
|
3070
3206
|
requiresMention: event.message.chat_type !== 'p2p',
|
|
3207
|
+
replyToMessageId: event.message.message_id,
|
|
3071
3208
|
}),
|
|
3072
3209
|
onInteractionResolved: (resolution) => this.#handleInteractionResolved(resolution),
|
|
3073
3210
|
files,
|
|
3074
3211
|
};
|
|
3075
3212
|
}
|
|
3076
3213
|
|
|
3077
|
-
async #sendAnswerText(chatId, answer, { deliveryId, presentation }) {
|
|
3214
|
+
async #sendAnswerText(chatId, answer, { deliveryId, presentation, replyTo = null }) {
|
|
3078
3215
|
const providerMessageIds = [];
|
|
3079
3216
|
for (const chunk of splitText(answer)) {
|
|
3080
3217
|
this.#signal?.throwIfAborted();
|
|
3081
|
-
const messageId = await this.#send(chatId, chunk);
|
|
3218
|
+
const messageId = await this.#send(chatId, chunk, { replyTo });
|
|
3082
3219
|
if (messageId) providerMessageIds.push(messageId);
|
|
3083
3220
|
}
|
|
3084
3221
|
return createDeliveryReceipt({
|
|
@@ -3182,6 +3319,7 @@ export class FeishuHarnessBridge {
|
|
|
3182
3319
|
{
|
|
3183
3320
|
deliveryId: messageId,
|
|
3184
3321
|
presentation: 'feishu-text',
|
|
3322
|
+
replyTo: messageId,
|
|
3185
3323
|
},
|
|
3186
3324
|
);
|
|
3187
3325
|
} catch (error) {
|
|
@@ -3252,6 +3390,7 @@ export class FeishuHarnessBridge {
|
|
|
3252
3390
|
{
|
|
3253
3391
|
deliveryId: messageId,
|
|
3254
3392
|
presentation: 'feishu-text-fallback',
|
|
3393
|
+
replyTo: messageId,
|
|
3255
3394
|
},
|
|
3256
3395
|
);
|
|
3257
3396
|
} catch (fallbackError) {
|
|
@@ -3302,6 +3441,7 @@ export class FeishuHarnessBridge {
|
|
|
3302
3441
|
{
|
|
3303
3442
|
deliveryId: messageId,
|
|
3304
3443
|
presentation: 'feishu-text-fallback',
|
|
3444
|
+
replyTo: messageId,
|
|
3305
3445
|
},
|
|
3306
3446
|
);
|
|
3307
3447
|
} catch (fallbackError) {
|
|
@@ -3368,11 +3508,11 @@ export class FeishuHarnessBridge {
|
|
|
3368
3508
|
const pending = this.#pendingInteractions.get(key);
|
|
3369
3509
|
if (!pending || pending !== expected || pending.submitting) {
|
|
3370
3510
|
if (this.#isResolvedQuestionReply(event, key)) {
|
|
3371
|
-
await this.#send(event.message.chat_id, INTERACTION_RESOLVED_TEXT()).catch(() => undefined);
|
|
3511
|
+
await this.#send(event.message.chat_id, INTERACTION_RESOLVED_TEXT(), { replyTo: event.message.message_id }).catch(() => undefined);
|
|
3372
3512
|
return;
|
|
3373
3513
|
}
|
|
3374
3514
|
if (claimed && (!pending || pending !== expected)) {
|
|
3375
|
-
await this.#send(event.message.chat_id, INTERACTION_RESOLVED_TEXT());
|
|
3515
|
+
await this.#send(event.message.chat_id, INTERACTION_RESOLVED_TEXT(), { replyTo: event.message.message_id });
|
|
3376
3516
|
return;
|
|
3377
3517
|
}
|
|
3378
3518
|
return this.#enqueueMessage(event, messageId, key, processingReaction, {
|
|
@@ -3430,7 +3570,7 @@ export class FeishuHarnessBridge {
|
|
|
3430
3570
|
if (error?.code === 'interaction-not-pending') {
|
|
3431
3571
|
this.#rememberResolvedInteraction(key, pending);
|
|
3432
3572
|
this.#clearPendingInteraction(key, pending.interactionId);
|
|
3433
|
-
await this.#send(event.message.chat_id, INTERACTION_RESOLVED_TEXT()).catch(() => undefined);
|
|
3573
|
+
await this.#send(event.message.chat_id, INTERACTION_RESOLVED_TEXT(), { replyTo: event.message.message_id }).catch(() => undefined);
|
|
3434
3574
|
return;
|
|
3435
3575
|
}
|
|
3436
3576
|
pending.submitting = false;
|
|
@@ -3448,12 +3588,13 @@ export class FeishuHarnessBridge {
|
|
|
3448
3588
|
actor,
|
|
3449
3589
|
chatId,
|
|
3450
3590
|
requiresMention,
|
|
3591
|
+
replyToMessageId,
|
|
3451
3592
|
}) {
|
|
3452
3593
|
if (await this.#approvals.handleRequested(interaction, {
|
|
3453
3594
|
key,
|
|
3454
3595
|
actor,
|
|
3455
3596
|
requiresMention,
|
|
3456
|
-
send: (text) => this.#send(chatId, text),
|
|
3597
|
+
send: (text) => this.#send(chatId, text, { replyTo: replyToMessageId }),
|
|
3457
3598
|
})) return;
|
|
3458
3599
|
|
|
3459
3600
|
// Approval requests return above; the existing question state machine stays unchanged.
|
|
@@ -3484,6 +3625,7 @@ export class FeishuHarnessBridge {
|
|
|
3484
3625
|
await this.#send(
|
|
3485
3626
|
chatId,
|
|
3486
3627
|
t('检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。'),
|
|
3628
|
+
{ replyTo: replyToMessageId },
|
|
3487
3629
|
).catch(() => undefined);
|
|
3488
3630
|
return;
|
|
3489
3631
|
}
|
|
@@ -3519,6 +3661,7 @@ export class FeishuHarnessBridge {
|
|
|
3519
3661
|
answers: [],
|
|
3520
3662
|
index: 0,
|
|
3521
3663
|
chatId,
|
|
3664
|
+
replyToMessageId,
|
|
3522
3665
|
queue: null,
|
|
3523
3666
|
claimedReplyMessageId: null,
|
|
3524
3667
|
submitting: false,
|
|
@@ -3553,6 +3696,9 @@ export class FeishuHarnessBridge {
|
|
|
3553
3696
|
pending.questions.length,
|
|
3554
3697
|
{ requiresMention: pending.requiresMention },
|
|
3555
3698
|
),
|
|
3699
|
+
// Reply to the message that started the turn so the question lands in
|
|
3700
|
+
// the same Feishu thread/topic instead of the group's default area.
|
|
3701
|
+
{ replyTo: pending.replyToMessageId },
|
|
3556
3702
|
);
|
|
3557
3703
|
if (messageId) {
|
|
3558
3704
|
pending.questionMessageIds.add(messageId);
|
|
@@ -3585,7 +3731,7 @@ export class FeishuHarnessBridge {
|
|
|
3585
3731
|
await this.#state.markSeen(messageId);
|
|
3586
3732
|
this.#status.lastMessageAt = new Date().toISOString();
|
|
3587
3733
|
this.#status.messagesReceived += 1;
|
|
3588
|
-
await this.#send(event.message.chat_id, INTERACTION_RESOLVED_TEXT()).catch(() => undefined);
|
|
3734
|
+
await this.#send(event.message.chat_id, INTERACTION_RESOLVED_TEXT(), { replyTo: event.message.message_id }).catch(() => undefined);
|
|
3589
3735
|
}
|
|
3590
3736
|
|
|
3591
3737
|
#takePendingInteraction(key, interactionId) {
|
|
@@ -3651,13 +3797,33 @@ export class FeishuHarnessBridge {
|
|
|
3651
3797
|
else processingReaction.success();
|
|
3652
3798
|
}
|
|
3653
3799
|
|
|
3654
|
-
async #send(chatId, text) {
|
|
3800
|
+
async #send(chatId, text, { replyTo } = {}) {
|
|
3801
|
+
const content = JSON.stringify({ text });
|
|
3802
|
+
if (replyTo) {
|
|
3803
|
+
try {
|
|
3804
|
+
const response = await this.#client.im.v1.message.reply({
|
|
3805
|
+
path: { message_id: replyTo },
|
|
3806
|
+
data: { msg_type: 'text', content },
|
|
3807
|
+
});
|
|
3808
|
+
if (response?.code && response.code !== 0) {
|
|
3809
|
+
throw new Error(`Feishu reply failed: ${response.msg || response.code}`);
|
|
3810
|
+
}
|
|
3811
|
+
return nonEmptyString(response?.data?.message_id);
|
|
3812
|
+
} catch (error) {
|
|
3813
|
+
// The referenced message may be gone (recalled/deleted); keep the
|
|
3814
|
+
// delivery promise by falling back to a plain chat message.
|
|
3815
|
+
this.#logger.warn?.(
|
|
3816
|
+
'[dsh-feishu] threaded reply failed; falling back to a plain message:',
|
|
3817
|
+
error?.message ?? String(error),
|
|
3818
|
+
);
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3655
3821
|
const response = await this.#client.im.v1.message.create({
|
|
3656
3822
|
params: { receive_id_type: 'chat_id' },
|
|
3657
3823
|
data: {
|
|
3658
3824
|
receive_id: chatId,
|
|
3659
3825
|
msg_type: 'text',
|
|
3660
|
-
content
|
|
3826
|
+
content,
|
|
3661
3827
|
},
|
|
3662
3828
|
});
|
|
3663
3829
|
if (response?.code && response.code !== 0) {
|