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